Add base path support Some users will host code-server behind a path-rewriting reverse proxy, for example domain.tld/my/base/path. This patch adds support for that since Code assumes everything is on / by default. To test this serve code-server behind a reverse proxy with a path like /code. Index: code-server/lib/vscode/src/vs/base/common/network.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/base/common/network.ts +++ code-server/lib/vscode/src/vs/base/common/network.ts @@ -151,8 +151,10 @@ class RemoteAuthoritiesImpl { } return URI.from({ scheme: platform.isWeb ? this._preferredWebSchema : Schemas.vscodeRemoteResource, - authority: `${host}:${port}`, - path: `/vscode-remote-resource`, + authority: platform.isWeb ? window.location.host : `${host}:${port}`, + path: platform.isWeb + ? URI.joinPath(URI.parse(window.location.href), `/vscode-remote-resource`).path + : `/vscode-remote-resource`, query }); } Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench-dev.html =================================================================== --- code-server.orig/lib/vscode/src/vs/code/browser/workbench/workbench-dev.html +++ code-server/lib/vscode/src/vs/code/browser/workbench/workbench-dev.html @@ -11,8 +11,8 @@ - - + + @@ -27,23 +27,26 @@ - - - + + + - - + + - + + - - - + + + Index: code-server/lib/vscode/src/vs/platform/remote/browser/browserSocketFactory.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/platform/remote/browser/browserSocketFactory.ts +++ code-server/lib/vscode/src/vs/platform/remote/browser/browserSocketFactory.ts @@ -274,7 +274,7 @@ export class BrowserSocketFactory implem connect(host: string, port: number, query: string, debugLabel: string, callback: IConnectCallback): void { const webSocketSchema = (/^https:/.test(window.location.href) ? 'wss' : 'ws'); - const socket = this._webSocketFactory.create(`${webSocketSchema}://${/:/.test(host) ? `[${host}]` : host}:${port}/?${query}&skipWebSocketFrames=false`, debugLabel); + const socket = this._webSocketFactory.create(`${webSocketSchema}://${window.location.host}${window.location.pathname}?${query}&skipWebSocketFrames=false`, debugLabel); const errorListener = socket.onError((err) => callback(err, undefined)); socket.onOpen(() => { errorListener.dispose(); @@ -282,6 +282,3 @@ export class BrowserSocketFactory implem }); } } - - - Index: code-server/lib/vscode/src/vs/server/node/webClientServer.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/server/node/webClientServer.ts +++ code-server/lib/vscode/src/vs/server/node/webClientServer.ts @@ -253,7 +253,10 @@ export class WebClientServer { return res.end(); } - const remoteAuthority = req.headers.host; + // It is not possible to reliably detect the remote authority on the server + // in all cases. Set this to something invalid to make sure we catch code + // that is using this when it should not. + const remoteAuthority = 'remote'; function escapeAttribute(value: string): string { return value.replace(/"/g, '"'); @@ -275,6 +278,8 @@ export class WebClientServer { accessToken: this._environmentService.args['github-auth'], scopes: [['user:email'], ['repo']] } : undefined; + const base = relativeRoot(getOriginalUrl(req)) + const vscodeBase = relativePath(getOriginalUrl(req)) const data = (await util.promisify(fs.readFile)(filePath)).toString() .replace('{{WORKBENCH_WEB_CONFIGURATION}}', escapeAttribute(JSON.stringify({ remoteAuthority, @@ -285,6 +290,7 @@ export class WebClientServer { folderUri: resolveWorkspaceURI(this._environmentService.args['default-folder']), workspaceUri: resolveWorkspaceURI(this._environmentService.args['default-workspace']), productConfiguration: >{ + rootEndpoint: base, codeServerVersion: this._productService.codeServerVersion, embedderIdentifier: 'server-distro', extensionsGallery: this._webExtensionResourceUrlTemplate ? { @@ -297,7 +303,9 @@ export class WebClientServer { } : undefined } }))) - .replace('{{WORKBENCH_AUTH_SESSION}}', () => authSessionInfo ? escapeAttribute(JSON.stringify(authSessionInfo)) : ''); + .replace('{{WORKBENCH_AUTH_SESSION}}', () => authSessionInfo ? escapeAttribute(JSON.stringify(authSessionInfo)) : '') + .replace(/{{BASE}}/g, base) + .replace(/{{VS_BASE}}/g, vscodeBase); const cspDirectives = [ 'default-src \'self\';', @@ -376,3 +384,70 @@ export class WebClientServer { return res.end(data); } } + +/** + * Remove extra slashes in a URL. + * + * This is meant to fill the job of `path.join` so you can concatenate paths and + * then normalize out any extra slashes. + * + * If you are using `path.join` you do not need this but note that `path` is for + * file system paths, not URLs. + */ +export const normalizeUrlPath = (url: string, keepTrailing = false): string => { + return url.replace(/\/\/+/g, "/").replace(/\/+$/, keepTrailing ? "/" : "") +} + +/** + * Get the relative path that will get us to the root of the page. For each + * slash we need to go up a directory. Will not have a trailing slash. + * + * For example: + * + * / => . + * /foo => . + * /foo/ => ./.. + * /foo/bar => ./.. + * /foo/bar/ => ./../.. + * + * All paths must be relative in order to work behind a reverse proxy since we + * we do not know the base path. Anything that needs to be absolute (for + * example cookies) must get the base path from the frontend. + * + * All relative paths must be prefixed with the relative root to ensure they + * work no matter the depth at which they happen to appear. + * + * For Express `req.originalUrl` should be used as they remove the base from the + * standard `url` property making it impossible to get the true depth. + */ +export const relativeRoot = (originalUrl: string): string => { + const depth = (originalUrl.split("?", 1)[0].match(/\//g) || []).length + return normalizeUrlPath("./" + (depth > 1 ? "../".repeat(depth - 1) : "")) +} + +/** + * Get the relative path to the current resource. + * + * For example: + * + * / => . + * /foo => ./foo + * /foo/ => . + * /foo/bar => ./bar + * /foo/bar/ => . + */ +export const relativePath = (originalUrl: string): string => { + const parts = originalUrl.split("?", 1)[0].split("/") + return normalizeUrlPath("./" + parts[parts.length - 1]) +} + +/** + * code-server serves Code using Express. Express removes the base from the url + * and puts the original in `originalUrl` so we must use this to get the correct + * depth. Code is not aware it is behind Express so the types do not match. We + * may want to continue moving code into Code and eventually remove the Express + * wrapper or move the web server back into code-server. + */ +export const getOriginalUrl = (req: http.IncomingMessage): string => { + return (req as any).originalUrl || req.url +} Index: code-server/lib/vscode/src/vs/base/common/product.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/base/common/product.ts +++ code-server/lib/vscode/src/vs/base/common/product.ts @@ -32,6 +32,7 @@ export type ExtensionVirtualWorkspaceSup export interface IProductConfiguration { readonly codeServerVersion?: string + readonly rootEndpoint?: string readonly version: string; readonly date?: string; Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/code/browser/workbench/workbench.ts +++ code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts @@ -482,6 +482,7 @@ function doCreateUri(path: string, query }); } + path = (window.location.pathname + "/" + path).replace(/\/\/+/g, "/") return URI.parse(window.location.href).with({ path, query }); } @@ -493,7 +494,7 @@ function doCreateUri(path: string, query if (!configElement || !configElementAttribute) { throw new Error('Missing web configuration element'); } - const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents; workspaceUri?: UriComponents } = JSON.parse(configElementAttribute); + const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents } = { ...JSON.parse(configElementAttribute), remoteAuthority: location.host } // Create workbench create(document.body, { Index: code-server/lib/vscode/src/vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader.ts +++ code-server/lib/vscode/src/vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader.ts @@ -16,7 +16,6 @@ import { getServiceMachineId } from 'vs/ import { IStorageService } from 'vs/platform/storage/common/storage'; import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { getTelemetryLevel, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; -import { RemoteAuthorities } from 'vs/base/common/network'; export const WEB_EXTENSION_RESOURCE_END_POINT = 'web-extension-resource'; @@ -72,7 +71,7 @@ export abstract class AbstractExtensionR public getExtensionGalleryResourceURL(galleryExtension: { publisher: string; name: string; version: string }, path?: string): URI | undefined { if (this._extensionGalleryResourceUrlTemplate) { const uri = URI.parse(format2(this._extensionGalleryResourceUrlTemplate, { publisher: galleryExtension.publisher, name: galleryExtension.name, version: galleryExtension.version, path: 'extension' })); - return this._isWebExtensionResourceEndPoint(uri) ? uri.with({ scheme: RemoteAuthorities.getPreferredWebSchema() }) : uri; + return this._isWebExtensionResourceEndPoint(uri) ? URI.joinPath(URI.parse(window.location.href), uri.path) : uri; } return undefined; }