diff --git a/lib/bare-server-modified/AbstractMessage.d.ts b/lib/bare-server-modified/AbstractMessage.d.ts new file mode 100644 index 0000000..8a5dbba --- /dev/null +++ b/lib/bare-server-modified/AbstractMessage.d.ts @@ -0,0 +1,34 @@ +/// +/// +import type { IncomingMessage, ServerResponse } from 'node:http'; +import { Headers } from 'headers-polyfill'; +import type { BareHeaders } from './requestUtil.js'; +export interface RequestInit { + method: string; + path: string; + headers: Headers | BareHeaders; +} +/** + * Abstraction for the data read from IncomingMessage + */ +export declare class Request { + body: IncomingMessage; + method: string; + headers: Headers; + url: URL; + constructor(body: IncomingMessage, init: RequestInit); +} +export type ResponseBody = Buffer | IncomingMessage; +export interface ResponseInit { + headers?: Headers | BareHeaders; + status?: number; + statusText?: string; +} +export declare class Response { + body?: ResponseBody; + status: number; + statusText?: string; + headers: Headers; + constructor(body: ResponseBody | undefined, init?: ResponseInit); +} +export declare function writeResponse(response: Response, res: ServerResponse): boolean; diff --git a/lib/bare-server-modified/AbstractMessage.js b/lib/bare-server-modified/AbstractMessage.js new file mode 100644 index 0000000..47fc4db --- /dev/null +++ b/lib/bare-server-modified/AbstractMessage.js @@ -0,0 +1,61 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.writeResponse = exports.Response = exports.Request = void 0; +const node_stream_1 = require("node:stream"); +const headers_polyfill_1 = require("headers-polyfill"); +/** + * Abstraction for the data read from IncomingMessage + */ +class Request { + body; + method; + headers; + url; + constructor(body, init) { + this.body = body; + this.method = init.method; + this.headers = new headers_polyfill_1.Headers(init.headers); + // Parse the URL pathname. Host doesn't matter. + this.url = new URL(init.path, 'http://bare-server-node'); + } +} +exports.Request = Request; +class Response { + body; + status; + statusText; + headers; + constructor(body, init = {}) { + if (body) { + this.body = body instanceof node_stream_1.Stream ? body : Buffer.from(body); + } + if (typeof init.status === 'number') { + this.status = init.status; + } + else { + this.status = 200; + } + if (typeof init.statusText === 'string') { + this.statusText = init.statusText; + } + this.headers = new headers_polyfill_1.Headers(init.headers); + } +} +exports.Response = Response; +function writeResponse(response, res) { + for (const [header, value] of response.headers) + res.setHeader(header, value); + res.writeHead(response.status, response.statusText); + if (response.body instanceof node_stream_1.Stream) { + const { body } = response; + res.on('close', () => body.destroy()); + body.pipe(res); + } + else if (response.body instanceof Buffer) + res.end(response.body); + else + res.end(); + return true; +} +exports.writeResponse = writeResponse; +//# sourceMappingURL=AbstractMessage.js.map \ No newline at end of file diff --git a/lib/bare-server-modified/BareServer.d.ts b/lib/bare-server-modified/BareServer.d.ts new file mode 100644 index 0000000..aa2b28c --- /dev/null +++ b/lib/bare-server-modified/BareServer.d.ts @@ -0,0 +1,90 @@ +/// +/// +/// +/// +/// +/// +/// +import type { LookupOneOptions } from 'node:dns'; +import EventEmitter from 'node:events'; +import type { Agent as HttpAgent, IncomingMessage, ServerResponse } from 'node:http'; +import type { Agent as HttpsAgent } from 'node:https'; +import type { Duplex } from 'node:stream'; +import type WebSocket from 'ws'; +import { Request, Response } from './AbstractMessage.js'; +import type { JSONDatabaseAdapter } from './Meta.js'; +export interface BareErrorBody { + code: string; + id: string; + message?: string; + stack?: string; +} +export declare class BareError extends Error { + status: number; + body: BareErrorBody; + constructor(status: number, body: BareErrorBody); +} +export declare const pkg: { + version: string; +}; +export declare function json(status: number, json: T): Response; +export type BareMaintainer = { + email?: string; + website?: string; +}; +export type BareProject = { + name?: string; + description?: string; + email?: string; + website?: string; + repository?: string; + version?: string; +}; +export type BareLanguage = 'NodeJS' | 'ServiceWorker' | 'Deno' | 'Java' | 'PHP' | 'Rust' | 'C' | 'C++' | 'C#' | 'Ruby' | 'Go' | 'Crystal' | 'Shell' | string; +export type BareManifest = { + maintainer?: BareMaintainer; + project?: BareProject; + versions: string[]; + language: BareLanguage; + memoryUsage?: number; +}; +export interface Options { + logErrors: boolean; + /** + * Callback for filtering the remote URL. + * @returns Nothing + * @throws An error if the remote is bad. + */ + filterRemote?: (remote: Readonly) => Promise | void; + /** + * DNS lookup + * May not get called when remote.host is an IP + * Use in combination with filterRemote to block IPs + */ + lookup: (hostname: string, options: LookupOneOptions, callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void) => void; + localAddress?: string; + family?: number; + maintainer?: BareMaintainer; + httpAgent: HttpAgent; + httpsAgent: HttpsAgent; + database: JSONDatabaseAdapter; + wss: WebSocket.Server; +} +export type RouteCallback = (request: Request, response: ServerResponse, options: Options) => Promise | Response; +export type SocketRouteCallback = (request: Request, socket: Duplex, head: Buffer, options: Options) => Promise | void; +export default class Server extends EventEmitter { + routes: Map; + socketRoutes: Map; + versions: string[]; + private closed; + private directory; + private options; + /** + * Remove all timers and listeners + */ + close(): void; + shouldRoute(request: IncomingMessage): boolean; + get instanceInfo(): BareManifest; + routeUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): Promise; + routeRequest(req: IncomingMessage, res: ServerResponse): Promise; +} diff --git a/lib/bare-server-modified/BareServer.js b/lib/bare-server-modified/BareServer.js new file mode 100644 index 0000000..ce2d153 --- /dev/null +++ b/lib/bare-server-modified/BareServer.js @@ -0,0 +1,168 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.json = exports.pkg = exports.BareError = void 0; +const node_events_1 = __importDefault(require("node:events")); +const node_fs_1 = require("node:fs"); +const node_path_1 = require("node:path"); +const http_errors_1 = __importDefault(require("http-errors")); +const AbstractMessage_js_1 = require("./AbstractMessage.js"); +class BareError extends Error { + status; + body; + constructor(status, body) { + super(body.message || body.code); + this.status = status; + this.body = body; + } +} +exports.BareError = BareError; +exports.pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, '..', 'package.json'), 'utf-8')); +const project = { + name: 'bare-server-node', + description: 'TOMPHTTP NodeJS Bare Server', + repository: 'https://github.com/tomphttp/bare-server-node', + version: exports.pkg.version, +}; +function json(status, json) { + const send = Buffer.from(JSON.stringify(json, null, '\t')); + return new AbstractMessage_js_1.Response(send, { + status, + headers: { + 'content-type': 'application/json', + 'content-length': send.byteLength.toString(), + }, + }); +} +exports.json = json; +class Server extends node_events_1.default { + routes = new Map(); + socketRoutes = new Map(); + versions = []; + closed = false; + directory; + options; + /** + * @internal + */ + constructor(directory, options) { + super(); + this.directory = directory; + this.options = options; + } + /** + * Remove all timers and listeners + */ + close() { + this.closed = true; + this.emit('close'); + } + shouldRoute(request) { + return (!this.closed && + request.url !== undefined && + request.url.startsWith(this.directory)); + } + get instanceInfo() { + return { + versions: this.versions, + language: 'NodeJS', + memoryUsage: Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100, + maintainer: this.options.maintainer, + project, + }; + } + async routeUpgrade(req, socket, head) { + const request = new AbstractMessage_js_1.Request(req, { + method: req.method, + path: req.url, + headers: req.headers, + }); + const service = request.url.pathname.slice(this.directory.length - 1); + if (this.socketRoutes.has(service)) { + const call = this.socketRoutes.get(service); + try { + await call(request, socket, head, this.options); + } + catch (error) { + if (this.options.logErrors) { + console.error(error); + } + socket.end(); + } + } + else { + socket.end(); + } + } + async routeRequest(req, res) { + const request = new AbstractMessage_js_1.Request(req, { + method: req.method, + path: req.url, + headers: req.headers, + }); + const service = request.url.pathname.slice(this.directory.length - 1); + let response; + try { + if (request.method === 'OPTIONS') { + response = new AbstractMessage_js_1.Response(undefined, { status: 200 }); + } + else if (service === '/') { + response = json(200, this.instanceInfo); + } + else if (this.routes.has(service)) { + const call = this.routes.get(service); + response = await call(request, res, this.options); + } + else { + throw new http_errors_1.default.NotFound(); + } + } + catch (error) { + if (this.options.logErrors) + console.error(error); + if (http_errors_1.default.isHttpError(error)) { + response = json(error.statusCode, { + code: 'UNKNOWN', + id: `error.${error.name}`, + message: error.message, + stack: error.stack, + }); + } + else if (error instanceof Error) { + response = json(500, { + code: 'UNKNOWN', + id: `error.${error.name}`, + message: error.message, + stack: error.stack, + }); + } + else { + response = json(500, { + code: 'UNKNOWN', + id: 'error.Exception', + message: error, + stack: new Error(error).stack, + }); + } + if (!(response instanceof AbstractMessage_js_1.Response)) { + if (this.options.logErrors) { + console.error('Cannot', request.method, request.url.pathname, ': Route did not return a response.'); + } + throw new http_errors_1.default.InternalServerError(); + } + } + response.headers.set('x-robots-tag', 'noindex'); + response.headers.set('access-control-allow-headers', '*'); + response.headers.set('access-control-allow-origin', '*'); + response.headers.set('access-control-allow-methods', '*'); + response.headers.set('access-control-expose-headers', '*'); + // don't fetch preflight on every request... + // instead, fetch preflight every 10 minutes + response.headers.set('access-control-max-age', '7200'); + (0, AbstractMessage_js_1.writeResponse)(response, res); + } +} +exports.default = Server; +//# sourceMappingURL=BareServer.js.map \ No newline at end of file diff --git a/lib/bare-server-modified/Meta.d.ts b/lib/bare-server-modified/Meta.d.ts new file mode 100644 index 0000000..46f3c3b --- /dev/null +++ b/lib/bare-server-modified/Meta.d.ts @@ -0,0 +1,33 @@ +import type { BareHeaders } from './requestUtil'; +export interface MetaV1 { + v: 1; + response?: { + headers: BareHeaders; + }; +} +export interface MetaV2 { + v: 2; + response?: { + status: number; + statusText: string; + headers: BareHeaders; + }; + sendHeaders: BareHeaders; + remote: string; + forwardHeaders: string[]; +} +export default interface CommonMeta { + value: MetaV1 | MetaV2; + expires: number; +} +export interface Database { + get(key: string): string | undefined | PromiseLike; + set(key: string, value: string): unknown; + has(key: string): boolean | PromiseLike; + delete(key: string): boolean | PromiseLike; + entries(): IterableIterator<[string, string]> | PromiseLike>; +} +/** + * Routine + */ +export declare function cleanupDatabase(database: Database): Promise; diff --git a/lib/bare-server-modified/Meta.js b/lib/bare-server-modified/Meta.js new file mode 100644 index 0000000..c7886e0 --- /dev/null +++ b/lib/bare-server-modified/Meta.js @@ -0,0 +1,43 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.cleanupDatabase = exports.JSONDatabaseAdapter = void 0; +/** + * @internal + */ +class JSONDatabaseAdapter { + impl; + constructor(impl) { + this.impl = impl; + } + async get(key) { + const res = await this.impl.get(key); + if (typeof res === 'string') + return JSON.parse(res); + } + async set(key, value) { + return await this.impl.set(key, JSON.stringify(value)); + } + async has(key) { + return await this.impl.has(key); + } + async delete(key) { + return await this.impl.delete(key); + } + async *[Symbol.asyncIterator]() { + for (const [id, value] of await this.impl.entries()) { + yield [id, JSON.parse(value)]; + } + } +} +exports.JSONDatabaseAdapter = JSONDatabaseAdapter; +/** + * Routine + */ +async function cleanupDatabase(database) { + const adapter = new JSONDatabaseAdapter(database); + for await (const [id, { expires }] of adapter) + if (expires < Date.now()) + database.delete(id); +} +exports.cleanupDatabase = cleanupDatabase; +//# sourceMappingURL=Meta.js.map \ No newline at end of file diff --git a/lib/bare-server-modified/V1.d.ts b/lib/bare-server-modified/V1.d.ts new file mode 100644 index 0000000..d152b77 --- /dev/null +++ b/lib/bare-server-modified/V1.d.ts @@ -0,0 +1,2 @@ +import type Server from './BareServer.js'; +export default function registerV1(server: Server): void; diff --git a/lib/bare-server-modified/V1.js b/lib/bare-server-modified/V1.js new file mode 100644 index 0000000..be0c6a9 --- /dev/null +++ b/lib/bare-server-modified/V1.js @@ -0,0 +1,254 @@ +"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 encodeProtocol_js_1 = require("./encodeProtocol.js"); +const headerUtil_js_1 = require("./headerUtil.js"); +const remoteUtil_js_1 = require("./remoteUtil.js"); +const requestUtil_js_1 = require("./requestUtil.js"); +const validProtocols = ['http:', 'https:', 'ws:', 'wss:']; +function loadForwardedHeaders(forward, target, request) { + for (const header of forward) { + const value = request.headers.get(header); + if (value !== null) + target[header] = value; + } +} +function readHeaders(request) { + const remote = Object.create(null); + const headers = Object.create(null); + for (const remoteProp of ['host', 'port', 'protocol', 'path']) { + const header = `x-bare-${remoteProp}`; + const value = request.headers.get(header); + if (value === null) + throw new BareServer_js_1.BareError(400, { + code: 'MISSING_BARE_HEADER', + id: `request.headers.${header}`, + message: `Header was not specified.`, + }); + 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; + } + const xBareHeaders = request.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') { + headers[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); + } + headers[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; + } + } + const xBareForwardHeaders = request.headers.get('x-bare-forward-headers'); + if (xBareForwardHeaders === null) + throw new BareServer_js_1.BareError(400, { + code: 'MISSING_BARE_HEADER', + id: `request.headers.x-bare-forward-headers`, + message: `Header was not specified.`, + }); + try { + loadForwardedHeaders(JSON.parse(xBareForwardHeaders), headers, request); + } + catch (error) { + throw new BareServer_js_1.BareError(400, { + code: 'INVALID_BARE_HEADER', + id: `request.headers.x-bare-forward-headers`, + message: `Header contained invalid JSON. (${error instanceof Error ? error.message : error})`, + }); + } + return { remote: (0, remoteUtil_js_1.remoteToURL)(remote), headers }; +} +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, headers } = readHeaders(request); + const response = await (0, requestUtil_js_1.fetch)(request, abort.signal, headers, remote, options); + const responseHeaders = new headers_polyfill_1.Headers(); + for (const header in response.headers) { + if (header === 'content-encoding' || header === 'x-content-encoding') + responseHeaders.set('content-encoding', (0, headerUtil_js_1.flattenHeader)(response.headers[header])); + else if (header === 'content-length') + responseHeaders.set('content-length', (0, headerUtil_js_1.flattenHeader)(response.headers[header])); + } + responseHeaders.set('x-bare-headers', JSON.stringify((0, headerUtil_js_1.mapHeadersFromArray)((0, headerUtil_js_1.rawHeaderNames)(response.rawHeaders), { + ...response.headers, + }))); + responseHeaders.set('x-bare-status', response.statusCode.toString()); + responseHeaders.set('x-bare-status-text', response.statusMessage); + return new AbstractMessage_js_1.Response(response, { status: 200, headers: responseHeaders }); +}; +const metaExpiration = 30e3; +const wsMeta = async (request, res, options) => { + if (request.method === 'OPTIONS') { + return new AbstractMessage_js_1.Response(undefined, { status: 200 }); + } + const id = request.headers.get('x-bare-id'); + if (id === null) + throw new BareServer_js_1.BareError(400, { + code: 'MISSING_BARE_HEADER', + id: 'request.headers.x-bare-id', + message: 'Header was not specified', + }); + const meta = await options.database.get(id); + // check if meta isn't undefined and if the version equals 1 + if (meta?.value.v !== 1) + throw new BareServer_js_1.BareError(400, { + code: 'INVALID_BARE_HEADER', + id: 'request.headers.x-bare-id', + message: 'Unregistered ID', + }); + await options.database.delete(id); + return (0, BareServer_js_1.json)(200, { + headers: meta.value.response?.headers, + }); +}; +const wsNewMeta = async (request, res, options) => { + const id = (0, requestUtil_js_1.randomHex)(16); + await options.database.set(id, { + value: { v: 1 }, + expires: Date.now() + metaExpiration, + }); + 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 [firstProtocol, data] = request.headers + .get('sec-websocket-protocol') + .split(/,\s*/g); + if (firstProtocol !== 'bare') { + socket.end(); + return; + } + const { remote, headers, forward_headers: forwardHeaders, id, } = JSON.parse((0, encodeProtocol_js_1.decodeProtocol)(data)); + loadForwardedHeaders(forwardHeaders, headers, request); + const [remoteResponse, remoteSocket] = await (0, requestUtil_js_1.upgradeFetch)(request, abort.signal, headers, (0, remoteUtil_js_1.remoteToURL)(remote), options); + remoteSocket.on('close', () => { + // console.log('Remote closed'); + socket.end(); + }); + socket.on('close', () => { + // console.log('Serving closed'); + 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(); + }); + if (typeof id === 'string') { + const meta = await options.database.get(id); + if (meta?.value.v === 1) { + meta.value.response = { + headers: (0, headerUtil_js_1.mapHeadersFromArray)((0, headerUtil_js_1.rawHeaderNames)(remoteResponse.rawHeaders), { + ...remoteResponse.headers, + }), + }; + await options.database.set(id, meta); + } + } + const responseHeaders = [ + `HTTP/1.1 101 Switching Protocols`, + `Upgrade: websocket`, + `Connection: Upgrade`, + `Sec-WebSocket-Protocol: bare`, + `Sec-WebSocket-Accept: ${remoteResponse.headers['sec-websocket-accept']}`, + ]; + if ('sec-websocket-extensions' in remoteResponse.headers) { + responseHeaders.push(`Sec-WebSocket-Extensions: ${remoteResponse.headers['sec-websocket-extensions']}`); + } + socket.write(responseHeaders.concat('', '').join('\r\n')); + remoteSocket.pipe(socket); + socket.pipe(remoteSocket); +}; +function registerV1(server) { + server.routes.set('/v1/', tunnelRequest); + server.routes.set('/v1/ws-new-meta', wsNewMeta); + server.routes.set('/v1/ws-meta', wsMeta); + server.socketRoutes.set('/v1/', tunnelSocket); + server.versions.push('v1'); +} +exports.default = registerV1; +//# sourceMappingURL=V1.js.map \ No newline at end of file diff --git a/lib/bare-server-modified/V2.d.ts b/lib/bare-server-modified/V2.d.ts new file mode 100644 index 0000000..602953c --- /dev/null +++ b/lib/bare-server-modified/V2.d.ts @@ -0,0 +1,2 @@ +import type Server from './BareServer.js'; +export default function registerV2(server: Server): void; diff --git a/lib/bare-server-modified/V2.js b/lib/bare-server-modified/V2.js new file mode 100644 index 0000000..05c93dc --- /dev/null +++ b/lib/bare-server-modified/V2.js @@ -0,0 +1,364 @@ +"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 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.create(null); + 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); + for (const remoteProp of ['host', 'port', 'protocol', 'path']) { + const header = `x-bare-${remoteProp}`; + const value = headers.get(header); + if (value === null) + throw new BareServer_js_1.BareError(400, { + code: 'MISSING_BARE_HEADER', + id: `request.headers.${header}`, + message: `Header was not specified.`, + }); + 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; + } + 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), + }); +}; +const metaExpiration = 30e3; +const getMeta = async (request, res, options) => { + if (request.method === 'OPTIONS') { + return new AbstractMessage_js_1.Response(undefined, { status: 200 }); + } + const id = request.headers.get('x-bare-id'); + if (id === null) + throw new BareServer_js_1.BareError(400, { + code: 'MISSING_BARE_HEADER', + id: 'request.headers.x-bare-id', + message: 'Header was not specified', + }); + 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: remote.toString(), + 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, new URL(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); + server.versions.push('v2'); +} +exports.default = registerV2; +//# sourceMappingURL=V2.js.map \ No newline at end of file diff --git a/lib/bare-server-modified/V3.d.ts b/lib/bare-server-modified/V3.d.ts new file mode 100644 index 0000000..53c098f --- /dev/null +++ b/lib/bare-server-modified/V3.d.ts @@ -0,0 +1,2 @@ +import type Server from './BareServer.js'; +export default function registerV3(server: Server): void; diff --git a/lib/bare-server-modified/V3.js b/lib/bare-server-modified/V3.js new file mode 100644 index 0000000..802bcb3 --- /dev/null +++ b/lib/bare-server-modified/V3.js @@ -0,0 +1,305 @@ +"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 \ No newline at end of file diff --git a/lib/bare-server-modified/createServer.d.ts b/lib/bare-server-modified/createServer.d.ts new file mode 100644 index 0000000..2838420 --- /dev/null +++ b/lib/bare-server-modified/createServer.d.ts @@ -0,0 +1,52 @@ +/// +/// +import { Agent as HttpAgent } from 'node:http'; +import { Agent as HttpsAgent } from 'node:https'; +import BareServer from './BareServer.js'; +import type { BareMaintainer, Options } from './BareServer.js'; +import type { Database } from './Meta.js'; +export declare const validIPFamily: number[]; +export type IPFamily = 0 | 4 | 6; +export interface BareServerInit { + logErrors?: boolean; + localAddress?: string; + /** + * When set, the default logic for blocking local IP addresses is disabled. + */ + filterRemote?: Options['filterRemote']; + /** + * When set, the default logic for blocking local IP addresses is disabled. + */ + lookup?: Options['lookup']; + /** + * If local IP addresses/DNS records should be blocked. + * @default true + */ + blockLocal?: boolean; + /** + * IP address family to use when resolving `host` or `hostname`. Valid values are `0`, `4`, and `6`. When unspecified/0, both IP v4 and v6 will be used. + */ + family?: IPFamily | number; + maintainer?: BareMaintainer; + httpAgent?: HttpAgent; + httpsAgent?: HttpsAgent; + /** + * If legacy clients should be supported (v1 & v2). If this is set to false, the database can be safely ignored. + * @default true + */ + legacySupport?: boolean; + database?: Database; +} +export interface Address { + address: string; + family: number; +} +/** + * Converts the address and family of a DNS lookup callback into an array if it wasn't already + */ +export declare function toAddressArray(address: string | Address[], family?: number): Address[]; +/** + * Create a Bare server. + * This will handle all lifecycles for unspecified options (httpAgent, httpsAgent, metaMap). + */ +export declare function createBareServer(directory: string, init?: BareServerInit): BareServer; diff --git a/lib/bare-server-modified/createServer.js b/lib/bare-server-modified/createServer.js new file mode 100644 index 0000000..695d300 --- /dev/null +++ b/lib/bare-server-modified/createServer.js @@ -0,0 +1,99 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createBareServer = exports.toAddressArray = exports.validIPFamily = void 0; +const node_dns_1 = require("node:dns"); +const node_http_1 = require("node:http"); +const node_https_1 = require("node:https"); +const ipaddr_js_1 = require("ipaddr.js"); +const ws_1 = require("ws"); +const BareServer_js_1 = __importDefault(require("./BareServer.js")); +const Meta_js_1 = require("./Meta.js"); +const V1_js_1 = __importDefault(require("./V1.js")); +const V2_js_1 = __importDefault(require("./V2.js")); +const V3_js_1 = __importDefault(require("./V3.js")); +exports.validIPFamily = [0, 4, 6]; +/** + * Converts the address and family of a DNS lookup callback into an array if it wasn't already + */ +function toAddressArray(address, family) { + if (typeof address === 'string') + return [ + { + address, + family, + }, + ]; + else + return address; +} +exports.toAddressArray = toAddressArray; +/** + * Create a Bare server. + * This will handle all lifecycles for unspecified options (httpAgent, httpsAgent, metaMap). + */ +function createBareServer(directory, init = {}) { + if (typeof directory !== 'string') + throw new Error('Directory must be specified.'); + if (!directory.startsWith('/') || !directory.endsWith('/')) + throw new RangeError('Directory must start and end with /'); + init.logErrors ??= false; + const cleanup = []; + if (typeof init.family === 'number' && !exports.validIPFamily.includes(init.family)) + throw new RangeError('init.family must be one of: 0, 4, 6'); + if (init.blockLocal ?? true) { + init.filterRemote ??= (url) => { + // if the remote is an IP then it didn't go through the init.lookup hook + // isValid determines if this is so + if ((0, ipaddr_js_1.isValid)(url.hostname) && (0, ipaddr_js_1.parse)(url.hostname).range() !== 'unicast') + throw new RangeError('Forbidden IP'); + }; + init.lookup ??= (hostname, options, callback) => (0, node_dns_1.lookup)(hostname, options, (err, address, family) => { + if (address && + toAddressArray(address, family).some(({ address }) => (0, ipaddr_js_1.parse)(address).range() !== 'unicast')) + callback(new RangeError('Forbidden IP'), '', -1); + else + callback(err, address, family); + }); + } + if (!init.httpAgent) { + const httpAgent = new node_http_1.Agent({ + keepAlive: true, + }); + init.httpAgent = httpAgent; + cleanup.push(() => httpAgent.destroy()); + } + if (!init.httpsAgent) { + const httpsAgent = new node_https_1.Agent({ + keepAlive: true, + }); + init.httpsAgent = httpsAgent; + cleanup.push(() => httpsAgent.destroy()); + } + if (!init.database) { + const database = new Map(); + const interval = setInterval(() => (0, Meta_js_1.cleanupDatabase)(database), 1000); + init.database = database; + cleanup.push(() => clearInterval(interval)); + } + const server = new BareServer_js_1.default(directory, { + ...init, + database: new Meta_js_1.JSONDatabaseAdapter(init.database), + wss: new ws_1.WebSocketServer({ noServer: true }), + }); + init.legacySupport ??= true; + if (init.legacySupport) { + (0, V1_js_1.default)(server); + (0, V2_js_1.default)(server); + } + (0, V3_js_1.default)(server); + server.once('close', () => { + for (const cb of cleanup) + cb(); + }); + return server; +} +exports.createBareServer = createBareServer; +//# sourceMappingURL=createServer.js.map \ No newline at end of file diff --git a/lib/bare-server-modified/encodeProtocol.d.ts b/lib/bare-server-modified/encodeProtocol.d.ts new file mode 100644 index 0000000..9468b7f --- /dev/null +++ b/lib/bare-server-modified/encodeProtocol.d.ts @@ -0,0 +1,3 @@ +export declare function validProtocol(protocol: string): boolean; +export declare function encodeProtocol(protocol: string): string; +export declare function decodeProtocol(protocol: string): string; diff --git a/lib/bare-server-modified/encodeProtocol.js b/lib/bare-server-modified/encodeProtocol.js new file mode 100644 index 0000000..cc9d769 --- /dev/null +++ b/lib/bare-server-modified/encodeProtocol.js @@ -0,0 +1,48 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.decodeProtocol = exports.encodeProtocol = exports.validProtocol = void 0; +const validChars = "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~"; +const reserveChar = '%'; +function validProtocol(protocol) { + for (let i = 0; i < protocol.length; i++) { + const char = protocol[i]; + if (!validChars.includes(char)) { + return false; + } + } + return true; +} +exports.validProtocol = validProtocol; +function encodeProtocol(protocol) { + let result = ''; + for (let i = 0; i < protocol.length; i++) { + const char = protocol[i]; + if (validChars.includes(char) && char !== reserveChar) { + result += char; + } + else { + const code = char.charCodeAt(0); + result += reserveChar + code.toString(16).padStart(2, '0'); + } + } + return result; +} +exports.encodeProtocol = encodeProtocol; +function decodeProtocol(protocol) { + let result = ''; + for (let i = 0; i < protocol.length; i++) { + const char = protocol[i]; + if (char === reserveChar) { + const code = parseInt(protocol.slice(i + 1, i + 3), 16); + const decoded = String.fromCharCode(code); + result += decoded; + i += 2; + } + else { + result += char; + } + } + return result; +} +exports.decodeProtocol = decodeProtocol; +//# sourceMappingURL=encodeProtocol.js.map \ No newline at end of file diff --git a/lib/bare-server-modified/headerUtil.d.ts b/lib/bare-server-modified/headerUtil.d.ts new file mode 100644 index 0000000..ecac542 --- /dev/null +++ b/lib/bare-server-modified/headerUtil.d.ts @@ -0,0 +1,8 @@ +import type { BareHeaders } from './requestUtil.js'; +export declare function objectFromRawHeaders(raw: string[]): BareHeaders; +export declare function rawHeaderNames(raw: string[]): string[]; +export declare function mapHeadersFromArray(from: string[], to: BareHeaders): BareHeaders; +/** + * Converts a header into an HTTP-ready comma joined header. + */ +export declare function flattenHeader(value: string | string[]): string; diff --git a/lib/bare-server-modified/headerUtil.js b/lib/bare-server-modified/headerUtil.js new file mode 100644 index 0000000..f9913e9 --- /dev/null +++ b/lib/bare-server-modified/headerUtil.js @@ -0,0 +1,48 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.flattenHeader = exports.mapHeadersFromArray = exports.rawHeaderNames = exports.objectFromRawHeaders = void 0; +function objectFromRawHeaders(raw) { + const result = Object.create(null); + for (let i = 0; i < raw.length; i += 2) { + const [header, value] = raw.slice(i, i + 2); + if (header in result) { + const v = result[header]; + if (Array.isArray(v)) + v.push(value); + else + result[header] = [v, value]; + } + else + result[header] = value; + } + return result; +} +exports.objectFromRawHeaders = objectFromRawHeaders; +function rawHeaderNames(raw) { + const result = []; + for (let i = 0; i < raw.length; i += 2) { + if (!result.includes(raw[i])) + result.push(raw[i]); + } + return result; +} +exports.rawHeaderNames = rawHeaderNames; +function mapHeadersFromArray(from, to) { + for (const header of from) { + if (header.toLowerCase() in to) { + const value = to[header.toLowerCase()]; + delete to[header.toLowerCase()]; + to[header] = value; + } + } + return to; +} +exports.mapHeadersFromArray = mapHeadersFromArray; +/** + * Converts a header into an HTTP-ready comma joined header. + */ +function flattenHeader(value) { + return Array.isArray(value) ? value.join(', ') : value; +} +exports.flattenHeader = flattenHeader; +//# sourceMappingURL=headerUtil.js.map \ No newline at end of file diff --git a/lib/bare-server-modified/remoteUtil.d.ts b/lib/bare-server-modified/remoteUtil.d.ts new file mode 100644 index 0000000..1b317cb --- /dev/null +++ b/lib/bare-server-modified/remoteUtil.d.ts @@ -0,0 +1,9 @@ +export interface BareRemote { + host: string; + port: number | string; + path: string; + protocol: string; +} +export declare function remoteToURL(remote: BareRemote): URL; +export declare function resolvePort(url: URL): number; +export declare function urlToRemote(url: URL): BareRemote; diff --git a/lib/bare-server-modified/remoteUtil.js b/lib/bare-server-modified/remoteUtil.js new file mode 100644 index 0000000..71e8739 --- /dev/null +++ b/lib/bare-server-modified/remoteUtil.js @@ -0,0 +1,36 @@ +"use strict"; +/* + * Utilities for converting remotes to URLs + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.urlToRemote = exports.resolvePort = exports.remoteToURL = void 0; +function remoteToURL(remote) { + return new URL(`${remote.protocol}${remote.host}:${remote.port}${remote.path}`); +} +exports.remoteToURL = remoteToURL; +function resolvePort(url) { + if (url.port) + return Number(url.port); + switch (url.protocol) { + case 'ws:': + case 'http:': + return 80; + case 'wss:': + case 'https:': + return 443; + default: + // maybe blob + return 0; + } +} +exports.resolvePort = resolvePort; +function urlToRemote(url) { + return { + protocol: url.protocol, + host: url.hostname, + port: resolvePort(url), + path: url.pathname + url.search, + }; +} +exports.urlToRemote = urlToRemote; +//# sourceMappingURL=remoteUtil.js.map \ No newline at end of file diff --git a/lib/bare-server-modified/requestUtil.d.ts b/lib/bare-server-modified/requestUtil.d.ts new file mode 100644 index 0000000..97e891f --- /dev/null +++ b/lib/bare-server-modified/requestUtil.d.ts @@ -0,0 +1,13 @@ +/// +/// +/// +import type { IncomingMessage } from 'node:http'; +import type { Duplex } from 'node:stream'; +import WebSocket from 'ws'; +import type { Request } from './AbstractMessage.js'; +import type { Options } from './BareServer.js'; +export type BareHeaders = Record; +export declare function randomHex(byteLength: number): string; +export declare function fetch(request: Request, signal: AbortSignal, requestHeaders: BareHeaders, remote: URL, options: Options): Promise; +export declare function upgradeFetch(request: Request, signal: AbortSignal, requestHeaders: BareHeaders, remote: URL, options: Options): Promise<[res: IncomingMessage, socket: Duplex, head: Buffer]>; +export declare function webSocketFetch(request: Request, requestHeaders: BareHeaders, remote: URL, protocols: string[], options: Options): Promise<[req: IncomingMessage, socket: WebSocket]>; diff --git a/lib/bare-server-modified/requestUtil.js b/lib/bare-server-modified/requestUtil.js new file mode 100644 index 0000000..792ad0c --- /dev/null +++ b/lib/bare-server-modified/requestUtil.js @@ -0,0 +1,185 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.webSocketFetch = exports.upgradeFetch = exports.fetch = exports.randomHex = void 0; +const node_crypto_1 = require("node:crypto"); +const node_http_1 = require("node:http"); +const node_https_1 = require("node:https"); +const ws_1 = __importDefault(require("ws")); +const BareServer_js_1 = require("./BareServer.js"); +function randomHex(byteLength) { + const bytes = new Uint8Array(byteLength); + (0, node_crypto_1.getRandomValues)(bytes); + let hex = ''; + for (const byte of bytes) + hex += byte.toString(16).padStart(2, '0'); + return hex; +} +exports.randomHex = randomHex; +function outgoingError(error) { + if (error instanceof Error) { + switch (error.code) { + case 'ENOTFOUND': + return new BareServer_js_1.BareError(500, { + code: 'HOST_NOT_FOUND', + id: 'request', + message: 'The specified host could not be resolved.', + }); + case 'ECONNREFUSED': + return new BareServer_js_1.BareError(500, { + code: 'CONNECTION_REFUSED', + id: 'response', + message: 'The remote rejected the request.', + }); + case 'ECONNRESET': + return new BareServer_js_1.BareError(500, { + code: 'CONNECTION_RESET', + id: 'response', + message: 'The request was forcibly closed.', + }); + case 'ETIMEOUT': + return new BareServer_js_1.BareError(500, { + code: 'CONNECTION_TIMEOUT', + id: 'response', + message: 'The response timed out.', + }); + } + } + return error; +} +async function fetch(request, signal, requestHeaders, remote, options) { + if (options.filterRemote) + await options.filterRemote(remote); + const req = { + method: request.method, + headers: requestHeaders, + setHost: false, + signal, + localAddress: options.localAddress, + family: options.family, + lookup: options.lookup, + }; + let outgoing; + // NodeJS will convert the URL into HTTP options automatically + // see https://github.com/nodejs/node/blob/e30e71665cab94118833cc536a43750703b19633/lib/internal/url.js#L1277 + if (remote.protocol === 'https:') + outgoing = (0, node_https_1.request)(remote, { + ...req, + agent: options.httpsAgent, + }); + else if (remote.protocol === 'http:') + outgoing = (0, node_http_1.request)(remote, { + ...req, + agent: options.httpAgent, + }); + else + throw new RangeError(`Unsupported protocol: '${remote.protocol}'`); + request.body.pipe(outgoing); + return await new Promise((resolve, reject) => { + outgoing.on('response', (response) => { + resolve(response); + }); + outgoing.on('upgrade', (req, socket) => { + reject('Remote did not send a response'); + socket.destroy(); + }); + outgoing.on('error', (error) => { + reject(outgoingError(error)); + }); + }); +} +exports.fetch = fetch; +async function upgradeFetch(request, signal, requestHeaders, remote, options) { + if (options.filterRemote) + await options.filterRemote(remote); + const req = { + headers: requestHeaders, + method: request.method, + timeout: 12e3, + setHost: false, + signal, + localAddress: options.localAddress, + family: options.family, + lookup: options.lookup, + }; + let outgoing; + // NodeJS will convert the URL into HTTP options automatically + // see https://github.com/nodejs/node/blob/e30e71665cab94118833cc536a43750703b19633/lib/internal/url.js#L1277 + // calling .replace on remote may look like it replaces other occurrences of wss:, but it only replaces the first which is remote.protocol + if (remote.protocol === 'wss:') + outgoing = (0, node_https_1.request)(remote.toString().replace('wss:', 'https:'), { + ...req, + agent: options.httpsAgent, + }); + else if (remote.protocol === 'ws:') + outgoing = (0, node_http_1.request)(remote.toString().replace('ws:', 'http:'), { + ...req, + agent: options.httpAgent, + }); + else + throw new RangeError(`Unsupported protocol: '${remote.protocol}'`); + outgoing.end(); + return await new Promise((resolve, reject) => { + outgoing.on('response', (res) => { + reject(new Error('Remote did not upgrade the WebSocket')); + res.destroy(); + }); + outgoing.on('upgrade', (res, socket, head) => { + resolve([res, socket, head]); + }); + outgoing.on('error', (error) => { + reject(outgoingError(error)); + }); + }); +} +exports.upgradeFetch = upgradeFetch; +async function webSocketFetch(request, requestHeaders, remote, protocols, options) { + if (options.filterRemote) + await options.filterRemote(remote); + const req = { + headers: requestHeaders, + method: request.method, + timeout: 12e3, + setHost: false, + localAddress: options.localAddress, + family: options.family, + lookup: options.lookup, + }; + let outgoing; + if (remote.protocol === 'wss:') + outgoing = new ws_1.default(remote, protocols, { + ...req, + agent: options.httpsAgent, + }); + else if (remote.protocol === 'ws:') + outgoing = new ws_1.default(remote, protocols, { + ...req, + agent: options.httpAgent, + }); + else + throw new RangeError(`Unsupported protocol: '${remote.protocol}'`); + return await new Promise((resolve, reject) => { + let request; + const cleanup = () => { + outgoing.removeEventListener('open', openListener); + outgoing.removeEventListener('open', openListener); + }; + outgoing.on('upgrade', (req) => { + request = req; + }); + const openListener = () => { + cleanup(); + resolve([request, outgoing]); + }; + const errorListener = (event) => { + cleanup(); + reject(outgoingError(event.error)); + }; + outgoing.addEventListener('open', openListener); + outgoing.addEventListener('error', errorListener); + }); +} +exports.webSocketFetch = webSocketFetch; +//# sourceMappingURL=requestUtil.js.map \ No newline at end of file diff --git a/lib/bare-server-modified/splitHeaderUtil.d.ts b/lib/bare-server-modified/splitHeaderUtil.d.ts new file mode 100644 index 0000000..ed9a454 --- /dev/null +++ b/lib/bare-server-modified/splitHeaderUtil.d.ts @@ -0,0 +1,14 @@ +import { Headers } from 'headers-polyfill'; +/** + * + * Splits headers according to spec + * @param headers + * @returns Split headers + */ +export declare function splitHeaders(headers: Headers): Headers; +/** + * Joins headers according to spec + * @param headers + * @returns Joined headers + */ +export declare function joinHeaders(headers: Headers): Headers; diff --git a/lib/bare-server-modified/splitHeaderUtil.js b/lib/bare-server-modified/splitHeaderUtil.js new file mode 100644 index 0000000..26baa27 --- /dev/null +++ b/lib/bare-server-modified/splitHeaderUtil.js @@ -0,0 +1,60 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.joinHeaders = exports.splitHeaders = void 0; +const headers_polyfill_1 = require("headers-polyfill"); +const BareServer_js_1 = require("./BareServer.js"); +const MAX_HEADER_VALUE = 3072; +/** + * + * Splits headers according to spec + * @param headers + * @returns Split headers + */ +function splitHeaders(headers) { + const output = new headers_polyfill_1.Headers(headers); + if (headers.has('x-bare-headers')) { + const value = headers.get('x-bare-headers'); + if (value.length > MAX_HEADER_VALUE) { + output.delete('x-bare-headers'); + let split = 0; + for (let i = 0; i < value.length; i += MAX_HEADER_VALUE) { + const part = value.slice(i, i + MAX_HEADER_VALUE); + const id = split++; + output.set(`x-bare-headers-${id}`, `;${part}`); + } + } + } + return output; +} +exports.splitHeaders = splitHeaders; +/** + * Joins headers according to spec + * @param headers + * @returns Joined headers + */ +function joinHeaders(headers) { + const output = new headers_polyfill_1.Headers(headers); + const prefix = 'x-bare-headers'; + if (headers.has(`${prefix}-0`)) { + const join = []; + for (const [header, value] of headers) { + if (!header.startsWith(prefix)) { + continue; + } + if (!value.startsWith(';')) { + throw new BareServer_js_1.BareError(400, { + code: 'INVALID_BARE_HEADER', + id: `request.headers.${header}`, + message: `Value didn't begin with semi-colon.`, + }); + } + const id = parseInt(header.slice(prefix.length + 1)); + join[id] = value.slice(1); + output.delete(header); + } + output.set(prefix, join.join('')); + } + return output; +} +exports.joinHeaders = joinHeaders; +//# sourceMappingURL=splitHeaderUtil.js.map \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5e6de78..4c28b25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.2.1", "license": "GNU-3.0-or-later", "dependencies": { - "@tomphttp/bare-server-node": "*", + "@tomphttp/bare-server-node": "^2.0.1", "cors": "*", "easyviolet": "github:Russell2259/Easyviolet", "express": "*", diff --git a/package.json b/package.json index 153b458..6a3f214 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "author": "Polaris Development Group", "license": "GNU-3.0-or-later", "dependencies": { - "@tomphttp/bare-server-node": "*", + "@tomphttp/bare-server-node": "^2.0.1", "cors": "*", "easyviolet": "github:Russell2259/Easyviolet", "express": "*", @@ -22,4 +22,4 @@ "mime": "*", "uuid": "^9.0.1" } -} \ No newline at end of file +}