diff --git a/src/node/proxy.ts b/src/node/proxy.ts index d59df1d3..da430f5b 100644 --- a/src/node/proxy.ts +++ b/src/node/proxy.ts @@ -1,10 +1,8 @@ -import { Request, Router } from "express" import proxyServer from "http-proxy" -import { HttpCode, HttpError } from "../common/http" -import { authenticated, ensureAuthenticated, redirect } from "./http" -import { Router as WsRouter } from "./wsRouter" +import { HttpCode } from "../common/http" export const proxy = proxyServer.createProxyServer({}) + proxy.on("error", (error, _, res) => { res.writeHead(HttpCode.ServerError) res.end(error.message) @@ -16,85 +14,3 @@ proxy.on("proxyRes", (res, req) => { res.headers.location = (req as any).base + res.headers.location } }) - -export const router = Router() - -/** - * Return the port if the request should be proxied. Anything that ends in a - * proxy domain and has a *single* subdomain should be proxied. Anything else - * should return `undefined` and will be handled as normal. - * - * For example if `coder.com` is specified `8080.coder.com` will be proxied - * but `8080.test.coder.com` and `test.8080.coder.com` will not. - */ -const maybeProxy = (req: Request): string | undefined => { - // Split into parts. - const host = req.headers.host || "" - const idx = host.indexOf(":") - const domain = idx !== -1 ? host.substring(0, idx) : host - const parts = domain.split(".") - - // There must be an exact match. - const port = parts.shift() - const proxyDomain = parts.join(".") - if (!port || !req.args["proxy-domain"].includes(proxyDomain)) { - return undefined - } - - return port -} - -router.all("*", (req, res, next) => { - const port = maybeProxy(req) - if (!port) { - return next() - } - - // Must be authenticated to use the proxy. - if (!authenticated(req)) { - // 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. - if (typeof req.headers.accepts === "string" && req.headers.accepts.split(",").includes("text/html")) { - // Let the login through. - if (/\/login\/?/.test(req.path)) { - return next() - } - // Redirect all other pages to the login. - return redirect(req, res, "login", { - to: req.path, - }) - } - - // 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("*", (req, _, next) => { - const port = maybeProxy(req) - if (!port) { - return next() - } - - // Must be authenticated to use the proxy. - ensureAuthenticated(req) - - proxy.ws(req, req.ws, req.head, { - ignorePath: true, - target: `http://0.0.0.0:${port}${req.originalUrl}`, - }) -}) diff --git a/src/node/routes/domainProxy.ts b/src/node/routes/domainProxy.ts new file mode 100644 index 00000000..ac249b80 --- /dev/null +++ b/src/node/routes/domainProxy.ts @@ -0,0 +1,87 @@ +import { Request, Router } from "express" +import { HttpCode, HttpError } from "../../common/http" +import { authenticated, ensureAuthenticated, redirect } from "../http" +import { proxy } from "../proxy" +import { Router as WsRouter } from "../wsRouter" + +export const router = Router() + +/** + * Return the port if the request should be proxied. Anything that ends in a + * proxy domain and has a *single* subdomain should be proxied. Anything else + * should return `undefined` and will be handled as normal. + * + * For example if `coder.com` is specified `8080.coder.com` will be proxied + * but `8080.test.coder.com` and `test.8080.coder.com` will not. + */ +const maybeProxy = (req: Request): string | undefined => { + // Split into parts. + const host = req.headers.host || "" + const idx = host.indexOf(":") + const domain = idx !== -1 ? host.substring(0, idx) : host + const parts = domain.split(".") + + // There must be an exact match. + const port = parts.shift() + const proxyDomain = parts.join(".") + if (!port || !req.args["proxy-domain"].includes(proxyDomain)) { + return undefined + } + + return port +} + +router.all("*", (req, res, next) => { + const port = maybeProxy(req) + if (!port) { + return next() + } + + // Must be authenticated to use the proxy. + if (!authenticated(req)) { + // 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. + if (typeof req.headers.accepts === "string" && req.headers.accepts.split(",").includes("text/html")) { + // Let the login through. + if (/\/login\/?/.test(req.path)) { + return next() + } + // Redirect all other pages to the login. + return redirect(req, res, "login", { + to: req.path, + }) + } + + // 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("*", (req, _, next) => { + const port = maybeProxy(req) + if (!port) { + return next() + } + + // Must be authenticated to use the proxy. + ensureAuthenticated(req) + + proxy.ws(req, req.ws, req.head, { + ignorePath: true, + target: `http://0.0.0.0:${port}${req.originalUrl}`, + }) +}) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 4643aa13..afb24f15 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -13,12 +13,12 @@ import { rootPath } from "../constants" import { Heart } from "../heart" import { replaceTemplates } from "../http" import { loadPlugins } from "../plugin" -import * as domainProxy from "../proxy" import { getMediaMime, paths } from "../util" import { WebsocketRequest } from "../wsRouter" +import * as domainProxy from "./domainProxy" import * as health from "./health" import * as login from "./login" -import * as proxy from "./proxy" +import * as proxy from "./pathProxy" // static is a reserved keyword. import * as _static from "./static" import * as update from "./update" diff --git a/src/node/routes/proxy.ts b/src/node/routes/pathProxy.ts similarity index 90% rename from src/node/routes/proxy.ts rename to src/node/routes/pathProxy.ts index ff6f4067..d21d08eb 100644 --- a/src/node/routes/proxy.ts +++ b/src/node/routes/pathProxy.ts @@ -1,7 +1,7 @@ import { Request, Router } from "express" import qs from "qs" import { HttpCode, HttpError } from "../../common/http" -import { authenticated, redirect } from "../http" +import { authenticated, ensureAuthenticated, redirect } from "../http" import { proxy } from "../proxy" import { Router as WsRouter } from "../wsRouter" @@ -38,7 +38,7 @@ router.all("/(:port)(/*)?", (req, res) => { export const wsRouter = WsRouter() -wsRouter.ws("/(:port)(/*)?", (req) => { +wsRouter.ws("/(:port)(/*)?", ensureAuthenticated, (req) => { proxy.ws(req, req.ws, req.head, { ignorePath: true, target: getProxyTarget(req, true),