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
@@ -212,7 +212,9 @@ class RemoteAuthoritiesImpl {
 		return URI.from({
 			scheme: platform.isWeb ? this._preferredWebSchema : Schemas.vscodeRemoteResource,
 			authority: `${host}:${port}`,
-			path: this._remoteResourcesPath,
+			path: platform.isWeb
+				? (window.location.pathname + "/" + this._remoteResourcesPath).replace(/\/\/+/g, "/")
+				: this._remoteResourcesPath,
 			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 @@
 		<meta name="mobile-web-app-capable" content="yes" />
 		<meta name="apple-mobile-web-app-capable" content="yes" />
 		<meta name="apple-mobile-web-app-title" content="Code">
-		<link rel="apple-touch-icon" sizes="192x192" href="/_static/src/browser/media/pwa-icon-192.png" />
-		<link rel="apple-touch-icon" sizes="512x512" href="/_static/src/browser/media/pwa-icon-512.png" />
+		<link rel="apple-touch-icon" sizes="192x192" href="{{BASE}}/_static/src/browser/media/pwa-icon-192.png" />
+		<link rel="apple-touch-icon" sizes="512x512" href="{{BASE}}/_static/src/browser/media/pwa-icon-512.png" />
 
 		<!-- Disable pinch zooming -->
 		<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
@@ -27,9 +27,9 @@
 		<meta id="vscode-workbench-builtin-extensions" data-settings="{{WORKBENCH_BUILTIN_EXTENSIONS}}">
 
 		<!-- Workbench Icon/Manifest/CSS -->
-		<link rel="icon" href="/_static/src/browser/media/favicon-dark-support.svg" />
-		<link rel="alternate icon" href="/_static/src/browser/media/favicon.ico" type="image/x-icon" />
-		<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
+		<link rel="icon" href="{{BASE}}/_static/src/browser/media/favicon-dark-support.svg" />
+		<link rel="alternate icon" href="{{BASE}}/_static/src/browser/media/favicon.ico" type="image/x-icon" />
+		<link rel="manifest" href="{{VS_BASE}}/manifest.json" crossorigin="use-credentials" />
 	</head>
 
 	<body aria-label="">
@@ -39,7 +39,7 @@
 	<script src="{{WORKBENCH_WEB_BASE_URL}}/out/vs/loader.js"></script>
 	<script src="{{WORKBENCH_WEB_BASE_URL}}/out/vs/webPackagePaths.js"></script>
 	<script>
-		const baseUrl = new URL('{{WORKBENCH_WEB_BASE_URL}}', window.location.origin).toString();
+		const baseUrl = new URL('{{WORKBENCH_WEB_BASE_URL}}', window.location).toString();
 		Object.keys(self.webPackagePaths).map(function (key, index) {
 			self.webPackagePaths[key] = `${baseUrl}/remote/web/node_modules/${key}/${self.webPackagePaths[key]}`;
 		});
Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench.html
===================================================================
--- code-server.orig/lib/vscode/src/vs/code/browser/workbench/workbench.html
+++ code-server/lib/vscode/src/vs/code/browser/workbench/workbench.html
@@ -11,8 +11,8 @@
 		<meta name="mobile-web-app-capable" content="yes" />
 		<meta name="apple-mobile-web-app-capable" content="yes" />
 		<meta name="apple-mobile-web-app-title" content="Code">
-		<link rel="apple-touch-icon" sizes="192x192" href="/_static/src/browser/media/pwa-icon-192.png" />
-		<link rel="apple-touch-icon" sizes="512x512" href="/_static/src/browser/media/pwa-icon-512.png" />
+		<link rel="apple-touch-icon" sizes="192x192" href="{{BASE}}/_static/src/browser/media/pwa-icon-192.png" />
+		<link rel="apple-touch-icon" sizes="512x512" href="{{BASE}}/_static/src/browser/media/pwa-icon-512.png" />
 
 		<!-- Disable pinch zooming -->
 		<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
@@ -24,9 +24,9 @@
 		<meta id="vscode-workbench-auth-session" data-settings="{{WORKBENCH_AUTH_SESSION}}">
 
 		<!-- Workbench Icon/Manifest/CSS -->
-		<link rel="icon" href="/_static/src/browser/media/favicon-dark-support.svg" />
-		<link rel="alternate icon" href="/_static/src/browser/media/favicon.ico" type="image/x-icon" />
-		<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
+		<link rel="icon" href="{{BASE}}/_static/src/browser/media/favicon-dark-support.svg" />
+		<link rel="alternate icon" href="{{BASE}}/_static/src/browser/media/favicon.ico" type="image/x-icon" />
+		<link rel="manifest" href="{{VS_BASE}}/manifest.json" crossorigin="use-credentials" />
 		<link data-name="vs/workbench/workbench.web.main" rel="stylesheet" href="{{WORKBENCH_WEB_BASE_URL}}/out/vs/workbench/workbench.web.main.css">
 
 	</head>
@@ -40,7 +40,7 @@
 	<script>
 
 		// Packages
-		const baseUrl = new URL('{{WORKBENCH_WEB_BASE_URL}}', window.location.origin).toString();
+		const baseUrl = new URL('{{WORKBENCH_WEB_BASE_URL}}', window.location).toString();
 		Object.keys(self.webPackagePaths).map(function (key, index) {
 			self.webPackagePaths[key] = `${baseUrl}/node_modules/${key}/${self.webPackagePaths[key]}`;
 		});
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
@@ -281,6 +281,7 @@ export class BrowserSocketFactory implem
 	connect({ host, port }: WebSocketRemoteConnection, path: string, query: string, debugLabel: string): Promise<ISocket> {
 		return new Promise<ISocket>((resolve, reject) => {
 			const webSocketSchema = (/^https:/.test(mainWindow.location.href) ? 'wss' : 'ws');
+			path = (mainWindow.location.pathname + "/" + path).replace(/\/\/+/g, "/")
 			const socket = this._webSocketFactory.create(`${webSocketSchema}://${(/:/.test(host) && !/\[/.test(host)) ? `[${host}]` : host}:${port}${path}?${query}&skipWebSocketFrames=false`, debugLabel);
 			const errorListener = socket.onError(reject);
 			socket.onOpen(() => {
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
@@ -269,16 +269,15 @@ export class WebClientServer {
 			return void res.end();
 		}
 
-		const getFirstHeader = (headerName: string) => {
-			const val = req.headers[headerName];
-			return Array.isArray(val) ? val[0] : val;
-		};
-
 		const useTestResolver = (!this._environmentService.isBuilt && this._environmentService.args['use-test-resolver']);
+		// For now we are getting the remote authority from the client to avoid
+		// needing specific configuration for reverse proxies to work.  Set this to
+		// something invalid to make sure we catch code that is using this value
+		// from the backend when it should not.
 		const remoteAuthority = (
 			useTestResolver
 				? 'test+test'
-				: (getFirstHeader('x-original-host') || getFirstHeader('x-forwarded-host') || req.headers.host)
+				: 'remote'
 		);
 		if (!remoteAuthority) {
 			return serveError(req, res, 400, `Bad request.`);
@@ -305,8 +304,12 @@ export class WebClientServer {
 			scopes: [['user:email'], ['repo']]
 		} : undefined;
 
+		const base = relativeRoot(getOriginalUrl(req))
+		const vscodeBase = relativePath(getOriginalUrl(req))
+
 		const productConfiguration = {
 			codeServerVersion: this._productService.codeServerVersion,
+			rootEndpoint: base,
 			embedderIdentifier: 'server-distro',
 			extensionsGallery: this._webExtensionResourceUrlTemplate && this._productService.extensionsGallery ? {
 				...this._productService.extensionsGallery,
@@ -335,7 +338,7 @@ export class WebClientServer {
 			folderUri: resolveWorkspaceURI(this._environmentService.args['default-folder']),
 			workspaceUri: resolveWorkspaceURI(this._environmentService.args['default-workspace']),
 			productConfiguration,
-			callbackRoute: this._callbackRoute
+			callbackRoute: vscodeBase + this._callbackRoute
 		};
 
 		const cookies = cookie.parse(req.headers.cookie || '');
@@ -352,9 +355,11 @@ export class WebClientServer {
 		const values: { [key: string]: string } = {
 			WORKBENCH_WEB_CONFIGURATION: asJSON(workbenchWebConfiguration),
 			WORKBENCH_AUTH_SESSION: authSessionInfo ? asJSON(authSessionInfo) : '',
-			WORKBENCH_WEB_BASE_URL: this._staticRoute,
+			WORKBENCH_WEB_BASE_URL: vscodeBase + this._staticRoute,
 			WORKBENCH_NLS_URL,
-			WORKBENCH_NLS_FALLBACK_URL: `${this._staticRoute}/out/nls.messages.js`
+			WORKBENCH_NLS_FALLBACK_URL: `${vscodeBase}${this._staticRoute}/out/nls.messages.js`,
+			BASE: base,
+			VS_BASE: vscodeBase,
 		};
 
 		if (useTestResolver) {
@@ -381,7 +386,7 @@ export class WebClientServer {
 			'default-src \'self\';',
 			'img-src \'self\' https: data: blob:;',
 			'media-src \'self\';',
-			`script-src 'self' 'unsafe-eval' ${WORKBENCH_NLS_BASE_URL ?? ''} ${this._getScriptCspHashes(data).join(' ')} '${webWorkerExtensionHostIframeScriptSHA}' ${useTestResolver ? '' : `http://${remoteAuthority}`};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html
+			`script-src 'self' 'unsafe-eval' ${WORKBENCH_NLS_BASE_URL ?? ''} ${this._getScriptCspHashes(data).join(' ')} '${webWorkerExtensionHostIframeScriptSHA}' ${useTestResolver ? '' : ``};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html
 			'child-src \'self\';',
 			`frame-src 'self' https://*.vscode-cdn.net data:;`,
 			'worker-src \'self\' data: blob:;',
@@ -454,3 +459,70 @@ export class WebClientServer {
 		return void 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
@@ -56,6 +56,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
@@ -304,7 +304,8 @@ class LocalStorageURLCallbackProvider ex
 			this.startListening();
 		}
 
-		return URI.parse(mainWindow.location.href).with({ path: this._callbackRoute, query: queryParams.join('&') });
+		const path = (mainWindow.location.pathname + "/" + this._callbackRoute).replace(/\/\/+/g, "/");
+		return URI.parse(mainWindow.location.href).with({ path: path, query: queryParams.join('&') });
 	}
 
 	private startListening(): void {
@@ -550,17 +551,6 @@ class WorkspaceProvider implements IWork
 	}
 }
 
-function readCookie(name: string): string | undefined {
-	const cookies = document.cookie.split('; ');
-	for (const cookie of cookies) {
-		if (cookie.startsWith(name + '=')) {
-			return cookie.substring(name.length + 1);
-		}
-	}
-
-	return undefined;
-}
-
 (function () {
 
 	// Find config by checking for DOM
@@ -569,8 +559,8 @@ function readCookie(name: string): strin
 	if (!configElement || !configElementAttribute) {
 		throw new Error('Missing web configuration element');
 	}
-	const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents; workspaceUri?: UriComponents; callbackRoute: string } = JSON.parse(configElementAttribute);
-	const secretStorageKeyPath = readCookie('vscode-secret-key-path');
+	const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents; workspaceUri?: UriComponents; callbackRoute: string } = { ...JSON.parse(configElementAttribute), remoteAuthority: location.host }
+	const secretStorageKeyPath = (window.location.pathname + "/mint-key").replace(/\/\/+/g, "/");
 	const secretStorageCrypto = secretStorageKeyPath && ServerKeyedAESCrypto.supported()
 		? new ServerKeyedAESCrypto(secretStorageKeyPath) : new TransparentCrypto();
 
Index: code-server/lib/vscode/src/vs/platform/extensionResourceLoader/common/extensionResourceLoader.ts
===================================================================
--- code-server.orig/lib/vscode/src/vs/platform/extensionResourceLoader/common/extensionResourceLoader.ts
+++ code-server/lib/vscode/src/vs/platform/extensionResourceLoader/common/extensionResourceLoader.ts
@@ -98,7 +98,7 @@ export abstract class AbstractExtensionR
 					: 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;
 	}