Skip to content

Make relative paths work at any depth #24

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/vs/base/common/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,19 @@ 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?: {
readonly url: string;
readonly scope: string;
}
readonly icons: Array<{ src: string; type: string; sizes: string }>;

//#regionend

readonly version: string;
Expand Down
20 changes: 10 additions & 10 deletions src/vs/code/browser/workbench/workbench-dev.html
Original file line number Diff line number Diff line change
Expand Up @@ -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/resources/server/code-192.png" />
<link rel="apple-touch-icon" sizes="512x512" href="./static/resources/server/code-512.png" />
<link rel="apple-touch-icon" sizes="192x192" href="{{BASE}}/static/resources/server/code-192.png" />
<link rel="apple-touch-icon" sizes="512x512" href="{{BASE}}/static/resources/server/code-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">
Expand All @@ -27,30 +27,30 @@
<meta id="vscode-workbench-builtin-extensions" data-settings="{{WORKBENCH_BUILTIN_EXTENSIONS}}">

<!-- Workbench Icon/Manifest/CSS -->
<link rel="icon" href="./static/resources/server/favicon-dark-support.svg" type="image/svg+xml" />
<link rel="alternate icon" href="./static/resources/server/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="{{BASE}}/static/resources/server/favicon-dark-support.svg" type="image/svg+xml" />
<link rel="alternate icon" href="{{BASE}}/static/resources/server/favicon.svg" type="image/svg+xml" />
<meta name="theme-color" content="{{CLIENT_BACKGROUND_COLOR}}">

<link rel="icon" href="./favicon.ico" type="image/x-icon" />
<link rel="manifest" href="./manifest.json" crossorigin="use-credentials">
<link rel="icon" href="{{BASE}}/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="{{BASE}}/manifest.json" crossorigin="use-credentials">
</head>

<body style="background-color: var(--vs-theme-background-color); color: var(--vs-theme-foreground-color)" aria-label="">
</body>

<!-- Startup (do not modify order of script tags!) -->
<script src="./static/out/vs/loader.js"></script>
<script src="./static/out/vs/webPackagePaths.js"></script>
<script src="{{BASE}}/static/out/vs/loader.js"></script>
<script src="{{BASE}}/static/out/vs/webPackagePaths.js"></script>
<script>
/**
* Updated to use relative path.
* @author coder
*/
Object.keys(self.webPackagePaths).map(function (key, index) {
self.webPackagePaths[key] = new URL(`./static/remote/web/node_modules/${key}/${self.webPackagePaths[key]}`, window.location.href).toString();
self.webPackagePaths[key] = new URL(`{{BASE}}/static/remote/web/node_modules/${key}/${self.webPackagePaths[key]}`, window.location.href).toString();
});
require.config({
baseUrl: new URL('./static/out', window.location.href).toString(),
baseUrl: new URL('{{BASE}}/static/out', window.location.href).toString(),
recordStats: true,
trustedTypesPolicy: window.trustedTypes?.createPolicy('amdLoader', {
createScriptURL(value) {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/code/browser/workbench/workbench-error.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

<!-- Workbench Icon/Manifest/CSS -->
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<link rel="manifest" href="./manifest.json" crossorigin="use-credentials">
<link rel="manifest" href="{{BASE}}/manifest.json" crossorigin="use-credentials">

<meta name="theme-color" content="{{CLIENT_BACKGROUND_COLOR}}">

Expand Down
28 changes: 14 additions & 14 deletions src/vs/code/browser/workbench/workbench.html
Original file line number Diff line number Diff line change
Expand Up @@ -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/resources/server/code-192.png" />
<link rel="apple-touch-icon" sizes="512x512" href="./static/resources/server/code-512.png" />
<link rel="apple-touch-icon" sizes="192x192" href="{{BASE}}/static/resources/server/code-192.png" />
<link rel="apple-touch-icon" sizes="512x512" href="{{BASE}}/static/resources/server/code-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">
Expand All @@ -24,32 +24,32 @@
<meta id="vscode-workbench-auth-session" data-settings="{{WORKBENCH_AUTH_SESSION}}">

<!-- Workbench Icon/Manifest/CSS -->
<link rel="icon" href="./static/resources/server/favicon-dark-support.svg" type="image/svg+xml" />
<link rel="alternate icon" href="./static/resources/server/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="{{BASE}}/static/resources/server/favicon-dark-support.svg" type="image/svg+xml" />
<link rel="alternate icon" href="{{BASE}}/static/resources/server/favicon.svg" type="image/svg+xml" />

<link rel="icon" href="./favicon.ico" type="image/x-icon" />
<link rel="manifest" href="./manifest.json" crossorigin="use-credentials">
<link rel="icon" href="{{BASE}}/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="{{BASE}}/manifest.json" crossorigin="use-credentials">
<meta name="theme-color" content="{{CLIENT_BACKGROUND_COLOR}}">

<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="./static/out/vs/workbench/workbench.web.api.css">
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="{{BASE}}/static/out/vs/workbench/workbench.web.api.css">
</head>

<body style="background-color: var(--vs-theme-background-color); color: var(--vs-theme-foreground-color)" aria-label="">
</body>

<!-- Startup (do not modify order of script tags!) -->
<script src="./static/out/vs/loader.js"></script>
<script src="./static/out/vs/webPackagePaths.js"></script>
<script src="{{BASE}}/static/out/vs/loader.js"></script>
<script src="{{BASE}}/static/out/vs/webPackagePaths.js"></script>
<script>
/**
* Updated to use relative path.
* @author coder
*/
Object.keys(self.webPackagePaths).map(function (key, index) {
self.webPackagePaths[key] = new URL(`./static/node_modules/${key}/${self.webPackagePaths[key]}`, window.location.href).toString();
self.webPackagePaths[key] = new URL(`{{BASE}}/static/node_modules/${key}/${self.webPackagePaths[key]}`, window.location.href).toString();
});
require.config({
baseUrl: new URL('./static/out', window.location.href).toString(),
baseUrl: new URL('{{BASE}}/static/out', window.location.href).toString(),
recordStats: true,
trustedTypesPolicy: window.trustedTypes?.createPolicy('amdLoader', {
createScriptURL(value) {
Expand All @@ -62,7 +62,7 @@
<script>
performance.mark('code/willLoadWorkbenchMain');
</script>
<script src="./static/out/vs/workbench/workbench.web.api.nls.js"></script>
<script src="./static/out/vs/workbench/workbench.web.api.js"></script>
<script src="./static/out/vs/code/browser/workbench/workbench.js"></script>
<script src="{{BASE}}/static/out/vs/workbench/workbench.web.api.nls.js"></script>
<script src="{{BASE}}/static/out/vs/workbench/workbench.web.api.js"></script>
<script src="{{BASE}}/static/out/vs/code/browser/workbench/workbench.js"></script>
</html>
90 changes: 74 additions & 16 deletions src/vs/server/common/net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
Expand All @@ -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
}
37 changes: 19 additions & 18 deletions src/vs/server/webClientServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<void> {
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.',
Expand Down Expand Up @@ -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(<IWorkbenchConstructionOptions>{
productConfiguration: {
Expand All @@ -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,
Expand All @@ -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\';',
Expand Down Expand Up @@ -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<void> => {
serveError = async (req: http.IncomingMessage, res: http.ServerResponse, code: number, message: string, parsedUrl?: url.UrlWithParsedQuery): Promise<void> => {
const { applicationName, commit = 'development', version } = this._productService;

res.statusCode = code;
Expand Down Expand Up @@ -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);
};
Expand Down
9 changes: 5 additions & 4 deletions src/vs/workbench/browser/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
});

Expand Down