Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@mutagen-d/node-proxy-server

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mutagen-d/node-proxy-server - npm Package Compare versions

Comparing version
0.2.0
to
1.0.0
+22
example/net-proxy/index.js
const { createProxyServer } = require('../../src');
const { connectionLogger } = require('../tool/connection.logger');
const port = 8080;
const time = () => new Date().toISOString()
const server = createProxyServer({ auth: true });
server.on('proxy-auth', (username, password, callback) => {
callback(true)
})
server.on('error', (error) => {
console.log(time(), 'server error', error)
})
server.on('connection', (socket) => {
socket.setTimeout(15 * 1000, () => socket.destroy())
})
connectionLogger(server)
server.listen(port, '0.0.0.0', () => console.log(time(), 'proxy-server listening port', port))
{
"name": "example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"net-proxy": "node ./net-proxy",
"ssh-proxy": "node ./shh-proxy",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"ssh2": "^1.11.0"
}
}
const fs = require('fs')
const util = require('util')
const path = require('path')
const os = require('os')
const { Client } = require('ssh2')
const { createProxyServer } = require('../../src')
const { connectionLogger } = require('../tool/connection.logger')
const time = () => new Date().toISOString()
const sshClient = new Client()
const forwardOut = util.promisify(sshClient.forwardOut)
const server = createProxyServer({
auth: true,
createProxyConnection: async (info) => {
const stream = await forwardOut.call(sshClient, info.srcHost, info.srcPort, info.dstHost, info.dstPort)
return stream;
},
})
server.on('proxy-auth', (username, password, callback) => {
callback(true)
})
server.on('error', (e) => {
console.log(time(), 'server error', e)
})
connectionLogger(server)
sshClient.connect({
host: 'localhost',
username: 'username',
privateKey: fs.readFileSync(path.join(os.homedir(), '.ssh/id_rsa')),
keepaliveInterval: 0,
})
sshClient.on('ready', () => {
const port = 8080;
console.log(time(), 'ssh-client ready')
server.listen(port, '0.0.0.0', () => console.log(time(), 'proxy-server listening port', port))
})
module.exports = { proxyServer: server, sshClient }
const time = () => new Date().toISOString()
/** @param {import('../../src').ProxyServer} server */
const connectionLogger = (server) => {
let count = 0;
server.on('connection', (socket) => {
count += 1;
const { remoteAddress: srcHost, remotePort: srcPort } = socket;
console.log(time(), count, 'connect', { srcHost, srcPort })
socket.on('close', () => {
count -= 1;
console.log(time(), count, 'disconnect', { srcHost, srcPort })
})
})
let pcount = 0;
server.on('proxy-connection', (stream, info) => {
const { dstHost, dstPort } = info;
pcount += 1;
console.log(time(), pcount, 'connect-proxy', { dstHost, dstPort })
stream.on('close', () => {
pcount -= 1;
console.log(time(), pcount, 'disconnect-proxy', { dstHost, dstPort })
})
})
return server;
}
module.exports = { connectionLogger }
const net = require('net')
const tls = require('tls')
const http = require('http')
const {
parseHTTP,
serializeHTTP,
Deffer,
isSocks4HandshakeData,
isSocks5HandshakeData,
socks4ResponseData,
socks5ResponseData,
toHex,
oneTime,
} = require('./tool');
const { ReadBuffer } = require('./read-buffer');
/**
* @typedef {import('./tool').HttpRequestOptions} HttpRequestOptions
* @typedef {(info: ConnectionInfo, options?: HttpRequestOptions) => Promise<import('stream').Duplex>} CreateProxyConnection
* @typedef {(userid: string, password: string, callback: (isAuth: boolean) => void, socket: net.Socket) => any} OnAuth
* @typedef {{
* on(event: 'http-proxy', listener: (socket: net.Socket, data: Buffer, options: HttpRequestOptions) => any): ProxyServer;
* on(event: 'http-proxy-connection', listener: (socket: net.Socket, data: Buffer, options: HttpRequestOptions) => any): ProxyServer;
* on(event: 'socks4-proxy', listener: (socket: net.Socket, data: Buffer) => any): ProxyServer;
* on(event: 'socks5-proxy', listener: (socket: net.Socket, data: Buffer) => any): ProxyServer;
* on(event: 'socks5-proxy-connection', listener: (socket: net.Socket, data: Buffer) => any): ProxyServer;
* on(event: 'proxy-auth', listener: OnAuth): ProxyServer;
* on(event: 'proxy-connection', listener: (connection: import('stream').Duplex, info: ConnectionInfo)): ProxyServer;
* } & net.Server} ProxyServer
* @typedef {{ dstHost: string, dstPort: number; srcHost: string; srcPort: number }} ConnectionInfo
* @typedef {{
* createProxyConnection?: CreateProxyConnection;
* auth?: boolean;
* }} ProxyServerOptions
*/
/**
* @param {ProxyServerOptions} [options]
*/
function createProxyServer(options) {
const auth = { enabled: options && options.auth }
const createProxyConnection = options && options.createProxyConnection || createTCPConnection
/** @type {ProxyServer} */
const server = net.createServer();
server.on('connection', (socket) => {
socket._server = server;
socket.on('error', onSocketError)
socket.once('data', onConnectionHandshake)
})
server.on('http-proxy', (socket, data, options) => {
if (!auth.enabled) {
server.emit('http-proxy-connection', socket, data, options)
return;
}
const proxyAuthHead = options.headers['proxy-authorization'];
const [type, token] = (proxyAuthHead || '').split(/\s+/g);
if (!proxyAuthHead || !type || type.toLowerCase() !== 'basic' || !token) {
socket.end([
'HTTP/1.1 407 Proxy Authentication Required',
'Proxy-Authenticate: Basic realm="Proxy Authentication Required"',
'\r\n'
].join('\r\n'), 'utf-8')
return;
}
const [username, password] = Buffer.from(token, 'base64').toString('utf-8').split(':');
if (!server.listenerCount('proxy-auth')) {
socket.end('HTTP/1.1 500 Internal Server Error\r\n\r\n', 'utf-8');
server.emit('error', new Error('require "proxy-auth" event listener'))
return;
}
socket.pause()
server.emit('proxy-auth', username, password, oneTime((isAuth) => {
socket.resume()
if (isAuth) {
server.emit('http-proxy-connection', socket, data, options)
} else {
socket.end('HTTP/1.1 401 Unathorized\r\n\r\n', 'utf-8')
}
}), socket)
})
server.on('http-proxy-connection', async (socket, data, options) => {
try {
const { remoteAddress: srcHost, remotePort: srcPort } = socket;
const { method, url } = options;
/** @type {import('stream').Duplex} */
let conn;
socket.pause()
if (method.toLowerCase() === 'connect') {
const [dstHost, dstPort] = url.split(':')
conn = await createProxyConnection({ srcHost, srcPort, dstHost, dstPort: +dstPort }, options)
server.emit('proxy-connection', conn, { dstHost, dstPort: +dstPort, srcHost, srcPort })
socket.write('HTTP/1.1 200 OK\r\n\r\n', 'utf-8')
} else {
const { host: dstHost, port: dstPort } = new URL(url);
conn = await createProxyConnection({ srcHost, srcPort, dstHost, dstPort: dstPort || 80 }, options)
server.emit('proxy-connection', conn, { dstHost, dstPort, srcHost, srcPort })
if (options.headers['proxy-authorization']) {
delete options.headers['proxy-authorization']
socket.write(serializeHTTP(options))
} else {
socket.write(data)
}
}
socket.resume();
conn.pipe(socket)
socket.pipe(conn)
socket.on('close', () => conn.destroy())
conn.on('error', onProxyError)
} catch (e) {
server.emit('error', e)
socket.resume()
socket.end(`HTTP/1.1 500 Internal Server Error\r\n\r\n`, 'utf-8')
}
})
server.on('socks4-proxy', async (socket, data) => {
try {
const { remoteAddress: srcHost, remotePort: srcPort } = socket;
const buf = new ReadBuffer(data)
buf.seek(1) // version byte
const command = buf.readUInt8();
const dstPort = buf.readUInt16BE()
const ip = buf.readArrayBuffer(4).join('.');
const userid = buf.readStringNT('ascii');
if (!userid) {
socket.end(socks4ResponseData(0x5D)) // NOUSERID
return;
}
let dstHost = ip
if (/^0\.0\.0\./.test(ip)) {
dstHost = buf.readStringNT('ascii');
}
if (!dstHost) {
socket.end(socks4ResponseData(0x5B)) // REJECTED
return;
}
/** @type {import('stream').Duplex} */
let conn;
socket.pause()
if (command === 0x01) {
conn = await createProxyConnection({ dstHost, dstPort, srcHost, srcPort })
server.emit('proxy-connection', conn, { dstHost, dstPort, srcHost, srcPort })
} else {
socket.resume()
socket.end(socks4ResponseData(0x5B)) // REJECTED
return;
}
socket.resume()
socket.write(socks4ResponseData(0x5A)) // SUCCESS
conn.pipe(socket).pipe(conn)
socket.on('close', () => conn.destroy())
conn.on('error', onProxyError)
} catch (e) {
server.emit('error', e)
socket.resume()
socket.end(socks4ResponseData(0x5C))
}
})
server.on('socks5-proxy', (socket, data) => {
const buf = new ReadBuffer(data)
buf.seek(1) // version byte
const size = buf.readUInt8()
const authTypes = buf.readArrayBuffer(size)
if (auth.enabled) {
if (authTypes.includes(0x02)) {
socket.write(socks5ResponseData(0x02)) // PWD AUTH
socket.once('data', onSocks5PasswordAuth)
return;
}
if (authTypes.includes(0x00)) {
socket.pause()
server.emit('proxy-auth', '', '', oneTime((isAuth) => {
socket.resume()
if (isAuth) {
socket.write(socks5ResponseData(0x00)) // NO AUTH
socket.once('data', onSocks5Connection)
} else {
socket.end(socks5ResponseData(0x01))
}
}), socket)
return;
}
} else if (authTypes.includes(0x00)) {
socket.resume()
socket.write(socks5ResponseData(0x00)) // NO AUTH
socket.once('data', onSocks5Connection)
return;
}
socket.end(socks5ResponseData(0xFF)) // NOT SUPPORTED AUTHENTICATION
})
server.on('socks5-proxy-connection', async (socket, data) => {
const { remoteAddress: srcHost, remotePort: srcPort } = socket;
const buf = new ReadBuffer(data)
buf.seek(1) // version byte
const command = buf.readUInt8();
buf.seek(1)
const addressType = buf.readUInt8();
let dstPort, dstHost, address, domSize;
switch (addressType) {
case 0x01:
address = buf.readArrayBuffer(buf.length, buf.position);
dstHost = buf.readArrayBuffer(4).join('.'); // IPv4
dstPort = buf.readUInt16BE()
break;
case 0x03:
address = buf.readArrayBuffer(buf.length, buf.position);
domSize = buf.readUInt8()
dstHost = buf.readArrayBuffer(domSize).toString('ascii'); // Name
dstPort = buf.readUInt16BE()
break;
case 0x04:
address = buf.readArrayBuffer(buf.length, buf.position)
dstHost = buf.readArrayBuffer(16).map(toHex).join(':') // IPv6
dstPort = buf.readUInt16BE()
break;
default:
socket.end(socks5ResponseData(0x08, 0)) // ADDRESS TYPE UNSUPPORTED
return;
}
try {
/** @type {net.Socket} */
let conn;
if (command === 0x01) {
socket.pause()
conn = await createProxyConnection({ dstHost, dstPort, srcHost, srcPort })
server.emit('proxy-connection', conn, { dstHost, dstPort, srcHost, srcPort })
socket.resume()
socket.write(socks5ResponseData(0x00, 0, addressType, ...address)) // SUCCESS
} else {
socket.end(socks5ResponseData(0x07, 0)) // COMMAND UNSUPPORTED
return;
}
conn.on('error', onProxyError)
conn.pipe(socket)
socket.pipe(conn)
socket.on('close', () => conn.destroy())
} catch (e) {
server.emit('error', e)
socket.end(socks5ResponseData(0x01, 0)) // FAILED
}
})
return server;
}
/**
* @this {net.Socket}
* @param {Buffer} data
*/
function onConnectionHandshake(data) {
const socket = this;
/** @type {ProxyServer} */
const server = socket._server;
if (isSocks4HandshakeData(data)) {
server.emit('socks4-proxy', socket, data)
return;
}
if (isSocks5HandshakeData(data)) {
server.emit('socks5-proxy', socket, data)
return;
}
const options = parseHTTP(data)
if (http.METHODS.includes(options.method)) {
server.emit('http-proxy', socket, data, options)
return;
}
socket.end('HTTP/1.1 405 Method Not Allowed\r\n\r\n', 'utf-8')
}
/** @type {CreateProxyConnection} */
function createTCPConnection(info, options) {
const socket = net.createConnection({ host: info.dstHost, port: info.dstPort })
const conn = options && options.url.indexOf('https:') === 0 ? new tls.TLSSocket(socket, { rejectUnauthorized: false }) : socket;
/** @type {Deffer<net.Socket>} */
const deffer = new Deffer()
conn.on('connect', () => deffer.resolve(conn))
conn.on('error', (err) => deffer.reject(err))
return deffer.promise;
}
/**
* @this {net.Socket}
* @param {Buffer} data
*/
function onSocks5PasswordAuth(data) {
const socket = this;
const buf = new ReadBuffer(data)
buf.seek(1) // version byte
const useridSize = buf.readUInt8()
const userid = buf.readArrayBuffer(useridSize).toString('ascii')
const passwordSize = buf.readUInt8()
const password = buf.readArrayBuffer(passwordSize).toString('ascii');
/** @type {ProxyServer} */
const server = socket._server;
socket.pause()
server.emit('proxy-auth', userid, password, oneTime((isAuth) => {
socket.resume()
if (isAuth) {
socket.write(socks5ResponseData(0x00)) // SUCCESS
socket.once('data', onSocks5Connection)
} else {
socket.end(socks5ResponseData(0x01)) // FAILED
}
}), socket)
}
/**
* @this {net.Socket}
* @param {Buffer} data
*/
function onSocks5Connection(data) {
/** @type {ProxyServer} */
const server = this._server;
server.emit('socks5-proxy-connection', this, data)
}
/**
* @this {net.Socket}
* @param {Error} error
*/
function onSocketError(error) { }
/**
* @this {import('stream').Duplex}
* @param {Error} error
*/
function onProxyError(error) { }
module.exports = { createProxyServer }
module.exports = { ReadBuffer }
/**
* @param {Buffer} buffer
* @param {number} [offset]
*/
function ReadBuffer(buffer, offset) {
/** @protected */
this._buf = typeof offset === 'number'
? buffer.subarray(offset, buffer.length)
: buffer;
/** @protected */
this._readOffset = 0;
}
Object.defineProperty(ReadBuffer.prototype, 'buffer', {
get: function () {
return this._buf;
},
enumerable: true,
configurable: true,
})
Object.defineProperty(ReadBuffer.prototype, 'position', {
get: function () {
return this._readOffset;
},
enumerable: true,
configurable: true,
})
Object.defineProperty(ReadBuffer.prototype, 'length', {
get: function () {
return this._buf.length;
},
enumerable: true,
configurable: true,
})
/** @param {number} byteOffset */
ReadBuffer.prototype.seek = function seek(byteOffset) {
this._readOffset += byteOffset;
return this._readOffset;
}
/** @param {number} [offset] */
ReadBuffer.prototype.readUInt16BE = function readUInt16BE(offset) {
return this._readNumberValue(this._buf.readUInt16BE, 2, offset)
}
/** @type {ReadBuffer['readUInt16BE']} */
ReadBuffer.prototype.readUint16BE = ReadBuffer.prototype.readUInt16BE;
/** @param {number} [offset] */
ReadBuffer.prototype.readUInt8 = function readUInt8(offset) {
return this._readNumberValue(this._buf.readUInt8, 1, offset)
}
/**
* @protected
* @template T
* @param {Extract<T, (offset: number) => any>} fn
* @param {number} byteSize
* @param {number} [offset]
* @return {ReturnType<T>}
*/
ReadBuffer.prototype._readNumberValue = function _readNumberValue(fn, byteSize, offset) {
if (typeof offset === 'number') {
return fn.call(this._buf, offset)
}
const value = fn.call(this._buf, this._readOffset)
this._readOffset += byteSize;
return value;
}
/**
* @param {BufferEncoding} encoding
*/
ReadBuffer.prototype.readStringNT = function readStringNT(encoding) {
let nullPos = this._buf.length;
for (let i = this._readOffset; i < this._buf.length; ++i) {
if (this._buf[i] === 0x00) {
nullPos = i;
break;
}
}
const value = this._buf.subarray(this._readOffset, this._readOffset + nullPos)
this._readOffset = nullPos + 1;
return value.toString(encoding)
}
/**
* @param {number} byteSize
* @param {number} [offset]
*/
ReadBuffer.prototype.readArrayBuffer = function readArrayBuffer(byteSize, offset) {
if (typeof offset === 'number') {
return this._buf.subarray(offset, offset + byteSize)
}
const value = this._buf.subarray(this._readOffset, this._readOffset + byteSize)
this._readOffset += byteSize;
return value;
}
/**
* @typedef {{
* method: string;
* url: string;
* version: string;
* headers: Record<string, string>;
* body: Buffer;
* }} HttpRequestOptions
*/
const LF = 0x0A
const CR = 0x0D
/**
* @param {Buffer} data
*/
function parseHTTP(data) {
let rawHeaders = ''
/** @type {Buffer} */
let body
for (let i = 0; i < data.length; ++i) {
if (data[i] === LF && data[i + 1] === LF) {
rawHeaders = data.subarray(0, i).toString('utf-8')
body = data.subarray(i + 2)
}
if (data[i] === CR && data[i + 1] === LF && data[i + 2] === CR && data[i + 3] === LF) {
rawHeaders = data.subarray(0, i).toString('utf-8')
body = data.subarray(i + 4)
}
}
const lines = rawHeaders.split(/\r?\n/g).filter(Boolean);
/** @type {Record<string, string>} */
const headers = lines.slice(1).reduce((acc, line) => {
const index = line.indexOf(':')
const key = line.slice(0, index)
const value = line.slice(index + 1)
acc[key.toLowerCase()] = value.trim();
return acc;
}, {})
const [method, url, version] = lines[0].split(/\s/g);
return {
method,
url,
version,
headers,
body,
}
}
/** @param {HttpRequestOptions} request */
function serializeHTTP(request) {
const { method, url, version, headers, body } = request;
const rawHeaders = [
`${method} ${url} ${version}`,
...Object.keys(headers).map(key => `${key}: ${headers[key]}`),
'\r\n'
].join('\r\n');
const bufferHead = Buffer.from(rawHeaders, 'utf-8')
return body ? Buffer.concat([bufferHead, body]) : bufferHead
}
/** @template T */
function Deffer() {
/** @type {Promise<T>} */
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
})
}
/** @param {Buffer} data */
const isSocks4HandshakeData = (data) => {
return data[0] === 0x04 && (data[1] === 0x01 || data[1] === 0x02 || data[1] === 0x03)
}
/** @param {Buffer} data */
const isSocks5HandshakeData = (data) => {
const size = data[1]
const auth = data.subarray(2, 2 + size)
return data[0] === 0x05 && (auth.includes(0x00) || auth.includes(0x02))
}
/** @param {number} code */
const socks4ResponseData = (code) => {
return Buffer.from([0, code, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01]);
}
/** @param {number[]} code */
const socks5ResponseData = (...code) => {
return Buffer.from([0x05, ...code])
}
const toHex = (v) => {
if (typeof v === 'number') {
return v.toString(16)
}
return v;
}
/**
* @template T
* @param {Extract<T, (...args: any[]) => any>} callback
*/
const oneTime = (callback) => {
let count = 0;
/** @type {T} */
const fn = (...args) => {
if (count++) return;
return callback(...args)
}
return fn;
}
module.exports = {
Deffer,
parseHTTP,
serializeHTTP,
isSocks4HandshakeData,
isSocks5HandshakeData,
socks4ResponseData,
socks5ResponseData,
toHex,
oneTime,
}
const { describe, expect, beforeAll, it } = require('@jest/globals')
const { serializeHTTP, parseHTTP } = require('./tool')
describe('tool', () => {
/** @type {Buffer} */
let DATA
/** @type {Buffer} */
let BODY
/** @type {string} */
let Json
/** @type {import('./tool').HttpRequestOptions} */
let OPTIONS
beforeAll(() => {
Json = '{ "hello": "world!" }'
BODY = Buffer.from(Json, 'utf-8')
OPTIONS = {
method: 'POST',
url: '/hello.json',
version: 'HTTP/1.0',
headers: {
'keep-alive': 'timeout=5, max=1000',
'content-type': 'application/json',
'content-length': `${BODY.length}`,
},
body: BODY,
}
DATA = serializeHTTP(OPTIONS)
})
it('parseHTTP', () => {
const res = parseHTTP(DATA)
expect(res.method).toEqual(OPTIONS.method)
expect(res.url).toEqual(OPTIONS.url)
expect(res.version).toEqual(OPTIONS.version)
Object.keys(OPTIONS.headers).forEach((key) => {
expect(res.headers).toHaveProperty(key.toLowerCase())
expect(res.headers[key.toLowerCase()]).toEqual(OPTIONS.headers[key])
})
expect(Buffer.compare(res.body, BODY)).toEqual(0)
})
describe('serializeHTTP', () => {
it('with body', () => {
const options = { ...OPTIONS }
const data = serializeHTTP(options)
const res = parseHTTP(data)
expect(res.method).toEqual(options.method)
expect(res.url).toEqual(options.url)
expect(res.version).toEqual(options.version)
Object.keys(options.headers).forEach((key) => {
expect(res.headers).toHaveProperty(key.toLowerCase())
expect(res.headers[key.toLowerCase()]).toEqual(options.headers[key])
})
expect(Buffer.compare(res.body, options.body)).toEqual(0)
})
it('empty body', () => {
const options = { ...OPTIONS, body: undefined }
const data = serializeHTTP(options)
const res = parseHTTP(data)
expect(res.method).toEqual(options.method)
expect(res.url).toEqual(options.url)
expect(res.version).toEqual(options.version)
Object.keys(options.headers).forEach((key) => {
expect(res.headers).toHaveProperty(key.toLowerCase())
expect(res.headers[key.toLowerCase()]).toEqual(options.headers[key])
})
expect(Buffer.compare(res.body, Buffer.from([]))).toEqual(0)
})
})
})
+11
-12
{
"name": "@mutagen-d/node-proxy-server",
"version": "0.2.0",
"version": "1.0.0",
"description": "Http and socks proxy server",
"main": "index.js",
"main": "src/index.js",
"scripts": {

@@ -14,18 +14,17 @@ "test": "jest"

"keywords": [
"proxy",
"server",
"http",
"https",
"socks",
"proxy",
"server"
"socks4",
"socks4a",
"socks5",
"socks5h"
],
"author": "mutagen-d",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"@jest/globals": "^28.1.3",
"axios": "^0.27.2",
"jest": "^28.1.3",
"proxy-agent": "^5.0.0",
"socks-proxy-agent": "^7.0.0"
"@jest/globals": "^29.1.2",
"jest": "^29.1.2"
}
}
}
+64
-86

@@ -1,114 +0,92 @@

# node-proxy-server
# Proxy Server
## Http and socks proxy server
Http and Socks proxy server with zero dependencies
This module provides `http` and `socks` proxy server implementation that can run both protocols in one server instance.
## Content
## Installation
- [Usage](#usage)
- [Authorization](#authorization)
- [Keep Alive](#keepalive)
- [Custom proxy connection](#custom-proxy-connection)
- [Examples](#examples)
```bash
npm install @mutagen-d/node-proxy-server
## Usage
```js
const { createProxyServer } = require('./src')
const port = 8080
const server = createProxyServer()
server.on('error', (error) => {
console.log('server error', error)
})
server.listen(port, '0.0.0.0', () => console.log('proxy-server listening port', port))
```
## Example
## Authorization
```js
const net = require('net')
const { createHttpProxy, createSocksProxy } = require('@mutagen-d/node-proxy-server')
const server = net.createServer()
const options = {
keepAlive: true,
keepAliveMsecs: 5000,
}
createHttpProxy(server, options)
createSocksProxy(server, options)
server.listen(8080)
const server = createProxyServer({ auth: true })
server.on('proxy-auth', (username, password, callback) => {
callback(username === 'login' && password === '1234')
})
```
## API
Only first `"proxy-auth"` event listener will be envoked
### `createHttpProxy([serverOrOptions [, options]])`
## KeepAlive
- `serverOrOptions` - `net.Server` or `HttpProxyServerOptions`
- `options` - `HttpProxyServerOptions`
`HttpProxyServerOptions`:
| name | type | required | default | description |
| ---------------- | --------------------- | -------- | ------- | ---------------------------------------------------------------------------------------- |
| `keepAlive` | `boolean` | `no` | `true` | controls keep-alive behavior |
| `keepAliveMsecs` | `number` | `no` | `1000` | inactivity timeout before close connection |
| `authType` | `"Basic" \| "Bearer"` | `no` | - | if defined then proxy authentication required |
| `authRealm` | `string` | `no` | - | |
| `onAuth` | `OnAuth` | `no` | - | |
| `useHttpRequest` | `boolean` | `no` | - | if `ture` then use `http.request` for HTTP requests, otherwise directly use `net.Socket` |
| `rewriteOptions` | `RewriteOptions` | `no` | - | if defined then rewrite connection options for each subsequence connections |
1. Authorization:
```js
const server = createHttpProxy({
authType: 'Basic',
onAuth: (auth, callback) => {
switch (auth.type) {
case 'Basic':
// authorize connection
callback(auth.username === 'test' && auth.password === '1234')
break
default:
// reject connection
callback(false)
break
}
},
server.on('connection', (socket) => {
socket.setTimeout(30 * 1000, () => socket.destroy())
})
```
2. Rewrite options:
## Custom proxy connection
Use `createProxyConnection` method.
By default `net` module is used to create connection, e.i.
```js
const server = createHttpProxy({
rewriteOptions: (req, callback) => {
if (req.url === 'localhost:443') {
callback({ rejectUnauthorized: false })
}
},
const net = require('net')
const { createProxyServer } = require('./src')
const server = createProxyServer({
createProxyConnection: async (info) => {
const socket = net.createConnection({ host: info.dstHost, port: info.dstPort })
return new Promise((resolve, reject) => {
socket.on('connect', () => resolve(socket))
socket.on('error', (error) => reject(error))
})
}
})
```
One can also use other methods to create connection, e.i [ssh2-dynamic-port-forwarding](https://github.com/mscdex/ssh2#dynamic-11-port-forwarding-using-a-socksv5-proxy-using-socksv5)
### `createSocksProxy([serverOrOptions [, options]])`
```js
const { createProxyServer } = require('./src')
const { Client } = require('ssh2')
const util = require('util')
- `serverOrOptions` - `net.Server` or `SocksProxyServerOptions`
- `options` - `SocksProxyServerOptions`
const sshClient = new Client()
const forwardOut = util.promisify(sshClient.forwardOut)
`SocksProxyServerOptions`:
| name | type | required | default | description |
| ---------------- | ---------------- | -------- | ------- | --------------------------------------------------------------------------- |
| `keepAlive` | `boolean` | `no` | `true` | controls keep-alive behavior |
| `keepAliveMsecs` | `number` | `no` | `1000` | inactivity timeout before close connection |
| `onAuth` | `OnAuth` | `no` | - | if defined then prefer password authentication |
| `rewriteOptions` | `RewriteOptions` | `no` | - | if defined then rewrite connection options for each subsequence connections |
1. Authorization
```js
const server = createSocksProxy({
onAuth: (username, password, callback) => {
callback(username === 'test' && password === '1234')
const server = createProxyServer({
createProxyConnection: async (info) => {
const stream = await forwardOut.call(sshClient, info.srcHost, info.srcPort, info.dstHost, info.dstPort)
return stream
},
})
sshClient.connect({
host: 'localhost',
username: 'username',
password: '12345',
})
sshClient.on('ready', () => {
const port = 8080
server.listen(port, '0.0.0.0', () => console.log('proxy-server listening port', port))
})
```
2. Rewrite options:
## Examples
```js
const server = createSocksProxy({
rewriteOptions: (_, callback) => {
callback({ rejectUnauthorized: false })
},
})
```
see [examples](./example)
const { createHttpProxy } = require('./src/http-proxy')
const { createSocksProxy } = require('./src/socks-proxy')
module.exports = { createHttpProxy, createSocksProxy }
const crypto = require('crypto')
const net = require('net');
const tls = require('tls');
const http = require('http');
const https = require('https');
const {
_parseRequest,
_getRawHeaders,
_setKeepAlive,
_onError,
_onClose,
_status407,
_setTimeout,
} = require('./tools')
module.exports = { createHttpProxy }
/**
* @template T
* @typedef {import('./tools').IfAny<T>} IfAny
*/
/**
* @typedef {{
* type: 'Basic' | 'Bearer';
* token?: string;
* username?: string;
* password?: string;
* }} AuthParams
*/
/**
* @typedef {import('./tools').IRequest} IRequest
*/
/**
* @typedef {(auth: AuthParams, callback: (authorized: boolean) => void) => void} OnAuth
* @typedef {(req: Pick<IRequest, 'method' | 'url' | 'headers'>, callback: (opts?: SSLProxyOptions & HttpProxyOptions) => void) => void} RewriteOptions
* @typedef {{
* onAuth?: OnAuth;
* keepAlive?: boolean;
* keepAliveMsecs?: number;
* authType?: 'Basic' | 'Bearer';
* authRealm?: string;
* useHttpRequest?: boolean;
* }} HttpProxyOptions
* @typedef {{
* rejectUnauthorized?: boolean;
* ca?: string;
* cert?: string;
* key?: string;
* }} SSLProxyOptions
* @typedef {HttpProxyOptions & { rewriteOptions?: RewriteOptions }} HttpProxyServerOptions
*/
/**
* @typedef {net.Server & { _http: HttpProxyOptions; _rewriteOptions?: RewriteOptions }} HttpProxyServer
* @typedef {net.Socket & { _http: HttpProxyOptions & SSLProxyOptions }} HttpProxySocket
*/
/**
* @template T
* @param {IfAny<T> extends true ? HttpProxyServerOptions : Extract<T, net.Server | HttpProxyServerOptions>} [server]
* @param {IfAny<T> extends true ? never : HttpProxyServerOptions} [opts]
* @return {HttpProxyServer}
*/
function createHttpProxy(server, opts) {
if (server instanceof http.Server) {
throw new Error('WARNING! http.Server not allowed, use net.Server instead')
}
/** @type {HttpProxyServer} */
const proxy = server instanceof net.Server ? server : net.createServer();
/** @type {HttpProxyServerOptions} */
const _opts = server instanceof net.Server ? opts : server;
proxy._http = Object.assign({ keepAlive: true, keepAliveMsecs: 1000 }, _opts)
proxy._rewriteOptions = _opts ? _opts.rewriteOptions : undefined
proxy.on('connection', _onConnection)
return proxy;
}
/**
* @this {HttpProxyServer}
* @param {HttpProxySocket} socket
*/
function _onConnection(socket) {
const server = this;
_setTimeout(socket, server._http.keepAliveMsecs)
socket._http = Object.assign({}, server._http);
socket._rewriteOptions = server._rewriteOptions;
socket.once('data', _onceData)
socket.on('proxy-headers', _onProxyHeaders)
socket.on('proxy-authorization', _onProxyAuthorization)
socket.on('proxy-request', _onProxyRequest)
}
/**
* @this {net.Socket}
* @param {Buffer} data
*/
function _onceData(data) {
const socket = this;
const string = data.toString('utf-8')
const line = string.split('\n', 1)[0].trim()
const [method] = line.split(' ', 1)
if (http.METHODS.includes(method)) {
socket.emit('proxy-headers', _parseRequest(data))
}
}
/**
* @this {HttpProxySocket}
* @param {import('./tools').IRequest} req
*/
function _onProxyHeaders(req) {
const socket = this;
/** @type {RewriteOptions} */
const rewriteOptions = socket._rewriteOptions
if (rewriteOptions) {
rewriteOptions(req, (opts) => {
Object.assign(socket._http, opts)
})
}
socket.on('error', _onError)
socket.on('close', _onClose)
const { keepAlive, keepAliveMsecs } = socket._http;
if (keepAlive && req.headers['connection'] && req.headers['connection'].toLowerCase() === 'keep-alive') {
_setKeepAlive(socket, keepAliveMsecs)
}
socket.emit('proxy-authorization', req)
}
/**
* @this {HttpProxySocket}
* @param {import('./tools').IRequest} req
*/
function _onProxyAuthorization(req) {
const socket = this;
const { authType, authRealm } = socket._http;
if (!authType) {
socket.emit('proxy-request', req)
return;
}
if (authType && !req.headers['proxy-authorization']) {
socket.end(_status407(authType, authRealm))
return;
}
const [type, token = ''] = req.headers['proxy-authorization'].split(/\s+/g)
if (type !== authType || !token) {
socket.end(_status407(authType, authRealm))
return;
}
const authCallback = (authorized) => {
if (authorized) {
socket.emit('proxy-request', req)
} else {
socket.end(_status407(authType, authRealm))
}
}
const { onAuth = _unAuth } = socket._http;
let username, password;
switch (type) {
case 'Basic':
([username, password] = Buffer.from(token, 'base64').toString('utf-8').split(':'))
onAuth({ type, username, password }, authCallback)
break;
case 'Bearer':
onAuth({ type, token }, authCallback)
break;
}
}
/**
* @this {HttpProxySocket}
* @param {import('./tools').IRequest} req
*/
function _onProxyRequest(req) {
const socket = this;
const { keepAlive, keepAliveMsecs } = socket._http;
if (req.method === 'CONNECT') {
const [host, port] = req.url.split(':');
const proxy = net.createConnection({ host, port })
if (keepAlive && req.headers['proxy-connection'] && req.headers['proxy-connection'].toLowerCase() === 'keep-alive') {
_setKeepAlive(proxy, keepAliveMsecs)
} else {
_setTimeout(proxy, keepAliveMsecs)
}
socket.pause();
proxy._name = `${host}:${port}`
proxy.on('error', _onError)
proxy.on('close', _onClose)
proxy.on('connect', () => {
socket.write('HTTP/1.1 200 OK\r\n\r\n', 'utf-8')
socket.resume();
socket.pipe(proxy, { end: false })
proxy.pipe(socket, { end: false })
})
} else {
if (req.url.indexOf('http://') !== 0 && req.url.indexOf('https://') !== 0) {
socket.end(_getRawHeaders({ statusCode: 400 }), 'utf-8')
return;
}
const url = new URL(req.url)
const defaultPort = url.protocol === 'http:' ? 80 : 443;
const { useHttpRequest } = socket._http;
if (useHttpRequest) {
const headers = Object.assign({}, req.headers)
delete headers['proxy-authorization']
socket.resume()
const httpx = url.protocol === 'https:' ? https : http;
const { httpAgent, httpsAgent } = _getAgent(socket._http)
const xReq = httpx.request({
agent: url.protocol === 'https:' ? httpsAgent : httpAgent,
method: req.method,
headers,
hostname: url.hostname,
protocol: url.protocol,
port: url.port,
path: url.pathname + url.search + url.hash,
}, (xRes) => {
socket.write(_getRawHeaders(xRes), 'utf-8')
if (xRes.headers['transfer-encoding'] === 'chunked') {
xRes.on('data', (chunk) => {
socket.write(chunk.length.toString(16) + '\r\n' + chunk + '\r\n')
})
xRes.on('end', () => socket.end('0\r\n\r\n'))
} else {
xRes.pipe(socket)
}
})
xReq.end(req.body.toString('utf-8'))
socket.pipe(xReq)
const _name = `${url.hostname}:${url.port || defaultPort}`
xReq._name = _name;
xReq.on('error', _onError)
} else {
const { rejectUnauthorized } = socket._http;
const port = url.port || defaultPort;
const conn = net.createConnection({ host: url.hostname, port });
const proxy = url.protocol === 'https:' ? new tls.TLSSocket(conn, { rejectUnauthorized }) : conn;
if (keepAlive && req.headers['connection'] && req.headers['connection'].toLowerCase() === 'keep-alive') {
_setKeepAlive(proxy, keepAliveMsecs)
} else {
_setTimeout(proxy, keepAliveMsecs)
}
const _name = `${url.hostname}:${port}`
proxy._name = _name;
const headers = Object.assign({}, req.headers)
delete headers['proxy-authorization']
const rawHeaders = _getRawHeaders({ ...req, headers })
proxy.write(Buffer.from(rawHeaders, 'utf-8'))
proxy.write(req.body)
proxy.on('error', _onError)
proxy.on('close', _onClose)
proxy.pipe(socket, { end: false })
socket.pipe(proxy, { end: false })
}
}
}
/** @type {OnAuth} */
function _unAuth(_auth, callback) {
callback(false)
}
function _hash(value) {
if (!value) {
return value;
}
/** @type {Record<string, string>} */
const results = _hash.results = _hash.results || {}
if (!results[value]) {
results[value] = crypto.createHash('sha256').update(value).digest('hex');
}
return results[value]
}
/**
* @param {HttpProxyOptions & { maxSockets?: number; ca?: string; cert?: string; key?: string; }} opts
*/
function _getAgent(opts) {
/** @type {Record<string, http.Agent>} */
const _httpAgents = _getAgent._httpAgents = _getAgent._httpAgents || {}
/** @type {Record<string, https.Agent>} */
const _httpsAgents = _getAgent._httpsAgents = _getAgent._httpsAgents || {}
const { keepAlive, keepAliveMsecs, rejectUnauthorized, maxSockets, ca, cert, key } = opts;
const name = `${keepAlive}:${keepAliveMsecs}:${maxSockets}:${rejectUnauthorized}:${_hash(ca)}:${_hash(cert)}:${_hash(key)}`
const httpAgent = _httpAgents[name] || new http.Agent({ keepAlive, keepAliveMsecs, maxSockets })
const httpsAgent = _httpsAgents[name] || new https.Agent({ keepAlive, keepAliveMsecs, maxSockets, rejectUnauthorized, ca, cert, key })
_httpAgents[name] = httpAgent;
_httpsAgents[name] = httpsAgent;
return { httpAgent, httpsAgent }
}
const net = require('net')
const http = require('http')
const {
_setKeepAlive,
_readChars,
_onError,
_onClose,
toHex,
_setTimeout,
} = require('./tools')
const CMD = {
CONNECT: 0x01,
BIND: 0x02,
UDP: 0x03,
[0x01]: 'CONNECT',
[0x02]: 'BIND',
[0x03]: 'UDP',
}
const REPv4 = {
SUCCESS: 0x5A,
REJECTED: 0x5B,
UNREACHABLE: 0x5C,
NOUSERID: 0x5D,
}
const REPv5 = {
SUCCESS: 0x00,
FAILED: 0x01,
NOTALLOWED: 0x02,
NETUNREACH: 0x03,
HOSTUNREACH: 0x04,
CONREFUSED: 0x05,
TTLEXPIRED: 0x06,
CMDUNSUPP: 0x07,
ATYPUNSUPP: 0x08,
}
const ATYP = {
IPv4: 0x01,
Name: 0x03,
IPv6: 0x04,
}
module.exports = { createSocksProxy }
/**
* @template T
* @typedef {import('./tools').IfAny<T>} IfAny
*/
/**
* @typedef {(username: string, password: string, callback: (authorized: boolean) => void) => void} OnAuth
* @typedef {{ onAuth?: OnAuth, keepAlive?: boolean; keepAliveMsecs?: number; }} SocksProxyOptions
* @typedef {(opts: { version: 4 | 5; }, callback: (opts: SocksProxyOptions) => void) => void} RewriteOptions
* @typedef {SocksProxyOptions & { rewriteOptions?: RewriteOptions }} SocksProxyServerOptions
*/
/**
* @typedef {net.Server & { _socks: SocksProxyOptions; _socksRewriteOptions?: RewriteOptions }} SocksProxyServer
* @typedef {net.Socket & { _socks: SocksProxyOptions }} SocksProxySocket
*/
/**
* @template T
* @param {IfAny<T> extends true ? SocksProxyServerOptions : Extract<T, net.Server | SocksProxyServerOptions>} [server]
* @param {IfAny<T> extends true ? never : SocksProxyServerOptions} [opts]
* @return {SocksProxyServer}
*/
function createSocksProxy(server, opts) {
if (server instanceof http.Server) {
throw new Error('http.Server not allowed, use net.Server instead')
}
const proxy = server instanceof net.Server ? server : net.createServer()
const _socks = server instanceof net.Server ? opts : server;
proxy._socks = Object.assign({ keepAlive: true, keepAliveMsecs: 1000 }, _socks)
proxy._socksRewriteOptions = _socks ? _socks.rewriteOptions : undefined;
proxy.on('connection', _onConnection)
return proxy;
}
/**
* @this {SocksProxyServer}
* @param {SocksProxySocket} socket
*/
function _onConnection(socket) {
const _socks = this._socks;
socket._socks = Object.assign({}, _socks);
socket._socksRewriteOptions = this._socksRewriteOptions
socket.once('data', _onceData)
_setTimeout(socket, _socks.keepAliveMsecs)
socket.on('socks4', _onSocks4)
socket.on('socks5', _onSocks5)
}
/**
* @this {SocksProxySocket}
* @param {Buffer} data
*/
function _onceData(data) {
const version = data[0]
switch (version) {
case 0x04:
this.emit('socks4', data)
break;
case 0x05:
this.emit('socks5', data)
break;
default:
return;
}
}
/**
* @this {SocksProxySocket}
* @param {Buffer} data
*/
function _onSocks4(data) {
const socket = this;
socket.on('error', _onError)
socket.on('close', _onClose)
const command = data[1]
const port = data.readUint16BE(2)
const ip = data.subarray(4, 8)
const userid = _readChars(data, 8)
if (!userid) {
socket.end(_socks4Rep(REPv4.NOUSERID))
return;
}
const address = ip.join('.')
let host = address;
if (ip[0] === 0x00 && ip[1] === 0x00 && ip[2] === 0x00 && ip[3] !== 0x00) {
const domain = _readChars(data, 8 + userid.length + 1)
if (!domain) {
socket.end(_socks4Rep(REPv4.REJECTED))
return;
}
host = domain.toString('ascii')
}
/** @type {RewriteOptions} */
const rewriteOptions = socket._socksRewriteOptions;
if (rewriteOptions) {
rewriteOptions({ version: 4 }, (opts) => {
Object.assign(socket._socks, opts)
})
}
const { keepAlive, keepAliveMsecs } = socket._socks;
if (keepAlive) {
_setKeepAlive(socket, keepAliveMsecs)
}
/** @type {net.Socket} */
let proxy
switch (command) {
case CMD.CONNECT:
proxy = net.createConnection({ host, port })
proxy._name = `${host}:${port}`
socket.write(_socks4Rep(REPv4.SUCCESS))
break;
case CMD.BIND:
proxy = net.createConnection({ port })
proxy._name = `:${port}`
socket.write(_socks4Rep(REPv4.SUCCESS))
break;
default:
socket.end(_socks4Rep(REPv4.REJECTED))
return;
}
if (keepAlive) {
_setKeepAlive(proxy, keepAliveMsecs)
} else {
_setTimeout(proxy, keepAliveMsecs)
}
proxy.on('error', _onError)
proxy.on('close', _onClose)
socket.pause()
proxy.on('connect', () => {
socket.resume()
proxy.pipe(socket)
socket.pipe(proxy)
})
}
/**
* @this {SocksProxySocket}
* @param {Buffer} data
*/
function _onSocks5(data) {
const socket = this;
socket.on('error', _onError)
socket.on('close', _onClose)
/** @type {RewriteOptions} */
const rewriteOptions = socket._socksRewriteOptions;
if (rewriteOptions) {
rewriteOptions({ version: 5 }, (opts) => {
Object.assign(socket._socks, opts)
})
}
const { onAuth, keepAlive, keepAliveMsecs } = socket._socks;
if (keepAlive) {
_setKeepAlive(socket, keepAliveMsecs)
}
const nauth = data[1]
const auth = data.subarray(2, 2 + nauth)
if (auth.includes(0x02) && onAuth) {
socket.write(Buffer.from([0x05, 0x02]))
socket.once('data', _onPwAuth)
return;
}
if (auth.includes(0x00) && !onAuth) {
socket.write(Buffer.from([0x05, 0x00]))
socket.once('data', _onSocks5Connection)
return;
}
socket.end(Buffer.from([0x05, 0xFF]))
}
/**
* @this {SocksProxySocket}
* @param {Buffer} data
*/
function _onPwAuth(data) {
const socket = this;
const { onAuth } = this._socks;
const version = data[0]
const idlen = data[1]
const id = data.subarray(2, 2 + idlen).toString('ascii')
const pwlen = data[2 + idlen]
const pw = data.subarray(3 + idlen, 3 + idlen + pwlen).toString('ascii')
onAuth(id, pw, (auth) => {
if (auth) {
socket.write(Buffer.from([version, 0x00]))
socket.once('data', _onSocks5Connection)
} else {
socket.end(Buffer.from([version, 0x01]))
}
})
}
/**
* @this {SocksProxySocket}
* @param {Buffer} data
*/
function _onSocks5Connection(data) {
const socket = this;
const version = data[0]
const command = data[1]
const addrtype = data[3]
let ipv4, domain, ipv6;
let port, portBuf;
let host;
let addr;
switch (addrtype) {
case ATYP.IPv4:
addr = data.subarray(3, 8)
ipv4 = data.subarray(4, 8)
port = data.readUint16BE(8)
portBuf = data.subarray(8, 10)
host = ipv4.join('.')
break;
case ATYP.Name:
addr = data.subarray(3, 5 + data[4])
domain = data.subarray(5, 5 + data[4]).toString('ascii')
port = data.readUint16BE(5 + data[4])
portBuf = data.subarray(5 + data[4], 7 + data[4])
host = domain
break;
case ATYP.IPv6:
addr = data.subarray(3, 20)
ipv6 = data.subarray(4, 20)
port = data.readUint16BE(20)
portBuf = data.subarray(20, 22)
host = ipv6.map(toHex).join(':')
break;
default:
socket.end(Buffer.from([0x05, REPv5.ATYPUNSUPP, 0]))
return;
}
/** @type {net.Socket} */
let proxy;
switch (command) {
case CMD.CONNECT:
case CMD.BIND:
proxy = net.createConnection({ host, port })
proxy._name = `${host}:${port}`
// TODO
socket.write(Buffer.from([0x05, REPv5.SUCCESS, 0, ...addr, ...portBuf]))
break;
case CMD.UDP:
// TODO
socket.end(Buffer.from([0x05, REPv5.CMDUNSUPP, 0]))
return;
default:
socket.end(Buffer.from([0x05, REPv5.CMDUNSUPP, 0]))
return;
}
const { keepAlive, keepAliveMsecs } = this._socks;
if (keepAlive) {
_setKeepAlive(proxy, keepAliveMsecs)
}
socket.pause()
proxy.on('error', _onError)
proxy.on('close', _onClose)
proxy.on('connect', () => {
socket.resume()
proxy.pipe(socket)
socket.pipe(proxy)
})
}
function _socks4Rep(message) {
return Buffer.from([0, message, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01])
}
const net = require('net')
const time = () => new Date().toISOString()
/**
* @template T
* @typedef {0 extends (1 & T) ? true : false} IfAny
* @see https://stackoverflow.com/questions/55541275/typescript-check-for-the-any-type
*/
/**
* @param {net.Socket} socket
*/
const info = (socket) => {
const { remoteAddress: address, remotePort: port, remoteFamily: family } = socket;
if (!address && socket._name) {
return socket._name;
}
return `${address}:${port}:${family}`
}
module.exports = {
time,
info,
_status407,
_parseRequest,
_getRawHeaders,
_setKeepAlive,
_onError,
_onClose,
_onTimeout,
_readChars,
_setTimeout,
toHex,
}
/**
* @typedef {{
* method: string;
* url: string;
* headers: Record<string, string>;
* body: Buffer;
* _rawHeaders: string;
* _rawData: Buffer;
* }} IRequest
*/
/**
*/
/**
* @param {'Basic' | 'Bearer'} [type]
* @param {string} [realm]
*/
function _status407(type, realm) {
return ''
+ 'HTTP/1.1 407 Proxy Authentication Required'
+ '\r\n'
+ `Proxy-Authenticate: ${type || 'Basic'} realm="${realm || 'Proxy authentication required'}"`
+ '\r\n\r\n';
}
/**
* @param {Buffer} data
* @return {IRequest}
*/
function _parseRequest(data) {
let _rawHeaders = ''
/** @type {Buffer} */
let body
for (let i = 0; i < data.length && i < 10000; ++i) {
if (data[i] === 0x0A && data[i + 1] === 0x0A) {
_rawHeaders = data.subarray(0, i).toString('utf-8')
body = data.subarray(i + 2)
}
if (data[i] === 0x0D && data[i + 1] === 0x0A && data[i + 2] === 0x0D && data[i + 3] === 0x0A) {
_rawHeaders = data.subarray(0, i).toString('utf-8')
body = data.subarray(i + 4)
}
}
if (!_rawHeaders) {
return { _rawHeaders, body, _rawData: data }
}
const [methodLine, ...restLines] = _rawHeaders.split('\n')
const [method, url] = methodLine.trim().split(' ', 2)
/** @type {Record<string, string>} */
const headers = {}
for (let i = 0, line, index; i < restLines.length; ++i) {
line = restLines[i].trim()
index = line.indexOf(':')
headers[line.slice(0, index).trim().toLowerCase()] = line.slice(index + 1).trim();
}
return {
method,
url,
headers,
body,
_rawHeaders,
_rawData: data,
}
}
/**
* @param {http.IncomingMessage} message
* @param {string} [version] default `1.1`
*/
function _getRawHeaders(message, version = '1.1') {
const { statusCode, statusMessage, headers, method, url } = message;
let rawHeaders = ''
if (method && url) {
rawHeaders += `${method} ${url} HTTP/${version}\r\n`
} else if (statusCode) {
rawHeaders += `HTTP/${version} ${statusCode} ${statusMessage || ''}\r\n`
}
for (let key in headers) {
rawHeaders += `${key}: ${headers[key]}\r\n`
}
return rawHeaders + '\r\n';
}
/**
* @param {net.Socket} socket
* @param {number} [msec] default `5000` ms
*/
function _setKeepAlive(socket, msec = 5000) {
socket.setKeepAlive(true, msec)
_setTimeout(socket, msec)
}
/**
* Destory `socket` after `msec` of inactivity
* @param {net.Socket} socket
* @param {number} [msec] default `5000` ms
*/
function _setTimeout(socket, msec = 5000) {
socket.setTimeout(msec)
if (!socket.listeners('timeout').includes(_streamDestroy)) {
socket.once('timeout', _streamDestroy)
}
}
/**
* @this {net.Socket}
* @param {Error} error
*/
function _onError(error) {
this.destroy(error)
}
/**
* @this {net.Socket}
*/
function _onClose() {
}
function _onTimeout() {
this.destroy()
}
/**
* @this {import('stream').Duplex}
* @param {Error} [error]
*/
function _streamDestroy(error) {
this.destroy(error)
}
/**
* @param {Buffer} chars
*/
function _readChars(chars, start = 0) {
const maxcount = 1000;
for (let i = start; i < maxcount && i < chars.length; ++i) {
if (chars[i] === 0x00) {
return chars.subarray(start, i);
}
}
}
/**
*
* @param {number} v
*/
function toHex(v) {
switch (typeof v) {
case 'number':
return v.toString(16)
case 'string':
default:
return v;
}
}
const fs = require('fs')
const path = require('path')
const net = require('net')
const http = require('http')
const https = require('https')
const axios = require('axios').default
const ProxyAgent = require('proxy-agent')
const { describe, beforeAll, afterAll, it, expect, afterEach } = require('@jest/globals')
const { createHttpProxy } = require('../src/http-proxy')
const time = () => new Date().toISOString()
/**
* @param {net.Server} server
*/
const trackSockets = (server) => {
/** @type {Record<string, net.Socket>} */
const sockets = {}
server.on('connection', (socket) => {
const name = socket.remoteAddress + ':' + socket.remotePort
sockets[name] = socket;
socket.on('close', () => {
delete sockets[name]
})
})
server.on('destroy', () => {
for (const name in sockets) {
sockets[name].destroy()
}
})
}
describe('http-proxy', () => {
/** @template T */
function Deffer() {
/** @type {Promise<T>} */
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
})
}
/** @param {import('net').Server} server */
const serverListen = async (server) => {
const deffer = new Deffer();
server.listen(() => {
const address = server.address();
deffer.resolve(address ? address.port : null)
})
server.on('error', (e) => {
console.log(time(), 'server-error', e)
})
trackSockets(server)
return deffer.promise;
}
/** @param {import('net').Server} server */
const serverClose = async (server) => {
const deffer = new Deffer();
server.emit('destroy')
server.close((error) => {
error ? deffer.reject(error) : deffer.resolve();
})
return deffer.promise;
}
/**
* @type {{
* server: import('../src/http-proxy').HttpProxyServer;
* port: number;
* auth: { username: string; password: string; }
* options: import('../src/http-proxy').HttpProxyServerOptions;
* }}
*/
const proxy = {
auth: {
username: 'test',
password: '1234',
},
options: {
keepAlive: true,
keepAliveMsecs: 1000,
authType: 'Basic',
authRealm: 'Need authentication',
onAuth: myAuth,
}
}
/**
* @type {{
* server: http.Server;
* port: number;
* onRequest: http.RequestListener;
* }}
*/
const _http = {
onRequest: function onRequest(req, res) {
if (req.url.includes('/json')) {
const json = { foo: 'bar' }
res.writeHead(200, {
'Server': 'test-server',
'Content-Type': 'application/json',
'Content-Length': JSON.stringify(json).length,
})
res.end(JSON.stringify(json))
} else {
res.writeHead(200, { 'Server': 'test-server', 'Content-Type': 'text/plain' })
res.end('test-body')
}
},
}
/**
* @type {{
* server: https.Server;
* port: number;
* ssl: {
* cert: string;
* key: string;
* }
* }}
*/
const _https = {
ssl: {
cert: fs.readFileSync(path.join(__dirname, './public-cert.pem'), 'utf-8'),
key: fs.readFileSync(path.join(__dirname, './private-key.pem'), 'utf-8'),
}
}
/**
* @type {{
* proxy: import('axios').AxiosProxyConfig;
* }}
*/
const _client = {}
/** @type {import('../src/http-proxy').OnAuth */
function myAuth(auth, callback) {
switch (auth.type) {
case 'Basic':
callback(auth.username === proxy.auth.username && auth.password === proxy.auth.password);
break;
case 'Bearer':
callback(auth.token === proxy.auth.password)
break;
default:
callback(false)
}
}
/**
* @param {{ method?: string; url: string; data?: any; headers?: Record<string, string>; } | string} config
* @param {import('axios').AxiosRequestConfig} [opts]
*/
const request = async (config, opts) => {
config = typeof config === 'string' ? { method: 'GET', url: config } : Object.assign({ method: 'GET' }, config)
const res = await axios.request({
...config,
validateStatus: (status) => {
return status >= 200 && status < 600;
},
proxy: { ..._client.proxy },
...opts,
headers: {
...config.headers,
...(opts ? opts.headers : undefined),
'connection': 'keep-alive',
},
})
return res
}
beforeAll(async () => {
proxy.server = createHttpProxy({ ...proxy.options })
_http.server = http.createServer(_http.onRequest)
_https.server = https.createServer(_https.ssl, _http.onRequest)
proxy.port = await serverListen(proxy.server);
_http.port = await serverListen(_http.server)
_https.port = await serverListen(_https.server)
_client.proxy = {
protocol: 'http:',
host: '127.0.0.1',
port: proxy.port,
}
})
afterAll(async () => {
await serverClose(proxy.server)
await serverClose(_http.server)
await serverClose(_https.server)
})
afterEach(() => {
proxy.server._rewriteOptions = proxy.options.rewriteOptions;
})
describe('create', () => {
it('http.Server', () => {
expect.assertions(1)
try {
const server = http.createServer()
const _proxy = createHttpProxy(server)
} catch (e) {
expect(e.message).toMatch(/http\.Server not allowed/i)
}
})
it('net.Server', () => {
const server = net.createServer()
const proxy = createHttpProxy(server)
expect(proxy).toEqual(server)
})
})
describe('Proxy Authentication Required', () => {
it('Basic', async () => {
const requests = [{
url: `http://127.0.0.1:${_http.port}/path?_=123`,
}, {
url: `https://127.0.0.1:${_https.port}/path?_=123`,
opts: {
proxy: { ..._client.proxy },
}
}]
await requests.reduce(async (promise, { url, opts }) => {
await promise;
const res = await request(url, opts)
expect(res.status).toBe(407)
expect(proxy.options.authRealm).toBeDefined()
expect(res.headers['proxy-authenticate']).toMatch('Basic')
expect(res.headers['proxy-authenticate']).not.toMatch('Bearer')
expect(res.headers['proxy-authenticate']).toMatch(proxy.options.authRealm)
}, Promise.resolve())
})
it('Bearer', async () => {
proxy.server._rewriteOptions = (req, callback) => {
callback({ authType: 'Bearer' })
}
const requests = [{
url: `http://127.0.0.1:${_http.port}/path?_=123`,
}, {
url: `https://127.0.0.1:${_https.port}/path?_=123`,
}]
await requests.reduce(async (promise, { url, opts }) => {
await promise
const res = await request(url, opts)
expect(res.status).toBe(407)
expect(proxy.options.authRealm).toBeDefined()
expect(res.headers['proxy-authenticate']).toMatch('Bearer')
expect(res.headers['proxy-authenticate']).not.toMatch('Basic')
expect(res.headers['proxy-authenticate']).toMatch(proxy.options.authRealm)
}, Promise.resolve())
})
it('Type mismatch', async () => {
proxy.server._rewriteOptions = (req, callback) => {
callback({ authType: 'Basic' })
}
const requests = [{
url: `http://127.0.0.1:${_http.port}/path?_=123`,
opts: {
headers: { 'proxy-authorization': 'Basic' },
}
}, {
url: `https://127.0.0.1:${_https.port}/path?_=123`,
opts: {
headers: { 'proxy-authorization': 'Basic' },
}
}]
await requests.reduce(async (promise, { url, opts }) => {
await promise
const res = await request(url, opts)
expect(res.status).toBe(407)
expect(proxy.options.authRealm).toBeDefined()
expect(res.headers['proxy-authenticate']).toMatch('Basic')
expect(res.headers['proxy-authenticate']).not.toMatch('Bearer')
expect(res.headers['proxy-authenticate']).toMatch(proxy.options.authRealm)
}, Promise.resolve())
})
it('Wrong credentials - Bearer', async () => {
proxy.server._rewriteOptions = (req, callback) => {
callback({ authType: 'Bearer' })
}
const requests = [{
url: `http://127.0.0.1:${_http.port}/path?_=123`,
opts: {
headers: { 'proxy-authorization': 'Bearer 1' },
}
}, {
url: `https://127.0.0.1:${_https.port}/path?_=123`,
opts: {
headers: { 'proxy-authorization': 'Bearer 321' },
}
}]
await requests.reduce(async (promise, { url, opts }) => {
await promise
const res = await request(url, opts)
expect(res.status).toBe(407)
expect(proxy.options.authRealm).toBeDefined()
expect(res.headers['proxy-authenticate']).toMatch('Bearer')
expect(res.headers['proxy-authenticate']).not.toMatch('Basic')
expect(res.headers['proxy-authenticate']).toMatch(proxy.options.authRealm)
}, Promise.resolve())
})
it('Wrong credentials - Basic', async () => {
proxy.server._rewriteOptions = (req, callback) => {
callback({ authType: 'Basic' })
}
const requests = [{
url: `http://127.0.0.1:${_http.port}/path?_=123`,
opts: {
headers: { 'proxy-authorization': 'Basic ' + Buffer.from('user:321').toString('base64') },
}
}, {
url: `https://127.0.0.1:${_https.port}/path?_=123`,
opts: {
headers: { 'proxy-authorization': 'Basic ' + Buffer.from('user:321').toString('base64') },
}
}]
await requests.reduce(async (promise, { url, opts }) => {
await promise
const res = await request(url, opts)
expect(res.status).toBe(407)
expect(proxy.options.authRealm).toBeDefined()
expect(res.headers['proxy-authenticate']).toMatch('Basic')
expect(res.headers['proxy-authenticate']).not.toMatch('Bearer')
expect(res.headers['proxy-authenticate']).toMatch(proxy.options.authRealm)
}, Promise.resolve())
})
})
describe('Proxy authorization', () => {
it('Basic', async () => {
proxy.server._rewriteOptions = (req, callback) => {
const opts = {}
if (req.url.includes('https://') && req.method !== 'CONNECT') {
Object.assign(opts, { rejectUnauthorized: false })
}
callback({ ...opts, authType: 'Basic' })
}
const requests = [{
url: `http://127.0.0.1:${_http.port}/path?_=123`,
}, {
url: `https://127.0.0.1:${_https.port}/path?_=123`,
}]
await requests.reduce(async (promise, { url, opts }) => {
await promise
const res = await request(url, {
proxy: {
..._client.proxy,
auth: proxy.auth,
},
headers: {
connection: 'keep-alive',
},
...opts,
})
expect(res.status).toBe(200)
expect(res.headers['server']).toBe('test-server')
expect(res.headers['content-type']).toBe('text/plain')
expect(res.data).toBe('test-body')
}, Promise.resolve())
})
it('Bearer', async () => {
proxy.server._rewriteOptions = (req, callback) => {
const opts = {}
if (req.url.includes('https://') && req.method !== 'CONNECT') {
Object.assign(opts, { rejectUnauthorized: false })
}
callback({ ...opts, authType: 'Bearer' })
}
const requests = [{
url: `http://127.0.0.1:${_http.port}/path?_=123`,
}, {
url: `https://127.0.0.1:${_https.port}/path?_=123`,
}]
await requests.reduce(async (promise, { url, opts }) => {
await promise
const res = await request(url, {
...opts,
headers: {
'proxy-authorization': 'Bearer ' + proxy.auth.password,
}
})
expect(res.status).toBe(200)
expect(res.headers['server']).toBe('test-server')
expect(res.headers['content-type']).toBe('text/plain')
expect(res.data).toBe('test-body')
}, Promise.resolve())
})
})
describe('Proxy https', () => {
it('google.com', async () => {
proxy.server._rewriteOptions = (_req, callback) => {
callback({ authType: undefined })
}
const res = await request('https://google.com', {
proxy: false,
httpsAgent: new ProxyAgent({
..._client.proxy,
})
})
expect(res.status).toBe(200)
})
})
describe('useHttpRequest', () => {
it('true', async () => {
proxy.server._rewriteOptions = (req, callback) => {
const opts = {}
if (req.url.includes('https:')) {
Object.assign(opts, { rejectUnauthorized: false })
}
callback({ ...opts, useHttpRequest: true })
}
const requests = [{
url: `http://127.0.0.1:${_http.port}/path?_=123`,
}, {
url: `https://127.0.0.1:${_https.port}/path?_=123`,
}]
await requests.reduce(async (promise, { url, opts }) => {
await promise
const res = await request(url, {
...opts,
proxy: {
..._client.proxy,
auth: proxy.auth,
}
})
expect(res.status).toBe(200)
expect(res.headers['server']).toBe('test-server')
expect(res.headers['content-type']).toBe('text/plain')
expect(res.data).toBe('test-body')
}, Promise.resolve())
})
})
describe('keepAlive', () => {
it('false', async () => {
proxy.server._rewriteOptions = (req, callback) => {
callback({ keepAlive: false, rejectUnauthorized: false, keepAliveMsecs: 400 })
}
const requests = [{
url: `http://127.0.0.1:${_http.port}/path?_=123`,
}, {
url: `https://127.0.0.1:${_https.port}/path?_=123`,
}]
await requests.reduce(async (promise, { url, opts }) => {
await promise
const res = await request(url, {
...opts,
proxy: {
..._client.proxy,
auth: proxy.auth,
}
})
expect(res.status).toBe(200)
expect(res.headers['server']).toBe('test-server')
expect(res.headers['content-type']).toBe('text/plain')
expect(res.data).toBe('test-body')
}, Promise.resolve())
})
})
describe('Bad request', () => {
it('Wrong url', async () => {
proxy.server._rewriteOptions = (req, callback) => {
callback({ keepAlive: false, authType: undefined })
}
const url = `http://127.0.0.1:${proxy.port}/path?_=1`
const res = await request(url, { proxy: false })
expect(res.status).toBeGreaterThanOrEqual(400)
expect(res.status).toBeLessThan(500)
})
})
describe('Proxy misc', () => {
it('json', async () => {
proxy.server._rewriteOptions = (req, callback) => {
callback({ keepAlive: false, authType: undefined, useHttpRequest: true })
}
const url = `http://127.0.0.1:${_http.port}/json?_=1`
const res = await request(url)
expect(res.status).toBe(200)
expect(res.data).toBeDefined()
expect(res.data).toHaveProperty('foo')
})
it('onAuth defaults', async () => {
proxy.server._rewriteOptions = (req, callback) => {
callback({ keepAlive: false, authType: 'Bearer', onAuth: undefined })
}
const url = `http://127.0.0.1:${_http.port}/json?_=1`
const res = await request(url, {
headers: {
'proxy-authorization': 'Bearer ' + proxy.auth.password,
}
})
expect(res.status).toBe(407)
})
})
})

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

const fs = require('fs')
const path = require('path')
const http = require('http')
const https = require('https')
const { describe, beforeAll, afterAll, it, expect } = require('@jest/globals')
const axios = require('axios').default;
const { SocksProxyAgent } = require('socks-proxy-agent')
const { createSocksProxy } = require('../src/socks-proxy')
describe('socks-proxy', () => {
/** @template T */
function Deffer() {
/** @type {Promise<T>} */
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
})
}
/** @param {import('net').Server} server */
const trackSockets = (server) => {
if (server._tracking) {
return;
}
server._tracking = true;
/** @type {Record<string, import('net').Socket>} */
const sockets = {}
server.on('connection', (socket) => {
const name = `${socket.remoteAddress}:${socket.remotePort}`
sockets[name] = socket;
socket.on('close', () => {
delete sockets[name]
})
})
server.on('destroy', () => {
for (const name in sockets) {
sockets[name].destroy()
delete sockets[name]
}
})
}
/** @param {import('net').Server} server */
const serverListen = async (server) => {
/** @type {Deffer<number>} */
const deffer = new Deffer()
server.listen(() => {
deffer.resolve(server.address().port)
})
trackSockets(server)
return deffer.promise;
}
/** @param {import('net').Server} server */
const serverClose = async (server) => {
const deffer = new Deffer()
server.close((error) => {
error ? deffer.reject(error) : deffer.resolve()
})
server.emit('destroy')
await deffer.promise;
}
/**
* @type {{
* server: import('../src/socks-proxy').SocksProxyServer;
* port: number;
* options: import('../src/socks-proxy').SocksProxyOptions;
* auth: { username: string; password: string; };
* }}
*/
const proxy = {
auth: {
username: 'test',
password: '1234',
},
options: {
keepAlive: true,
keepAliveMsecs: 1000,
onAuth: (username, password, callback) => {
callback(proxy.auth.username === username && proxy.auth.password === password)
},
}
}
/**
* @type {{
* server: http.Server;
* port: number;
* onRequest: http.RequestListener;
* }}
*/
const _http = {
onRequest: (req, res) => {
if (req.url.includes('/json')) {
const json = JSON.stringify({ foo: 'bar' })
res.writeHead(200, {
'Content-Type': 'application/json',
'Content-Length': json.length,
})
res.end(json)
} else {
const text = 'test-socks'
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Length': text.length,
})
res.end(text)
}
}
}
/**
* @type {{
* server: https.Server;
* port: number;
* cert: string;
* key: string;
* }}
*/
const _https = {}
beforeAll(async () => {
proxy.server = createSocksProxy(proxy.options)
_http.server = http.createServer(_http.onRequest);
_https.cert = fs.readFileSync(path.join(__dirname, 'public-cert.pem'), 'utf-8')
_https.key = fs.readFileSync(path.join(__dirname, 'private-key.pem'), 'utf-8')
_https.server = https.createServer({ cert: _https.cert, key: _https.key }, _http.onRequest);
proxy.port = await serverListen(proxy.server)
_http.port = await serverListen(_http.server)
_https.port = await serverListen(_https.server)
})
afterAll(async () => {
await serverClose(proxy.server)
await serverClose(_http.server)
await serverClose(_https.server)
})
it('Invalid params', () => {
const server = http.createServer()
expect.assertions(1)
try {
const proxy = createSocksProxy(server)
} catch (e) {
expect(e.message).toMatch(/http\.Server not allowed/i)
}
})
it('Successfull request', async () => {
const config = {
hostname: '127.0.0.1',
port: proxy.port,
tls: {
rejectUnauthorized: false,
},
}
const agents = [{
protocol: 'socks4:'
}, {
protocol: 'socks4a:',
}, {
protocol: 'socks5:',
username: proxy.auth.username,
password: proxy.auth.password,
}, {
protocol: 'socks5h:',
username: proxy.auth.username,
password: proxy.auth.password,
}].map(opts => new SocksProxyAgent({ ...config, ...opts }))
const run = async () => {
const requests = [{
url: `http://localhost:${_http.port}/path?_=${Date.now()}`,
}, {
url: `https://localhost:${_https.port}/json?_=${Date.now()}`,
}, {
url: `https://example.com/?_=${Date.now()}`,
}]
const promises = agents.map(async (agent) => {
await requests.reduce(async (promise, { url }) => {
await promise;
const res = await axios.get(url, {
httpAgent: agent,
httpsAgent: agent,
validateStatus: (status) => status >= 200 && status < 600,
})
expect(res.status).toEqual(200)
expect(res.data).toBeDefined()
if (url.includes('/json')) {
expect(res.data).toHaveProperty('foo')
} else if (!url.includes('example.com')) {
expect(res.data).toEqual('test-socks')
}
}, Promise.resolve())
})
await Promise.all(promises)
}
await run()
proxy.server._socksRewriteOptions = (_, callback) => {
callback({ keepAlive: false })
}
await run()
}, 10 * 1000)
it('Unsupported auth type error', async () => {
const config = {
hostname: '127.0.0.1',
port: proxy.port,
tls: {
rejectUnauthorized: false,
},
}
const agent = new SocksProxyAgent({ ...config, protocol: 'socks5h:' })
const url = `http://localhost:${_http.port}/path?_=${Date.now()}`
expect.assertions(1)
try {
const res = await axios.get(url, {
httpAgent: agent,
validateStatus: (status) => status >= 200 && status < 600,
})
} catch (e) {
expect(e.message).toBeDefined()
}
})
it('No auth', async () => {
const config = {
hostname: '127.0.0.1',
port: proxy.port,
tls: {
rejectUnauthorized: false,
},
}
proxy.server._socksRewriteOptions = (_, callback) => {
callback({ onAuth: undefined })
}
const agent = new SocksProxyAgent({ ...config, protocol: 'socks5h:' })
const url = `http://localhost:${_http.port}/path?_=${Date.now()}`
const res = await axios.get(url, {
httpAgent: agent,
validateStatus: (status) => status >= 200 && status < 600,
})
expect(res.status).toBe(200)
})
})