Started to implement server
This commit is contained in:
1 parent
1f2ea8b7e5
commit
17a1b4bed3
177 files changed
+29894
-74
No files matched your search
+273
@@ -0,0 +1,273 @@
|
||||
// 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();
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user