Skip to content

Instantly share code, notes, and snippets.

@BlueSedDragon
Created August 27, 2020 14:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BlueSedDragon/a0daf2c785fc4af43292f8a34f6c4007 to your computer and use it in GitHub Desktop.
Save BlueSedDragon/a0daf2c785fc4af43292f8a34f6c4007 to your computer and use it in GitHub Desktop.
httpilltun.js
const process = require('process');
process.on('uncaughtException', console.error);
// used for the httpilltun itself.
const http = require('http');
const websocket = require('./node_modules/websocket');
const stream = require('stream');
const fs = require('fs');
const https = require('https');
const url = require('url');
const crypto = require('crypto');
// used for connecting to remote.
const dns = require('dns');
const net = require('net');
const dgram = require('dgram');
// some information.
const NAME = 'httpilltun';
const VERSION = 1;
/* HTTP Header Arguments */
const ARGV = 'x-' + NAME;
const ARGV_VER = ARGV + '-version';
const ARGV_ACTION = ARGV + '-action';
const ARGV_SESSION = ARGV + '-session';
const ARGV_SESSION_ID = ARGV_SESSION + '-id';
const ARGV_SESSION_SEQ = ARGV_SESSION + '-seq';
const ARGV_PROXY = ARGV + '-proxy'
const ARGV_PROXY_NETWORK = ARGV_PROXY + '-network';
const ARGV_PROXY_NETWORK_TYPE = ARGV_PROXY_NETWORK + '-type';
const ARGV_PROXY_NETWORK_HOSTNAME = ARGV_PROXY_NETWORK + '-hostname';
const ARGV_PROXY_TRANSPORT = ARGV_PROXY + '-transport';
const ARGV_PROXY_TRANSPORT_TYPE = ARGV_PROXY_TRANSPORT + '-type';
const ARGV_PROXY_TRANSPORT_PORT = ARGV_PROXY_TRANSPORT + '-port';
const ARGV_PROXY_TRANSPORT_ID = ARGV_PROXY_TRANSPORT + '-id';
/* End */
// Allowed Characters of The HTTP Path.
const CHARS = '1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM';
/* HTTP Content Types */
const CONTENT_TYPE = {};
CONTENT_TYPE.TEXT = 'text/plain; charset=utf-8';
CONTENT_TYPE.BINARY = 'application/octet-stream';
CONTENT_TYPE.HTML = 'text/html; charset=utf-8';
CONTENT_TYPE.JS = 'text/javascript; charset=utf-8';
/* End */
/* HTTP Headers Default Value */
const HEADERS_SERVER = () => {
return {
'access-control-allow-origin': '*'
};
};
/* Some Functions */
const arrow = (obj) => {
// sort: min to max.
var keys = Object.keys(obj).sort();
var chunk = undefined;
for (let i of keys) {
chunk = null;
// get this list or skip.
let list = obj[i];
if (list === true) continue;
// exception handle.
if (!list) break;
if (list.length <= 0) break;
// out-of-order handle.
if (BigInt(i) > 0n && (!obj[i - 1])) break;
// check if end.
let end = 0;
if (list[list.length - 1].length === 0) {
end = 1;
}
// get all chunk.
chunk = Buffer.concat(list);
list.length = 0;
// set status to done.
if (end) obj[i] = true;
// done.
break;
}
return chunk;
};
// used to display log.
const log = function (...args) {
var type = args[0].toUpperCase();
args.shift();
console.log(`[${type}](${Date.now() / 1000}):`, ...args);
};
// used to generate new id.
const generate_id = (function () {
var salt = crypto.randomBytes(1048576);
var count = 0n;
return (function () {
var data = [];
data.push(salt);
data.push(Buffer.from(String(count++)));
data = Buffer.concat(data);
var id = (
crypto.createHash('sha3-512')
.update(data)
.digest()
.hexSlice()
.toLowerCase()
);
return id;
});
})();
/* End */
const Session = class {
id = ''; // String{}, Session ID.
network = {
type: '', // String{}, IPv4 or IPv6 or Domain (or new protocol at future)
hostname: '' // String{}, Network Hostname. domain example: remote-host.example.com. ip address example: 1.2.3.4 or 1234:abc::d (or new format at future).
};
transport = {
type: '', // String{}, TCP or UDP (or QUIC at future)
port: 0, // Integer{}, 1 ~ 65535, used for TCP and UDP.
id: Buffer.from([]) // Buffer{}, maybe used for the QUIC at future.
};
};
const ServerSession = class extends Session {
upstream = null; // tcp connection or udp socket or ...
callback = null;
send_list = {}; // $seq : Array[ Buffer{}, Buffer{}, ... ]
recv_list = []; // Array[ Buffer{}, Buffer{}, ... ] (no seq needed)
// server just need the recv seq only.
recv_seq = 0n;
TCP() {
if (this.upstream) return false;
var connection = net.connect({
host: this.network.hostname,
port: this.transport.port
}, () => {
this.callback(null, connection);
connection.on('data', (chunk) => {
if (this.closed) return;
this.recv_list.push(chunk);
});
var sender = () => {
if (this.closed) return;
let chunk = arrow(this.send_list);
if (chunk) connection.write(chunk);
setTimeout(sender, 0);
};
sender();
this.upstream = connection;
});
this.closing = () => {
try { connection.destroy(); } catch (e) { }
};
{
let er = () => {
try { connection.destroy(); } catch (e) { }
this.close();
}
connection.on('close', er);
connection.on('end', er);
connection.on('error', er);
}
return true;
}
UDP() {
if (this.upstream) return false;
var socket = null;
switch (this.network.type) {
case 'ipv4':
socket = dgram.createSocket('udp4');
break;
case 'ipv6':
socket = dgram.createSocket('udp6');
break;
case 'domain':
dns.resolve6(this.network.hostname, (err6, ip6) => {
if (err6 || (!ip6[0])) {
dns.resolve4(this.network.hostname, (err4, ip4) => {
if (err4 || (!ip4[0])) {
this.callback(new Error(`cannot to resolve domain. AAAA: ${String(err6)}, A: ${String(err4)}`), null);
return;
}
this.network.hostname = ip4[0];
this.network.type = 'ipv4';
setTimeout(() => void this.UDP(), 0);
});
return;
}
this.network.hostname = ip6[0];
this.network.type = 'ipv6';
setTimeout(() => void this.UDP(), 0);
});
break;
default:
this.callback(new Error('bad type of network layer.'), null);
return;
}
socket.on('message', (data, info) => {
if (this.closed) return;
if (
info.hostname !== this.network.hostname ||
info.port !== this.network.port
) return;
this.recv_list.push(data);
});
{
let er = () => {
try { socket.close(); } catch (e) { }
this.close();
}
socket.on('close', er);
socket.on('end', er);
socket.on('error', er);
}
var sender = () => {
if (this.closed) return;
var chunk = arrow(this.send_list);
if (chunk) {
try {
socket.send(chunk, this.transport.port, this.network.hostname);
} catch (err) {
this.close();
throw err;
}
}
setTimeout(sender, 0);
};
sender();
this.callback(null, socket);
this.upstream = socket;
this.closing = () => {
try { socket.close(); } catch (e) { }
};
return true;
}
connect() {
switch (this.transport.type) {
case 'tcp':
this.TCP();
break;
case 'udp':
this.UDP();
break;
default:
throw (new Error('bad type of transport layer.'));
}
}
closed = false;
close() {
if (this.closed) return;
this.closed = true;
this.closing();
}
};
const ClientSession = class extends Session {
closed = false;
id = null;
server = { // server address.
hostname: 'server.example',
port: 443,
path: 'path-to-server'
};
send_list = []; // Array[ Buffer{}, Buffer{}, ... ] (no seq needed)
recv_list = {}; // $seq : Array[ Buffer{}, Buffer{}, ... ]
// client just need the send seq only.
send_seq = 0n;
request(options, callback) {
options.agent = (new https.Agent({
keepAlive: false,
maxSockets: 1,
maxFreeSockets: 0
}));
if (options.headers) {
options.headers['connection'] = 'close';
}
var request = https.request(options, callback);
return request;
}
new(callback) {
if (this.closed) {
callback((new Error('session closed.')), null);
return false;
}
var request = this.request({
host: this.server.hostname,
port: this.server.port,
path: `/${this.server.path}`,
method: 'GET',
headers: {
[ARGV_ACTION]: 'new',
[ARGV_PROXY_NETWORK_TYPE]: this.network.type,
[ARGV_PROXY_NETWORK_HOSTNAME]: this.network.hostname,
[ARGV_PROXY_TRANSPORT_TYPE]: this.transport.type,
[ARGV_PROXY_TRANSPORT_PORT]: this.transport.port
}
}, (response) => {
var code = response.statusCode;
if (code !== 200) {
response.once('data', (chunk) => {
log('warning', `ClientSession.New: the http status code of server response is not 200. Code: ${code}, Data: ${chunk.hexSlice()}, Message: ${String(chunk)}.`);
});
callback(new Error('ClientSession.New bad the status code of the http response.'), response);
return;
}
var id = response.headers[ARGV_SESSION_ID];
this.id = id;
callback(null, response);
});
request.end();
return true;
}
send(chunk) {
if (Array.isArray(chunk)) {
let tmp = [];
for (let it of chunk) {
tmp.push(Buffer.from(it));
}
chunk = Buffer.concat(tmp);
}
this.send_list.push(chunk);
this.sender();
}
recv(callback) {
{
let er = () => {
var chunk = arrow(this.recv_list);
if (chunk) {
callback(null, chunk);
return;
}
setTimeout(er, 0);
};
er();
}
this.recver();
}
sender() {
if ((!this.id) || this.closed) throw (new Error('no session!'));
if (this.send_list.length <= 0) return true;
var request = this.request({
host: this.server.hostname,
port: this.server.port,
path: `/${this.server.path}`,
method: 'POST',
headers: {
[ARGV_ACTION]: 'send',
[ARGV_SESSION_ID]: this.id,
[ARGV_SESSION_SEQ]: String(this.send_seq++),
'content-type': CONTENT_TYPE.BINARY
}
}, (response) => {
var code = response.statusCode;
if (code !== 200) {
response.once('data', (chunk) => {
log('warning', `ClientSession.Send: the http status code of server response is not 200. Code: ${code}, Data: ${chunk.hexSlice()}, Message: ${String(chunk)}.`);
});
//this.closed = true;
return;
}
});
while (this.send_list.length > 0) {
let chunk = this.send_list.shift();
request.write(chunk);
}
request.end();
}
recver(callback) {
if ((typeof callback) !== 'function') callback = () => { };
if ((!this.id) || this.closed) {
callback((new Error('no session!')), null);
return false;
}
var called = false;
var request = this.request({
host: this.server.hostname,
port: this.server.port,
path: `/${this.server.path}`,
method: 'GET',
headers: {
[ARGV_ACTION]: 'recv',
[ARGV_SESSION_ID]: this.id
}
}, (response) => {
var code = response.statusCode;
switch (code) {
case 200:
let ed = 0;
let seq = BigInt(response.headers[ARGV_SESSION_SEQ]);
if (!this.recv_list[seq]) this.recv_list[seq] = [];
let list = this.recv_list[seq];
response.on('data', (chunk) => {
if (ed) return;
list.push(chunk);
});
response.once('end', () => {
ed = 1;
list.push(Buffer.from([]));
});
break;
case 204:
break;
default:
response.once('data', (chunk) => {
log('warning', `ClientSession.Recv: the http status code of server response is not 200. Code: ${code}, Data: ${chunk.hexSlice()}, Message: ${String(chunk)}.`);
});
this.closed = true;
callback((new Error('bad http status code.')))
return;
}
called = true;
callback(null);
});
{
let er = (err) => {
if (called) return;
called = true;
callback(err);
};
request.on('close', er);
request.on('end', er);
request.on('error', er);
}
request.end();
return true;
}
end() {
if ((!this.id) || this.closed) throw (new Error('no session!'));
var request = this.request({
host: this.server.hostname,
port: this.server.port,
path: `/${this.server.path}`,
method: 'GET',
headers: {
[ARGV_ACTION]: 'end',
[ARGV_SESSION_ID]: this.id
}
}, (response) => {
var code = response.statusCode;
if (code !== 200) {
response.once('data', (chunk) => {
log('warning', `ClientSession.New: the http status code of server response is not 200. Code: ${code}, Data: ${chunk.hexSlice()}, Message: ${String(chunk)}.`);
});
return;
}
});
request.end();
}
close() {
this.end();
this.closed = true;
}
};
const Server = class {
listen = {
type: 'httpilltun',
// [const] hostname: '127.0.0.1',
port: 8008,
/*
== the format of path ==
# the character must in the following range. and no other character are allowed.
#* 0-9
#* a-z
#* A-Z
# recommended change it to a random value and length 30 at least.
#* can randomly press the keyboard to generate a random value.
*/
path: 'httpilltun'
};
sessions = {}; // $id : Session{}
socket = null; // http.Server{}
listening(callback) {
// path check.
for (let i of this.listen.path) {
if (CHARS.indexOf(i) === -1)
throw (new Error('Server.Listen: have a invalid character in the path.'));
}
var socket = http.createServer((...args) => this.handler(...args));
socket.listen({
host: '127.0.0.1',
port: this.listen.port,
backlog: 10,
ipv6Only: false
}, callback);
this.socket = socket;
}
handler(request, response) {
var path = url.parse(request.url).pathname;
if (path !== `/${this.listen.path}`) {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(404, head);
response.end('404 Not Found');
return;
}
if (request.method === 'HEAD') {
response.writeHead(204);
response.end();
return;
}
var action = (request.headers[ARGV_ACTION] || '').toLowerCase();
var id = null;
var session = null;
switch (action) {
case 'new':
let new_session = (new ServerSession());
new_session.network.hostname = request.headers[ARGV_PROXY_NETWORK_HOSTNAME];
if (!new_session.network.hostname) {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(400, head);
response.end('Error.Client.httpilltun.Proxy.Network.Hostname: it is empty!');
return;
}
new_session.network.type = (request.headers[ARGV_PROXY_NETWORK_TYPE] || '').toLowerCase();
if (!new_session.network.type) {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(400, head);
response.end('Error.Client.httpilltun.Proxy.Network.Type: it is empty!');
return;
}
switch (new_session.network.type) {
case 'ipv4':
if (!net.isIPv4(new_session.network.hostname)) {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(400, head);
response.end(`Error.UserInput.httpilltun.Proxy.Network.Address: the actual type of network layer hostname is not $${ARGV_PROXY_NETWORK_TYPE}`);
return;
}
break;
case 'ipv6':
if (!net.isIPv6(new_session.network.hostname)) {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(400, head);
response.end(`Error.UserInput.httpilltun.Proxy.Network.Address: the actual type of network layer hostname is not $${ARGV_PROXY_NETWORK_TYPE}`);
return;
}
break;
case 'domain':
break;
default:
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(400, head);
response.end('Error.Client.httpilltun.Proxy.Network.Type: the type of network layer is unsupported.');
return;
}
new_session.transport.type = (request.headers[ARGV_PROXY_TRANSPORT_TYPE] || '').toLowerCase();
if (!new_session.transport.type) {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(400, head);
response.end('Error.Client.httpilltun.Proxy.Transport.Type: it is empty!');
return;
}
switch (new_session.transport.type) {
case 'tcp':
break;
case 'udp':
break;
default:
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(400, head);
response.end('Error.Client.httpilltun.Proxy.Transport.Type: the type of transport layer is unsupported.');
return;
}
new_session.transport.port = Number(request.headers[ARGV_PROXY_TRANSPORT_PORT]);
if (
(!new_session.transport.port) ||
Number.isNaN(new_session.transport.port) ||
(!Number.isSafeInteger(new_session.transport.port)) ||
new_session.transport.port <= 0 ||
new_session.transport.port > 65535
) {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(400, head);
response.end('Error.Client.httpilltun.Proxy.Transport.Port: the port of transport layer is invalid.');
return;
}
let new_id = this.new_id();
new_session.id = new_id;
new_session.callback = (error, upstream) => {
if (error) {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(500, head);
response.end(`Error.Server.httpilltun.Proxy.Remote: cannot connect to remote.\n${String(error)}`);
throw error;
}
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
head[ARGV_SESSION_ID] = new_id;
response.writeHead(200, head);
response.end('Info.Server.httpilltun.Proxy.Remote: successfully connected to remote!');
};
new_session.connect(); // try connect to remote.
this.sessions[new_id] = new_session;
break;
case 'send':
if (request.method !== 'POST') {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(400, head);
response.end('Error.Client.HTTP.Request.Method: POST is needed.');
return;
}
id = request.headers[ARGV_SESSION_ID];
if (!id) {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(400, head);
response.end(`Error.Client.httpilltun.Session.ID: session id (header: ${ARGV_SESSION_ID}) is empty!`);
return;
}
session = this.sessions[id];
if (!session) {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(400, head);
response.end('Error.Client.httpilltun.Session.ID: session is not found.');
return;
}
if (session.closed) {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(500, head);
response.end('Warning.Server.httpilltun.Proxy.Remote: remote socket is closed.');
return;
}
let seq = request.headers[ARGV_SESSION_SEQ];
if (!seq) {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(400, head);
response.end('Error.Client.httpilltun.Session.Seq: it is empty!');
}
try {
seq = BigInt(seq);
} catch (e) {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(400, head);
response.end('Error.Client.httpilltun.Session.Seq: it is invalid!');
}
if (!session.send_list[seq]) session.send_list[seq] = [];
let list = session.send_list[seq];
var ed = 0;
request.on('data', (chunk) => {
if (ed) return;
list.push(chunk);
});
request.once('end', () => {
ed = 1;
list.push(Buffer.from([]));
let head = HEADERS_SERVER();
response.writeHead(200, head);
response.end();
});
break;
case 'recv':
id = request.headers[ARGV_SESSION_ID];
if (!id) {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(400, head);
response.end(`Error.Client.httpilltun.Session.ID: session id (header: ${ARGV_SESSION_ID}) is empty!`);
return;
}
session = this.sessions[id];
if (!session) {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(400, head);
response.end('Error.Client.httpilltun.Session.ID: session is not found.');
return;
}
{
let time = Date.now();
let timeout = 10 * 1000;
let er = () => {
if (session.recv_list.length > 0) {
let data = [];
while (session.recv_list.length > 0) {
let chunk = session.recv_list.shift();
data.push(chunk);
}
data = Buffer.concat(data);
let head = HEADERS_SERVER();
head[ARGV_SESSION_SEQ] = String(session.recv_seq++);
head['content-type'] = CONTENT_TYPE.BINARY;
head['content-length'] = String(data.length);
response.writeHead(200, head);
response.write(data);
response.end();
return;
} else {
if (session.closed) {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(500, head);
response.end('Warning.Server.httpilltun.Proxy.Remote: remote socket is closed.');
return;
}
}
if ((Date.now() - time) > timeout) {
let head = HEADERS_SERVER();
response.writeHead(204, head);
response.end();
return;
}
setTimeout(er, 0);
};
er();
}
break;
case 'end':
id = request.headers[ARGV_SESSION_ID];
if (!id) {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(400, head);
response.end(`Error.Client.httpilltun.Session.ID: session id (header: ${ARGV_SESSION_ID}) is empty!`);
return;
}
session = this.sessions[id];
if (!session) {
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(400, head);
response.end('Error.Client.httpilltun.Session.ID: session is not found.');
return;
}
// close remote connection.
try { session.close(); } catch (e) { }
let head = HEADERS_SERVER();
response.writeHead(200, head);
response.end();
break;
default:
{
let head = HEADERS_SERVER();
head['content-type'] = CONTENT_TYPE.TEXT;
response.writeHead(400, head);
}
response.end('Error.Client.httpilltun.Action: it is invalid!');
return;
}
}
// can to start or stop this server by calling the following functions.
start(callback) {
if (this.socket) return false;
this.listening(callback);
return true;
}
stop() {
if (!this.socket) return false;
try { this.socket.destroy(); } catch (e) { }
this.socket = null;
return true;
}
// generate id and anti-repetition.
new_id() {
var id = null;
do {
id = generate_id();
} while (this.sessions[id]);
return id;
}
};
/*
== Connection Map ==
=== Recommended ===
# [httpilltun Client] <===> HTTPS Connection <===> [HTTP Reverse Proxy] <===> Localhost HTTP Connection <===> [httpilltun Server]
# [httpilltun Client] <===> HTTPS Connection <===> [CDN] <===> HTTPS Connection ===> [Nginx or other HTTP reverse proxy] <===> Localhost HTTP Connection <===> [httpilltun Server]
=== Not Recommended ===
# (cannot anti active probing) [httpilltun Client] <===> HTTPS Connection <===> [httpilltun Server]
# [httpilltun Client] <===> HTTPS Connection <===> [CDN] <===> HTTP Connection ===> [Nginx or other HTTP reverse proxy] <===> HTTP Connection <===> [httpilltun Server]
# [httpilltun Client] <===> HTTPS Connection <===> [CDN] <===> HTTP Connection <===> [httpilltun Server]
=== STRONGLY NOT RECOMMENDED ===
# (INSECURE!) [httpilltun Client] <===> HTTP Connection <===> [httpilltun Server]
# (INSECURE!) [httpilltun Client] <===> HTTP Connection <===> {ANY} <===> HTTP Connection <===> [httpilltun Server]
# (INSECURE!) [httpilltun Client] <===> HTTP Connection <===> {ANY} <===> HTTPS Connection <===> [httpilltun Server]
== Config Example ==
=== Nginx ===
<pre>
server {
listen 0.0.0.0:443 ssl;
listen [::]:443 ssl;
ssl_certificate /path/to/tls/certificate.file;
ssl_certificate_key /path/to/tls/key.file;
location = /PathToHttpilltunServer {
# httpilltun server listening address.
proxy_pass http://127.0.0.1:8008;
}
}
</pre>
*/
const Client = class {
listen = {
type: 'socks',
hostname: '127.0.0.1',
port: 2080
};
server = {
type: 'httpilltun',
hostname: 'localhost',
port: 8008,
path: 'httpilltun'
};
sessions = {}; // $id : Session{}
listening(callback) {
this.listening_socks(callback);
}
listening_socks(callback) {
var socket = net.createServer((...args) => void this.parser(...args));
socket.listen({
host: this.listen.hostname,
port: this.listen.port,
backlog: 10,
ipv6Only: false
}, callback);
this.socket = socket;
}
parser(connection) {
var session = this.create_session();
session.server = { ... (this.server) };
var handle = () => {
this.handler({ accept, reject, session, connection });
}
var accept = null;
var reject = null;
connection.once('data', (request) => {
switch (request[0]) {
case 0x04: // socks4.
accept = () => connection.write(Buffer.from([0x00, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));
reject = () => connection.write(Buffer.from([0x00, 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));
session.network.type = 'ipv4';
session.transport.type = 'tcp';
switch (request[1]) {
case 0x01: // command: connect
{ // get port.
let port = request.slice(2, 4);
if (port.length !== 2) { // invalid port.
connection.destroy();
return;
}
session.transport.port = parseInt(port.hexSlice(), 16);
};
{ // get ip.
let ip = request.slice(4, 8); // get ip address
if (ip.length !== 4) { // invalid ip.
connection.destroy();
return;
}
session.network.hostname = ip.join('.');
};
if (session.network.hostname.startsWith('0.0.0.')) { // socks4a.
let tmp = request.slice(8);
/* remove username */
let count = 0;
for (let i of tmp) {
++count;
if (i === 0x00) break;
}
tmp = tmp.slice(count);
/* get domain name */
let name = [];
for (let i of tmp) {
if (i === 0x00) break;
name.push(i);
}
name = Buffer.from(name);
/* set domain name */
session.network.type = 'domain';
session.network.hostname = String(name);
}
log('debug', 'sock4 hostname:', JSON.stringify(session.network.hostname));
break;
default: // cannot handle other command.
connection.destroy();
return;
}
handle();
break;
case 0x05: // socks5.
accept = () => connection.write(Buffer.from([0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));
reject = () => connection.write(Buffer.from([0x05, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));
connection.write(Buffer.from([0x05, 0x00])); // no auth.
connection.once('data', (request) => {
if (request[0] !== 0x05) { // invalid request handle: close connection.
connection.destroy();
return;
}
switch (request[1]) {
case 0x01: // tcp.
session.transport.type = 'tcp';
break;
case 0x03: // udp.
session.transport.type = 'udp';
break;
default: // cannot handle other transport layer protocol.
connection.destroy();
return;
}
switch (request[3]) {
case 0x01: // ipv4.
session.network.type = 'ipv4';
{ // get ip.
let ip = request.slice(4, 8);
if (ip.length !== 4) { // invalid ip.
connection.destroy();
return;
}
session.network.hostname = ip.join('.');
};
{ // get port.
let port = request.slice(8, 10);
if (port.length !== 2) { // invalid port.
connection.destroy();
return;
}
session.transport.port = parseInt(port.hexSlice(), 16);
};
break;
case 0x04: // ipv6.
session.network.type = 'ipv6';
{ // get ip.
let tmp = request.slice(4, 20);
if (tmp.length !== 16) { // invalid ip.
connection.destroy();
return;
}
let ip = [];
while (tmp.length > 0) {
ip.push(tmp.slice(0, 2).hexSlice().toLowerCase());
tmp = tmp.slice(2);
}
session.network.hostname = ip.join(':');
};
{ // get port.
let port = request.slice(20, 22);
if (port.length !== 2) { // invalid port.
connection.destroy();
return;
}
session.transport.port = parseInt(port.hexSlice(), 16);
};
break;
case 0x03: // domain.
session.network.type = 'domain';
let name_end = 0;
{ // get domain.
let name = request.slice(4);
let len = name[0];
name = name.slice(1);
name = name.slice(0, len);
name_end = (4 + 1 + len); // prefix length (4 bytes) + name point length (1 byte) + name length (${len} bytes)
session.network.hostname = String(name);
};
{ // get port.
let port = request.slice(name_end, name_end + 2);
if (port.length !== 2) { // invalid port.
connection.destroy();
return;
}
session.transport.port = parseInt(port.hexSlice(), 16);
};
break;
default: // cannot handle other network layer protocol.
connection.destroy();
return;
}
handle();
});
break;
default: // maybe is http proxy request?
connection.write('HTTP/1.0 400 Bad Request\r\nContent-Type: text/plain\r\n\r\n== 400 Bad Request ==\r\n\r\n* This is a socks proxy.\r\n* This is not a web server.\r\n* This is not a http proxy.\r\n');
connection.destroy();
return;
}
});
}
create_session() {
var session = (new ClientSession());
return session;
}
handler(obj) {
var { accept, reject, connection, session } = obj;
this.session_operation(session);
session.new((err, res) => {
if (err) {
session.close();
reject();
throw err;
}
{
let closer = () => {
if (session.closed) return;
session.end();
session.close();
};
connection.on('close', closer);
connection.on('end', closer);
connection.on('error', closer);
}
// sender
connection.on('data', (chunk) => {
if (session.closed) return;
session.send(chunk);
});
// recver
{
let er = () => {
if (session.closed) return;
session.recv((err, chunk) => {
setTimeout(er, 0);
if (err) throw err;
connection.write(chunk);
});
};
er();
}
// updater
{
let interval = 0;
let update = () => {
if (session.closed) {
try { connection.destroy(); } catch (e) { }
return;
}
setTimeout(update, interval);
};
update();
}
// accept socks.
accept();
});
}
start(callback) {
this.closed = false;
this.listening(callback);
}
stop() {
this.closed = true;
this.socket.destroy();
}
};
const BrowserClientSession = class extends ClientSession {
ws_connection = null;
ws_id = null;
ws_seq = 0n;
request(options, callback) {
var info = {};
info.type = 'request';
info.mark = `${this.id}/${this.seq++}`;
info.url = `https://${options.host}:${options.port}${options.path}`;
info.method = options.method;
info.headers = options.headers;
info.body = []; // Uint8Array
var ed = 0;
return {
write: (chunk) => {
if (ed) return;
for (let it of chunk) {
info.body.push(it);
}
},
end: () => {
ed = 1;
var info_json = JSON.stringify(info);
this.connection.send(info_json);
var ws_recv = (message) => {
var reinfo_json = message.utf8Data;
var reinfo = JSON.parse(reinfo_json);
if (reinfo.mark !== info.mark) return;
this.connection.removeListener('message', ws_recv);
var response = {};
response.statusCode = reinfo.code;
response.headers = reinfo.headers;
response.on = (event, callback) => {
if (event !== 'data') return;
setTimeout(() => {
callback(Buffer.from(reinfo.body));
}, 0);
return response;
};
response.once = (event, callback) => {
if (event !== 'end') return;
setTimeout(() => {
callback();
}, 0);
return response;
};
response.end = () => {
return response;
};
callback(response);
};
this.connection.on('message', ws_recv);
}
};
}
on() {
this.connection.on('message', (message) => {
var json = message.utf8Data;
var reinfo = JSON.parse(json);
if (reinfo.seq.split('/')[0] !== this.id) { }
});
}
}
const BrowserClient = class extends Client {
ws_connection = null; // a websocket connection from the browser.
ws_id = 0n;
listening(callback) {
this.listening_socks(callback);
this.listening_websocket(callback);
}
listening_websocket(callback) {
if ((typeof callback) !== 'function') callback = () => { };
var http_root = `${__dirname}/bc/`;
var http_type = {
'.html': CONTENT_TYPE.HTML,
'.js': CONTENT_TYPE.JS
};
var http_server = http.createServer((request, response) => {
var file = url.parse(request.url).pathname;
if (file === '/') {
response.writeHead(302, { 'location': '/index.html' });
response.end();
return;
}
var final = (`.${file.split('.').pop()}`).toLowerCase();
var type = http_type[final];
if (!type) {
response.writeHead(500);
response.end('file type not found.');
return;
}
fs.readFile(`${http_root}/${file}`, (err, data) => {
if (err) {
response.writeHead(500);
response.end(String(err));
return;
}
response.writeHead(200, { 'content-type': type });
response.end(data);
});
});
var ws_server = (new websocket.server({
httpServer: http_server,
autoAcceptConnections: false
}));
ws_server.on('request', (request) => {
var path = url.parse(request.httpRequest.url).pathname;
if (path !== '/ws') {
request.reject();
return;
}
this.websocket_connection = request.accept();
});
http_server.listen({
host: '127.0.0.1',
port: 9888
}, () => {
var info = http_server.address();
log('notice', `please use a browser to visit this url: http://${info.address}:${info.port}/`)
callback();
});
}
create_session() {
var session = (new BrowserClientSession());
session.connection = this.connection;
session.ws_id = String(this.ws_id++);
session.on();
return session;
}
}
// test...
bc = new BrowserClient();
bc.start();
// end
module.exports = { Client, Server };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment