diff --git a/LICENSE b/LICENSE index 2071b23..bd66f50 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) +Copyright (c) 2023 Hammer Technologies LLC 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: diff --git a/package.json b/package.json new file mode 100644 index 0000000..100228b --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "@lumeweb/libhyperproxy", + "version": "0.1.0", + "main": "dist/index.js", + "devDependencies": { + "@types/node": "^18.11.18", + "@types/streamx": "^2.9.1", + "prettier": "^2.8.2", + "typescript": "^4.9.4" + }, + "dependencies": { + "compact-encoding": "^2.11.0", + "protomux": "^3.4.0", + "streamx": "^2.13.0" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d10c697 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,37 @@ +import Proxy from "./proxy.js"; +import Socket from "./socket.js"; +import Peer, { + DataSocketOptions, + PeerOptions, + PeerOptionsWithProxy, + OnOpen, + OnSend, + OnReceive, + OnClose, +} from "./peer.js"; +import Server from "./server.js"; + +export { + Proxy, + Socket, + Server, + Peer, + DataSocketOptions, + PeerOptions, + PeerOptionsWithProxy, + OnOpen, + OnSend, + OnReceive, + OnClose, +}; + +export function createSocket(port: number, host: string): Socket { + return new Socket({ + remotePort: port, + remoteAddress: host, + }); +} + +export function createServer(): Server { + return new Server(); +} diff --git a/src/peer.ts b/src/peer.ts new file mode 100644 index 0000000..c7d3bf7 --- /dev/null +++ b/src/peer.ts @@ -0,0 +1,100 @@ +import Proxy from "./proxy.js"; +import Socket from "./socket.js"; +export type OnOpen = ( + socket: Socket, + data: any +) => + | { connect: boolean } + | Promise<{ connect: boolean }> + | Promise + | void; +export type OnData = (data: any) => void; +export type OnReceive = OnData; +export type OnClose = OnData; +export type OnSend = OnData; + +export interface DataSocketOptions { + onopen?: OnOpen; + onreceive?: OnReceive; + onsend?: OnSend; + onclose?: OnClose; +} + +export interface PeerOptions { + peer: any; + muxer: any; +} + +export interface PeerOptionsWithProxy extends PeerOptions { + proxy: Proxy; +} + +export default class Peer { + private _proxy: Proxy; + private _peer: any; + private _muxer: any; + private _socket?: Socket; + private _onopen: OnOpen; + private _onreceive: OnReceive; + private _onsend: OnSend; + private _onclose: OnClose; + + constructor({ + proxy, + peer, + muxer, + onopen, + onreceive, + onsend, + onclose, + }: PeerOptionsWithProxy & DataSocketOptions) { + this._proxy = proxy; + this._peer = peer; + this._muxer = muxer; + this._onopen = onopen; + this._onreceive = onreceive; + this._onsend = onsend; + this._onclose = onclose; + } + + async init() { + const write = async (data: any, cb: Function) => { + pipe.send(data); + await this._onsend?.(data); + cb(); + }; + const self = this; + const channel = this._muxer.createChannel({ + protocol: this._proxy.protocol, + async onopen(m: any) { + if (!m) { + m = Buffer.from([]); + } + self._socket = new Socket({ + remoteAddress: self._peer.rawStream.remoteHost, + remotePort: self._peer.rawStream.remotePort, + write, + }); + self._socket.on("end", () => channel.close()); + let ret = await self._onopen?.(self._socket, m); + if (!ret || (ret && ret.connect === false)) { + // @ts-ignore + self._socket.emit("connect"); + } + self._socket.emit("data", m); + }, + async onclose() { + self._socket?.end(); + await self._onclose?.(self._socket); + }, + }); + const pipe = channel.addMessage({ + async onmessage(m: any) { + self._socket.emit("data", m); + await self._onreceive?.(m); + }, + }); + + await channel.open(); + } +} diff --git a/src/proxy.ts b/src/proxy.ts new file mode 100644 index 0000000..22d117d --- /dev/null +++ b/src/proxy.ts @@ -0,0 +1,80 @@ +import Protomux from "protomux"; +import Peer, { PeerOptions, DataSocketOptions } from "./peer.js"; + +export interface ProxyOptions extends DataSocketOptions { + swarm: any; + protocol: string; + listen?: boolean; + autostart?: boolean; +} + +export default class Proxy { + private _listen: any; + private _socketOptions: DataSocketOptions; + private _autostart: boolean; + + constructor({ + swarm, + protocol, + onopen, + onreceive, + onsend, + onclose, + listen = false, + autostart = false, + }: ProxyOptions) { + this._swarm = swarm; + this._protocol = protocol; + this._listen = listen; + this._autostart = autostart; + this._socketOptions = { onopen, onreceive, onsend, onclose }; + this.init(); + } + + private _swarm: any; + + get swarm(): any { + return this._swarm; + } + + private _protocol: string; + + get protocol(): string { + return this._protocol; + } + public handlePeer({ + peer, + muxer, + ...options + }: DataSocketOptions & PeerOptions) { + const conn = new Peer({ proxy: this, peer, muxer, ...options }); + conn.init(); + } + + protected _init() { + // Implement in subclasses + } + + private async init() { + if (this._listen) { + this._swarm.on("connection", this._handleConnection.bind(this)); + } + await this._init(); + } + + private _handleConnection(peer: any) { + const muxer = Protomux.from(peer); + const handlePeer = this.handlePeer.bind(this, { + peer, + muxer, + ...this._socketOptions, + }); + + if (this._autostart) { + handlePeer(); + return; + } + + muxer.pair(this._protocol, handlePeer); + } +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..9f59587 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,45 @@ +import EventEmitter from "events"; + +export default class Server extends EventEmitter { + address() { + return { + address: "127.0.0.1", + family: "IPv4", + port: 0, + }; + } + + async close() { + return; + } + + async getConnections() { + return 0; + } + + async listen(...args: any[]) { + const address = this.address(); + this.emit("listening", address); + return address; + } + + get listening() { + return false; + } + + set listening(value) {} + + get maxConnections() { + return undefined; + } + + set maxConnections(value) {} + + ref() { + return this; + } + + unref() { + return this; + } +} diff --git a/src/socket.ts b/src/socket.ts new file mode 100644 index 0000000..20b6235 --- /dev/null +++ b/src/socket.ts @@ -0,0 +1,113 @@ +import { Duplex, DuplexEvents, Callback } from "streamx"; + +const IPV4 = "IPv4"; +const IPV6 = "IPv6"; + +type AddressFamily = "IPv6" | "IPv4"; + +interface SocketOptions { + allowHalfOpen?: boolean; + remoteAddress?: string; + remotePort?: number; + write?: ( + this: Duplex>, + data: any, + cb: Callback + ) => void; +} + +export default class Socket extends Duplex { + private _allowHalfOpen: boolean; + public remoteAddress: any; + public remotePort: any; + public remoteFamily: AddressFamily; + + public bufferSize; + + constructor({ + allowHalfOpen = false, + remoteAddress, + remotePort, + write, + }: SocketOptions = {}) { + super({ write }); + this._allowHalfOpen = allowHalfOpen; + this.remoteAddress = remoteAddress; + this.remotePort = remotePort; + + if (remoteAddress) { + const type = Socket.isIP(remoteAddress); + if (!type) { + throw Error("invalid remoteAddress"); + } + + this.remoteFamily = type === 6 ? IPV4 : IPV6; + } + } + + private _connecting: boolean; + + get connecting(): boolean { + return this._connecting; + } + + get readyState(): string { + if (this._connecting) { + return "opening"; + } else if (this.readable && this.writable) { + return "open"; + } else if (this.readable && !this.writable) { + return "readOnly"; + } else if (!this.readable && this.writable) { + return "writeOnly"; + } else { + return "closed"; + } + } + + listen() { + throw new Error("Not supported"); + } + + setTimeout(msecs, callback) { + throw new Error("Not implemented"); + } + + _onTimeout() { + // @ts-ignore + this.emit("timeout"); + } + + setNoDelay(enable: boolean) {} + + setKeepAlive(setting: any, msecs: number) {} + + address() { + return { + address: this.remoteAddress, + port: this.remotePort, + family: this.remoteFamily, + }; + } + static isIP(input: string): number { + if (Socket.isIPv4(input)) { + return 4; + } else if (Socket.isIPv6(input)) { + return 6; + } else { + return 0; + } + } + + static isIPv4(input: string) { + return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test( + input + ); + } + + static isIPv6(input: string) { + return /^(([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))$/.test( + input + ); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2f6b041 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "esnext", + "esModuleInterop": true, + "outDir": "dist", + "moduleResolution": "node", + "declaration": true, + }, + "include": ["src"], + "exclude": [ + "node_modules" + ] +}