Fix redirects through subpath proxy

This commit is contained in:
Asher 2020-03-31 14:56:01 -05:00
parent fd339a7433
commit e7e7b0ffb7
No known key found for this signature in database
GPG Key ID: D63C1EF81242354A
3 changed files with 51 additions and 38 deletions

View File

@ -17,7 +17,7 @@
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
crossorigin="use-credentials" crossorigin="use-credentials"
/> />
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.pnggg" /> <link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" /> <link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
<meta id="coder-options" data-settings="{{OPTIONS}}" /> <meta id="coder-options" data-settings="{{OPTIONS}}" />
</head> </head>

View File

@ -6,6 +6,10 @@ import * as querystring from "querystring"
import { HttpCode, HttpError } from "../../common/http" import { HttpCode, HttpError } from "../../common/http"
import { HttpProvider, HttpProviderOptions, HttpProxyProvider, HttpResponse, Route } from "../http" import { HttpProvider, HttpProviderOptions, HttpProxyProvider, HttpResponse, Route } from "../http"
interface Request extends http.IncomingMessage {
base?: string
}
/** /**
* Proxy HTTP provider. * Proxy HTTP provider.
*/ */
@ -24,6 +28,12 @@ export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider
super(options) super(options)
this.proxyDomains = proxyDomains.map((d) => d.replace(/^\*\./, "")).filter((d, i, arr) => arr.indexOf(d) === i) this.proxyDomains = proxyDomains.map((d) => d.replace(/^\*\./, "")).filter((d, i, arr) => arr.indexOf(d) === i)
this.proxy.on("error", (error) => logger.warn(error.message)) this.proxy.on("error", (error) => logger.warn(error.message))
// Intercept the response to rewrite absolute redirects against the base path.
this.proxy.on("proxyRes", (response, request: Request) => {
if (response.headers.location && response.headers.location.startsWith("/") && request.base) {
response.headers.location = request.base + response.headers.location
}
})
} }
public async handleRequest( public async handleRequest(
@ -41,14 +51,15 @@ export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider
} }
// Ensure there is a trailing slash so relative paths work correctly. // Ensure there is a trailing slash so relative paths work correctly.
const base = route.base.replace(/^\//, "") const port = route.base.replace(/^\//, "")
if (isRoot && !route.originalPath.endsWith("/")) { const base = `${this.options.base}/${port}`
if (isRoot && !route.fullPath.endsWith("/")) {
return { return {
redirect: `/proxy/${base}/`, redirect: `${base}/`,
} }
} }
const payload = this.doProxy(route.requestPath, route.query, request, response, base) const payload = this.doProxy(route, request, response, port, base)
if (payload) { if (payload) {
return payload return payload
} }
@ -63,7 +74,9 @@ export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider
head: Buffer, head: Buffer,
): Promise<void> { ): Promise<void> {
this.ensureAuthenticated(request) this.ensureAuthenticated(request)
this.doProxy(route.requestPath, route.query, request, socket, head, route.base.replace(/^\//, "")) const port = route.base.replace(/^\//, "")
const base = `${this.options.base}/${port}`
this.doProxy(route, request, { socket, head }, port, base)
} }
public getCookieDomain(host: string): string { public getCookieDomain(host: string): string {
@ -84,7 +97,7 @@ export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider
response: http.ServerResponse, response: http.ServerResponse,
): HttpResponse | undefined { ): HttpResponse | undefined {
const port = this.getPort(request) const port = this.getPort(request)
return port ? this.doProxy(route.fullPath, route.query, request, response, port) : undefined return port ? this.doProxy(route, request, response, port) : undefined
} }
public maybeProxyWebSocket( public maybeProxyWebSocket(
@ -94,7 +107,7 @@ export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider
head: Buffer, head: Buffer,
): HttpResponse | undefined { ): HttpResponse | undefined {
const port = this.getPort(request) const port = this.getPort(request)
return port ? this.doProxy(route.fullPath, route.query, request, socket, head, port) : undefined return port ? this.doProxy(route, request, { socket, head }, port) : undefined
} }
private getPort(request: http.IncomingMessage): string | undefined { private getPort(request: http.IncomingMessage): string | undefined {
@ -121,57 +134,55 @@ export class ProxyHttpProvider extends HttpProvider implements HttpProxyProvider
} }
private doProxy( private doProxy(
path: string, route: Route,
query: querystring.ParsedUrlQuery,
request: http.IncomingMessage, request: http.IncomingMessage,
response: http.ServerResponse, response: http.ServerResponse,
portStr: string, portStr: string,
base?: string,
): HttpResponse ): HttpResponse
private doProxy( private doProxy(
path: string, route: Route,
query: querystring.ParsedUrlQuery,
request: http.IncomingMessage, request: http.IncomingMessage,
socket: net.Socket, response: { socket: net.Socket; head: Buffer },
head: Buffer,
portStr: string, portStr: string,
base?: string,
): HttpResponse ): HttpResponse
private doProxy( private doProxy(
path: string, route: Route,
query: querystring.ParsedUrlQuery,
request: http.IncomingMessage, request: http.IncomingMessage,
responseOrSocket: http.ServerResponse | net.Socket, response: http.ServerResponse | { socket: net.Socket; head: Buffer },
headOrPortStr: Buffer | string, portStr: string,
portStr?: string, base?: string,
): HttpResponse { ): HttpResponse {
const _portStr = typeof headOrPortStr === "string" ? headOrPortStr : portStr const port = parseInt(portStr, 10)
if (!_portStr) {
return {
code: HttpCode.BadRequest,
content: "Port must be provided",
}
}
const port = parseInt(_portStr, 10)
if (isNaN(port)) { if (isNaN(port)) {
return { return {
code: HttpCode.BadRequest, code: HttpCode.BadRequest,
content: `"${_portStr}" is not a valid number`, content: `"${portStr}" is not a valid number`,
} }
} }
// REVIEW: Absolute redirects need to be based on the subpath but I'm not
// sure how best to get this information to the `proxyRes` event handler.
// For now I'm sticking it on the request object which is passed through to
// the event.
;(request as Request).base = base
const hxxp = response instanceof http.ServerResponse
const path = base ? route.fullPath.replace(base, "") : route.fullPath
const options: proxy.ServerOptions = { const options: proxy.ServerOptions = {
autoRewrite: true,
changeOrigin: true, changeOrigin: true,
ignorePath: true, ignorePath: true,
target: `http://127.0.0.1:${port}${path}${ target: `${hxxp ? "http" : "ws"}://127.0.0.1:${port}${path}${
Object.keys(query).length > 0 ? `?${querystring.stringify(query)}` : "" Object.keys(route.query).length > 0 ? `?${querystring.stringify(route.query)}` : ""
}`, }`,
ws: !hxxp,
} }
if (responseOrSocket instanceof net.Socket) { if (response instanceof http.ServerResponse) {
this.proxy.ws(request, responseOrSocket, headOrPortStr, options) this.proxy.web(request, response, options)
} else { } else {
this.proxy.web(request, responseOrSocket, options) this.proxy.ws(request, response.socket, response.head, options)
} }
return { handled: true } return { handled: true }

View File

@ -596,7 +596,7 @@ export class HttpServer {
`Path=${normalize(payload.cookie.path || "/", true)}`, `Path=${normalize(payload.cookie.path || "/", true)}`,
domain ? `Domain=${(this.proxy && this.proxy.getCookieDomain(domain)) || domain}` : undefined, domain ? `Domain=${(this.proxy && this.proxy.getCookieDomain(domain)) || domain}` : undefined,
// "HttpOnly", // "HttpOnly",
"SameSite=strict", "SameSite=lax",
] ]
.filter((l) => !!l) .filter((l) => !!l)
.join(";"), .join(";"),
@ -633,9 +633,11 @@ export class HttpServer {
if (error.code === "ENOENT" || error.code === "EISDIR") { if (error.code === "ENOENT" || error.code === "EISDIR") {
e = new HttpError("Not found", HttpCode.NotFound) e = new HttpError("Not found", HttpCode.NotFound)
} }
logger.debug("Request error", field("url", request.url))
logger.debug(error.stack)
const code = typeof e.code === "number" ? e.code : HttpCode.ServerError const code = typeof e.code === "number" ? e.code : HttpCode.ServerError
logger.debug("Request error", field("url", request.url), field("code", code))
if (code >= HttpCode.ServerError) {
logger.error(error.stack)
}
const payload = await route.provider.getErrorRoot(route, code, code, e.message) const payload = await route.provider.getErrorRoot(route, code, code, e.message)
write({ write({
code, code,