From 391542ff6dc1942eae31276dfa9468b91191a229 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Mon, 15 Jan 2024 23:45:45 +0000 Subject: [PATCH 01/60] pretend to handle push Signed-off-by: dzdidi --- src/git-remote-pear.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index 25a57a4..f029667 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -70,8 +70,20 @@ async function talkToGit (refs, drive) { process.stdin.on('readable', async function () { const chunk = process.stdin.read() if (chunk === 'capabilities\n') { + process.stdout.write('list\n') // TODO: support push + process.stdout.write('push\n') // TODO: support push process.stdout.write('fetch\n\n') - } else if (chunk === 'list\n') { + } else if (chunk && chunk.search(/^push/) !== -1) { + let [src, dst] = chunk.split(':') + src = src.split(' ')[1] + const isForce = src.startsWith('+') + if (isForce) src = src.slice(1) + + // TODO: write to something + console.warn('src:', src, 'dst:', dst) + process.stdout.write('\n\n') + process.exit(0) + } else if (chunk && chunk.search(/^list/) !== -1) { // list && list for-push Object.keys(refs).forEach(function (branch, i) { process.stdout.write(refs[branch] + ' ' + branch + '\n') }) From 8c5b9e5dddef264cb5467ddc716dddecdb7cd807 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Mon, 15 Jan 2024 23:53:37 +0000 Subject: [PATCH 02/60] disable force push Signed-off-by: dzdidi --- src/git-remote-pear.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index f029667..ce4bd9f 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -77,7 +77,12 @@ async function talkToGit (refs, drive) { let [src, dst] = chunk.split(':') src = src.split(' ')[1] const isForce = src.startsWith('+') - if (isForce) src = src.slice(1) + if (isForce) { + src = src.slice(1) + console.warn('force push is disabled') + process.stdout.write('\n\n') + process.exit(0) + } // TODO: write to something console.warn('src:', src, 'dst:', dst) From 1781ca2f9957d29a0a24c8a9f5727159f315b7dc Mon Sep 17 00:00:00 2001 From: dzdidi Date: Tue, 16 Jan 2024 09:23:38 +0000 Subject: [PATCH 03/60] identify delete and force flags Signed-off-by: dzdidi --- src/git-remote-pear.js | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index ce4bd9f..2822cea 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -70,22 +70,16 @@ async function talkToGit (refs, drive) { process.stdin.on('readable', async function () { const chunk = process.stdin.read() if (chunk === 'capabilities\n') { - process.stdout.write('list\n') // TODO: support push - process.stdout.write('push\n') // TODO: support push + process.stdout.write('list\n') + process.stdout.write('push\n') process.stdout.write('fetch\n\n') } else if (chunk && chunk.search(/^push/) !== -1) { - let [src, dst] = chunk.split(':') - src = src.split(' ')[1] - const isForce = src.startsWith('+') - if (isForce) { - src = src.slice(1) - console.warn('force push is disabled') - process.stdout.write('\n\n') - process.exit(0) - } + const [_command, path] = chunk.split(' ') + const [src, dst] = path.split(':') + + const isDelete = !src + const isForce = src.startsWith('+') - // TODO: write to something - console.warn('src:', src, 'dst:', dst) process.stdout.write('\n\n') process.exit(0) } else if (chunk && chunk.search(/^list/) !== -1) { // list && list for-push From 46081de3d5b6bfbf80daf8bf11a7d8f72592afb1 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Thu, 18 Jan 2024 11:26:52 +0000 Subject: [PATCH 04/60] TODO comments Signed-off-by: dzdidi --- src/git-remote-pear.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index 2822cea..60e98b2 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -73,13 +73,27 @@ async function talkToGit (refs, drive) { process.stdout.write('list\n') process.stdout.write('push\n') process.stdout.write('fetch\n\n') - } else if (chunk && chunk.search(/^push/) !== -1) { + } else if (chunk && chunk.search(/^push/) !== -1) {j const [_command, path] = chunk.split(' ') const [src, dst] = path.split(':') const isDelete = !src const isForce = src.startsWith('+') + // TODO: options: + // ---- push by pull ---- + // - init for share (daemon, etc) + // - push branch to local remote + // - send rpc command to origin + // Mapping of RPC commands to git commands on origin: + // - normal push - git pull + // - force push - git reset --hard / + // - delete branch - git branch -D / + // + // ---- push by push ---- + // + // + process.stdout.write('\n\n') process.exit(0) } else if (chunk && chunk.search(/^list/) !== -1) { // list && list for-push From 39eebdf62965a22080a2ff56e4f7a30d35397c1d Mon Sep 17 00:00:00 2001 From: dzdidi Date: Mon, 22 Jan 2024 13:11:59 +0000 Subject: [PATCH 05/60] start daemon on push Signed-off-by: dzdidi --- src/git-remote-pear.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index 60e98b2..32ac52e 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -9,6 +9,7 @@ const Hyperdrive = require('hyperdrive') const crypto = require('hypercore-crypto') const git = require('./git.js') +const home = require('./home') const url = process.argv[3] const matches = url.match(/pear:\/\/([a-f0-9]{64})\/(.*)/) @@ -80,6 +81,14 @@ async function talkToGit (refs, drive) { const isDelete = !src const isForce = src.startsWith('+') + if (!home.getDaemonPid()) { + const daemon = spawn('git-peard', opts) + home.storeDaemonPid(daemon.pid) + // TODO: remove in case of error or exit but allow unref + // daemon.on('error', home.removeDaemonPid) + // daemon.on('exit', home.removeDaemonPid) + daemon.unref() + } // TODO: options: // ---- push by pull ---- // - init for share (daemon, etc) From 8dedb6bf17fe7217134322e1737f8c4ed746bbbe Mon Sep 17 00:00:00 2001 From: dzdidi Date: Tue, 23 Jan 2024 17:06:33 +0000 Subject: [PATCH 06/60] more elaborate todos on push capabilities Signed-off-by: dzdidi --- src/git-remote-pear.js | 52 ++++++++++++++++++++++++++---------------- src/home.js | 5 ++++ src/rpc.js | 7 ++++++ 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index 32ac52e..5eb1853 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -11,6 +11,8 @@ const crypto = require('hypercore-crypto') const git = require('./git.js') const home = require('./home') +const fs = require('fs') + const url = process.argv[3] const matches = url.match(/pear:\/\/([a-f0-9]{64})\/(.*)/) @@ -59,10 +61,10 @@ swarm.on('connection', async (socket) => { const refsRes = await rpc.request('get-refs', Buffer.from(repoName)) - await talkToGit(JSON.parse(refsRes.toString()), drive) + await talkToGit(JSON.parse(refsRes.toString()), drive, repoName) }) -async function talkToGit (refs, drive) { +async function talkToGit (refs, drive, repoName) { for (const ref in refs) { console.warn(refs[ref] + '\t' + ref) } @@ -74,34 +76,44 @@ async function talkToGit (refs, drive) { process.stdout.write('list\n') process.stdout.write('push\n') process.stdout.write('fetch\n\n') - } else if (chunk && chunk.search(/^push/) !== -1) {j + } else if (chunk && chunk.search(/^push/) !== -1) { const [_command, path] = chunk.split(' ') const [src, dst] = path.split(':') const isDelete = !src const isForce = src.startsWith('+') - if (!home.getDaemonPid()) { - const daemon = spawn('git-peard', opts) - home.storeDaemonPid(daemon.pid) - // TODO: remove in case of error or exit but allow unref - // daemon.on('error', home.removeDaemonPid) - // daemon.on('exit', home.removeDaemonPid) - daemon.unref() - } - // TODO: options: - // ---- push by pull ---- - // - init for share (daemon, etc) - // - push branch to local remote - // - send rpc command to origin + // XXX: it looks like git is trying to establish network connection first, so if it can not + // reach push destination it hangs everything on capabilities exchange + // TODO: add timeout + + // FOR TESTING RUN SECOND INSTANCE OF GIT-PEARD + // if (!home.isDaemonRunning()) { + // const opts = { + // detached: true, + // stdio: [ 'ignore', home.getOutStream(), home.getErrStream() ] + // } + // const daemon = spawn('git-peard', opts) + // home.storeDaemonPid(daemon.pid) + // // TODO: remove in case of error or exit but allow unref + // // daemon.on('error', home.removeDaemonPid) + // // daemon.on('exit', home.removeDaemonPid) + // daemon.unref() + // } + + console.error('TODO') + // TODO: get repo name (current dir name instead or name from url) + // if (home.isShared(repoName)) { + // git push branch + // } else { + // share repo with current branch + // } + + // - send rpc command to remote (url) // Mapping of RPC commands to git commands on origin: // - normal push - git pull // - force push - git reset --hard / // - delete branch - git branch -D / - // - // ---- push by push ---- - // - // process.stdout.write('\n\n') process.exit(0) diff --git a/src/home.js b/src/home.js index 1db2e5d..921c4bc 100644 --- a/src/home.js +++ b/src/home.js @@ -90,6 +90,10 @@ function getDaemonPid () { } } +function isDaemonRunning () { + return fs.existsSync(`${APP_HOME}/.daemon.pid`) +} + function removeDaemonPid () { try { fs.unlinkSync(`${APP_HOME}/.daemon.pid`) @@ -114,5 +118,6 @@ module.exports = { getErrStream, storeDaemonPid, getDaemonPid, + isDaemonRunning, removeDaemonPid } diff --git a/src/rpc.js b/src/rpc.js index 34ad1f2..ad1c343 100644 --- a/src/rpc.js +++ b/src/rpc.js @@ -16,9 +16,16 @@ module.exports = class RPC { // for example check of peerInfo.publicKey is in a list of allowed keys // 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)) + /* -- PUSH HANDLERS -- */ + // TODO: reponders to pull requests + // normal push: git pull + // force push: git reset --hard url/ + // delete branch: git branch -D url/ + this.connections[peerInfo.publicKey] = rpc } From a38bd12ad077738475b7e5949623df719033a2fe Mon Sep 17 00:00:00 2001 From: dzdidi Date: Tue, 23 Jan 2024 22:50:18 +0000 Subject: [PATCH 07/60] scaffold for push rpc Signed-off-by: dzdidi --- src/git-remote-pear.js | 70 +++++++++++++++++++++++------------------- src/rpc.js | 22 ++++++++++--- 2 files changed, 57 insertions(+), 35 deletions(-) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index 5eb1853..e6144dc 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -1,5 +1,6 @@ #!/usr/bin/env node +const { spawn } = require('child_process') const ProtomuxRPC = require('protomux-rpc') const RAM = require('random-access-memory') @@ -25,7 +26,23 @@ const targetKey = matches[1] const repoName = matches[2] const store = new Corestore(RAM) -const swarm = new Hyperswarm() +const swarm = new Hyperswarm({ keypair: home.getKeyPair() }) + +let daemonPid +if (!home.isDaemonRunning()) { + const opts = { + detached: true, + stdio: [ 'ignore', home.getOutStream(), home.getErrStream() ] + } + const daemon = spawn('git-peard', opts) + daemonPid = daemon.pid + home.storeDaemonPid(daemonPid) + // TODO: remove in case of error or exit but allow unref + // daemon.on('error', home.removeDaemonPid) + // daemon.on('exit', home.removeDaemonPid) + console.error('started daemon', daemonPid) + daemon.unref() +} swarm.join(crypto.discoveryKey(Buffer.from(targetKey, 'hex')), { server: false }) @@ -61,10 +78,10 @@ swarm.on('connection', async (socket) => { const refsRes = await rpc.request('get-refs', Buffer.from(repoName)) - await talkToGit(JSON.parse(refsRes.toString()), drive, repoName) + await talkToGit(JSON.parse(refsRes.toString()), drive, repoName, rpc) }) -async function talkToGit (refs, drive, repoName) { +async function talkToGit (refs, drive, repoName, rpc) { for (const ref in refs) { console.warn(refs[ref] + '\t' + ref) } @@ -83,37 +100,28 @@ async function talkToGit (refs, drive, repoName) { const isDelete = !src const isForce = src.startsWith('+') - // XXX: it looks like git is trying to establish network connection first, so if it can not - // reach push destination it hangs everything on capabilities exchange - // TODO: add timeout + if (!home.isShared(repoName)) { + home.shareAppFolder(name) + } + await git.push(src.replace('refs/heads/', '')) - // FOR TESTING RUN SECOND INSTANCE OF GIT-PEARD - // if (!home.isDaemonRunning()) { - // const opts = { - // detached: true, - // stdio: [ 'ignore', home.getOutStream(), home.getErrStream() ] - // } - // const daemon = spawn('git-peard', opts) - // home.storeDaemonPid(daemon.pid) - // // TODO: remove in case of error or exit but allow unref - // // daemon.on('error', home.removeDaemonPid) - // // daemon.on('exit', home.removeDaemonPid) - // daemon.unref() - // } + console.error('_command', _command) + let command + if (isDelete) { + command = 'delete-branch-from-repo' + } else if (isForce) { + command = 'force-push-to-repo' + } else { + command = 'push-to-repo' + } - console.error('TODO') - // TODO: get repo name (current dir name instead or name from url) - // if (home.isShared(repoName)) { - // git push branch - // } else { - // share repo with current branch - // } + const publicKey = home.readPk() + const res = await rpc.request(command, Buffer.from(repoName + ':' + dst + ':' + publicKey)) - // - send rpc command to remote (url) - // Mapping of RPC commands to git commands on origin: - // - normal push - git pull - // - force push - git reset --hard / - // - delete branch - git branch -D / + console.error('killing', daemonPid) + process.kill(daemonPid || home.getDaemonPid()) + console.error('killed') + home.removeDaemonPid() process.stdout.write('\n\n') process.exit(0) diff --git a/src/rpc.js b/src/rpc.js index ad1c343..9fca37b 100644 --- a/src/rpc.js +++ b/src/rpc.js @@ -21,10 +21,9 @@ module.exports = class RPC { rpc.respond('get-refs', async req => await this.getRefsHandler(req)) /* -- PUSH HANDLERS -- */ - // TODO: reponders to pull requests - // normal push: git pull - // force push: git reset --hard url/ - // delete branch: git branch -D url/ + rpc.respond('push-to-repo', async req => this.pushHandler(req)) + rpc.respond('force-push-to-repo', req => this.forcePushHandler(req)) + rpc.respond('delete-branch-from-repo', req => this.deleteBranchHandler(req)) this.connections[peerInfo.publicKey] = rpc } @@ -42,4 +41,19 @@ module.exports = class RPC { return Buffer.from(JSON.stringify(res)) } + + pushHandler (req) { + console.error('req', req.toString()) + console.error('pushHandler not implemented') + } + + forcePushHandler (req) { + console.error('req', req.toString()) + console.error('forcePushHandler not implemented') + } + + deleteBranchHandler (req) { + console.error('req', req.toString()) + console.error('deleteBranchHandler not implemented') + } } From ec17ae0a71168ada32f3306c3acec0b716e82756 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Tue, 23 Jan 2024 22:53:29 +0000 Subject: [PATCH 08/60] fix push rpc payload Signed-off-by: dzdidi --- src/git-remote-pear.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index e6144dc..66573ac 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -95,7 +95,7 @@ async function talkToGit (refs, drive, repoName, rpc) { process.stdout.write('fetch\n\n') } else if (chunk && chunk.search(/^push/) !== -1) { const [_command, path] = chunk.split(' ') - const [src, dst] = path.split(':') + let [src, dst] = path.split(':') const isDelete = !src const isForce = src.startsWith('+') @@ -103,9 +103,12 @@ async function talkToGit (refs, drive, repoName, rpc) { if (!home.isShared(repoName)) { home.shareAppFolder(name) } - await git.push(src.replace('refs/heads/', '')) + + console.error('dst', JSON.stringify(dst)) + dst = dst.replace('refs/heads/', '').replace('\n\n', '') + console.error('dst', JSON.stringify(dst)) + await git.push(dst) - console.error('_command', _command) let command if (isDelete) { command = 'delete-branch-from-repo' @@ -116,7 +119,7 @@ async function talkToGit (refs, drive, repoName, rpc) { } const publicKey = home.readPk() - const res = await rpc.request(command, Buffer.from(repoName + ':' + dst + ':' + publicKey)) + const res = await rpc.request(command, Buffer.from(`${publicKey}/${repoName}:${dst}`)) console.error('killing', daemonPid) process.kill(daemonPid || home.getDaemonPid()) From cf038ff27aa7b5a1259a39d96ad544b8fd4d851c Mon Sep 17 00:00:00 2001 From: dzdidi Date: Tue, 23 Jan 2024 23:11:39 +0000 Subject: [PATCH 09/60] push rpc command draft Signed-off-by: dzdidi --- src/rpc.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/rpc.js b/src/rpc.js index 9fca37b..423fc09 100644 --- a/src/rpc.js +++ b/src/rpc.js @@ -43,17 +43,42 @@ module.exports = class RPC { } pushHandler (req) { + // TODO: check ACL + // XXX: from the inside of a bare repo: + // git fetch url : console.error('req', req.toString()) console.error('pushHandler not implemented') } forcePushHandler (req) { + // TODO: check ACL + // XXX: from the inside of a bare repo: + // git reset --hard url console.error('req', req.toString()) console.error('forcePushHandler not implemented') } deleteBranchHandler (req) { + // TODO: check ACL + // XXX: from the inside of a bare repo: + // git push -d pear console.error('req', req.toString()) console.error('deleteBranchHandler not implemented') } + + parsePushCommand(req) { + const [url, branch] = req.toString().split(':') + const [key, repo] = url.split('/') + return { + url: `pear://${url}`, + repo, + key, + branch + } + } + + loadACL(repoName) { + // TODO: read contact of .git-daemon-export-ok + // find key and its permissions + } } From c146669a11102f61c57b7d4b7799f1dbd38b2c7e Mon Sep 17 00:00:00 2001 From: dzdidi Date: Tue, 23 Jan 2024 23:27:26 +0000 Subject: [PATCH 10/60] push rpc command draft improved Signed-off-by: dzdidi --- src/git-remote-pear.js | 3 +-- src/rpc.js | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index 66573ac..cba4e9c 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -121,11 +121,10 @@ async function talkToGit (refs, drive, repoName, rpc) { const publicKey = home.readPk() const res = await rpc.request(command, Buffer.from(`${publicKey}/${repoName}:${dst}`)) - console.error('killing', daemonPid) process.kill(daemonPid || home.getDaemonPid()) - console.error('killed') home.removeDaemonPid() + process.stdout.write(res.toString()) process.stdout.write('\n\n') process.exit(0) } else if (chunk && chunk.search(/^list/) !== -1) { // list && list for-push diff --git a/src/rpc.js b/src/rpc.js index 423fc09..39b67c3 100644 --- a/src/rpc.js +++ b/src/rpc.js @@ -44,24 +44,26 @@ module.exports = class RPC { pushHandler (req) { // TODO: check ACL - // XXX: from the inside of a bare repo: - // git fetch url : + // collect stdout to buffer and return it + // const process = spawn('git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: getCodePath(name) } }) console.error('req', req.toString()) console.error('pushHandler not implemented') } forcePushHandler (req) { - // TODO: check ACL - // XXX: from the inside of a bare repo: - // git reset --hard url + // TODO: + // check ACL + // collect stdout to buffer and return it + // const process = spawn('git', ['reset', '--hard', url, branch], { env: { GIT_DIR: getCodePath(name) } }) console.error('req', req.toString()) console.error('forcePushHandler not implemented') } deleteBranchHandler (req) { - // TODO: check ACL - // XXX: from the inside of a bare repo: - // git push -d pear + // TODO: + // check ACL + // collect stdout to buffer and return it + // const process = spawn('git', ['branch', '-d', branch], { env: { GIT_DIR: getCodePath(name) } }) console.error('req', req.toString()) console.error('deleteBranchHandler not implemented') } From 1173108deae9585cce5e57e53d208d105f8307b2 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 10:47:36 +0000 Subject: [PATCH 11/60] tmp commit Signed-off-by: dzdidi --- src/rpc.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/rpc.js b/src/rpc.js index 39b67c3..2e25c51 100644 --- a/src/rpc.js +++ b/src/rpc.js @@ -43,6 +43,12 @@ module.exports = class RPC { } pushHandler (req) { + console.error('pushHandler is to be implemented') + const { url, repo, key, branch } = this.parsePushCommand(req) + console.error('url', url) + console.error('repo', repo) + console.error('key', key) + console.error('branch', branch) // TODO: check ACL // collect stdout to buffer and return it // const process = spawn('git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: getCodePath(name) } }) @@ -51,6 +57,7 @@ module.exports = class RPC { } forcePushHandler (req) { + const { url, repo, key, branch } = this.parsePushCommand(req) // TODO: // check ACL // collect stdout to buffer and return it @@ -60,6 +67,7 @@ module.exports = class RPC { } deleteBranchHandler (req) { + const { url, repo, key, branch } = this.parsePushCommand(req) // TODO: // check ACL // collect stdout to buffer and return it From 1b4f06e5115a472fb6c6775d255938905755ec6e Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 10:50:37 +0000 Subject: [PATCH 12/60] tmp commit1 Signed-off-by: dzdidi --- src/rpc.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/rpc.js b/src/rpc.js index 2e25c51..bf6de15 100644 --- a/src/rpc.js +++ b/src/rpc.js @@ -43,7 +43,7 @@ module.exports = class RPC { } pushHandler (req) { - console.error('pushHandler is to be implemented') + console.error('pushHandler is to be implemented', req.toString()) const { url, repo, key, branch } = this.parsePushCommand(req) console.error('url', url) console.error('repo', repo) @@ -52,7 +52,6 @@ module.exports = class RPC { // TODO: check ACL // collect stdout to buffer and return it // const process = spawn('git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: getCodePath(name) } }) - console.error('req', req.toString()) console.error('pushHandler not implemented') } From db463f9a8b51b76461b26e273665ddc939563bad Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:00:11 +0000 Subject: [PATCH 13/60] tmp commit2 Signed-off-by: dzdidi --- src/rpc.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/rpc.js b/src/rpc.js index bf6de15..ac1a447 100644 --- a/src/rpc.js +++ b/src/rpc.js @@ -43,16 +43,20 @@ module.exports = class RPC { } pushHandler (req) { - console.error('pushHandler is to be implemented', req.toString()) const { url, repo, key, branch } = this.parsePushCommand(req) - console.error('url', url) - console.error('repo', repo) - console.error('key', key) - console.error('branch', branch) // TODO: check ACL // collect stdout to buffer and return it - // const process = spawn('git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: getCodePath(name) } }) - console.error('pushHandler not implemented') + const process = spawn('git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: getCodePath(name) } }) + const outBuffer = new Buffer() + const errBuffer = new Buffer() + process.stdout.on('data', data => { + outBuffer.push(data) + console.error('data', JSON.stringify(data.toString())) + }) + process.stderr.on('data', data => { + errBuffer.push(data) + console.error('error', JSON.stringify(data.toString())) + }) } forcePushHandler (req) { From b1512a58ad309da02b53912ea16665daa28ab46b Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:03:39 +0000 Subject: [PATCH 14/60] tmp commit3 Signed-off-by: dzdidi --- src/rpc.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rpc.js b/src/rpc.js index ac1a447..1f8d394 100644 --- a/src/rpc.js +++ b/src/rpc.js @@ -51,11 +51,11 @@ module.exports = class RPC { const errBuffer = new Buffer() process.stdout.on('data', data => { outBuffer.push(data) - console.error('data', JSON.stringify(data.toString())) + console.error('data:', JSON.stringify(data.toString())) }) process.stderr.on('data', data => { errBuffer.push(data) - console.error('error', JSON.stringify(data.toString())) + console.error('error:', JSON.stringify(data.toString())) }) } From d386c95f7ebf4e8f500cad55a3540ec6d9e7ac37 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:06:25 +0000 Subject: [PATCH 15/60] shrinkwrap update Signed-off-by: dzdidi --- npm-shrinkwrap.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index ce944db..1700503 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -12,10 +12,10 @@ "dependencies": { "chokidar": "^3.5.3", "commander": "^11.0.0", - "corestore": "^6.10.1", - "hyperdrive": "^11.5.3", - "hyperswarm": "^4.5.1", - "protomux-rpc": "^1.4.1", + "corestore": "^6.15.13", + "hyperdrive": "^11.6.3", + "hyperswarm": "^4.7.13", + "protomux-rpc": "^1.5.1", "random-access-memory": "^6.2.0" }, "bin": { From a8419486ecb9903ab53e52fb6f377943ed06d0b7 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:08:16 +0000 Subject: [PATCH 16/60] tmp Signed-off-by: dzdidi --- src/rpc.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/rpc.js b/src/rpc.js index 1f8d394..b55334c 100644 --- a/src/rpc.js +++ b/src/rpc.js @@ -57,6 +57,10 @@ module.exports = class RPC { errBuffer.push(data) console.error('error:', JSON.stringify(data.toString())) }) + + process.on('close', code => { + console.error(`child process exited with code ${code}`) + }) } forcePushHandler (req) { From 2edb639fdeea7e691505df59f521320a50841054 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:10:12 +0000 Subject: [PATCH 17/60] rpc to link Signed-off-by: dzdidi --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5299b9b..b6a9b5b 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "bin": { "git-peard": "./src/gitpeard.js", "git-remote-pear": "./src/git-remote-pear.js", - "git-pear": "./src/cli.js" + "git-pear": "./src/cli.js", + "rpc": "./src/rpc.js" }, "repository": { "type": "git", From 5049a6051612325294418275ee15bbc71bef3d78 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:12:46 +0000 Subject: [PATCH 18/60] add spawn dep Signed-off-by: dzdidi --- src/rpc.js | 1 + 1 file changed, 1 insertion(+) mode change 100644 => 100755 src/rpc.js diff --git a/src/rpc.js b/src/rpc.js old mode 100644 new mode 100755 index b55334c..89a242c --- a/src/rpc.js +++ b/src/rpc.js @@ -1,4 +1,5 @@ const ProtomuxRPC = require('protomux-rpc') +const { spawn } = require('child_process') module.exports = class RPC { constructor (announcedRefs, repositories, drives) { From efcb302dfbb6961a81e25ba52d41291831009316 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:19:14 +0000 Subject: [PATCH 19/60] add home dep Signed-off-by: dzdidi --- src/rpc.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/rpc.js b/src/rpc.js index 89a242c..0a255b1 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -1,5 +1,6 @@ const ProtomuxRPC = require('protomux-rpc') const { spawn } = require('child_process') +const home = require('./home') module.exports = class RPC { constructor (announcedRefs, repositories, drives) { @@ -47,7 +48,7 @@ module.exports = class RPC { const { url, repo, key, branch } = this.parsePushCommand(req) // TODO: check ACL // collect stdout to buffer and return it - const process = spawn('git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: getCodePath(name) } }) + const process = spawn('git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: home.getCodePath(name) } }) const outBuffer = new Buffer() const errBuffer = new Buffer() process.stdout.on('data', data => { @@ -69,7 +70,7 @@ module.exports = class RPC { // TODO: // check ACL // collect stdout to buffer and return it - // const process = spawn('git', ['reset', '--hard', url, branch], { env: { GIT_DIR: getCodePath(name) } }) + // const process = spawn('git', ['reset', '--hard', url, branch], { env: { GIT_DIR: home.getCodePath(name) } }) console.error('req', req.toString()) console.error('forcePushHandler not implemented') } @@ -79,7 +80,7 @@ module.exports = class RPC { // TODO: // check ACL // collect stdout to buffer and return it - // const process = spawn('git', ['branch', '-d', branch], { env: { GIT_DIR: getCodePath(name) } }) + // const process = spawn('git', ['branch', '-d', branch], { env: { GIT_DIR: home.getCodePath(name) } }) console.error('req', req.toString()) console.error('deleteBranchHandler not implemented') } From 242f84b23dac09a470a77d73d6229db3ea7420e8 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:21:01 +0000 Subject: [PATCH 20/60] fix repo ref Signed-off-by: dzdidi --- src/rpc.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rpc.js b/src/rpc.js index 0a255b1..aec6b7a 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -48,7 +48,7 @@ module.exports = class RPC { const { url, repo, key, branch } = this.parsePushCommand(req) // TODO: check ACL // collect stdout to buffer and return it - const process = spawn('git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: home.getCodePath(name) } }) + const process = spawn('git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: home.getCodePath(repo) } }) const outBuffer = new Buffer() const errBuffer = new Buffer() process.stdout.on('data', data => { @@ -70,7 +70,7 @@ module.exports = class RPC { // TODO: // check ACL // collect stdout to buffer and return it - // const process = spawn('git', ['reset', '--hard', url, branch], { env: { GIT_DIR: home.getCodePath(name) } }) + // const process = spawn('git', ['reset', '--hard', url, branch], { env: { GIT_DIR: home.getCodePath(repo) } }) console.error('req', req.toString()) console.error('forcePushHandler not implemented') } @@ -80,7 +80,7 @@ module.exports = class RPC { // TODO: // check ACL // collect stdout to buffer and return it - // const process = spawn('git', ['branch', '-d', branch], { env: { GIT_DIR: home.getCodePath(name) } }) + // const process = spawn('git', ['branch', '-d', branch], { env: { GIT_DIR: home.getCodePath(repo) } }) console.error('req', req.toString()) console.error('deleteBranchHandler not implemented') } From a867a3d6853de7317425192b841d37ad3514e4b5 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:22:31 +0000 Subject: [PATCH 21/60] fix buffer Signed-off-by: dzdidi --- src/rpc.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/rpc.js b/src/rpc.js index aec6b7a..e9ae78d 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -49,15 +49,15 @@ module.exports = class RPC { // TODO: check ACL // collect stdout to buffer and return it const process = spawn('git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: home.getCodePath(repo) } }) - const outBuffer = new Buffer() - const errBuffer = new Buffer() + const outBuffer = Buffer.from('') + const errBuffer = Buffer.from('') process.stdout.on('data', data => { - outBuffer.push(data) console.error('data:', JSON.stringify(data.toString())) + outBuffer.push(data) }) process.stderr.on('data', data => { - errBuffer.push(data) console.error('error:', JSON.stringify(data.toString())) + errBuffer.push(data) }) process.on('close', code => { From 7a4353ffc4a148b87ecd42dde72c4b192b5ae5df Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:25:46 +0000 Subject: [PATCH 22/60] fix buffer 2 Signed-off-by: dzdidi --- src/rpc.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rpc.js b/src/rpc.js index e9ae78d..70d3dff 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -52,11 +52,11 @@ module.exports = class RPC { const outBuffer = Buffer.from('') const errBuffer = Buffer.from('') process.stdout.on('data', data => { - console.error('data:', JSON.stringify(data.toString())) + console.error('git data:', JSON.stringify(data.toString())) outBuffer.push(data) }) process.stderr.on('data', data => { - console.error('error:', JSON.stringify(data.toString())) + console.error('git error:', JSON.stringify(data.toString())) errBuffer.push(data) }) From c4b6be55274db4682dca30788bb03b8ce68e0dcb Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:26:47 +0000 Subject: [PATCH 23/60] fix buffer 3 Signed-off-by: dzdidi --- src/rpc.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rpc.js b/src/rpc.js index 70d3dff..e9ae78d 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -52,11 +52,11 @@ module.exports = class RPC { const outBuffer = Buffer.from('') const errBuffer = Buffer.from('') process.stdout.on('data', data => { - console.error('git data:', JSON.stringify(data.toString())) + console.error('data:', JSON.stringify(data.toString())) outBuffer.push(data) }) process.stderr.on('data', data => { - console.error('git error:', JSON.stringify(data.toString())) + console.error('error:', JSON.stringify(data.toString())) errBuffer.push(data) }) From b7d9749a1108606dd8de48027bd9ef7b48f23c21 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:33:31 +0000 Subject: [PATCH 24/60] tmp: assume daemon is always running Signed-off-by: dzdidi --- src/git-remote-pear.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index cba4e9c..6d389f7 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -28,21 +28,21 @@ const repoName = matches[2] const store = new Corestore(RAM) const swarm = new Hyperswarm({ keypair: home.getKeyPair() }) -let daemonPid -if (!home.isDaemonRunning()) { - const opts = { - detached: true, - stdio: [ 'ignore', home.getOutStream(), home.getErrStream() ] - } - const daemon = spawn('git-peard', opts) - daemonPid = daemon.pid - home.storeDaemonPid(daemonPid) - // TODO: remove in case of error or exit but allow unref - // daemon.on('error', home.removeDaemonPid) - // daemon.on('exit', home.removeDaemonPid) - console.error('started daemon', daemonPid) - daemon.unref() -} +// let daemonPid +// if (!home.isDaemonRunning()) { +// const opts = { +// detached: true, +// stdio: [ 'ignore', home.getOutStream(), home.getErrStream() ] +// } +// const daemon = spawn('git-peard', opts) +// daemonPid = daemon.pid +// home.storeDaemonPid(daemonPid) +// // TODO: remove in case of error or exit but allow unref +// // daemon.on('error', home.removeDaemonPid) +// // daemon.on('exit', home.removeDaemonPid) +// console.error('started daemon', daemonPid) +// daemon.unref() +// } swarm.join(crypto.discoveryKey(Buffer.from(targetKey, 'hex')), { server: false }) @@ -121,8 +121,8 @@ async function talkToGit (refs, drive, repoName, rpc) { const publicKey = home.readPk() const res = await rpc.request(command, Buffer.from(`${publicKey}/${repoName}:${dst}`)) - process.kill(daemonPid || home.getDaemonPid()) - home.removeDaemonPid() + // process.kill(daemonPid || home.getDaemonPid()) + // home.removeDaemonPid() process.stdout.write(res.toString()) process.stdout.write('\n\n') From fc2eb1d6b712259123a8ac028a62c0a83a653f96 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:36:39 +0000 Subject: [PATCH 25/60] fix: buffer concat Signed-off-by: dzdidi --- src/rpc.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/rpc.js b/src/rpc.js index e9ae78d..627d311 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -49,15 +49,15 @@ module.exports = class RPC { // TODO: check ACL // collect stdout to buffer and return it const process = spawn('git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: home.getCodePath(repo) } }) - const outBuffer = Buffer.from('') - const errBuffer = Buffer.from('') + let outBuffer = Buffer.from('') + let errBuffer = Buffer.from('') process.stdout.on('data', data => { console.error('data:', JSON.stringify(data.toString())) - outBuffer.push(data) + outBuffer = Buffer.concat([outBuffer, data]) }) process.stderr.on('data', data => { console.error('error:', JSON.stringify(data.toString())) - errBuffer.push(data) + errBuffer = Buffer.concat([errBuffer, data]) }) process.on('close', code => { From 9c4e4b65e7a68b9ecbee8aa79742600f1c3b6af3 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:42:03 +0000 Subject: [PATCH 26/60] return resolve buffer Signed-off-by: dzdidi --- src/rpc.js | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/rpc.js b/src/rpc.js index 627d311..94812ec 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -23,7 +23,7 @@ module.exports = class RPC { rpc.respond('get-refs', async req => await this.getRefsHandler(req)) /* -- PUSH HANDLERS -- */ - rpc.respond('push-to-repo', async req => this.pushHandler(req)) + rpc.respond('push-to-repo', async req => await this.pushHandler(req)) rpc.respond('force-push-to-repo', req => this.forcePushHandler(req)) rpc.respond('delete-branch-from-repo', req => this.deleteBranchHandler(req)) @@ -44,24 +44,26 @@ module.exports = class RPC { return Buffer.from(JSON.stringify(res)) } - pushHandler (req) { + async pushHandler (req) { const { url, repo, key, branch } = this.parsePushCommand(req) // TODO: check ACL // collect stdout to buffer and return it - const process = spawn('git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: home.getCodePath(repo) } }) - let outBuffer = Buffer.from('') - let errBuffer = Buffer.from('') - process.stdout.on('data', data => { - console.error('data:', JSON.stringify(data.toString())) - outBuffer = Buffer.concat([outBuffer, data]) - }) - process.stderr.on('data', data => { - console.error('error:', JSON.stringify(data.toString())) - errBuffer = Buffer.concat([errBuffer, data]) - }) + return await new Promise((resolve, reject) => { + const process = spawn('git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: home.getCodePath(repo) } }) + //let outBuffer = Buffer.from('') + // process.stdout.on('data', data => { + // console.error('data:', JSON.stringify(data.toString())) + // outBuffer = Buffer.concat([outBuffer, data]) + // }) + let errBuffer = Buffer.from('') + process.stderr.on('data', data => { + errBuffer = Buffer.concat([errBuffer, data]) + }) - process.on('close', code => { - console.error(`child process exited with code ${code}`) + process.on('close', code => { + console.error(`child process exited with code ${code}`) + return code === 0 ? resolve(errBuffer) : reject(errBuffer) + }) }) } From 654ed2b890ac5eb53170e9db0158972e579959c1 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:45:00 +0000 Subject: [PATCH 27/60] handle resolve buffer Signed-off-by: dzdidi --- src/git-remote-pear.js | 1 + src/rpc.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index 6d389f7..00a1f00 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -124,6 +124,7 @@ async function talkToGit (refs, drive, repoName, rpc) { // process.kill(daemonPid || home.getDaemonPid()) // home.removeDaemonPid() + console.error('res', res.toString()) process.stdout.write(res.toString()) process.stdout.write('\n\n') process.exit(0) diff --git a/src/rpc.js b/src/rpc.js index 94812ec..1111ecc 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -61,6 +61,7 @@ module.exports = class RPC { }) process.on('close', code => { + console.error('out:', errBuffer.toString()) console.error(`child process exited with code ${code}`) return code === 0 ? resolve(errBuffer) : reject(errBuffer) }) From 6c0c644cd190be1b4e3d05bae7bb9bf317bfb964 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:46:01 +0000 Subject: [PATCH 28/60] handle resolve buffer p2 Signed-off-by: dzdidi --- src/rpc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rpc.js b/src/rpc.js index 1111ecc..8d3496d 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -58,6 +58,7 @@ module.exports = class RPC { let errBuffer = Buffer.from('') process.stderr.on('data', data => { errBuffer = Buffer.concat([errBuffer, data]) + console.error('out:', errBuffer.toString()) }) process.on('close', code => { From 75864c073b7d055e614a8a6b603ece2b7f04aad5 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:48:10 +0000 Subject: [PATCH 29/60] no write res to git Signed-off-by: dzdidi --- src/git-remote-pear.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index 00a1f00..e6008c5 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -104,9 +104,7 @@ async function talkToGit (refs, drive, repoName, rpc) { home.shareAppFolder(name) } - console.error('dst', JSON.stringify(dst)) dst = dst.replace('refs/heads/', '').replace('\n\n', '') - console.error('dst', JSON.stringify(dst)) await git.push(dst) let command @@ -125,7 +123,7 @@ async function talkToGit (refs, drive, repoName, rpc) { // home.removeDaemonPid() console.error('res', res.toString()) - process.stdout.write(res.toString()) + // process.stdout.write(res.toString()) process.stdout.write('\n\n') process.exit(0) } else if (chunk && chunk.search(/^list/) !== -1) { // list && list for-push From b838205f71f072707aa36b33b813f5acee8e7b52 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:51:07 +0000 Subject: [PATCH 30/60] change fetch to pull in bear Signed-off-by: dzdidi --- src/git-remote-pear.js | 2 +- src/rpc.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index e6008c5..4944656 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -122,7 +122,7 @@ async function talkToGit (refs, drive, repoName, rpc) { // process.kill(daemonPid || home.getDaemonPid()) // home.removeDaemonPid() - console.error('res', res.toString()) + console.error('response', res.toString()) // process.stdout.write(res.toString()) process.stdout.write('\n\n') process.exit(0) diff --git a/src/rpc.js b/src/rpc.js index 8d3496d..71ee96b 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -49,7 +49,7 @@ module.exports = class RPC { // TODO: check ACL // collect stdout to buffer and return it return await new Promise((resolve, reject) => { - const process = spawn('git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: home.getCodePath(repo) } }) + const process = spawn('git', ['pull', url, `${branch}:${branch}`], { env: { GIT_DIR: home.getCodePath(repo) } }) //let outBuffer = Buffer.from('') // process.stdout.on('data', data => { // console.error('data:', JSON.stringify(data.toString())) From f3d7adadd85661907545ee087c53d975e7597a29 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:54:03 +0000 Subject: [PATCH 31/60] debug inside bare Signed-off-by: dzdidi --- src/rpc.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rpc.js b/src/rpc.js index 71ee96b..b7efc20 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -49,12 +49,13 @@ module.exports = class RPC { // TODO: check ACL // collect stdout to buffer and return it return await new Promise((resolve, reject) => { - const process = spawn('git', ['pull', url, `${branch}:${branch}`], { env: { GIT_DIR: home.getCodePath(repo) } }) + const process = spawn('git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: home.getCodePath(repo) } }) //let outBuffer = Buffer.from('') // process.stdout.on('data', data => { // console.error('data:', JSON.stringify(data.toString())) // outBuffer = Buffer.concat([outBuffer, data]) // }) + console.error('ARGS:'. process.argv) let errBuffer = Buffer.from('') process.stderr.on('data', data => { errBuffer = Buffer.concat([errBuffer, data]) From f91506b332dc4000231be1181758ec345ae9cea7 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:55:17 +0000 Subject: [PATCH 32/60] fix typo Signed-off-by: dzdidi --- src/rpc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rpc.js b/src/rpc.js index b7efc20..30606ac 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -55,7 +55,7 @@ module.exports = class RPC { // console.error('data:', JSON.stringify(data.toString())) // outBuffer = Buffer.concat([outBuffer, data]) // }) - console.error('ARGS:'. process.argv) + console.error('ARGS:', process.argv) let errBuffer = Buffer.from('') process.stderr.on('data', data => { errBuffer = Buffer.concat([errBuffer, data]) From 51e14388b451dbcd60bd36bdc5a5180112333a2a Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 11:58:01 +0000 Subject: [PATCH 33/60] debugs Signed-off-by: dzdidi --- src/rpc.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rpc.js b/src/rpc.js index 30606ac..79e2ebb 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -59,11 +59,11 @@ module.exports = class RPC { let errBuffer = Buffer.from('') process.stderr.on('data', data => { errBuffer = Buffer.concat([errBuffer, data]) - console.error('out:', errBuffer.toString()) + console.error('out in str:', errBuffer.toString()) }) process.on('close', code => { - console.error('out:', errBuffer.toString()) + console.error('out on close:', errBuffer.toString()) console.error(`child process exited with code ${code}`) return code === 0 ? resolve(errBuffer) : reject(errBuffer) }) From 584fe02d4e459e6af87191e5c3b9939c0f53fe0a Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 12:00:14 +0000 Subject: [PATCH 34/60] better debugs Signed-off-by: dzdidi --- src/rpc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rpc.js b/src/rpc.js index 79e2ebb..323692f 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -55,7 +55,7 @@ module.exports = class RPC { // console.error('data:', JSON.stringify(data.toString())) // outBuffer = Buffer.concat([outBuffer, data]) // }) - console.error('ARGS:', process.argv) + console.error('ARGS:', 'git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: home.getCodePath(repo) } }) let errBuffer = Buffer.from('') process.stderr.on('data', data => { errBuffer = Buffer.concat([errBuffer, data]) From 31b6952a06c9cdf69e6be345b54f51bc3b84a4f9 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Wed, 24 Jan 2024 12:06:24 +0000 Subject: [PATCH 35/60] push working Signed-off-by: dzdidi --- src/git-remote-pear.js | 1 - src/rpc.js | 10 +--------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index 4944656..7149045 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -122,7 +122,6 @@ async function talkToGit (refs, drive, repoName, rpc) { // process.kill(daemonPid || home.getDaemonPid()) // home.removeDaemonPid() - console.error('response', res.toString()) // process.stdout.write(res.toString()) process.stdout.write('\n\n') process.exit(0) diff --git a/src/rpc.js b/src/rpc.js index 323692f..e105b13 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -50,21 +50,13 @@ module.exports = class RPC { // collect stdout to buffer and return it return await new Promise((resolve, reject) => { const process = spawn('git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: home.getCodePath(repo) } }) - //let outBuffer = Buffer.from('') - // process.stdout.on('data', data => { - // console.error('data:', JSON.stringify(data.toString())) - // outBuffer = Buffer.concat([outBuffer, data]) - // }) - console.error('ARGS:', 'git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: home.getCodePath(repo) } }) let errBuffer = Buffer.from('') process.stderr.on('data', data => { errBuffer = Buffer.concat([errBuffer, data]) - console.error('out in str:', errBuffer.toString()) }) + // TODO: write buffer to standard output with ACL process.on('close', code => { - console.error('out on close:', errBuffer.toString()) - console.error(`child process exited with code ${code}`) return code === 0 ? resolve(errBuffer) : reject(errBuffer) }) }) From a3d69c7397567e91577e766a0d064cc6621b15ee Mon Sep 17 00:00:00 2001 From: dzdidi Date: Thu, 25 Jan 2024 06:34:46 +0000 Subject: [PATCH 36/60] Force push and delete Signed-off-by: dzdidi --- package.json | 3 +-- src/git-remote-pear.js | 23 ++++++++++++-------- src/git.js | 6 ++++-- src/rpc.js | 48 +++++++++++++++++++++++++----------------- 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index b6a9b5b..5299b9b 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,7 @@ "bin": { "git-peard": "./src/gitpeard.js", "git-remote-pear": "./src/git-remote-pear.js", - "git-pear": "./src/cli.js", - "rpc": "./src/rpc.js" + "git-pear": "./src/cli.js" }, "repository": { "type": "git", diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index 7149045..166e6b0 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -82,9 +82,6 @@ swarm.on('connection', async (socket) => { }) async function talkToGit (refs, drive, repoName, rpc) { - for (const ref in refs) { - console.warn(refs[ref] + '\t' + ref) - } process.stdin.setEncoding('utf8') const didFetch = false process.stdin.on('readable', async function () { @@ -103,17 +100,20 @@ async function talkToGit (refs, drive, repoName, rpc) { if (!home.isShared(repoName)) { home.shareAppFolder(name) } - + dst = dst.replace('refs/heads/', '').replace('\n\n', '') - await git.push(dst) let command if (isDelete) { - command = 'delete-branch-from-repo' + command = 'd-branch' } else if (isForce) { - command = 'force-push-to-repo' + await git.push(src, isForce) + src = src.replace('+', '') + command = 'f-push' } else { - command = 'push-to-repo' + console.warn('pushing', src, dst) + await git.push(src) + command = 'push' } const publicKey = home.readPk() @@ -122,15 +122,20 @@ async function talkToGit (refs, drive, repoName, rpc) { // process.kill(daemonPid || home.getDaemonPid()) // home.removeDaemonPid() - // process.stdout.write(res.toString()) process.stdout.write('\n\n') process.exit(0) } else if (chunk && chunk.search(/^list/) !== -1) { // list && list for-push + for (const ref in refs) { + console.warn(refs[ref] + '\t' + ref) + } Object.keys(refs).forEach(function (branch, i) { process.stdout.write(refs[branch] + ' ' + branch + '\n') }) process.stdout.write('\n') } else if (chunk && chunk.search(/^fetch/) !== -1) { + for (const ref in refs) { + console.warn(refs[ref] + '\t' + ref) + } const lines = chunk.split(/\n/).filter(l => l !== '') const targets = [] diff --git a/src/git.js b/src/git.js index f733555..3cb7c69 100644 --- a/src/git.js +++ b/src/git.js @@ -31,8 +31,10 @@ async function addRemote (name) { return await doGit(init) } -async function push (branch = 'master') { - const push = spawn('git', ['push', 'pear', branch]) +async function push (branch = 'master', force = false) { + const args = ['push', 'pear', branch] + if (force) args.push('-f') + const push = spawn('git', args) return await doGit(push) } diff --git a/src/rpc.js b/src/rpc.js index e105b13..8ab4fd4 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -23,9 +23,9 @@ module.exports = class RPC { rpc.respond('get-refs', async req => await this.getRefsHandler(req)) /* -- PUSH HANDLERS -- */ - rpc.respond('push-to-repo', async req => await this.pushHandler(req)) - rpc.respond('force-push-to-repo', req => this.forcePushHandler(req)) - rpc.respond('delete-branch-from-repo', req => this.deleteBranchHandler(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 } @@ -47,7 +47,6 @@ module.exports = class RPC { async pushHandler (req) { const { url, repo, key, branch } = this.parsePushCommand(req) // TODO: check ACL - // collect stdout to buffer and return it return await new Promise((resolve, reject) => { const process = spawn('git', ['fetch', url, `${branch}:${branch}`], { env: { GIT_DIR: home.getCodePath(repo) } }) let errBuffer = Buffer.from('') @@ -55,31 +54,42 @@ module.exports = class RPC { errBuffer = Buffer.concat([errBuffer, data]) }) - // TODO: write buffer to standard output with ACL process.on('close', code => { return code === 0 ? resolve(errBuffer) : reject(errBuffer) }) }) } - forcePushHandler (req) { + async forcePushHandler (req) { const { url, repo, key, branch } = this.parsePushCommand(req) - // TODO: - // check ACL - // collect stdout to buffer and return it - // const process = spawn('git', ['reset', '--hard', url, branch], { env: { GIT_DIR: home.getCodePath(repo) } }) - console.error('req', req.toString()) - console.error('forcePushHandler not implemented') + // TODO: check ACL + return await new Promise((resolve, reject) => { + const process = spawn('git', ['fetch', url, `${branch}:${branch}`, '--force'], { env: { GIT_DIR: home.getCodePath(repo) } }) + let errBuffer = Buffer.from('') + process.stderr.on('data', data => { + errBuffer = Buffer.concat([errBuffer, data]) + }) + + process.on('close', code => { + return code === 0 ? resolve(errBuffer) : reject(errBuffer) + }) + }) } - deleteBranchHandler (req) { + async deleteBranchHandler (req) { const { url, repo, key, branch } = this.parsePushCommand(req) - // TODO: - // check ACL - // collect stdout to buffer and return it - // const process = spawn('git', ['branch', '-d', branch], { env: { GIT_DIR: home.getCodePath(repo) } }) - console.error('req', req.toString()) - console.error('deleteBranchHandler not implemented') + // TODO: check ACL + return await new Promise((resolve, reject) => { + const process = spawn('git', ['branch', '-D', branch], { env: { GIT_DIR: home.getCodePath(repo) } }) + let errBuffer = Buffer.from('') + process.stderr.on('data', data => { + errBuffer = Buffer.concat([errBuffer, data]) + }) + + process.on('close', code => { + return code === 0 ? resolve(errBuffer) : reject(errBuffer) + }) + }) } parsePushCommand(req) { From c414940c54354bf2593a3bc6eef669c697688eba Mon Sep 17 00:00:00 2001 From: dzdidi Date: Thu, 25 Jan 2024 07:55:38 +0000 Subject: [PATCH 37/60] start daemon on push Signed-off-by: dzdidi --- src/git-remote-pear.js | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index 166e6b0..5b794b4 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -28,21 +28,21 @@ const repoName = matches[2] const store = new Corestore(RAM) const swarm = new Hyperswarm({ keypair: home.getKeyPair() }) -// let daemonPid -// if (!home.isDaemonRunning()) { -// const opts = { -// detached: true, -// stdio: [ 'ignore', home.getOutStream(), home.getErrStream() ] -// } -// const daemon = spawn('git-peard', opts) -// daemonPid = daemon.pid -// home.storeDaemonPid(daemonPid) -// // TODO: remove in case of error or exit but allow unref -// // daemon.on('error', home.removeDaemonPid) -// // daemon.on('exit', home.removeDaemonPid) -// console.error('started daemon', daemonPid) -// daemon.unref() -// } +let daemonPid +if (!home.isDaemonRunning()) { + const opts = { + detached: true, + stdio: [ 'ignore', home.getOutStream(), home.getErrStream() ] + } + const daemon = spawn('git-peard', opts) + daemonPid = daemon.pid + home.storeDaemonPid(daemonPid) + // TODO: remove in case of error or exit but allow unref + // daemon.on('error', home.removeDaemonPid) + // daemon.on('exit', home.removeDaemonPid) + console.error('started daemon', daemonPid) + daemon.unref() +} swarm.join(crypto.discoveryKey(Buffer.from(targetKey, 'hex')), { server: false }) @@ -110,17 +110,20 @@ async function talkToGit (refs, drive, repoName, rpc) { await git.push(src, isForce) src = src.replace('+', '') command = 'f-push' + console.warn('To', url) } else { - console.warn('pushing', src, dst) await git.push(src) command = 'push' + console.warn('To', url) } const publicKey = home.readPk() const res = await rpc.request(command, Buffer.from(`${publicKey}/${repoName}:${dst}`)) - // process.kill(daemonPid || home.getDaemonPid()) - // home.removeDaemonPid() + if (daemonPid) { + process.kill(daemonPid) + home.removeDaemonPid() + } process.stdout.write('\n\n') process.exit(0) From d3c16cc4be383c1d36ff2caf73b19570114e637f Mon Sep 17 00:00:00 2001 From: dzdidi Date: Thu, 25 Jan 2024 07:57:33 +0000 Subject: [PATCH 38/60] exit if daemon is not running Signed-off-by: dzdidi --- src/git-remote-pear.js | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index 5b794b4..57fc805 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -28,20 +28,9 @@ const repoName = matches[2] const store = new Corestore(RAM) const swarm = new Hyperswarm({ keypair: home.getKeyPair() }) -let daemonPid if (!home.isDaemonRunning()) { - const opts = { - detached: true, - stdio: [ 'ignore', home.getOutStream(), home.getErrStream() ] - } - const daemon = spawn('git-peard', opts) - daemonPid = daemon.pid - home.storeDaemonPid(daemonPid) - // TODO: remove in case of error or exit but allow unref - // daemon.on('error', home.removeDaemonPid) - // daemon.on('exit', home.removeDaemonPid) - console.error('started daemon', daemonPid) - daemon.unref() + console.error('Please start git pear daemon') + process.exit(1) } swarm.join(crypto.discoveryKey(Buffer.from(targetKey, 'hex')), { server: false }) @@ -120,11 +109,6 @@ async function talkToGit (refs, drive, repoName, rpc) { const publicKey = home.readPk() const res = await rpc.request(command, Buffer.from(`${publicKey}/${repoName}:${dst}`)) - if (daemonPid) { - process.kill(daemonPid) - home.removeDaemonPid() - } - process.stdout.write('\n\n') process.exit(0) } else if (chunk && chunk.search(/^list/) !== -1) { // list && list for-push From 1837a4bae8497f71fb8f01305c3ace1e3dedcdba Mon Sep 17 00:00:00 2001 From: dzdidi Date: Thu, 25 Jan 2024 08:01:43 +0000 Subject: [PATCH 39/60] Add remote url to git push out Signed-off-by: dzdidi --- src/git-remote-pear.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index 57fc805..f140492 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -96,14 +96,14 @@ async function talkToGit (refs, drive, repoName, rpc) { if (isDelete) { command = 'd-branch' } else if (isForce) { + console.warn('To', url) await git.push(src, isForce) src = src.replace('+', '') command = 'f-push' - console.warn('To', url) } else { + console.warn('To', url) await git.push(src) command = 'push' - console.warn('To', url) } const publicKey = home.readPk() From 9d66b66221d49f6a9f07de5f528139807473689f Mon Sep 17 00:00:00 2001 From: dzdidi Date: Thu, 25 Jan 2024 17:22:17 +0000 Subject: [PATCH 40/60] ACL prep Signed-off-by: dzdidi --- Readme.md | 4 +- npm-shrinkwrap.json | 202 ++++++++++++++++++++++++++++++++++++++++- package.json | 1 + src/acl/index.js | 20 ++++ src/acl/nip98.js | 49 ++++++++++ src/cli.js | 2 + src/git-remote-pear.js | 41 +++++++-- src/git.js | 21 ++++- src/home.js | 7 +- src/rpc-request.js | 0 src/rpc.js | 79 ++++++++-------- 11 files changed, 376 insertions(+), 50 deletions(-) create mode 100644 src/acl/index.js create mode 100644 src/acl/nip98.js create mode 100644 src/rpc-request.js diff --git a/Readme.md b/Readme.md index dbdd75b..4b4fc19 100644 --- a/Readme.md +++ b/Readme.md @@ -48,7 +48,7 @@ All data will be persisted in application directory (default `~/.gitpear`). To c * `git pear list [-s, --shared]` - list all or (only shared) repositories -## Usage example +## Usage example (NO PUSH) Please not this is only remote helper and its intention is only to enable direct `clone|fetch|pull` of repository hosted on private computer. @@ -94,3 +94,5 @@ git checkout master git fetch origin git pull ``` + +## Usage example (PUSH) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 1700503..82ec373 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -15,6 +15,7 @@ "corestore": "^6.15.13", "hyperdrive": "^11.6.3", "hyperswarm": "^4.7.13", + "nostr-tools": "^2.1.5", "protomux-rpc": "^1.5.1", "random-access-memory": "^6.2.0" }, @@ -239,6 +240,47 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@noble/ciphers": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz", + "integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -274,6 +316,53 @@ "node": ">= 8" } }, + "node_modules/@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@scure/bip32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", + "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", + "dependencies": { + "@noble/curves": "~1.1.0", + "@noble/hashes": "~1.3.1", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -3008,6 +3097,36 @@ "node": ">=0.10.0" } }, + "node_modules/nostr-tools": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.1.5.tgz", + "integrity": "sha512-Gug/j54YGQ0ewB09dZW3mS9qfXWFlcOQMlyb1MmqQsuNO/95mfNOQSBi+jZ61O++Y+jG99SzAUPFLopUsKf0MA==", + "dependencies": { + "@noble/ciphers": "0.2.0", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.1", + "@scure/base": "1.1.1", + "@scure/bip32": "1.3.1", + "@scure/bip39": "1.2.1" + }, + "optionalDependencies": { + "nostr-wasm": "v0.1.0" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/nostr-wasm": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", + "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", + "optional": true + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4390,7 +4509,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4838,6 +4957,31 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@noble/ciphers": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz", + "integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==" + }, + "@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "requires": { + "@noble/hashes": "1.3.2" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==" + } + } + }, + "@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==" + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4864,6 +5008,40 @@ "fastq": "^1.6.0" } }, + "@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==" + }, + "@scure/bip32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", + "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", + "requires": { + "@noble/curves": "~1.1.0", + "@noble/hashes": "~1.3.1", + "@scure/base": "~1.1.0" + }, + "dependencies": { + "@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "requires": { + "@noble/hashes": "1.3.1" + } + } + } + }, + "@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "requires": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + } + }, "@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -6982,6 +7160,26 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, + "nostr-tools": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.1.5.tgz", + "integrity": "sha512-Gug/j54YGQ0ewB09dZW3mS9qfXWFlcOQMlyb1MmqQsuNO/95mfNOQSBi+jZ61O++Y+jG99SzAUPFLopUsKf0MA==", + "requires": { + "@noble/ciphers": "0.2.0", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.1", + "@scure/base": "1.1.1", + "@scure/bip32": "1.3.1", + "@scure/bip39": "1.2.1", + "nostr-wasm": "v0.1.0" + } + }, + "nostr-wasm": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", + "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", + "optional": true + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8028,7 +8226,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true + "devOptional": true }, "udx-native": { "version": "1.7.12", diff --git a/package.json b/package.json index 5299b9b..ed34376 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "corestore": "^6.15.13", "hyperdrive": "^11.6.3", "hyperswarm": "^4.7.13", + "nostr-tools": "^2.1.5", "protomux-rpc": "^1.5.1", "random-access-memory": "^6.2.0" } diff --git a/src/acl/index.js b/src/acl/index.js new file mode 100644 index 0000000..54037db --- /dev/null +++ b/src/acl/index.js @@ -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 +} diff --git a/src/acl/nip98.js b/src/acl/nip98.js new file mode 100644 index 0000000..f9f48b7 --- /dev/null +++ b/src/acl/nip98.js @@ -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 }}) +// })() diff --git a/src/cli.js b/src/cli.js index 71ab6e8..d6ad6b5 100755 --- a/src/cli.js +++ b/src/cli.js @@ -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) } diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index f140492..0cc08b3 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -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) diff --git a/src/git.js b/src/git.js index 3cb7c69..41bf38d 100644 --- a/src/git.js +++ b/src/git.js @@ -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 } diff --git a/src/home.js b/src/home.js index 921c4bc..a7db197 100644 --- a/src/home.js +++ b/src/home.js @@ -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, } diff --git a/src/rpc-request.js b/src/rpc-request.js new file mode 100644 index 0000000..e69de29 diff --git a/src/rpc.js b/src/rpc.js index 8ab4fd4..9b67540 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -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 - } } From 781ddb65dbfe977f9f0ffef11eb975d51160c982 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Thu, 25 Jan 2024 21:39:59 +0000 Subject: [PATCH 41/60] dummy acl Signed-off-by: dzdidi --- src/home.js | 5 +++++ src/rpc.js | 17 ++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/home.js b/src/home.js index a7db197..90e3c3d 100644 --- a/src/home.js +++ b/src/home.js @@ -30,6 +30,10 @@ function isShared (name) { return fs.existsSync(`${APP_HOME}/${name}/.git-daemon-export-ok`) } +function getACL (name) { + return fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`).toString().split('\n').filter(Boolean) +} + function list (sharedOnly) { const repos = fs.readdirSync(APP_HOME) if (!sharedOnly) return repos.filter(r => !r.startsWith('.') && isInitialized(r)) @@ -125,4 +129,5 @@ module.exports = { isDaemonRunning, removeDaemonPid, shareWith, + getACL, } diff --git a/src/rpc.js b/src/rpc.js index 9b67540..e872d8e 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -101,17 +101,24 @@ module.exports = class RPC { async parseReq(req) { let payload let request = JSON.parse(req.toString()) + const result = { + repoName: request.body.url?.split('/')?.pop(), + branch: request.body.data?.split('#')[0], + url: request.body.url + } if (process.env.GIT_PEAR_AUTH) { payload = await acl.getId({ ...request.body, payload: request.header }) + // read .git-daemon-export-ok + // check if payload.userId is presenet there + const aclList = home.getACL(result.repoName) + if (!aclList.includes(payload.userId)) { + throw new Error(`You are not allowed to access this repo: ${payload.userId}`) + } } - return { - repoName: request.body.url?.split('/')?.pop(), - branch: request.body.data?.split('#')[0], - url: request.body.url - } + return result } } From f5bbcdecd935fe362a939e620e1ec3f98560b424 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Fri, 26 Jan 2024 08:34:52 +0000 Subject: [PATCH 42/60] handle missing header Signed-off-by: dzdidi --- src/rpc.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/rpc.js b/src/rpc.js index e872d8e..9cb8ce0 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -107,6 +107,8 @@ module.exports = class RPC { url: request.body.url } if (process.env.GIT_PEAR_AUTH) { + if (!request.header) throw new Error('You are not allowed to access this repo') + payload = await acl.getId({ ...request.body, payload: request.header @@ -115,7 +117,7 @@ module.exports = class RPC { // check if payload.userId is presenet there const aclList = home.getACL(result.repoName) if (!aclList.includes(payload.userId)) { - throw new Error(`You are not allowed to access this repo: ${payload.userId}`) + throw new Error('You are not allowed to access this repo') } } From a8ae12f76a8475fbbd0e317b7c0a1465ff6df6b4 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Fri, 26 Jan 2024 12:30:12 +0000 Subject: [PATCH 43/60] granual persmissions Signed-off-by: dzdidi --- src/home.js | 15 +++++++++++++-- src/rpc.js | 10 +++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/home.js b/src/home.js index 90e3c3d..89a9767 100644 --- a/src/home.js +++ b/src/home.js @@ -15,7 +15,13 @@ function shareAppFolder (name) { } function shareWith (userId, branch = '*', permissions = 'rw') { - fs.appendFileSync(`${APP_HOME}/.git-daemon-export-ok`, `${userId}:${branch}:${permissions}\n`) + if (!fs.existsSync(`${APP_HOME}/.git-daemon-export-ok`)) { + fs.writeFileSync(`${APP_HOME}/.git-daemon-export-ok`, '') + } + if (permissions.split('').some(p => !['r', 'w'].includes(p))) { + throw new Error('Permissions must be r, w or rw') + } + fs.appendFileSync(`${APP_HOME}/.git-daemon-export-ok`, `${userId}\t${branch}\t${permissions}\n`) } function unshareAppFolder (name) { @@ -31,7 +37,12 @@ function isShared (name) { } function getACL (name) { - return fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`).toString().split('\n').filter(Boolean) + const entries = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`).toString().split('\n').filter(Boolean) + const res = {} + for (const entry of entries) { + const [userId, branch, permissions] = entry.split('\t') + res[userId] = { branch, permissions } + } } function list (sharedOnly) { diff --git a/src/rpc.js b/src/rpc.js index 9cb8ce0..e24ccca 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -109,14 +109,10 @@ module.exports = class RPC { if (process.env.GIT_PEAR_AUTH) { if (!request.header) throw new Error('You are not allowed to access this repo') - payload = await acl.getId({ - ...request.body, - payload: request.header - }) - // read .git-daemon-export-ok - // check if payload.userId is presenet there + payload = await acl.getId({ ...request.body, payload: request.header }) const aclList = home.getACL(result.repoName) - if (!aclList.includes(payload.userId)) { + // TODO: read specific permissions for the user + if (!Object.keys(aclList).includes(payload.userId)) { throw new Error('You are not allowed to access this repo') } } From 70b3a6c585a1ccdc42bba216f90e2ae84128fceb Mon Sep 17 00:00:00 2001 From: dzdidi Date: Fri, 26 Jan 2024 12:32:01 +0000 Subject: [PATCH 44/60] granual persmissions: fix return Signed-off-by: dzdidi --- src/home.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/home.js b/src/home.js index 89a9767..078ec8d 100644 --- a/src/home.js +++ b/src/home.js @@ -43,6 +43,7 @@ function getACL (name) { const [userId, branch, permissions] = entry.split('\t') res[userId] = { branch, permissions } } + return res } function list (sharedOnly) { From 0911049fa502b5ccfb49cdb06e43d8fe60d8468c Mon Sep 17 00:00:00 2001 From: dzdidi Date: Fri, 26 Jan 2024 15:02:37 +0000 Subject: [PATCH 45/60] advanced acl: draft Signed-off-by: dzdidi --- src/home.js | 17 ++++++++++++++++- src/rpc.js | 21 ++++++++------------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/home.js b/src/home.js index 078ec8d..24615d2 100644 --- a/src/home.js +++ b/src/home.js @@ -21,6 +21,19 @@ function shareWith (userId, branch = '*', permissions = 'rw') { if (permissions.split('').some(p => !['r', 'w'].includes(p))) { throw new Error('Permissions must be r, w or rw') } + // TODO: read file + // generate new conent + // merge with old file + // store file + // + // EXAMPLE: + // { + // protectedBranches: ['master'], + // ACL: { + // '': { '': 'r|w|rw' }, + // '*': { '*': 'r' } + // } + // } fs.appendFileSync(`${APP_HOME}/.git-daemon-export-ok`, `${userId}\t${branch}\t${permissions}\n`) } @@ -41,9 +54,11 @@ function getACL (name) { const res = {} for (const entry of entries) { const [userId, branch, permissions] = entry.split('\t') - res[userId] = { branch, permissions } + if (!res[userId]) res[userId] = [] + res[userId].push({ branch, permissions }) } return res + // TODO: have protected branch setting - the ACL must be assigned explicitly } function list (sharedOnly) { diff --git a/src/rpc.js b/src/rpc.js index e24ccca..253c644 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -98,7 +98,7 @@ module.exports = class RPC { }) } - async parseReq(req) { + async parseReq(req, access, branch = '*') { let payload let request = JSON.parse(req.toString()) const result = { @@ -106,17 +106,12 @@ module.exports = class RPC { branch: request.body.data?.split('#')[0], url: request.body.url } - if (process.env.GIT_PEAR_AUTH) { - if (!request.header) throw new Error('You are not allowed to access this repo') + if (!process.env.GIT_PEAR_AUTH) return result + if (!request.header) throw new Error('You are not allowed to access this repo') - payload = await acl.getId({ ...request.body, payload: request.header }) - const aclList = home.getACL(result.repoName) - // TODO: read specific permissions for the user - if (!Object.keys(aclList).includes(payload.userId)) { - throw new Error('You are not allowed to access this repo') - } - } + payload = await acl.getId({ ...request.body, payload: request.header }) + const aclList = home.getACL(result.repoName) + const userACL = aclList[payload.userId] + if (!userACL) throw new Error('You are not allowed to access this repo') - return result - } -} + if (result.branch !== 'master' From fff32c6f3ea6ec72bd80c458e292fe3f26a9e7b2 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Sun, 28 Jan 2024 11:22:43 +0000 Subject: [PATCH 46/60] acl: more drafts Signed-off-by: dzdidi --- src/cli.js | 1 + src/home.js | 67 +++++++++++++++++++++++----------------------- src/rpc.js | 69 +++++++++++++++++++++++++++++++----------------- test/rpc.test.js | 6 +++-- 4 files changed, 84 insertions(+), 59 deletions(-) diff --git a/src/cli.js b/src/cli.js index d6ad6b5..4cfb7e3 100755 --- a/src/cli.js +++ b/src/cli.js @@ -68,6 +68,7 @@ program process.exit(1) }) + program .command('unshare') .description('unshare a gitpear repo') diff --git a/src/home.js b/src/home.js index 24615d2..9d2d1aa 100644 --- a/src/home.js +++ b/src/home.js @@ -10,31 +10,37 @@ function createAppFolder (name) { fs.mkdirSync(`${APP_HOME}/${name}/code`, { recursive: true }) } -function shareAppFolder (name) { - fs.openSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'w') +function shareAppFolder (name, entry) { + const p = `${APP_HOME}/${name}/.git-daemon-export-ok` + fs.openSync(p, 'a') + const aclFile = fs.readFileSync(p, 'utf8') + const aclJson = JSON.parse(aclFile || '{ "protectedBranches": ["master"], "ACL": {}}') + + let [userId = '*', permissions = 'r', branch = '*'] = entry?.split(':') || [] + + if (!aclJson.ACL[userId]) aclJson[userId] = { [branch]: permissions } + fs.writeFileSync(p, JSON.stringify(aclJson)) } -function shareWith (userId, branch = '*', permissions = 'rw') { - if (!fs.existsSync(`${APP_HOME}/.git-daemon-export-ok`)) { - fs.writeFileSync(`${APP_HOME}/.git-daemon-export-ok`, '') - } - if (permissions.split('').some(p => !['r', 'w'].includes(p))) { - throw new Error('Permissions must be r, w or rw') - } - // TODO: read file - // generate new conent - // merge with old file - // store file - // - // EXAMPLE: - // { - // protectedBranches: ['master'], - // ACL: { - // '': { '': 'r|w|rw' }, - // '*': { '*': 'r' } - // } - // } - fs.appendFileSync(`${APP_HOME}/.git-daemon-export-ok`, `${userId}\t${branch}\t${permissions}\n`) +function addProtectedBranch (name, branch) { + const aclFile = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'utf8') + const aclJson = JSON.parse(aclFile || '{ "protectedBranches": [], "ACL": {}}') + if (!aclJson.protectedBranches.includes(branch)) aclJson.protectedBranches.push(branch) + fs.writeFileSync(aclFile, JSON.stringify(aclJson)) +} + +function removeProtectedBranch (name, branch) { + const aclFile = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'a') + const aclJson = JSON.parse(aclFile || '{ "protectedBranches": [], "ACL": {}}') + aclJson.protectedBranches = aclJson.protectedBranches.filter(b => b !== branch) + fs.writeFileSync(aclFile, JSON.stringify(aclJson)) +} + +function removeUserFromACL (name, userId) { + const aclFile = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'a') + const aclJson = JSON.parse(aclFile || '{ "protectedBranches": [], "ACL": {}}') + delete aclJson.ACL[userId] + fs.writeFileSync(aclFile, JSON.stringify(aclJson)) } function unshareAppFolder (name) { @@ -50,15 +56,11 @@ function isShared (name) { } function getACL (name) { - const entries = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`).toString().split('\n').filter(Boolean) - const res = {} - for (const entry of entries) { - const [userId, branch, permissions] = entry.split('\t') - if (!res[userId]) res[userId] = [] - res[userId].push({ branch, permissions }) - } - return res - // TODO: have protected branch setting - the ACL must be assigned explicitly + if (!fs.existsSync(`${APP_HOME}/${name}/.git-daemon-export-ok`)) throw new Error('Repo is not shared') + + const aclFile = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'r') + aclJson = JSON.parse(aclFile || '{ "protectedBranches": [], "ACL": {}}') + return aclJson } function list (sharedOnly) { @@ -155,6 +157,5 @@ module.exports = { getDaemonPid, isDaemonRunning, removeDaemonPid, - shareWith, getACL, } diff --git a/src/rpc.js b/src/rpc.js index 253c644..9ba949b 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -20,19 +20,19 @@ module.exports = class RPC { // which can in turn be stored in a .git-daemon-export-ok file /* -- PULL HANDLERS -- */ - rpc.respond('get-repos', async req => await this.getReposHandler(req)) - rpc.respond('get-refs', async req => await this.getRefsHandler(req)) + rpc.respond('get-repos', async req => await this.getReposHandler(peerInfo.publicKey, req)) + rpc.respond('get-refs', async req => await this.getRefsHandler(peerInfo.publicKey, req)) /* -- PUSH HANDLERS -- */ - 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)) + rpc.respond('push', async req => await this.pushHandler(peerInfo.publicKey, req)) + rpc.respond('f-push', async req => await this.forcePushHandler(peerInfo.publicKey, req)) + rpc.respond('d-branch', async req => await this.deleteBranchHandler(peerInfo.publicKey, req)) this.connections[peerInfo.publicKey] = rpc } - async getReposHandler (req) { - const { branch, url } = await this.parseReq(req) + async getReposHandler (publicKey, req) { + const { branch, url } = await this.parseReq(publicKey, req, 'r') const res = {} for (const repoName in this.repositories) { @@ -43,15 +43,15 @@ module.exports = class RPC { return Buffer.from(JSON.stringify(res)) } - async getRefsHandler (req) { - const { repoName, branch, url } = await this.parseReq(req) + async getRefsHandler (publicKey, req) { + const { repoName, branch, url } = await this.parseReq(publicKey, req, 'r') const res = this.repositories[repoName] return Buffer.from(JSON.stringify(res)) } - async pushHandler (req) { - const { url, repoName, branch } = await this.parseReq(req) + async pushHandler (publicKey, req) { + const { url, repoName, branch } = await this.parseReq(publicKey, req, 'w') return await new Promise((resolve, reject) => { const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) } const child = spawn('git', ['fetch', url, `${branch}:${branch}`], { env }) @@ -66,8 +66,8 @@ module.exports = class RPC { }) } - async forcePushHandler (req) { - const { url, repoName, branch } = await this.parseReq(req) + async forcePushHandler (publicKey, req) { + const { url, repoName, branch } = await this.parseReq(publicKey, req, 'w') return await new Promise((resolve, reject) => { const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) } const child = spawn('git', ['fetch', url, `${branch}:${branch}`, '--force'], { env }) @@ -82,8 +82,8 @@ module.exports = class RPC { }) } - async deleteBranchHandler (req) { - const { url, repoName, branch } = await this.parseReq(req) + async deleteBranchHandler (publicKey, req) { + const { url, repoName, branch } = await this.parseReq(publicKey, req, 'w') return await new Promise((resolve, reject) => { const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) } const child = spawn('git', ['branch', '-D', branch], { env }) @@ -98,20 +98,41 @@ module.exports = class RPC { }) } - async parseReq(req, access, branch = '*') { - let payload + async parseReq(publicKey, req, access, branch = '*') { + if (!req) throw new Error('Request is empty') let request = JSON.parse(req.toString()) - const result = { + const parsed = { repoName: request.body.url?.split('/')?.pop(), branch: request.body.data?.split('#')[0], url: request.body.url } - if (!process.env.GIT_PEAR_AUTH) return result - if (!request.header) throw new Error('You are not allowed to access this repo') + if (!process.env.GIT_PEAR_AUTH) return parsed - payload = await acl.getId({ ...request.body, payload: request.header }) - const aclList = home.getACL(result.repoName) - const userACL = aclList[payload.userId] + if (process.env.GIT_PEAR_AUTH !== 'naitive' && !request.header) { + throw new Error('You are not allowed to access this repo') + } + + let userId + if (process.env.GIT_PEAR_AUTH === 'naitive') { + userId = publicKey + } else { + userId = (await acl.getId({ ...request.body, payload: request.header })).userId + } + const aclObj = home.getACL(parsed.repoName) + const userACL = aclObj[userId] || aclObj['*'] if (!userACL) throw new Error('You are not allowed to access this repo') - if (result.branch !== 'master' + if (aclObj.protectecBranches.includes(branch)) { + // protected branch must have exaplicit access grant + if (access === 'w') { + + } else { + // + } + } else { + + } + + return parsed + } +} diff --git a/test/rpc.test.js b/test/rpc.test.js index 9b9a7d2..37ee5e3 100644 --- a/test/rpc.test.js +++ b/test/rpc.test.js @@ -40,7 +40,8 @@ test('e2e', async t => { clientStore.replicate(socket) const rpc = new ProtomuxRPC(socket) - const reposRes = await rpc.request('get-repos') + let payload = Buffer.from(JSON.stringify({ body: { url, method: 'get-repos' } })) + const reposRes = await rpc.request('get-repos', payload) const reposJSON = JSON.parse(reposRes.toString()) const driveKey = Buffer.from(reposJSON.foo, 'hex') @@ -53,7 +54,8 @@ test('e2e', async t => { await drive.core.update({ wait: true }) - const refsRes = await rpc.request('get-refs', Buffer.from('foo')) + payload = Buffer.from(JSON.stringify({ body: { url, method: 'get-refs', data: 'foo' }})) + const refsRes = await rpc.request('get-refs', payload) t.ok(refsRes) const want = Object.values(JSON.parse(refsRes.toString()))[0] From b18f5ca8fe0b3afe42121d41fbd2593da123b6f5 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Sun, 28 Jan 2024 11:44:45 +0000 Subject: [PATCH 47/60] fix tests Signed-off-by: dzdidi --- test/rpc.test.js | 4 +++- test_home.tar.gz | Bin 39154 -> 2103 bytes 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/rpc.test.js b/test/rpc.test.js index 37ee5e3..fc90b99 100644 --- a/test/rpc.test.js +++ b/test/rpc.test.js @@ -40,6 +40,8 @@ test('e2e', async t => { clientStore.replicate(socket) const rpc = new ProtomuxRPC(socket) + const repoName = 'foo' + const url = `${serverSwarm.keyPair.publicKey.toString('hex')}/${repoName}` let payload = Buffer.from(JSON.stringify({ body: { url, method: 'get-repos' } })) const reposRes = await rpc.request('get-repos', payload) const reposJSON = JSON.parse(reposRes.toString()) @@ -54,7 +56,7 @@ test('e2e', async t => { await drive.core.update({ wait: true }) - payload = Buffer.from(JSON.stringify({ body: { url, method: 'get-refs', data: 'foo' }})) + payload = Buffer.from(JSON.stringify({ body: { url, method: 'get-refs', data: repoName }})) const refsRes = await rpc.request('get-refs', payload) t.ok(refsRes) diff --git a/test_home.tar.gz b/test_home.tar.gz index e1291c94c7e06c9260cc807eae387c38d89f58b6..826e8efd55dd0df8f8fc98280c23d41742c4c66e 100644 GIT binary patch literal 2103 zcmV-72*~#ziwFP!000001ML}IZ`(Ms&-oR6SU?mY%O9~*AV3e-O?${*(Z)y?*oPw6 zDl!$TmL)GqCG{Hq?>9s0gOcso$u{ZT9_PgtIbWO^&J0P(gzUb0Ug!b59*yv~|9aFj ze@l4PAC5+&;ql2}2zY%pO-9eGmx5f#fD`hHe~Tpxr#tcX&Hs;rME~E$(#JpkSMeM& z(7`9e;V%Cl4`8l89G;xK?vIX@|A)QN@D=Gjhca#WKlgu_r9u*PK6yhp`y-Dzqs|SF zrA(OP9_jf=n6r_nCKiF5np5}bn=^CP_q&3JT^xo`tMMTIwxD5DYAjTbI*8RF zi5T;29t-ApBoNCqBtZKQvN|F}Gvf@E4eongdd8L{PEi9_DMUyd5sIU4-n=0W zGI3l~Q^sk|jueKH4=6syp%lNxa!MV}A_2O(1;(aApMm0fheGS4b<1Y?BFTcuI0$Ei z-e+VQ+%b34e9~VeK}wI^%9JaDDwFkt_AHj5&DZh*;@83zfk#S1qg}I$xT}XTfBN*lwwIDN#t6n}g6u_#yn2>^F#FcBO`?)19!pZki{FqpgYz zg`8wLEIVE>s>$`7{;%YPNv1-L7Md*rA*j*_jQpUTD>JW%n8k}zB6-fp`T(Nfz))cArwy;lQ5H4`uD*9^ov^xrpyrxlw8l{^%O{cLvq}DbGA# z1GumMJ2~mM`@g}!_J1z|Wm!=j;QD2IK%=!|64K;3j(KJ>hD6oK8fHRoWmk`gDI`3P z#6tg@cqC7&v0u_y#z7MQiQ@?*K97LVGstLTRM(KYJ9R%XeKtyinfDcPEUuKcr3m+_ zl>x>0kWM?3fX%bC!@eytE<4$*l&gr8L3Xd}0hOMx>iy~z9@?SMkFir*t`RyL7TJap zWtmfPO5z!#DZ3|EtYC=22mOgh%F)k?0<^jbIR5+zKiB2@cF<#q+{xp*DP5TZnyD#ava2bd)m-=wB@dv3TM`461vEx z?nV{w_nhmmCih3i9QLnCc3U)Q4K*rp^znPlWcw7g80Wjxs@DAkwM@1T21cGHaXLGA zk&hy)mDo51F!@ex`*93)fWyB#Rt;=*b7_+q`H9X{2u*6k$ zwNjUYMsDMJ&=1ly%hQleE?FeD737PXuPl`QU6ABVY#}i!bhU(*3>+6RC=j!UM9kPy zY~mllf5M@X3N>cjR3dh9RoUcNBnYT-x`F)Bk!|4+Q2C!P!e@W~ zIoX;2N4>$Q_5Nqye_jd>%m0TZYmKXq&c6c3lr@aWL^} zzD-1xnl=#CXoWcDnat+-x6AQa;|`-3?!eZl%TNocsDQY8jQV$2tt>Awkc8(Uta4}N zT7#c$PNTqJ88kCAe(Qu>KLY@LQ4wN+AI%a41Wk^viE8q0i)tWhp>659lHoio zC518;!;0XD*J#PvU`x^7d=!yYKRG<;l+%6O76g!HJrcLjDeMpi(?Y2u|zhUALnm+7}ejMobj#yP|A3={`s? z$2Jc(jd+RL;o^SoU7IZQJyE5PHkKZwU%KGknSgzRg4RBMeKAzhXhm{5+TvK1oeuD2 z^X{ogF-CIPOl}h}7|+Ux+KdBpz9?XtZ3M*=Q&~}7P)uFD#s1o|BCU$tth&0Q8da=1 zzA9e&>0RbLEmj8pKdst@130^0T}$-Y%^uw>Sg>Hhf&~i}ELgB$!GZ+~7A#n>V8Ma~ h3l=O`uwcQ01q&7|Sg>Hhf`$KX_zwjk7n%T2005u{4g3HA literal 39154 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 From e8cbdc5e648c11a2766cf8707ac352e75d52e8d8 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Sun, 28 Jan 2024 12:18:26 +0000 Subject: [PATCH 48/60] fix tests no overwrite Signed-off-by: dzdidi --- src/home.js | 4 ++-- src/rpc-request.js | 0 test/home.test.js | 26 +++++++++++++------------- test_home.tar.gz | Bin 2103 -> 38160 bytes 4 files changed, 15 insertions(+), 15 deletions(-) delete mode 100644 src/rpc-request.js diff --git a/src/home.js b/src/home.js index 9d2d1aa..4ac2dad 100644 --- a/src/home.js +++ b/src/home.js @@ -18,7 +18,7 @@ function shareAppFolder (name, entry) { let [userId = '*', permissions = 'r', branch = '*'] = entry?.split(':') || [] - if (!aclJson.ACL[userId]) aclJson[userId] = { [branch]: permissions } + if (!aclJson.ACL[userId]) aclJson.ACL[userId] = { [branch]: permissions } fs.writeFileSync(p, JSON.stringify(aclJson)) } @@ -58,7 +58,7 @@ function isShared (name) { function getACL (name) { if (!fs.existsSync(`${APP_HOME}/${name}/.git-daemon-export-ok`)) throw new Error('Repo is not shared') - const aclFile = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'r') + const aclFile = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'utf8') aclJson = JSON.parse(aclFile || '{ "protectedBranches": [], "ACL": {}}') return aclJson } diff --git a/src/rpc-request.js b/src/rpc-request.js deleted file mode 100644 index e69de29..0000000 diff --git a/test/home.test.js b/test/home.test.js index 115b014..2381886 100644 --- a/test/home.test.js +++ b/test/home.test.js @@ -9,33 +9,33 @@ test('getAppHome', t => { }) test('createAppFolder, share, is shared, unshare, isInitialized, list, getCodePath', t => { - home.createAppFolder('test') + home.createAppFolder('test_code') - t.ok(fs.existsSync(path.join(home.APP_HOME, 'test', 'code'))) + t.ok(fs.existsSync(path.join(home.APP_HOME, 'test_code', 'code'))) - t.absent(home.isShared('test')) - t.absent(fs.existsSync(path.join(home.APP_HOME, 'test', '.git-daemon-export-ok'))) + t.absent(home.isShared('test_code')) + t.absent(fs.existsSync(path.join(home.APP_HOME, 'test_code', '.git-daemon-export-ok'))) - home.shareAppFolder('test') + home.shareAppFolder('test_code') - t.ok(home.isShared('test')) - t.ok(fs.existsSync(path.join(home.APP_HOME, 'test', '.git-daemon-export-ok'))) + t.ok(home.isShared('test_code')) + t.ok(fs.existsSync(path.join(home.APP_HOME, 'test_code', '.git-daemon-export-ok'))) - home.unshareAppFolder('test') + home.unshareAppFolder('test_code') - t.absent(home.isShared('test')) - t.absent(fs.existsSync(path.join(home.APP_HOME, 'test', '.git-daemon-export-ok'))) + t.absent(home.isShared('test_code')) + t.absent(fs.existsSync(path.join(home.APP_HOME, 'test_code', '.git-daemon-export-ok'))) - t.absent(home.isInitialized('test')) + t.absent(home.isInitialized('test_code')) t.ok(home.isInitialized('foo')) t.alike(new Set(home.list()), new Set(['foo', 'bar', 'zar'])) t.alike(new Set(home.list(true)), new Set(['foo', 'bar'])) - t.alike(path.resolve(home.getCodePath('test')), path.resolve(path.join(home.APP_HOME, 'test', 'code'))) + t.alike(path.resolve(home.getCodePath('test_code')), path.resolve(path.join(home.APP_HOME, 'test_code', 'code'))) t.teardown(() => { - fs.rmSync(path.join(home.APP_HOME, 'test', 'code'), { recursive: true }) + fs.rmSync(path.join(home.APP_HOME, 'test_code'), { recursive: true }) }) }) diff --git a/test_home.tar.gz b/test_home.tar.gz index 826e8efd55dd0df8f8fc98280c23d41742c4c66e..cdbf5e48769a95835f3cabcbd387798d94e640b1 100644 GIT binary patch literal 38160 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 From f466308755fb503c0939989633d741a9e4ec8312 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Sun, 28 Jan 2024 14:02:55 +0000 Subject: [PATCH 49/60] clean Signed-off-by: dzdidi --- src/acl.js | 20 ++++++++++++++++++++ src/{acl => auth}/index.js | 0 src/{acl => auth}/nip98.js | 0 src/git-remote-pear.js | 9 ++++----- src/home.js | 36 ++---------------------------------- src/rpc.js | 16 +++------------- 6 files changed, 29 insertions(+), 52 deletions(-) create mode 100644 src/acl.js rename src/{acl => auth}/index.js (100%) rename src/{acl => auth}/nip98.js (100%) diff --git a/src/acl.js b/src/acl.js new file mode 100644 index 0000000..4216d19 --- /dev/null +++ b/src/acl.js @@ -0,0 +1,20 @@ +const home = require('./home') + +const roles = { + admin: { + description: 'Read and write to all branches', + }, + contributor: { + description: 'Read and write to all branches except protected ones', + }, + viewer: { + description: 'Read all branches', + }, +} +const DEFAULT_ACL = { + visibibility: 'public', // public|private + protectedBranches: ['master'], + ACL: {} +} + + diff --git a/src/acl/index.js b/src/auth/index.js similarity index 100% rename from src/acl/index.js rename to src/auth/index.js diff --git a/src/acl/nip98.js b/src/auth/nip98.js similarity index 100% rename from src/acl/nip98.js rename to src/auth/nip98.js diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index 0cc08b3..4824212 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -11,7 +11,7 @@ const crypto = require('hypercore-crypto') const git = require('./git.js') const home = require('./home') -const acl = require('./acl') +const auth = require('./auth') const fs = require('fs') @@ -42,7 +42,7 @@ swarm.on('connection', async (socket) => { let payload = { body: { url, method: 'get-repos' } } if (process.env.GIT_PEAR_AUTH) { - payload.header = await acl.getToken(payload.body) + payload.header = await auth.getToken(payload.body) } const reposRes = await rpc.request('get-repos', Buffer.from(JSON.stringify(payload))) @@ -71,10 +71,9 @@ swarm.on('connection', async (socket) => { await drive.core.update({ wait: true }) - // TODO: ACL payload = { body: { url, method: 'get-refs', data: repoName }} if (process.env.GIT_PEAR_AUTH) { - payload.header = await acl.getToken(payload.body) + payload.header = await auth.getToken(payload.body) } const refsRes = await rpc.request('get-refs', Buffer.from(JSON.stringify(payload))) @@ -128,7 +127,7 @@ async function talkToGit (refs, drive, repoName, rpc, commit) { method } } if (process.env.GIT_PEAR_AUTH) { - payload.header = await acl.getToken(payload.body) + payload.header = await auth.getToken(payload.body) } const res = await rpc.request(method, Buffer.from(JSON.stringify(payload))) diff --git a/src/home.js b/src/home.js index 4ac2dad..e2d8162 100644 --- a/src/home.js +++ b/src/home.js @@ -10,38 +10,10 @@ function createAppFolder (name) { fs.mkdirSync(`${APP_HOME}/${name}/code`, { recursive: true }) } -function shareAppFolder (name, entry) { - const p = `${APP_HOME}/${name}/.git-daemon-export-ok` - fs.openSync(p, 'a') - const aclFile = fs.readFileSync(p, 'utf8') - const aclJson = JSON.parse(aclFile || '{ "protectedBranches": ["master"], "ACL": {}}') - - let [userId = '*', permissions = 'r', branch = '*'] = entry?.split(':') || [] - - if (!aclJson.ACL[userId]) aclJson.ACL[userId] = { [branch]: permissions } - fs.writeFileSync(p, JSON.stringify(aclJson)) +function shareAppFolder (name) { + fs.openSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'w') } -function addProtectedBranch (name, branch) { - const aclFile = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'utf8') - const aclJson = JSON.parse(aclFile || '{ "protectedBranches": [], "ACL": {}}') - if (!aclJson.protectedBranches.includes(branch)) aclJson.protectedBranches.push(branch) - fs.writeFileSync(aclFile, JSON.stringify(aclJson)) -} - -function removeProtectedBranch (name, branch) { - const aclFile = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'a') - const aclJson = JSON.parse(aclFile || '{ "protectedBranches": [], "ACL": {}}') - aclJson.protectedBranches = aclJson.protectedBranches.filter(b => b !== branch) - fs.writeFileSync(aclFile, JSON.stringify(aclJson)) -} - -function removeUserFromACL (name, userId) { - const aclFile = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'a') - const aclJson = JSON.parse(aclFile || '{ "protectedBranches": [], "ACL": {}}') - delete aclJson.ACL[userId] - fs.writeFileSync(aclFile, JSON.stringify(aclJson)) -} function unshareAppFolder (name) { fs.unlinkSync(`${APP_HOME}/${name}/.git-daemon-export-ok`) @@ -57,10 +29,6 @@ function isShared (name) { function getACL (name) { if (!fs.existsSync(`${APP_HOME}/${name}/.git-daemon-export-ok`)) throw new Error('Repo is not shared') - - const aclFile = fs.readFileSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'utf8') - aclJson = JSON.parse(aclFile || '{ "protectedBranches": [], "ACL": {}}') - return aclJson } function list (sharedOnly) { diff --git a/src/rpc.js b/src/rpc.js index 9ba949b..b089e19 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -1,7 +1,7 @@ const ProtomuxRPC = require('protomux-rpc') const { spawn } = require('child_process') const home = require('./home') -const acl = require('./acl') +const auth = require('./auth') module.exports = class RPC { constructor (announcedRefs, repositories, drives) { @@ -98,7 +98,7 @@ module.exports = class RPC { }) } - async parseReq(publicKey, req, access, branch = '*') { + async parseReq(publicKey, req) { if (!req) throw new Error('Request is empty') let request = JSON.parse(req.toString()) const parsed = { @@ -116,22 +116,12 @@ module.exports = class RPC { if (process.env.GIT_PEAR_AUTH === 'naitive') { userId = publicKey } else { - userId = (await acl.getId({ ...request.body, payload: request.header })).userId + userId = (await auth.getId({ ...request.body, payload: request.header })).userId } const aclObj = home.getACL(parsed.repoName) const userACL = aclObj[userId] || aclObj['*'] if (!userACL) throw new Error('You are not allowed to access this repo') - if (aclObj.protectecBranches.includes(branch)) { - // protected branch must have exaplicit access grant - if (access === 'w') { - - } else { - // - } - } else { - - } return parsed } From 5c743fabc2e1813430ba696faa95c6c121d38bce Mon Sep 17 00:00:00 2001 From: dzdidi Date: Sun, 28 Jan 2024 15:33:57 +0000 Subject: [PATCH 50/60] ACL draft integration Signed-off-by: dzdidi --- src/acl.js | 96 +++++++++++++++++++++++++++++++++++++++++++++++- src/home.js | 10 ++--- src/rpc.js | 83 ++++++++++++++++++++++++++++++----------- test/acl.test.js | 59 +++++++++++++++++++++++++++++ 4 files changed, 220 insertions(+), 28 deletions(-) create mode 100644 test/acl.test.js diff --git a/src/acl.js b/src/acl.js index 4216d19..4becc35 100644 --- a/src/acl.js +++ b/src/acl.js @@ -1,6 +1,7 @@ const home = require('./home') +const fs = require('fs') -const roles = { +const ROLES = { admin: { description: 'Read and write to all branches', }, @@ -12,9 +13,100 @@ const roles = { }, } const DEFAULT_ACL = { - visibibility: 'public', // public|private + visibility: 'public', // public|private protectedBranches: ['master'], ACL: {} } +function getUserRole (repoName, user) { + const acl = getACL(repoName) + return acl.ACL[user] +} +function getAdmins (repoName) { + const acl = getACL(repoName) + return Object.keys(acl.ACL).filter(user => acl.ACL[user] === 'admin') +} + +function getContributors (repoName) { + const acl = getACL(repoName) + const contributors = Object.keys(acl.ACL).filter(user => acl.ACL[user] === 'contributor') + const admins = getAdmins(repoName) + return [...contributors, ...admins].filter((user, i, arr) => arr.indexOf(user) === i) +} + +function getViewers (repoName) { + const acl = getACL(repoName) + const viewers = Object.keys(acl.ACL).filter(user => acl.ACL[user] === 'viewer') + const contributors = getContributors(repoName) + const admins = getAdmins(repoName) + + return [...viewers, ...contributors, ...admins].filter((user, i, arr) => arr.indexOf(user) === i) +} + +function grantAccessToUser (repoName, user, role) { + if (!ROLES[role]) throw new Error(`Role ${role} does not exist`) + if (!Object.keys(ROLES).includes(role)) throw new Error(`Role ${role} is not allowed`) + + const acl = getACL(repoName) + acl.ACL[user] = role + setACL(repoName, acl) +} + +function addProtectedBranch (repoName, branch) { + const acl = getACL(repoName) + acl.protectedBranches.push(branch) + setACL(repoName, acl) +} + +function removeProtectedBranch (repoName, branch) { + const acl = getACL(repoName) + acl.protectedBranches = acl.protectedBranches.filter(b => b !== branch) + setACL(repoName, acl) +} + +function makeRepoPublic (repoName) { + const acl = getACL(repoName) + acl.visibility = 'public' + setACL(repoName, acl) +} + +function getRepoVisibility (repoName) { + const acl = getACL(repoName) + return acl.visibility +} + +function makeRepoPrivate (repoName) { + const acl = getACL(repoName) + acl.visibility = 'private' + setACL(repoName, acl) +} + +function setACL (repoName, acl = DEFAULT_ACL) { + acl = { ...DEFAULT_ACL, ...acl } + if (['public', 'private'].indexOf(acl.visibility) === -1) throw new Error('Visibility must be public or private') + + const content = JSON.stringify(acl, null, 2) + fs.writeFileSync(home.getACLFilePath(repoName), content) + + return acl +} + +function getACL (repoName) { + return JSON.parse(fs.readFileSync(home.getACLFilePath(repoName), 'utf8') || JSON.stringify(DEFAULT_ACL)) +} + +module.exports = { + getUserRole, + grantAccessToUser, + makeRepoPublic, + makeRepoPrivate, + getRepoVisibility, + setACL, + getACL, + addProtectedBranch, + removeProtectedBranch, + getAdmins, + getContributors, + getViewers, +} diff --git a/src/home.js b/src/home.js index e2d8162..37d6036 100644 --- a/src/home.js +++ b/src/home.js @@ -14,6 +14,10 @@ function shareAppFolder (name) { fs.openSync(`${APP_HOME}/${name}/.git-daemon-export-ok`, 'w') } +function getACLFilePath (name) { + if (!fs.existsSync(`${APP_HOME}/${name}/.git-daemon-export-ok`)) throw new Error('Repo is not shared') + return `${APP_HOME}/${name}/.git-daemon-export-ok` +} function unshareAppFolder (name) { fs.unlinkSync(`${APP_HOME}/${name}/.git-daemon-export-ok`) @@ -27,10 +31,6 @@ function isShared (name) { return fs.existsSync(`${APP_HOME}/${name}/.git-daemon-export-ok`) } -function getACL (name) { - if (!fs.existsSync(`${APP_HOME}/${name}/.git-daemon-export-ok`)) throw new Error('Repo is not shared') -} - function list (sharedOnly) { const repos = fs.readdirSync(APP_HOME) if (!sharedOnly) return repos.filter(r => !r.startsWith('.') && isInitialized(r)) @@ -125,5 +125,5 @@ module.exports = { getDaemonPid, isDaemonRunning, removeDaemonPid, - getACL, + getACLFilePath } diff --git a/src/rpc.js b/src/rpc.js index b089e19..c8d754b 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -2,6 +2,7 @@ const ProtomuxRPC = require('protomux-rpc') const { spawn } = require('child_process') const home = require('./home') const auth = require('./auth') +const acl = require('./acl') module.exports = class RPC { constructor (announcedRefs, repositories, drives) { @@ -32,26 +33,47 @@ module.exports = class RPC { } async getReposHandler (publicKey, req) { - const { branch, url } = await this.parseReq(publicKey, req, 'r') + const { branch, url, userId } = await this.parseReq(publicKey, req) const res = {} 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') + const isPublic = (acl.getACL(repoName).visibility === 'public') + if (isPublic || acl.getViewers(repoName).includes(userId)) { + res[repoName] = this.drives[repoName].key.toString('hex') + } } return Buffer.from(JSON.stringify(res)) } async getRefsHandler (publicKey, req) { - const { repoName, branch, url } = await this.parseReq(publicKey, req, 'r') + const { repoName, branch, url, userId } = await this.parseReq(publicKey, req) const res = this.repositories[repoName] - return Buffer.from(JSON.stringify(res)) + const isPublic = (acl.getACL(repoName).visibility === 'public') + if (isPublic || acl.getViewers(repoName).includes(userId)) { + return Buffer.from(JSON.stringify(res)) + } else { + throw new Error('You are not allowed to access this repo') + } } async pushHandler (publicKey, req) { - const { url, repoName, branch } = await this.parseReq(publicKey, req, 'w') + const { url, repoName, branch, userId } = await this.parseReq(publicKey, req) + const isContributor = acl.getContributors(repoName).includes(userId) + + if (!isContributor) { + throw new Error('You are not allowed to push to this repo') + } + + const isProtectedBranch = acl.getACL(repoName).protectedBranches.includes(branch) + const isAdmin = acl.getAdmins(repoName).includes(userId) + + if (isProtectedBranch && !isAdmin) { + throw new Error('You are not allowed to push to this branch') + } + return await new Promise((resolve, reject) => { const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) } const child = spawn('git', ['fetch', url, `${branch}:${branch}`], { env }) @@ -67,7 +89,20 @@ module.exports = class RPC { } async forcePushHandler (publicKey, req) { - const { url, repoName, branch } = await this.parseReq(publicKey, req, 'w') + const { url, repoName, branch, userId } = await this.parseReq(publicKey, req) + const isContributor = acl.getContributors(repoName).includes(userId) + + if (!isContributor) { + throw new Error('You are not allowed to push to this repo') + } + + const isProtectedBranch = acl.getACL(repoName).protectedBranches.includes(branch) + const isAdmin = acl.getAdmins(repoName).includes(userId) + + if (isProtectedBranch && !isAdmin) { + throw new Error('You are not allowed to push to this branch') + } + return await new Promise((resolve, reject) => { const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) } const child = spawn('git', ['fetch', url, `${branch}:${branch}`, '--force'], { env }) @@ -83,7 +118,19 @@ module.exports = class RPC { } async deleteBranchHandler (publicKey, req) { - const { url, repoName, branch } = await this.parseReq(publicKey, req, 'w') + const { url, repoName, branch, userId } = await this.parseReq(publicKey, req) + const isContributor = acl.getContributors(repoName).includes(userId) + + if (!isContributor) { + throw new Error('You are not allowed to push to this repo') + } + + const isProtectedBranch = acl.getACL(repoName).protectedBranches.includes(branch) + const isAdmin = acl.getAdmins(repoName).includes(userId) + + if (isProtectedBranch && !isAdmin) { + throw new Error('You are not allowed to push to this branch') + } return await new Promise((resolve, reject) => { const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) } const child = spawn('git', ['branch', '-D', branch], { env }) @@ -104,25 +151,19 @@ module.exports = class RPC { const parsed = { repoName: request.body.url?.split('/')?.pop(), branch: request.body.data?.split('#')[0], - url: request.body.url + url: request.body.url, + userId: await this.authenticate(publicKey, request), } - if (!process.env.GIT_PEAR_AUTH) return parsed + return parsed + } + async authenticate (publicKey, req) { + if (!process.env.GIT_PEAR_AUTH) return publicKey + if (process.env.GIT_PEAR_AUTH === 'naitive') return publicKey if (process.env.GIT_PEAR_AUTH !== 'naitive' && !request.header) { throw new Error('You are not allowed to access this repo') } - let userId - if (process.env.GIT_PEAR_AUTH === 'naitive') { - userId = publicKey - } else { - userId = (await auth.getId({ ...request.body, payload: request.header })).userId - } - const aclObj = home.getACL(parsed.repoName) - const userACL = aclObj[userId] || aclObj['*'] - if (!userACL) throw new Error('You are not allowed to access this repo') - - - return parsed + return (await auth.getId({ ...request.body, payload: request.header })).userId } } diff --git a/test/acl.test.js b/test/acl.test.js new file mode 100644 index 0000000..a52ef81 --- /dev/null +++ b/test/acl.test.js @@ -0,0 +1,59 @@ +const test = require('brittle') +const acl = require('../src/acl') + +test('acl', async t => { + const repoName = 'foo' + + t.test('setACL', async t => { + const aclObj = acl.setACL(repoName) + t.is(aclObj.visibility, 'public') + t.is(aclObj.protectedBranches.length, 1) + t.is(aclObj.protectedBranches[0], 'master') + t.is(Object.keys(aclObj.ACL).length, 0) + }) + + test('getACL', async t => { + const aclObj = acl.setACL(repoName) + t.alike(acl.getACL(repoName), aclObj) + }) + + test('makeRepoPublic', async t => { + acl.makeRepoPublic(repoName) + t.is(acl.getRepoVisibility(repoName), 'public') + }) + + test('makeRepoPrivate', async t => { + acl.makeRepoPrivate(repoName) + t.is(acl.getRepoVisibility(repoName), 'private') + }) + + test('grantAccessToUser', async t => { + acl.grantAccessToUser(repoName, 'user1', 'admin') + t.alike(acl.getUserRole(repoName, 'user1'), 'admin') + }) + + test('addProtectedBranch', async t => { + acl.addProtectedBranch(repoName, 'branch1') + t.is(acl.getACL(repoName).protectedBranches.length, 2) + }) + + test('removeProtectedBranch', async t => { + acl.removeProtectedBranch(repoName, 'branch1') + t.is(acl.getACL(repoName).protectedBranches.length, 1) + }) + + test('getAdmins', async t => { + acl.grantAccessToUser(repoName, 'user2', 'admin') + t.alike(acl.getAdmins(repoName), ['user1', 'user2']) + }) + + test('getContributors', async t => { + acl.grantAccessToUser(repoName, 'user3', 'contributor') + t.alike(acl.getContributors(repoName), ['user3', 'user1', 'user2']) + }) + + test('getViewers', async t => { + acl.grantAccessToUser(repoName, 'user4', 'viewer') + t.alike(acl.getViewers(repoName), ['user4', 'user3', 'user1', 'user2']) + }) +}) From e2cc611bc832874a7c33508c554ecb1aead69a48 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Sun, 28 Jan 2024 16:31:10 +0000 Subject: [PATCH 51/60] ACL cli not tested Signed-off-by: dzdidi --- src/acl.js | 7 ++++ src/cli.js | 105 ++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/src/acl.js b/src/acl.js index 4becc35..08b7b7e 100644 --- a/src/acl.js +++ b/src/acl.js @@ -53,6 +53,12 @@ function grantAccessToUser (repoName, user, role) { setACL(repoName, acl) } +function revokeAccessFromUser (repoName, user) { + const acl = getACL(repoName) + delete acl.ACL[user] + setACL(repoName, acl) +} + function addProtectedBranch (repoName, branch) { const acl = getACL(repoName) acl.protectedBranches.push(branch) @@ -109,4 +115,5 @@ module.exports = { getAdmins, getContributors, getViewers, + revokeAccessFromUser, } diff --git a/src/cli.js b/src/cli.js index 4cfb7e3..3756e19 100755 --- a/src/cli.js +++ b/src/cli.js @@ -9,6 +9,7 @@ const path = require('path') const fs = require('fs') const home = require('./home') +const acl = require('./acl') const git = require('./git') const pkg = require('../package.json') @@ -46,6 +47,7 @@ program if (options.share) { home.shareAppFolder(name) + acl.setACL(name) await git.push() console.log(`Shared "${name}" project`) } @@ -55,26 +57,109 @@ program .command('share') .description('share a gitpear repo') .addArgument(new commander.Argument('[p]', 'path to the repo').default('.')) - .action(async (p, options) => { - const name = path.resolve(p).split(path.sep).pop() - if ((home.isInitialized(name))) { - home.shareAppFolder(name) - await git.push() - console.log(`Shared "${name}" project`) + .addArgument(new commander.Argument('[v]', 'visibility of the repo').default('public')) + .action(async (p, v, options) => { + const fullPath = path.resolve(p) + if (!fs.existsSync(path.join(fullPath, '.git'))) { + console.error('Not a git repo') + process.exit(1) + } + + const name = fullPath.split(path.sep).pop() + if (!home.isInitialized(name)) { + console.error(`${name} is not initialized`) + process.exit(1) + } + + home.shareAppFolder(name) + acl.setACL(name, { visibility: v }) + await git.push() + console.log(`Shared "${name}" project, as ${v} repo`) + return + }) + +program + .command('acl') + .description('set acl of a gitpear repo') + .addArgument(new commander.Argument('[a]', 'actiont to perform').choices(['add', 'remove', 'list']).default('list')) + .addArgument(new commander.Argument('[u]', 'user to add/remove/list').default('')) + .addArgument(new commander.Argument('[p]', 'path to the repo').default('.')) + .action(async (a, u, p, options) => { + const fullPath = path.resolve(p) + if (!fs.existsSync(path.join(fullPath, '.git'))) { + console.error('Not a git repo') + process.exit(1) + } + + const name = fullPath.split(path.sep).pop() + if (!home.isInitialized(name)) { + console.error(`${name} is not initialized`) + process.exit(1) + } + const repoACL = acl.getACL(name) + + if (a === 'list' && !u) { + console.log('Visibility:', '\t', repoACL.visibility) + console.log('User:', '\t', 'Role:') + for (const user in repoACL.ACL) { + console.log(user, '\t', repoACL.ACL[user]) + } return } - console.error(`${name} is not initialized`) - process.exit(1) - }) + if (a === 'list') { + console.log('Visibility:', '\t', repoACL.visibility) + console.log('User:', u, '\t', 'Role:', repoACL.ACL[u]) + return + } + if (a === 'add') { + if (!u) { + console.error('User not provided') + process.exit(1) + } + + const [ userId, role ] = u.split(':') + if (repoACL.ACL[userId]) { + console.error(`${userId} already has access to ${name} as ${repoACL.ACL[userId]}`) + process.exit(1) + } + + acl.grantAccessToUser(name, userId, role) + console.log(`Added ${userId} to ${name} as ${role}`) + return + } + + if (a === 'remove') { + if (!u) { + console.error('User not provided') + process.exit(1) + } + + if (!repoACL.ACL[u]) { + console.error(`${u} does not have access to ${name}`) + process.exit(1) + } + + acl.revokeAccessFromUser(name, u) + console.log(`Removed ${u} from ${name}`) + return + } + }) + program .command('unshare') .description('unshare a gitpear repo') .addArgument(new commander.Argument('[p]', 'path to the repo').default('.')) .action((p, options) => { - const name = path.resolve(p).split(path.sep).pop() + const fullPath = path.resolve(p) + if (!fs.existsSync(path.join(fullPath, '.git'))) { + console.error('Not a git repo') + process.exit(1) + } + + const name = fullPath.split(path.sep).pop() if ((home.isInitialized(name))) { home.unshareAppFolder(name) console.log(`Unshared "${name}" project`) From a0b2d1fc2469a1df5a4f6e0793b9e535b7287423 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Sun, 28 Jan 2024 17:01:13 +0000 Subject: [PATCH 52/60] cleanups Signed-off-by: dzdidi --- src/auth/nip98.js | 21 --------------------- src/rpc.js | 6 +++--- 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/src/auth/nip98.js b/src/auth/nip98.js index f9f48b7..42e2751 100644 --- a/src/auth/nip98.js +++ b/src/auth/nip98.js @@ -26,24 +26,3 @@ 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 }}) -// })() diff --git a/src/rpc.js b/src/rpc.js index c8d754b..84dd7ec 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -157,9 +157,9 @@ module.exports = class RPC { return parsed } - async authenticate (publicKey, req) { - if (!process.env.GIT_PEAR_AUTH) return publicKey - if (process.env.GIT_PEAR_AUTH === 'naitive') return publicKey + async authenticate (publicKey, request) { + if (!process.env.GIT_PEAR_AUTH) return publicKey.toString('hex') + if (process.env.GIT_PEAR_AUTH === 'naitive') return publicKey.toString('hex') if (process.env.GIT_PEAR_AUTH !== 'naitive' && !request.header) { throw new Error('You are not allowed to access this repo') } From 58cbbdb6e29062155af6686bc0d98e8a534d2c68 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Sun, 28 Jan 2024 21:12:56 +0000 Subject: [PATCH 53/60] acl: remove native Signed-off-by: dzdidi --- src/rpc.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/rpc.js b/src/rpc.js index 84dd7ec..bdedf13 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -147,7 +147,7 @@ module.exports = class RPC { async parseReq(publicKey, req) { if (!req) throw new Error('Request is empty') - let request = JSON.parse(req.toString()) + const request = JSON.parse(req.toString()) const parsed = { repoName: request.body.url?.split('/')?.pop(), branch: request.body.data?.split('#')[0], @@ -159,10 +159,7 @@ module.exports = class RPC { async authenticate (publicKey, request) { if (!process.env.GIT_PEAR_AUTH) return publicKey.toString('hex') - if (process.env.GIT_PEAR_AUTH === 'naitive') return publicKey.toString('hex') - if (process.env.GIT_PEAR_AUTH !== 'naitive' && !request.header) { - throw new Error('You are not allowed to access this repo') - } + if (!request.header) throw new Error('You are not allowed to access this repo') return (await auth.getId({ ...request.body, payload: request.header })).userId } From 38f1d53e5d29ad6014bc6e72b3989cfe35489f2e Mon Sep 17 00:00:00 2001 From: dzdidi Date: Mon, 29 Jan 2024 09:37:03 +0000 Subject: [PATCH 54/60] branch protection Signed-off-by: dzdidi --- src/acl.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/acl.js b/src/acl.js index 08b7b7e..b22344c 100644 --- a/src/acl.js +++ b/src/acl.js @@ -61,6 +61,7 @@ function revokeAccessFromUser (repoName, user) { function addProtectedBranch (repoName, branch) { const acl = getACL(repoName) + if (acl.protectedBranches.includes(branch)) throw new Error(`${branch} is already protected`) acl.protectedBranches.push(branch) setACL(repoName, acl) } From 8a4285bca42f40b925a88a3d6f7120198c656540 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Mon, 29 Jan 2024 09:37:18 +0000 Subject: [PATCH 55/60] enable pus honly authenticated mod Signed-off-by: dzdidi --- src/rpc.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/rpc.js b/src/rpc.js index bdedf13..c2a3677 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -24,10 +24,12 @@ module.exports = class RPC { rpc.respond('get-repos', async req => await this.getReposHandler(peerInfo.publicKey, req)) rpc.respond('get-refs', async req => await this.getRefsHandler(peerInfo.publicKey, req)) - /* -- PUSH HANDLERS -- */ - rpc.respond('push', async req => await this.pushHandler(peerInfo.publicKey, req)) - rpc.respond('f-push', async req => await this.forcePushHandler(peerInfo.publicKey, req)) - rpc.respond('d-branch', async req => await this.deleteBranchHandler(peerInfo.publicKey, req)) + if (process.env.GIT_PEAR_AUTH) { + /* -- PUSH HANDLERS -- */ + rpc.respond('push', async req => await this.pushHandler(peerInfo.publicKey, req)) + rpc.respond('f-push', async req => await this.forcePushHandler(peerInfo.publicKey, req)) + rpc.respond('d-branch', async req => await this.deleteBranchHandler(peerInfo.publicKey, req)) + } this.connections[peerInfo.publicKey] = rpc } @@ -63,6 +65,8 @@ module.exports = class RPC { const { url, repoName, branch, userId } = await this.parseReq(publicKey, req) const isContributor = acl.getContributors(repoName).includes(userId) + console.error('pushHandler', { url, repoName, branch, userId, isContributor }) + if (!isContributor) { throw new Error('You are not allowed to push to this repo') } From d99fc95270beef6eafcd8da8ef54545a75d5953c Mon Sep 17 00:00:00 2001 From: dzdidi Date: Mon, 29 Jan 2024 09:37:40 +0000 Subject: [PATCH 56/60] do autmatic shareing of repo Signed-off-by: dzdidi --- src/git-remote-pear.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/git-remote-pear.js b/src/git-remote-pear.js index 4824212..088b9e2 100755 --- a/src/git-remote-pear.js +++ b/src/git-remote-pear.js @@ -12,6 +12,7 @@ const crypto = require('hypercore-crypto') const git = require('./git.js') const home = require('./home') const auth = require('./auth') +const acl = require('./acl') const fs = require('fs') @@ -106,6 +107,13 @@ async function talkToGit (refs, drive, repoName, rpc, commit) { dst = dst.replace('refs/heads/', '').replace('\n\n', '') + try { home.createAppFolder(repoName) } catch (e) { } + try { await git.createBareRepo(repoName) } catch (e) { } + try { await git.addRemote(repoName) } catch (e) { } + try { await git.push(dst) } catch (e) { } + try { home.shareAppFolder(repoName) } catch (e) { } + try { acl.setACL(repoName, acl.getACL(repoName)) } catch (e) { } + let method if (isDelete) { method = 'd-branch' From a172732eed49ef4607480d0be4d766fc664e8b63 Mon Sep 17 00:00:00 2001 From: dzdidi Date: Mon, 29 Jan 2024 09:37:59 +0000 Subject: [PATCH 57/60] cli for branch protection rules Signed-off-by: dzdidi --- Readme.md | 77 +++++++++++++++++++++++++++++++++++++++++++++------- src/cli.js | 80 ++++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 132 insertions(+), 25 deletions(-) diff --git a/Readme.md b/Readme.md index 4b4fc19..aabfd97 100644 --- a/Readme.md +++ b/Readme.md @@ -32,27 +32,48 @@ npm nix See `./result` - for binaries build by nix. To make the available add to path by running `PATH="${PATH:+${PATH}:}${PWD}/result/bin/"` -## +## Running All data will be persisted in application directory (default `~/.gitpear`). To change it. Provide environment variable `GIT_PEAR` * `git pear daemon <-s, --start | -k, --stop>` - start or stop daemon - * `git pear key` - print out public key. Share it with your peers so that they can do `git pull pear:/` - * `git pear init [-s, --share] ` - It will create [bare repository](https://git-scm.com/docs/git-init#Documentation/git-init.txt---bare) of the same name in application directory (default ~/.gitpear/). It will add [git remote](https://git-scm.com/docs/git-remote) in current repository with name `pear`. So just like in traditional flow doing `git push orign`, here we do `git push pear`. By default repository will not be shared. To enable sharing provide `-s` or call `gitpear share ` later - * `git pear share ` - makes repository sharable - * `git pear unshare ` - stop sharing repository - * `git pear list [-s, --shared]` - list all or (only shared) repositories -## Usage example (NO PUSH) +### ACL (for authenticated access to enable support of PUSH) -Please not this is only remote helper and its intention is only to enable direct `clone|fetch|pull` of repository hosted on private computer. +Support of `push` capabilities only enabled for authenticated users. Currently supported authentication is based on [NIP98](https://github.com/nostr-protocol/nips/blob/master/98.md). +To start daemon with authenticated support provile environment varibales `GIT_PEAR_AUTH` with value `nip98` and `GIT_PEAR_AUTH_NSEC` with value of your [NIP19 nsec](https://github.com/nostr-protocol/nips/blob/master/19.md). +For example: +``` +GIT_PEAR_AUTH=nip98 GIT_PEAR_AUTH_NSEC=nsec.... git pear daemon -s +``` -Collaboration is possible however with the following flow between Alice and Bob in a pure peer-to-peer manner of git. +To manage access to repository use one or combination of the following commands, if `path` is not provide the command will be executed in the current directory. For `userId` use [NIP19 npub](https://github.com/nostr-protocol/nips/blob/master/19.md). + +* `git pear acl [command] ` - ACL managegement +* `git pear acl list [userId] ` - list repository visitbility and user's role (or roles of all users if userId is not provided) +* `git pear acl add ` - add user as a "role" to repository, available roles are `viewer`, `contributor`, `admin`. Roles exaplained: + * `viewer` - can read all branches; + * `contributor` - can edit all branches except protected (default master) + * `admin` - can edit protected branches +* `git pear acl remove ` - revoke use access to repository + + +### Branch protection rules +It is possible to setup basic branch protection rules (master is proteted by default). +* `git pear branch`, same as `git pear branch list .` - list protection rules +* `git pear branch add ` - mark branch as protected (defatul repo path is ".") +* `git pear branch remove ` - unmark branch as protected + +# Examples of usage + +## Un authenticated usage example (no push) + +Collaboration is possible with the following flow between Alice and Bob in a pure peer-to-peer manner of git. 1. Both Alice and Bob have gitpear installed and Alice wants Bob to help her with repo Repo 2. Alice steps are: @@ -95,4 +116,40 @@ git fetch origin git pull ``` -## Usage example (PUSH) +## Authenticated usage example (push) + +Collaboration is possible with the following flow between Carol and David in a pure peer-to-peer manner of git. + +### Carol steps (as a server of code) +1. Start daemon +* `GIT_PEAR_AUTH_NSEC= GIT_PEAR_AUTH='nip98' git pear daemon -s` +2. Go to repository +* `cd repo` +3. Initialize git pear repository +* `git pear init .` +4. Share repository wit hviben visibility () - (default is `public`) +* `git pear share . ` +5. Add Daviv as a `contirbutor`. +6. List David's npub as a contributor +* `git pear acl add :contributor` +7. Retreive repo url and share it with Dave +* `git pear list -s` + +### Dave side (a collaborator for code) +1. Start daemon. This will be needed later for push. Not that no auth or sec are provided which means that push to this place will not be supportedd. +* `git pear daemon -s` +2. Clone repository. Authorization data and type are necesary for server (Carol) to grant corresponding access persmissions +* `GIT_PEAR_AUTH_NSEC= GIT_PEAR_AUTH='nip98' git clone pear:///` +3. Do the necessary change in separate branch +* `git checkout -b feat/david` +* // do change +* `git add .` +* `git commit -s -m 'made by David'` +4. Push branch to origin +* `GIT_PEAR_AUTH_NSEC= GIT_PEAR_AUTH='nip98' git push origin feat/david` + +### Carol steps +1. For Carol the changes will arrive as branch `feat/david` into her `pear` +* `git fetch pear` +2. From there she can do +* `git diff pear/feat/david` or `git pull pear feat/david` ... merge to master and push to `pear` diff --git a/src/cli.js b/src/cli.js index 3756e19..4a5361b 100755 --- a/src/cli.js +++ b/src/cli.js @@ -38,17 +38,23 @@ program process.exit(1) } - home.createAppFolder(name) - console.log(`Added project "${name}" to gitpear`) - await git.createBareRepo(name) - console.log(`Created bare repo for "${name}"`) - await git.addRemote(name) - console.log(`Added git remote for "${name}" as "pear"`) + try { + home.createAppFolder(name) + console.log(`Added project "${name}" to gitpear`) + } catch (e) { } + try { + await git.createBareRepo(name) + console.log(`Created bare repo for "${name}"`) + } catch (e) { } + try { + await git.addRemote(name) + console.log(`Added git remote for "${name}" as "pear"`) + } catch (e) { } if (options.share) { - home.shareAppFolder(name) - acl.setACL(name) - await git.push() + try { home.shareAppFolder(name) } catch (e) { } + try { acl.setACL(name) } catch (e) { } + try { await git.push() } catch (e) { } console.log(`Shared "${name}" project`) } }) @@ -71,13 +77,55 @@ program process.exit(1) } - home.shareAppFolder(name) - acl.setACL(name, { visibility: v }) - await git.push() + try { home.shareAppFolder(name) } catch (e) { } + try { acl.setACL(name, { visibility: v }) } catch (e) { } + try { await git.push() } catch (e) { } console.log(`Shared "${name}" project, as ${v} repo`) return }) +program + .command('branch') + .description('branch protection rules') + .addArgument(new commander.Argument('[a]', 'actiont to perform').choices(['add', 'remove', 'list']).default('list')) + .addArgument(new commander.Argument('[b]', 'branch name').default('')) + .addArgument(new commander.Argument('[p]', 'path to the repo').default('.')) + .action(async (a, b, p, options) => { + const fullPath = path.resolve(p) + if (!fs.existsSync(path.join(fullPath, '.git'))) { + console.error('Not a git repo') + process.exit(1) + } + + const name = fullPath.split(path.sep).pop() + if (!home.isInitialized(name)) { + console.error(`${name} is not initialized`) + process.exit(1) + } + + if (a === 'list' && !b) { logBranches(name) } + + if (a === 'add') { + acl.addProtectedBranch(name, b) + logBranches(name) + } + + if (a === 'remove') { + acl.removeProtectedBranch(name, b) + logBranches(name) + } + + function logBranches(name) { + const repoACL = acl.getACL(name) + console.log('Visibility:', '\t', repoACL.visibility) + console.log('Branch:') + for (const branch of repoACL.protectedBranches) { console.log(branch) } + } + + return + }) + + program .command('acl') .description('set acl of a gitpear repo') @@ -85,6 +133,8 @@ program .addArgument(new commander.Argument('[u]', 'user to add/remove/list').default('')) .addArgument(new commander.Argument('[p]', 'path to the repo').default('.')) .action(async (a, u, p, options) => { + + // TODO: add branch protection logic const fullPath = path.resolve(p) if (!fs.existsSync(path.join(fullPath, '.git'))) { console.error('Not a git repo') @@ -99,7 +149,7 @@ program const repoACL = acl.getACL(name) if (a === 'list' && !u) { - console.log('Visibility:', '\t', repoACL.visibility) + console.log('Repo Visibility:', '\t', repoACL.visibility) console.log('User:', '\t', 'Role:') for (const user in repoACL.ACL) { console.log(user, '\t', repoACL.ACL[user]) @@ -108,8 +158,8 @@ program } if (a === 'list') { - console.log('Visibility:', '\t', repoACL.visibility) - console.log('User:', u, '\t', 'Role:', repoACL.ACL[u]) + console.log('Repo Visibility:', '\t', repoACL.visibility) + console.log('User:', u, '\t', repoACL.ACL[u]) return } From 3ed49d3d04efecc0de05e8ce66d5daf765f8a35b Mon Sep 17 00:00:00 2001 From: dzdidi Date: Mon, 29 Jan 2024 09:40:23 +0000 Subject: [PATCH 58/60] Readme: update Signed-off-by: dzdidi --- Readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index aabfd97..22928d1 100644 --- a/Readme.md +++ b/Readme.md @@ -116,7 +116,7 @@ git fetch origin git pull ``` -## Authenticated usage example (push) +## Authenticated usage example (push) - at your own risk Collaboration is possible with the following flow between Carol and David in a pure peer-to-peer manner of git. @@ -142,7 +142,7 @@ Collaboration is possible with the following flow between Carol and David in a p * `GIT_PEAR_AUTH_NSEC= GIT_PEAR_AUTH='nip98' git clone pear:///` 3. Do the necessary change in separate branch * `git checkout -b feat/david` -* // do change +* do change * `git add .` * `git commit -s -m 'made by David'` 4. Push branch to origin From 2b59f64e33f0b3c1662c197bb378da63bcc2f22f Mon Sep 17 00:00:00 2001 From: dzdidi Date: Mon, 29 Jan 2024 09:46:39 +0000 Subject: [PATCH 59/60] rpc: cleanup Signed-off-by: dzdidi --- src/rpc.js | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/rpc.js b/src/rpc.js index c2a3677..e3338cc 100755 --- a/src/rpc.js +++ b/src/rpc.js @@ -65,18 +65,12 @@ module.exports = class RPC { const { url, repoName, branch, userId } = await this.parseReq(publicKey, req) const isContributor = acl.getContributors(repoName).includes(userId) - console.error('pushHandler', { url, repoName, branch, userId, isContributor }) - - if (!isContributor) { - throw new Error('You are not allowed to push to this repo') - } + if (!isContributor) throw new Error('You are not allowed to push to this repo') const isProtectedBranch = acl.getACL(repoName).protectedBranches.includes(branch) const isAdmin = acl.getAdmins(repoName).includes(userId) - if (isProtectedBranch && !isAdmin) { - throw new Error('You are not allowed to push to this branch') - } + if (isProtectedBranch && !isAdmin) throw new Error('You are not allowed to push to this branch') return await new Promise((resolve, reject) => { const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) } @@ -96,16 +90,12 @@ module.exports = class RPC { const { url, repoName, branch, userId } = await this.parseReq(publicKey, req) const isContributor = acl.getContributors(repoName).includes(userId) - if (!isContributor) { - throw new Error('You are not allowed to push to this repo') - } + if (!isContributor) throw new Error('You are not allowed to push to this repo') const isProtectedBranch = acl.getACL(repoName).protectedBranches.includes(branch) const isAdmin = acl.getAdmins(repoName).includes(userId) - if (isProtectedBranch && !isAdmin) { - throw new Error('You are not allowed to push to this branch') - } + if (isProtectedBranch && !isAdmin) throw new Error('You are not allowed to push to this branch') return await new Promise((resolve, reject) => { const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) } @@ -125,16 +115,13 @@ module.exports = class RPC { const { url, repoName, branch, userId } = await this.parseReq(publicKey, req) const isContributor = acl.getContributors(repoName).includes(userId) - if (!isContributor) { - throw new Error('You are not allowed to push to this repo') - } + if (!isContributor) throw new Error('You are not allowed to push to this repo') const isProtectedBranch = acl.getACL(repoName).protectedBranches.includes(branch) const isAdmin = acl.getAdmins(repoName).includes(userId) - if (isProtectedBranch && !isAdmin) { - throw new Error('You are not allowed to push to this branch') - } + if (isProtectedBranch && !isAdmin) throw new Error('You are not allowed to push to this branch') + return await new Promise((resolve, reject) => { const env = { ...process.env, GIT_DIR: home.getCodePath(repoName) } const child = spawn('git', ['branch', '-D', branch], { env }) From 0b228ff1dcae26d3c04277dbfd6334773f944f2d Mon Sep 17 00:00:00 2001 From: dzdidi Date: Mon, 29 Jan 2024 09:46:46 +0000 Subject: [PATCH 60/60] fix typo Signed-off-by: dzdidi --- Readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Readme.md b/Readme.md index 22928d1..c84c4f7 100644 --- a/Readme.md +++ b/Readme.md @@ -46,7 +46,7 @@ All data will be persisted in application directory (default `~/.gitpear`). To c ### ACL (for authenticated access to enable support of PUSH) Support of `push` capabilities only enabled for authenticated users. Currently supported authentication is based on [NIP98](https://github.com/nostr-protocol/nips/blob/master/98.md). -To start daemon with authenticated support provile environment varibales `GIT_PEAR_AUTH` with value `nip98` and `GIT_PEAR_AUTH_NSEC` with value of your [NIP19 nsec](https://github.com/nostr-protocol/nips/blob/master/19.md). +To start daemon with authenticated support provide environment varibales `GIT_PEAR_AUTH` with value `nip98` and `GIT_PEAR_AUTH_NSEC` with value of your [NIP19 nsec](https://github.com/nostr-protocol/nips/blob/master/19.md). For example: ``` GIT_PEAR_AUTH=nip98 GIT_PEAR_AUTH_NSEC=nsec.... git pear daemon -s @@ -73,7 +73,7 @@ It is possible to setup basic branch protection rules (master is proteted by def ## Un authenticated usage example (no push) -Collaboration is possible with the following flow between Alice and Bob in a pure peer-to-peer manner of git. +Collaboration is possible with the following flow between Alice and Bob in a peer-to-peer manner. 1. Both Alice and Bob have gitpear installed and Alice wants Bob to help her with repo Repo 2. Alice steps are: @@ -118,7 +118,7 @@ git pull ## Authenticated usage example (push) - at your own risk -Collaboration is possible with the following flow between Carol and David in a pure peer-to-peer manner of git. +Collaboration is possible with the following flow between Carol and David in a peer-to-peer manner. ### Carol steps (as a server of code) 1. Start daemon