From a859e8024b687791b981dacbf25cb327630f571b Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 8 Dec 2021 18:33:23 +0000 Subject: [PATCH] Make relative paths work at any depth --- src/vs/base/common/product.ts | 5 +- .../code/browser/workbench/workbench-dev.html | 20 ++--- .../browser/workbench/workbench-error.html | 2 +- src/vs/code/browser/workbench/workbench.html | 28 +++--- src/vs/server/common/net.ts | 90 +++++++++++++++---- src/vs/server/webClientServer.ts | 37 ++++---- src/vs/workbench/browser/client.ts | 9 +- 7 files changed, 126 insertions(+), 65 deletions(-) diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index ce1b1926eb60e..11b2dd05b6503 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -33,10 +33,11 @@ export type ExtensionVirtualWorkspaceSupport = { export interface IProductConfiguration { //#region Code Server Additions - + readonly codeServerVersion?: string; readonly auth?: AuthType; + readonly base: string; readonly logoutEndpointUrl: string; readonly proxyEndpointUrlTemplate?: string; readonly serviceWorker?: { @@ -44,7 +45,7 @@ export interface IProductConfiguration { readonly scope: string; } readonly icons: Array<{ src: string; type: string; sizes: string }>; - + //#regionend readonly version: string; diff --git a/src/vs/code/browser/workbench/workbench-dev.html b/src/vs/code/browser/workbench/workbench-dev.html index 79d59255217b2..7caeae015cfaa 100644 --- a/src/vs/code/browser/workbench/workbench-dev.html +++ b/src/vs/code/browser/workbench/workbench-dev.html @@ -11,8 +11,8 @@ - - + + @@ -27,30 +27,30 @@ - - + + - - + + - - + + - + + - - - + + + diff --git a/src/vs/server/common/net.ts b/src/vs/server/common/net.ts index 82c2ab077c575..8c4583858cfe5 100644 --- a/src/vs/server/common/net.ts +++ b/src/vs/server/common/net.ts @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { posix } from 'vs/base/common/path'; +import * as http from 'http'; /** * See [Web app manifest on MDN](https://developer.mozilla.org/en-US/docs/Web/Manifest) for additional information. @@ -26,21 +26,6 @@ export interface ClientTheme { export const ICON_SIZES = [192, 512]; -/** - * Returns the relative path prefix for a given URL path. - * @remark This is especially useful when creating URLs which have to remain - * relative to an initial request. - * - * @example - * ```ts - * const url = new URL('https://www.example.com/foo/bar/baz.js') - * getPathPrefix(url.pathname) // '/foo/bar/' - * ``` - */ -export function getPathPrefix(pathname: string) { - return posix.join(posix.dirname(pathname), '/'); -} - class HTTPError extends Error { constructor (message: string, public readonly code: number) { super(message); @@ -52,3 +37,76 @@ export class HTTPNotFoundError extends HTTPError { super(message, 404); } } + +/** + * 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. + * + * @author coder + */ +export const normalize = (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. + * + * @author coder + */ +export const relativeRoot = (originalUrl: string): string => { + const depth = (originalUrl.split("?", 1)[0].match(/\//g) || []).length + return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : "")) +} + +/** + * Get the relative path to the current resource. + * + * For example: + * + * / => . + * /foo => ./foo + * /foo/ => ./ + * /foo/bar => ./bar + * /foo/bar/ => ./bar/ + */ +export const relativePath = (originalUrl: string): string => { + const parts = originalUrl.split("?", 1)[0].split("/") + return normalize("./" + parts[parts.length - 1]) +} + +/** + * code-server serves VS 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. VS Code is not aware it is behind Express so the + * types do not match. We may want to continue moving code into VS Code and + * eventually remove the Express wrapper. + * + * @author coder + */ +export const getOriginalUrl = (req: http.IncomingMessage): string => { + return (req as any).originalUrl || req.url +} diff --git a/src/vs/server/webClientServer.ts b/src/vs/server/webClientServer.ts index 87227981faefd..00a04d9140926 100644 --- a/src/vs/server/webClientServer.ts +++ b/src/vs/server/webClientServer.ts @@ -25,7 +25,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; // eslint-disable-next-line code-import-patterns import type { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api'; import { editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; -import { ClientTheme, getPathPrefix, HTTPNotFoundError, WebManifest } from 'vs/server/common/net'; +import { ClientTheme, getOriginalUrl, HTTPNotFoundError, relativePath, relativeRoot, WebManifest } from 'vs/server/common/net'; import { IServerThemeService } from 'vs/server/serverThemeService'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; @@ -194,17 +194,13 @@ export class WebClientServer { * PWA manifest file. This informs the browser that the app may be installed. */ private async _handleManifest(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise { - const pathPrefix = getPathPrefix(parsedUrl.pathname!); + // The manifest URL is used as the base when resolving URLs so we can just + // use . without having to check the depth since we serve it at the root. const clientTheme = await this.fetchClientTheme(); - const startUrl = pathPrefix.substring( - 0, - pathPrefix.lastIndexOf('/') + 1 - ); - const webManifest: WebManifest = { name: this._productService.nameLong, short_name: this._productService.nameShort, - start_url: normalize(startUrl), + start_url: '.', display: 'fullscreen', 'background-color': clientTheme.backgroundColor, description: 'Run editors on a remote server.', @@ -320,6 +316,8 @@ export class WebClientServer { 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({ productConfiguration: { @@ -330,17 +328,18 @@ export class WebClientServer { // Service Worker serviceWorker: { - scope: './', - url: `./${this._environmentService.serviceWorkerFileName}` + scope: vscodeBase + '/', + url: vscodeBase + '/' + this._environmentService.serviceWorkerFileName, }, // Endpoints - logoutEndpointUrl: './logout', - webEndpointUrl: './static', - webEndpointUrlTemplate: './static', - webviewContentExternalBaseUrlTemplate: './webview/{{uuid}}/', + base, + logoutEndpointUrl: base + '/logout', + webEndpointUrl: vscodeBase + '/static', + webEndpointUrlTemplate: vscodeBase + '/static', + webviewContentExternalBaseUrlTemplate: vscodeBase + '/webview/{{uuid}}/', - updateUrl: './update/check' + updateUrl: base + '/update/check' }, folderUri: (workspacePath && isFolder) ? transformer.transformOutgoing(URI.file(workspacePath)) : undefined, workspaceUri: (workspacePath && !isFolder) ? transformer.transformOutgoing(URI.file(workspacePath)) : undefined, @@ -356,7 +355,8 @@ export class WebClientServer { }))) .replace(/{{CLIENT_BACKGROUND_COLOR}}/g, () => backgroundColor) .replace(/{{CLIENT_FOREGROUND_COLOR}}/g, () => foregroundColor) - .replace('{{WORKBENCH_AUTH_SESSION}}', () => authSessionInfo ? escapeAttribute(JSON.stringify(authSessionInfo)) : ''); + .replace('{{WORKBENCH_AUTH_SESSION}}', () => authSessionInfo ? escapeAttribute(JSON.stringify(authSessionInfo)) : '') + .replace(/{{BASE}}/g, () => vscodeBase); const cspDirectives = [ 'default-src \'self\';', @@ -505,7 +505,7 @@ export class WebClientServer { return res.end(JSON.stringify(knownCallbackUri)); } - serveError = async (_req: http.IncomingMessage, res: http.ServerResponse, code: number, message: string, parsedUrl?: url.UrlWithParsedQuery): Promise => { + serveError = async (req: http.IncomingMessage, res: http.ServerResponse, code: number, message: string, parsedUrl?: url.UrlWithParsedQuery): Promise => { const { applicationName, commit = 'development', version } = this._productService; res.statusCode = code; @@ -533,7 +533,8 @@ export class WebClientServer { .replace(/{{ERROR_MESSAGE}}/g, () => message) .replace(/{{ERROR_FOOTER}}/g, () => `${version} - ${commit}`) .replace(/{{CLIENT_BACKGROUND_COLOR}}/g, () => clientTheme.backgroundColor) - .replace(/{{CLIENT_FOREGROUND_COLOR}}/g, () => clientTheme.foregroundColor); + .replace(/{{CLIENT_FOREGROUND_COLOR}}/g, () => clientTheme.foregroundColor) + .replace(/{{BASE}}/g, () => relativePath(getOriginalUrl(req))); res.end(data); }; diff --git a/src/vs/workbench/browser/client.ts b/src/vs/workbench/browser/client.ts index ffa033abb8895..3c7c8aacfa378 100644 --- a/src/vs/workbench/browser/client.ts +++ b/src/vs/workbench/browser/client.ts @@ -176,7 +176,7 @@ export class CodeServerClientAdditions extends Disposable { } private appendSessionCommands() { - const { auth, logoutEndpointUrl } = this.productConfiguration; + const { auth, base, logoutEndpointUrl } = this.productConfiguration; // Use to show or hide logout commands and menu options. this.contextKeyService.createKey(CodeServerClientAdditions.AUTH_KEY, auth === AuthType.Password); @@ -190,9 +190,10 @@ export class CodeServerClientAdditions extends Disposable { * @file 'code-server/src/node/route/logout.ts' */ const logoutUrl = new URL(logoutEndpointUrl!, window.location.href); - // Add base param as this session may be stored within a nested path. - logoutUrl.searchParams.set('base', window.location.pathname); - + // Inform the backend about the path since the proxy might have rewritten + // it out of the headers and cookies must be set with absolute paths. + logoutUrl.searchParams.set('base', base || "."); + logoutUrl.searchParams.set('href', window.location.href); window.location.assign(logoutUrl); });