Skip to content

Commit 6b7945d

Browse files
committed
Move proxy logic into main HTTP server
This makes the code much more internally consistent (providers just return payloads, include the proxy provider).
1 parent af6fa7e commit 6b7945d

File tree

3 files changed

+184
-222
lines changed

3 files changed

+184
-222
lines changed

src/node/app/proxy.ts

+14-159
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,12 @@
1-
import { logger } from "@coder/logger"
21
import * as http from "http"
3-
import proxy from "http-proxy"
4-
import * as net from "net"
5-
import * as querystring from "querystring"
62
import { HttpCode, HttpError } from "../../common/http"
7-
import { HttpProvider, HttpProviderOptions, HttpProxyProvider, HttpResponse, Route } from "../http"
8-
9-
interface Request extends http.IncomingMessage {
10-
base?: string
11-
}
3+
import { HttpProvider, HttpResponse, Route, WsResponse } from "../http"
124

135
/**
146
* Proxy HTTP provider.
157
*/
16-
export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider {
17-
/**
18-
* Proxy domains are stored here without the leading `*.`
19-
*/
20-
public readonly proxyDomains: Set<string>
21-
private readonly proxy = proxy.createProxyServer({})
22-
23-
/**
24-
* Domains can be provided in the form `coder.com` or `*.coder.com`. Either
25-
* way, `<number>.coder.com` will be proxied to `number`.
26-
*/
27-
public constructor(options: HttpProviderOptions, proxyDomains: string[] = []) {
28-
super(options)
29-
this.proxyDomains = new Set(proxyDomains.map((d) => d.replace(/^\*\./, "")))
30-
this.proxy.on("error", (error) => logger.warn(error.message))
31-
// Intercept the response to rewrite absolute redirects against the base path.
32-
this.proxy.on("proxyRes", (response, request: Request) => {
33-
if (response.headers.location && response.headers.location.startsWith("/") && request.base) {
34-
response.headers.location = request.base + response.headers.location
35-
}
36-
})
37-
}
38-
39-
public async handleRequest(
40-
route: Route,
41-
request: http.IncomingMessage,
42-
response: http.ServerResponse,
43-
): Promise<HttpResponse> {
8+
export class ProxyHttpProvider extends HttpProvider {
9+
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
4410
if (!this.authenticated(request)) {
4511
if (this.isRoot(route)) {
4612
return { redirect: "/login", query: { to: route.fullPath } }
@@ -56,133 +22,22 @@ export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider
5622
}
5723

5824
const port = route.base.replace(/^\//, "")
59-
const base = `${this.options.base}/${port}`
60-
const payload = this.doProxy(route, request, response, port, base)
61-
if (payload) {
62-
return payload
25+
return {
26+
proxy: {
27+
base: `${this.options.base}/${port}`,
28+
port,
29+
},
6330
}
64-
65-
throw new HttpError("Not found", HttpCode.NotFound)
6631
}
6732

68-
public async handleWebSocket(
69-
route: Route,
70-
request: http.IncomingMessage,
71-
socket: net.Socket,
72-
head: Buffer,
73-
): Promise<void> {
33+
public async handleWebSocket(route: Route, request: http.IncomingMessage): Promise<WsResponse> {
7434
this.ensureAuthenticated(request)
7535
const port = route.base.replace(/^\//, "")
76-
const base = `${this.options.base}/${port}`
77-
this.doProxy(route, request, { socket, head }, port, base)
78-
}
79-
80-
public getCookieDomain(host: string): string {
81-
let current: string | undefined
82-
this.proxyDomains.forEach((domain) => {
83-
if (host.endsWith(domain) && (!current || domain.length < current.length)) {
84-
current = domain
85-
}
86-
})
87-
// Setting the domain to localhost doesn't seem to work for subdomains (for
88-
// example dev.localhost).
89-
return current && current !== "localhost" ? current : host
90-
}
91-
92-
public maybeProxyRequest(
93-
route: Route,
94-
request: http.IncomingMessage,
95-
response: http.ServerResponse,
96-
): HttpResponse | undefined {
97-
const port = this.getPort(request)
98-
return port ? this.doProxy(route, request, response, port) : undefined
99-
}
100-
101-
public maybeProxyWebSocket(
102-
route: Route,
103-
request: http.IncomingMessage,
104-
socket: net.Socket,
105-
head: Buffer,
106-
): HttpResponse | undefined {
107-
const port = this.getPort(request)
108-
return port ? this.doProxy(route, request, { socket, head }, port) : undefined
109-
}
110-
111-
private getPort(request: http.IncomingMessage): string | undefined {
112-
// No proxy until we're authenticated. This will cause the login page to
113-
// show as well as let our assets keep loading normally.
114-
if (!this.authenticated(request)) {
115-
return undefined
116-
}
117-
118-
// Split into parts.
119-
const host = request.headers.host || ""
120-
const idx = host.indexOf(":")
121-
const domain = idx !== -1 ? host.substring(0, idx) : host
122-
const parts = domain.split(".")
123-
124-
// There must be an exact match.
125-
const port = parts.shift()
126-
const proxyDomain = parts.join(".")
127-
if (!port || !this.proxyDomains.has(proxyDomain)) {
128-
return undefined
129-
}
130-
131-
return port
132-
}
133-
134-
private doProxy(
135-
route: Route,
136-
request: http.IncomingMessage,
137-
response: http.ServerResponse,
138-
portStr: string,
139-
base?: string,
140-
): HttpResponse
141-
private doProxy(
142-
route: Route,
143-
request: http.IncomingMessage,
144-
response: { socket: net.Socket; head: Buffer },
145-
portStr: string,
146-
base?: string,
147-
): HttpResponse
148-
private doProxy(
149-
route: Route,
150-
request: http.IncomingMessage,
151-
response: http.ServerResponse | { socket: net.Socket; head: Buffer },
152-
portStr: string,
153-
base?: string,
154-
): HttpResponse {
155-
const port = parseInt(portStr, 10)
156-
if (isNaN(port)) {
157-
return {
158-
code: HttpCode.BadRequest,
159-
content: `"${portStr}" is not a valid number`,
160-
}
161-
}
162-
163-
// REVIEW: Absolute redirects need to be based on the subpath but I'm not
164-
// sure how best to get this information to the `proxyRes` event handler.
165-
// For now I'm sticking it on the request object which is passed through to
166-
// the event.
167-
;(request as Request).base = base
168-
169-
const isHttp = response instanceof http.ServerResponse
170-
const path = base ? route.fullPath.replace(base, "") : route.fullPath
171-
const options: proxy.ServerOptions = {
172-
changeOrigin: true,
173-
ignorePath: true,
174-
target: `${isHttp ? "http" : "ws"}://127.0.0.1:${port}${path}${
175-
Object.keys(route.query).length > 0 ? `?${querystring.stringify(route.query)}` : ""
176-
}`,
177-
ws: !isHttp,
178-
}
179-
180-
if (response instanceof http.ServerResponse) {
181-
this.proxy.web(request, response, options)
182-
} else {
183-
this.proxy.ws(request, response.socket, response.head, options)
36+
return {
37+
proxy: {
38+
base: `${this.options.base}/${port}`,
39+
port,
40+
},
18441
}
185-
186-
return { handled: true }
18742
}
18843
}

src/node/entry.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const main = async (args: Args): Promise<void> => {
4343
host: args.host || (args.auth === AuthType.Password && typeof args.cert !== "undefined" ? "0.0.0.0" : "localhost"),
4444
password: originalPassword ? hash(originalPassword) : undefined,
4545
port: typeof args.port !== "undefined" ? args.port : process.env.PORT ? parseInt(process.env.PORT, 10) : 8080,
46+
proxyDomains: args["proxy-domain"],
4647
socket: args.socket,
4748
...(args.cert && !args.cert.value
4849
? await generateCertificate()
@@ -60,11 +61,10 @@ const main = async (args: Args): Promise<void> => {
6061
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
6162
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
6263
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"])
63-
const proxy = httpServer.registerHttpProvider("/proxy", ProxyHttpProvider, args["proxy-domain"])
64+
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
6465
httpServer.registerHttpProvider("/login", LoginHttpProvider)
6566
httpServer.registerHttpProvider("/static", StaticHttpProvider)
6667
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
67-
httpServer.registerProxy(proxy)
6868

6969
ipcMain().onDispose(() => httpServer.dispose())
7070

@@ -94,9 +94,9 @@ const main = async (args: Args): Promise<void> => {
9494
logger.info(" - Not serving HTTPS")
9595
}
9696

97-
if (proxy.proxyDomains.size > 0) {
98-
logger.info(` - Proxying the following domain${proxy.proxyDomains.size === 1 ? "" : "s"}:`)
99-
proxy.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
97+
if (httpServer.proxyDomains.size > 0) {
98+
logger.info(` - Proxying the following domain${httpServer.proxyDomains.size === 1 ? "" : "s"}:`)
99+
httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
100100
}
101101

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

0 commit comments

Comments
 (0)