2020-03-23 19:47:01 +01:00
|
|
|
import * as http from "http"
|
|
|
|
import { HttpCode, HttpError } from "../../common/http"
|
2020-03-23 20:26:47 +01:00
|
|
|
import { HttpProvider, HttpProviderOptions, HttpProxyProvider, HttpResponse, Route } from "../http"
|
2020-03-23 19:47:01 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Proxy HTTP provider.
|
|
|
|
*/
|
2020-03-23 20:26:47 +01:00
|
|
|
export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider {
|
2020-03-23 20:51:58 +01:00
|
|
|
/**
|
|
|
|
* Proxy domains are stored here without the leading `*.`
|
|
|
|
*/
|
2020-03-23 20:26:47 +01:00
|
|
|
public readonly proxyDomains: string[]
|
|
|
|
|
2020-03-23 20:51:58 +01:00
|
|
|
/**
|
|
|
|
* Domains can be provided in the form `coder.com` or `*.coder.com`. Either
|
|
|
|
* way, `<number>.coder.com` will be proxied to `number`.
|
|
|
|
*/
|
2020-03-23 20:26:47 +01:00
|
|
|
public constructor(options: HttpProviderOptions, proxyDomains: string[] = []) {
|
2020-03-23 19:47:01 +01:00
|
|
|
super(options)
|
2020-03-23 20:26:47 +01:00
|
|
|
this.proxyDomains = proxyDomains.map((d) => d.replace(/^\*\./, "")).filter((d, i, arr) => arr.indexOf(d) === i)
|
2020-03-23 19:47:01 +01:00
|
|
|
}
|
|
|
|
|
2020-03-23 20:26:47 +01:00
|
|
|
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
|
|
|
if (!this.authenticated(request)) {
|
|
|
|
if (route.requestPath === "/index.html") {
|
|
|
|
return { redirect: "/login", query: { to: route.fullPath } }
|
|
|
|
}
|
|
|
|
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
|
2020-03-23 19:47:01 +01:00
|
|
|
}
|
2020-03-23 20:26:47 +01:00
|
|
|
|
2020-03-23 19:47:01 +01:00
|
|
|
const payload = this.proxy(route.base.replace(/^\//, ""))
|
2020-03-23 20:26:47 +01:00
|
|
|
if (payload) {
|
|
|
|
return payload
|
2020-03-23 19:47:01 +01:00
|
|
|
}
|
2020-03-23 20:26:47 +01:00
|
|
|
|
|
|
|
throw new HttpError("Not found", HttpCode.NotFound)
|
2020-03-23 19:47:01 +01:00
|
|
|
}
|
|
|
|
|
2020-03-23 20:51:58 +01:00
|
|
|
public getCookieDomain(host: string): string {
|
|
|
|
let current: string | undefined
|
|
|
|
this.proxyDomains.forEach((domain) => {
|
|
|
|
if (host.endsWith(domain) && (!current || domain.length < current.length)) {
|
|
|
|
current = domain
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return current || host
|
2020-03-23 19:47:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public maybeProxy(request: http.IncomingMessage): HttpResponse | undefined {
|
2020-03-23 20:26:47 +01:00
|
|
|
// 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)) {
|
2020-03-23 19:47:01 +01:00
|
|
|
return undefined
|
|
|
|
}
|
|
|
|
|
2020-03-23 20:51:58 +01:00
|
|
|
// At minimum there needs to be sub.domain.tld.
|
2020-03-23 20:26:47 +01:00
|
|
|
const host = request.headers.host
|
2020-03-23 20:51:58 +01:00
|
|
|
const parts = host && host.split(".")
|
|
|
|
if (!parts || parts.length < 3) {
|
2020-03-23 19:47:01 +01:00
|
|
|
return undefined
|
|
|
|
}
|
|
|
|
|
2020-03-23 20:51:58 +01:00
|
|
|
// There must be an exact match.
|
|
|
|
const port = parts.shift()
|
|
|
|
const proxyDomain = parts.join(".")
|
|
|
|
if (!port || !this.proxyDomains.includes(proxyDomain)) {
|
2020-03-23 19:47:01 +01:00
|
|
|
return undefined
|
|
|
|
}
|
|
|
|
|
2020-03-23 20:51:58 +01:00
|
|
|
return this.proxy(port)
|
2020-03-23 19:47:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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}`,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|