├── .gitignore ├── LICENSE ├── README.md ├── bin.js ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | yarn* 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is released under the MIT license: 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyproxy 2 | 3 | A peer-to-peer proxy server and client that uses [Hypercore Protocol](https://hypercore-protocol.org/) to proxy TCP connections over hyperswarm and hypercore-protocol. 4 | 5 | This provides an easy way to e.g. share an HTTP server running on your computer with friends, without having to deal with port forwardings etc, because Hypercore Protocol handles this for you. 6 | 7 | *This is an experiment and not yet tested for anything serious.* 8 | 9 | - When opening an inbound proxy server, a random `key` will be created and printed 10 | - Anyone who knows this `key` can connect to the proxy and expose it as if it were a local server 11 | - There can be more than one inbound proxy per key. When connecting, currently whatever peer answers first is used, and others are fallbacks if the first fails. 12 | - *TODO:* Add optional capability creation when opening inbound proxies and verification when connecting to them 13 | 14 | ## Installation 15 | 16 | ```sh 17 | npm install -g hyproxy 18 | ``` 19 | 20 | ## Example 21 | 22 | Let's say you want to share a HTTP server from your computer with friends. Maybe to quickly share some files, or to test some web app, or whatever. For example, you can `npm install -g http-server` for a simple http server. 23 | 24 | Now, you can do the following: 25 | ``` 26 | $ http-server /some/directory 27 | 28 | Starting up http-server, serving . 29 | Available on: 30 | http://127.0.0.1:8080 31 | 32 | $ hyproxy listen -p 8080 33 | 34 | inbound proxy to localhost:8080 listening. 35 | access via f1dd4fa6801a659168c48eab3018f168a621f58677f5cfa6e495da16a7dd5218 36 | ``` 37 | and then send the printed long key to others. they can then do: 38 | ``` 39 | $ hyproxy connect -k f1dd4fa6801a659168c48eab3018f168a621f58677f5cfa6e495da16a7dd5218 40 | outbound proxy to f1dd4fa6801a659168c48eab3018f168a621f58677f5cfa6e495da16a7dd5218 opened. 41 | access via localhost:9999 42 | ``` 43 | and then your friends can open [`http://localhost:9999`](http://localhost:9999) to access the HTTP server you just opened. 44 | 45 | ## Command-line usage 46 | 47 | ``` 48 | USAGE: hyproxy [options] 49 | 50 | Options in listen mode: 51 | -p, --port Port to proxy to (required) 52 | -h, --host Hostname to proxy to (default: localhost) 53 | -s, --storage Storage directory to persist keys across restarts (optional) 54 | 55 | Options in connect mode: 56 | -k, --key Key to connect to (required) 57 | -p, --port Port for local proxy server (default: 9990 or a free port) 58 | -h, --host Hostname for local proxy server (default: localhost) 59 | ``` 60 | 61 | ## API usage 62 | 63 | ```javascript 64 | const HyperProxy = require('hyproxy') 65 | const hyproxy = new HyperProxy({ storage: '/tmp/hyproxy' }) 66 | await hyproxy.outbound(key, port, host) 67 | ``` 68 | 69 | #### `proxy = new HyperProxy(opts)` 70 | 71 | Create a new proxy manager. 72 | 73 | Options include: 74 | - `storage`: Storage to persist keys (optional, default to in-memory) 75 | - `corestore`: Pass your [corestore](https://github.com/andrewosh/corestore) instance (optional) 76 | - `networker`: Pass your [@corestore/networker](https://github.com/andrewosh/corestore-networker) instance (optional) 77 | 78 | #### `await proxy.outbound(key, port, host)` 79 | 80 | Create a new outbound proxy that connects to a peer on `key` and exposes a local proxy server on `host:port`. 81 | 82 | - `key`: The key to an inbound hyproxy server (required) 83 | - `port`: Port for local proxy server (defaults to a free port) 84 | - `host`: Hostname on which the local proxy server binds (defaults to `localhost`) 85 | 86 | Returns an object with `{ key, port, host }`. 87 | 88 | #### `await proxy.inbound(key, port, host)` 89 | 90 | Create a new inbound proxy that listens for peers on `key` and forwards connections to `host:port`. 91 | 92 | - `key`: Hypercore key to accept connections on. May be `null`, then the key will be derived so that it stays the same for the same `host:port` pairs (if storage is not inmemory) 93 | - `port`: Port to forward connections to (required) 94 | - `host`: Host to forward connections to 95 | 96 | Returns an object with `{ key, port, host }`. 97 | -------------------------------------------------------------------------------- /bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const minimist = require('minimist') 4 | const HyperProxy = require('.') 5 | 6 | const argv = minimist(process.argv.slice(2), { 7 | alias: { 8 | p: 'port', 9 | h: 'host', 10 | k: 'key', 11 | s: 'storage' 12 | } 13 | }) 14 | 15 | const mode = argv._[0] 16 | main(mode, argv).catch(onerror) 17 | 18 | async function main (mode, opts) { 19 | const proxies = new HyperProxy({ storage: opts.storage }) 20 | if (mode === 'connect') { 21 | const proxy = await proxies.outbound(opts.key, opts.port, opts.host) 22 | console.log(`outbound proxy to ${proxy.key.toString('hex')} connected.`) 23 | console.log(`naccess via ${proxy.host}:${proxy.port}`) 24 | } else if (mode === 'listen') { 25 | const proxy = await proxies.inbound(opts.key, opts.port, opts.host) 26 | console.log(`inbound proxy to ${proxy.host}:${proxy.port} listening.`) 27 | console.log(`access via ${proxy.key.toString('hex')}`) 28 | } else { 29 | onerror() 30 | } 31 | // wait forever 32 | // TODO: Die on errors ;) 33 | await new Promise(resolve => {}) 34 | } 35 | 36 | function onerror (err) { 37 | if (err) console.error(err.message) 38 | console.error(`USAGE: hyproxy [options] 39 | 40 | Options in listen mode: 41 | -p, --port Port to proxy to (required) 42 | -h, --host Hostname to proxy to (default: localhost) 43 | -s, --storage Storage directory to persist keys across restarts (optional) 44 | 45 | Options in connect mode: 46 | -k, --key Key to connect to (required) 47 | -p, --port Port for local proxy server (default: 9999) 48 | -h, --host Hostname for local proxy server (default: localhost) 49 | `) 50 | } 51 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const net = require('net') 2 | const prettyHash = require('pretty-hash') 3 | const Corestore = require('corestore') 4 | const Networker = require('@corestore/networker') 5 | const ram = require('random-access-memory') 6 | const debug = require('debug')('hyproxy') 7 | const getPort = require('get-port') 8 | const { EventEmitter } = require('events') 9 | 10 | const TYP_DISCOVER = 0 11 | const TYP_ACK = 1 12 | const TYP_CONNECT = 2 13 | const TYP_DATA = 3 14 | const TYP_CLOSE = 4 15 | 16 | const NAMESPACE = 'hyproxy-v1' 17 | const EXTENSION_NAME = NAMESPACE + ':extension' 18 | 19 | const kRemoteClosed = Symbol('remote-closed') 20 | 21 | module.exports = class HyperProxy extends EventEmitter { 22 | constructor (opts = {}) { 23 | super() 24 | this._corestore = opts.corestore || new Corestore(opts.storage || ram) 25 | this._networker = opts.networker || new Networker(this._corestore) 26 | } 27 | 28 | async open () { 29 | if (this.opened) return 30 | await new Promise((resolve, reject) => { 31 | this._corestore.ready(err => err ? reject(err) : resolve()) 32 | }) 33 | debug(`init, pubKey: ${prettyHash(this._networker.keyPair.publicKey)}`) 34 | this.opened = true 35 | } 36 | 37 | async outbound (key, port, host = 'localhost') { 38 | await this.open() 39 | 40 | if (!key) throw new Error('Key is required') 41 | const feed = await this._feed(key) 42 | 43 | const proxy = new OutboundProxy(feed, { port, host }) 44 | await proxy.listen() 45 | proxy.on('error', err => this.emit('error', err)) 46 | 47 | this._networker.configure(feed.discoveryKey, { announce: true }) 48 | return proxy 49 | } 50 | 51 | async inbound (key, port, host = 'localhost') { 52 | await this.open() 53 | if (!port) throw new Error('Port is required') 54 | 55 | let name = null 56 | if (!key) name = [NAMESPACE, host, port].join(':') 57 | const feed = await this._feed(key, name) 58 | 59 | const proxy = new InboundProxy(feed, { port, host }) 60 | proxy.on('error', err => this.emit('error', err)) 61 | 62 | this._networker.configure(feed.discoveryKey, { lookup: true }) 63 | return proxy 64 | } 65 | 66 | async _feed (key, name) { 67 | if (name) { 68 | var feed = this._corestore.namespace(name).default() 69 | } else { 70 | feed = this._corestore.get({ key }) 71 | } 72 | await new Promise((resolve, reject) => { 73 | feed.ready(err => err ? reject(err) : resolve()) 74 | }) 75 | return feed 76 | } 77 | } 78 | 79 | class HyproxyExtension { 80 | constructor (handlers) { 81 | this.handlers = handlers 82 | } 83 | 84 | registerExtension (feed) { 85 | const self = this 86 | const ext = feed.registerExtension(EXTENSION_NAME, { 87 | onmessage (message, peer) { 88 | const { type, id, data } = decodeMessage(message) 89 | self.onmessage(peer, type, id, data) 90 | }, 91 | onerror (err) { 92 | self.onerror(err) 93 | } 94 | }) 95 | feed.on('peer-open', peer => { 96 | debug('peer-open', fmtPeer(peer)) 97 | if (this.handlers.onpeeropen) this.handlers.onpeeropen(peer) 98 | }) 99 | feed.on('peer-remove', peer => { 100 | debug('peer-close', fmtPeer(peer)) 101 | if (this.handlers.onpeerclose) this.handlers.onpeerclose(peer) 102 | }) 103 | this.ext = ext 104 | } 105 | 106 | onerror (err) { 107 | if (this.handlers.onerror) this.handlers.onerror(err) 108 | else throw err 109 | } 110 | 111 | onmessage (peer, type, id, data) { 112 | debug(`recv from ${fmtPeer(peer)}:${id} typ ${type} len ${data.length}`) 113 | if (type === TYP_DISCOVER) return this.handlers.ondiscover(peer, id, data) 114 | if (type === TYP_ACK) return this.handlers.onack(peer, id, data) 115 | if (type === TYP_CONNECT) return this.handlers.onconnect(peer, id, data) 116 | if (type === TYP_DATA) return this.handlers.ondata(peer, id, data) 117 | if (type === TYP_CLOSE) return this.handlers.onclose(peer, id, data) 118 | } 119 | 120 | send (peer, id, type, data) { 121 | debug(`send to ${fmtPeer(peer)}:${id} typ ${type} len ${(data && data.length) || 0}`) 122 | const buf = encodeMessage(id, type, data) 123 | this.ext.send(buf, peer) 124 | } 125 | 126 | broadcast (id, type, data) { 127 | debug(`broadcast type ${type} id ${id} len ${data && data.length}`) 128 | const buf = encodeMessage(id, type, data) 129 | this.ext.broadcast(buf) 130 | } 131 | } 132 | 133 | class ProxyBase extends EventEmitter { 134 | constructor (feed) { 135 | super() 136 | this.connections = new PeerMap() 137 | this.ext = new HyproxyExtension(this) 138 | this.ext.registerExtension(feed) 139 | this.key = feed.key 140 | } 141 | 142 | addSocket (peer, id, socket) { 143 | this.connections.set(peer, id, socket) 144 | socket.on('data', data => { 145 | this.ext.send(peer, id, TYP_DATA, data) 146 | }) 147 | socket.on('error', (err) => { 148 | debug(`socket ${fmtPeer(peer)}:${id} closed (${err.message})`) 149 | }) 150 | socket.on('close', () => { 151 | if (!socket[kRemoteClosed]) this.ext.send(peer, id, TYP_CLOSE) 152 | debug(`socket ${fmtPeer(peer)}:${id} closed`) 153 | this.connections.delete(peer, id) 154 | }) 155 | } 156 | 157 | ondata (peer, id, data) { 158 | const socket = this.connections.get(peer, id) 159 | if (!socket) return 160 | socket.write(data) 161 | } 162 | 163 | onclose (peer, id, data) { 164 | const socket = this.connections.get(peer, id) 165 | if (!socket) return 166 | socket[kRemoteClosed] = true 167 | socket.destroy() 168 | } 169 | 170 | onpeerclose (peer) { 171 | const err = new Error('Peer connection lost') 172 | this.connections.foreach(peer, socket => { 173 | socket[kRemoteClosed] = true 174 | socket.destroy(err) 175 | }) 176 | this.connections.delete(peer) 177 | } 178 | 179 | onerror (err) { 180 | this.emit('error', err) 181 | } 182 | } 183 | 184 | class InboundProxy extends ProxyBase { 185 | constructor (feed, { port, host }) { 186 | super(feed) 187 | this.port = port 188 | this.host = host 189 | } 190 | 191 | ondiscover (peer, id, data) { 192 | this.ext.send(peer, id, TYP_ACK) 193 | } 194 | 195 | onack () { 196 | // do nothing 197 | } 198 | 199 | onconnect (peer, id, data) { 200 | const socket = net.connect(this.port, this.host) 201 | this.addSocket(peer, id, socket) 202 | } 203 | } 204 | 205 | class OutboundProxy extends ProxyBase { 206 | constructor (feed, { port, host }) { 207 | super(feed) 208 | this.port = port 209 | this.host = host 210 | this.server = net.createServer(this.ontcpconnection.bind(this)) 211 | this.peers = new Set() 212 | this._cnt = 0 213 | } 214 | 215 | async listen () { 216 | if (!this.port) { 217 | this.port = await getPort({ port: getPort.makeRange(9990, 9999) }) 218 | } 219 | await new Promise((resolve, reject) => { 220 | this.server.listen(this.port, this.host, err => { 221 | err ? reject(err) : resolve() 222 | }) 223 | }) 224 | this.server.on('error', err => this.emit('error', err)) 225 | this.ext.broadcast(0, TYP_DISCOVER) 226 | } 227 | 228 | onpeeropen (peer) { 229 | this.ext.send(peer, 0, TYP_DISCOVER) 230 | } 231 | 232 | onpeerclose (peer) { 233 | super.onpeerclose(peer) 234 | this.peers.delete(peer) 235 | } 236 | 237 | ondiscover () { 238 | // do nothing 239 | } 240 | 241 | onack (peer, id, data) { 242 | // TODO: Verify the peer somehow, check a capability. 243 | this.peers.add(peer) 244 | } 245 | 246 | ontcpconnection (socket) { 247 | const peer = this._selectPeer() 248 | if (!peer) return socket.destroy() 249 | const id = ++this._cnt 250 | this.addSocket(peer, id, socket) 251 | this.ext.send(peer, id, TYP_CONNECT) 252 | } 253 | 254 | _selectPeer () { 255 | const peers = Array.from(this.peers.values()) 256 | if (!peers.length) return null 257 | return peers[0] 258 | } 259 | } 260 | 261 | class PeerMap { 262 | constructor (onclose) { 263 | this.map = new Map() 264 | } 265 | 266 | set (peer, id, socket) { 267 | if (!this.has(peer, null)) this.map.set(rkey(peer), new Map()) 268 | if (id !== null) this.get(peer, null).set(id, socket) 269 | } 270 | 271 | delete (peer, id) { 272 | if (!this.has(peer)) return 273 | if (id !== null) return this.get(peer).delete(id) 274 | this.map.delete(rkey(peer)) 275 | } 276 | 277 | foreach (peer, fn) { 278 | if (!this.has(peer)) return 279 | for (const socket of this.get(peer).values()) { 280 | fn(socket) 281 | } 282 | } 283 | 284 | get (peer, id) { 285 | if (!this.has(peer, id)) return null 286 | if (id === null) return this.map.get(rkey(peer)) 287 | return this.map.get(rkey(peer)).get(id) 288 | } 289 | 290 | has (peer, id) { 291 | if (id === null) return this.map.has(rkey(peer)) 292 | if (!this.map.has(rkey(peer))) return false 293 | return this.map.get(rkey(peer)).has(id) 294 | } 295 | } 296 | 297 | function encodeMessage (id, type, data) { 298 | if (!data) data = Buffer.alloc(0) 299 | if (!Buffer.isBuffer(data)) data = Buffer.from(data) 300 | const header = id << 4 | type 301 | const headerBuf = Buffer.alloc(4) 302 | headerBuf.writeUInt32LE(header) 303 | return Buffer.concat([headerBuf, data]) 304 | } 305 | 306 | function decodeMessage (buf) { 307 | const headerBuf = buf.slice(0, 4) 308 | const header = headerBuf.readUInt32LE() 309 | const type = header & 0b1111 310 | const id = header >> 4 311 | const data = buf.slice(4) 312 | return { type, id, data } 313 | } 314 | 315 | function fmtPeer (peer) { 316 | return prettyHash(peer.remotePublicKey) 317 | } 318 | 319 | function rkey (peer) { 320 | return peer.remotePublicKey.toString('hex') 321 | } 322 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyproxy", 3 | "version": "0.2.0", 4 | "description": "Proxy TCP connections over hypercore-protocol", 5 | "bin": { 6 | "hyproxy": "bin.js" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/Frando/hyproxy" 11 | }, 12 | "keywords": [ 13 | "hypercore-protocol", 14 | "proxy" 15 | ], 16 | "author": "Franz Heinzmann ", 17 | "bugs": { 18 | "url": "https://github.com/Frando/hyproxy/issues" 19 | }, 20 | "homepage": "https://github.com/Frando/hyproxy#readme", 21 | "main": "index.js", 22 | "license": "MIT", 23 | "dependencies": { 24 | "@corestore/networker": "^1.0.4", 25 | "corestore": "^5.8.1", 26 | "debug": "^4.3.1", 27 | "get-port": "^5.1.1", 28 | "minimist": "^1.2.5", 29 | "pretty-hash": "^1.0.1", 30 | "random-access-memory": "^3.1.1" 31 | } 32 | } 33 | --------------------------------------------------------------------------------