feat: Passwordless cross-device authentication

- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
This commit is contained in:
Bernt
2026-07-07 07:11:50 +00:00
parent 4aa984ad74
commit 6989a98d75
61843 changed files with 5491611 additions and 872231 deletions
+46
View File
@@ -0,0 +1,46 @@
MIT License
Copyright (c) Pooya Parsa <pooya@pi0.io>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
----
Based on http-party/node-http-proxy (9b96cd7)
Copyright (c) 2010-2016 Charlie Robbins, Jarrett Cruger & the Contributors.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+263
View File
@@ -0,0 +1,263 @@
# 🔀 httpxy
[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![bundle][bundle-src]][bundle-href]
[![Codecov][codecov-src]][codecov-href]
A Full-Featured HTTP and WebSocket Proxy for Node.js
## Proxy Fetch
`proxyFetch` is a proxy utility with web standard (`Request`/`Response`) interfaces. It forwards requests to a specific server address (TCP host/port or Unix socket), bypassing the URL's hostname.
```ts
import { proxyFetch } from "httpxy";
// TCP — using a URL string
const res = await proxyFetch("http://127.0.0.1:3000", "http://example.com/api/data");
console.log(await res.json());
// Unix socket — using a URL string
const res2 = await proxyFetch("unix:/tmp/app.sock", "http://localhost/health");
console.log(await res2.text());
// Or use an object for more control
const res3 = await proxyFetch({ host: "127.0.0.1", port: 3000 }, "http://example.com/api/data");
// Using a Request object
const req = new Request("http://example.com/api/data", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: "value" }),
});
const res4 = await proxyFetch("http://127.0.0.1:3000", req);
// Using a URL string with RequestInit
const res5 = await proxyFetch("http://127.0.0.1:3000", "http://example.com/api/data", {
method: "PUT",
headers: { Authorization: "Bearer token" },
body: JSON.stringify({ updated: true }),
});
```
It accepts the same `input` and `init` arguments as the global `fetch`, including `Request` objects and streaming bodies, and returns a standard `Response`. Redirects are handled manually by default.
## Proxy Upgrade
`proxyUpgrade` is a standalone WebSocket upgrade proxy. It forwards `upgrade` requests to a target server without needing a `ProxyServer` instance — the WebSocket counterpart to `proxyFetch`.
```ts
import { createServer } from "node:http";
import { proxyUpgrade } from "httpxy";
const server = createServer((req, res) => {
// Handle regular HTTP requests...
});
server.on("upgrade", (req, socket, head) => {
proxyUpgrade("http://127.0.0.1:8080", req, socket, head);
});
server.listen(3000);
```
It accepts the same `addr` formats as `proxyFetch` (`"http://host:port"`, `"unix:/path"`, or `{ host, port }` / `{ socketPath }`), and returns a `Promise<Socket>` that resolves with the upstream proxy socket once the WebSocket connection is established.
```ts
// With options
server.on("upgrade", (req, socket, head) => {
proxyUpgrade({ host: "127.0.0.1", port: 8080 }, req, socket, head, {
// changeOrigin: true, // rewrite Host header
// xfwd: false, // disable x-forwarded-* headers (enabled by default)
});
});
```
## Proxy Server
> [!NOTE]
> Proxy server was originally forked from [http-party/node-http-proxy](https://github.com/http-party/node-http-proxy).
Create proxy:
```ts
import { createServer } from "node:http";
import { createProxyServer } from "httpxy";
const proxy = createProxyServer({});
const server = createServer(async (req, res) => {
try {
await proxy.web(req, res, {
target: address /* address of your proxy server here */,
});
} catch (error) {
console.error(error);
res.statusCode = 500;
res.end("Proxy error: " + error.toString());
}
});
server.listen(3000, () => {
console.log("Proxy is listening on http://localhost:3000");
});
```
## Options
| Option | Type | Default | Description |
| ----------------------- | -------------------------------------- | ---------- | --------------------------------------------------------------------------- |
| `target` | `string \| URL \| ProxyTargetDetailed` | — | Target server URL |
| `forward` | `string \| URL` | — | Forward server URL (pipes request without the target's response) |
| `agent` | `http.Agent \| false` | keep-alive | Shared keep-alive agent by default. Set `false` to disable connection reuse |
| `ssl` | `https.ServerOptions` | — | Object passed to `https.createServer()` |
| `ws` | `boolean` | `false` | Enable WebSocket proxying |
| `xfwd` | `boolean` | `false` | Add `x-forwarded-*` headers |
| `secure` | `boolean` | — | Verify SSL certificates |
| `toProxy` | `boolean` | `false` | Pass absolute URL as path (proxy-to-proxy) |
| `prependPath` | `boolean` | `true` | Prepend the target's path to the proxy path |
| `ignorePath` | `boolean` | `false` | Ignore the incoming request path |
| `localAddress` | `string` | — | Local interface to bind for outgoing connections |
| `changeOrigin` | `boolean` | `false` | Change the `Host` header to match the target URL |
| `preserveHeaderKeyCase` | `boolean` | `false` | Keep original letter case of response header keys |
| `auth` | `string` | — | Basic authentication (`'user:password'`) for `Authorization` header |
| `hostRewrite` | `string` | — | Rewrite the `Location` hostname on redirects (301/302/307/308) |
| `autoRewrite` | `boolean` | `false` | Rewrite `Location` host/port on redirects based on the request |
| `protocolRewrite` | `string` | — | Rewrite `Location` protocol on redirects (`'http'` or `'https'`) |
| `cookieDomainRewrite` | `false \| string \| object` | `false` | Rewrite domain of `Set-Cookie` headers |
| `cookiePathRewrite` | `false \| string \| object` | `false` | Rewrite path of `Set-Cookie` headers |
| `headers` | `object` | — | Extra headers to add to target requests |
| `proxyTimeout` | `number` | `120000` | Timeout (ms) for the proxy request to the target |
| `timeout` | `number` | — | Timeout (ms) for the incoming request |
| `selfHandleResponse` | `boolean` | `false` | Disable automatic response piping (handle `proxyRes` yourself) |
| `followRedirects` | `boolean \| number` | `false` | Follow HTTP redirects from target. `true` = max 5 hops; number = custom max |
| `buffer` | `stream.Stream` | — | Stream to use as request body instead of the incoming request |
## Events
| Event | Arguments | Description |
| ------------ | ---------------------------------------- | ------------------------------------------------------ |
| `error` | `(err, req, res, target)` | An error occurred during proxying |
| `proxyReq` | `(proxyReq, req, res, options)` | Before request is sent to target (modify headers here) |
| `proxyRes` | `(proxyRes, req, res)` | Response received from target |
| `proxyReqWs` | `(proxyReq, req, socket, options, head)` | Before WebSocket upgrade request is sent |
| `open` | `(proxySocket)` | WebSocket connection opened |
| `close` | `(proxyRes, proxySocket, proxyHead)` | WebSocket connection closed |
| `start` | `(req, res, target)` | Proxy processing started |
| `end` | `(req, res, proxyRes)` | Proxy request completed |
## Examples
### HTTP Proxy
```ts
import { createServer } from "node:http";
import { createProxyServer } from "httpxy";
const proxy = createProxyServer({});
const server = createServer(async (req, res) => {
await proxy.web(req, res, { target: "http://localhost:8080" });
});
server.listen(3000);
```
### WebSocket Proxy
```ts
import { createServer } from "node:http";
import { createProxyServer } from "httpxy";
const proxy = createProxyServer({ target: "http://localhost:8080", ws: true });
const server = createServer(async (req, res) => {
await proxy.web(req, res);
});
server.on("upgrade", (req, socket, head) => {
proxy.ws(req, socket, { target: "http://localhost:8080" }, head);
});
server.listen(3000);
```
### Modify Request Headers
```ts
import { createServer } from "node:http";
import { createProxyServer } from "httpxy";
const proxy = createProxyServer({ target: "http://localhost:8080" });
proxy.on("proxyReq", (proxyReq) => {
proxyReq.setHeader("X-Forwarded-By", "httpxy");
});
const server = createServer(async (req, res) => {
await proxy.web(req, res);
});
server.listen(3000);
```
### HTTPS Proxy
```ts
import { readFileSync } from "node:fs";
import { createProxyServer } from "httpxy";
const proxy = createProxyServer({
ssl: {
key: readFileSync("server-key.pem", "utf8"),
cert: readFileSync("server-cert.pem", "utf8"),
},
target: "https://localhost:8443",
secure: false, // allow self-signed certificates
});
proxy.listen(3000);
```
### Standalone Proxy Server
```ts
import { createProxyServer } from "httpxy";
const proxy = createProxyServer({
target: "http://localhost:8080",
changeOrigin: true,
});
proxy.listen(3000);
```
## Development
- Clone this repository
- Install latest LTS version of [Node.js](https://nodejs.org/en/)
- Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`
- Install dependencies using `pnpm install`
- Run interactive tests using `pnpm dev`
## Acknowledgements
Performance optimizations in httpxy were inspired by analysis of [fast-proxy](https://github.com/fastify/fast-proxy) and [@fastify/http-proxy](https://github.com/fastify/fastify-http-proxy).
## License
Made with 💛
Published under [MIT License](./LICENSE).
<!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/httpxy?style=flat&colorA=18181B&colorB=F0DB4F
[npm-version-href]: https://npmjs.com/package/httpxy
[npm-downloads-src]: https://img.shields.io/npm/dm/httpxy?style=flat&colorA=18181B&colorB=F0DB4F
[npm-downloads-href]: https://npmjs.com/package/httpxy
[codecov-src]: https://img.shields.io/codecov/c/gh/unjs/httpxy/main?style=flat&colorA=18181B&colorB=F0DB4F
[codecov-href]: https://codecov.io/gh/unjs/httpxy
[bundle-src]: https://img.shields.io/bundlephobia/minzip/httpxy?style=flat&colorA=18181B&colorB=F0DB4F
[bundle-href]: https://bundlephobia.com/result?p=httpxy
+280
View File
@@ -0,0 +1,280 @@
import httpNative, { IncomingMessage, ServerResponse } from "node:http";
import http2, { Http2ServerRequest, Http2ServerResponse } from "node:http2";
import { EventEmitter } from "node:events";
import net, { Socket } from "node:net";
import * as stream from "node:stream";
import { Duplex } from "node:stream";
interface ProxyTargetDetailed {
host?: string;
port?: number | string;
protocol?: string;
hostname?: string;
pathname?: string;
socketPath?: string;
key?: string;
passphrase?: string;
pfx?: Buffer | string;
cert?: string;
ca?: string;
ciphers?: string;
secureProtocol?: string;
}
type ProxyTarget = string | URL | ProxyTargetDetailed;
/** Resolved proxy address — either TCP (host + port) or Unix socket. */
type ProxyAddr = {
host?: string;
port: number;
socketPath?: undefined;
} | {
host?: undefined;
port?: undefined;
socketPath: string;
};
interface ProxyServerOptions {
/** URL string to be parsed. */
target?: ProxyTarget;
/** URL string to be parsed. */
forward?: ProxyTarget;
/** Object to be passed to http(s).request. */
agent?: any;
/** Enable HTTP/2 listener, default is `false` */
http2?: boolean;
/** Object to be passed to https.createServer()
* or http2.createSecureServer() if the `http2` option is enabled
*/
ssl?: any;
/** If you want to proxy websockets. */
ws?: boolean;
/** Adds x- forward headers. */
xfwd?: boolean;
/** Verify SSL certificate. */
secure?: boolean;
/** Explicitly specify if we are proxying to another proxy. */
toProxy?: boolean;
/** Specify whether you want to prepend the target's path to the proxy path. */
prependPath?: boolean;
/** Specify whether you want to ignore the proxy path of the incoming request. */
ignorePath?: boolean;
/** Local interface string to bind for outgoing connections. */
localAddress?: string;
/** Changes the origin of the host header to the target URL. */
changeOrigin?: boolean;
/** specify whether you want to keep letter case of response header key */
preserveHeaderKeyCase?: boolean;
/** Basic authentication i.e. 'user:password' to compute an Authorization header. */
auth?: string;
/** Rewrites the location hostname on (301 / 302 / 307 / 308) redirects, Default: null. */
hostRewrite?: string;
/** Rewrites the location host/ port on (301 / 302 / 307 / 308) redirects based on requested host/ port.Default: false. */
autoRewrite?: boolean;
/** Rewrites the location protocol on (301 / 302 / 307 / 308) redirects to 'http' or 'https'.Default: null. */
protocolRewrite?: string;
/** Rewrites domain of set-cookie headers. */
cookieDomainRewrite?: false | string | {
[oldDomain: string]: string;
};
/** Rewrites path of set-cookie headers. Default: false */
cookiePathRewrite?: false | string | {
[oldPath: string]: string;
};
/** Object with extra headers to be added to target requests. */
headers?: {
[header: string]: string;
};
/** Timeout (in milliseconds) when proxy receives no response from target. Default: 120000 (2 minutes) */
proxyTimeout?: number;
/** Timeout (in milliseconds) for incoming requests */
timeout?: number;
/** If set to true, none of the webOutgoing passes are called and it's your responsibility to appropriately return the response by listening and acting on the proxyRes event */
selfHandleResponse?: boolean;
/** Follow HTTP redirects from target. `true` = max 5 hops; number = custom max. */
followRedirects?: boolean | number;
/** Buffer */
buffer?: stream.Stream;
}
type ResOfType<T extends "web" | "ws"> = T extends "ws" ? T extends "web" ? ServerResponse | Http2ServerResponse | Socket : Socket : T extends "web" ? ServerResponse | Http2ServerResponse : never;
type ProxyMiddleware<T extends ServerResponse | Http2ServerResponse | Socket> = (req: IncomingMessage | Http2ServerRequest, res: T, opts: ProxyServerOptions & {
target: URL | ProxyTargetDetailed;
forward: URL;
}, server: ProxyServer<IncomingMessage | Http2ServerRequest, ServerResponse | Http2ServerResponse>, head?: Buffer, callback?: (err: any, req: IncomingMessage | Http2ServerRequest, socket: T, url?: any) => void) => void | true;
interface ProxyServerEventMap<Req extends httpNative.IncomingMessage | http2.Http2ServerRequest = httpNative.IncomingMessage, Res extends httpNative.ServerResponse | http2.Http2ServerResponse = httpNative.ServerResponse> {
error: [err: Error, req?: Req, res?: Res | net.Socket, target?: URL | ProxyTarget];
start: [req: Req, res: Res, target: URL | ProxyTarget];
econnreset: [err: Error, req: Req, res: Res, target: URL | ProxyTarget];
proxyReq: [proxyReq: httpNative.ClientRequest, req: Req, res: Res, options: ProxyServerOptions];
proxyReqWs: [proxyReq: httpNative.ClientRequest, req: Req, socket: net.Socket, options: ProxyServerOptions, head: any];
proxyRes: [proxyRes: httpNative.IncomingMessage, req: Req, res: Res];
end: [req: Req, res: Res, proxyRes: httpNative.IncomingMessage];
open: [proxySocket: net.Socket];
/** @deprecated */
proxySocket: [proxySocket: net.Socket];
close: [proxyRes: Req, proxySocket: net.Socket, proxyHead: any];
}
declare class ProxyServer<Req extends httpNative.IncomingMessage | http2.Http2ServerRequest = httpNative.IncomingMessage, Res extends httpNative.ServerResponse | http2.Http2ServerResponse = httpNative.ServerResponse> extends EventEmitter<ProxyServerEventMap<Req, Res>> {
private _server?;
_webPasses: ProxyMiddleware<httpNative.ServerResponse>[];
_wsPasses: ProxyMiddleware<net.Socket>[];
options: ProxyServerOptions;
web: (req: Req, res: Res, opts?: ProxyServerOptions, head?: any) => Promise<void>;
ws: (req: Req, socket: net.Socket, opts: ProxyServerOptions, head?: any) => Promise<void>;
/**
* Creates the proxy server with specified options.
* @param options - Config object passed to the proxy
*/
constructor(options?: ProxyServerOptions);
/**
* A function that wraps the object in a webserver, for your convenience
* @param port - Port to listen on
* @param hostname - The hostname to listen on
* @param listeningListener - A callback function that is called when the server starts listening
*/
listen(port: number, hostname?: string, listeningListener?: () => void): this;
/**
* A function that closes the inner webserver and stops listening on given port
*/
close(callback?: () => void): void;
before<Type extends "ws" | "web">(type: Type, passName: string, pass: ProxyMiddleware<ResOfType<Type>>): void;
after<Type extends "ws" | "web">(type: Type, passName: string, pass: ProxyMiddleware<ResOfType<Type>>): void;
/** @internal */
_getPasses<Type extends "ws" | "web">(type: Type): ProxyMiddleware<ResOfType<Type>>[];
}
/**
* Creates the proxy server.
*
* Examples:
*
* httpProxy.createProxyServer({ .. }, 8000)
* // => '{ web: [Function], ws: [Function] ... }'
*
* @param {Object} Options Config object passed to the proxy
*
* @return {Object} Proxy Proxy object with handlers for `ws` and `web` requests
*
* @api public
*/
declare function createProxyServer(options?: ProxyServerOptions): ProxyServer<httpNative.IncomingMessage, httpNative.ServerResponse>;
/**
* Options for {@link proxyFetch}.
*/
interface ProxyFetchOptions {
/**
* Timeout in milliseconds for the upstream request.
* Rejects with an error if the upstream does not respond within this time.
*/
timeout?: number;
/**
* Add `x-forwarded-for`, `x-forwarded-port`, `x-forwarded-proto`, and
* `x-forwarded-host` headers derived from the input URL.
* Default: `false`.
*/
xfwd?: boolean;
/**
* Rewrite the `Host` header to match the target address.
* Default: `false` (original host from the input URL is kept).
*/
changeOrigin?: boolean;
/**
* HTTP agent for connection pooling / reuse.
* Default: `false` (no agent, no keep-alive).
*/
agent?: any;
/**
* Follow HTTP redirects from the upstream.
* `true` = max 5 hops; number = custom max.
* Default: `false` (manual redirect, raw 3xx responses are returned).
*/
followRedirects?: boolean | number;
/**
* TLS options forwarded to `https.request` (e.g. `{ rejectUnauthorized: false }`).
* Also controls certificate verification — set `rejectUnauthorized: false` to skip.
* Default: none.
*/
ssl?: Record<string, unknown>;
}
/**
* Proxy a request to a specific server address (TCP host/port or Unix socket)
* using web standard {@link Request}/{@link Response} interfaces.
*
* Supports both HTTP and HTTPS upstream targets.
*
* @param addr - The target server address. Can be a URL string (`http://host:port`, `https://host:port`, `unix:/path`), or an object with `host`/`port` for TCP or `socketPath` for Unix sockets.
* @param input - The request URL (string or URL) or a {@link Request} object.
* @param inputInit - Optional {@link RequestInit} or {@link Request} to override method, headers, and body.
* @param opts - Optional proxy options.
*/
declare function proxyFetch(addr: string | ProxyAddr, input: string | URL | Request, inputInit?: RequestInit | Request, opts?: ProxyFetchOptions): Promise<Response>;
/**
* Options for {@link proxyUpgrade}.
*/
interface ProxyUpgradeOptions {
/**
* Add `x-forwarded-for`, `x-forwarded-port`, and `x-forwarded-proto` headers.
* Default: `true`.
*/
xfwd?: boolean;
/**
* Rewrite the `Host` header to match the target.
* Default: `false` (original host is kept).
*/
changeOrigin?: boolean;
/**
* Extra headers to include in the upstream upgrade request.
* Default: none.
*/
headers?: Record<string, string>;
/**
* TLS options forwarded to `https.request`.
* Default: none.
*/
ssl?: Record<string, unknown>;
/**
* Whether to verify upstream TLS certificates.
* Default: `true`.
*/
secure?: boolean;
/**
* HTTP/HTTPS agent used for the upstream request.
* Default: `false` (no keep-alive agent is used).
*/
agent?: any;
/**
* Local interface address to bind for upstream connections.
* Default: OS-selected local address.
*/
localAddress?: string;
/**
* Basic auth credentials in `username:password` format.
* Default: none.
*/
auth?: string;
/**
* Prepend the target path to the proxied request path.
* Default: `true`.
*/
prependPath?: boolean;
/**
* Ignore the incoming request path when building the upstream path.
* Default: `false` (incoming path is used).
*/
ignorePath?: boolean;
/**
* Send absolute URL in request path when proxying to another proxy.
* Default: `false` (path-only request target is used).
*/
toProxy?: boolean;
}
/**
* Proxy a WebSocket upgrade request to a target address without creating a
* {@link ProxyServer} instance. Similar to {@link proxyFetch} but for
* WebSocket upgrades.
*
* @param addr - Target server address. Can be a URL string (`http://host:port`, `ws://host:port`, `unix:/path`), or an object with `host`/`port` for TCP or `socketPath` for Unix sockets.
* @param req - The incoming HTTP upgrade request.
* @param socket - The network socket between the server and client.
* @param head - The first packet of the upgraded stream (may be empty).
* @param opts - Optional proxy options.
* @returns A promise that resolves with the upstream proxy socket once the
* WebSocket connection is established, or rejects on error.
*/
declare function proxyUpgrade(addr: string | ProxyAddr, req: IncomingMessage, socket: Duplex, head?: Buffer, opts?: ProxyUpgradeOptions): Promise<Socket>;
export { type ProxyAddr, type ProxyFetchOptions, ProxyServer, type ProxyServerEventMap, type ProxyServerOptions, type ProxyTarget, type ProxyTargetDetailed, type ProxyUpgradeOptions, createProxyServer, proxyFetch, proxyUpgrade };
+841
View File
@@ -0,0 +1,841 @@
import httpNative, { request } from "node:http";
import httpsNative, { request as request$1 } from "node:https";
import http2 from "node:http2";
import { EventEmitter } from "node:events";
import "node:net";
import { Readable } from "node:stream";
const upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i;
const defaultAgents = {
http: new httpNative.Agent({
keepAlive: true,
maxSockets: 256,
maxFreeSockets: 64
}),
https: new httpsNative.Agent({
keepAlive: true,
maxSockets: 256,
maxFreeSockets: 64
})
};
const isSSL = /^https|wss/;
const HTTP2_HEADER_BLACKLIST = [
":method",
":path",
":scheme",
":authority"
];
function setupOutgoing(outgoing, options, req, forward) {
outgoing.port = options[forward || "target"].port || (isSSL.test(options[forward || "target"].protocol ?? "http") ? 443 : 80);
for (const e of [
"host",
"hostname",
"socketPath",
"pfx",
"key",
"passphrase",
"cert",
"ca",
"ciphers",
"secureProtocol"
]) {
const value = options[forward || "target"][e];
if (value !== void 0) outgoing[e] = value;
}
if (outgoing.host === void 0 && typeof outgoing.hostname === "string") {
const bracketedHost = outgoing.hostname.includes(":") && !outgoing.hostname.startsWith("[") ? `[${outgoing.hostname}]` : outgoing.hostname;
outgoing.host = outgoing.port ? `${bracketedHost}:${outgoing.port}` : bracketedHost;
}
outgoing.method = options.method || req.method;
outgoing.headers = { ...req.headers };
if (req.headers?.[":authority"]) outgoing.headers.host = req.headers[":authority"];
if (options.headers) for (const key of Object.keys(options.headers)) outgoing.headers[key] = options.headers[key];
if (req.httpVersionMajor > 1) for (const header of HTTP2_HEADER_BLACKLIST) delete outgoing.headers[header];
if (options.auth) outgoing.auth = options.auth;
if (options.ca) outgoing.ca = options.ca;
if (isSSL.test(options[forward || "target"].protocol ?? "http")) outgoing.rejectUnauthorized = options.secure === void 0 ? true : options.secure;
if (options.agent !== void 0) outgoing.agent = options.agent || false;
else if (req.httpVersionMajor > 1 || upgradeHeader.test(req.headers.connection || "")) outgoing.agent = false;
else {
const targetProto = options[forward || "target"].protocol ?? "http";
outgoing.agent = isSSL.test(targetProto) ? defaultAgents.https : defaultAgents.http;
}
outgoing.localAddress = options.localAddress;
if (!outgoing.agent) {
outgoing.headers = outgoing.headers || {};
if (typeof outgoing.headers.connection !== "string" || !upgradeHeader.test(outgoing.headers.connection)) outgoing.headers.connection = "close";
}
const target = options[forward || "target"];
const targetPath = target && options.prependPath !== false ? target.pathname || "" : "";
const targetSearch = target instanceof URL && options.prependPath !== false ? target.search || "" : "";
const reqUrl = req.url || "";
const qIdx = reqUrl.indexOf("?");
const reqPath = qIdx === -1 ? reqUrl : reqUrl.slice(0, qIdx);
const reqSearch = qIdx === -1 ? "" : reqUrl.slice(qIdx);
const normalizedPath = reqPath ? reqPath[0] === "/" ? reqPath : "/" + reqPath : "/";
let outgoingPath = options.toProxy ? "/" + reqUrl : normalizedPath + reqSearch;
outgoingPath = options.ignorePath ? "" : outgoingPath;
let fullPath = joinURL(targetPath, outgoingPath);
if (targetSearch) fullPath = fullPath.includes("?") ? fullPath.replace("?", targetSearch + "&") : fullPath + targetSearch;
outgoing.path = fullPath;
if (options.changeOrigin) outgoing.headers.host = requiresPort(outgoing.port, options[forward || "target"].protocol) && !hasPort(outgoing.host) ? outgoing.host + ":" + outgoing.port : outgoing.host ?? void 0;
return outgoing;
}
function joinURL(base, path) {
if (!base || base === "/") return path || "/";
if (!path || path === "/") return base || "/";
const baseHasTrailing = base[base.length - 1] === "/";
const pathHasLeading = path[0] === "/";
if (baseHasTrailing && pathHasLeading) return base + path.slice(1);
if (!baseHasTrailing && !pathHasLeading) return base + "/" + path;
return base + path;
}
function setupSocket(socket) {
socket.setTimeout(0);
socket.setNoDelay(true);
socket.setKeepAlive(true, 0);
return socket;
}
function getPort(req) {
const hostHeader = req.headers[":authority"] || req.headers.host;
const res = hostHeader ? hostHeader.match(/:(\d+)/) : "";
if (res) return res[1];
return hasEncryptedConnection(req) ? "443" : "80";
}
function hasEncryptedConnection(req) {
const socket = req.socket;
return !!socket && "encrypted" in socket && socket.encrypted;
}
function rewriteCookieProperty(header, config, property) {
if (Array.isArray(header)) return header.map(function(headerElement) {
return rewriteCookieProperty(headerElement, config, property);
});
return header.replace(new RegExp(String.raw`(;\s*` + property + "=)([^;]+)", "i"), function(match, prefix, previousValue) {
let newValue;
if (previousValue in config) newValue = config[previousValue];
else if ("*" in config) newValue = config["*"];
else return match;
return newValue ? prefix + newValue : "";
});
}
function parseAddr(addr) {
if (typeof addr === "string") {
if (addr.startsWith("unix:")) return { socketPath: addr.slice(5) };
const url = new URL(addr);
return {
host: url.hostname,
port: Number(url.port) || (isSSL.test(url.protocol) ? 443 : 80)
};
}
if (!addr.socketPath && !addr.port) throw new Error("ProxyAddr must have either `port` or `socketPath`");
return addr;
}
function hasPort(host) {
return host ? !!~host.indexOf(":") : false;
}
function requiresPort(_port, _protocol) {
const protocol = _protocol?.split(":")[0];
const port = +_port;
if (!port) return false;
switch (protocol) {
case "http":
case "ws": return port !== 80;
case "https":
case "wss": return port !== 443;
case "ftp": return port !== 21;
case "gopher": return port !== 70;
case "file": return false;
}
return port !== 0;
}
function defineProxyMiddleware(m) {
return m;
}
function defineProxyOutgoingMiddleware(m) {
return m;
}
const redirectRegex = /^201|30([12378])$/;
const webOutgoingMiddleware = [
defineProxyOutgoingMiddleware((req, res, proxyRes) => {
if (req.httpVersion === "1.0" || req.httpVersionMajor >= 2 || proxyRes.statusCode === 204 || proxyRes.statusCode === 304) delete proxyRes.headers["transfer-encoding"];
}),
defineProxyOutgoingMiddleware((req, res, proxyRes) => {
if (req.httpVersion === "1.0") proxyRes.headers.connection = req.headers.connection || "close";
else if (req.httpVersionMajor < 2 && !proxyRes.headers.connection) proxyRes.headers.connection = req.headers.connection || "keep-alive";
else if (req.httpVersionMajor >= 2) delete proxyRes.headers.connection;
}),
defineProxyOutgoingMiddleware((req, res, proxyRes, options) => {
if ((options.hostRewrite || options.autoRewrite || options.protocolRewrite) && proxyRes.headers.location && redirectRegex.test(String(proxyRes.statusCode))) {
const target = _toURL(options.target);
const u = new URL(proxyRes.headers.location, target);
if (target.host !== u.host) return;
if (options.hostRewrite) u.host = options.hostRewrite;
else if (options.autoRewrite) {
if (req.headers[":authority"]) u.host = req.headers[":authority"];
else if (req.headers.host) u.host = req.headers.host;
}
if (options.protocolRewrite) u.protocol = options.protocolRewrite;
proxyRes.headers.location = u.toString();
}
}),
defineProxyOutgoingMiddleware((req, res, proxyRes, options) => {
const rewriteCookieDomainConfig = typeof options.cookieDomainRewrite === "string" ? { "*": options.cookieDomainRewrite } : options.cookieDomainRewrite;
const rewriteCookiePathConfig = typeof options.cookiePathRewrite === "string" ? { "*": options.cookiePathRewrite } : options.cookiePathRewrite;
const preserveHeaderKeyCase = options.preserveHeaderKeyCase;
let rawHeaderKeyMap;
const setHeader = function(key, header) {
if (header === void 0 || !String(key).trim()) return;
if (rewriteCookieDomainConfig && key.toLowerCase() === "set-cookie") header = rewriteCookieProperty(header, rewriteCookieDomainConfig, "domain");
if (rewriteCookiePathConfig && key.toLowerCase() === "set-cookie") header = rewriteCookieProperty(header, rewriteCookiePathConfig, "path");
try {
res.setHeader(String(key).trim(), header);
} catch {}
};
if (preserveHeaderKeyCase && proxyRes.rawHeaders !== void 0) {
rawHeaderKeyMap = {};
for (let i = 0; i < proxyRes.rawHeaders.length; i += 2) {
const key = proxyRes.rawHeaders[i];
rawHeaderKeyMap[key.toLowerCase()] = key;
}
}
for (let key of Object.keys(proxyRes.headers)) {
const header = proxyRes.headers[key];
if (preserveHeaderKeyCase && rawHeaderKeyMap) key = rawHeaderKeyMap[key] || key;
setHeader(key, header);
}
}),
defineProxyOutgoingMiddleware((req, res, proxyRes) => {
res.statusCode = proxyRes.statusCode;
if (proxyRes.statusMessage && req.httpVersionMajor < 2) res.statusMessage = proxyRes.statusMessage;
})
];
function _toURL(target) {
if (target instanceof URL) return target;
if (typeof target === "string") return new URL(target);
const protocol = target.protocol || "http:";
const host = target.host || target.hostname || "localhost";
const port = target.port;
return new URL(`${protocol}//${host}${port ? ":" + port : ""}`);
}
const nativeAgents = {
http: httpNative,
https: httpsNative
};
const redirectStatuses = /* @__PURE__ */ new Set([
301,
302,
303,
307,
308
]);
const webIncomingMiddleware = [
defineProxyMiddleware((req) => {
if ((req.method === "DELETE" || req.method === "OPTIONS") && !req.headers["content-length"] && !req.headers["transfer-encoding"]) req.headers["content-length"] = "0";
}),
defineProxyMiddleware((req, res, options) => {
if (options.timeout) req.socket.setTimeout(options.timeout, () => {
req.socket.destroy();
});
}),
defineProxyMiddleware((req, res, options) => {
if (!options.xfwd) return;
const encrypted = req.isSpdy || hasEncryptedConnection(req);
const values = {
for: req.connection.remoteAddress || req.socket.remoteAddress,
port: getPort(req),
proto: encrypted ? "https" : "http"
};
for (const header of [
"for",
"port",
"proto"
]) {
const key = "x-forwarded-" + header;
if (!req.headers[key] && values[header] !== void 0) req.headers[key] = values[header];
}
req.headers["x-forwarded-host"] = req.headers["x-forwarded-host"] || req.headers[":authority"] || req.headers.host || "";
}),
defineProxyMiddleware((req, res, options, server, head, callback) => {
server.emit("start", req, res, options.target || options.forward);
const http = nativeAgents.http;
const https = nativeAgents.https;
const maxRedirects = typeof options.followRedirects === "number" ? options.followRedirects : options.followRedirects ? 5 : 0;
if (options.forward) {
const forwardReq = (isSSL.test(options.forward.protocol || "http") ? https : http).request(setupOutgoing(options.ssl || {}, options, req, "forward"));
const forwardError = createErrorHandler(forwardReq, options.forward);
req.on("error", forwardError);
forwardReq.on("error", forwardError);
(options.buffer || req).pipe(forwardReq);
if (!options.target) {
res.end();
return;
}
}
const proxyReq = (isSSL.test(options.target.protocol || "http") ? https : http).request(setupOutgoing(options.ssl || {}, options, req));
proxyReq.on("socket", (_socket) => {
if (server && !proxyReq.getHeader("expect")) server.emit("proxyReq", proxyReq, req, res, options);
});
if (options.proxyTimeout) proxyReq.setTimeout(options.proxyTimeout, function() {
proxyReq.destroy();
});
res.on("close", function() {
if (!res.writableFinished) proxyReq.destroy();
});
const proxyError = createErrorHandler(proxyReq, options.target);
req.on("error", proxyError);
proxyReq.on("error", proxyError);
function createErrorHandler(proxyReq, url) {
return function proxyError(err) {
if (!req.socket?.writable && err.code === "ECONNRESET") {
server.emit("econnreset", err, req, res, url);
return proxyReq.destroy();
}
if (callback) callback(err, req, res, url);
else server.emit("error", err, req, res, url);
};
}
let bodyBuffer;
if (maxRedirects > 0) {
const chunks = [];
const source = options.buffer || req;
source.on("data", (chunk) => {
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
proxyReq.write(chunk);
});
source.on("end", () => {
bodyBuffer = Buffer.concat(chunks);
proxyReq.end();
});
source.on("error", (err) => {
proxyReq.destroy(err);
});
} else proxyReq.on("socket", (socket) => {
if (socket.pending) socket.on("connect", () => (options.buffer || req).pipe(proxyReq));
else (options.buffer || req).pipe(proxyReq);
});
function handleResponse(proxyRes, redirectCount, currentUrl) {
const statusCode = proxyRes.statusCode;
if (maxRedirects > 0 && redirectStatuses.has(statusCode) && redirectCount < maxRedirects && proxyRes.headers.location) {
proxyRes.resume();
const location = new URL(proxyRes.headers.location, currentUrl);
const preserveMethod = statusCode === 307 || statusCode === 308;
const redirectMethod = preserveMethod ? req.method || "GET" : "GET";
const isHTTPS = isSSL.test(location.protocol);
const agent = isHTTPS ? https : http;
const redirectHeaders = { ...req.headers };
if (options.headers) Object.assign(redirectHeaders, options.headers);
redirectHeaders.host = location.host;
if (location.host !== currentUrl.host) {
delete redirectHeaders.authorization;
delete redirectHeaders.cookie;
}
if (!preserveMethod) {
delete redirectHeaders["content-length"];
delete redirectHeaders["content-type"];
delete redirectHeaders["transfer-encoding"];
}
const redirectOpts = {
hostname: location.hostname,
port: location.port || (isHTTPS ? 443 : 80),
path: location.pathname + location.search,
method: redirectMethod,
headers: redirectHeaders,
agent: options.agent || false
};
if (isHTTPS) redirectOpts.rejectUnauthorized = options.secure === void 0 ? true : options.secure;
const redirectReq = agent.request(redirectOpts);
if (server && !redirectReq.getHeader("expect")) server.emit("proxyReq", redirectReq, req, res, options);
if (options.proxyTimeout) redirectReq.setTimeout(options.proxyTimeout, () => {
redirectReq.destroy();
});
const redirectError = createErrorHandler(redirectReq, location);
redirectReq.on("error", redirectError);
redirectReq.on("response", (nextRes) => {
handleResponse(nextRes, redirectCount + 1, location);
});
if (preserveMethod && bodyBuffer && bodyBuffer.length > 0) redirectReq.end(bodyBuffer);
else redirectReq.end();
return;
}
if (server) server.emit("proxyRes", proxyRes, req, res);
if (!res.headersSent && !options.selfHandleResponse) {
for (const pass of webOutgoingMiddleware) if (pass(req, res, proxyRes, options)) break;
}
if (res.finished) {
if (server) server.emit("end", req, res, proxyRes);
} else {
res.on("close", function() {
proxyRes.destroy();
});
proxyRes.on("close", function() {
if (!proxyRes.complete && !res.destroyed) res.destroy();
});
proxyRes.on("error", function(err) {
if (!res.destroyed) res.destroy(err);
if (server.listenerCount("error") > 0) server.emit("error", err, req, res, currentUrl);
});
proxyRes.on("end", function() {
if (server) server.emit("end", req, res, proxyRes);
});
if (!options.selfHandleResponse) proxyRes.pipe(res);
}
}
proxyReq.on("response", function(proxyRes) {
handleResponse(proxyRes, 0, options.target);
});
})
];
const websocketIncomingMiddleware = [
defineProxyMiddleware((req, socket) => {
if (req.method !== "GET" || !req.headers.upgrade) {
socket.destroy();
return true;
}
if (req.headers.upgrade.toLowerCase() !== "websocket") {
socket.destroy();
return true;
}
}),
defineProxyMiddleware((req, socket, options) => {
if (!options.xfwd) return;
const values = {
for: req.connection.remoteAddress || req.socket.remoteAddress,
port: getPort(req),
proto: hasEncryptedConnection(req) ? "wss" : "ws"
};
for (const header of [
"for",
"port",
"proto"
]) {
const key = "x-forwarded-" + header;
if (!req.headers[key] && values[header] !== void 0) req.headers[key] = values[header];
}
}),
defineProxyMiddleware((req, socket, options, server, head, callback) => {
const createHttpHeader = function(line, headers) {
return Object.keys(headers).reduce(function(head, key) {
const value = headers[key];
if (!Array.isArray(value)) {
head.push(key + ": " + value);
return head;
}
for (const element of value) head.push(key + ": " + element);
return head;
}, [line]).join("\r\n") + "\r\n\r\n";
};
setupSocket(socket);
if (head && head.length > 0) socket.unshift(head);
socket.on("error", onSocketError);
const proxyReq = (isSSL.test(options.target.protocol || "http") ? httpsNative : httpNative).request(setupOutgoing(options.ssl || {}, options, req));
if (server) server.emit("proxyReqWs", proxyReq, req, socket, options, head);
proxyReq.on("error", onOutgoingError);
proxyReq.on("response", function(res) {
if (!res.upgrade) if (!socket.destroyed && socket.writable) {
socket.write(createHttpHeader("HTTP/" + res.httpVersion + " " + res.statusCode + " " + res.statusMessage, res.headers));
res.on("error", onOutgoingError);
res.pipe(socket);
} else res.resume();
});
proxyReq.on("upgrade", function(proxyRes, proxySocket, proxyHead) {
proxySocket.on("error", onOutgoingError);
proxySocket.on("end", function() {
server.emit("close", proxyRes, proxySocket, proxyHead);
});
socket.removeListener("error", onSocketError);
socket.on("error", function() {
proxySocket.end();
});
setupSocket(proxySocket);
if (proxyHead && proxyHead.length > 0) proxySocket.unshift(proxyHead);
socket.write(createHttpHeader("HTTP/1.1 101 Switching Protocols", proxyRes.headers));
proxySocket.pipe(socket).pipe(proxySocket);
server.emit("open", proxySocket);
server.emit("proxySocket", proxySocket);
});
proxyReq.end();
function onSocketError(err) {
if (callback) callback(err, req, socket);
else server.emit("error", err, req, socket);
proxyReq.destroy();
}
function onOutgoingError(err) {
if (callback) callback(err, req, socket);
else server.emit("error", err, req, socket);
socket.end();
}
})
];
var ProxyServer = class extends EventEmitter {
_server;
_webPasses = [...webIncomingMiddleware];
_wsPasses = [...websocketIncomingMiddleware];
options;
web;
ws;
constructor(options = {}) {
super();
this.options = options || {};
this.options.prependPath = options.prependPath !== false;
this.web = _createProxyFn("web", this);
this.ws = _createProxyFn("ws", this);
}
listen(port, hostname, listeningListener) {
const closure = (req, res) => {
return this.web(req, res);
};
if (this.options.http2) {
if (!this.options.ssl) throw new Error("HTTP/2 requires ssl option");
this._server = http2.createSecureServer({
...this.options.ssl,
allowHTTP1: true
}, closure);
} else if (this.options.ssl) this._server = httpsNative.createServer(this.options.ssl, closure);
else this._server = httpNative.createServer(closure);
if (this.options.ws) this._server.on("upgrade", (req, socket, head) => {
this.ws(req, socket, this.options, head).catch(() => {});
});
this._server.listen(port, hostname, listeningListener);
return this;
}
close(callback) {
if (this._server) this._server.close((...args) => {
this._server = void 0;
if (callback) Reflect.apply(callback, void 0, args);
});
}
before(type, passName, pass) {
if (type !== "ws" && type !== "web") throw new Error("type must be `web` or `ws`");
const passes = this._getPasses(type);
let i = false;
for (const [idx, v] of passes.entries()) if (v.name === passName) i = idx;
if (i === false) throw new Error("No such pass");
passes.splice(i, 0, pass);
}
after(type, passName, pass) {
if (type !== "ws" && type !== "web") throw new Error("type must be `web` or `ws`");
const passes = this._getPasses(type);
let i = false;
for (const [idx, v] of passes.entries()) if (v.name === passName) i = idx;
if (i === false) throw new Error("No such pass");
passes.splice(i++, 0, pass);
}
_getPasses(type) {
return type === "ws" ? this._wsPasses : this._webPasses;
}
};
function createProxyServer(options = {}) {
return new ProxyServer(options);
}
function _createProxyFn(type, server) {
return function(req, res, opts, head) {
const requestOptions = {
...opts,
...server.options
};
for (const key of ["target", "forward"]) if (typeof requestOptions[key] === "string") requestOptions[key] = new URL(requestOptions[key]);
if (!requestOptions.target && !requestOptions.forward) {
this.emit("error", /* @__PURE__ */ new Error("Must provide a proper URL as target"));
return Promise.resolve();
}
let _resolve;
let _reject;
const callbackPromise = new Promise((resolve, reject) => {
_resolve = resolve;
_reject = reject;
});
res.on("close", () => {
_resolve();
});
res.on("error", (error) => {
_reject(error);
});
for (const pass of server._getPasses(type)) {
let stop;
try {
stop = pass(req, res, requestOptions, server, head, (error, _req, _res, url) => {
if (server.listenerCount("error") > 0) {
server.emit("error", error, req, res, url);
_resolve();
} else _reject(error);
});
} catch (error) {
if (server.listenerCount("error") > 0) {
server.emit("error", error, req, res, requestOptions.target || requestOptions.forward);
_resolve();
} else _reject(error);
break;
}
if (stop) {
_resolve();
break;
}
}
return callbackPromise;
};
}
async function proxyFetch(addr, input, inputInit, opts) {
const resolvedAddr = parseAddr(addr);
let useHTTPS = false;
let addrBasePath = "";
if (typeof addr === "string" && !addr.startsWith("unix:")) {
const addrURL = new URL(addr);
useHTTPS = isSSL.test(addrURL.protocol);
if (addrURL.pathname && addrURL.pathname !== "/") addrBasePath = addrURL.pathname;
}
let url;
let init;
if (input instanceof Request) {
url = new URL(input.url);
init = {
...toInit(input),
...toInit(inputInit)
};
} else {
url = new URL(input);
init = toInit(inputInit);
}
init = {
redirect: "manual",
...init
};
if (init.body) init.duplex = "half";
const requestPath = url.pathname + url.search;
const path = addrBasePath ? joinURL(addrBasePath, requestPath) : requestPath;
const reqHeaders = {};
if (init.headers) if (!(init.headers instanceof Headers) && !Array.isArray(init.headers)) Object.assign(reqHeaders, init.headers);
else for (const [key, value] of init.headers) {
const existing = reqHeaders[key];
if (existing === void 0) reqHeaders[key] = value;
else reqHeaders[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
}
if (opts?.xfwd) {
if (!reqHeaders["x-forwarded-for"]) reqHeaders["x-forwarded-for"] = url.hostname;
if (!reqHeaders["x-forwarded-port"]) reqHeaders["x-forwarded-port"] = url.port || (url.protocol === "https:" ? "443" : "80");
if (!reqHeaders["x-forwarded-proto"]) reqHeaders["x-forwarded-proto"] = url.protocol.replace(":", "");
if (!reqHeaders["x-forwarded-host"]) reqHeaders["x-forwarded-host"] = url.host;
}
if (opts?.changeOrigin) if (resolvedAddr.socketPath) reqHeaders.host = "localhost";
else {
const targetHost = resolvedAddr.host || "localhost";
const targetPort = resolvedAddr.port;
reqHeaders.host = targetPort && targetPort !== (useHTTPS ? 443 : 80) ? `${targetHost}:${targetPort}` : targetHost;
}
const maxRedirects = typeof opts?.followRedirects === "number" ? opts.followRedirects : opts?.followRedirects ? 5 : 0;
const body = maxRedirects > 0 ? await _bufferBody(init.body) : _toNodeStream(init.body);
const agent = opts?.agent !== void 0 ? opts.agent || false : useHTTPS ? defaultAgents.https : defaultAgents.http;
const res = await _sendRequest(useHTTPS ? request$1 : request, init.method || "GET", path, reqHeaders, resolvedAddr, body, {
signal: init.signal || void 0,
agent,
timeout: opts?.timeout,
ssl: opts?.ssl,
maxRedirects,
redirectCount: 0,
originalHeaders: reqHeaders
});
const resHeaders = [];
const rawHeaders = res.rawHeaders;
for (let i = 0; i < rawHeaders.length; i += 2) {
const key = rawHeaders[i];
const keyLower = key.toLowerCase();
if (keyLower === "transfer-encoding" || keyLower === "keep-alive" || keyLower === "connection") continue;
resHeaders.push([key, rawHeaders[i + 1]]);
}
const hasBody = res.statusCode !== 204 && res.statusCode !== 304;
return new Response(hasBody ? Readable.toWeb(res) : null, {
status: res.statusCode,
statusText: res.statusMessage,
headers: resHeaders
});
}
function toInit(init) {
if (!init) return;
if (init instanceof Request) return {
method: init.method,
headers: init.headers,
body: init.body,
duplex: init.body ? "half" : void 0
};
return init;
}
function _toNodeStream(body) {
if (!body) return;
if (typeof body === "string") return Buffer.from(body);
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return Buffer.from(body);
if (body instanceof ReadableStream) return Readable.fromWeb(body);
if (body instanceof Blob) return Readable.fromWeb(body.stream());
return Buffer.from(String(body));
}
async function _bufferBody(body) {
if (!body) return;
if (typeof body === "string") return Buffer.from(body);
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return Buffer.from(body);
if (body instanceof ReadableStream) {
const readable = Readable.fromWeb(body);
const chunks = [];
for await (const chunk of readable) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
return Buffer.concat(chunks);
}
if (body instanceof Blob) return Buffer.from(await body.arrayBuffer());
return Buffer.from(String(body));
}
const _redirectStatuses = /* @__PURE__ */ new Set([
301,
302,
303,
307,
308
]);
function _sendRequest(doRequest, method, path, headers, addr, body, opts) {
return new Promise((resolve, reject) => {
const reqOpts = {
method,
path,
headers,
agent: opts.agent
};
if (addr.socketPath) reqOpts.socketPath = addr.socketPath;
else {
reqOpts.hostname = addr.host || "localhost";
reqOpts.port = addr.port;
}
if (opts.signal) reqOpts.signal = opts.signal;
if (opts.ssl) Object.assign(reqOpts, opts.ssl);
const req = doRequest(reqOpts, (res) => {
const statusCode = res.statusCode;
if (opts.maxRedirects > 0 && _redirectStatuses.has(statusCode) && opts.redirectCount < opts.maxRedirects && res.headers.location) {
res.resume();
const currentURL = new URL(path, `http://${addr.host || "localhost"}:${addr.port || 80}`);
const location = new URL(res.headers.location, currentURL);
const redirectHTTPS = isSSL.test(location.protocol);
const preserveMethod = statusCode === 307 || statusCode === 308;
const redirectMethod = preserveMethod ? method : "GET";
const redirectHeaders = { ...opts.originalHeaders };
redirectHeaders.host = location.host;
if (location.host !== currentURL.host) {
delete redirectHeaders.authorization;
delete redirectHeaders.cookie;
}
if (!preserveMethod) {
delete redirectHeaders["content-length"];
delete redirectHeaders["content-type"];
delete redirectHeaders["transfer-encoding"];
}
_sendRequest(redirectHTTPS ? request$1 : request, redirectMethod, location.pathname + location.search, redirectHeaders, {
host: location.hostname,
port: Number(location.port) || (redirectHTTPS ? 443 : 80)
}, preserveMethod ? body : void 0, {
...opts,
redirectCount: opts.redirectCount + 1
}).then(resolve, reject);
return;
}
resolve(res);
});
req.on("error", reject);
if (opts.timeout) req.setTimeout(opts.timeout, () => {
req.destroy(/* @__PURE__ */ new Error("Proxy request timed out"));
});
if (body instanceof Readable) {
body.on("error", (err) => {
req.destroy(err);
reject(err);
});
body.pipe(req);
} else if (body) req.end(body);
else req.end();
});
}
function proxyUpgrade(addr, req, socket, head, opts) {
const resolvedAddr = parseAddr(addr);
let useSSL = false;
if (typeof addr === "string" && !addr.startsWith("unix:")) useSSL = isSSL.test(new URL(addr).protocol);
if (req.method !== "GET" || req.headers.upgrade?.toLowerCase() !== "websocket") {
socket.destroy();
return Promise.reject(/* @__PURE__ */ new Error("Not a valid WebSocket upgrade request"));
}
if (opts?.xfwd !== false) {
const xfFor = req.headers["x-forwarded-for"];
const xfPort = req.headers["x-forwarded-port"];
const xfProto = req.headers["x-forwarded-proto"];
req.headers["x-forwarded-for"] = `${xfFor ? `${xfFor},` : ""}${req.socket?.remoteAddress}`;
req.headers["x-forwarded-port"] = `${xfPort ? `${xfPort},` : ""}${getPort(req)}`;
req.headers["x-forwarded-proto"] = `${xfProto ? `${xfProto},` : ""}${hasEncryptedConnection(req) ? "wss" : "ws"}`;
}
const target = _buildTargetURL(resolvedAddr, useSSL);
const requestOptions = {
...opts,
target,
prependPath: opts?.prependPath !== false
};
const outgoing = setupOutgoing(requestOptions.ssl || {}, requestOptions, req);
const sock = socket;
return new Promise((resolve, reject) => {
let settled = false;
setupSocket(sock);
if (head && head.length > 0) sock.unshift(head);
sock.once("error", onSocketError);
const proxyReq = (isSSL.test(target.protocol) ? request$1 : request)(outgoing);
proxyReq.once("error", onOutgoingError);
proxyReq.once("response", (res) => {
if (!res.upgrade) {
if (!sock.destroyed && sock.writable) {
sock.write(_createHttpHeader(`HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}`, res.headers));
res.on("error", onOutgoingError);
res.pipe(sock);
} else res.resume();
if (!settled) {
settled = true;
reject(/* @__PURE__ */ new Error("Upstream server did not upgrade the connection"));
}
}
});
proxyReq.once("upgrade", (proxyRes, proxySocket, proxyHead) => {
proxySocket.once("error", onOutgoingError);
sock.removeListener("error", onSocketError);
sock.once("error", () => {
proxySocket.end();
});
setupSocket(proxySocket);
if (proxyHead && proxyHead.length > 0) proxySocket.unshift(proxyHead);
sock.write(_createHttpHeader("HTTP/1.1 101 Switching Protocols", proxyRes.headers));
proxySocket.pipe(sock).pipe(proxySocket);
settled = true;
resolve(proxySocket);
});
proxyReq.end();
function onSocketError(err) {
proxyReq.destroy();
if (!settled) {
settled = true;
reject(err);
}
}
function onOutgoingError(err) {
sock.end();
if (!settled) {
settled = true;
reject(err);
}
}
});
}
function _buildTargetURL(addr, useSSL = false) {
const protocol = useSSL ? "https" : "http";
if (addr.socketPath) {
const url = new URL(`${protocol}://unix`);
url.socketPath = addr.socketPath;
return url;
}
return new URL(`${protocol}://${addr.host || "localhost"}${addr.port ? `:${addr.port}` : ""}`);
}
function _createHttpHeader(line, headers) {
let result = line;
for (const key of Object.keys(headers)) {
const value = headers[key];
if (value === void 0) continue;
if (Array.isArray(value)) for (const element of value) result += `\r\n${key}: ${element}`;
else result += `\r\n${key}: ${value}`;
}
return `${result}\r\n\r\n`;
}
export { ProxyServer, createProxyServer, proxyFetch, proxyUpgrade };
+56
View File
@@ -0,0 +1,56 @@
{
"name": "httpxy",
"version": "0.5.4",
"description": "A full-featured HTTP proxy for Node.js.",
"license": "MIT",
"repository": "unjs/httpxy",
"files": [
"dist"
],
"type": "module",
"sideEffects": false,
"types": "./dist/index.d.mts",
"exports": {
".": "./dist/index.mjs"
},
"scripts": {
"build": "obuild",
"dev": "vitest",
"test:ecosystem": "node scripts/ecosystem-test.mjs",
"lint": "oxlint . && oxfmt --check",
"fmt": "oxlint . --fix && oxfmt",
"prepack": "pnpm run build",
"release": "pnpm test && pnpm build && changelogen --release && npm publish && git push --follow-tags",
"test": "pnpm lint && pnpm typecheck && vitest run --coverage",
"typecheck": "tsgo --noEmit"
},
"devDependencies": {
"@types/async": "^3.2.25",
"@types/concat-stream": "^2.0.3",
"@types/express": "^5.0.6",
"@types/node": "^26.0.1",
"@types/semver": "^7.7.1",
"@types/sse": "^0.0.0",
"@types/ws": "^8.18.1",
"@typescript/native-preview": "latest",
"@vitest/coverage-v8": "^4.1.9",
"async": "^3.2.6",
"changelogen": "^0.6.2",
"concat-stream": "^2.0.0",
"eslint-config-unjs": "^0.6.2",
"expect.js": "^0.3.1",
"obuild": "^0.4.37",
"ofetch": "^1.5.1",
"oxfmt": "^0.56.0",
"oxlint": "^1.71.0",
"semver": "^7.8.5",
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"sse": "^0.0.8",
"typescript": "^6.0.3",
"undici": "^8.5.0",
"vitest": "^4.1.9",
"ws": "^8.21.0"
},
"packageManager": "pnpm@11.1.3"
}