Handle authentication with proxy
The cookie will be set for the proxy domain so it'll work for all of its subdomains.
This commit is contained in:
parent
90fd1f7dd1
commit
3a98d856a5
@ -1,50 +1,52 @@
|
|||||||
import * as http from "http"
|
import * as http from "http"
|
||||||
import { HttpCode, HttpError } from "../../common/http"
|
import { HttpCode, HttpError } from "../../common/http"
|
||||||
import { AuthType, HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
import { HttpProvider, HttpProviderOptions, HttpProxyProvider, HttpResponse, Route } from "../http"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Proxy HTTP provider.
|
* Proxy HTTP provider.
|
||||||
*/
|
*/
|
||||||
export class ProxyHttpProvider extends HttpProvider {
|
export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider {
|
||||||
public constructor(options: HttpProviderOptions, private readonly proxyDomains: string[]) {
|
public readonly proxyDomains: string[]
|
||||||
|
|
||||||
|
public constructor(options: HttpProviderOptions, proxyDomains: string[] = []) {
|
||||||
super(options)
|
super(options)
|
||||||
|
this.proxyDomains = proxyDomains.map((d) => d.replace(/^\*\./, "")).filter((d, i, arr) => arr.indexOf(d) === i)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleRequest(route: Route): Promise<HttpResponse> {
|
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||||
if (this.options.auth !== AuthType.Password || route.requestPath !== "/index.html") {
|
if (!this.authenticated(request)) {
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
if (route.requestPath === "/index.html") {
|
||||||
|
return { redirect: "/login", query: { to: route.fullPath } }
|
||||||
|
}
|
||||||
|
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = this.proxy(route.base.replace(/^\//, ""))
|
const payload = this.proxy(route.base.replace(/^\//, ""))
|
||||||
if (!payload) {
|
if (payload) {
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
return payload
|
||||||
}
|
}
|
||||||
return payload
|
|
||||||
|
throw new HttpError("Not found", HttpCode.NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRoot(route: Route, error?: Error): Promise<HttpResponse> {
|
public getProxyDomain(host?: string): string | undefined {
|
||||||
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/login.html")
|
|
||||||
response.content = response.content.replace(/{{ERROR}}/, error ? `<div class="error">${error.message}</div>` : "")
|
|
||||||
return this.replaceTemplates(route, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a response if the request should be proxied. Anything that ends in a
|
|
||||||
* proxy domain and has a subdomain should be proxied. The port is found in
|
|
||||||
* the top-most subdomain.
|
|
||||||
*
|
|
||||||
* For example, if the proxy domain is `coder.com` then `8080.coder.com` and
|
|
||||||
* `test.8080.coder.com` will both proxy to `8080` but `8080.test.coder.com`
|
|
||||||
* will have an error because `test` isn't a port. If the proxy domain was
|
|
||||||
* `test.coder.com` then it would work.
|
|
||||||
*/
|
|
||||||
public maybeProxy(request: http.IncomingMessage): HttpResponse | undefined {
|
|
||||||
const host = request.headers.host
|
|
||||||
if (!host || !this.proxyDomains) {
|
if (!host || !this.proxyDomains) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxyDomain = this.proxyDomains.find((d) => host.endsWith(d))
|
return this.proxyDomains.find((d) => host.endsWith(d))
|
||||||
if (!proxyDomain) {
|
}
|
||||||
|
|
||||||
|
public maybeProxy(request: http.IncomingMessage): HttpResponse | undefined {
|
||||||
|
// No proxy until we're authenticated. This will cause the login page to
|
||||||
|
// show as well as let our assets keep loading normally.
|
||||||
|
if (!this.authenticated(request)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = request.headers.host
|
||||||
|
const proxyDomain = this.getProxyDomain(host)
|
||||||
|
if (!host || !proxyDomain) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,19 +56,11 @@ const main = async (args: Args): Promise<void> => {
|
|||||||
throw new Error("--cert-key is missing")
|
throw new Error("--cert-key is missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Domains can be in the form `coder.com` or `*.coder.com`. Either way,
|
|
||||||
* `[number].coder.com` will be proxied to `number`.
|
|
||||||
*/
|
|
||||||
const proxyDomains = args["proxy-domain"]
|
|
||||||
? args["proxy-domain"].map((d) => d.replace(/^\*\./, "")).filter((d, i, arr) => arr.indexOf(d) === i)
|
|
||||||
: []
|
|
||||||
|
|
||||||
const httpServer = new HttpServer(options)
|
const httpServer = new HttpServer(options)
|
||||||
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
|
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
|
||||||
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
|
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
|
||||||
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"])
|
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"])
|
||||||
const proxy = httpServer.registerHttpProvider("/proxy", ProxyHttpProvider, proxyDomains)
|
const proxy = httpServer.registerHttpProvider("/proxy", ProxyHttpProvider, args["proxy-domain"])
|
||||||
httpServer.registerHttpProvider("/login", LoginHttpProvider)
|
httpServer.registerHttpProvider("/login", LoginHttpProvider)
|
||||||
httpServer.registerHttpProvider("/static", StaticHttpProvider)
|
httpServer.registerHttpProvider("/static", StaticHttpProvider)
|
||||||
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
|
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
|
||||||
@ -102,11 +94,11 @@ const main = async (args: Args): Promise<void> => {
|
|||||||
logger.info(" - Not serving HTTPS")
|
logger.info(" - Not serving HTTPS")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (proxyDomains.length === 1) {
|
if (proxy.proxyDomains.length === 1) {
|
||||||
logger.info(` - Proxying *.${proxyDomains[0]}`)
|
logger.info(` - Proxying *.${proxy.proxyDomains[0]}`)
|
||||||
} else if (proxyDomains && proxyDomains.length > 1) {
|
} else if (proxy.proxyDomains.length > 1) {
|
||||||
logger.info(" - Proxying the following domains:")
|
logger.info(" - Proxying the following domains:")
|
||||||
proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
|
proxy.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`)
|
logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`)
|
||||||
|
@ -395,7 +395,29 @@ export interface HttpProvider3<A1, A2, A3, T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface HttpProxyProvider {
|
export interface HttpProxyProvider {
|
||||||
|
/**
|
||||||
|
* Return a response if the request should be proxied. Anything that ends in a
|
||||||
|
* proxy domain and has a subdomain should be proxied. The port is found in
|
||||||
|
* the top-most subdomain.
|
||||||
|
*
|
||||||
|
* For example, if the proxy domain is `coder.com` then `8080.coder.com` and
|
||||||
|
* `test.8080.coder.com` will both proxy to `8080` but `8080.test.coder.com`
|
||||||
|
* will have an error because `test` isn't a port. If the proxy domain was
|
||||||
|
* `test.coder.com` then it would work.
|
||||||
|
*/
|
||||||
maybeProxy(request: http.IncomingMessage): HttpResponse | undefined
|
maybeProxy(request: http.IncomingMessage): HttpResponse | undefined
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the matching proxy domain based on the provided host.
|
||||||
|
*/
|
||||||
|
getProxyDomain(host: string): string | undefined
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domains can be provided in the form `coder.com` or `*.coder.com`. Either
|
||||||
|
* way, `<number>.coder.com` will be proxied to `number`. The domains are
|
||||||
|
* stored here without the `*.`.
|
||||||
|
*/
|
||||||
|
readonly proxyDomains: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -538,7 +560,13 @@ export class HttpServer {
|
|||||||
"Set-Cookie": [
|
"Set-Cookie": [
|
||||||
`${payload.cookie.key}=${payload.cookie.value}`,
|
`${payload.cookie.key}=${payload.cookie.value}`,
|
||||||
`Path=${normalize(payload.cookie.path || "/", true)}`,
|
`Path=${normalize(payload.cookie.path || "/", true)}`,
|
||||||
request.headers.host ? `Domain=${request.headers.host}` : undefined,
|
// Set the cookie against the host so it can be used in
|
||||||
|
// subdomains. Use a matching proxy domain if possible so
|
||||||
|
// requests to any of those subdomains will already be
|
||||||
|
// authenticated.
|
||||||
|
request.headers.host
|
||||||
|
? `Domain=${(this.proxy && this.proxy.getProxyDomain(request.headers.host)) || request.headers.host}`
|
||||||
|
: undefined,
|
||||||
// "HttpOnly",
|
// "HttpOnly",
|
||||||
"SameSite=strict",
|
"SameSite=strict",
|
||||||
]
|
]
|
||||||
@ -566,8 +594,8 @@ export class HttpServer {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const payload =
|
const payload =
|
||||||
(this.proxy && this.proxy.maybeProxy(request)) ||
|
|
||||||
this.maybeRedirect(request, route) ||
|
this.maybeRedirect(request, route) ||
|
||||||
|
(this.proxy && this.proxy.maybeProxy(request)) ||
|
||||||
(await route.provider.handleRequest(route, request))
|
(await route.provider.handleRequest(route, request))
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
throw new HttpError("Not found", HttpCode.NotFound)
|
||||||
|
Reference in New Issue
Block a user