diff --git a/src/bridge/bridge.ts b/src/bridge/bridge.ts index 1530496c..2437d641 100644 --- a/src/bridge/bridge.ts +++ b/src/bridge/bridge.ts @@ -1,3 +1,11 @@ +/** + * @module fly + */ + +/** + * @private + */ + import './proxy_stream' import './fetch' import './heap' @@ -16,15 +24,24 @@ import { MemoryCacheStore } from '../memory_cache_store'; const errNoSuchBridgeFn = "Attempted to call a unregistered bridge function." +/** + * @private + */ interface IterableIterator extends Iterator { [Symbol.iterator](): IterableIterator; } +/** + * @private + */ export interface BridgeOptions { cacheStore?: CacheStore fileStore?: FileStore } +/** + * @private + */ export class Bridge { cacheStore: CacheStore fileStore?: FileStore diff --git a/src/utils/build.ts b/src/utils/build.ts index b6d371b6..97291421 100644 --- a/src/utils/build.ts +++ b/src/utils/build.ts @@ -97,7 +97,8 @@ export function getWebpackConfig(cwd: string, opts?: AppBuilderOptions): webpack conf.resolve = Object.assign({ alias: Object.assign({}, conf.resolve.alias, { - "@fly/image": v8EnvPath + "/fly/image" + "@fly/image": v8EnvPath + "/fly/image", + "@fly/proxy": v8EnvPath + "/fly/proxy" }) }, conf.resolve) diff --git a/v8env/ts/bridge.d.ts b/v8env/ts/bridge.d.ts index 300be2e0..9766ae1a 100644 --- a/v8env/ts/bridge.d.ts +++ b/v8env/ts/bridge.d.ts @@ -1 +1,4 @@ +/** + * @module fly + */ declare const bridge: any \ No newline at end of file diff --git a/v8env/ts/fly/proxy.ts b/v8env/ts/fly/proxy.ts new file mode 100644 index 00000000..343fbc05 --- /dev/null +++ b/v8env/ts/fly/proxy.ts @@ -0,0 +1,116 @@ +/** + * @module fly/proxy + * Library for proxying requests to origins. + */ +/** + * This generates a `fetch` like function for proxying requests to a given origin. + * When this function makes origin requests, it adds standard proxy headers like + * `X-Forwarded-Host` and `X-Forwarded-For`. It also passes headers from the original + * request to the origin. + * @param origin A URL to an origin, can include a path to rebase requests. + * @param options Options and headers to control origin request. + */ +export default function proxy(origin: string | URL, options?: ProxyOptions) { + return function proxyFetch(req: RequestInfo, init?: RequestInit) { + if (!options) { + options = {} + } + const breq = buildProxyRequest(origin, options, req, init) + return fetch(breq) + } +} + +interface FlyRequest extends Request { + url: string +} + +/** + * @protected + * @hidden + * @param origin + * @param options + * @param req + * @param init + */ +export function buildProxyRequest(origin: string | URL, options: ProxyOptions, req: RequestInfo, init?: RequestInit) { + + if (typeof req === "string") { + req = new Request(req) + } + const url = new URL(req.url) + let breq: FlyRequest | null = null + + if (req instanceof Request) { + breq = req.clone() + } else { + breq = new Request(req) + } + + if (typeof origin === "string") { + origin = new URL(origin) + } + + url.hostname = origin.hostname + url.protocol = origin.protocol + url.port = origin.port + + if (options.stripPath && typeof options.stripPath === 'string') { + // remove basePath so we can serve `onehosthame.com/dir/` from `origin.com/` + url.pathname = url.pathname.substring(options.stripPath.length) + } + if (origin.pathname && origin.pathname.length > 0) { + url.pathname = [origin.pathname.replace(/\/$/, ''), url.pathname.replace(/^\//, "")].join("/") + } + if (url.pathname.startsWith("//")) { + url.pathname = url.pathname.substring(1) + } + + breq.url = url.toString() + // we extend req with remoteAddr + breq.headers.set("x-forwarded-for", (req).remoteAddr) + breq.headers.set("x-forwarded-host", url.hostname) + + if (options.headers) { + for (const h of Object.getOwnPropertyNames(options.headers)) { + const v = options.headers[h] + if (v === false) { + breq.headers.delete(h) + } else if (v && typeof v === "string") { + breq.headers.set(h, v) + } + } + } + return breq +} + +/** + * Options for `proxy`. + */ +export interface ProxyOptions { + /** + * Replace this portion of URL path before making request to origin. + * + * For example, this makes a request to `https://fly.io/path1/to/document.html`: + * ```javascript + * const opts = { rewritePath: "/path2/"} + * const origin = proxy("https://fly.io/path1/", opts) + * origin("https://somehostname.com/path2/to/document.html") + * ``` + */ + stripPath?: string, + + /** + * Headers to set on backend request. Each header accepts either a `boolean` or `string`. + * * If set to `false`, strip header entirely before sending. + * * `true` or `undefined` send the header through unmodified from the original request. + * * `string` header values are sent as is + */ + headers?: { + [key: string]: string | boolean | undefined, + /** + * Host header to set before sending origin request. Some sites only respond to specific + * host headers. + */ + host?: string | boolean + } +} \ No newline at end of file diff --git a/v8env_test/proxy.spec.js b/v8env_test/proxy.spec.js new file mode 100644 index 00000000..e974afc3 --- /dev/null +++ b/v8env_test/proxy.spec.js @@ -0,0 +1,20 @@ +import { expect } from 'chai' + +import * as proxy from '@fly/proxy' + +const origin = "https://fly.io/proxy/" +const req = new Request("https://wat.com/path/to/thing", { headers: { "host": "wut.com" } }) +describe("proxy", () => { + it('includes host header and base path properly', () => { + const breq = proxy.buildProxyRequest(origin, {}, req) + const url = new URL(breq.url) + expect(breq.headers.get("host")).to.eq("wut.com") + expect(url.pathname).to.eq("/proxy/path/to/thing") + }) + + it('rewrite paths properly', () => { + const breq = proxy.buildProxyRequest(origin, { rewritePath: "/path/to/" }, req) + const url = new URL(breq.url) + expect(url.pathname).to.eq("/proxy/thing") + }) +}) \ No newline at end of file