feat: Complete zCode CLI X with Telegram bot integration

- Add full Telegram bot functionality with Z.AI API integration
- Implement 4 tools: Bash, FileEdit, WebSearch, Git
- Add 3 agents: Code Reviewer, Architect, DevOps Engineer
- Add 6 skills for common coding tasks
- Add systemd service file for 24/7 operation
- Add nginx configuration for HTTPS webhook
- Add comprehensive documentation
- Implement WebSocket server for real-time updates
- Add logging system with Winston
- Add environment validation

🤖 zCode CLI X - Agentic coder with Z.AI + Telegram integration
This commit is contained in:
admin
2026-05-05 09:01:26 +00:00
Unverified
parent 4a7035dd92
commit 875c7f9b91
24688 changed files with 3224957 additions and 221 deletions

View File

@@ -0,0 +1 @@
/home/uroma2/zcode-cli-x/~/.npm-cache/@pondwader/socks5-server@1.0.10@@@1

View File

@@ -0,0 +1,27 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- uses: pnpm/action-setup@v4
with:
version: 9
run_install: true
- run: pnpm test

View File

@@ -0,0 +1,20 @@
MIT License
Copyright (c) 2024 PondWader
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE

View File

@@ -0,0 +1,151 @@
# node-socks5-server
A Node.js implementation of a socks5 server written in TypeScript.
The library handles the protocol side but allows you to gain fine-grained control of connections and how they're handled.
> **Features:**
- Override the handling of socket proxying
- Handle authentication yourself
- Full type support
- Process Duplex streams as connections
## Installation
With npm:
```
npm i @pondwader/socks5-server
```
With yarn:
```
yarn add @pondwader/socks5-server
```
## Basic usage
Spin up a basic socks5 proxy server with just this code:
```js
const { createServer } = require('@pondwader/socks5-server');
createServer({
port: 5000
})
```
Or handle the listening yourself:
```js
const { createServer } = require('@pondwader/socks5-server');
const server = createServer();
server.listen(5000, '127.0.0.1', () => {
console.log('Server listening on port 5000');
})
```
## Username-password authentication
```js
const { createServer } = require('@pondwader/socks5-server');
createServer({
port: 5000,
auth: {
username: 'user123',
password: 'password123'
}
})
```
Or handle the authentication yourself:
```js
const { createServer } = require('@pondwader/socks5-server');
const server = createServer({
port: 5000
})
// Using a synchronous function
server.setAuthHandler((conn) => {
return conn.username === 'user123' && conn.password === 'password123';
})
// Using a promise
server.setAuthHandler((conn) => {
return new Promise(resolve => {
resolve(conn.username === 'user123' && conn.password === 'password123');
})
})
// Using callbacks
server.setAuthHandler((conn, accept, reject) => {
if (conn.username === 'user123' && conn.password === 'password123') accept();
else reject();
})
```
## Rejecting connections for breaking ruleset
You can reject connections that beak your ruleset:
```js
const { createServer } = require('@pondwader/socks5-server');
const server = createServer({
port: 5000
})
// Using a synchronous return
server.setRulesetValidator((conn) => {
return conn.destPort !== 25;
});
// Using a promise
server.setRulesetValidator((conn) => {
return new Promise(resolve => {
resolve(conn.destPort !== 25);
})
});
// Using callbacks
server.setRulesetValidator((conn, accept, deny) => {
if (conn.destPort === 25) deny();
else accept();
});
```
You also have to access to `<Socks5Connection>.destAddress`.
## Handling the proxying of connections
By default the library will handle connections itself using the built in connection handler, but you can override this to use your own handler.
[See the built in connection handling function here to further your understanding on how to handle connections.](https://github.com/PondWader/node-socks5-server/blob/main/src/connectionHandler.ts)
You can set your handling function:
```js
const { createServer } = require('@pondwader/socks5-server');
const server = createServer({
port: 5000
})
server.setConnectionHandler((conn, sendStatus) => {
const { socket, destAddress, destPort } = conn;
/*
You need to send a status before the client should start sending data in the socket.
If you send REQUEST_GRANTED the client should begin sending data, any other status will close the socket.
REQUEST_GRANTED,
GENERAL_FAILURE,
CONNECTION_NOT_ALLOWED,
NETWORK_UNREACHABLE,
HOST_UNREACHABLE,
CONNECTION_REFUSED,
TTL_EXPIRED,
COMMAND_NOT_SUPPORTED
*/
// Do stuff here
})
```
## Handling commands other than `connect`
The library only has a built in handler for connections using the `connect` command, this is used for TCP socket proxying and is by far the most common command however, you may wish to add support for other commands.
The other command types are `udp` and `bind`. To handle these you will need to make your own connection handler (see section above). **Note:** the `Socks5Connection` class exposes the `command` property which gives you access to the command sent by the client.
You will also need to add the commands you want to handle to the supported commands set. The `Socks5Server` class has the `supportedCommands` property which is a [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) instance.
For example:
`<Socks5Server>.supportedCommands.add('udp');`
## You can also pass Duplex streams as connections...
```js
const { Duplex } = require('streams');
server._handleConnection(new Duplex());
```
## Metadata
The `Socks5Connection` class has a metadata attribute which starts as an empty object, you can put data in this to pass data about a connection between seperate handlers.

View File

@@ -0,0 +1,87 @@
import * as net from 'net';
import net__default from 'net';
import { Duplex } from 'stream';
declare class Socks5Connection {
socket: Duplex;
server: Socks5Server;
username?: string;
password?: string;
destAddress?: string;
destPort?: number;
command?: keyof typeof Socks5ConnectionCommand;
private errorHandler;
metadata: any;
constructor(server: Socks5Server, socket: Duplex);
private readBytes;
private handleGreeting;
private handleUserPassword;
private handleConnectionRequest;
private connect;
}
declare enum Socks5ConnectionCommand {
connect = 1,
bind = 2,
udp = 3
}
declare enum Socks5ConnectionStatus {
REQUEST_GRANTED = 0,
GENERAL_FAILURE = 1,
CONNECTION_NOT_ALLOWED = 2,
NETWORK_UNREACHABLE = 3,
HOST_UNREACHABLE = 4,
CONNECTION_REFUSED = 5,
TTL_EXPIRED = 6,
COMMAND_NOT_SUPPORTED = 7,
ADDRESS_TYPE_NOT_SUPPORTED = 8
}
type AuthSocks5Connection = Socks5Connection & {
username: string;
password: string;
};
type InitialisedSocks5Connection = Socks5Connection & {
destAddress: string;
destPort: number;
command: keyof typeof Socks5ConnectionCommand;
};
declare class Socks5Server {
authHandler?: (connection: AuthSocks5Connection, accept: () => void, deny: () => void) => boolean | Promise<boolean> | any;
rulesetValidator?: (connection: InitialisedSocks5Connection, accept: () => void, deny: () => void) => boolean | Promise<boolean> | void;
connectionHandler: (connection: InitialisedSocks5Connection, sendStatus: (status: keyof typeof Socks5ConnectionStatus) => void) => void;
supportedCommands: Set<keyof typeof Socks5ConnectionCommand>;
private server;
constructor();
listen(port?: number, hostname?: string, backlog?: number, listeningListener?: () => void): this;
listen(port?: number, hostname?: string, listeningListener?: () => void): this;
listen(port?: number, backlog?: number, listeningListener?: () => void): this;
listen(port?: number, listeningListener?: () => void): this;
listen(path: string, backlog?: number, listeningListener?: () => void): this;
listen(path: string, listeningListener?: () => void): this;
listen(options: net.ListenOptions, listeningListener?: () => void): this;
listen(handle: any, backlog?: number, listeningListener?: () => void): this;
listen(handle: any, listeningListener?: () => void): this;
close(callback?: ((err?: Error | undefined) => void) | undefined): this;
setAuthHandler(handler: typeof this.authHandler): this;
disableAuthHandler(): this;
setRulesetValidator(handler: typeof this.rulesetValidator): this;
disableRulesetValidator(): this;
setConnectionHandler(handler: typeof this.connectionHandler): this;
useDefaultConnectionHandler(): this;
_handleConnection(socket: Duplex): this;
}
declare function export_default(connection: InitialisedSocks5Connection, sendStatus: (status: keyof typeof Socks5ConnectionStatus) => void): void | net__default.Socket;
type ServerOptions = {
auth?: {
username: string;
password: string;
};
port?: number;
hostname?: string;
};
declare function createServer(opts?: ServerOptions): Socks5Server;
export { Socks5Server, createServer, export_default as defaultConnectionHandler };

View File

@@ -0,0 +1,87 @@
import * as net from 'net';
import net__default from 'net';
import { Duplex } from 'stream';
declare class Socks5Connection {
socket: Duplex;
server: Socks5Server;
username?: string;
password?: string;
destAddress?: string;
destPort?: number;
command?: keyof typeof Socks5ConnectionCommand;
private errorHandler;
metadata: any;
constructor(server: Socks5Server, socket: Duplex);
private readBytes;
private handleGreeting;
private handleUserPassword;
private handleConnectionRequest;
private connect;
}
declare enum Socks5ConnectionCommand {
connect = 1,
bind = 2,
udp = 3
}
declare enum Socks5ConnectionStatus {
REQUEST_GRANTED = 0,
GENERAL_FAILURE = 1,
CONNECTION_NOT_ALLOWED = 2,
NETWORK_UNREACHABLE = 3,
HOST_UNREACHABLE = 4,
CONNECTION_REFUSED = 5,
TTL_EXPIRED = 6,
COMMAND_NOT_SUPPORTED = 7,
ADDRESS_TYPE_NOT_SUPPORTED = 8
}
type AuthSocks5Connection = Socks5Connection & {
username: string;
password: string;
};
type InitialisedSocks5Connection = Socks5Connection & {
destAddress: string;
destPort: number;
command: keyof typeof Socks5ConnectionCommand;
};
declare class Socks5Server {
authHandler?: (connection: AuthSocks5Connection, accept: () => void, deny: () => void) => boolean | Promise<boolean> | any;
rulesetValidator?: (connection: InitialisedSocks5Connection, accept: () => void, deny: () => void) => boolean | Promise<boolean> | void;
connectionHandler: (connection: InitialisedSocks5Connection, sendStatus: (status: keyof typeof Socks5ConnectionStatus) => void) => void;
supportedCommands: Set<keyof typeof Socks5ConnectionCommand>;
private server;
constructor();
listen(port?: number, hostname?: string, backlog?: number, listeningListener?: () => void): this;
listen(port?: number, hostname?: string, listeningListener?: () => void): this;
listen(port?: number, backlog?: number, listeningListener?: () => void): this;
listen(port?: number, listeningListener?: () => void): this;
listen(path: string, backlog?: number, listeningListener?: () => void): this;
listen(path: string, listeningListener?: () => void): this;
listen(options: net.ListenOptions, listeningListener?: () => void): this;
listen(handle: any, backlog?: number, listeningListener?: () => void): this;
listen(handle: any, listeningListener?: () => void): this;
close(callback?: ((err?: Error | undefined) => void) | undefined): this;
setAuthHandler(handler: typeof this.authHandler): this;
disableAuthHandler(): this;
setRulesetValidator(handler: typeof this.rulesetValidator): this;
disableRulesetValidator(): this;
setConnectionHandler(handler: typeof this.connectionHandler): this;
useDefaultConnectionHandler(): this;
_handleConnection(socket: Duplex): this;
}
declare function export_default(connection: InitialisedSocks5Connection, sendStatus: (status: keyof typeof Socks5ConnectionStatus) => void): void | net__default.Socket;
type ServerOptions = {
auth?: {
username: string;
password: string;
};
port?: number;
hostname?: string;
};
declare function createServer(opts?: ServerOptions): Socks5Server;
export { Socks5Server, createServer, export_default as defaultConnectionHandler };

View File

@@ -0,0 +1,346 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
Socks5Server: () => Socks5Server,
createServer: () => createServer,
defaultConnectionHandler: () => connectionHandler_default
});
module.exports = __toCommonJS(src_exports);
// src/Server.ts
var import_net2 = __toESM(require("net"));
// src/types.ts
var Socks5ConnectionCommand = /* @__PURE__ */ ((Socks5ConnectionCommand2) => {
Socks5ConnectionCommand2[Socks5ConnectionCommand2["connect"] = 1] = "connect";
Socks5ConnectionCommand2[Socks5ConnectionCommand2["bind"] = 2] = "bind";
Socks5ConnectionCommand2[Socks5ConnectionCommand2["udp"] = 3] = "udp";
return Socks5ConnectionCommand2;
})(Socks5ConnectionCommand || {});
var Socks5ConnectionStatus = /* @__PURE__ */ ((Socks5ConnectionStatus2) => {
Socks5ConnectionStatus2[Socks5ConnectionStatus2["REQUEST_GRANTED"] = 0] = "REQUEST_GRANTED";
Socks5ConnectionStatus2[Socks5ConnectionStatus2["GENERAL_FAILURE"] = 1] = "GENERAL_FAILURE";
Socks5ConnectionStatus2[Socks5ConnectionStatus2["CONNECTION_NOT_ALLOWED"] = 2] = "CONNECTION_NOT_ALLOWED";
Socks5ConnectionStatus2[Socks5ConnectionStatus2["NETWORK_UNREACHABLE"] = 3] = "NETWORK_UNREACHABLE";
Socks5ConnectionStatus2[Socks5ConnectionStatus2["HOST_UNREACHABLE"] = 4] = "HOST_UNREACHABLE";
Socks5ConnectionStatus2[Socks5ConnectionStatus2["CONNECTION_REFUSED"] = 5] = "CONNECTION_REFUSED";
Socks5ConnectionStatus2[Socks5ConnectionStatus2["TTL_EXPIRED"] = 6] = "TTL_EXPIRED";
Socks5ConnectionStatus2[Socks5ConnectionStatus2["COMMAND_NOT_SUPPORTED"] = 7] = "COMMAND_NOT_SUPPORTED";
Socks5ConnectionStatus2[Socks5ConnectionStatus2["ADDRESS_TYPE_NOT_SUPPORTED"] = 8] = "ADDRESS_TYPE_NOT_SUPPORTED";
return Socks5ConnectionStatus2;
})(Socks5ConnectionStatus || {});
// src/Connection.ts
var Socks5Connection = class {
constructor(server, socket) {
this.errorHandler = () => {
};
this.metadata = {};
this.socket = socket;
this.server = server;
socket.on("error", this.errorHandler);
socket.pause();
this.handleGreeting();
}
readBytes(len) {
return new Promise((resolve) => {
let buf = Buffer.allocUnsafe(len);
let offset = 0;
const dataListener = (chunk) => {
const readAmount = Math.min(chunk.length, len - offset);
chunk.copy(buf, offset, 0, readAmount);
offset += readAmount;
if (offset < len) return;
this.socket.removeListener("data", dataListener);
this.socket.push(chunk.subarray(readAmount));
resolve(buf);
this.socket.pause();
};
this.socket.on("data", dataListener);
this.socket.resume();
});
}
async handleGreeting() {
const ver = (await this.readBytes(1)).readUInt8();
if (ver !== 5) return this.socket.destroy();
const authMethodsAmount = (await this.readBytes(1)).readUInt8();
if (authMethodsAmount > 128 || authMethodsAmount === 0) return this.socket.destroy();
const authMethods = await this.readBytes(authMethodsAmount);
const authMethodByteCode = this.server.authHandler ? 2 : 0;
if (!authMethods.includes(authMethodByteCode)) {
this.socket.write(Buffer.from([
5,
// Version 5 - Socks5
255
// no acceptable auth modes were offered
]));
return this.socket.destroy();
}
this.socket.write(Buffer.from([
5,
// Version 5 - Socks5
authMethodByteCode
// The chosen auth method, 0x00 for no auth, 0x02 for user-pass
]));
if (this.server.authHandler) this.handleUserPassword();
else this.handleConnectionRequest();
}
async handleUserPassword() {
await this.readBytes(1);
const usernameLength = (await this.readBytes(1)).readUint8();
const username = (await this.readBytes(usernameLength)).toString();
const passwordLength = (await this.readBytes(1)).readUint8();
const password = (await this.readBytes(passwordLength)).toString();
this.username = username;
this.password = password;
let calledBack = false;
const acceptCallback = () => {
if (calledBack) return;
calledBack = true;
this.socket.write(Buffer.from([
1,
// User pass auth version
0
// Success
]));
this.handleConnectionRequest();
};
const denyCallback = () => {
if (calledBack) return;
calledBack = true;
this.socket.write(Buffer.from([
1,
// User pass auth version
1
// Failure
]));
this.socket.destroy();
};
const resp = await this.server.authHandler(this, acceptCallback, denyCallback);
if (resp === true) acceptCallback();
else if (resp === false) denyCallback();
}
async handleConnectionRequest() {
await this.readBytes(1);
const commandByte = (await this.readBytes(1))[0];
const command = Socks5ConnectionCommand[commandByte];
if (!command) return this.socket.destroy();
this.command = command;
await this.readBytes(1);
const addrType = (await this.readBytes(1)).readUInt8();
let address = "";
switch (addrType) {
case 1:
address = (await this.readBytes(4)).join(".");
break;
case 3:
const hostLength = (await this.readBytes(1)).readUInt8();
address = (await this.readBytes(hostLength)).toString();
break;
case 4:
const bytes = await this.readBytes(16);
for (let i = 0; i < 16; i++) {
if (i % 2 === 0 && i > 0) address += ":";
address += `${bytes[i] < 16 ? "0" : ""}${bytes[i].toString(16)}`;
}
break;
default:
this.socket.destroy();
return;
}
const port = (await this.readBytes(2)).readUInt16BE();
if (!this.server.supportedCommands.has(command)) {
this.socket.write(Buffer.from([5, 7 /* COMMAND_NOT_SUPPORTED */]));
return this.socket.destroy();
}
this.destAddress = address;
this.destPort = port;
let calledBack = false;
const acceptCallback = () => {
if (calledBack) return;
calledBack = true;
this.connect();
};
if (!this.server.rulesetValidator) return acceptCallback();
const denyCallback = () => {
if (calledBack) return;
calledBack = true;
this.socket.write(Buffer.from([
5,
2,
// connection not allowed by ruleset
0,
1,
0,
0,
0,
0,
0,
0
]));
this.socket.destroy();
};
const resp = await this.server.rulesetValidator(this, acceptCallback, denyCallback);
if (resp === true) acceptCallback();
else if (resp === false) denyCallback();
}
connect() {
this.socket.removeListener("error", this.errorHandler);
this.server.connectionHandler(this, (status) => {
if (Socks5ConnectionStatus[status] === void 0) throw new Error(`"${status}" is not a valid status.`);
this.socket.write(Buffer.from([
5,
Socks5ConnectionStatus[status],
0,
1,
0,
0,
0,
0,
0,
0
]));
if (status !== "REQUEST_GRANTED") {
this.socket.destroy();
}
});
this.socket.resume();
}
};
// src/connectionHandler.ts
var import_net = __toESM(require("net"));
function connectionHandler_default(connection, sendStatus) {
if (connection.command !== "connect") return sendStatus("COMMAND_NOT_SUPPORTED");
connection.socket.on("error", () => {
});
const stream = import_net.default.createConnection({
host: connection.destAddress,
port: connection.destPort
});
stream.setNoDelay();
let streamOpened = false;
stream.on("error", (err) => {
if (!streamOpened) {
switch (err.code) {
case "EINVAL":
case "ENOENT":
case "ENOTFOUND":
case "ETIMEDOUT":
case "EADDRNOTAVAIL":
case "EHOSTUNREACH":
sendStatus("HOST_UNREACHABLE");
break;
case "ENETUNREACH":
sendStatus("NETWORK_UNREACHABLE");
break;
case "ECONNREFUSED":
sendStatus("CONNECTION_REFUSED");
break;
default:
sendStatus("GENERAL_FAILURE");
}
}
});
stream.on("ready", () => {
streamOpened = true;
sendStatus("REQUEST_GRANTED");
connection.socket.pipe(stream).pipe(connection.socket);
});
connection.socket.on("close", () => stream.destroy());
return stream;
}
// src/Server.ts
var Socks5Server = class {
constructor() {
this.supportedCommands = /* @__PURE__ */ new Set(["connect"]);
this.connectionHandler = connectionHandler_default;
this.server = import_net2.default.createServer((socket) => {
socket.setNoDelay();
this._handleConnection(socket);
});
}
listen(...args) {
this.server.listen(...args);
return this;
}
close(callback) {
this.server.close(callback);
return this;
}
setAuthHandler(handler) {
this.authHandler = handler;
return this;
}
disableAuthHandler() {
this.authHandler = void 0;
return this;
}
setRulesetValidator(handler) {
this.rulesetValidator = handler;
return this;
}
disableRulesetValidator() {
this.rulesetValidator = void 0;
return this;
}
setConnectionHandler(handler) {
this.connectionHandler = handler;
return this;
}
useDefaultConnectionHandler() {
this.connectionHandler = connectionHandler_default;
return this;
}
// Not private because someone may want to inject a duplex stream to be handled as a connection
_handleConnection(socket) {
new Socks5Connection(this, socket);
return this;
}
};
// src/index.ts
function createServer(opts) {
const server = new Socks5Server();
if (opts?.auth) server.setAuthHandler((conn) => {
return conn.username === opts.auth.username && conn.password === opts.auth.password;
});
if (opts?.port) server.listen(opts.port, opts.hostname);
return server;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Socks5Server,
createServer,
defaultConnectionHandler
});

View File

@@ -0,0 +1,307 @@
// src/Server.ts
import net2 from "net";
// src/types.ts
var Socks5ConnectionCommand = /* @__PURE__ */ ((Socks5ConnectionCommand2) => {
Socks5ConnectionCommand2[Socks5ConnectionCommand2["connect"] = 1] = "connect";
Socks5ConnectionCommand2[Socks5ConnectionCommand2["bind"] = 2] = "bind";
Socks5ConnectionCommand2[Socks5ConnectionCommand2["udp"] = 3] = "udp";
return Socks5ConnectionCommand2;
})(Socks5ConnectionCommand || {});
var Socks5ConnectionStatus = /* @__PURE__ */ ((Socks5ConnectionStatus2) => {
Socks5ConnectionStatus2[Socks5ConnectionStatus2["REQUEST_GRANTED"] = 0] = "REQUEST_GRANTED";
Socks5ConnectionStatus2[Socks5ConnectionStatus2["GENERAL_FAILURE"] = 1] = "GENERAL_FAILURE";
Socks5ConnectionStatus2[Socks5ConnectionStatus2["CONNECTION_NOT_ALLOWED"] = 2] = "CONNECTION_NOT_ALLOWED";
Socks5ConnectionStatus2[Socks5ConnectionStatus2["NETWORK_UNREACHABLE"] = 3] = "NETWORK_UNREACHABLE";
Socks5ConnectionStatus2[Socks5ConnectionStatus2["HOST_UNREACHABLE"] = 4] = "HOST_UNREACHABLE";
Socks5ConnectionStatus2[Socks5ConnectionStatus2["CONNECTION_REFUSED"] = 5] = "CONNECTION_REFUSED";
Socks5ConnectionStatus2[Socks5ConnectionStatus2["TTL_EXPIRED"] = 6] = "TTL_EXPIRED";
Socks5ConnectionStatus2[Socks5ConnectionStatus2["COMMAND_NOT_SUPPORTED"] = 7] = "COMMAND_NOT_SUPPORTED";
Socks5ConnectionStatus2[Socks5ConnectionStatus2["ADDRESS_TYPE_NOT_SUPPORTED"] = 8] = "ADDRESS_TYPE_NOT_SUPPORTED";
return Socks5ConnectionStatus2;
})(Socks5ConnectionStatus || {});
// src/Connection.ts
var Socks5Connection = class {
constructor(server, socket) {
this.errorHandler = () => {
};
this.metadata = {};
this.socket = socket;
this.server = server;
socket.on("error", this.errorHandler);
socket.pause();
this.handleGreeting();
}
readBytes(len) {
return new Promise((resolve) => {
let buf = Buffer.allocUnsafe(len);
let offset = 0;
const dataListener = (chunk) => {
const readAmount = Math.min(chunk.length, len - offset);
chunk.copy(buf, offset, 0, readAmount);
offset += readAmount;
if (offset < len) return;
this.socket.removeListener("data", dataListener);
this.socket.push(chunk.subarray(readAmount));
resolve(buf);
this.socket.pause();
};
this.socket.on("data", dataListener);
this.socket.resume();
});
}
async handleGreeting() {
const ver = (await this.readBytes(1)).readUInt8();
if (ver !== 5) return this.socket.destroy();
const authMethodsAmount = (await this.readBytes(1)).readUInt8();
if (authMethodsAmount > 128 || authMethodsAmount === 0) return this.socket.destroy();
const authMethods = await this.readBytes(authMethodsAmount);
const authMethodByteCode = this.server.authHandler ? 2 : 0;
if (!authMethods.includes(authMethodByteCode)) {
this.socket.write(Buffer.from([
5,
// Version 5 - Socks5
255
// no acceptable auth modes were offered
]));
return this.socket.destroy();
}
this.socket.write(Buffer.from([
5,
// Version 5 - Socks5
authMethodByteCode
// The chosen auth method, 0x00 for no auth, 0x02 for user-pass
]));
if (this.server.authHandler) this.handleUserPassword();
else this.handleConnectionRequest();
}
async handleUserPassword() {
await this.readBytes(1);
const usernameLength = (await this.readBytes(1)).readUint8();
const username = (await this.readBytes(usernameLength)).toString();
const passwordLength = (await this.readBytes(1)).readUint8();
const password = (await this.readBytes(passwordLength)).toString();
this.username = username;
this.password = password;
let calledBack = false;
const acceptCallback = () => {
if (calledBack) return;
calledBack = true;
this.socket.write(Buffer.from([
1,
// User pass auth version
0
// Success
]));
this.handleConnectionRequest();
};
const denyCallback = () => {
if (calledBack) return;
calledBack = true;
this.socket.write(Buffer.from([
1,
// User pass auth version
1
// Failure
]));
this.socket.destroy();
};
const resp = await this.server.authHandler(this, acceptCallback, denyCallback);
if (resp === true) acceptCallback();
else if (resp === false) denyCallback();
}
async handleConnectionRequest() {
await this.readBytes(1);
const commandByte = (await this.readBytes(1))[0];
const command = Socks5ConnectionCommand[commandByte];
if (!command) return this.socket.destroy();
this.command = command;
await this.readBytes(1);
const addrType = (await this.readBytes(1)).readUInt8();
let address = "";
switch (addrType) {
case 1:
address = (await this.readBytes(4)).join(".");
break;
case 3:
const hostLength = (await this.readBytes(1)).readUInt8();
address = (await this.readBytes(hostLength)).toString();
break;
case 4:
const bytes = await this.readBytes(16);
for (let i = 0; i < 16; i++) {
if (i % 2 === 0 && i > 0) address += ":";
address += `${bytes[i] < 16 ? "0" : ""}${bytes[i].toString(16)}`;
}
break;
default:
this.socket.destroy();
return;
}
const port = (await this.readBytes(2)).readUInt16BE();
if (!this.server.supportedCommands.has(command)) {
this.socket.write(Buffer.from([5, 7 /* COMMAND_NOT_SUPPORTED */]));
return this.socket.destroy();
}
this.destAddress = address;
this.destPort = port;
let calledBack = false;
const acceptCallback = () => {
if (calledBack) return;
calledBack = true;
this.connect();
};
if (!this.server.rulesetValidator) return acceptCallback();
const denyCallback = () => {
if (calledBack) return;
calledBack = true;
this.socket.write(Buffer.from([
5,
2,
// connection not allowed by ruleset
0,
1,
0,
0,
0,
0,
0,
0
]));
this.socket.destroy();
};
const resp = await this.server.rulesetValidator(this, acceptCallback, denyCallback);
if (resp === true) acceptCallback();
else if (resp === false) denyCallback();
}
connect() {
this.socket.removeListener("error", this.errorHandler);
this.server.connectionHandler(this, (status) => {
if (Socks5ConnectionStatus[status] === void 0) throw new Error(`"${status}" is not a valid status.`);
this.socket.write(Buffer.from([
5,
Socks5ConnectionStatus[status],
0,
1,
0,
0,
0,
0,
0,
0
]));
if (status !== "REQUEST_GRANTED") {
this.socket.destroy();
}
});
this.socket.resume();
}
};
// src/connectionHandler.ts
import net from "net";
function connectionHandler_default(connection, sendStatus) {
if (connection.command !== "connect") return sendStatus("COMMAND_NOT_SUPPORTED");
connection.socket.on("error", () => {
});
const stream = net.createConnection({
host: connection.destAddress,
port: connection.destPort
});
stream.setNoDelay();
let streamOpened = false;
stream.on("error", (err) => {
if (!streamOpened) {
switch (err.code) {
case "EINVAL":
case "ENOENT":
case "ENOTFOUND":
case "ETIMEDOUT":
case "EADDRNOTAVAIL":
case "EHOSTUNREACH":
sendStatus("HOST_UNREACHABLE");
break;
case "ENETUNREACH":
sendStatus("NETWORK_UNREACHABLE");
break;
case "ECONNREFUSED":
sendStatus("CONNECTION_REFUSED");
break;
default:
sendStatus("GENERAL_FAILURE");
}
}
});
stream.on("ready", () => {
streamOpened = true;
sendStatus("REQUEST_GRANTED");
connection.socket.pipe(stream).pipe(connection.socket);
});
connection.socket.on("close", () => stream.destroy());
return stream;
}
// src/Server.ts
var Socks5Server = class {
constructor() {
this.supportedCommands = /* @__PURE__ */ new Set(["connect"]);
this.connectionHandler = connectionHandler_default;
this.server = net2.createServer((socket) => {
socket.setNoDelay();
this._handleConnection(socket);
});
}
listen(...args) {
this.server.listen(...args);
return this;
}
close(callback) {
this.server.close(callback);
return this;
}
setAuthHandler(handler) {
this.authHandler = handler;
return this;
}
disableAuthHandler() {
this.authHandler = void 0;
return this;
}
setRulesetValidator(handler) {
this.rulesetValidator = handler;
return this;
}
disableRulesetValidator() {
this.rulesetValidator = void 0;
return this;
}
setConnectionHandler(handler) {
this.connectionHandler = handler;
return this;
}
useDefaultConnectionHandler() {
this.connectionHandler = connectionHandler_default;
return this;
}
// Not private because someone may want to inject a duplex stream to be handled as a connection
_handleConnection(socket) {
new Socks5Connection(this, socket);
return this;
}
};
// src/index.ts
function createServer(opts) {
const server = new Socks5Server();
if (opts?.auth) server.setAuthHandler((conn) => {
return conn.username === opts.auth.username && conn.password === opts.auth.password;
});
if (opts?.port) server.listen(opts.port, opts.hostname);
return server;
}
export {
Socks5Server,
createServer,
connectionHandler_default as defaultConnectionHandler
};

View File

@@ -0,0 +1,31 @@
{
"name": "@pondwader/socks5-server",
"version": "1.0.10",
"description": "A Node.js socks5 server implementation enabling fine-grained connection control.",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"repository": "https://github.com/PondWader/node-socks5-server",
"bugs": {
"url": "https://github.com/PondWader/node-socks5-server/issues"
},
"homepage": "https://github.com/PondWader/node-socks5-server#readme",
"author": "PondWader",
"license": "MIT",
"devDependencies": {
"@types/node": "^18.19.39",
"jest": "^29.7.0",
"tsup": "^8.1.0",
"typescript": "^5.5.2"
},
"keywords": [
"socks5",
"socks5-server",
"typescript"
],
"scripts": {
"release": "pnpm run build && pnpm publish",
"build": "tsup ./src/index.ts --format cjs,esm --dts",
"test": "pnpm run build && jest"
}
}