@letticdo/bare-server-node-tomphttp
Version:
The Bare Server implementation in NodeJS.
369 lines • 13.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const AbstractMessage_js_1 = require("./AbstractMessage.js");
const BareServer_js_1 = require("./BareServer.js");
const headerUtil_js_1 = require("./headerUtil.js");
const requestUtil_js_1 = require("./requestUtil.js");
const splitHeaderUtil_js_1 = require("./splitHeaderUtil.js");
const headers_polyfill_1 = require("headers-polyfill");
const validProtocols = ['http:', 'https:', 'ws:', 'wss:'];
const forbiddenForwardHeaders = [
'connection',
'transfer-encoding',
'host',
'connection',
'origin',
'referer',
];
const forbiddenPassHeaders = [
'vary',
'connection',
'transfer-encoding',
'access-control-allow-headers',
'access-control-allow-methods',
'access-control-expose-headers',
'access-control-max-age',
'access-control-request-headers',
'access-control-request-method',
];
// common defaults
const defaultForwardHeaders = [
'accept-encoding',
'accept-language',
'sec-websocket-extensions',
'sec-websocket-key',
'sec-websocket-version',
];
const defaultPassHeaders = [
'content-encoding',
'content-length',
'last-modified',
];
// defaults if the client provides a cache key
const defaultCacheForwardHeaders = [
'if-modified-since',
'if-none-match',
'cache-control',
];
const defaultCachePassHeaders = ['cache-control', 'etag'];
const cacheNotModified = 304;
function loadForwardedHeaders(forward, target, request) {
for (const header of forward) {
if (request.headers.has(header)) {
target[header] = request.headers.get(header);
}
}
}
const splitHeaderValue = /,\s*/g;
function readHeaders(request) {
const remote = Object.setPrototypeOf({}, null);
const sendHeaders = Object.setPrototypeOf({}, null);
const passHeaders = [...defaultPassHeaders];
const passStatus = [];
const forwardHeaders = [...defaultForwardHeaders];
// should be unique
const cache = request.url.searchParams.has('cache');
if (cache) {
passHeaders.push(...defaultCachePassHeaders);
passStatus.push(cacheNotModified);
forwardHeaders.push(...defaultCacheForwardHeaders);
}
const headers = (0, splitHeaderUtil_js_1.joinHeaders)(request.headers);
for (const remoteProp of ['host', 'port', 'protocol', 'path']) {
const header = `x-bare-${remoteProp}`;
if (headers.has(header)) {
const value = headers.get(header);
switch (remoteProp) {
case 'port':
if (isNaN(parseInt(value))) {
throw new BareServer_js_1.BareError(400, {
code: 'INVALID_BARE_HEADER',
id: `request.headers.${header}`,
message: `Header was not a valid integer.`,
});
}
break;
case 'protocol':
if (!validProtocols.includes(value)) {
throw new BareServer_js_1.BareError(400, {
code: 'INVALID_BARE_HEADER',
id: `request.headers.${header}`,
message: `Header was invalid`,
});
}
break;
}
remote[remoteProp] = value;
}
else {
throw new BareServer_js_1.BareError(400, {
code: 'MISSING_BARE_HEADER',
id: `request.headers.${header}`,
message: `Header was not specified.`,
});
}
}
if (headers.has('x-bare-headers')) {
try {
const json = JSON.parse(headers.get('x-bare-headers'));
for (const header in json) {
const value = json[header];
if (typeof value === 'string') {
sendHeaders[header] = value;
}
else if (Array.isArray(value)) {
const array = [];
for (const val of value) {
if (typeof val !== 'string') {
throw new BareServer_js_1.BareError(400, {
code: 'INVALID_BARE_HEADER',
id: `bare.headers.${header}`,
message: `Header was not a String.`,
});
}
array.push(val);
}
sendHeaders[header] = array;
}
else {
throw new BareServer_js_1.BareError(400, {
code: 'INVALID_BARE_HEADER',
id: `bare.headers.${header}`,
message: `Header was not a String.`,
});
}
}
}
catch (error) {
if (error instanceof SyntaxError) {
throw new BareServer_js_1.BareError(400, {
code: 'INVALID_BARE_HEADER',
id: `request.headers.x-bare-headers`,
message: `Header contained invalid JSON. (${error.message})`,
});
}
else {
throw error;
}
}
}
else {
throw new BareServer_js_1.BareError(400, {
code: 'MISSING_BARE_HEADER',
id: `request.headers.x-bare-headers`,
message: `Header was not specified.`,
});
}
if (headers.has('x-bare-pass-status')) {
const parsed = headers.get('x-bare-pass-status').split(splitHeaderValue);
for (const value of parsed) {
const number = parseInt(value);
if (isNaN(number)) {
throw new BareServer_js_1.BareError(400, {
code: 'INVALID_BARE_HEADER',
id: `request.headers.x-bare-pass-status`,
message: `Array contained non-number value.`,
});
}
else {
passStatus.push(number);
}
}
}
if (headers.has('x-bare-pass-headers')) {
const parsed = headers.get('x-bare-pass-headers').split(splitHeaderValue);
for (let header of parsed) {
header = header.toLowerCase();
if (forbiddenPassHeaders.includes(header)) {
throw new BareServer_js_1.BareError(400, {
code: 'FORBIDDEN_BARE_HEADER',
id: `request.headers.x-bare-forward-headers`,
message: `A forbidden header was passed.`,
});
}
else {
passHeaders.push(header);
}
}
}
if (headers.has('x-bare-forward-headers')) {
const parsed = headers
.get('x-bare-forward-headers')
.split(splitHeaderValue);
for (let header of parsed) {
header = header.toLowerCase();
if (forbiddenForwardHeaders.includes(header)) {
throw new BareServer_js_1.BareError(400, {
code: 'FORBIDDEN_BARE_HEADER',
id: `request.headers.x-bare-forward-headers`,
message: `A forbidden header was forwarded.`,
});
}
else {
forwardHeaders.push(header);
}
}
}
return {
remote,
sendHeaders,
passHeaders,
passStatus,
forwardHeaders,
};
}
const tunnelRequest = async (request, res, options) => {
const abort = new AbortController();
request.body.on('close', () => {
if (!request.body.complete)
abort.abort();
});
res.on('close', () => {
abort.abort();
});
const { remote, sendHeaders, passHeaders, passStatus, forwardHeaders } = readHeaders(request);
loadForwardedHeaders(forwardHeaders, sendHeaders, request);
const response = await (0, requestUtil_js_1.fetch)(request, abort.signal, sendHeaders, remote, options);
const responseHeaders = new headers_polyfill_1.Headers();
for (const header of passHeaders) {
if (!(header in response.headers))
continue;
responseHeaders.set(header, (0, headerUtil_js_1.flattenHeader)(response.headers[header]));
}
const status = passStatus.includes(response.statusCode)
? response.statusCode
: 200;
if (status !== cacheNotModified) {
responseHeaders.set('x-bare-status', response.statusCode.toString());
responseHeaders.set('x-bare-status-text', response.statusMessage);
responseHeaders.set('x-bare-headers', JSON.stringify((0, headerUtil_js_1.mapHeadersFromArray)((0, headerUtil_js_1.rawHeaderNames)(response.rawHeaders), {
...response.headers,
})));
}
return new AbstractMessage_js_1.Response(response, {
status,
headers: (0, splitHeaderUtil_js_1.splitHeaders)(responseHeaders),
});
};
const metaExpiration = 30e3;
const getMeta = async (request, res, options) => {
if (request.method === 'OPTIONS') {
return new AbstractMessage_js_1.Response(undefined, { status: 200 });
}
if (!request.headers.has('x-bare-id')) {
throw new BareServer_js_1.BareError(400, {
code: 'MISSING_BARE_HEADER',
id: 'request.headers.x-bare-id',
message: 'Header was not specified',
});
}
const id = request.headers.get('x-bare-id');
const meta = await options.database.get(id);
if (meta?.value.v !== 2)
throw new BareServer_js_1.BareError(400, {
code: 'INVALID_BARE_HEADER',
id: 'request.headers.x-bare-id',
message: 'Unregistered ID',
});
if (!meta.value.response)
throw new BareServer_js_1.BareError(400, {
code: 'INVALID_BARE_HEADER',
id: 'request.headers.x-bare-id',
message: 'Meta not ready',
});
await options.database.delete(id);
const responseHeaders = new headers_polyfill_1.Headers();
responseHeaders.set('x-bare-status', meta.value.response.status.toString());
responseHeaders.set('x-bare-status-text', meta.value.response.statusText);
responseHeaders.set('x-bare-headers', JSON.stringify(meta.value.response.headers));
return new AbstractMessage_js_1.Response(undefined, {
status: 200,
headers: (0, splitHeaderUtil_js_1.splitHeaders)(responseHeaders),
});
};
const newMeta = async (request, res, options) => {
const { remote, sendHeaders, forwardHeaders } = readHeaders(request);
const id = (0, requestUtil_js_1.randomHex)(16);
await options.database.set(id, {
expires: Date.now() + metaExpiration,
value: {
v: 2,
remote,
sendHeaders,
forwardHeaders,
},
});
return new AbstractMessage_js_1.Response(Buffer.from(id));
};
const tunnelSocket = async (request, socket, head, options) => {
const abort = new AbortController();
request.body.on('close', () => {
if (!request.body.complete)
abort.abort();
});
socket.on('close', () => {
abort.abort();
});
if (!request.headers.has('sec-websocket-protocol')) {
socket.end();
return;
}
const id = request.headers.get('sec-websocket-protocol');
const meta = await options.database.get(id);
if (meta?.value.v !== 2) {
socket.end();
return;
}
loadForwardedHeaders(meta.value.forwardHeaders, meta.value.sendHeaders, request);
const [remoteResponse, remoteSocket] = await (0, requestUtil_js_1.upgradeFetch)(request, abort.signal, meta.value.sendHeaders, meta.value.remote, options);
remoteSocket.on('close', () => {
socket.end();
});
socket.on('close', () => {
remoteSocket.end();
});
remoteSocket.on('error', (error) => {
if (options.logErrors) {
console.error('Remote socket error:', error);
}
socket.end();
});
socket.on('error', (error) => {
if (options.logErrors) {
console.error('Serving socket error:', error);
}
remoteSocket.end();
});
const remoteHeaders = new headers_polyfill_1.Headers(remoteResponse.headers);
meta.value.response = {
headers: (0, headerUtil_js_1.mapHeadersFromArray)((0, headerUtil_js_1.rawHeaderNames)(remoteResponse.rawHeaders), {
...remoteResponse.headers,
}),
status: remoteResponse.statusCode,
statusText: remoteResponse.statusMessage,
};
await options.database.set(id, meta);
const responseHeaders = [
`HTTP/1.1 101 Switching Protocols`,
`Upgrade: websocket`,
`Connection: Upgrade`,
`Sec-WebSocket-Protocol: ${id}`,
];
if (remoteHeaders.has('sec-websocket-extensions')) {
responseHeaders.push(`Sec-WebSocket-Extensions: ${remoteHeaders.get('sec-websocket-extensions')}`);
}
if (remoteHeaders.has('sec-websocket-accept')) {
responseHeaders.push(`Sec-WebSocket-Accept: ${remoteHeaders.get('sec-websocket-accept')}`);
}
socket.write(responseHeaders.concat('', '').join('\r\n'));
remoteSocket.pipe(socket);
socket.pipe(remoteSocket);
};
function registerV2(server) {
server.routes.set('/v2/', tunnelRequest);
server.routes.set('/v2/ws-new-meta', newMeta);
server.routes.set('/v2/ws-meta', getMeta);
server.socketRoutes.set('/v2/', tunnelSocket);
}
exports.default = registerV2;
//# sourceMappingURL=V2.js.map