Holy-Unblocker/bare/V1.mjs
2022-02-25 22:56:50 -08:00

384 lines
9.1 KiB
JavaScript

import http from 'node:http';
import https from 'node:https';
import { MapHeaderNamesFromArray, RawHeaderNames } from './HeaderUtil.mjs';
import { decode_protocol } from './EncodeProtocol.mjs';
import { Response } from './Response.mjs';
import { randomBytes } from 'node:crypto';
import { promisify } from 'node:util';
const randomBytesAsync = promisify(randomBytes);
// max of 4 concurrent sockets, rest is queued while busy? set max to 75
// const http_agent = http.Agent();
// const https_agent = https.Agent();
async function Fetch(server_request, request_headers, url){
const options = {
host: url.host,
port: url.port,
path: url.path,
method: server_request.method,
headers: request_headers,
};
let outgoing;
if(url.protocol === 'https:'){
outgoing = https.request(options);
}else if(url.protocol === 'http:'){
outgoing = http.request(options);
}else{
throw new RangeError(`Unsupported protocol: '${url.protocol}'`);
}
server_request.pipe(outgoing);
return await new Promise((resolve, reject) => {
outgoing.on('response', resolve);
outgoing.on('error', reject);
});
}
function load_forwarded_headers(request, forward, target){
const raw = RawHeaderNames(request.rawHeaders);
for(let header of forward){
for(let cap of raw){
if(cap.toLowerCase() == header){
// header exists and real capitalization was found
target[cap] = request.headers[header];
}
}
}
}
function read_headers(server_request, request_headers){
const remote = Object.setPrototypeOf({}, null);
const headers = Object.setPrototypeOf({}, null);
for(let remote_prop of ['host','port','protocol','path']){
const header = `x-bare-${remote_prop}`;
if(header in request_headers){
let value = request_headers[header];
if(remote_prop === 'port'){
value = parseInt(value);
if(isNaN(value))return {
error: {
code: 'INVALID_BARE_HEADER',
id: `request.headers.${header}`,
message: `Header was not a valid integer.`,
},
};
}
remote[remote_prop] = value;
}else{
return {
error: {
code: 'MISSING_BARE_HEADER',
id: `request.headers.${header}`,
message: `Header was not specified.`,
},
};
}
}
if('x-bare-headers' in request_headers){
let json;
try{
json = JSON.parse(request_headers['x-bare-headers']);
for(let header in json){
if(typeof json[header] !== 'string' && !Array.isArray(json[header])){
return {
error: {
code: 'INVALID_BARE_HEADER',
id: `bare.headers.${header}`,
message: `Header was not a String or Array.`,
},
};
}
}
}catch(err){
return {
error: {
code: 'INVALID_BARE_HEADER',
id: `request.headers.${header}`,
message: `Header contained invalid JSON. (${err.message})`,
},
};
}
Object.assign(headers, json);
}else{
return {
error: {
code: 'MISSING_BARE_HEADER',
id: `request.headers.x-bare-headers`,
message: `Header was not specified.`,
},
};
}
if('x-bare-forward-headers' in request_headers){
let json;
try{
json = JSON.parse(request_headers['x-bare-forward-headers']);
}catch(err){
return {
error: {
code: 'INVALID_BARE_HEADER',
id: `request.headers.x-bare-forward-headers`,
message: `Header contained invalid JSON. (${err.message})`,
},
};
}
load_forwarded_headers(server_request, json, headers);
}else{
return {
error: {
code: 'MISSING_BARE_HEADER',
id: `request.headers.x-bare-forward-headers`,
message: `Header was not specified.`,
},
};
}
return { remote, headers };
}
export async function v1(server, server_request){
const response_headers = Object.setPrototypeOf({}, null);
response_headers['x-robots-tag'] = 'noindex';
response_headers['access-control-allow-headers'] = '*';
response_headers['access-control-allow-origin'] = '*';
response_headers['access-control-expose-headers'] = '*';
const { error, remote, headers } = read_headers(server_request, server_request.headers);
if(error){
// sent by browser, not client
if(server_request.method === 'OPTIONS'){
return new Response(undefined, 200, response_headers);
}else{
return server.json(400, error);
}
}
let response;
try{
response = await Fetch(server_request, headers, remote);
}catch(err){
if(err instanceof Error){
switch(err.code){
case'ENOTFOUND':
return server.json(500, {
code: 'HOST_NOT_FOUND',
id: 'request',
message: 'The specified host could not be resolved.',
});
case'ECONNREFUSED':
return server.json(500, {
code: 'CONNECTION_REFUSED',
id: 'response',
message: 'The remote rejected the request.',
});
case'ECONNRESET':
return server.json(500, {
code: 'CONNECTION_RESET',
id: 'response',
message: 'The request was forcibly closed.',
});
case'ETIMEOUT':
return server.json(500, {
code: 'CONNECTION_TIMEOUT',
id: 'response',
message: 'The response timed out.',
});
}
}
throw err;
}
for(let header in response.headers){
if(header === 'content-encoding' || header === 'x-content-encoding'){
response_headers['content-encoding'] = response.headers[header];
}else if(header === 'content-length'){
response_headers['content-length'] = response.headers[header];
}
}
response_headers['x-bare-headers'] = JSON.stringify(MapHeaderNamesFromArray(RawHeaderNames(response.rawHeaders), {...response.headers}));
response_headers['x-bare-status'] = response.statusCode
response_headers['x-bare-status-text'] = response.statusMessage;
return new Response(response, 200, response_headers);
}
// prevent users from specifying id=__proto__ or id=constructor
const temp_meta = Object.setPrototypeOf({}, null);
setInterval(() => {
for(let id in temp_meta){
if(temp_meta[id].expires < Date.now()){
delete temp_meta[id];
}
}
}, 1e3);
export async function v1wsmeta(server, server_request){
if(!('x-bare-id' in server_request.headers)){
return server.json(400, {
code: 'MISSING_BARE_HEADER',
id: 'request.headers.x-bare-id',
message: 'Header was not specified',
});
}
const id = server_request.headers['x-bare-id'];
if(!(id in temp_meta)){
return server.json(400, {
code: 'INVALID_BARE_HEADER',
id: 'request.headers.x-bare-id',
message: 'Unregistered ID',
});
}
const { meta } = temp_meta[id];
delete temp_meta[id];
return server.json(200, meta);
}
export async function v1wsnewmeta(server, server_request){
const id = (await randomBytesAsync(32)).toString('hex');
temp_meta[id] = {
expires: Date.now() + 30e3,
};
return new Response(Buffer.from(id.toString('hex')))
}
export async function v1socket(server, server_request, server_socket, server_head){
if(!server_request.headers['sec-websocket-protocol']){
server_socket.end();
return;
}
const [ first_protocol, data ] = server_request.headers['sec-websocket-protocol'].split(/,\s*/g);
if(first_protocol !== 'bare'){
server_socket.end();
return;
}
const {
remote,
headers,
forward_headers,
id,
} = JSON.parse(decode_protocol(data));
load_forwarded_headers(server_request, forward_headers, headers);
const options = {
host: remote.host,
port: remote.port,
path: remote.path,
headers,
method: server_request.method,
};
let request_stream;
let response_promise = new Promise((resolve, reject) => {
try{
if(remote.protocol === 'wss:'){
request_stream = https.request(options, res => {
reject(`Remote didn't upgrade the request`);
});
}else if(remote.protocol === 'ws:'){
request_stream = http.request(options, res => {
reject(`Remote didn't upgrade the request`);
});
}else{
return reject(new RangeError(`Unsupported protocol: '${remote.protocol}'`));
}
request_stream.on('upgrade', (...args) => {
resolve(args)
});
request_stream.on('error', reject);
request_stream.write(server_head);
request_stream.end();
}catch(err){
reject(err);
}
});
const [ response, socket, head ] = await response_promise;
if('id' in temp_meta){
if(typeof id !== 'string'){
socket.end();
return;
}
const meta = {
headers: MapHeaderNamesFromArray(RawHeaderNames(response.rawHeaders), {...response.headers}),
};
temp_meta[id].meta = meta;
}
const response_headers = [
`HTTP/1.1 101 Switching Protocols`,
`Upgrade: websocket`,
`Connection: Upgrade`,
`Sec-WebSocket-Protocol: bare`,
`Sec-WebSocket-Accept: ${response.headers['sec-websocket-accept']}`,
];
if('sec-websocket-extensions' in response.headers){
response_headers.push(`Sec-WebSocket-Extensions: ${response.headers['sec-websocket-extensions']}`);
}
server_socket.write(response_headers.concat('', '').join('\r\n'));
server_socket.write(head);
socket.on('close', () => {
// console.log('Remote closed');
server_socket.end();
});
server_socket.on('close', () => {
// console.log('Serving closed');
socket.end();
});
socket.on('error', err => {
server.error('Remote socket error:', err);
server_socket.end();
});
server_socket.on('error', err => {
server.error('Serving socket error:', err);
socket.end();
});
socket.pipe(server_socket);
server_socket.pipe(socket);
}