Skip to content

Commit 198c113

Browse files
committed
Handle authentication with proxy
The cookie will be set for the proxy domain so it'll work for all of its subdomains.
1 parent 679e858 commit 198c113

File tree

3 files changed

+64
-42
lines changed

3 files changed

+64
-42
lines changed

src/node/app/proxy.ts

+29-27
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,52 @@
11
import * as http from "http"
22
import { HttpCode, HttpError } from "../../common/http"
3-
import { AuthType, HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
3+
import { HttpProvider, HttpProviderOptions, HttpProxyProvider, HttpResponse, Route } from "../http"
44

55
/**
66
* Proxy HTTP provider.
77
*/
8-
export class ProxyHttpProvider extends HttpProvider {
9-
public constructor(options: HttpProviderOptions, private readonly proxyDomains: string[]) {
8+
export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider {
9+
public readonly proxyDomains: string[]
10+
11+
public constructor(options: HttpProviderOptions, proxyDomains: string[] = []) {
1012
super(options)
13+
this.proxyDomains = proxyDomains.map((d) => d.replace(/^\*\./, "")).filter((d, i, arr) => arr.indexOf(d) === i)
1114
}
1215

13-
public async handleRequest(route: Route): Promise<HttpResponse> {
14-
if (this.options.auth !== AuthType.Password || route.requestPath !== "/index.html") {
15-
throw new HttpError("Not found", HttpCode.NotFound)
16+
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
17+
if (!this.authenticated(request)) {
18+
if (route.requestPath === "/index.html") {
19+
return { redirect: "/login", query: { to: route.fullPath } }
20+
}
21+
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
1622
}
23+
1724
const payload = this.proxy(route.base.replace(/^\//, ""))
18-
if (!payload) {
19-
throw new HttpError("Not found", HttpCode.NotFound)
25+
if (payload) {
26+
return payload
2027
}
21-
return payload
28+
29+
throw new HttpError("Not found", HttpCode.NotFound)
2230
}
2331

24-
public async getRoot(route: Route, error?: Error): Promise<HttpResponse> {
25-
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/login.html")
26-
response.content = response.content.replace(/{{ERROR}}/, error ? `<div class="error">${error.message}</div>` : "")
27-
return this.replaceTemplates(route, response)
32+
public getProxyDomain(host?: string): string | undefined {
33+
if (!host || !this.proxyDomains) {
34+
return undefined
35+
}
36+
37+
return this.proxyDomains.find((d) => host.endsWith(d))
2838
}
2939

30-
/**
31-
* Return a response if the request should be proxied. Anything that ends in a
32-
* proxy domain and has a subdomain should be proxied. The port is found in
33-
* the top-most subdomain.
34-
*
35-
* For example, if the proxy domain is `coder.com` then `8080.coder.com` and
36-
* `test.8080.coder.com` will both proxy to `8080` but `8080.test.coder.com`
37-
* will have an error because `test` isn't a port. If the proxy domain was
38-
* `test.coder.com` then it would work.
39-
*/
4040
public maybeProxy(request: http.IncomingMessage): HttpResponse | undefined {
41-
const host = request.headers.host
42-
if (!host || !this.proxyDomains) {
41+
// No proxy until we're authenticated. This will cause the login page to
42+
// show as well as let our assets keep loading normally.
43+
if (!this.authenticated(request)) {
4344
return undefined
4445
}
4546

46-
const proxyDomain = this.proxyDomains.find((d) => host.endsWith(d))
47-
if (!proxyDomain) {
47+
const host = request.headers.host
48+
const proxyDomain = this.getProxyDomain(host)
49+
if (!host || !proxyDomain) {
4850
return undefined
4951
}
5052

src/node/entry.ts

+5-13
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,11 @@ const main = async (args: Args): Promise<void> => {
4646
throw new Error("--cert-key is missing")
4747
}
4848

49-
/**
50-
* Domains can be in the form `coder.com` or `*.coder.com`. Either way,
51-
* `[number].coder.com` will be proxied to `number`.
52-
*/
53-
const proxyDomains = args["proxy-domain"]
54-
? args["proxy-domain"].map((d) => d.replace(/^\*\./, "")).filter((d, i, arr) => arr.indexOf(d) === i)
55-
: []
56-
5749
const httpServer = new HttpServer(options)
5850
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
5951
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
6052
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"])
61-
const proxy = httpServer.registerHttpProvider("/proxy", ProxyHttpProvider, proxyDomains)
53+
const proxy = httpServer.registerHttpProvider("/proxy", ProxyHttpProvider, args["proxy-domain"])
6254
httpServer.registerHttpProvider("/login", LoginHttpProvider)
6355
httpServer.registerHttpProvider("/static", StaticHttpProvider)
6456
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
@@ -109,11 +101,11 @@ const main = async (args: Args): Promise<void> => {
109101
logger.info(" - Not serving HTTPS")
110102
}
111103

112-
if (proxyDomains.length === 1) {
113-
logger.info(` - Proxying *.${proxyDomains[0]}`)
114-
} else if (proxyDomains && proxyDomains.length > 1) {
104+
if (proxy.proxyDomains.length === 1) {
105+
logger.info(` - Proxying *.${proxy.proxyDomains[0]}`)
106+
} else if (proxy.proxyDomains.length > 1) {
115107
logger.info(" - Proxying the following domains:")
116-
proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
108+
proxy.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
117109
}
118110

119111
logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`)

src/node/http.ts

+30-2
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,29 @@ export interface HttpProvider3<A1, A2, A3, T> {
395395
}
396396

397397
export interface HttpProxyProvider {
398+
/**
399+
* Return a response if the request should be proxied. Anything that ends in a
400+
* proxy domain and has a subdomain should be proxied. The port is found in
401+
* the top-most subdomain.
402+
*
403+
* For example, if the proxy domain is `coder.com` then `8080.coder.com` and
404+
* `test.8080.coder.com` will both proxy to `8080` but `8080.test.coder.com`
405+
* will have an error because `test` isn't a port. If the proxy domain was
406+
* `test.coder.com` then it would work.
407+
*/
398408
maybeProxy(request: http.IncomingMessage): HttpResponse | undefined
409+
410+
/**
411+
* Get the matching proxy domain based on the provided host.
412+
*/
413+
getProxyDomain(host: string): string | undefined
414+
415+
/**
416+
* Domains can be provided in the form `coder.com` or `*.coder.com`. Either
417+
* way, `<number>.coder.com` will be proxied to `number`. The domains are
418+
* stored here without the `*.`.
419+
*/
420+
readonly proxyDomains: string[]
399421
}
400422

401423
/**
@@ -538,7 +560,13 @@ export class HttpServer {
538560
"Set-Cookie": [
539561
`${payload.cookie.key}=${payload.cookie.value}`,
540562
`Path=${normalize(payload.cookie.path || "/", true)}`,
541-
request.headers.host ? `Domain=${request.headers.host}` : undefined,
563+
// Set the cookie against the host so it can be used in
564+
// subdomains. Use a matching proxy domain if possible so
565+
// requests to any of those subdomains will already be
566+
// authenticated.
567+
request.headers.host
568+
? `Domain=${(this.proxy && this.proxy.getProxyDomain(request.headers.host)) || request.headers.host}`
569+
: undefined,
542570
// "HttpOnly",
543571
"SameSite=strict",
544572
]
@@ -566,8 +594,8 @@ export class HttpServer {
566594

567595
try {
568596
const payload =
569-
(this.proxy && this.proxy.maybeProxy(request)) ||
570597
this.maybeRedirect(request, route) ||
598+
(this.proxy && this.proxy.maybeProxy(request)) ||
571599
(await route.provider.handleRequest(route, request))
572600
if (!payload) {
573601
throw new HttpError("Not found", HttpCode.NotFound)

0 commit comments

Comments
 (0)