hyproxy
Version:
Proxy TCP connections over hypercore-protocol
322 lines (273 loc) • 8.33 kB
JavaScript
const net = require('net')
const prettyHash = require('pretty-hash')
const Corestore = require('corestore')
const Networker = require('@corestore/networker')
const ram = require('random-access-memory')
const debug = require('debug')('hyproxy')
const getPort = require('get-port')
const { EventEmitter } = require('events')
const TYP_DISCOVER = 0
const TYP_ACK = 1
const TYP_CONNECT = 2
const TYP_DATA = 3
const TYP_CLOSE = 4
const NAMESPACE = 'hyproxy-v1'
const EXTENSION_NAME = NAMESPACE + ':extension'
const kRemoteClosed = Symbol('remote-closed')
module.exports = class HyperProxy extends EventEmitter {
constructor (opts = {}) {
super()
this._corestore = opts.corestore || new Corestore(opts.storage || ram)
this._networker = opts.networker || new Networker(this._corestore)
}
async open () {
if (this.opened) return
await new Promise((resolve, reject) => {
this._corestore.ready(err => err ? reject(err) : resolve())
})
debug(`init, pubKey: ${prettyHash(this._networker.keyPair.publicKey)}`)
this.opened = true
}
async outbound (key, port, host = 'localhost') {
await this.open()
if (!key) throw new Error('Key is required')
const feed = await this._feed(key)
const proxy = new OutboundProxy(feed, { port, host })
await proxy.listen()
proxy.on('error', err => this.emit('error', err))
this._networker.configure(feed.discoveryKey, { announce: true })
return proxy
}
async inbound (key, port, host = 'localhost') {
await this.open()
if (!port) throw new Error('Port is required')
let name = null
if (!key) name = [NAMESPACE, host, port].join(':')
const feed = await this._feed(key, name)
const proxy = new InboundProxy(feed, { port, host })
proxy.on('error', err => this.emit('error', err))
this._networker.configure(feed.discoveryKey, { lookup: true })
return proxy
}
async _feed (key, name) {
if (name) {
var feed = this._corestore.namespace(name).default()
} else {
feed = this._corestore.get({ key })
}
await new Promise((resolve, reject) => {
feed.ready(err => err ? reject(err) : resolve())
})
return feed
}
}
class HyproxyExtension {
constructor (handlers) {
this.handlers = handlers
}
registerExtension (feed) {
const self = this
const ext = feed.registerExtension(EXTENSION_NAME, {
onmessage (message, peer) {
const { type, id, data } = decodeMessage(message)
self.onmessage(peer, type, id, data)
},
onerror (err) {
self.onerror(err)
}
})
feed.on('peer-open', peer => {
debug('peer-open', fmtPeer(peer))
if (this.handlers.onpeeropen) this.handlers.onpeeropen(peer)
})
feed.on('peer-remove', peer => {
debug('peer-close', fmtPeer(peer))
if (this.handlers.onpeerclose) this.handlers.onpeerclose(peer)
})
this.ext = ext
}
onerror (err) {
if (this.handlers.onerror) this.handlers.onerror(err)
else throw err
}
onmessage (peer, type, id, data) {
debug(`recv from ${fmtPeer(peer)}:${id} typ ${type} len ${data.length}`)
if (type === TYP_DISCOVER) return this.handlers.ondiscover(peer, id, data)
if (type === TYP_ACK) return this.handlers.onack(peer, id, data)
if (type === TYP_CONNECT) return this.handlers.onconnect(peer, id, data)
if (type === TYP_DATA) return this.handlers.ondata(peer, id, data)
if (type === TYP_CLOSE) return this.handlers.onclose(peer, id, data)
}
send (peer, id, type, data) {
debug(`send to ${fmtPeer(peer)}:${id} typ ${type} len ${(data && data.length) || 0}`)
const buf = encodeMessage(id, type, data)
this.ext.send(buf, peer)
}
broadcast (id, type, data) {
debug(`broadcast type ${type} id ${id} len ${data && data.length}`)
const buf = encodeMessage(id, type, data)
this.ext.broadcast(buf)
}
}
class ProxyBase extends EventEmitter {
constructor (feed) {
super()
this.connections = new PeerMap()
this.ext = new HyproxyExtension(this)
this.ext.registerExtension(feed)
this.key = feed.key
}
addSocket (peer, id, socket) {
this.connections.set(peer, id, socket)
socket.on('data', data => {
this.ext.send(peer, id, TYP_DATA, data)
})
socket.on('error', (err) => {
debug(`socket ${fmtPeer(peer)}:${id} closed (${err.message})`)
})
socket.on('close', () => {
if (!socket[kRemoteClosed]) this.ext.send(peer, id, TYP_CLOSE)
debug(`socket ${fmtPeer(peer)}:${id} closed`)
this.connections.delete(peer, id)
})
}
ondata (peer, id, data) {
const socket = this.connections.get(peer, id)
if (!socket) return
socket.write(data)
}
onclose (peer, id, data) {
const socket = this.connections.get(peer, id)
if (!socket) return
socket[kRemoteClosed] = true
socket.destroy()
}
onpeerclose (peer) {
const err = new Error('Peer connection lost')
this.connections.foreach(peer, socket => {
socket[kRemoteClosed] = true
socket.destroy(err)
})
this.connections.delete(peer)
}
onerror (err) {
this.emit('error', err)
}
}
class InboundProxy extends ProxyBase {
constructor (feed, { port, host }) {
super(feed)
this.port = port
this.host = host
}
ondiscover (peer, id, data) {
this.ext.send(peer, id, TYP_ACK)
}
onack () {
// do nothing
}
onconnect (peer, id, data) {
const socket = net.connect(this.port, this.host)
this.addSocket(peer, id, socket)
}
}
class OutboundProxy extends ProxyBase {
constructor (feed, { port, host }) {
super(feed)
this.port = port
this.host = host
this.server = net.createServer(this.ontcpconnection.bind(this))
this.peers = new Set()
this._cnt = 0
}
async listen () {
if (!this.port) {
this.port = await getPort({ port: getPort.makeRange(9990, 9999) })
}
await new Promise((resolve, reject) => {
this.server.listen(this.port, this.host, err => {
err ? reject(err) : resolve()
})
})
this.server.on('error', err => this.emit('error', err))
this.ext.broadcast(0, TYP_DISCOVER)
}
onpeeropen (peer) {
this.ext.send(peer, 0, TYP_DISCOVER)
}
onpeerclose (peer) {
super.onpeerclose(peer)
this.peers.delete(peer)
}
ondiscover () {
// do nothing
}
onack (peer, id, data) {
// TODO: Verify the peer somehow, check a capability.
this.peers.add(peer)
}
ontcpconnection (socket) {
const peer = this._selectPeer()
if (!peer) return socket.destroy()
const id = ++this._cnt
this.addSocket(peer, id, socket)
this.ext.send(peer, id, TYP_CONNECT)
}
_selectPeer () {
const peers = Array.from(this.peers.values())
if (!peers.length) return null
return peers[0]
}
}
class PeerMap {
constructor (onclose) {
this.map = new Map()
}
set (peer, id, socket) {
if (!this.has(peer, null)) this.map.set(rkey(peer), new Map())
if (id !== null) this.get(peer, null).set(id, socket)
}
delete (peer, id) {
if (!this.has(peer)) return
if (id !== null) return this.get(peer).delete(id)
this.map.delete(rkey(peer))
}
foreach (peer, fn) {
if (!this.has(peer)) return
for (const socket of this.get(peer).values()) {
fn(socket)
}
}
get (peer, id) {
if (!this.has(peer, id)) return null
if (id === null) return this.map.get(rkey(peer))
return this.map.get(rkey(peer)).get(id)
}
has (peer, id) {
if (id === null) return this.map.has(rkey(peer))
if (!this.map.has(rkey(peer))) return false
return this.map.get(rkey(peer)).has(id)
}
}
function encodeMessage (id, type, data) {
if (!data) data = Buffer.alloc(0)
if (!Buffer.isBuffer(data)) data = Buffer.from(data)
const header = id << 4 | type
const headerBuf = Buffer.alloc(4)
headerBuf.writeUInt32LE(header)
return Buffer.concat([headerBuf, data])
}
function decodeMessage (buf) {
const headerBuf = buf.slice(0, 4)
const header = headerBuf.readUInt32LE()
const type = header & 0b1111
const id = header >> 4
const data = buf.slice(4)
return { type, id, data }
}
function fmtPeer (peer) {
return prettyHash(peer.remotePublicKey)
}
function rkey (peer) {
return peer.remotePublicKey.toString('hex')
}