Signed-off-by: dzdidi <deniszalessky@gmail.com>
This commit is contained in:
dzdidi
2024-01-25 17:22:17 +00:00
parent 1837a4bae8
commit 9d66b66221
11 changed files with 376 additions and 50 deletions

20
src/acl/index.js Normal file
View File

@@ -0,0 +1,20 @@
function getId(data) {
if (!process.env.GIT_PEAR_AUTH) return payload
if (process.env.GIT_PEAR_AUTH === 'nip98') {
const nip98 = require('./nip98')
return nip98.getId(data)
}
}
async function getToken(payload) {
if (!process.env.GIT_PEAR_AUTH) return userId
if (process.env.GIT_PEAR_AUTH === 'nip98') {
const nip98 = require('./nip98')
return nip98.getToken(payload)
}
}
module.exports = {
getId,
getToken
}

49
src/acl/nip98.js Normal file
View File

@@ -0,0 +1,49 @@
const { nip98, nip19, finalizeEvent } = require('nostr-tools')
async function getToken({ url, method, data }) {
const { data: sK } = nip19.decode(process.env.GIT_PEAR_AUTH_NSEC)
return nip98.getToken(
url,
method,
(e) => finalizeEvent(e, sK),
false,
data
)
}
// FIXME
async function getId({ payload, url, method, data }) {
const event = JSON.parse(Buffer.from(payload, 'base64').toString())
const isValid = await nip98.validateEvent(event, url, method, data)
if (!isValid) throw new Error('Invalid event')
return {
...event,
userId: nip19.npubEncode(event.pubkey)
}
}
module.exports = {
getId,
getToken
}
// ;(async () => {
// const repo = 'gitpear'
// const url = `pear://d1672d338b8e24223cd0dc6c6b5e04ebabf091fc2b470204abdb98fa5fc59072/${repo}`
// const commit = '1837a4bae8497f71fb8f01305c3ace1e3dedcdba'
// const method = 'push'
// const branch = 'test'
// const data = `${branch}#${commit}`
//
// let payload
// let npub
//
// payload = await getToken({ url, method, data })
// npub = await getId({ payload, url, method, data })
//
// payload = await getToken({url, method: 'get-repos'})
// npub = await getId({ payload, url, method: 'get-repos' })
//
// payload = await getToken({url, method: 'get-refs', data: { repo }})
// npub = await getId({ payload, url, method: 'get-refs', data: { repo }})
// })()

View File

@@ -32,6 +32,8 @@ program
const name = fullPath.split(path.sep).pop()
if ((home.isInitialized(name))) {
console.error(`${name} is already initialized`)
await git.addRemote(name)
console.log(`Added git remote for "${name}" as "pear"`)
process.exit(1)
}

View File

@@ -11,6 +11,7 @@ const crypto = require('hypercore-crypto')
const git = require('./git.js')
const home = require('./home')
const acl = require('./acl')
const fs = require('fs')
@@ -39,7 +40,12 @@ swarm.on('connection', async (socket) => {
store.replicate(socket)
const rpc = new ProtomuxRPC(socket)
const reposRes = await rpc.request('get-repos')
let payload = { body: { url, method: 'get-repos' } }
if (process.env.GIT_PEAR_AUTH) {
payload.header = await acl.getToken(payload.body)
}
const reposRes = await rpc.request('get-repos', Buffer.from(JSON.stringify(payload)))
const repositories = JSON.parse(reposRes.toString())
if (!repositories) {
console.error('Failed to retrieve repositories')
@@ -65,12 +71,21 @@ swarm.on('connection', async (socket) => {
await drive.core.update({ wait: true })
const refsRes = await rpc.request('get-refs', Buffer.from(repoName))
// TODO: ACL
payload = { body: { url, method: 'get-refs', data: repoName }}
if (process.env.GIT_PEAR_AUTH) {
payload.header = await acl.getToken(payload.body)
}
const refsRes = await rpc.request('get-refs', Buffer.from(JSON.stringify(payload)))
await talkToGit(JSON.parse(refsRes.toString()), drive, repoName, rpc)
let commit
try {
commit = await git.getCommit()
} catch (e) { }
await talkToGit(JSON.parse(refsRes.toString()), drive, repoName, rpc, commit)
})
async function talkToGit (refs, drive, repoName, rpc) {
async function talkToGit (refs, drive, repoName, rpc, commit) {
process.stdin.setEncoding('utf8')
const didFetch = false
process.stdin.on('readable', async function () {
@@ -92,22 +107,30 @@ async function talkToGit (refs, drive, repoName, rpc) {
dst = dst.replace('refs/heads/', '').replace('\n\n', '')
let command
let method
if (isDelete) {
command = 'd-branch'
method = 'd-branch'
} else if (isForce) {
console.warn('To', url)
await git.push(src, isForce)
src = src.replace('+', '')
command = 'f-push'
method = 'f-push'
} else {
console.warn('To', url)
await git.push(src)
command = 'push'
method = 'push'
}
const publicKey = home.readPk()
const res = await rpc.request(command, Buffer.from(`${publicKey}/${repoName}:${dst}`))
let payload = { body: {
url: `pear://${publicKey}/${repoName}`,
data: `${dst}#${commit}`,
method
} }
if (process.env.GIT_PEAR_AUTH) {
payload.header = await acl.getToken(payload.body)
}
const res = await rpc.request(method, Buffer.from(JSON.stringify(payload)))
process.stdout.write('\n\n')
process.exit(0)

View File

@@ -1,6 +1,25 @@
const { getCodePath } = require('./home')
const { spawn } = require('child_process')
async function getCommit () {
return await new Promise((resolve, reject) => {
const process = spawn('git', ['rev-parse', 'HEAD'])
let outBuffer = Buffer.from('')
process.stdout.on('data', data => {
outBuffer = Buffer.concat([outBuffer, data])
})
let errBuffer = Buffer.from('')
process.stderr.on('err', data => {
errBuffer = Buffer.concat([errBuffer, data])
})
process.on('close', code => {
return code === 0 ? resolve(outBuffer.toString().replace('\n', '')) : reject(errBuffer)
})
})
}
async function lsPromise (url) {
const ls = spawn('git', ['ls-remote', url])
const res = {}
@@ -182,4 +201,4 @@ async function unpackStream (packStream) {
})
}
module.exports = { lsPromise, uploadPack, unpackFile, unpackStream, createBareRepo, addRemote, push }
module.exports = { lsPromise, uploadPack, unpackFile, unpackStream, createBareRepo, addRemote, push, getCommit }

View File

@@ -14,6 +14,10 @@ function shareAppFolder (name) {
fs.openSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'w')
}
function shareWith (userId, branch = '*', permissions = 'rw') {
fs.appendFileSync(`${APP_HOME}/.git-daemon-export-ok`, `${userId}:${branch}:${permissions}\n`)
}
function unshareAppFolder (name) {
fs.unlinkSync(`${APP_HOME}/${name}/.git-daemon-export-ok`)
}
@@ -119,5 +123,6 @@ module.exports = {
storeDaemonPid,
getDaemonPid,
isDaemonRunning,
removeDaemonPid
removeDaemonPid,
shareWith,
}

0
src/rpc-request.js Normal file
View File

View File

@@ -1,6 +1,7 @@
const ProtomuxRPC = require('protomux-rpc')
const { spawn } = require('child_process')
const home = require('./home')
const acl = require('./acl')
module.exports = class RPC {
constructor (announcedRefs, repositories, drives) {
@@ -19,92 +20,98 @@ module.exports = class RPC {
// which can in turn be stored in a .git-daemon-export-ok file
/* -- PULL HANDLERS -- */
rpc.respond('get-repos', req => this.getReposHandler(req))
rpc.respond('get-refs', async req => await this.getRefsHandler(req))
rpc.respond('get-repos', async req => await this.getReposHandler(req))
rpc.respond('get-refs', async req => await this.getRefsHandler(req))
/* -- PUSH HANDLERS -- */
rpc.respond('push', async req => await this.pushHandler(req))
rpc.respond('f-push', async req => await this.forcePushHandler(req))
rpc.respond('push', async req => await this.pushHandler(req))
rpc.respond('f-push', async req => await this.forcePushHandler(req))
rpc.respond('d-branch', async req => await this.deleteBranchHandler(req))
this.connections[peerInfo.publicKey] = rpc
}
getReposHandler (_req) {
async getReposHandler (req) {
const { branch, url } = await this.parseReq(req)
const res = {}
for (const repo in this.repositories) {
res[repo] = this.drives[repo].key.toString('hex')
for (const repoName in this.repositories) {
// TODO: add only public repos and those which are shared with the peer
// Alternatively return only requested repo
res[repoName] = this.drives[repoName].key.toString('hex')
}
return Buffer.from(JSON.stringify(res))
}
getRefsHandler (req) {
const res = this.repositories[req.toString()]
async getRefsHandler (req) {
const { repoName, branch, url } = await this.parseReq(req)
const res = this.repositories[repoName]
return Buffer.from(JSON.stringify(res))
}
async pushHandler (req) {
const { url, repo, key, branch } = this.parsePushCommand(req)
// TODO: check ACL
const { url, repoName, branch } = await this.parseReq(req)
return await new Promise((resolve, reject) => {
const process = spawn('git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: home.getCodePath(repo) } })
const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) }
const child = spawn('git', ['fetch', url, `${branch}:${branch}`], { env })
let errBuffer = Buffer.from('')
process.stderr.on('data', data => {
child.stderr.on('data', data => {
errBuffer = Buffer.concat([errBuffer, data])
})
process.on('close', code => {
child.on('close', code => {
return code === 0 ? resolve(errBuffer) : reject(errBuffer)
})
})
}
async forcePushHandler (req) {
const { url, repo, key, branch } = this.parsePushCommand(req)
// TODO: check ACL
const { url, repoName, branch } = await this.parseReq(req)
return await new Promise((resolve, reject) => {
const process = spawn('git', ['fetch', url, `${branch}:${branch}`, '--force'], { env: { GIT_DIR: home.getCodePath(repo) } })
const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) }
const child = spawn('git', ['fetch', url, `${branch}:${branch}`, '--force'], { env })
let errBuffer = Buffer.from('')
process.stderr.on('data', data => {
child.stderr.on('data', data => {
errBuffer = Buffer.concat([errBuffer, data])
})
process.on('close', code => {
child.on('close', code => {
return code === 0 ? resolve(errBuffer) : reject(errBuffer)
})
})
}
async deleteBranchHandler (req) {
const { url, repo, key, branch } = this.parsePushCommand(req)
// TODO: check ACL
const { url, repoName, branch } = await this.parseReq(req)
return await new Promise((resolve, reject) => {
const process = spawn('git', ['branch', '-D', branch], { env: { GIT_DIR: home.getCodePath(repo) } })
const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) }
const child = spawn('git', ['branch', '-D', branch], { env })
let errBuffer = Buffer.from('')
process.stderr.on('data', data => {
child.stderr.on('data', data => {
errBuffer = Buffer.concat([errBuffer, data])
})
process.on('close', code => {
child.on('close', code => {
return code === 0 ? resolve(errBuffer) : reject(errBuffer)
})
})
}
parsePushCommand(req) {
const [url, branch] = req.toString().split(':')
const [key, repo] = url.split('/')
async parseReq(req) {
let payload
let request = JSON.parse(req.toString())
if (process.env.GIT_PEAR_AUTH) {
payload = await acl.getId({
...request.body,
payload: request.header
})
}
return {
url: `pear://${url}`,
repo,
key,
branch
repoName: request.body.url?.split('/')?.pop(),
branch: request.body.data?.split('#')[0],
url: request.body.url
}
}
loadACL(repoName) {
// TODO: read contact of .git-daemon-export-ok
// find key and its permissions
}
}