305 lines
11 KiB
JavaScript
305 lines
11 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const headers_polyfill_1 = require("headers-polyfill");
|
|
const AbstractMessage_js_1 = require("./AbstractMessage.js");
|
|
const BareServer_js_1 = require("./BareServer.js");
|
|
const headerUtil_js_1 = require("./headerUtil.js");
|
|
const remoteUtil_js_1 = require("./remoteUtil.js");
|
|
const requestUtil_js_1 = require("./requestUtil.js");
|
|
const splitHeaderUtil_js_1 = require("./splitHeaderUtil.js");
|
|
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'];
|
|
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 sendHeaders = Object.create(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);
|
|
const xBareURL = headers.get('x-bare-url');
|
|
if (xBareURL === null)
|
|
throw new BareServer_js_1.BareError(400, {
|
|
code: 'MISSING_BARE_HEADER',
|
|
id: `request.headers.x-bare-url`,
|
|
message: `Header was not specified.`,
|
|
});
|
|
const remote = (0, remoteUtil_js_1.urlToRemote)(new URL(xBareURL));
|
|
const xBareHeaders = headers.get('x-bare-headers');
|
|
if (xBareHeaders === null)
|
|
throw new BareServer_js_1.BareError(400, {
|
|
code: 'MISSING_BARE_HEADER',
|
|
id: `request.headers.x-bare-headers`,
|
|
message: `Header was not specified.`,
|
|
});
|
|
try {
|
|
const json = JSON.parse(xBareHeaders);
|
|
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;
|
|
}
|
|
}
|
|
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: (0, remoteUtil_js_1.remoteToURL)(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),
|
|
});
|
|
};
|
|
function readSocket(socket) {
|
|
return new Promise((resolve, reject) => {
|
|
const messageListener = (event) => {
|
|
cleanup();
|
|
if (typeof event.data !== 'string')
|
|
return reject(new TypeError('the first websocket message was not a text frame'));
|
|
try {
|
|
resolve(JSON.parse(event.data));
|
|
}
|
|
catch (err) {
|
|
reject(err);
|
|
}
|
|
};
|
|
const closeListener = () => {
|
|
cleanup();
|
|
};
|
|
const cleanup = () => {
|
|
socket.removeEventListener('message', messageListener);
|
|
socket.removeEventListener('close', closeListener);
|
|
clearTimeout(timeout);
|
|
};
|
|
const timeout = setTimeout(() => {
|
|
cleanup();
|
|
reject(new Error('Timed out before metadata could be read'));
|
|
}, 10e3);
|
|
socket.addEventListener('message', messageListener);
|
|
socket.addEventListener('close', closeListener);
|
|
});
|
|
}
|
|
const tunnelSocket = async (request, socket, head, options) => options.wss.handleUpgrade(request.body, socket, head, async (client) => {
|
|
let _remoteSocket;
|
|
try {
|
|
const connectPacket = await readSocket(client);
|
|
if (connectPacket.type !== 'connect')
|
|
throw new Error('Client did not send open packet.');
|
|
loadForwardedHeaders(connectPacket.forwardHeaders, connectPacket.headers, request);
|
|
const [remoteReq, remoteSocket] = await (0, requestUtil_js_1.webSocketFetch)(request, connectPacket.headers, new URL(connectPacket.remote), connectPacket.protocols, options);
|
|
_remoteSocket = remoteSocket;
|
|
const setCookieHeader = remoteReq.headers['set-cookie'];
|
|
const setCookies = setCookieHeader !== undefined
|
|
? Array.isArray(setCookieHeader)
|
|
? setCookieHeader
|
|
: [setCookieHeader]
|
|
: [];
|
|
client.send(JSON.stringify({
|
|
type: 'open',
|
|
protocol: remoteSocket.protocol,
|
|
setCookies,
|
|
}),
|
|
// use callback to wait for this message to buffer and finally send before doing any piping
|
|
// otherwise the client will receive a random message from the remote before our open message
|
|
() => {
|
|
remoteSocket.addEventListener('message', (event) => {
|
|
client.send(event.data);
|
|
});
|
|
client.addEventListener('message', (event) => {
|
|
remoteSocket.send(event.data);
|
|
});
|
|
remoteSocket.addEventListener('close', () => {
|
|
client.close();
|
|
});
|
|
client.addEventListener('close', () => {
|
|
remoteSocket.close();
|
|
});
|
|
remoteSocket.addEventListener('error', (error) => {
|
|
if (options.logErrors) {
|
|
console.error('Remote socket error:', error);
|
|
}
|
|
client.close();
|
|
});
|
|
client.addEventListener('error', (error) => {
|
|
if (options.logErrors) {
|
|
console.error('Serving socket error:', error);
|
|
}
|
|
remoteSocket.close();
|
|
});
|
|
});
|
|
}
|
|
catch (err) {
|
|
if (options.logErrors)
|
|
console.error(err);
|
|
client.close();
|
|
if (_remoteSocket)
|
|
_remoteSocket.close();
|
|
}
|
|
});
|
|
function registerV3(server) {
|
|
server.routes.set('/v3/', tunnelRequest);
|
|
server.socketRoutes.set('/v3/', tunnelSocket);
|
|
server.versions.push('v3');
|
|
}
|
|
exports.default = registerV3;
|
|
//# sourceMappingURL=V3.js.map
|