mirror of
https://github.com/QuiteAFancyEmerald/Holy-Unblocker.git
synced 2025-05-15 21:00:00 -04:00
384 lines
9.1 KiB
JavaScript
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);
|
|
}
|