Add proxy provider
It'll be able to handle /proxy requests as well as subdomains.
This commit is contained in:
parent
77ad73d579
commit
90fd1f7dd1
|
@ -0,0 +1,83 @@
|
||||||
|
import * as http from "http"
|
||||||
|
import { HttpCode, HttpError } from "../../common/http"
|
||||||
|
import { AuthType, HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy HTTP provider.
|
||||||
|
*/
|
||||||
|
export class ProxyHttpProvider extends HttpProvider {
|
||||||
|
public constructor(options: HttpProviderOptions, private readonly proxyDomains: string[]) {
|
||||||
|
super(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handleRequest(route: Route): Promise<HttpResponse> {
|
||||||
|
if (this.options.auth !== AuthType.Password || route.requestPath !== "/index.html") {
|
||||||
|
throw new HttpError("Not found", HttpCode.NotFound)
|
||||||
|
}
|
||||||
|
const payload = this.proxy(route.base.replace(/^\//, ""))
|
||||||
|
if (!payload) {
|
||||||
|
throw new HttpError("Not found", HttpCode.NotFound)
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRoot(route: Route, error?: Error): Promise<HttpResponse> {
|
||||||
|
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/login.html")
|
||||||
|
response.content = response.content.replace(/{{ERROR}}/, error ? `<div class="error">${error.message}</div>` : "")
|
||||||
|
return this.replaceTemplates(route, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a response if the request should be proxied. Anything that ends in a
|
||||||
|
* proxy domain and has a subdomain should be proxied. The port is found in
|
||||||
|
* the top-most subdomain.
|
||||||
|
*
|
||||||
|
* For example, if the proxy domain is `coder.com` then `8080.coder.com` and
|
||||||
|
* `test.8080.coder.com` will both proxy to `8080` but `8080.test.coder.com`
|
||||||
|
* will have an error because `test` isn't a port. If the proxy domain was
|
||||||
|
* `test.coder.com` then it would work.
|
||||||
|
*/
|
||||||
|
public maybeProxy(request: http.IncomingMessage): HttpResponse | undefined {
|
||||||
|
const host = request.headers.host
|
||||||
|
if (!host || !this.proxyDomains) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyDomain = this.proxyDomains.find((d) => host.endsWith(d))
|
||||||
|
if (!proxyDomain) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyDomainLength = proxyDomain.split(".").length
|
||||||
|
const portStr = host
|
||||||
|
.split(".")
|
||||||
|
.slice(0, -proxyDomainLength)
|
||||||
|
.pop()
|
||||||
|
|
||||||
|
if (!portStr) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.proxy(portStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
private proxy(portStr: string): HttpResponse {
|
||||||
|
if (!portStr) {
|
||||||
|
return {
|
||||||
|
code: HttpCode.BadRequest,
|
||||||
|
content: "Port must be provided",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const port = parseInt(portStr, 10)
|
||||||
|
if (isNaN(port)) {
|
||||||
|
return {
|
||||||
|
code: HttpCode.BadRequest,
|
||||||
|
content: `"${portStr}" is not a valid number`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: HttpCode.Ok,
|
||||||
|
content: `will proxy this to ${port}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import { CliMessage } from "../../lib/vscode/src/vs/server/ipc"
|
||||||
import { ApiHttpProvider } from "./app/api"
|
import { ApiHttpProvider } from "./app/api"
|
||||||
import { DashboardHttpProvider } from "./app/dashboard"
|
import { DashboardHttpProvider } from "./app/dashboard"
|
||||||
import { LoginHttpProvider } from "./app/login"
|
import { LoginHttpProvider } from "./app/login"
|
||||||
|
import { ProxyHttpProvider } from "./app/proxy"
|
||||||
import { StaticHttpProvider } from "./app/static"
|
import { StaticHttpProvider } from "./app/static"
|
||||||
import { UpdateHttpProvider } from "./app/update"
|
import { UpdateHttpProvider } from "./app/update"
|
||||||
import { VscodeHttpProvider } from "./app/vscode"
|
import { VscodeHttpProvider } from "./app/vscode"
|
||||||
|
@ -35,14 +36,6 @@ const main = async (args: Args): Promise<void> => {
|
||||||
const auth = args.auth || AuthType.Password
|
const auth = args.auth || AuthType.Password
|
||||||
const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword()))
|
const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword()))
|
||||||
|
|
||||||
/**
|
|
||||||
* Domains can be in the form `coder.com` or `*.coder.com`. Either way,
|
|
||||||
* `[number].coder.com` will be proxied to `number`.
|
|
||||||
*/
|
|
||||||
const normalizeProxyDomains = (domains?: string[]): string[] => {
|
|
||||||
return domains ? domains.map((d) => d.replace(/^\*\./, "")).filter((d, i) => domains.indexOf(d) === i) : []
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spawn the main HTTP server.
|
// Spawn the main HTTP server.
|
||||||
const options: HttpServerOptions = {
|
const options: HttpServerOptions = {
|
||||||
auth,
|
auth,
|
||||||
|
@ -50,7 +43,6 @@ const main = async (args: Args): Promise<void> => {
|
||||||
host: args.host || (args.auth === AuthType.Password && typeof args.cert !== "undefined" ? "0.0.0.0" : "localhost"),
|
host: args.host || (args.auth === AuthType.Password && typeof args.cert !== "undefined" ? "0.0.0.0" : "localhost"),
|
||||||
password: originalPassword ? hash(originalPassword) : undefined,
|
password: originalPassword ? hash(originalPassword) : undefined,
|
||||||
port: typeof args.port !== "undefined" ? args.port : process.env.PORT ? parseInt(process.env.PORT, 10) : 8080,
|
port: typeof args.port !== "undefined" ? args.port : process.env.PORT ? parseInt(process.env.PORT, 10) : 8080,
|
||||||
proxyDomains: normalizeProxyDomains(args["proxy-domain"]),
|
|
||||||
socket: args.socket,
|
socket: args.socket,
|
||||||
...(args.cert && !args.cert.value
|
...(args.cert && !args.cert.value
|
||||||
? await generateCertificate()
|
? await generateCertificate()
|
||||||
|
@ -64,13 +56,23 @@ const main = async (args: Args): Promise<void> => {
|
||||||
throw new Error("--cert-key is missing")
|
throw new Error("--cert-key is missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domains can be in the form `coder.com` or `*.coder.com`. Either way,
|
||||||
|
* `[number].coder.com` will be proxied to `number`.
|
||||||
|
*/
|
||||||
|
const proxyDomains = args["proxy-domain"]
|
||||||
|
? args["proxy-domain"].map((d) => d.replace(/^\*\./, "")).filter((d, i, arr) => arr.indexOf(d) === i)
|
||||||
|
: []
|
||||||
|
|
||||||
const httpServer = new HttpServer(options)
|
const httpServer = new HttpServer(options)
|
||||||
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
|
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
|
||||||
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
|
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
|
||||||
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"])
|
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"])
|
||||||
|
const proxy = httpServer.registerHttpProvider("/proxy", ProxyHttpProvider, proxyDomains)
|
||||||
httpServer.registerHttpProvider("/login", LoginHttpProvider)
|
httpServer.registerHttpProvider("/login", LoginHttpProvider)
|
||||||
httpServer.registerHttpProvider("/static", StaticHttpProvider)
|
httpServer.registerHttpProvider("/static", StaticHttpProvider)
|
||||||
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
|
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
|
||||||
|
httpServer.registerProxy(proxy)
|
||||||
|
|
||||||
ipcMain().onDispose(() => httpServer.dispose())
|
ipcMain().onDispose(() => httpServer.dispose())
|
||||||
|
|
||||||
|
@ -100,13 +102,11 @@ const main = async (args: Args): Promise<void> => {
|
||||||
logger.info(" - Not serving HTTPS")
|
logger.info(" - Not serving HTTPS")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.proxyDomains && options.proxyDomains.length === 1) {
|
if (proxyDomains.length === 1) {
|
||||||
logger.info(` - Proxying *.${options.proxyDomains[0]}`)
|
logger.info(` - Proxying *.${proxyDomains[0]}`)
|
||||||
} else if (options.proxyDomains && options.proxyDomains.length > 1) {
|
} else if (proxyDomains && proxyDomains.length > 1) {
|
||||||
logger.info(" - Proxying the following domains:")
|
logger.info(" - Proxying the following domains:")
|
||||||
options.proxyDomains.forEach((domain) => {
|
proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
|
||||||
logger.info(` - *.${domain}`)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`)
|
logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`)
|
||||||
|
|
|
@ -99,7 +99,6 @@ export interface HttpServerOptions {
|
||||||
readonly commit?: string
|
readonly commit?: string
|
||||||
readonly host?: string
|
readonly host?: string
|
||||||
readonly password?: string
|
readonly password?: string
|
||||||
readonly proxyDomains?: string[]
|
|
||||||
readonly port?: number
|
readonly port?: number
|
||||||
readonly socket?: string
|
readonly socket?: string
|
||||||
}
|
}
|
||||||
|
@ -395,6 +394,10 @@ export interface HttpProvider3<A1, A2, A3, T> {
|
||||||
new (options: HttpProviderOptions, a1: A1, a2: A2, a3: A3): T
|
new (options: HttpProviderOptions, a1: A1, a2: A2, a3: A3): T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HttpProxyProvider {
|
||||||
|
maybeProxy(request: http.IncomingMessage): HttpResponse | undefined
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An HTTP server. Its main role is to route incoming HTTP requests to the
|
* An HTTP server. Its main role is to route incoming HTTP requests to the
|
||||||
* appropriate provider for that endpoint then write out the response. It also
|
* appropriate provider for that endpoint then write out the response. It also
|
||||||
|
@ -407,6 +410,7 @@ export class HttpServer {
|
||||||
private readonly providers = new Map<string, HttpProvider>()
|
private readonly providers = new Map<string, HttpProvider>()
|
||||||
private readonly heart: Heart
|
private readonly heart: Heart
|
||||||
private readonly socketProvider = new SocketProxyProvider()
|
private readonly socketProvider = new SocketProxyProvider()
|
||||||
|
private proxy?: HttpProxyProvider
|
||||||
|
|
||||||
public constructor(private readonly options: HttpServerOptions) {
|
public constructor(private readonly options: HttpServerOptions) {
|
||||||
this.heart = new Heart(path.join(xdgLocalDir, "heartbeat"), async () => {
|
this.heart = new Heart(path.join(xdgLocalDir, "heartbeat"), async () => {
|
||||||
|
@ -481,6 +485,14 @@ export class HttpServer {
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a provider as a proxy. It will be consulted before any other
|
||||||
|
* provider.
|
||||||
|
*/
|
||||||
|
public registerProxy(proxy: HttpProxyProvider): void {
|
||||||
|
this.proxy = proxy
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start listening on the specified port.
|
* Start listening on the specified port.
|
||||||
*/
|
*/
|
||||||
|
@ -551,8 +563,12 @@ export class HttpServer {
|
||||||
response.end()
|
response.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = this.maybeRedirect(request, route) || (await route.provider.handleRequest(route, request))
|
const payload =
|
||||||
|
(this.proxy && this.proxy.maybeProxy(request)) ||
|
||||||
|
this.maybeRedirect(request, route) ||
|
||||||
|
(await route.provider.handleRequest(route, request))
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
throw new HttpError("Not found", HttpCode.NotFound)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue