diff --git a/dist/index.d.ts b/dist/index.d.ts index 904a845..d384bbc 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -2,6 +2,10 @@ 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, }; +import DummySocket from "./proxies/multiSocket/dummySocket.js"; +import TcpSocket from "./proxies/multiSocket/tcpSocket.js"; +import BasicProxy from "./proxies/basic.js"; +import MultiSocketProxy from "./proxies/multiSocket.js"; +export { Proxy, Socket, Server, Peer, DataSocketOptions, PeerOptions, PeerOptionsWithProxy, OnOpen, OnSend, OnReceive, OnClose, DummySocket, TcpSocket, BasicProxy, MultiSocketProxy, }; export declare function createSocket(port: number, host: string): Socket; export declare function createServer(): Server; diff --git a/dist/index.js b/dist/index.js index 07b46df..e66a121 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.createServer = exports.createSocket = exports.Peer = exports.Server = exports.Socket = exports.Proxy = void 0; +exports.createServer = exports.createSocket = exports.MultiSocketProxy = exports.BasicProxy = exports.TcpSocket = exports.DummySocket = exports.Peer = exports.Server = exports.Socket = exports.Proxy = void 0; const proxy_js_1 = __importDefault(require("./proxy.js")); exports.Proxy = proxy_js_1.default; const socket_js_1 = __importDefault(require("./socket.js")); @@ -12,6 +12,14 @@ const peer_js_1 = __importDefault(require("./peer.js")); exports.Peer = peer_js_1.default; const server_js_1 = __importDefault(require("./server.js")); exports.Server = server_js_1.default; +const dummySocket_js_1 = __importDefault(require("./proxies/multiSocket/dummySocket.js")); +exports.DummySocket = dummySocket_js_1.default; +const tcpSocket_js_1 = __importDefault(require("./proxies/multiSocket/tcpSocket.js")); +exports.TcpSocket = tcpSocket_js_1.default; +const basic_js_1 = __importDefault(require("./proxies/basic.js")); +exports.BasicProxy = basic_js_1.default; +const multiSocket_js_1 = __importDefault(require("./proxies/multiSocket.js")); +exports.MultiSocketProxy = multiSocket_js_1.default; function createSocket(port, host) { return new socket_js_1.default({ remotePort: port, diff --git a/dist/proxies/basic.d.ts b/dist/proxies/basic.d.ts new file mode 100644 index 0000000..31b7c21 --- /dev/null +++ b/dist/proxies/basic.d.ts @@ -0,0 +1,3 @@ +import Proxy from "../proxy.js"; +export default class BasicProxy extends Proxy { +} diff --git a/dist/proxies/basic.js b/dist/proxies/basic.js new file mode 100644 index 0000000..a6ffc4c --- /dev/null +++ b/dist/proxies/basic.js @@ -0,0 +1,9 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const proxy_js_1 = __importDefault(require("../proxy.js")); +class BasicProxy extends proxy_js_1.default { +} +exports.default = BasicProxy; diff --git a/dist/proxies/multiSocket.d.ts b/dist/proxies/multiSocket.d.ts new file mode 100644 index 0000000..38be622 --- /dev/null +++ b/dist/proxies/multiSocket.d.ts @@ -0,0 +1,33 @@ +/// +import Proxy, { ProxyOptions } from "../proxy.js"; +import type { TcpSocketConnectOpts } from "net"; +import Peer from "../peer.js"; +import { PeerEntity } from "./multiSocket/types.js"; +export interface MultiSocketProxyOptions extends ProxyOptions { + socketClass?: any; + server: boolean; + allowedPorts?: number[]; +} +export default class MultiSocketProxy extends Proxy { + private socketClass; + private _peers; + private _nextPeer; + private _server; + private _allowedPorts; + constructor(options: MultiSocketProxyOptions); + private _socketMap; + get socketMap(): Map; + private _sockets; + get sockets(): Map; + handleNewPeerChannel(peer: Peer, channel: any): void; + handleClosePeer(peer: Peer): Promise; + get(pubkey: Uint8Array): PeerEntity | undefined; + update(pubkey: Uint8Array, data: Partial): void; + createSocket(options: TcpSocketConnectOpts): Promise; + private _registerOpenSocketMessage; + private _registerWriteSocketMessage; + private _registerCloseSocketMessage; + private _registerTimeoutSocketMessage; + private _registerErrorSocketMessage; + private _toString; +} diff --git a/dist/proxies/multiSocket.js b/dist/proxies/multiSocket.js new file mode 100644 index 0000000..fc5ba85 --- /dev/null +++ b/dist/proxies/multiSocket.js @@ -0,0 +1,226 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const proxy_js_1 = __importDefault(require("../proxy.js")); +const tcpSocket_js_1 = __importDefault(require("./multiSocket/tcpSocket.js")); +const compact_encoding_1 = require("compact-encoding"); +const serialize_error_1 = require("serialize-error"); +const b4a_1 = __importDefault(require("b4a")); +const util_js_1 = require("../util.js"); +const dummySocket_js_1 = __importDefault(require("./multiSocket/dummySocket.js")); +const socketEncoding = { + preencode(state, m) { + compact_encoding_1.uint.preencode(state, m.id); + compact_encoding_1.uint.preencode(state, m.remoteId); + }, + encode(state, m) { + compact_encoding_1.uint.encode(state, m.id); + compact_encoding_1.uint.encode(state, m.remoteId); + }, + decode(state, m) { + return { + remoteId: compact_encoding_1.uint.decode(state, m), + id: compact_encoding_1.uint.decode(state, m), + }; + }, +}; +const writeSocketEncoding = { + preencode(state, m) { + socketEncoding.preencode(state, m); + compact_encoding_1.raw.preencode(state, m.data); + }, + encode(state, m) { + socketEncoding.encode(state, m); + compact_encoding_1.raw.encode(state, m.data); + }, + decode(state, m) { + const socket = socketEncoding.decode(state, m); + return { + ...socket, + data: compact_encoding_1.raw.decode(state, m), + }; + }, +}; +const errorSocketEncoding = { + decode(state, m) { + const socket = socketEncoding.decode(state, m); + return { + ...socket, + err: (0, serialize_error_1.deserializeError)(compact_encoding_1.json.decode(state, m)), + }; + }, +}; +const nextSocketId = (0, util_js_1.idFactory)(1); +class MultiSocketProxy extends proxy_js_1.default { + socketClass; + _peers = new Map(); + _nextPeer = (0, util_js_1.roundRobinFactory)(this._peers); + _server = false; + _allowedPorts = []; + constructor(options) { + super({ + createDefaultMessage: false, + ...options, + }); + if (options.socketClass) { + this.socketClass = options.socketClass; + } + else { + if (options.server) { + this.socketClass = tcpSocket_js_1.default; + } + else { + this.socketClass = dummySocket_js_1.default; + } + } + if (options.server) { + this._server = true; + } + } + _socketMap = new Map(); + get socketMap() { + return this._socketMap; + } + _sockets = new Map(); + get sockets() { + return this._sockets; + } + handleNewPeerChannel(peer, channel) { + this.update(peer.socket.remotePublicKey, { peer }); + this._registerOpenSocketMessage(peer, channel); + this._registerWriteSocketMessage(peer, channel); + this._registerCloseSocketMessage(peer, channel); + this._registerTimeoutSocketMessage(peer, channel); + this._registerErrorSocketMessage(peer, channel); + } + async handleClosePeer(peer) { + for (const item of this._sockets) { + if (item[1].peer.peer === peer) { + item[1].end(); + } + } + const pubkey = this._toString(peer.socket.remotePublicKey); + if (this._peers.has(pubkey)) { + this._peers.delete(pubkey); + } + } + get(pubkey) { + if (this._peers.has(this._toString(pubkey))) { + return this._peers.get(this._toString(pubkey)); + } + return undefined; + } + update(pubkey, data) { + const peer = this.get(pubkey) ?? {}; + this._peers.set(this._toString(pubkey), { + ...peer, + ...data, + ...{ + messages: { + ...peer?.messages, + ...data?.messages, + }, + }, + }); + } + async createSocket(options) { + if (!this._peers.size) { + throw new Error("no peers found"); + } + const peer = this._nextPeer(); + const socketId = nextSocketId(); + const socket = new this.socketClass(socketId, this, peer, options); + this._sockets.set(socketId, socket); + return socket; + } + _registerOpenSocketMessage(peer, channel) { + const self = this; + const message = channel.addMessage({ + encoding: { + preencode: compact_encoding_1.json.preencode, + encode: compact_encoding_1.json.encode, + decode: this._server ? compact_encoding_1.json.encode : socketEncoding.decode, + }, + async onmessage(m) { + if (self._allowedPorts.length && + !self._allowedPorts.includes(m.port)) { + self.get(peer.socket.remotePublicKey).messages.errorSocket.send({ + id: m.id, + err: new Error(`port ${m.port} not allowed`), + }); + return; + } + m = m; + if (self._server) { + new self.socketClass(nextSocketId(), m, self, self.get(peer.socket.remotePublicKey), m).connect(); + return; + } + const socket = self._sockets.get(m.id); + if (socket) { + socket.remoteId = m.remoteId; + // @ts-ignore + socket.emit("connect"); + } + }, + }); + this.update(peer.socket.remotePublicKey, { + messages: { openSocket: message }, + }); + } + _registerWriteSocketMessage(peer, channel) { + const self = this; + const message = channel.addMessage({ + encoding: writeSocketEncoding, + onmessage(m) { + self._sockets.get(m.id)?.push(m.data); + }, + }); + this.update(peer.socket.remotePublicKey, { + messages: { writeSocket: message }, + }); + } + _registerCloseSocketMessage(peer, channel) { + const self = this; + const message = channel.addMessage({ + encoding: socketEncoding, + onmessage(m) { + self._sockets.get(m.id)?.end(); + }, + }); + this.update(peer.socket.remotePublicKey, { + messages: { closeSocket: message }, + }); + } + _registerTimeoutSocketMessage(peer, channel) { + const self = this; + const message = channel.addMessage({ + encoding: socketEncoding, + onmessage(m) { + // @ts-ignore + self._sockets.get(m.id)?.emit("timeout"); + }, + }); + this.update(peer.socket.remotePublicKey, { + messages: { timeoutSocket: message }, + }); + } + _registerErrorSocketMessage(peer, channel) { + const self = this; + const message = channel.addMessage({ + encoding: errorSocketEncoding, + onmessage(m) { + // @ts-ignore + self._sockets.get(m.id)?.emit("error", m.err); + }, + }); + this.update(peer.socket.remotePublicKey, { + messages: { errorSocket: message }, + }); + } + _toString(pubkey) { + return b4a_1.default.from(pubkey).toString("hex"); + } +} +exports.default = MultiSocketProxy; diff --git a/dist/proxies/multiSocket/dummySocket.d.ts b/dist/proxies/multiSocket/dummySocket.d.ts new file mode 100644 index 0000000..f6cd7b1 --- /dev/null +++ b/dist/proxies/multiSocket/dummySocket.d.ts @@ -0,0 +1,20 @@ +/// +import { Callback, Duplex } from "streamx"; +import { TcpSocketConnectOpts } from "net"; +import MultiSocketProxy from "../multiSocket.js"; +import { PeerEntity } from "./types.js"; +export default class DummySocket extends Duplex { + private _options; + private _id; + private _proxy; + private _connectTimeout?; + constructor(id: number, manager: MultiSocketProxy, peer: PeerEntity, options: TcpSocketConnectOpts); + private _remoteId; + set remoteId(value: number); + private _peer; + get peer(): any; + _write(data: any, cb: any): Promise; + _destroy(cb: Callback): Promise; + connect(): Promise; + setTimeout(ms: number, cb: Function): void; +} diff --git a/dist/proxies/multiSocket/dummySocket.js b/dist/proxies/multiSocket/dummySocket.js new file mode 100644 index 0000000..004ad01 --- /dev/null +++ b/dist/proxies/multiSocket/dummySocket.js @@ -0,0 +1,64 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const streamx_1 = require("streamx"); +const timers_1 = require("timers"); +const util_js_1 = require("../../util.js"); +class DummySocket extends streamx_1.Duplex { + _options; + _id; + _proxy; + _connectTimeout; + constructor(id, manager, peer, options) { + super(); + this._id = id; + this._proxy = manager; + this._peer = peer; + this._options = options; + // @ts-ignore + this.on("timeout", () => { + if (this._connectTimeout) { + (0, timers_1.clearTimeout)(this._connectTimeout); + } + }); + } + _remoteId = 0; + set remoteId(value) { + this._remoteId = value; + this._proxy.socketMap.set(this._id, value); + } + _peer; + get peer() { + return this._peer; + } + async _write(data, cb) { + (await (0, util_js_1.maybeGetAsyncProperty)(this._peer.messages.writeSocket))?.send({ + id: this._id, + remoteId: this._remoteId, + data, + }); + cb(); + } + async _destroy(cb) { + (await (0, util_js_1.maybeGetAsyncProperty)(this._peer.messages.closeSocket))?.send({ + id: this._id, + remoteId: this._remoteId, + }); + this._proxy.socketMap.delete(this._id); + this._proxy.sockets.delete(this._id); + } + async connect() { + (await (0, util_js_1.maybeGetAsyncProperty)(this._peer.messages.openSocket))?.send({ + ...this._options, + id: this._id, + }); + } + setTimeout(ms, cb) { + if (this._connectTimeout) { + (0, timers_1.clearTimeout)(this._connectTimeout); + } + this._connectTimeout = setTimeout(() => { + cb && cb(); + }, ms); + } +} +exports.default = DummySocket; diff --git a/dist/proxies/multiSocket/tcpSocket.d.ts b/dist/proxies/multiSocket/tcpSocket.d.ts new file mode 100644 index 0000000..f5004de --- /dev/null +++ b/dist/proxies/multiSocket/tcpSocket.d.ts @@ -0,0 +1,19 @@ +/// +import { Callback, Duplex } from "streamx"; +import { TcpSocketConnectOpts } from "net"; +import MultiSocketProxy from "../multiSocket.js"; +import { PeerEntity } from "./types.js"; +export default class TcpSocket extends Duplex { + private _options; + private _id; + private _remoteId; + private _proxy; + private _socket?; + constructor(id: number, remoteId: number, manager: MultiSocketProxy, peer: PeerEntity, options: TcpSocketConnectOpts); + private _peer; + get peer(): any; + _write(data: any, cb: any): void; + _destroy(cb: Callback): void; + connect(): void; + private _getSocketRequest; +} diff --git a/dist/proxies/multiSocket/tcpSocket.js b/dist/proxies/multiSocket/tcpSocket.js new file mode 100644 index 0000000..af5dddc --- /dev/null +++ b/dist/proxies/multiSocket/tcpSocket.js @@ -0,0 +1,89 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const streamx_1 = require("streamx"); +const net = __importStar(require("net")); +class TcpSocket extends streamx_1.Duplex { + _options; + _id; + _remoteId; + _proxy; + _socket; + constructor(id, remoteId, manager, peer, options) { + super(); + this._remoteId = remoteId; + this._proxy = manager; + this._id = id; + this._peer = peer; + this._options = options; + this._proxy.sockets.set(this._id, this); + this._proxy.socketMap.set(this._id, this._remoteId); + } + _peer; + get peer() { + return this._peer; + } + _write(data, cb) { + this._peer.messages.writeSocket?.send({ + ...this._getSocketRequest(), + data, + }); + cb(); + } + _destroy(cb) { + this._proxy.sockets.delete(this._id); + this._proxy.socketMap.delete(this._id); + this._peer.messages.closeSocket?.send(this._getSocketRequest()); + } + connect() { + this.on("error", (err) => { + this._peer.messages.errorSocket?.send({ + ...this._getSocketRequest(), + err, + }); + }); + // @ts-ignore + this.on("timeout", () => { + this._peer.messages.timeoutSocket?.send(this._getSocketRequest()); + }); + // @ts-ignore + this.on("connect", () => { + this._peer.messages.openSocket?.send(this._getSocketRequest()); + }); + this._socket = net.connect(this._options); + ["timeout", "error", "connect", "end", "destroy", "close"].forEach((event) => { + this._socket?.on(event, (...args) => this.emit(event, ...args)); + }); + this._socket.pipe(this); + this.pipe(this._socket); + } + _getSocketRequest() { + return { + id: this._id, + remoteId: this._remoteId, + }; + } +} +exports.default = TcpSocket; diff --git a/dist/proxies/multiSocket/types.d.ts b/dist/proxies/multiSocket/types.d.ts new file mode 100644 index 0000000..e8bcdb2 --- /dev/null +++ b/dist/proxies/multiSocket/types.d.ts @@ -0,0 +1,28 @@ +import Peer from "../../peer.js"; +export interface SocketRequest { + remoteId: number; + id: number; +} +export type CloseSocketRequest = SocketRequest; +export interface WriteSocketRequest extends SocketRequest { + data: Uint8Array; +} +export interface ErrorSocketRequest extends SocketRequest { + err: Error; +} +type Message = { + send: (pubkey: Uint8Array | any) => void; +}; +export interface PeerEntityMessages { + keyExchange: Message; + openSocket: Message; + writeSocket: Message; + closeSocket: Message; + timeoutSocket: Message; + errorSocket: Message; +} +export interface PeerEntity { + messages: PeerEntityMessages | Partial; + peer: Peer; +} +export {}; diff --git a/dist/proxies/multiSocket/types.js b/dist/proxies/multiSocket/types.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/dist/proxies/multiSocket/types.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/dist/proxy.d.ts b/dist/proxy.d.ts index 4496e9f..1bd61c1 100644 --- a/dist/proxy.d.ts +++ b/dist/proxy.d.ts @@ -5,7 +5,7 @@ export interface ProxyOptions extends DataSocketOptions { listen?: boolean; autostart?: boolean; } -export default class Proxy { +export default abstract class Proxy { private _listen; private _socketOptions; private _autostart; diff --git a/dist/util.d.ts b/dist/util.d.ts new file mode 100644 index 0000000..8aa6fce --- /dev/null +++ b/dist/util.d.ts @@ -0,0 +1,4 @@ +export declare function idFactory(start: number, step?: number, limit?: number): () => number; +export declare function roundRobinFactory(list: Map): () => T; +export declare function maybeGetAsyncProperty(object: any): Promise; +export declare function isPromise(obj: Promise): boolean; diff --git a/dist/util.js b/dist/util.js new file mode 100644 index 0000000..a0dca59 --- /dev/null +++ b/dist/util.js @@ -0,0 +1,41 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isPromise = exports.maybeGetAsyncProperty = exports.roundRobinFactory = exports.idFactory = void 0; +function idFactory(start, step = 1, limit = 2 ** 32) { + let id = start; + return function nextId() { + const nextId = id; + id += step; + if (id >= limit) + id = start; + return nextId; + }; +} +exports.idFactory = idFactory; +function roundRobinFactory(list) { + let index = 0; + return () => { + const keys = [...list.keys()].sort(); + if (index >= keys.length) { + index = 0; + } + return list.get(keys[index++]); + }; +} +exports.roundRobinFactory = roundRobinFactory; +async function maybeGetAsyncProperty(object) { + if (typeof object === "function") { + object = object(); + } + if (isPromise(object)) { + object = await object; + } + return object; +} +exports.maybeGetAsyncProperty = maybeGetAsyncProperty; +function isPromise(obj) { + return (!!obj && + (typeof obj === "object" || typeof obj === "function") && + typeof obj.then === "function"); +} +exports.isPromise = isPromise;