Skip to content

Commit

Permalink
Merge pull request #12 from HDegroote/add-test
Browse files Browse the repository at this point in the history
Add --private flag + add some end-to-end tests
  • Loading branch information
prdn authored Jan 6, 2024
2 parents 1b05f14 + fc75588 commit 2838a57
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 27 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Build Status
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 https://github.com/actions/checkout/releases/tag/v4.1.1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 https://github.com/actions/setup-node/releases/tag/v3.8.2
with:
node-version: 18
- run: npm install
- run: npm test
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ options:
--cert-skip : skip certificate check when connecting to the service
--seed SEED : seed (command-line)
--compress : enable chunk compression
--private : make the proxy private (do not leak the access capability to the DHT)
```

```sh
Expand Down Expand Up @@ -104,6 +105,7 @@ options:
-s SERVER_PEER_KEY : server peer key (command-line)
-i keypair.json : keypair file
--compress : enable chunk compression
--private : access a private hypertele server (expects -s to contain the server's seed instead of the public key)
```

Read more about using identities here: https://github.com/prdn/hyper-cmd-docs/blob/main/identity.md
Expand Down
50 changes: 34 additions & 16 deletions client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@
const HyperDHT = require('hyperdht')
const net = require('net')
const argv = require('minimist')(process.argv.slice(2))
const b4a = require('b4a')
const libNet = require('@hyper-cmd/lib-net')
const libUtils = require('@hyper-cmd/lib-utils')
const libKeys = require('@hyper-cmd/lib-keys')
const goodbye = require('graceful-goodbye')
const connPiper = libNet.connPiper

const helpMsg = 'Usage:\nhypertele -p port_listen -u unix_socket ?--address service_address ?-c conf.json ?-i identity.json ?-s peer_key'
const helpMsg = 'Usage:\nhypertele -p port_listen -u unix_socket ?--address service_address ?-c conf.json ?-i identity.json ?-s peer_key ?--private'

if (argv.help) {
console.log(helpMsg)
process.exit(-1)
}

if (!argv.u && !+argv.p) {
if (!argv.u && argv.p == null) {
console.error('Error: proxy port invalid')
process.exit(-1)
}
Expand All @@ -28,8 +29,35 @@ const conf = {}

const target = argv.u ? argv.u : +argv.p

let keyPair = null
if (argv.i) {
keyPair = libUtils.resolveIdentity([], argv.i)

if (!keyPair) {
console.error('Error: identity file invalid')
process.exit(-1)
}

keyPair = libKeys.parseKeyPair(keyPair)
}

conf.private = argv.private != null
if (conf.private) {
if (keyPair != null) throw new Error('The --private flag is not compatible with the -i(dentity) flag, since the identity is derived from the peer key')
const seed = argv.s
keyPair = HyperDHT.keyPair(b4a.from(seed, 'hex'))
}

// Unofficial opt, only used for tests
let bootstrap = null
if (argv.bootstrap) {
bootstrap = [{ host: '127.0.0.1', port: argv.bootstrap }]
}

if (argv.s) {
conf.peer = libUtils.resolveHostToKey([], argv.s)
conf.peer = conf.private
? keyPair.publicKey
: libUtils.resolveHostToKey([], argv.s)
}

if (argv.c) {
Expand All @@ -52,21 +80,10 @@ if (!peer) {

const debug = argv.debug

let keyPair = null
if (argv.i) {
keyPair = libUtils.resolveIdentity([], argv.i)

if (!keyPair) {
console.error('Error: identity file invalid')
process.exit(-1)
}

keyPair = libKeys.parseKeyPair(keyPair)
}

const stats = {}

const dht = new HyperDHT({
bootstrap,
keyPair
})

Expand All @@ -92,7 +109,8 @@ if (argv.u) {
} else {
const targetHost = argv.address || '127.0.0.1'
proxy.listen(target, targetHost, () => {
console.log(`Server ready @${targetHost}:${target}`)
const { address, port } = proxy.address()
console.log(`Server ready @${address}:${port}`)
})
}

Expand Down
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"@hyper-cmd/lib-keys": "https://github.com/holepunchto/hyper-cmd-lib-keys#v0.0.2",
"@hyper-cmd/lib-net": "https://github.com/holepunchto/hyper-cmd-lib-net#v0.0.8",
"@hyper-cmd/lib-utils": "https://github.com/holepunchto/hyper-cmd-lib-utils#v0.0.2",
"b4a": "^1.6.4",
"graceful-goodbye": "^1.3.0",
"hyperdht": "^6.11.0",
"minimist": "^1.2.5"
Expand All @@ -24,5 +25,12 @@
"bugs": {
"url": "https://github.com/bitfinexcom/hypertele/issues"
},
"homepage": "https://github.com/bitfinexcom/hypertele"
"homepage": "https://github.com/bitfinexcom/hypertele",
"devDependencies": {
"brittle": "^3.3.2",
"standard": "^17.1.0"
},
"scripts": {
"test": "standard && brittle test/end-to-end-tests.js"
}
}
45 changes: 35 additions & 10 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
#!/usr/bin/env node
const HyperDHT = require('hyperdht')
const net = require('net')
const b4a = require('b4a')
const argv = require('minimist')(process.argv.slice(2))
const libNet = require('@hyper-cmd/lib-net')
const libUtils = require('@hyper-cmd/lib-utils')
const libKeys = require('@hyper-cmd/lib-keys')
const goodbye = require('graceful-goodbye')
const connPiper = libNet.connPiper

const helpMsg = 'Usage:\nhypertele-server -l service_port -u unix_socket ?--address service_address ?-c conf.json ?--seed seed ?--cert-skip'
const helpMsg = 'Usage:\nhypertele-server -l service_port -u unix_socket ?--address service_address ?-c conf.json ?--seed seed ?--cert-skip ?--private'

if (argv.help) {
console.log(helpMsg)
Expand Down Expand Up @@ -52,25 +53,45 @@ if (conf.allow) {
conf.allow = libKeys.prepKeyList(conf.allow)
}

conf.private = false
if (argv.private) {
if (conf.allow) throw new Error('--private flag is not compatible with an allow list, as the private key derived from the seed is the capability')
conf.private = true
}

// Unofficial opt, only used for tests
let bootstrap = null
if (argv.bootstrap) {
bootstrap = [{ host: '127.0.0.1', port: argv.bootstrap }]
}

const debug = argv.debug

const seed = Buffer.from(conf.seed, 'hex')

const dht = new HyperDHT()
const dht = new HyperDHT({ bootstrap })
const keyPair = HyperDHT.keyPair(seed)

const stats = {}

const destIp = argv.address || '127.0.0.1'

const server = dht.createServer({
firewall: (remotePublicKey, remoteHandshakePayload) => {
if (conf.allow && !libKeys.checkAllowList(conf.allow, remotePublicKey)) {
return true
}
const privateFirewall = (remotePublicKey) => {
return !b4a.equals(remotePublicKey, keyPair.publicKey)
}

return false
},
const allowListFirewall = (remotePublicKey, remoteHandshakePayload) => {
if (conf.allow && !libKeys.checkAllowList(conf.allow, remotePublicKey)) {
return true
}

return false
}

const firewall = conf.private ? privateFirewall : allowListFirewall

const server = dht.createServer({
firewall,
reusableSocket: true
}, c => {
connPiper(c, () => {
Expand All @@ -83,7 +104,11 @@ const server = dht.createServer({
})

server.listen(keyPair).then(() => {
console.log('hypertele:', keyPair.publicKey.toString('hex'))
if (conf.private) {
console.log(`hypertele (private): connect with seed ${b4a.toString(seed, 'hex')} (listening on ${b4a.toString(keyPair.publicKey, 'hex')})`)
} else {
console.log('hypertele:', keyPair.publicKey.toString('hex'))
}
})

if (debug) {
Expand Down
160 changes: 160 additions & 0 deletions test/end-to-end-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
const { spawn } = require('child_process')
const { once } = require('events')
const path = require('path')
const http = require('http')
const createTestnet = require('hyperdht/testnet')
const test = require('brittle')
const HyperDHT = require('hyperdht')
const b4a = require('b4a')

const MAIN_DIR = path.dirname(__dirname)
const SERVER_EXECUTABLE = path.join(MAIN_DIR, 'server.js')
const CLIENT_EXECUTABLE = path.join(MAIN_DIR, 'client.js')

test('Can proxy in private mode', async t => {
const { bootstrap } = await createTestnet(3, t.teardown)
const portToProxy = await setupDummyServer(t.teardown)
const seed = 'a'.repeat(64)

await setupHyperteleServer(portToProxy, seed, bootstrap, t, { isPrivate: true })
const clientPort = await setupHyperteleClient(seed, bootstrap, t, { isPrivate: true })

const res = await request(clientPort)
t.is(res.data, 'You got served', 'Proxy works')
})

test('Cannot access private-mode server with public key', async t => {
const { bootstrap } = await createTestnet(3, t.teardown)
const portToProxy = await setupDummyServer(t.teardown)
const seed = 'a'.repeat(64)
const keypair = HyperDHT.keyPair(b4a.from(seed, 'hex'))
const pubKey = b4a.toString(keypair.publicKey, 'hex')

await setupHyperteleServer(portToProxy, seed, bootstrap, t, { isPrivate: true })
const clientPort = await setupHyperteleClient(pubKey, bootstrap, t, { isPrivate: false })

// Could also be a socket hangup if more time is given
await t.exception(async () => await request(clientPort), /Request timeout/)
})

test('Can proxy in non-private mode', async t => {
const { bootstrap } = await createTestnet(3, t.teardown)
const portToProxy = await setupDummyServer(t.teardown)
const seed = 'a'.repeat(64)
const keypair = HyperDHT.keyPair(b4a.from(seed, 'hex'))
const pubKey = b4a.toString(keypair.publicKey, 'hex')

await setupHyperteleServer(portToProxy, seed, bootstrap, t, { isPrivate: false })
const clientPort = await setupHyperteleClient(pubKey, bootstrap, t, { isPrivate: false })

const res = await request(clientPort)
t.is(res.data, 'You got served', 'Proxy works')
})

async function setupDummyServer (teardown) {
const server = http.createServer(async (req, res) => {
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end('You got served')
})
teardown(() => server.close())

server.listen({ port: 0, host: '127.0.0.1' })
await once(server, 'listening')
return server.address().port
}

async function setupHyperteleServer (portToProxy, seed, bootstrap, t, { isPrivate = false } = {}) {
const args = [
SERVER_EXECUTABLE,
'-l',
portToProxy,
'--seed',
seed,
'--bootstrap',
bootstrap[0].port
]
if (isPrivate) args.push('--private')

const setupServer = spawn('node', args)
t.teardown(() => setupServer.kill('SIGKILL'))

setupServer.stderr.on('data', (data) => {
console.error(data.toString())
t.fail('Failed to setup hypertele server')
})

await new Promise(resolve => {
setupServer.stdout.on('data', (data) => {
if (data.includes('hypertele')) {
resolve()
}
})
})
}

async function setupHyperteleClient (seed, bootstrap, t, { isPrivate = false } = {}) {
const args = [
CLIENT_EXECUTABLE,
'-p',
0, // random
'-s',
seed,
'--bootstrap',
bootstrap[0].port
]
if (isPrivate) args.push('--private')

const setupClient = spawn('node', args)
t.teardown(() => setupClient.kill('SIGKILL'))

setupClient.stderr.on('data', (data) => {
console.error(data.toString())
t.fail('Failed to setup hypertele client')
})

const clientPort = await new Promise(resolve => {
setupClient.stdout.on('data', (data) => {
const msg = data.toString()
if (msg.includes('Server ready')) {
const port = msg.slice(msg.search(':') + 1)
resolve(port)
}
})
})

return clientPort
}

async function request (port, { msTimeout = 500 } = {}) {
const link = `http://127.0.0.1:${port}`

return new Promise((resolve, reject) => {
const req = http.get(link, {
headers: {
Connection: 'close'
}
})

req.setTimeout(msTimeout,
() => {
reject(new Error('Request timeout'))
req.destroy()
}
)

req.on('error', reject)
req.on('response', function (res) {
let buf = ''

res.setEncoding('utf-8')

res.on('data', function (data) {
buf += data
})

res.on('end', function () {
resolve({ status: res.statusCode, data: buf })
})
})
})
}

0 comments on commit 2838a57

Please sign in to comment.