Archived
1
0

Implement the actual proxy

This commit is contained in:
Asher
2020-03-23 18:02:31 -05:00
parent 2086648c87
commit 8aa5675ba2
9 changed files with 177 additions and 30 deletions

View File

@ -43,7 +43,8 @@ export class ApiHttpProvider extends HttpProvider {
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
this.ensureAuthenticated(request)
if (route.requestPath !== "/index.html") {
// Only serve root pages.
if (route.requestPath && route.requestPath !== "/index.html") {
throw new HttpError("Not found", HttpCode.NotFound)
}

View File

@ -20,7 +20,8 @@ export class DashboardHttpProvider extends HttpProvider {
}
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
if (route.requestPath !== "/index.html") {
// Only serve root pages.
if (route.requestPath && route.requestPath !== "/index.html") {
throw new HttpError("Not found", HttpCode.NotFound)
}

View File

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

View File

@ -1,4 +1,6 @@
import * as http from "http"
import proxy from "http-proxy"
import * as net from "net"
import { HttpCode, HttpError } from "../../common/http"
import { HttpProvider, HttpProviderOptions, HttpProxyProvider, HttpResponse, Route } from "../http"
@ -10,6 +12,7 @@ export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider
* Proxy domains are stored here without the leading `*.`
*/
public readonly proxyDomains: string[]
private readonly proxy = proxy.createProxyServer({})
/**
* Domains can be provided in the form `coder.com` or `*.coder.com`. Either
@ -20,15 +23,20 @@ export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider
this.proxyDomains = proxyDomains.map((d) => d.replace(/^\*\./, "")).filter((d, i, arr) => arr.indexOf(d) === i)
}
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
public async handleRequest(
route: Route,
request: http.IncomingMessage,
response: http.ServerResponse,
): Promise<HttpResponse> {
if (!this.authenticated(request)) {
if (route.requestPath === "/index.html") {
return { redirect: "/login", query: { to: route.fullPath } }
// Only redirect from the root. Other requests get an unauthorized error.
if (route.requestPath && route.requestPath !== "/index.html") {
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
}
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
return { redirect: "/login", query: { to: route.fullPath } }
}
const payload = this.proxy(route.base.replace(/^\//, ""))
const payload = this.doProxy(route.requestPath, request, response, route.base.replace(/^\//, ""))
if (payload) {
return payload
}
@ -36,6 +44,16 @@ export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider
throw new HttpError("Not found", HttpCode.NotFound)
}
public async handleWebSocket(
route: Route,
request: http.IncomingMessage,
socket: net.Socket,
head: Buffer,
): Promise<void> {
this.ensureAuthenticated(request)
this.doProxy(route.requestPath, request, socket, head, route.base.replace(/^\//, ""))
}
public getCookieDomain(host: string): string {
let current: string | undefined
this.proxyDomains.forEach((domain) => {
@ -46,7 +64,26 @@ export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider
return current || host
}
public maybeProxy(request: http.IncomingMessage): HttpResponse | undefined {
public maybeProxyRequest(
route: Route,
request: http.IncomingMessage,
response: http.ServerResponse,
): HttpResponse | undefined {
const port = this.getPort(request)
return port ? this.doProxy(route.fullPath, request, response, port) : undefined
}
public maybeProxyWebSocket(
route: Route,
request: http.IncomingMessage,
socket: net.Socket,
head: Buffer,
): HttpResponse | undefined {
const port = this.getPort(request)
return port ? this.doProxy(route.fullPath, request, socket, head, port) : undefined
}
private getPort(request: http.IncomingMessage): string | undefined {
// No proxy until we're authenticated. This will cause the login page to
// show as well as let our assets keep loading normally.
if (!this.authenticated(request)) {
@ -67,26 +104,58 @@ export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider
return undefined
}
return this.proxy(port)
return port
}
private proxy(portStr: string): HttpResponse {
if (!portStr) {
private doProxy(
path: string,
request: http.IncomingMessage,
response: http.ServerResponse,
portStr: string,
): HttpResponse
private doProxy(
path: string,
request: http.IncomingMessage,
socket: net.Socket,
head: Buffer,
portStr: string,
): HttpResponse
private doProxy(
path: string,
request: http.IncomingMessage,
responseOrSocket: http.ServerResponse | net.Socket,
headOrPortStr: Buffer | string,
portStr?: string,
): HttpResponse {
const _portStr = typeof headOrPortStr === "string" ? headOrPortStr : portStr
if (!_portStr) {
return {
code: HttpCode.BadRequest,
content: "Port must be provided",
}
}
const port = parseInt(portStr, 10)
const port = parseInt(_portStr, 10)
if (isNaN(port)) {
return {
code: HttpCode.BadRequest,
content: `"${portStr}" is not a valid number`,
content: `"${_portStr}" is not a valid number`,
}
}
return {
code: HttpCode.Ok,
content: `will proxy this to ${port}`,
const options: proxy.ServerOptions = {
autoRewrite: true,
changeOrigin: true,
ignorePath: true,
target: `http://127.0.0.1:${port}${path}`,
}
if (responseOrSocket instanceof net.Socket) {
this.proxy.ws(request, responseOrSocket, headOrPortStr, options)
} else {
this.proxy.web(request, responseOrSocket, options)
}
return { handled: true }
}
}

View File

@ -61,7 +61,8 @@ export class UpdateHttpProvider extends HttpProvider {
this.ensureAuthenticated(request)
this.ensureMethod(request)
if (route.requestPath !== "/index.html") {
// Only serve root pages.
if (route.requestPath && route.requestPath !== "/index.html") {
throw new HttpError("Not found", HttpCode.NotFound)
}

View File

@ -128,7 +128,8 @@ export class VscodeHttpProvider extends HttpProvider {
switch (route.base) {
case "/":
if (route.requestPath !== "/index.html") {
// Only serve this at the root.
if (route.requestPath && route.requestPath !== "/index.html") {
throw new HttpError("Not found", HttpCode.NotFound)
} else if (!this.authenticated(request)) {
return { redirect: "/login", query: { to: this.options.base } }

View File

@ -77,6 +77,10 @@ export interface HttpResponse<T = string | Buffer | object> {
* `undefined` to remove a query variable.
*/
query?: Query
/**
* Indicates the request was handled and nothing else needs to be done.
*/
handled?: boolean
}
/**
@ -104,10 +108,26 @@ export interface HttpServerOptions {
}
export interface Route {
/**
* Base path part (in /test/path it would be "/test").
*/
base: string
/**
* Remaining part of the route (in /test/path it would be "/path"). It can be
* blank.
*/
requestPath: string
/**
* Query variables included in the request.
*/
query: querystring.ParsedUrlQuery
/**
* Normalized version of `originalPath`.
*/
fullPath: string
/**
* Original path of the request without any modifications.
*/
originalPath: string
}
@ -152,7 +172,11 @@ export abstract class HttpProvider {
/**
* Handle requests to the registered endpoint.
*/
public abstract handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse>
public abstract handleRequest(
route: Route,
request: http.IncomingMessage,
response: http.ServerResponse,
): Promise<HttpResponse>
/**
* Get the base relative to the provided route. For each slash we need to go
@ -403,7 +427,21 @@ export interface HttpProxyProvider {
* For example if `coder.com` is specified `8080.coder.com` will be proxied
* but `8080.test.coder.com` and `test.8080.coder.com` will not.
*/
maybeProxy(request: http.IncomingMessage): HttpResponse | undefined
maybeProxyRequest(
route: Route,
request: http.IncomingMessage,
response: http.ServerResponse,
): HttpResponse | undefined
/**
* Same concept as `maybeProxyRequest` but for web sockets.
*/
maybeProxyWebSocket(
route: Route,
request: http.IncomingMessage,
socket: net.Socket,
head: Buffer,
): HttpResponse | undefined
/**
* Get the domain that should be used for setting a cookie. This will allow
@ -584,12 +622,11 @@ export class HttpServer {
try {
const payload =
this.maybeRedirect(request, route) ||
(this.proxy && this.proxy.maybeProxy(request)) ||
(await route.provider.handleRequest(route, request))
if (!payload) {
throw new HttpError("Not found", HttpCode.NotFound)
(this.proxy && this.proxy.maybeProxyRequest(route, request, response)) ||
(await route.provider.handleRequest(route, request, response))
if (!payload.handled) {
write(payload)
}
write(payload)
} catch (error) {
let e = error
if (error.code === "ENOENT" || error.code === "EISDIR") {
@ -662,7 +699,9 @@ export class HttpServer {
throw new HttpError("Not found", HttpCode.NotFound)
}
await route.provider.handleWebSocket(route, request, await this.socketProvider.createProxy(socket), head)
if (!this.proxy || !this.proxy.maybeProxyWebSocket(route, request, socket, head)) {
await route.provider.handleWebSocket(route, request, await this.socketProvider.createProxy(socket), head)
}
} catch (error) {
socket.destroy(error)
logger.warn(`discarding socket connection: ${error.message}`)
@ -684,7 +723,6 @@ export class HttpServer {
// Happens if it's a plain `domain.com`.
base = "/"
}
requestPath = requestPath || "/index.html"
return { base, requestPath }
}