Created
August 27, 2020 14:26
-
-
Save BlueSedDragon/a0daf2c785fc4af43292f8a34f6c4007 to your computer and use it in GitHub Desktop.
httpilltun.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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