| 1 | import { defineConfig } from './config' |
| 2 | import { |
| 3 | ADMIN_URI, API_URI, Callback, CFG, isLocalHost, join, makeMatcher, removeStarting, SPECIAL_URI, try_, |
| 4 | enforceFinal, enforceStarting |
| 5 | } from './misc' |
| 6 | import Koa from 'koa' |
| 7 | import { disconnect } from './connections' |
| 8 | import { baseUrl } from './listen' |
| 9 | import _ from 'lodash' |
| 10 | |
| 11 | export const roots = defineConfig(CFG.roots, {} as { [hostMask: string]: string }, map => { |
| 12 | const list = Object.keys(map) |
| 13 | const matchers = list.map(hostMask => makeMatcher(hostMask)) |
| 14 | const values = Object.values(map).map(x => enforceFinal('/', enforceStarting('/', x.replace(/\/{2,}/g, '/')))) |
| 15 | return (host: string) => values[matchers.findIndex(m => m(host))] |
| 16 | }) |
| 17 | const forceAddress = defineConfig(CFG.force_address, false) |
| 18 | |
| 19 | export const rootsMiddleware: Koa.Middleware = (ctx, next) => |
| 20 | (() => { |
| 21 | if (!ctx.path) // it was once reported "Cannot read properties of null (reading 'startsWith')" but I can't reproduce it, and it shouldn't happen anyway |
| 22 | return disconnect(ctx, 'no path') // let's assume that such requests can't be served anyway. This will leave a trace with an ip, anyway |
| 23 | ctx.state.originalPath = ctx.path |
| 24 | let params: undefined | typeof ctx.state.params | typeof ctx.query // undefined if we are not going to work on api parameters |
| 25 | if (ctx.path.startsWith(SPECIAL_URI)) { // special uris should be excluded... |
| 26 | if (!ctx.path.startsWith(API_URI)) return // ...unless it's an api |
| 27 | params = ctx.state.params || ctx.query // for api we'll translate params |
| 28 | changeUriParams(v => removeStarting(ctx.state.revProxyPath, v)) // this removal must be done before adding the root; this operation doesn't conceptually belong to "roots", and it may be placed in different middleware, but it's convenient to do it here |
| 29 | const { referer } = ctx.headers |
| 30 | if (referer && try_(() => new URL(referer).pathname.startsWith(ctx.state.revProxyPath + ADMIN_URI))) return // exclude apis for admin-panel |
| 31 | } |
| 32 | if (_.isEmpty(roots.get())) return |
| 33 | const root = ctx.state.root = roots.compiled()(ctx.host) |
| 34 | if (!ctx.state.skipFilters && forceAddress.get() |
| 35 | && root === undefined && !isLocalHost(ctx) && ctx.host !== baseUrl.compiled()) |
| 36 | return disconnect(ctx, forceAddress.key()) // returning truthy will not call next |
| 37 | if (!root || root === '/') return // no transformation is required |
| 38 | changeUriParams(v => join(root, v)) |
| 39 | if (!params) |
| 40 | ctx.path = join(root, ctx.path) |
| 41 | |
| 42 | function changeUriParams(cb: Callback<string, string>) { |
| 43 | if (!params) return |
| 44 | for (const [k, v] of Object.entries(params)) |
| 45 | if (k.startsWith('uri')) |
| 46 | params[k] = Array.isArray(v) ? v.map(cb) : _.isString(v) ? cb(v) : v |
| 47 | } |
| 48 | })() || next() |
| 49 | |
| 50 | declare module "koa" { |
| 51 | interface DefaultState { |
| 52 | originalPath: string // before roots is applied |
| 53 | root?: string |
| 54 | } |
| 55 | } |