Compare commits

...

108 Commits

Author SHA1 Message Date
semantic-release-bot 03a8db88d8 chore(release): 0.0.2-develop.1 [skip ci]
## [0.0.2-develop.1](https://git.lumeweb.com/LumeWeb/libhyperproxy/compare/v0.0.1...v0.0.2-develop.1) (2023-07-05)
2023-07-05 10:26:20 +00:00
Derrick Hammer 2c931edb85
ci: add missing config 2023-07-05 06:25:15 -04:00
Derrick Hammer abd3d1d136
ci: setup 2023-07-05 06:23:26 -04:00
Derrick Hammer 1ad6c1d692
refactor: bug fixes, added type asserts and use ? chaining where needed 2023-07-05 06:19:41 -04:00
Derrick Hammer 8797249ae5
refactor: switch to new devops 2023-07-05 06:17:57 -04:00
Derrick Hammer 85cc802977
*update dist 2023-04-16 19:00:57 -04:00
Derrick Hammer eb65447adb
*convert data to buffer 2023-04-16 19:00:33 -04:00
Derrick Hammer c2031b4c7f
*Update dist 2023-04-16 17:34:59 -04:00
Derrick Hammer ec937e786b
* Refactor socket preencoding and encoding to use socketEncoding when _server is defined. 2023-04-16 17:34:42 -04:00
Derrick Hammer f3e5cebe45
*update dist 2023-04-16 17:17:49 -04:00
Derrick Hammer 9cb6d0b6e4
* Fix typo in multiSocket.ts where "m" should be "m.id". 2023-04-16 17:17:31 -04:00
Derrick Hammer e3e6757e47
* Refactor multiSocket.ts to check for server mode before checking for allowedPorts inclusion when accepting a new socket connection. 2023-04-16 17:13:47 -04:00
Derrick Hammer 3a1ea39a79
*Update dist 2023-04-16 14:29:26 -04:00
Derrick Hammer e19e4c1744
* Add serializeError import to multiSocket.ts and implement preencode and encode functions using JSON serialization for error messages. 2023-04-16 14:29:07 -04:00
Derrick Hammer 3a41474b26
*update dist 2023-04-16 14:20:12 -04:00
Derrick Hammer 339dbba08c
* Fix error in multiSocket.ts where 'this' was used instead of 'self' to refer to the current object. 2023-04-16 14:19:55 -04:00
Derrick Hammer c4c1fd8f6b
*Update dist 2023-04-16 14:15:51 -04:00
Derrick Hammer 623601df27
* Fix decoding method to correctly parse JSON data in multiSocket.ts. 2023-04-16 14:15:21 -04:00
Derrick Hammer cfd5f69cfb
*Update dist 2023-04-16 07:11:59 -04:00
Derrick Hammer bc85076e7f
* Refactor peer.ts to use call both handle and on* callbacks for open and close
* emit "peerChannelOpen" event on new channel open.
2023-04-16 07:11:39 -04:00
Derrick Hammer 85ff38d871
* Add initialization of messages and empty implementation of handleChannelOnOpen in Peer class. 2023-04-16 07:10:21 -04:00
Derrick Hammer 39499397ea
* Remove the invocation of the _onclose callback function when the socket is closed. 2023-04-16 07:09:42 -04:00
Derrick Hammer 1882752839
* Add await keyword to multiSocket.ts to await for message to be added to the peer channel. 2023-04-16 07:09:09 -04:00
Derrick Hammer 9791e7c4a1
*Update dist 2023-04-16 06:08:50 -04:00
Derrick Hammer d30528caa0
* Add event emitter for when peer connection is established. 2023-04-16 06:08:31 -04:00
Derrick Hammer b3d6fd4668
*Update dist 2023-04-16 05:48:53 -04:00
Derrick Hammer b04db8668b
*Put _nextPeer into the constructor 2023-04-16 05:48:32 -04:00
Derrick Hammer 35bf05c25e
*Update dist 2023-04-16 05:38:59 -04:00
Derrick Hammer f842cd84c4
**createSocket does not need to be async 2023-04-16 05:38:36 -04:00
Derrick Hammer c3c8e6fb3b
*Update dist 2023-04-16 05:24:00 -04:00
Derrick Hammer 96dd1ad46e
* as public key may be from the web client, need to use new _getPublicKey utility that uses maybeGetAsyncProperty 2023-04-16 05:23:36 -04:00
Derrick Hammer 10a7b4ebc6
*Update dist 2023-04-16 03:42:14 -04:00
Derrick Hammer 1c9a430d5e
* Refactor handlePeer function to use async/await and emit "peer" event, and make Proxy class extend EventEmitter. 2023-04-16 03:41:50 -04:00
Derrick Hammer 8b678e81e8
*Update dist 2023-04-16 03:17:02 -04:00
Derrick Hammer 13ff64002d
* Update references from peer.socket to peer.stream in multiSocket.ts. 2023-04-16 03:16:40 -04:00
Derrick Hammer 40e139df0d
*Update dist 2023-04-16 03:06:53 -04:00
Derrick Hammer 518bdca8ad
* Refactor Peer and MultiSocket classes to handle new peer channels more efficiently and cleanly. 2023-04-16 03:06:34 -04:00
Derrick Hammer 84bc6ce1cb
*update dist 2023-04-16 00:48:03 -04:00
Derrick Hammer 605f760fe3
*missing init 2023-04-16 00:47:45 -04:00
Derrick Hammer a8419313b5
*update dist 2023-04-15 22:38:07 -04:00
Derrick Hammer f11e3fed78
*implement handlePeer 2023-04-15 22:37:49 -04:00
Derrick Hammer 95d866c69e
*Remove _socketOptions overrides 2023-04-15 22:35:22 -04:00
Derrick Hammer 5fa5385249
*update dist 2023-04-15 22:17:57 -04:00
Derrick Hammer 774e84996e
*further refactoring 2023-04-15 22:17:30 -04:00
Derrick Hammer 4b1b828c69
*update dist 2023-04-15 19:45:35 -04:00
Derrick Hammer 8ea1ad3006
*make _socketOptions protected 2023-04-15 19:45:16 -04:00
Derrick Hammer ddf6b2c9d8
*add getter for socketOptions 2023-04-15 19:44:01 -04:00
Derrick Hammer 422c11b9b8
*update dist 2023-04-15 19:35:19 -04:00
Derrick Hammer b2b041c3a1
*override callbacks 2023-04-15 19:34:45 -04:00
Derrick Hammer d63fa22d00
*make protected 2023-04-15 19:34:24 -04:00
Derrick Hammer f8b8633287
*update dist 2023-04-15 18:40:09 -04:00
Derrick Hammer c01d866d8b
*refactoring
*create basicproxy
*create multisocket proxy with dummy and tcp socket classes based off IPFS proxy code
2023-04-15 18:39:47 -04:00
Derrick Hammer 22023baedc
*update dist 2023-04-09 12:24:05 -04:00
Derrick Hammer 94342e6929
*async fetch remotePublicKey 2023-04-09 12:23:49 -04:00
Derrick Hammer 6099a6c4f6
*Update dist 2023-04-09 12:15:38 -04:00
Derrick Hammer 5c666d38c0
*Need to async fetch rawStream 2023-04-09 12:15:23 -04:00
Derrick Hammer 94e817f045
*Update dist 2023-04-08 20:56:45 -04:00
Derrick Hammer 92245a6c1c
*As a kernel-based protomux may be used, need to await on createChannel and addMessage 2023-04-08 20:56:26 -04:00
Derrick Hammer 8e0edc4bcd
*Update dist 2023-03-15 08:14:29 -04:00
Derrick Hammer c1d495a54b
*add createDefaultMessage option to disable the default socket pipe 2023-03-15 08:14:07 -04:00
Derrick Hammer 27261fedd2
*Update dist 2023-03-07 03:25:02 -05:00
Derrick Hammer 2e639eb92a
*Move socket creation outside channel onopen 2023-03-07 03:24:15 -05:00
Derrick Hammer 619fe7913c
*Update dist 2023-03-05 03:00:28 -05:00
Derrick Hammer 7bbfe577c3
*Add getter for socket 2023-03-05 03:00:04 -05:00
Derrick Hammer 41984ae588
*Update dist 2023-03-03 05:22:55 -05:00
Derrick Hammer 6c6f6c4954
*Add onchannel to allow creating additional messages 2023-03-03 05:22:29 -05:00
Derrick Hammer b14509d6f1
*Update dist 2023-03-02 05:41:30 -05:00
Derrick Hammer b3a7c0b4e1
*Add getter for channel 2023-03-02 05:41:13 -05:00
Derrick Hammer baf0ac31e6
*Update dist 2023-03-02 05:37:30 -05:00
Derrick Hammer ae2ee61bec
*Update types and add new bound versions 2023-03-02 05:37:01 -05:00
Derrick Hammer b9611cecd7
*Update OnOpen type 2023-03-02 05:30:51 -05:00
Derrick Hammer e5f4f59477
*Don't bind this, but inject ourselves as the first argument 2023-03-02 05:30:14 -05:00
Derrick Hammer c9cfb64c80
*Update dist 2023-03-02 05:26:40 -05:00
Derrick Hammer 4220037402
*Store channel as a private property
*Bind all callbacks to the current peer
2023-03-02 05:25:50 -05:00
Derrick Hammer 1d4cb917c5
*update dist 2023-02-25 22:49:50 -05:00
Derrick Hammer 08b4bbc893
*make alias types optional 2023-02-25 22:49:34 -05:00
Derrick Hammer 3b7be7e892
*alias close to end 2023-02-25 22:48:57 -05:00
Derrick Hammer f89e2716f5
*Update dist 2023-02-25 22:47:19 -05:00
Derrick Hammer 84f2f96e12
*Change WS aliases to be conditional
*Remove on/off aliases since streamx already provides it
2023-02-25 22:46:34 -05:00
Derrick Hammer b1ad8399d3
*Update dist 2023-02-25 22:38:46 -05:00
Derrick Hammer 7980aa29c7
*Add websocket compatibility with readyState 2023-02-25 22:38:25 -05:00
Derrick Hammer d94eec7c7e
*update dist 2023-02-25 22:21:39 -05:00
Derrick Hammer 68cd8008a6
*add emulateWebsocket to proxy 2023-02-25 22:21:09 -05:00
Derrick Hammer 6b42c2400f
*Update dist 2023-02-25 22:16:59 -05:00
Derrick Hammer bdacd01849
*add emulateWebsocket to peer 2023-02-25 22:16:40 -05:00
Derrick Hammer 603d7c4e14
*update dist 2023-02-25 22:12:36 -05:00
Derrick Hammer 4d75c11299
*event message must pass a MessageEvent 2023-02-25 22:12:05 -05:00
Derrick Hammer 29c6a909b8
*Update dist 2023-02-25 22:10:17 -05:00
Derrick Hammer 68c12f22be
*Add option to emulate a websocket 2023-02-25 22:10:00 -05:00
Derrick Hammer 11550659d0
*alias write to send to be WS compatible 2023-02-25 22:06:37 -05:00
Derrick Hammer c90e022795
*Update dist 2023-02-25 12:07:21 -05:00
Derrick Hammer 39471ad9c2
*Create on/off event handler aliases 2023-02-25 12:06:51 -05:00
Derrick Hammer 8bae1c11dd
*Update dist 2023-02-25 01:51:50 -05:00
Derrick Hammer ab98103503
*Alias addListener and removeListener to have WS compatible API methods 2023-02-25 01:51:35 -05:00
Derrick Hammer 8734d8180c
*Update dist 2023-02-24 23:51:59 -05:00
Derrick Hammer 9a73a2625a
*Bug fix ip version detection 2023-02-24 23:51:50 -05:00
Derrick Hammer 0a35d7f062
*Update dist 2023-02-24 23:14:57 -05:00
Derrick Hammer 5e41c10ab6
*Update pass peers public key 2023-02-24 23:14:36 -05:00
Derrick Hammer 10d3c1fde8
*Update dist 2023-02-24 20:04:45 -05:00
Derrick Hammer 108e1b3dd8
*Add remotePublicKey to socket class 2023-02-24 20:04:30 -05:00
Derrick Hammer 3ce2313c5a
*Update dist 2023-02-16 01:02:20 -05:00
Derrick Hammer 47c93b1f1a
*need to use socket destroy 2023-02-16 01:02:00 -05:00
Derrick Hammer 035be11686
*Update dist 2023-02-06 10:52:26 -05:00
Derrick Hammer d48dde7bd1
*Add declares for readable and writable 2023-02-06 10:52:13 -05:00
Derrick Hammer 5b87187bc0
*Update dist 2023-02-06 05:51:41 -05:00
Derrick Hammer b4b30cbdea
*If data is a Uint8Array, convert to a buffer 2023-02-06 05:51:13 -05:00
Derrick Hammer 81559342be
*Add dist 2023-01-12 12:50:38 -05:00
Derrick Hammer 4e12788db5
*Initial commit 2023-01-12 12:39:23 -05:00
19 changed files with 20594 additions and 1 deletions

13
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,13 @@
name: Build/Publish
on:
push:
branches:
- master
- develop
- develop-*
jobs:
main:
uses: lumeweb/github-node-deploy-workflow/.github/workflows/main.yml@master
secrets: inherit

5
.presetterrc.json Normal file
View File

@ -0,0 +1,5 @@
{
"preset": [
"@lumeweb/node-library-preset"
]
}

1
CHANGELOG.md Normal file
View File

@ -0,0 +1 @@
## [0.0.2-develop.1](https://git.lumeweb.com/LumeWeb/libhyperproxy/compare/v0.0.1...v0.0.2-develop.1) (2023-07-05)

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) <year> <copyright holders>
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:

19419
npm-shrinkwrap.json generated Normal file

File diff suppressed because it is too large Load Diff

35
package.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "@lumeweb/libhyperproxy",
"version": "0.0.2-develop.1",
"main": "lib/index.js",
"type": "module",
"repository": {
"type": "git",
"url": "gitea@git.lumeweb.com:LumeWeb/libhyperproxy.git"
},
"devDependencies": {
"@lumeweb/node-library-preset": "^0.2.7",
"@types/serialize-error": "^4.0.1",
"@types/streamx": "^2.9.1",
"presetter": "*"
},
"readme": "ERROR: No README data found!",
"scripts": {
"prepare": "presetter bootstrap",
"build": "run build",
"semantic-release": "semantic-release"
},
"dependencies": {
"binconv": "^0.2.0",
"compact-encoding": "^2.12.0",
"protomux": "^3.5.0",
"serialize-error": "^11.0.0",
"streamx": "^2.15.0"
},
"files": [
"lib"
],
"publishConfig": {
"access": "public"
}
}

45
src/index.ts Normal file
View File

@ -0,0 +1,45 @@
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";
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 function createSocket(port: number, host: string): Socket {
return new Socket({
remotePort: port,
remoteAddress: host,
});
}
export function createServer(): Server {
return new Server();
}

132
src/peer.ts Normal file
View File

@ -0,0 +1,132 @@
import Proxy from "./proxy.js";
import Socket from "./socket.js";
export type OnOpen = (
peer: Peer,
socket: Socket,
data: any,
) =>
| { connect: boolean }
| Promise<{ connect: boolean }>
| Promise<void>
| void;
export type OnData = (peer: Peer, data: any) => void;
export type OnReceive = OnData;
export type OnClose = OnData;
export type OnSend = OnData;
export type OnChannel = (peer: Peer, channel: any) => void;
export type OnOpenBound = (
socket: Socket,
data: any,
) =>
| { connect: boolean }
| Promise<{ connect: boolean }>
| Promise<void>
| void;
export type OnDataBound = (data: any) => void;
export type OnReceiveBound = OnDataBound;
export type OnCloseBound = OnDataBound;
export type OnSendBound = OnDataBound;
export type OnChannelBound = (channel: any) => void;
export interface DataSocketOptions {
onopen?: OnOpen;
onreceive?: OnReceive;
onsend?: OnSend;
onclose?: OnClose;
onchannel?: OnChannel;
emulateWebsocket?: boolean;
}
export interface PeerOptions {
peer: any;
muxer: any;
}
export interface PeerOptionsWithProxy extends PeerOptions {
proxy: Proxy;
}
export default abstract class Peer {
protected _proxy: Proxy;
protected _peer: any;
protected _muxer: any;
protected _onopen: OnOpenBound;
protected _onreceive: OnReceiveBound;
protected _onsend: OnSendBound;
protected _onclose: OnCloseBound;
protected _onchannel: OnChannelBound;
protected _emulateWebsocket: boolean;
constructor({
proxy,
peer,
muxer,
onopen,
onreceive,
onsend,
onclose,
onchannel,
emulateWebsocket = false,
}: PeerOptionsWithProxy & DataSocketOptions) {
this._proxy = proxy;
this._peer = peer;
this._muxer = muxer;
this._onopen = onopen?.bind(undefined, this) as OnOpenBound;
this._onreceive = onreceive?.bind(undefined, this) as OnReceiveBound;
this._onsend = onsend?.bind(undefined, this) as OnSendBound;
this._onclose = onclose?.bind(undefined, this) as OnCloseBound;
this._onchannel = onchannel?.bind(undefined, this) as OnChannelBound;
this._emulateWebsocket = emulateWebsocket;
}
protected _socket?: Socket;
get socket(): Socket {
return this._socket as Socket;
}
protected _channel?: any;
get channel(): any {
return this._channel;
}
protected abstract initSocket();
protected abstract handleChannelOnOpen(m: any): Promise<void>;
protected abstract handleChannelOnClose(socket: Socket): Promise<void>;
protected async initChannel() {
const self = this;
this._channel = await this._muxer.createChannel({
protocol: this._proxy.protocol,
onopen: async (m: any) => {
await this.handleChannelOnOpen(m);
// @ts-ignore
await this._onopen?.(this._channel);
},
onclose: async (socket: Socket) => {
await this.handleChannelOnClose(socket);
// @ts-ignore
await this._onclose?.();
},
});
await this.initMessages();
await this._onchannel?.(this._channel);
await this._channel.open();
this._proxy.emit("peerChannelOpen", this);
}
async init() {
await this.initSocket();
await this.initChannel();
}
protected async initMessages() {}
}

14
src/proxies/basic.ts Normal file
View File

@ -0,0 +1,14 @@
import Proxy from "../proxy.js";
import { DataSocketOptions, PeerOptions } from "../peer.js";
import BasicPeer from "./basic/peer.js";
export default class BasicProxy extends Proxy {
protected handlePeer({
peer,
muxer,
...options
}: DataSocketOptions & PeerOptions) {
const conn = new BasicPeer({ proxy: this, peer, muxer, ...options });
conn.init();
}
}

61
src/proxies/basic/peer.ts Normal file
View File

@ -0,0 +1,61 @@
import BasePeer from "#peer.js";
import { maybeGetAsyncProperty } from "#util.js";
import Socket from "#socket.js";
import { Buffer } from "buffer";
export default class Peer extends BasePeer {
private _pipe?: any;
protected async initSocket() {
const self = this;
const raw = await maybeGetAsyncProperty(self._peer.rawStream);
this._socket = new Socket({
remoteAddress: raw.remoteHost,
remotePort: raw.remotePort,
remotePublicKey: await maybeGetAsyncProperty(self._peer.remotePublicKey),
async write(data: any, cb: Function) {
self._pipe?.send(data);
await self._onsend?.(data);
cb();
},
emulateWebsocket: self._emulateWebsocket,
});
}
protected async handleChannelOnOpen(m: any) {
if (!m) {
m = Buffer.from([]);
}
if (m instanceof Uint8Array) {
m = Buffer.from(m);
}
this._socket?.on("end", () => this._channel.close());
let ret = await this._onopen?.(this._socket as Socket, m);
if (!ret || (ret && ret.connect === false)) {
// @ts-ignore
self._socket?.emit("connect");
}
this._socket?.emit("data", m);
}
protected async handleChannelOnClose(socket: Socket): Promise<void> {
this._socket?.destroy();
}
protected async initMessages(): Promise<void> {
const self = this;
this._pipe = await this._channel.addMessage({
async onmessage(m: any) {
if (m instanceof Uint8Array) {
m = Buffer.from(m);
}
self._socket?.emit("data", m);
await self._onreceive?.(m);
},
});
}
}

311
src/proxies/multiSocket.ts Normal file
View File

@ -0,0 +1,311 @@
import Proxy, { ProxyOptions } from "../proxy.js";
import TcpSocket from "./multiSocket/tcpSocket.js";
import { json, raw, uint } from "compact-encoding";
import { deserializeError, serializeError } from "serialize-error";
import b4a from "b4a";
import type { TcpSocketConnectOpts } from "net";
import { DataSocketOptions, PeerOptions } from "../peer.js";
import {
roundRobinFactory,
idFactory,
maybeGetAsyncProperty,
} from "../util.js";
import {
CloseSocketRequest,
ErrorSocketRequest,
PeerEntity,
SocketRequest,
WriteSocketRequest,
} from "./multiSocket/types.js";
import DummySocket from "./multiSocket/dummySocket.js";
import Peer from "./multiSocket/peer.js";
import { uint8ArrayToHexString } from "binconv";
export interface MultiSocketProxyOptions extends ProxyOptions {
socketClass?: any;
server: boolean;
allowedPorts?: number[];
}
const socketEncoding = {
preencode(state: any, m: SocketRequest) {
uint.preencode(state, m.id);
uint.preencode(state, m.remoteId);
},
encode(state: any, m: SocketRequest) {
uint.encode(state, m.id);
uint.encode(state, m.remoteId);
},
decode(state: any, m: any): SocketRequest {
return {
remoteId: uint.decode(state, m),
id: uint.decode(state, m),
};
},
};
const writeSocketEncoding = {
preencode(state: any, m: WriteSocketRequest) {
socketEncoding.preencode(state, m);
raw.preencode(state, m.data);
},
encode(state: any, m: WriteSocketRequest) {
socketEncoding.encode(state, m);
raw.encode(state, m.data);
},
decode(state: any, m: any): WriteSocketRequest {
const socket = socketEncoding.decode(state, m);
return {
...socket,
data: raw.decode(state, m),
};
},
};
const errorSocketEncoding = {
preencode(state: any, m: ErrorSocketRequest) {
socketEncoding.preencode(state, m);
json.preencode(state, serializeError(m.err));
},
encode(state: any, m: ErrorSocketRequest) {
socketEncoding.encode(state, m);
json.encode(state, serializeError(m.err));
},
decode(state: any, m: any): ErrorSocketRequest {
const socket = socketEncoding.decode(state, m);
return {
...socket,
err: deserializeError(json.decode(state, m)),
};
},
};
const nextSocketId = idFactory(1);
export default class MultiSocketProxy extends Proxy {
async handlePeer({
peer,
muxer,
...options
}: DataSocketOptions & PeerOptions) {
const conn = new Peer({
...this.socketOptions,
proxy: this,
peer,
muxer,
...options,
});
await conn.init();
this.emit("peer", conn);
}
private socketClass: any;
private _peers: Map<string, PeerEntity> = new Map<string, PeerEntity>();
private _nextPeer;
private _server = false;
private _allowedPorts: number[] = [];
constructor(options: MultiSocketProxyOptions) {
super(options);
if (options.socketClass) {
this.socketClass = options.socketClass;
} else {
if (options.server) {
this.socketClass = TcpSocket;
} else {
this.socketClass = DummySocket;
}
}
if (options.server) {
this._server = true;
}
this._nextPeer = roundRobinFactory(this._peers);
}
private _socketMap = new Map<number, number>();
get socketMap(): Map<number, number> {
return this._socketMap;
}
private _sockets = new Map<number, typeof this.socketClass>();
get sockets(): Map<number, any> {
return this._sockets;
}
async handleNewPeerChannel(peer: Peer) {
this.update(await this._getPublicKey(peer), {
peer,
});
await this._registerOpenSocketMessage(peer);
await this._registerWriteSocketMessage(peer);
await this._registerCloseSocketMessage(peer);
await this._registerTimeoutSocketMessage(peer);
await this._registerErrorSocketMessage(peer);
}
async handleClosePeer(peer: Peer) {
for (const item of this._sockets) {
if (item[1].peer.peer === peer) {
item[1].end();
}
}
const pubkey = this._toString(await this._getPublicKey(peer));
if (this._peers.has(pubkey)) {
this._peers.delete(pubkey);
}
}
public get(pubkey: Uint8Array): PeerEntity | undefined {
if (this._peers.has(this._toString(pubkey))) {
return this._peers.get(this._toString(pubkey)) as PeerEntity;
}
return undefined;
}
public update(pubkey: Uint8Array, data: Partial<PeerEntity>): void {
const peer = this.get(pubkey) ?? ({} as PeerEntity);
this._peers.set(this._toString(pubkey), {
...peer,
...data,
...{
messages: {
...peer?.messages,
...data?.messages,
},
},
} as PeerEntity);
}
public createSocket(options: TcpSocketConnectOpts): typeof this.socketClass {
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;
}
private async _registerOpenSocketMessage(peer: Peer) {
const self = this;
const message = await peer.channel.addMessage({
encoding: {
preencode: this._server ? socketEncoding.preencode : json.preencode,
encode: this._server ? socketEncoding.encode : json.encode,
decode: this._server ? json.decode : socketEncoding.decode,
},
async onmessage(m: SocketRequest | TcpSocketConnectOpts) {
if (self._server) {
if (
self._allowedPorts.length &&
!self._allowedPorts.includes((m as TcpSocketConnectOpts).port)
) {
self
.get(await self._getPublicKey(peer))
?.messages.errorSocket?.send({
id: (m as SocketRequest).id,
err: new Error(
`port ${(m as TcpSocketConnectOpts).port} not allowed`,
),
});
return;
}
}
m = m as SocketRequest;
if (self._server) {
new self.socketClass(
nextSocketId(),
m.id,
self,
self.get(await self._getPublicKey(peer)) as PeerEntity,
m,
).connect();
return;
}
const socket = self._sockets.get(m.id);
if (socket) {
socket.remoteId = m.remoteId;
// @ts-ignore
socket.emit("connect");
}
},
});
this.update(await this._getPublicKey(peer), {
messages: { openSocket: message },
});
}
private async _registerWriteSocketMessage(peer: Peer) {
const self = this;
const message = await peer.channel.addMessage({
encoding: writeSocketEncoding,
onmessage(m: WriteSocketRequest) {
self._sockets.get(m.id)?.push(b4a.from(m.data));
},
});
this.update(await this._getPublicKey(peer), {
messages: { writeSocket: message },
});
}
private async _registerCloseSocketMessage(peer: Peer) {
const self = this;
const message = await peer.channel.addMessage({
encoding: socketEncoding,
onmessage(m: CloseSocketRequest) {
self._sockets.get(m.id)?.end();
},
});
this.update(await this._getPublicKey(peer), {
messages: { closeSocket: message },
});
}
private async _registerTimeoutSocketMessage(peer: Peer) {
const self = this;
const message = await peer.channel.addMessage({
encoding: socketEncoding,
onmessage(m: SocketRequest) {
// @ts-ignore
self._sockets.get(m.id)?.emit("timeout");
},
});
this.update(await this._getPublicKey(peer), {
messages: { timeoutSocket: message },
});
}
private async _registerErrorSocketMessage(peer: Peer) {
const self = this;
const message = await peer.channel.addMessage({
encoding: errorSocketEncoding,
onmessage(m: ErrorSocketRequest) {
self._sockets.get(m.id)?.emit("error", m.err);
},
});
this.update(await this._getPublicKey(peer), {
messages: { errorSocket: message },
});
}
private _toString(pubkey: Uint8Array) {
return uint8ArrayToHexString(pubkey);
}
private async _getPublicKey(peer: Peer) {
return maybeGetAsyncProperty(peer.stream.remotePublicKey);
}
}

View File

@ -0,0 +1,84 @@
import { Callback, Duplex } from "streamx";
import { TcpSocketConnectOpts } from "net";
import { clearTimeout } from "timers";
import MultiSocketProxy from "../multiSocket.js";
import { PeerEntity, SocketRequest, WriteSocketRequest } from "./types.js";
import { maybeGetAsyncProperty } from "#util.js";
import Socket, { SocketOptions } from "#socket.js";
export default class DummySocket extends Socket {
private _options: TcpSocketConnectOpts;
private _id: number;
private _proxy: MultiSocketProxy;
private _connectTimeout?: number;
constructor(
id: number,
manager: MultiSocketProxy,
peer: PeerEntity,
connectOptions: TcpSocketConnectOpts,
socketOptions: SocketOptions,
) {
super(socketOptions);
this._id = id;
this._proxy = manager;
this._peer = peer;
this._options = connectOptions;
// @ts-ignore
this.on("timeout", () => {
if (this._connectTimeout) {
clearTimeout(this._connectTimeout);
}
});
}
private _remoteId = 0;
set remoteId(value: number) {
this._remoteId = value;
this._proxy.socketMap.set(this._id, value);
}
private _peer;
get peer() {
return this._peer;
}
public async _write(data: any, cb: any): Promise<void> {
(await maybeGetAsyncProperty(this._peer.messages.writeSocket))?.send({
id: this._id,
remoteId: this._remoteId,
data,
} as WriteSocketRequest);
cb();
}
public async _destroy(cb: Callback) {
(await maybeGetAsyncProperty(this._peer.messages.closeSocket))?.send({
id: this._id,
remoteId: this._remoteId,
} as SocketRequest);
this._proxy.socketMap.delete(this._id);
this._proxy.sockets.delete(this._id);
}
public async connect() {
(await maybeGetAsyncProperty(this._peer.messages.openSocket))?.send({
...this._options,
id: this._id,
});
}
public setTimeout(ms: number, cb: Function) {
if (this._connectTimeout) {
clearTimeout(this._connectTimeout);
}
this._connectTimeout = setTimeout(() => {
cb && cb();
}, ms) as any;
}
}

View File

@ -0,0 +1,21 @@
import BasePeer from "#peer.js";
import Socket from "#socket.js";
import MultiSocketProxy from "../multiSocket.js";
export default class Peer extends BasePeer {
protected async initMessages(): Promise<void> {
await this._proxy.handleNewPeerChannel(this);
}
protected declare _proxy: MultiSocketProxy;
protected async initSocket() {}
get stream(): any {
return this._muxer.stream;
}
protected async handleChannelOnClose(socket: Socket): Promise<void> {
return this._proxy.handleClosePeer(this);
}
protected async handleChannelOnOpen(m: any): Promise<void> {}
}

View File

@ -0,0 +1,90 @@
import { Callback } from "streamx";
import * as net from "net";
import { Socket, TcpSocketConnectOpts } from "net";
import MultiSocketProxy from "../multiSocket.js";
import { PeerEntity, SocketRequest, WriteSocketRequest } from "./types.js";
import BaseSocket from "../../socket.js";
export default class TcpSocket extends BaseSocket {
private _options;
private _id: number;
private _remoteId: number;
private _proxy: MultiSocketProxy;
private _socket?: Socket;
constructor(
id: number,
remoteId: number,
manager: MultiSocketProxy,
peer: PeerEntity,
options: TcpSocketConnectOpts,
) {
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);
}
private _peer;
get peer() {
return this._peer;
}
public _write(data: any, cb: any): void {
this._peer.messages.writeSocket?.send({
...this._getSocketRequest(),
data,
} as WriteSocketRequest);
cb();
}
public _destroy(cb: Callback) {
this._proxy.sockets.delete(this._id);
this._proxy.socketMap.delete(this._id);
this._peer.messages.closeSocket?.send(this._getSocketRequest());
}
public connect() {
this.on("error", (err: Error) => {
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: any) =>
this.emit(event as any, ...args),
);
},
);
this._socket.pipe(this as any);
this.pipe(this._socket);
}
private _getSocketRequest(): SocketRequest {
return {
id: this._id,
remoteId: this._remoteId,
};
}
}

View File

@ -0,0 +1,34 @@
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<PeerEntityMessages>;
peer: Peer;
}

94
src/proxy.ts Normal file
View File

@ -0,0 +1,94 @@
import Protomux from "protomux";
import { DataSocketOptions, PeerOptions } from "./peer.js";
import EventEmitter from "events";
export interface ProxyOptions extends DataSocketOptions {
swarm: any;
protocol: string;
listen?: boolean;
autostart?: boolean;
}
export default abstract class Proxy extends EventEmitter {
protected _listen: any;
protected _autostart: boolean;
constructor({
swarm,
protocol,
onopen,
onreceive,
onsend,
onclose,
onchannel,
listen = false,
autostart = false,
emulateWebsocket = false,
}: ProxyOptions) {
super();
this._swarm = swarm;
this._protocol = protocol;
this._listen = listen;
this._autostart = autostart;
this._socketOptions = {
onopen,
onreceive,
onsend,
onclose,
onchannel,
emulateWebsocket,
};
this.init();
}
protected _socketOptions: DataSocketOptions;
get socketOptions(): DataSocketOptions {
return this._socketOptions;
}
private _swarm: any;
get swarm(): any {
return this._swarm;
}
private _protocol: string;
get protocol(): string {
return this._protocol;
}
protected abstract handlePeer({
peer,
muxer,
...options
}: DataSocketOptions & PeerOptions);
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);
}
}

45
src/server.ts Normal file
View File

@ -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;
}
}

149
src/socket.ts Normal file
View File

@ -0,0 +1,149 @@
import { Callback, Duplex, DuplexEvents } from "streamx";
const IPV4 = "IPv4";
const IPV6 = "IPv6";
type AddressFamily = "IPv6" | "IPv4";
export interface SocketOptions {
allowHalfOpen?: boolean;
remoteAddress?: string;
remotePort?: number;
remotePublicKey?: Uint8Array;
write?: (
this: Duplex<any, any, any, any, true, true, DuplexEvents<any, any>>,
data: any,
cb: Callback,
) => void;
emulateWebsocket?: boolean;
}
export default class Socket extends Duplex {
private _allowHalfOpen: boolean;
public remoteAddress: any;
public remotePort: any;
public remoteFamily?: AddressFamily;
declare readable: true;
declare writable: true;
public remotePublicKey: Uint8Array;
private _emulateWebsocket: boolean;
declare addEventListener?: typeof this.addListener;
declare removeEventListener?: typeof this.removeListener;
declare send?: typeof this.write;
declare close?: typeof this.end;
constructor({
allowHalfOpen = false,
remoteAddress,
remotePort,
remotePublicKey,
write,
emulateWebsocket = false,
}: SocketOptions = {}) {
super({ write });
this._allowHalfOpen = allowHalfOpen;
this.remoteAddress = remoteAddress;
this.remotePort = remotePort;
this.remotePublicKey = remotePublicKey as Uint8Array;
this._emulateWebsocket = emulateWebsocket;
if (remoteAddress) {
const type = Socket.isIP(remoteAddress);
if (!type) {
throw Error("invalid remoteAddress");
}
this.remoteFamily = type === 6 ? IPV6 : IPV4;
}
if (this._emulateWebsocket) {
this.addEventListener = this.addListener;
this.removeEventListener = this.removeListener;
this.send = this.write;
this.close = this.end;
this.addEventListener("data", (data: any) =>
// @ts-ignore
this.emit("message", new MessageEvent("data", { data })),
);
}
}
private _connecting?: boolean;
get connecting(): boolean {
return this._connecting as boolean;
}
get readyState(): string | number {
if (this._emulateWebsocket) {
if (this._connecting) {
return 0;
} else if (this.readable && this.writable) {
return 1;
} else {
return 3;
}
}
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,
);
}
}

40
src/util.ts Normal file
View File

@ -0,0 +1,40 @@
export function idFactory(start: number, step = 1, limit = 2 ** 32) {
let id = start;
return function nextId() {
const nextId = id;
id += step;
if (id >= limit) id = start;
return nextId;
};
}
export function roundRobinFactory<T>(list: Map<string, any>) {
let index = 0;
return (): T => {
const keys = [...list.keys()].sort();
if (index >= keys.length) {
index = 0;
}
return list.get(keys[index++]);
};
}
export async function maybeGetAsyncProperty(object: any) {
if (typeof object === "function") {
object = object();
}
if (isPromise(object)) {
object = await object;
}
return object;
}
export function isPromise(obj: Promise<any>) {
return (
!!obj &&
(typeof obj === "object" || typeof obj === "function") &&
typeof obj.then === "function"
);
}