Files
3dverse-testtask/vendor/deno.land/x/oak@v12.1.0/middleware/proxy.ts
T

274 lines
8.4 KiB
TypeScript

// Copyright 2018-2022 the oak authors. All rights reserved. MIT license.
import type { State } from "../application.ts";
import type { Context } from "../context.ts";
import type { Middleware } from "../middleware.ts";
import type {
RouteParams,
RouterContext,
RouterMiddleware,
} from "../router.ts";
import { isRouterContext } from "../util.ts";
export type Fetch = (input: Request) => Promise<Response>;
export type ProxyMatchFunction<
R extends string,
P extends RouteParams<R> = RouteParams<R>,
// deno-lint-ignore no-explicit-any
S extends State = Record<string, any>,
> = (ctx: Context<S> | RouterContext<R, P, S>) => boolean;
export type ProxyMapFunction<R extends string, P extends RouteParams<R>> = (
path: R,
params?: P,
) => R;
export type ProxyHeadersFunction<S extends State> = (
ctx: Context<S>,
) => HeadersInit | Promise<HeadersInit>;
export type ProxyRouterHeadersFunction<
R extends string,
P extends RouteParams<R>,
S extends State,
> = (ctx: RouterContext<R, P, S>) => HeadersInit | Promise<HeadersInit>;
export interface ProxyOptions<
R extends string,
P extends RouteParams<R> = RouteParams<R>,
// deno-lint-ignore no-explicit-any
S extends State = Record<string, any>,
> {
/** A callback hook that is called after the response is received which allows
* the response content type to be adjusted. This is for situations where the
* content type provided by the proxy server might not be suitable for
* responding with. */
contentType?(
url: string,
contentType?: string,
): Promise<string | undefined> | string | undefined;
/** The fetch function to use to proxy the request. This defaults to the
* global `fetch` function. This is designed for test mocking purposes. */
fetch?: Fetch;
/** Additional headers that should be set in the response. The value can
* be a headers init value or a function that returns or resolves with a
* headers init value. */
headers?:
| HeadersInit
| ProxyHeadersFunction<S>
| ProxyRouterHeadersFunction<R, P, S>;
/** Either a record or a proxy map function that will allow proxied requests
* being handled by the middleware to be remapped to a different remote
* path. */
map?: Record<string, R> | ProxyMapFunction<R, P>;
/** A string, regular expression or proxy match function what determines if
* the proxy middleware should proxy the request.
*
* If the value is a string the match will be true if the requests pathname
* starts with the string. In the case of a regular expression, if the
* pathname
*/
match?:
| string
| RegExp
| ProxyMatchFunction<R, P, S>;
/** A flag that indicates if traditional proxy headers should be set in the
* response. This defaults to `true`.
*/
proxyHeaders?: boolean;
/** A callback hook which will be called before each proxied fetch request
* to allow the native `Request` to be modified or replaced. */
request?(req: Request): Request | Promise<Request>;
/** A callback hook which will be called after each proxied fetch response
* is received to allow the native `Response` to be modified or replaced. */
response?(res: Response): Response | Promise<Response>;
}
const FORWARDED_RE =
/^(,[ \\t]*)*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*([ \\t]*,([ \\t]*([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?(;([!#$%&'*+.^_`|~0-9A-Za-z-]+=([!#$%&'*+.^_`|~0-9A-Za-z-]+|\"([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\"))?)*)?)*$/;
function createMatcher<
R extends string,
P extends RouteParams<R>,
S extends State,
>(
{ match }: ProxyOptions<R, P, S>,
) {
return function matches(ctx: RouterContext<R, P, S>): boolean {
if (!match) {
return true;
}
if (typeof match === "string") {
return ctx.request.url.pathname.startsWith(match);
}
if (match instanceof RegExp) {
return match.test(ctx.request.url.pathname);
}
return match(ctx);
};
}
async function createRequest<
R extends string,
P extends RouteParams<R>,
S extends State,
>(
target: string | URL,
ctx: Context<S> | RouterContext<R, P, S>,
{ headers: optHeaders, map, proxyHeaders = true, request: reqFn }:
ProxyOptions<R, P, S>,
): Promise<Request> {
let path = ctx.request.url.pathname as R;
let params: P | undefined;
if (isRouterContext<R, P, S>(ctx)) {
params = ctx.params;
}
if (map && typeof map === "function") {
path = map(path, params);
} else if (map) {
path = map[path] ?? path;
}
const url = new URL(String(target));
if (url.pathname.endsWith("/") && path.startsWith("/")) {
url.pathname = `${url.pathname}${path.slice(1)}`;
} else if (!url.pathname.endsWith("/") && !path.startsWith("/")) {
url.pathname = `${url.pathname}/${path}`;
} else {
url.pathname = `${url.pathname}${path}`;
}
url.search = ctx.request.url.search;
const body = getBodyInit(ctx);
const headers = new Headers(ctx.request.headers);
if (optHeaders) {
if (typeof optHeaders === "function") {
optHeaders = await optHeaders(ctx as RouterContext<R, P, S>);
}
for (const [key, value] of iterableHeaders(optHeaders)) {
headers.set(key, value);
}
}
if (proxyHeaders) {
const maybeForwarded = headers.get("forwarded");
const ip = ctx.request.ip.startsWith("[")
? `"${ctx.request.ip}"`
: ctx.request.ip;
const host = headers.get("host");
if (maybeForwarded && FORWARDED_RE.test(maybeForwarded)) {
let value = `for=${ip}`;
if (host) {
value += `;host=${host}`;
}
headers.append("forwarded", value);
} else {
headers.append("x-forwarded-for", ip);
if (host) {
headers.append("x-forwarded-host", host);
}
}
}
const init: RequestInit = {
body,
headers,
method: ctx.request.method,
redirect: "follow",
};
let request = new Request(url.toString(), init);
if (reqFn) {
request = await reqFn(request);
}
return request;
}
function getBodyInit<
R extends string,
P extends RouteParams<R>,
S extends State,
>(
ctx: Context<S> | RouterContext<R, P, S>,
): BodyInit | null {
if (!ctx.request.hasBody) {
return null;
}
return ctx.request.body({ type: "stream" }).value;
}
function iterableHeaders(
headers: HeadersInit,
): IterableIterator<[string, string]> {
if (headers instanceof Headers) {
return headers.entries();
} else if (Array.isArray(headers)) {
return headers.values() as IterableIterator<[string, string]>;
} else {
return Object.entries(headers).values() as IterableIterator<
[string, string]
>;
}
}
async function processResponse<
R extends string,
P extends RouteParams<R>,
S extends State,
>(
response: Response,
ctx: Context<S> | RouterContext<R, P, S>,
{ contentType: contentTypeFn, response: resFn }: ProxyOptions<R, P, S>,
) {
if (resFn) {
response = await resFn(response);
}
if (response.body) {
ctx.response.body = response.body;
} else {
ctx.response.body = null;
}
ctx.response.status = response.status;
for (const [key, value] of response.headers) {
ctx.response.headers.append(key, value);
}
if (contentTypeFn) {
const value = await contentTypeFn(
response.url,
ctx.response.headers.get("content-type") ?? undefined,
);
if (value != null) {
ctx.response.headers.set("content-type", value);
}
}
}
/**
* Middleware that provides a back-to-back proxy for requests.
*
* @param target
* @param options
*/
export function proxy<S extends State>(
target: string | URL,
options?: ProxyOptions<string, RouteParams<string>, S>,
): Middleware<S>;
export function proxy<
R extends string,
P extends RouteParams<R>,
S extends State,
>(
target: string | URL,
options: ProxyOptions<R, P, S> = {},
): RouterMiddleware<R, P, S> {
const matches = createMatcher(options);
return async function proxy(ctx, next) {
if (!matches(ctx)) {
return next();
}
const request = await createRequest(target, ctx, options);
const { fetch = globalThis.fetch } = options;
const response = await fetch(request);
await processResponse(response, ctx, options);
return next();
};
}