├── .gitignore ├── LICENSE ├── README.md ├── client.js ├── package.json ├── server.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # This isn't an application, so I don't want to track this. 64 | package-lock.json 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 RangerMauve 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyperswarm-proxy-ws 2 | Proxy hyperswarm connections over websockets with auto-reconnect logic 3 | 4 | ``` 5 | npm -s hyperswarm-proxy-ws 6 | ``` 7 | 8 | Uses the [hyperswarm-proxy](https://github.com/RangerMauve/hyperswarm-proxy) module. 9 | 10 | ## Example 11 | 12 | ```js 13 | 14 | const HyperswarmServer = require('hyperswarm-proxy-ws/server') 15 | 16 | // Initialize the proxy server 17 | // Also specify any options for hyperswarm here 18 | // https://github.com/hyperswarm/hyperswarm 19 | const server = new HyperswarmServer() 20 | 21 | // Start listening on clients via websocket protocol 22 | server.listen(3472) 23 | 24 | 25 | const HyperswarmClient = require('hyperswarm-proxy-ws/client') 26 | 27 | // Initialize a proxied hyperswarm 28 | // Also specify any options for hyperswarm-proxy client 29 | // https://github.com/RangerMauve/hyperswarm-proxy#client 30 | const swarm = new HyperswarmClient({ 31 | // Specify a list of proxy servers available to connect to 32 | proxy: ['ws://127.0.0.1:3472'] 33 | }) 34 | 35 | // Same as with hyperswarm 36 | swarm.on('connection', (connection, info) => { 37 | const stream = getSomeStream(info.topic) 38 | 39 | // Pipe the data somewhere 40 | // E.G. hyperdrive.replicate() 41 | connection.pipe(stream).pipe(connection) 42 | }) 43 | 44 | swarm.join(topic) 45 | 46 | swarm.leave(topic) 47 | ``` 48 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | const HyperswarmProxyClient = require('hyperswarm-proxy/client') 2 | const websocket = require('websocket-stream') 3 | 4 | const DEFAULT_PORT = '4977' // HYPR on a cellphone keypad 5 | const LOCAL_PROXY = `ws://localhost:${DEFAULT_PORT}` 6 | const DEFAULT_PROXY = [LOCAL_PROXY] 7 | const DEFAULT_RECONNECT_DELAY = 1000 8 | 9 | class HyperswarmProxyWSClient extends HyperswarmProxyClient { 10 | constructor (opts = {}) { 11 | super(opts) 12 | 13 | const { proxy = DEFAULT_PROXY, reconnectDelay = DEFAULT_RECONNECT_DELAY } = opts 14 | 15 | this.reconnectDelay = reconnectDelay 16 | this.proxy = null 17 | 18 | this._urls = typeof proxy === 'string' ? [proxy] : proxy 19 | this._urlIndex = 0 20 | 21 | this.reconnect() 22 | } 23 | 24 | reconnect () { 25 | this._nextUrl(); 26 | 27 | const localSocket = websocket(LOCAL_PROXY) 28 | 29 | // Re-emit errors 30 | localSocket.on('error', (e) => this.emit('connection-error', e)) 31 | 32 | localSocket.once('error', () => { 33 | // Couldn't connect to a local proxy 34 | // Attempt to connect to the internet proxy 35 | const proxySocket = websocket(this.proxy) 36 | 37 | // Re-emit errors 38 | proxySocket.on('error', (e) => this.emit('connection-error', e)) 39 | 40 | proxySocket.once('close', () => { 41 | setTimeout(() => { 42 | if (this.destroyed) return 43 | this.reconnect() 44 | }, this.reconnectDelay) 45 | }) 46 | 47 | super.reconnect(proxySocket) 48 | }) 49 | super.reconnect(localSocket) 50 | } 51 | 52 | _nextUrl() { 53 | this.proxy = this._urls[this._urlIndex++ % this._urls.length] 54 | return this.proxy 55 | } 56 | } 57 | 58 | module.exports = HyperswarmProxyWSClient 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperswarm-proxy-ws", 3 | "version": "1.2.0", 4 | "description": "Proxy hyperswarm connections over websockets with auto-reconnect logic", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/RangerMauve/hyperswarm-proxy-ws.git" 12 | }, 13 | "keywords": [ 14 | "hyperswarm", 15 | "ws", 16 | "websocket", 17 | "dat" 18 | ], 19 | "author": "rangermauve", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/RangerMauve/hyperswarm-proxy-ws/issues" 23 | }, 24 | "homepage": "https://github.com/RangerMauve/hyperswarm-proxy-ws#readme", 25 | "dependencies": { 26 | "hyperswarm-proxy": "^1.4.0", 27 | "websocket-stream": "^5.5.0" 28 | }, 29 | "devDependencies": { 30 | "hyperswarm": "^2.2.1", 31 | "get-port": "^5.0.0", 32 | "tape": "^4.11.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const HyperswarmProxyServer = require('hyperswarm-proxy/server') 2 | const websocket = require('websocket-stream') 3 | const http = require('http') 4 | 5 | class HyperswarmProxyWSServer extends HyperswarmProxyServer { 6 | constructor (opts = {}) { 7 | super(opts) 8 | const { server } = opts 9 | if (server) this.listenOnServer(server) 10 | } 11 | 12 | listenOnServer (server) { 13 | this.server = server 14 | this.websocketServer = websocket.createServer({ server }, (socket) => { 15 | this.handleStream(socket) 16 | }) 17 | } 18 | 19 | listen (...args) { 20 | const server = http.createServer() 21 | this.listenOnServer(server) 22 | server.listen(...args) 23 | } 24 | 25 | destroy (cb) { 26 | // Closing the server rather than the websocket server actually closes the handles. 🤯 27 | this.server.close(() => { 28 | super.destroy(cb) 29 | }) 30 | } 31 | } 32 | 33 | module.exports = HyperswarmProxyWSServer 34 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const hyperswarm = require('@hyperswarm/network') 2 | const Client = require('./client') 3 | const Server = require('./server') 4 | 5 | const test = require('tape') 6 | const getPort = require('get-port') 7 | const crypto = require('crypto') 8 | 9 | test('discover and make connections', async (t) => { 10 | // Each test should use a different topic to avoid connecting to other machines running the test 11 | const TEST_TOPIC = makeTopic('HYPERSWARM-PROXY-TEST' + Math.random()) 12 | const TEST_MESSAGE = 'Hello World' 13 | 14 | t.plan(5) 15 | 16 | try { 17 | const server = new Server() 18 | 19 | const network = hyperswarm({ 20 | socket: handleSocket 21 | }) 22 | 23 | const port = await getPort() 24 | 25 | const proxy = `ws://localhost:${port}` 26 | 27 | server.listen(port) 28 | 29 | const swarm = new Client({ 30 | proxy: [ 31 | 'ws://localhost:4000', // wrong url 32 | proxy, 33 | ] 34 | }) 35 | 36 | let connectionCount = 0 37 | 38 | process.once('SIGINT', cleanupAndExit) 39 | // process.once('uncaughtException', cleanupAndExit) 40 | 41 | swarm.on('connection', handleConnection) 42 | 43 | network.bind(() => { 44 | network.announce(TEST_TOPIC) 45 | swarm.join(TEST_TOPIC) 46 | }) 47 | 48 | function cleanupAndExit (e) { 49 | if (e) { 50 | t.fail(e) 51 | } 52 | cleanup(() => { 53 | process.exit(0) 54 | }) 55 | } 56 | 57 | function cleanup (cb) { 58 | swarm.destroy(() => { 59 | server.destroy(() => { 60 | network.close(() => { 61 | if (cb) cb() 62 | }) 63 | }) 64 | }) 65 | } 66 | function handleConnection (connection, info) { 67 | connection.on('error', () => { 68 | // Whatever 69 | }) 70 | 71 | if (connectionCount++) return connection.end() 72 | 73 | t.equal(swarm.proxy, proxy, 'should use the working proxy') 74 | 75 | t.deepEqual(info.peer.topic, TEST_TOPIC, 'got connection in client') 76 | connection.on('data', () => { 77 | t.pass('got data from peer') 78 | connection.end() 79 | }) 80 | 81 | connection.once('close', () => { 82 | cleanup(() => { 83 | t.end() 84 | }) 85 | }) 86 | connection.write(TEST_MESSAGE) 87 | } 88 | 89 | function handleSocket (socket) { 90 | t.pass('got connection to peer') 91 | socket.on('data', () => { 92 | t.pass('got data to peer') 93 | socket.end(TEST_MESSAGE) 94 | }) 95 | } 96 | } catch (e) { 97 | t.fail(e) 98 | } 99 | }) 100 | 101 | function makeTopic (text) { 102 | return crypto.createHash('sha256') 103 | .update(text) 104 | .digest() 105 | } 106 | --------------------------------------------------------------------------------