114 lines
3.4 KiB
TypeScript
114 lines
3.4 KiB
TypeScript
import { Request, Router } from "express"
|
|
import { HttpCode, HttpError } from "../../common/http"
|
|
import { getHost, ensureProxyEnabled, authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http"
|
|
import { proxy } from "../proxy"
|
|
import { Router as WsRouter } from "../wsRouter"
|
|
|
|
export const router = Router()
|
|
|
|
const proxyDomainToRegex = (matchString: string): RegExp => {
|
|
const escapedMatchString = matchString.replace(/[.*+?^$()|[\]\\]/g, "\\$&")
|
|
|
|
// Replace {{port}} with a regex group to capture the port
|
|
// Replace {{host}} with .+ to allow any host match (so rely on DNS record here)
|
|
let regexString = escapedMatchString.replace("{{port}}", "(\\d+)")
|
|
regexString = regexString.replace("{{host}}", ".+")
|
|
|
|
regexString = regexString.replace(/[{}]/g, "\\$&") //replace any '{}' that might be left
|
|
|
|
return new RegExp("^" + regexString + "$")
|
|
}
|
|
|
|
let proxyRegexes: RegExp[] = []
|
|
const proxyDomainsToRegex = (proxyDomains: string[]): RegExp[] => {
|
|
if (proxyDomains.length !== proxyRegexes.length) {
|
|
proxyRegexes = proxyDomains.map(proxyDomainToRegex)
|
|
}
|
|
return proxyRegexes
|
|
}
|
|
|
|
/**
|
|
* Return the port if the request should be proxied.
|
|
*
|
|
* The proxy-domain should be of format anyprefix-{{port}}-anysuffix.{{host}}, where {{host}} is optional
|
|
* e.g. code-8080.domain.tld would match for code-{{port}}.domain.tld and code-{{port}}.{{host}}.
|
|
*
|
|
*/
|
|
const maybeProxy = (req: Request): string | undefined => {
|
|
const reqDomain = getHost(req)
|
|
if (reqDomain === undefined) {
|
|
return undefined
|
|
}
|
|
|
|
const regexs = proxyDomainsToRegex(req.args["proxy-domain"])
|
|
|
|
for (const regex of regexs) {
|
|
const match = reqDomain.match(regex)
|
|
|
|
if (match) {
|
|
return match[1] // match[1] contains the port
|
|
}
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
router.all(/.*/, async (req, res, next) => {
|
|
const port = maybeProxy(req)
|
|
if (!port) {
|
|
return next()
|
|
}
|
|
|
|
ensureProxyEnabled(req)
|
|
|
|
// Must be authenticated to use the proxy.
|
|
const isAuthenticated = await authenticated(req)
|
|
if (!isAuthenticated) {
|
|
// Let the assets through since they're used on the login page.
|
|
if (req.path.startsWith("/_static/") && req.method === "GET") {
|
|
return next()
|
|
}
|
|
|
|
// Assume anything that explicitly accepts text/html is a user browsing a
|
|
// page (as opposed to an xhr request). Don't use `req.accepts()` since
|
|
// *every* request that I've seen (in Firefox and Chromium at least)
|
|
// includes `*/*` making it always truthy. Even for css/javascript.
|
|
if (req.headers.accept && req.headers.accept.includes("text/html")) {
|
|
// Let the login through.
|
|
if (/\/login\/?/.test(req.path)) {
|
|
return next()
|
|
}
|
|
// Redirect all other pages to the login.
|
|
const to = self(req)
|
|
return redirect(req, res, "login", {
|
|
to: to !== "/" ? to : undefined,
|
|
})
|
|
}
|
|
|
|
// Everything else gets an unauthorized message.
|
|
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
|
|
}
|
|
|
|
proxy.web(req, res, {
|
|
ignorePath: true,
|
|
target: `http://0.0.0.0:${port}${req.originalUrl}`,
|
|
})
|
|
})
|
|
|
|
export const wsRouter = WsRouter()
|
|
|
|
wsRouter.ws(/.*/, async (req, _, next) => {
|
|
const port = maybeProxy(req)
|
|
if (!port) {
|
|
return next()
|
|
}
|
|
|
|
ensureProxyEnabled(req)
|
|
ensureOrigin(req)
|
|
await ensureAuthenticated(req)
|
|
proxy.ws(req, req.ws, req.head, {
|
|
ignorePath: true,
|
|
target: `http://0.0.0.0:${port}${req.originalUrl}`,
|
|
})
|
|
})
|