Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2db4734

Browse files
committedMar 30, 2020
Add proxy provider
It'll be able to handle /proxy requests as well as subdomains.
1 parent 4ef09bf commit 2db4734

File tree

3 files changed

+116
-17
lines changed

3 files changed

+116
-17
lines changed
 

‎src/node/app/proxy.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import * as http from "http"
2+
import { HttpCode, HttpError } from "../../common/http"
3+
import { AuthType, HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
4+
5+
/**
6+
* Proxy HTTP provider.
7+
*/
8+
export class ProxyHttpProvider extends HttpProvider {
9+
public constructor(options: HttpProviderOptions, private readonly proxyDomains: string[]) {
10+
super(options)
11+
}
12+
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+
}
17+
const payload = this.proxy(route.base.replace(/^\//, ""))
18+
if (!payload) {
19+
throw new HttpError("Not found", HttpCode.NotFound)
20+
}
21+
return payload
22+
}
23+
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)
28+
}
29+
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+
*/
40+
public maybeProxy(request: http.IncomingMessage): HttpResponse | undefined {
41+
const host = request.headers.host
42+
if (!host || !this.proxyDomains) {
43+
return undefined
44+
}
45+
46+
const proxyDomain = this.proxyDomains.find((d) => host.endsWith(d))
47+
if (!proxyDomain) {
48+
return undefined
49+
}
50+
51+
const proxyDomainLength = proxyDomain.split(".").length
52+
const portStr = host
53+
.split(".")
54+
.slice(0, -proxyDomainLength)
55+
.pop()
56+
57+
if (!portStr) {
58+
return undefined
59+
}
60+
61+
return this.proxy(portStr)
62+
}
63+
64+
private proxy(portStr: string): HttpResponse {
65+
if (!portStr) {
66+
return {
67+
code: HttpCode.BadRequest,
68+
content: "Port must be provided",
69+
}
70+
}
71+
const port = parseInt(portStr, 10)
72+
if (isNaN(port)) {
73+
return {
74+
code: HttpCode.BadRequest,
75+
content: `"${portStr}" is not a valid number`,
76+
}
77+
}
78+
return {
79+
code: HttpCode.Ok,
80+
content: `will proxy this to ${port}`,
81+
}
82+
}
83+
}

‎src/node/entry.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { CliMessage } from "../../lib/vscode/src/vs/server/ipc"
55
import { ApiHttpProvider } from "./app/api"
66
import { DashboardHttpProvider } from "./app/dashboard"
77
import { LoginHttpProvider } from "./app/login"
8+
import { ProxyHttpProvider } from "./app/proxy"
89
import { StaticHttpProvider } from "./app/static"
910
import { UpdateHttpProvider } from "./app/update"
1011
import { VscodeHttpProvider } from "./app/vscode"
@@ -35,22 +36,13 @@ const main = async (args: Args): Promise<void> => {
3536
const auth = args.auth || AuthType.Password
3637
const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword()))
3738

38-
/**
39-
* Domains can be in the form `coder.com` or `*.coder.com`. Either way,
40-
* `[number].coder.com` will be proxied to `number`.
41-
*/
42-
const normalizeProxyDomains = (domains?: string[]): string[] => {
43-
return domains ? domains.map((d) => d.replace(/^\*\./, "")).filter((d, i) => domains.indexOf(d) === i) : []
44-
}
45-
4639
// Spawn the main HTTP server.
4740
const options: HttpServerOptions = {
4841
auth,
4942
commit,
5043
host: args.host || (args.auth === AuthType.Password && typeof args.cert !== "undefined" ? "0.0.0.0" : "localhost"),
5144
password: originalPassword ? hash(originalPassword) : undefined,
5245
port: typeof args.port !== "undefined" ? args.port : process.env.PORT ? parseInt(process.env.PORT, 10) : 8080,
53-
proxyDomains: normalizeProxyDomains(args["proxy-domain"]),
5446
socket: args.socket,
5547
...(args.cert && !args.cert.value
5648
? await generateCertificate()
@@ -64,13 +56,23 @@ const main = async (args: Args): Promise<void> => {
6456
throw new Error("--cert-key is missing")
6557
}
6658

59+
/**
60+
* Domains can be in the form `coder.com` or `*.coder.com`. Either way,
61+
* `[number].coder.com` will be proxied to `number`.
62+
*/
63+
const proxyDomains = args["proxy-domain"]
64+
? args["proxy-domain"].map((d) => d.replace(/^\*\./, "")).filter((d, i, arr) => arr.indexOf(d) === i)
65+
: []
66+
6767
const httpServer = new HttpServer(options)
6868
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
6969
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
7070
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"])
71+
const proxy = httpServer.registerHttpProvider("/proxy", ProxyHttpProvider, proxyDomains)
7172
httpServer.registerHttpProvider("/login", LoginHttpProvider)
7273
httpServer.registerHttpProvider("/static", StaticHttpProvider)
7374
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
75+
httpServer.registerProxy(proxy)
7476

7577
ipcMain().onDispose(() => httpServer.dispose())
7678

@@ -100,13 +102,11 @@ const main = async (args: Args): Promise<void> => {
100102
logger.info(" - Not serving HTTPS")
101103
}
102104

103-
if (options.proxyDomains && options.proxyDomains.length === 1) {
104-
logger.info(` - Proxying *.${options.proxyDomains[0]}`)
105-
} else if (options.proxyDomains && options.proxyDomains.length > 1) {
105+
if (proxyDomains.length === 1) {
106+
logger.info(` - Proxying *.${proxyDomains[0]}`)
107+
} else if (proxyDomains && proxyDomains.length > 1) {
106108
logger.info(" - Proxying the following domains:")
107-
options.proxyDomains.forEach((domain) => {
108-
logger.info(` - *.${domain}`)
109-
})
109+
proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
110110
}
111111

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

‎src/node/http.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ export interface HttpServerOptions {
9999
readonly commit?: string
100100
readonly host?: string
101101
readonly password?: string
102-
readonly proxyDomains?: string[]
103102
readonly port?: number
104103
readonly socket?: string
105104
}
@@ -395,6 +394,10 @@ export interface HttpProvider3<A1, A2, A3, T> {
395394
new (options: HttpProviderOptions, a1: A1, a2: A2, a3: A3): T
396395
}
397396

397+
export interface HttpProxyProvider {
398+
maybeProxy(request: http.IncomingMessage): HttpResponse | undefined
399+
}
400+
398401
/**
399402
* An HTTP server. Its main role is to route incoming HTTP requests to the
400403
* appropriate provider for that endpoint then write out the response. It also
@@ -407,6 +410,7 @@ export class HttpServer {
407410
private readonly providers = new Map<string, HttpProvider>()
408411
private readonly heart: Heart
409412
private readonly socketProvider = new SocketProxyProvider()
413+
private proxy?: HttpProxyProvider
410414

411415
public constructor(private readonly options: HttpServerOptions) {
412416
this.heart = new Heart(path.join(xdgLocalDir, "heartbeat"), async () => {
@@ -481,6 +485,14 @@ export class HttpServer {
481485
return p
482486
}
483487

488+
/**
489+
* Register a provider as a proxy. It will be consulted before any other
490+
* provider.
491+
*/
492+
public registerProxy(proxy: HttpProxyProvider): void {
493+
this.proxy = proxy
494+
}
495+
484496
/**
485497
* Start listening on the specified port.
486498
*/
@@ -551,8 +563,12 @@ export class HttpServer {
551563
response.end()
552564
}
553565
}
566+
554567
try {
555-
const payload = this.maybeRedirect(request, route) || (await route.provider.handleRequest(route, request))
568+
const payload =
569+
(this.proxy && this.proxy.maybeProxy(request)) ||
570+
this.maybeRedirect(request, route) ||
571+
(await route.provider.handleRequest(route, request))
556572
if (!payload) {
557573
throw new HttpError("Not found", HttpCode.NotFound)
558574
}

0 commit comments

Comments
 (0)
Please sign in to comment.