diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..64f6bf2 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,15 @@ +package-lock.json +yarn.lock + +**/node_modules +**/*.log +test/setup/tmp-disposable-nodes-addrs.json +dist +coverage +.nyc_output +**/*.swp +**/*.bak +examples/sub-module/**/bundle.js +examples/sub-module/**/*-minified.js +examples/sub-module/*-bundle.js +docs diff --git a/server/.travis.yml b/server/.travis.yml new file mode 100644 index 0000000..32d81f9 --- /dev/null +++ b/server/.travis.yml @@ -0,0 +1,28 @@ +language: node_js +cache: npm +stages: + - check + - test + - cov + +node_js: + - '10' + +os: + - linux + - osx + - windows + +script: npx nyc -s npm run test:node -- --bail +after_success: npx nyc report --reporter=text-lcov > coverage.lcov && npx codecov + +jobs: + include: + - stage: check + script: + - npx aegir commitlint --travis + - npx aegir dep-check -- -i wrtc -i electron-webrtc + - npm run lint + +notifications: + email: false diff --git a/server/CHANGELOG.md b/server/CHANGELOG.md new file mode 100644 index 0000000..255e4b8 --- /dev/null +++ b/server/CHANGELOG.md @@ -0,0 +1,119 @@ + +# [0.4.0](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/compare/v0.3.0...v0.4.0) (2019-07-19) + + +### Features + +* switches to async/await and upgrade hapi to v18 ([946e8a1](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/946e8a1)) + + +### BREAKING CHANGES + +* All functions that took callbacks now return promises + + + + +# [0.3.0](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/compare/v0.2.4...v0.3.0) (2018-11-29) + + +### Bug Fixes + +* dont use 'this' in root anon function ([c6a833e](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/c6a833e)) +* logo was broken on main page ([#25](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/issues/25)) ([41eed04](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/41eed04)) +* regex bug for ipv4 test ([#24](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/issues/24)) ([696ed92](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/696ed92)) +* remove warning for too many listeners on socket.io sockets ([#28](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/issues/28)) ([3d9b96e](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/3d9b96e)) + + +### Features + +* include existing peers in response to ss-join ([f12aea3](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/f12aea3)) +* use node 10 in docker image ([#26](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/issues/26)) ([91db9cf](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/91db9cf)) + + + + +## [0.2.4](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/compare/v0.2.3...v0.2.4) (2018-10-16) + + +### Bug Fixes + +* give crypto.verify a buffer ([#23](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/issues/23)) ([0c8c290](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/0c8c290)) +* make it executable available through websocket-star ([d18087c](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/d18087c)) + + + + +## [0.2.3](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/compare/v0.2.2...v0.2.3) (2018-02-12) + + + + +## [0.2.2](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/compare/v0.2.1...v0.2.2) (2017-12-07) + + +### Features + +* Add libp2p logo to about page ([66be194](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/66be194)) +* cryptoChallenge can be enabled by default after all! https://github.com/ipfs/js-ipfs/pull/1090/files\#r153143252 ([143a0a4](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/143a0a4)) + + + + +## [0.2.1](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/compare/v0.2.0...v0.2.1) (2017-11-19) + + +### Bug Fixes + +* Docker cmd - feat: Disable metrics option ([21f95d2](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/21f95d2)) +* release command ([37e5b1f](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/37e5b1f)) + + + + +# [0.2.0](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/compare/v0.1.2...v0.2.0) (2017-10-28) + + +### Bug Fixes + +* {webrtc => websocket} ([df53c25](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/df53c25)) +* discovery fix - fix: debug log name ([1f163b8](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/1f163b8)) +* lint ([585525e](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/585525e)) +* package.json ([f5e91fe](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/f5e91fe)) +* small name fix ([de84807](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/de84807)) + + +### Features + +* Joins metric - fix: config ([81c8eb7](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/81c8eb7)) +* Link directly to readme in about page ([d7fba03](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/d7fba03)) +* metrics (WIP) - feat: Dockerfile - fix/feat: various other things ([fa518b1](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/fa518b1)) +* Update README - feat: Use dumb-init in docker-image ([4fbed33](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/4fbed33)) + + + + +## [0.1.2](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/compare/v0.1.1...v0.1.2) (2017-09-08) + + +### Bug Fixes + +* point to right location of bin ([3049ca8](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/3049ca8)) + + + + +## [0.1.1](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/compare/v0.1.0...v0.1.1) (2017-09-08) + + +### Bug Fixes + +* add main to package.json ([7ff704c](https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/commit/7ff704c)) + + + + +# 0.1.0 (2017-09-08) + + + diff --git a/server/DEPLOYMENT.md b/server/DEPLOYMENT.md new file mode 100644 index 0000000..046c1ec --- /dev/null +++ b/server/DEPLOYMENT.md @@ -0,0 +1,20 @@ +# Deployment + +## IPFS Infra + +We have a [dokku](https://github.com/ipfs/ops-requests/issues/31) setup ready for this to be deployed, to deploy simple do (you have to have permission first): + +```sh +# if you already have added the remote, you don't need to do it again +> git remote add dokku dokku@cloud.ipfs.team:ws-star +> git push dokku master +``` + +More info: https://github.com/libp2p/js-libp2p-webrtc-star/pull/48 + +## Other + +# mkg20001 +The nodes `ws-star-signal-{2,4,h}.servep2p.com` run on `host0.zion.host` + +Upgrades are done by running `bash /home/maciej/upgrade-rendezvous.sh` which runs docker pull and re-creates the containers diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..00c9c0f --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,7 @@ +FROM node:10 +RUN wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 && chmod +x /usr/local/bin/dumb-init +WORKDIR /usr/src/app +COPY package.json . +RUN npm i --production +COPY . . +ENTRYPOINT ["/usr/local/bin/dumb-init", "node", "--max-old-space-size=8192", "src/bin.js"] diff --git a/server/LICENSE b/server/LICENSE new file mode 100644 index 0000000..bbfffbf --- /dev/null +++ b/server/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 libp2p + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/server/Procfile b/server/Procfile new file mode 100644 index 0000000..28fe750 --- /dev/null +++ b/server/Procfile @@ -0,0 +1 @@ +web: npm run start diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..4352857 --- /dev/null +++ b/server/README.md @@ -0,0 +1,108 @@ +# libp2p-websocket-star-rendezvous + +[![](https://img.shields.io/badge/made%20by-mkg20001-blue.svg?style=flat-square)](http://ipn.io) +[![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p) +[![Discourse posts](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io) +[![](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-websocket-star-rendezvous.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-websocket-star-rendezvous) +[![](https://img.shields.io/travis/libp2p/js-libp2p-websocket-star-rendezvous.svg?style=flat-square)](https://travis-ci.com/libp2p/js-libp2p-websocket-star-rendezvous) +[![Dependency Status](https://david-dm.org/libp2p/js-libp2p-websocket-star-rendezvous.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-websocket-star-rendezvous) +[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) + +> The rendezvous service for [libp2p-websocket-star](https://github.com/libp2p/js-libp2p-websocket-star). + +## Lead Maintainer + +[Jacob Heun](https://github.com/jacobheun) + +## Descriptions + +Nodes using `libp2p-websocket-star` will connect to a known point in the network, a rendezvous point where they can learn about other nodes (Discovery) and route their messages to other nodes (2 hop message routing, also known as relay). + +## Usage + +`libp2p-websocket-star-rendezvous` is the rendezvous server required for `libp2p-websocket-star` and can be used to start a rendezvous server for development. To do that, first install the module globally in your machine with: + +```bash +> npm install --global libp2p-websocket-star-rendezvous +``` + +This will install a `rendezvous` CLI tool. Now you can spawn the server with: + +```bash +> rendezvous --port=9090 --host=127.0.0.1 +``` + +Defaults: + +- `port` - 9090 +- `host` - '0.0.0.0' + +## Docker + +A docker image is offered for running this service in production + +``` +docker pull libp2p/websocket-star-rendezvous:release +docker run -d -p 9090:9090 --name rendezvous libp2p/websocket-star-rendezvous:release +``` + +To disable prometheus metrics run the server with `-e DISABLE_METRICS=1` + +``` +docker run -d -p 9090:9090 --name rendezvous -e DISABLE_METRICS=1 libp2p/websocket-star-rendezvous:release +``` + +## Hosted Rendezvous server + +We host a rendezvous server at `ws-star.discovery.libp2p.io` that can be used for practical demos and experimentation, it **should not be used for apps in production**. + +A libp2p-websocket-star address, using the signalling server we provide, looks like: + +`/dns4/ws-star.discovery.libp2p.io/wss/p2p-websocket-star/ipfs/` + +Note: The address above indicates WebSockets Secure, which can be accessed from both http and https. + + +### Using WSS + +To be able to interact with a rendezvous server from an HTTPS site, you will need to use websocket secure. To host a secure websocket server, you must provide a keypair to the server. + +#### Using key and certificate + +```bash +> rendezvous --key="path/to/key.key" --cert="path/to/cert.cert" +``` + +#### Using PFX with passphrase + +```bash +> rendezvous --pfx="path/to/pair.pfx" --passphrase="passphrase" +``` + + +### This module uses `pull-streams` + +We expose a streaming interface based on `pull-streams`, rather then on the Node.js core streams implementation (aka Node.js streams). `pull-streams` offers us a better mechanism for error handling and flow control guarantees. If you would like to know more about why we did this, see the discussion at this [issue](https://github.com/ipfs/js-ipfs/issues/362). + +You can learn more about pull-streams at: + +- [The history of Node.js streams, nodebp April 2014](https://www.youtube.com/watch?v=g5ewQEuXjsQ) +- [The history of streams, 2016](http://dominictarr.com/post/145135293917/history-of-streams) +- [pull-streams, the simple streaming primitive](http://dominictarr.com/post/149248845122/pull-streams-pull-streams-are-a-very-simple) +- [pull-streams documentation](https://pull-stream.github.io/) + +#### Converting `pull-streams` to Node.js Streams + +If you are a Node.js streams user, you can convert a pull-stream to a Node.js stream using the module [`pull-stream-to-stream`](https://github.com/pull-stream/pull-stream-to-stream), giving you an instance of a Node.js stream that is linked to the pull-stream. For example: + +```js +const pullToStream = require('pull-stream-to-stream') + +const nodeStreamInstance = pullToStream(pullStreamInstance) +// nodeStreamInstance is an instance of a Node.js Stream +``` + +To learn more about this utility, visit https://pull-stream.github.io/#pull-stream-to-stream. + +LICENSE MIT diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..f0d714d --- /dev/null +++ b/server/package.json @@ -0,0 +1,82 @@ +{ + "name": "libp2p-websocket-star-rendezvous", + "version": "0.4.0", + "description": "The rendezvous service for libp2p-websocket-star", + "leadMaintainer": "Jacob Heun ", + "main": "src/index.js", + "files": [ + "dist", + "src" + ], + "bin": { + "rendezvous": "src/bin.js", + "websocket-star": "src/bin.js" + }, + "scripts": { + "start": "node src/bin.js", + "lint": "aegir lint", + "build": "aegir build", + "test": "aegir test -t node", + "test:node": "aegir test -t node", + "release": "aegir release -t node", + "release-minor": "aegir release --type minor -t node", + "release-major": "aegir release --type major -t node", + "coverage": "aegir coverage", + "coverage-publish": "aegir coverage --provider coveralls" + }, + "keywords": [ + "libp2p", + "websocket" + ], + "license": "MIT", + "dependencies": { + "data-queue": "0.0.3", + "debug": "^4.1.1", + "@hapi/hapi": "^18.3.1", + "@hapi/inert": "^5.2.1", + "libp2p-crypto": "~0.17.0", + "mafmt": "^6.0.7", + "menoetius": "~0.0.2", + "merge-recursive": "0.0.3", + "minimist": "^1.2.0", + "multiaddr": "^6.1.0", + "once": "^1.4.0", + "peer-id": "~0.13.1", + "peer-info": "~0.16.0", + "prom-client": "^11.5.3", + "socket.io": "^2.0.4", + "socket.io-client": "^2.0.4", + "socket.io-pull-stream": "^0.1.1", + "uuid": "^3.1.0" + }, + "directories": { + "test": "test" + }, + "devDependencies": { + "aegir": "^19.0.5", + "chai": "^4.2.0", + "dirty-chai": "^2.0.1", + "lodash": "^4.17.11" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p-websocket-star-rendezvous.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p-websocket-star-rendezvous/issues" + }, + "homepage": "https://github.com/libp2p/js-libp2p-websocket-star-rendezvous#readme", + "contributors": [ + "David Dias ", + "Dirk McCormick ", + "Haad ", + "Jacob Heun ", + "Jim Pick ", + "Justin Maier ", + "LEE JAE HO ", + "Vasco Santos ", + "Victor Bjelkholm ", + "achingbrain ", + "mkg20001 " + ] +} diff --git a/server/src/bin.js b/server/src/bin.js new file mode 100755 index 0000000..4327db2 --- /dev/null +++ b/server/src/bin.js @@ -0,0 +1,42 @@ +#!/usr/bin/env node + +'use strict' + +const signalling = require('./index') +const argv = require('minimist')(process.argv.slice(2)) + +/* eslint-disable no-console */ + +async function start () { + const server = await signalling.start({ + port: argv.port || argv.p || process.env.PORT || 9090, + host: argv.host || argv.h || process.env.HOST || '0.0.0.0', + key: argv.key || process.env.KEY, + cert: argv.cert || process.env.CERT, + pfx: argv.pfx || process.env.PFX, + passphrase: argv.passphrase || process.env.PFX_PASSPHRASE, + cryptoChallenge: !(argv.disableCryptoChallenge || process.env.DISABLE_CRYPTO_CHALLENGE), + strictMultiaddr: !(argv.disableStrictMultiaddr || process.env.DISABLE_STRICT_MULTIADDR), + metrics: !(argv.disableMetrics || process.env.DISABLE_METRICS) + }) + + console.log('Listening on:', server.info.uri) + + process.on('SIGINT', async () => { + try { + await server.stop() + } catch (err) { + console.error(err) + process.exit(2) + } + + console.log('Rendezvous server stopped') + process.exit(0) + }) +} + +start() + .catch((err) => { + console.error(err) + process.exit(2) + }) diff --git a/server/src/config.js b/server/src/config.js new file mode 100644 index 0000000..6a31c85 --- /dev/null +++ b/server/src/config.js @@ -0,0 +1,22 @@ +'use strict' + +const debug = require('debug') +const log = debug('libp2p-websocket-star-rendezvous') +log.error = debug('libp2p-websocket-star-rendezvous:error') + +module.exports = { + log: log, + hapi: { + port: process.env.PORT || 13579, + host: '0.0.0.0', + options: { + routes: { + cors: true + } + } + }, + refreshPeerListIntervalMS: 10000, + cryptoChallenge: true, + strictMultiaddr: false, + metrics: false +} diff --git a/server/src/index.html b/server/src/index.html new file mode 100644 index 0000000..ad9e848 --- /dev/null +++ b/server/src/index.html @@ -0,0 +1,63 @@ + + + + + + + Signalling Server + + + + + +
+
+ Libp2p Logo +

This is a libp2p-websocket-star signalling-server

+

Signaling Servers are used in libp2p to allow browsers and clients with restricted port-forwarding
to communicate with other peers in the libp2p network

+
+ ยป Learn more +
+ + + + + diff --git a/server/src/index.js b/server/src/index.js new file mode 100644 index 0000000..a421b1f --- /dev/null +++ b/server/src/index.js @@ -0,0 +1,59 @@ +'use strict' + +const Hapi = require('@hapi/hapi') +const path = require('path') +const menoetius = require('menoetius') +const merge = require('merge-recursive').recursive +const Inert = require('@hapi/inert') +const { readFileSync } = require('fs') + +exports = module.exports + +exports.start = async (options = {}) => { + const config = merge(Object.assign({}, require('./config')), Object.assign({}, options)) + const log = config.log + + const port = options.port || config.hapi.port + const host = options.host || config.hapi.host + + let tls + if (options.key && options.cert) { + tls = { + key: readFileSync(options.key), + cert: readFileSync(options.cert), + passphrase: options.passphrase + } + } else if (options.pfx && options.passphrase) { + tls = { + pfx: readFileSync(options.pfx), + passphrase: options.passphrase + } + } + + const http = new Hapi.Server(Object.assign({ + port, + host, + tls + }, config.hapi.options)) + + await http.register(Inert) + await http.start() + + log('rendezvous server has started on: ' + http.info.uri) + + http.peers = require('./routes')(config, http).peers + + http.route({ + method: 'GET', + path: '/', + handler: (request, reply) => reply.file(path.join(__dirname, 'index.html'), { + confine: false + }) + }) + + if (config.metrics) { + menoetius.instrument(http) + } + + return http +} diff --git a/server/src/routes.js b/server/src/routes.js new file mode 100644 index 0000000..8811df1 --- /dev/null +++ b/server/src/routes.js @@ -0,0 +1,229 @@ +'use strict' + +/* eslint-disable standard/no-callback-literal */ +// Needed because JSON.stringify(Error) returns "{}" + +const SocketIO = require('socket.io') +const sp = require('socket.io-pull-stream') +const util = require('./utils') +const uuid = require('uuid') +const client = require('prom-client') +const fake = { + gauge: { + set: () => {} + }, + counter: { + inc: () => {} + } +} + +module.exports = (config, http) => { + const log = config.log + const io = new SocketIO(http.listener) + const proto = new util.Protocol(log) + const getConfig = () => config + + proto.addRequest('ss-join', ['multiaddr', 'string', 'function'], join) + proto.addRequest('ss-leave', ['multiaddr'], leave) + proto.addRequest('disconnect', [], disconnect) + proto.addRequest('ss-dial', ['multiaddr', 'multiaddr', 'string', 'function'], dial) // dialFrom, dialTo, dialId, cb + io.on('connection', handle) + + log('create new server', config) + + const _peers = {} + const nonces = {} + + const peersMetric = config.metrics ? new client.Gauge({ name: 'rendezvous_peers', help: 'peers online now' }) : fake.gauge + const dialsSuccessTotal = config.metrics ? new client.Counter({ name: 'rendezvous_dials_total_success', help: 'sucessfully completed dials since server started' }) : fake.counter + const dialsFailureTotal = config.metrics ? new client.Counter({ name: 'rendezvous_dials_total_failure', help: 'failed dials since server started' }) : fake.counter + const dialsTotal = config.metrics ? new client.Counter({ name: 'rendezvous_dials_total', help: 'all dials since server started' }) : fake.counter + const joinsSuccessTotal = config.metrics ? new client.Counter({ name: 'rendezvous_joins_total_success', help: 'sucessfully completed joins since server started' }) : fake.counter + const joinsFailureTotal = config.metrics ? new client.Counter({ name: 'rendezvous_joins_total_failure', help: 'failed joins since server started' }) : fake.counter + const joinsTotal = config.metrics ? new client.Counter({ name: 'rendezvous_joins_total', help: 'all joins since server started' }) : fake.counter + + const refreshMetrics = () => peersMetric.set(Object.keys(_peers).length) + + function safeEmit (addr, event, arg) { + const peer = _peers[addr] + if (!peer) { + log('trying to emit %s but peer is gone', event) + return + } + + peer.emit(event, arg) + } + + function handle (socket) { + socket.addrs = [] + socket.cleanaddrs = {} + socket.setMaxListeners(0) + sp(socket, { + codec: 'buffer' + }) + proto.handleSocket(socket) + } + + // join this signaling server network + function join (socket, multiaddr, pub, cb) { + const log = socket.log = config.log.bind(config.log, '[' + socket.id + ']') + + if (getConfig().strictMultiaddr && !util.validateMa(multiaddr)) { + joinsTotal.inc() + joinsFailureTotal.inc() + return cb('Invalid multiaddr') + } + + if (getConfig().cryptoChallenge) { + if (!pub.length) { + joinsTotal.inc() + joinsFailureTotal.inc() + return cb('Crypto Challenge required but no Id provided') + } + + if (!nonces[socket.id]) { + nonces[socket.id] = {} + } + + if (nonces[socket.id][multiaddr]) { + log('response cryptoChallenge', multiaddr) + + nonces[socket.id][multiaddr].key.verify( + Buffer.from(nonces[socket.id][multiaddr].nonce), + Buffer.from(pub, 'hex'), + (err, ok) => { + if (err || !ok) { + joinsTotal.inc() + joinsFailureTotal.inc() + } + if (err) { return cb('Crypto error') } // the errors NEED to be a string otherwise JSON.stringify() turns them into {} + if (!ok) { return cb('Signature Invalid') } + + joinFinalize(socket, multiaddr, cb) + }) + } else { + joinsTotal.inc() + const addr = multiaddr.split('ipfs/').pop() + + log('do cryptoChallenge', multiaddr, addr) + + util.getIdAndValidate(pub, addr, (err, key) => { + if (err) { joinsFailureTotal.inc(); return cb(err) } + const nonce = uuid() + uuid() + + socket.once('disconnect', () => { + delete nonces[socket.id] + }) + + nonces[socket.id][multiaddr] = { nonce: nonce, key: key } + cb(null, nonce) + }) + } + } else { + joinsTotal.inc() + joinFinalize(socket, multiaddr, cb) + } + } + + function joinFinalize (socket, multiaddr, cb) { + const log = getConfig().log.bind(getConfig().log, '[' + socket.id + ']') + _peers[multiaddr] = socket + if (!socket.stopSendingPeersIntv) socket.stopSendingPeersIntv = {} + joinsSuccessTotal.inc() + refreshMetrics() + socket.addrs.push(multiaddr) + log('registered as', multiaddr) + + // discovery + + let refreshInterval = setInterval(sendPeers, getConfig().refreshPeerListIntervalMS) + + socket.once('disconnect', stopSendingPeers) + + sendPeers() + + function sendPeers () { + const list = Object.keys(_peers) + log(multiaddr, 'sending', (list.length - 1).toString(), 'peer(s)') + list.forEach((mh) => { + if (mh === multiaddr) { + return + } + + safeEmit(mh, 'ws-peer', multiaddr) + }) + } + + function stopSendingPeers () { + if (refreshInterval) { + log(multiaddr, 'stop sending peers') + clearInterval(refreshInterval) + refreshInterval = null + } + } + + socket.stopSendingPeersIntv[multiaddr] = stopSendingPeers + + const otherPeers = Object.keys(_peers).filter(mh => mh !== multiaddr) + cb(null, null, otherPeers) + } + + function leave (socket, multiaddr) { + if (_peers[multiaddr] && _peers[multiaddr].id === socket.id) { + socket.log('leaving', multiaddr) + delete _peers[multiaddr] + socket.addrs = socket.addrs.filter(m => m !== multiaddr) + if (socket.stopSendingPeersIntv[multiaddr]) { + socket.stopSendingPeersIntv[multiaddr]() + delete socket.stopSendingPeersIntv[multiaddr] + } + refreshMetrics() + } + } + + function disconnect (socket) { + socket.log('disconnected') + Object.keys(_peers).forEach((mh) => { + if (_peers[mh].id === socket.id) { + leave(socket, mh) + } + }) + } + + function dial (socket, from, to, dialId, cb) { + const log = socket.log + const s = socket.addrs.filter((a) => a === from)[0] + + dialsTotal.inc() + + if (!s) { + dialsFailureTotal.inc() + return cb('Not authorized for this address') + } + + log(from, 'is dialing', to) + const peer = _peers[to] + + if (!peer) { + dialsFailureTotal.inc() + return cb('Peer not found') + } + + socket.createProxy(dialId + '.dialer', peer) + + peer.emit('ss-incomming', dialId, from, err => { + if (err) { + dialsFailureTotal.inc() + return cb(err) + } + + dialsSuccessTotal.inc() + peer.createProxy(dialId + '.listener', socket) + cb() + }) + } + + return { + peers: () => _peers + } +} diff --git a/server/src/utils.js b/server/src/utils.js new file mode 100644 index 0000000..b026b7c --- /dev/null +++ b/server/src/utils.js @@ -0,0 +1,119 @@ +'use strict' + +const multiaddr = require('multiaddr') +const Id = require('peer-id') +const crypto = require('libp2p-crypto') +const mafmt = require('mafmt') + +function isIP (ma) { + const protos = ma.protos() + + if (protos[0].code !== 4 && protos[0].code !== 41) { + return false + } + if (protos[1].code !== 6 && protos[1].code !== 17) { + return false + } + + return true +} + +function cleanUrlSIO (ma) { + const maStrSplit = ma.toString().split('/') + + if (isIP(ma)) { + if (maStrSplit[1] === 'ip4') { + return 'http://' + maStrSplit[2] + ':' + maStrSplit[4] + } else if (maStrSplit[1] === 'ip6') { + return 'http://[' + maStrSplit[2] + ']:' + maStrSplit[4] + } else { + throw new Error('invalid multiaddr: ' + ma.toString()) + } + } else if (multiaddr.isName(ma)) { + const wsProto = ma.protos()[1].name + if (wsProto === 'ws') { + return 'http://' + maStrSplit[2] + } else if (wsProto === 'wss') { + return 'https://' + maStrSplit[2] + } else { + throw new Error('invalid multiaddr: ' + ma.toString()) + } + } else { + throw new Error('invalid multiaddr: ' + ma.toString()) + } +} + +const types = { + string: (v) => (typeof v === 'string'), + object: (v) => (typeof v === 'object'), + multiaddr: (v) => { + if (!types.string(v)) { return } + + try { + multiaddr(v) + return true + } catch (err) { + return false + } + }, + function: (v) => (typeof v === 'function') +} + +function validate (def, data) { + if (!Array.isArray(data)) throw new Error('Data is not an array') + def.forEach((type, index) => { + if (!types[type]) { + throw new Error('Type ' + type + ' does not exist') + } + + if (!types[type](data[index])) { + throw new Error('Data at index ' + index + ' is invalid for type ' + type) + } + }) +} + +function Protocol (log) { + log = log || function noop () {} + + this.requests = {} + this.addRequest = (name, def, handle) => { + this.requests[name] = { def: def, handle: handle } + } + this.handleSocket = (socket) => { + socket.r = {} + for (let request in this.requests) { + if (Object.prototype.hasOwnProperty.call(this.requests, request)) { + const r = this.requests[request] + socket.on(request, function () { + const data = [...arguments] + try { + validate(r.def, data) + data.unshift(socket) + r.handle.apply(null, data) + } catch (err) { + log(err) + log('peer %s has sent invalid data for request %s', socket.id || '', request, data) + } + }) + } + } + } +} + +function getIdAndValidate (pub, id, cb) { + Id.createFromPubKey(Buffer.from(pub, 'hex')) + .then(_id => { + if (_id.toB58String() !== id) { + throw Error('Id is not matching') + } + + return crypto.keys.unmarshalPublicKey(Buffer.from(pub, 'hex')) + }, cb) +} + +exports = module.exports +exports.cleanUrlSIO = cleanUrlSIO +exports.validate = validate +exports.Protocol = Protocol +exports.getIdAndValidate = getIdAndValidate +exports.validateMa = (ma) => mafmt.WebSocketStar.matches(multiaddr(ma)) diff --git a/server/test/rendezvous.spec.js b/server/test/rendezvous.spec.js new file mode 100644 index 0000000..5d29db5 --- /dev/null +++ b/server/test/rendezvous.spec.js @@ -0,0 +1,213 @@ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) +const io = require('socket.io-client') +const multiaddr = require('multiaddr') +const uuid = require('uuid') + +const rendezvous = require('../src') + +describe('rendezvous', () => { + it('start and stop signalling server (default port)', async () => { + const server = await rendezvous.start() + + expect(server.info.port).to.equal(13579) + expect(server.info.protocol).to.equal('http') + expect(server.info.address).to.equal('0.0.0.0') + + await server.stop() + }) + + it('start and stop signalling server (custom port)', async () => { + const options = { + port: 12345 + } + + const server = await rendezvous.start(options) + + expect(server.info.port).to.equal(options.port) + expect(server.info.protocol).to.equal('http') + expect(server.info.address).to.equal('0.0.0.0') + + await server.stop() + }) +}) + +describe('signalling server client', () => { + const sioOptions = { + transports: ['websocket'], + 'force new connection': true + } + + let sioUrl + let r + let c1 + let c2 + let c3 + let c4 + + let c1mh = multiaddr('/ip4/127.0.0.1/tcp/9090/ws/p2p-websocket-star/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo1') + let c2mh = multiaddr('/ip4/127.0.0.1/tcp/9090/ws/p2p-websocket-star/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo2') + let c3mh = multiaddr('/ip4/127.0.0.1/tcp/9090/ws/p2p-websocket-star/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo3') + let c4mh = multiaddr('/ip4/127.0.0.1/tcp/9090/ws/p2p-websocket-star/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4') + + before(async () => { + const options = { + port: 12345, + refreshPeerListIntervalMS: 1000, + cryptoChallenge: false, + strictMultiaddr: false, + metrics: true + } + + const server = await rendezvous.start(options) + + expect(server.info.port).to.equal(12345) + expect(server.info.protocol).to.equal('http') + expect(server.info.address).to.equal('0.0.0.0') + sioUrl = server.info.uri + r = server + }) + + after(async () => { + if (c1) { + c1.disconnect() + } + + if (c2) { + c2.disconnect() + } + + if (r) { + await r.stop() + } + }) + + it('zero peers', () => { + expect(Object.keys(r.peers).length).to.equal(0) + }) + + it('connect one client', (done) => { + c1 = io.connect(sioUrl, sioOptions) + c1.on('connect', done) + }) + + it('connect three more clients', (done) => { + let count = 0 + + c2 = io.connect(sioUrl, sioOptions) + c3 = io.connect(sioUrl, sioOptions) + c4 = io.connect(sioUrl, sioOptions) + + c2.on('connect', connected) + c3.on('connect', connected) + c4.on('connect', connected) + + function connected () { + if (++count === 3) { + done() + } + } + }) + + it('ss-join first client', (done) => { + c1.emit('ss-join', c1mh.toString(), '', (err, sig, peers) => { + expect(err).to.not.exist() + expect(peers).to.eql([]) + expect(Object.keys(r.peers()).length).to.equal(1) + done() + }) + }) + + it('ss-join and ss-leave second client', (done) => { + let c1WsPeerEvent + c1.once('ws-peer', (p) => { + c1WsPeerEvent = p + }) + + c2.emit('ss-join', c2mh.toString(), '', (err, sig, peers) => { + expect(err).to.not.exist() + expect(peers).to.eql([c1mh.toString()]) + expect(c1WsPeerEvent).to.equal(c2mh.toString()) + expect(Object.keys(r.peers()).length).to.equal(2) + c2.emit('ss-leave', c2mh.toString()) + + setTimeout(() => { + expect(Object.keys(r.peers()).length).to.equal(1) + done() + }, 10) + }) + }) + + it('ss-join and disconnect third client', (done) => { + c3.emit('ss-join', c3mh.toString(), '', (err, sig, peers) => { + expect(err).to.not.exist() + expect(peers).to.eql([c1mh.toString()]) + expect(Object.keys(r.peers()).length).to.equal(2) + c3.disconnect() + setTimeout(() => { + expect(Object.keys(r.peers()).length).to.equal(1) + done() + }, 10) + }) + }) + + it('ss-join the fourth', (done) => { + c1.once('ws-peer', (multiaddr) => { + expect(multiaddr).to.equal(c4mh.toString()) + expect(Object.keys(r.peers()).length).to.equal(2) + done() + }) + c4.emit('ss-join', c4mh.toString(), '', () => {}) + }) + + it('c1 dial c4', done => { + const dialId = uuid() + c4.once('ss-incomming', (dialId, dialFrom, cb) => { + expect(dialId).to.eql(dialId) + expect(dialFrom).to.eql(c1mh.toString()) + cb() + }) + c1.emit('ss-dial', c1mh.toString(), c4mh.toString(), dialId, err => { + expect(err).to.not.exist() + done() + }) + }) + + it('c1 dial c2 fail (does not exist() anymore)', done => { + const dialId = uuid() + c1.emit('ss-dial', c1mh.toString(), c2mh.toString(), dialId, err => { + expect(err).to.exist() + done() + }) + }) + + it('disconnects every client', (done) => { + [c1, c2, c3, c4].forEach((c) => c.disconnect()) + done() + }) + + it('emits ws-peer every second', (done) => { + let peersEmitted = 0 + + c1 = io.connect(sioUrl, sioOptions) + c2 = io.connect(sioUrl, sioOptions) + c1.emit('ss-join', '/ip4/0.0.0.0', '', err => expect(err).to.not.exist()) + c2.emit('ss-join', '/ip4/127.0.0.1', '', err => expect(err).to.not.exist()) + + c1.on('ws-peer', (p) => { + expect(p).to.be.equal('/ip4/127.0.0.1') + check() + }) + + function check () { + if (++peersEmitted === 2) { + done() + } + } + }).timeout(4000) +})