Skip to content

Commit 35830af

Browse files
committed
Implement the actual proxy
1 parent c7b38e4 commit 35830af

File tree

9 files changed

+178
-31
lines changed

9 files changed

+178
-31
lines changed

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717
"devDependencies": {
1818
"@types/adm-zip": "^0.4.32",
1919
"@types/fs-extra": "^8.0.1",
20+
"@types/http-proxy": "^1.17.4",
2021
"@types/mocha": "^5.2.7",
2122
"@types/node": "^12.12.7",
2223
"@types/parcel-bundler": "^1.12.1",
2324
"@types/pem": "^1.9.5",
2425
"@types/safe-compare": "^1.1.0",
2526
"@types/semver": "^7.1.0",
26-
"@types/tar-fs": "^1.16.2",
2727
"@types/ssh2": "0.5.39",
2828
"@types/ssh2-streams": "^0.1.6",
2929
"@types/tar-fs": "^1.16.1",
@@ -53,14 +53,16 @@
5353
"@coder/logger": "1.1.11",
5454
"adm-zip": "^0.4.14",
5555
"fs-extra": "^8.1.0",
56+
"http-proxy": "^1.18.0",
5657
"httpolyglot": "^0.1.2",
5758
"node-pty": "^0.9.0",
5859
"pem": "^1.14.2",
5960
"safe-compare": "^1.1.4",
6061
"semver": "^7.1.3",
61-
"tar": "^6.0.1",
6262
"ssh2": "^0.8.7",
63+
"tar": "^6.0.1",
6364
"tar-fs": "^2.0.0",
65+
"vfile-message": "^2.0.2",
6466
"ws": "^7.2.0"
6567
}
6668
}

src/node/app/api.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ export class ApiHttpProvider extends HttpProvider {
4343

4444
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
4545
this.ensureAuthenticated(request)
46-
if (route.requestPath !== "/index.html") {
46+
// Only serve root pages.
47+
if (route.requestPath && route.requestPath !== "/index.html") {
4748
throw new HttpError("Not found", HttpCode.NotFound)
4849
}
4950

src/node/app/dashboard.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export class DashboardHttpProvider extends HttpProvider {
2020
}
2121

2222
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
23-
if (route.requestPath !== "/index.html") {
23+
// Only serve root pages.
24+
if (route.requestPath && route.requestPath !== "/index.html") {
2425
throw new HttpError("Not found", HttpCode.NotFound)
2526
}
2627

src/node/app/login.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ interface LoginPayload {
1818
*/
1919
export class LoginHttpProvider extends HttpProvider {
2020
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
21-
if (this.options.auth !== AuthType.Password || route.requestPath !== "/index.html") {
21+
// Only serve root pages and only if password authentication is enabled.
22+
if (this.options.auth !== AuthType.Password || (route.requestPath && route.requestPath !== "/index.html")) {
2223
throw new HttpError("Not found", HttpCode.NotFound)
2324
}
2425
switch (route.base) {

src/node/app/proxy.ts

+83-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import * as http from "http"
2+
import proxy from "http-proxy"
3+
import * as net from "net"
24
import { HttpCode, HttpError } from "../../common/http"
35
import { HttpProvider, HttpProviderOptions, HttpProxyProvider, HttpResponse, Route } from "../http"
46

@@ -10,6 +12,7 @@ export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider
1012
* Proxy domains are stored here without the leading `*.`
1113
*/
1214
public readonly proxyDomains: string[]
15+
private readonly proxy = proxy.createProxyServer({})
1316

1417
/**
1518
* Domains can be provided in the form `coder.com` or `*.coder.com`. Either
@@ -20,22 +23,37 @@ export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider
2023
this.proxyDomains = proxyDomains.map((d) => d.replace(/^\*\./, "")).filter((d, i, arr) => arr.indexOf(d) === i)
2124
}
2225

23-
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
26+
public async handleRequest(
27+
route: Route,
28+
request: http.IncomingMessage,
29+
response: http.ServerResponse,
30+
): Promise<HttpResponse> {
2431
if (!this.authenticated(request)) {
25-
if (route.requestPath === "/index.html") {
26-
return { redirect: "/login", query: { to: route.fullPath } }
32+
// Only redirect from the root. Other requests get an unauthorized error.
33+
if (route.requestPath && route.requestPath !== "/index.html") {
34+
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
2735
}
28-
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
36+
return { redirect: "/login", query: { to: route.fullPath } }
2937
}
3038

31-
const payload = this.proxy(route.base.replace(/^\//, ""))
39+
const payload = this.doProxy(route.requestPath, request, response, route.base.replace(/^\//, ""))
3240
if (payload) {
3341
return payload
3442
}
3543

3644
throw new HttpError("Not found", HttpCode.NotFound)
3745
}
3846

47+
public async handleWebSocket(
48+
route: Route,
49+
request: http.IncomingMessage,
50+
socket: net.Socket,
51+
head: Buffer,
52+
): Promise<void> {
53+
this.ensureAuthenticated(request)
54+
this.doProxy(route.requestPath, request, socket, head, route.base.replace(/^\//, ""))
55+
}
56+
3957
public getCookieDomain(host: string): string {
4058
let current: string | undefined
4159
this.proxyDomains.forEach((domain) => {
@@ -46,7 +64,26 @@ export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider
4664
return current || host
4765
}
4866

49-
public maybeProxy(request: http.IncomingMessage): HttpResponse | undefined {
67+
public maybeProxyRequest(
68+
route: Route,
69+
request: http.IncomingMessage,
70+
response: http.ServerResponse,
71+
): HttpResponse | undefined {
72+
const port = this.getPort(request)
73+
return port ? this.doProxy(route.fullPath, request, response, port) : undefined
74+
}
75+
76+
public maybeProxyWebSocket(
77+
route: Route,
78+
request: http.IncomingMessage,
79+
socket: net.Socket,
80+
head: Buffer,
81+
): HttpResponse | undefined {
82+
const port = this.getPort(request)
83+
return port ? this.doProxy(route.fullPath, request, socket, head, port) : undefined
84+
}
85+
86+
private getPort(request: http.IncomingMessage): string | undefined {
5087
// No proxy until we're authenticated. This will cause the login page to
5188
// show as well as let our assets keep loading normally.
5289
if (!this.authenticated(request)) {
@@ -67,26 +104,58 @@ export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider
67104
return undefined
68105
}
69106

70-
return this.proxy(port)
107+
return port
71108
}
72109

73-
private proxy(portStr: string): HttpResponse {
74-
if (!portStr) {
110+
private doProxy(
111+
path: string,
112+
request: http.IncomingMessage,
113+
response: http.ServerResponse,
114+
portStr: string,
115+
): HttpResponse
116+
private doProxy(
117+
path: string,
118+
request: http.IncomingMessage,
119+
socket: net.Socket,
120+
head: Buffer,
121+
portStr: string,
122+
): HttpResponse
123+
private doProxy(
124+
path: string,
125+
request: http.IncomingMessage,
126+
responseOrSocket: http.ServerResponse | net.Socket,
127+
headOrPortStr: Buffer | string,
128+
portStr?: string,
129+
): HttpResponse {
130+
const _portStr = typeof headOrPortStr === "string" ? headOrPortStr : portStr
131+
if (!_portStr) {
75132
return {
76133
code: HttpCode.BadRequest,
77134
content: "Port must be provided",
78135
}
79136
}
80-
const port = parseInt(portStr, 10)
137+
138+
const port = parseInt(_portStr, 10)
81139
if (isNaN(port)) {
82140
return {
83141
code: HttpCode.BadRequest,
84-
content: `"${portStr}" is not a valid number`,
142+
content: `"${_portStr}" is not a valid number`,
85143
}
86144
}
87-
return {
88-
code: HttpCode.Ok,
89-
content: `will proxy this to ${port}`,
145+
146+
const options: proxy.ServerOptions = {
147+
autoRewrite: true,
148+
changeOrigin: true,
149+
ignorePath: true,
150+
target: `http://127.0.0.1:${port}${path}`,
90151
}
152+
153+
if (responseOrSocket instanceof net.Socket) {
154+
this.proxy.ws(request, responseOrSocket, headOrPortStr, options)
155+
} else {
156+
this.proxy.web(request, responseOrSocket, options)
157+
}
158+
159+
return { handled: true }
91160
}
92161
}

src/node/app/update.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ export class UpdateHttpProvider extends HttpProvider {
6161
this.ensureAuthenticated(request)
6262
this.ensureMethod(request)
6363

64-
if (route.requestPath !== "/index.html") {
64+
// Only serve root pages.
65+
if (route.requestPath && route.requestPath !== "/index.html") {
6566
throw new HttpError("Not found", HttpCode.NotFound)
6667
}
6768

src/node/app/vscode.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ export class VscodeHttpProvider extends HttpProvider {
128128

129129
switch (route.base) {
130130
case "/":
131-
if (route.requestPath !== "/index.html") {
131+
// Only serve this at the root.
132+
if (route.requestPath && route.requestPath !== "/index.html") {
132133
throw new HttpError("Not found", HttpCode.NotFound)
133134
} else if (!this.authenticated(request)) {
134135
return { redirect: "/login", query: { to: this.options.base } }

src/node/http.ts

+47-9
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ export interface HttpResponse<T = string | Buffer | object> {
7777
* `undefined` to remove a query variable.
7878
*/
7979
query?: Query
80+
/**
81+
* Indicates the request was handled and nothing else needs to be done.
82+
*/
83+
handled?: boolean
8084
}
8185

8286
/**
@@ -104,10 +108,26 @@ export interface HttpServerOptions {
104108
}
105109

106110
export interface Route {
111+
/**
112+
* Base path part (in /test/path it would be "/test").
113+
*/
107114
base: string
115+
/**
116+
* Remaining part of the route (in /test/path it would be "/path"). It can be
117+
* blank.
118+
*/
108119
requestPath: string
120+
/**
121+
* Query variables included in the request.
122+
*/
109123
query: querystring.ParsedUrlQuery
124+
/**
125+
* Normalized version of `originalPath`.
126+
*/
110127
fullPath: string
128+
/**
129+
* Original path of the request without any modifications.
130+
*/
111131
originalPath: string
112132
}
113133

@@ -152,7 +172,11 @@ export abstract class HttpProvider {
152172
/**
153173
* Handle requests to the registered endpoint.
154174
*/
155-
public abstract handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse>
175+
public abstract handleRequest(
176+
route: Route,
177+
request: http.IncomingMessage,
178+
response: http.ServerResponse,
179+
): Promise<HttpResponse>
156180

157181
/**
158182
* Get the base relative to the provided route. For each slash we need to go
@@ -403,7 +427,21 @@ export interface HttpProxyProvider {
403427
* For example if `coder.com` is specified `8080.coder.com` will be proxied
404428
* but `8080.test.coder.com` and `test.8080.coder.com` will not.
405429
*/
406-
maybeProxy(request: http.IncomingMessage): HttpResponse | undefined
430+
maybeProxyRequest(
431+
route: Route,
432+
request: http.IncomingMessage,
433+
response: http.ServerResponse,
434+
): HttpResponse | undefined
435+
436+
/**
437+
* Same concept as `maybeProxyRequest` but for web sockets.
438+
*/
439+
maybeProxyWebSocket(
440+
route: Route,
441+
request: http.IncomingMessage,
442+
socket: net.Socket,
443+
head: Buffer,
444+
): HttpResponse | undefined
407445

408446
/**
409447
* Get the domain that should be used for setting a cookie. This will allow
@@ -584,12 +622,11 @@ export class HttpServer {
584622
try {
585623
const payload =
586624
this.maybeRedirect(request, route) ||
587-
(this.proxy && this.proxy.maybeProxy(request)) ||
588-
(await route.provider.handleRequest(route, request))
589-
if (!payload) {
590-
throw new HttpError("Not found", HttpCode.NotFound)
625+
(this.proxy && this.proxy.maybeProxyRequest(route, request, response)) ||
626+
(await route.provider.handleRequest(route, request, response))
627+
if (!payload.handled) {
628+
write(payload)
591629
}
592-
write(payload)
593630
} catch (error) {
594631
let e = error
595632
if (error.code === "ENOENT" || error.code === "EISDIR") {
@@ -662,7 +699,9 @@ export class HttpServer {
662699
throw new HttpError("Not found", HttpCode.NotFound)
663700
}
664701

665-
await route.provider.handleWebSocket(route, request, await this.socketProvider.createProxy(socket), head)
702+
if (!this.proxy || !this.proxy.maybeProxyWebSocket(route, request, socket, head)) {
703+
await route.provider.handleWebSocket(route, request, await this.socketProvider.createProxy(socket), head)
704+
}
666705
} catch (error) {
667706
socket.destroy(error)
668707
logger.warn(`discarding socket connection: ${error.message}`)
@@ -684,7 +723,6 @@ export class HttpServer {
684723
// Happens if it's a plain `domain.com`.
685724
base = "/"
686725
}
687-
requestPath = requestPath || "/index.html"
688726
return { base, requestPath }
689727
}
690728

0 commit comments

Comments
 (0)