1- import { logger } from "@coder/logger"
21import * as http from "http"
3- import proxy from "http-proxy"
4- import * as net from "net"
5- import * as querystring from "querystring"
62import { 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}
0 commit comments