*Initial commit

This commit is contained in:
Derrick Hammer 2023-01-12 12:39:23 -05:00
parent d586adb553
commit 4e12788db5
Signed by: pcfreak30
GPG Key ID: C997C339BE476FF2
8 changed files with 406 additions and 1 deletions

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:

16
package.json Normal file
View File

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

37
src/index.ts Normal file
View File

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

100
src/peer.ts Normal file
View File

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

80
src/proxy.ts Normal file
View File

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

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

113
src/socket.ts Normal file
View File

@ -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<any, any, any, any, true, true, DuplexEvents<any, any>>,
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
);
}
}

14
tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "esnext",
"esModuleInterop": true,
"outDir": "dist",
"moduleResolution": "node",
"declaration": true,
},
"include": ["src"],
"exclude": [
"node_modules"
]
}