Skip to content

Commit 26ccfd6

Browse files
code-asherZauberNerd
authored andcommitted
Make relative paths work at any depth (microsoft#24)
1 parent eea5fec commit 26ccfd6

File tree

7 files changed

+126
-65
lines changed

7 files changed

+126
-65
lines changed

src/vs/base/common/product.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,19 @@ export type ExtensionVirtualWorkspaceSupport = {
3333

3434
export interface IProductConfiguration {
3535
//#region Code Server Additions
36-
36+
3737
readonly codeServerVersion?: string;
3838
readonly auth?: AuthType;
3939

40+
readonly base: string;
4041
readonly logoutEndpointUrl: string;
4142
readonly proxyEndpointUrlTemplate?: string;
4243
readonly serviceWorker?: {
4344
readonly url: string;
4445
readonly scope: string;
4546
}
4647
readonly icons: Array<{ src: string; type: string; sizes: string }>;
47-
48+
4849
//#regionend
4950

5051
readonly version: string;

src/vs/code/browser/workbench/workbench-dev.html

+10-10
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
<meta name="mobile-web-app-capable" content="yes" />
1212
<meta name="apple-mobile-web-app-capable" content="yes" />
1313
<meta name="apple-mobile-web-app-title" content="Code">
14-
<link rel="apple-touch-icon" sizes="192x192" href="./static/resources/server/code-192.png" />
15-
<link rel="apple-touch-icon" sizes="512x512" href="./static/resources/server/code-512.png" />
14+
<link rel="apple-touch-icon" sizes="192x192" href="{{BASE}}/static/resources/server/code-192.png" />
15+
<link rel="apple-touch-icon" sizes="512x512" href="{{BASE}}/static/resources/server/code-512.png" />
1616

1717
<!-- Disable pinch zooming -->
1818
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
@@ -27,30 +27,30 @@
2727
<meta id="vscode-workbench-builtin-extensions" data-settings="{{WORKBENCH_BUILTIN_EXTENSIONS}}">
2828

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

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

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

4141
<!-- Startup (do not modify order of script tags!) -->
42-
<script src="./static/out/vs/loader.js"></script>
43-
<script src="./static/out/vs/webPackagePaths.js"></script>
42+
<script src="{{BASE}}/static/out/vs/loader.js"></script>
43+
<script src="{{BASE}}/static/out/vs/webPackagePaths.js"></script>
4444
<script>
4545
/**
4646
* Updated to use relative path.
4747
* @author coder
4848
*/
4949
Object.keys(self.webPackagePaths).map(function (key, index) {
50-
self.webPackagePaths[key] = new URL(`./static/remote/web/node_modules/${key}/${self.webPackagePaths[key]}`, window.location.href).toString();
50+
self.webPackagePaths[key] = new URL(`{{BASE}}/static/remote/web/node_modules/${key}/${self.webPackagePaths[key]}`, window.location.href).toString();
5151
});
5252
require.config({
53-
baseUrl: new URL('./static/out', window.location.href).toString(),
53+
baseUrl: new URL('{{BASE}}/static/out', window.location.href).toString(),
5454
recordStats: true,
5555
trustedTypesPolicy: window.trustedTypes?.createPolicy('amdLoader', {
5656
createScriptURL(value) {

src/vs/code/browser/workbench/workbench-error.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

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

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

src/vs/code/browser/workbench/workbench.html

+14-14
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
<meta name="mobile-web-app-capable" content="yes" />
1212
<meta name="apple-mobile-web-app-capable" content="yes" />
1313
<meta name="apple-mobile-web-app-title" content="Code">
14-
<link rel="apple-touch-icon" sizes="192x192" href="./static/resources/server/code-192.png" />
15-
<link rel="apple-touch-icon" sizes="512x512" href="./static/resources/server/code-512.png" />
14+
<link rel="apple-touch-icon" sizes="192x192" href="{{BASE}}/static/resources/server/code-192.png" />
15+
<link rel="apple-touch-icon" sizes="512x512" href="{{BASE}}/static/resources/server/code-512.png" />
1616

1717
<!-- Disable pinch zooming -->
1818
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
@@ -24,32 +24,32 @@
2424
<meta id="vscode-workbench-auth-session" data-settings="{{WORKBENCH_AUTH_SESSION}}">
2525

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

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

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

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

4040
<!-- Startup (do not modify order of script tags!) -->
41-
<script src="./static/out/vs/loader.js"></script>
42-
<script src="./static/out/vs/webPackagePaths.js"></script>
41+
<script src="{{BASE}}/static/out/vs/loader.js"></script>
42+
<script src="{{BASE}}/static/out/vs/webPackagePaths.js"></script>
4343
<script>
4444
/**
4545
* Updated to use relative path.
4646
* @author coder
4747
*/
4848
Object.keys(self.webPackagePaths).map(function (key, index) {
49-
self.webPackagePaths[key] = new URL(`./static/node_modules/${key}/${self.webPackagePaths[key]}`, window.location.href).toString();
49+
self.webPackagePaths[key] = new URL(`{{BASE}}/static/node_modules/${key}/${self.webPackagePaths[key]}`, window.location.href).toString();
5050
});
5151
require.config({
52-
baseUrl: new URL('./static/out', window.location.href).toString(),
52+
baseUrl: new URL('{{BASE}}/static/out', window.location.href).toString(),
5353
recordStats: true,
5454
trustedTypesPolicy: window.trustedTypes?.createPolicy('amdLoader', {
5555
createScriptURL(value) {
@@ -62,7 +62,7 @@
6262
<script>
6363
performance.mark('code/willLoadWorkbenchMain');
6464
</script>
65-
<script src="./static/out/vs/workbench/workbench.web.api.nls.js"></script>
66-
<script src="./static/out/vs/workbench/workbench.web.api.js"></script>
67-
<script src="./static/out/vs/code/browser/workbench/workbench.js"></script>
65+
<script src="{{BASE}}/static/out/vs/workbench/workbench.web.api.nls.js"></script>
66+
<script src="{{BASE}}/static/out/vs/workbench/workbench.web.api.js"></script>
67+
<script src="{{BASE}}/static/out/vs/code/browser/workbench/workbench.js"></script>
6868
</html>

src/vs/server/common/net.ts

+74-16
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Licensed under the MIT License. See License.txt in the project root for license information.
55
*--------------------------------------------------------------------------------------------*/
66

7-
import { posix } from 'vs/base/common/path';
7+
import * as http from 'http';
88

99
/**
1010
* 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 {
2626

2727
export const ICON_SIZES = [192, 512];
2828

29-
/**
30-
* Returns the relative path prefix for a given URL path.
31-
* @remark This is especially useful when creating URLs which have to remain
32-
* relative to an initial request.
33-
*
34-
* @example
35-
* ```ts
36-
* const url = new URL('https://www.example.com/foo/bar/baz.js')
37-
* getPathPrefix(url.pathname) // '/foo/bar/'
38-
* ```
39-
*/
40-
export function getPathPrefix(pathname: string) {
41-
return posix.join(posix.dirname(pathname), '/');
42-
}
43-
4429
class HTTPError extends Error {
4530
constructor (message: string, public readonly code: number) {
4631
super(message);
@@ -52,3 +37,76 @@ export class HTTPNotFoundError extends HTTPError {
5237
super(message, 404);
5338
}
5439
}
40+
41+
/**
42+
* Remove extra slashes in a URL.
43+
*
44+
* This is meant to fill the job of `path.join` so you can concatenate paths and
45+
* then normalize out any extra slashes.
46+
*
47+
* If you are using `path.join` you do not need this but note that `path` is for
48+
* file system paths, not URLs.
49+
*
50+
* @author coder
51+
*/
52+
export const normalize = (url: string, keepTrailing = false): string => {
53+
return url.replace(/\/\/+/g, "/").replace(/\/+$/, keepTrailing ? "/" : "")
54+
}
55+
56+
/**
57+
* Get the relative path that will get us to the root of the page. For each
58+
* slash we need to go up a directory. Will not have a trailing slash.
59+
*
60+
* For example:
61+
*
62+
* / => .
63+
* /foo => .
64+
* /foo/ => ./..
65+
* /foo/bar => ./..
66+
* /foo/bar/ => ./../..
67+
*
68+
* All paths must be relative in order to work behind a reverse proxy since we
69+
* we do not know the base path. Anything that needs to be absolute (for
70+
* example cookies) must get the base path from the frontend.
71+
*
72+
* All relative paths must be prefixed with the relative root to ensure they
73+
* work no matter the depth at which they happen to appear.
74+
*
75+
* For Express `req.originalUrl` should be used as they remove the base from the
76+
* standard `url` property making it impossible to get the true depth.
77+
*
78+
* @author coder
79+
*/
80+
export const relativeRoot = (originalUrl: string): string => {
81+
const depth = (originalUrl.split("?", 1)[0].match(/\//g) || []).length
82+
return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : ""))
83+
}
84+
85+
/**
86+
* Get the relative path to the current resource.
87+
*
88+
* For example:
89+
*
90+
* / => .
91+
* /foo => ./foo
92+
* /foo/ => ./
93+
* /foo/bar => ./bar
94+
* /foo/bar/ => ./bar/
95+
*/
96+
export const relativePath = (originalUrl: string): string => {
97+
const parts = originalUrl.split("?", 1)[0].split("/")
98+
return normalize("./" + parts[parts.length - 1])
99+
}
100+
101+
/**
102+
* code-server serves VS Code using Express. Express removes the base from
103+
* the url and puts the original in `originalUrl` so we must use this to get
104+
* the correct depth. VS Code is not aware it is behind Express so the
105+
* types do not match. We may want to continue moving code into VS Code and
106+
* eventually remove the Express wrapper.
107+
*
108+
* @author coder
109+
*/
110+
export const getOriginalUrl = (req: http.IncomingMessage): string => {
111+
return (req as any).originalUrl || req.url
112+
}

src/vs/server/webClientServer.ts

+19-18
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { IProductService } from 'vs/platform/product/common/productService';
2525
// eslint-disable-next-line code-import-patterns
2626
import type { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api';
2727
import { editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry';
28-
import { ClientTheme, getPathPrefix, HTTPNotFoundError, WebManifest } from 'vs/server/common/net';
28+
import { ClientTheme, getOriginalUrl, HTTPNotFoundError, relativePath, relativeRoot, WebManifest } from 'vs/server/common/net';
2929
import { IServerThemeService } from 'vs/server/serverThemeService';
3030
import { isFalsyOrWhitespace } from 'vs/base/common/strings';
3131

@@ -193,17 +193,13 @@ export class WebClientServer {
193193
* PWA manifest file. This informs the browser that the app may be installed.
194194
*/
195195
private async _handleManifest(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
196-
const pathPrefix = getPathPrefix(parsedUrl.pathname!);
196+
// The manifest URL is used as the base when resolving URLs so we can just
197+
// use . without having to check the depth since we serve it at the root.
197198
const clientTheme = await this.fetchClientTheme();
198-
const startUrl = pathPrefix.substring(
199-
0,
200-
pathPrefix.lastIndexOf('/') + 1
201-
);
202-
203199
const webManifest: WebManifest = {
204200
name: this._productService.nameLong,
205201
short_name: this._productService.nameShort,
206-
start_url: normalize(startUrl),
202+
start_url: '.',
207203
display: 'fullscreen',
208204
'background-color': clientTheme.backgroundColor,
209205
description: 'Run editors on a remote server.',
@@ -319,6 +315,8 @@ export class WebClientServer {
319315
scopes: [['user:email'], ['repo']]
320316
} : undefined;
321317

318+
const base = relativeRoot(getOriginalUrl(req))
319+
const vscodeBase = relativePath(getOriginalUrl(req))
322320
const data = (await util.promisify(fs.readFile)(filePath)).toString()
323321
.replace('{{WORKBENCH_WEB_CONFIGURATION}}', escapeAttribute(JSON.stringify(<IWorkbenchConstructionOptions>{
324322
productConfiguration: {
@@ -329,17 +327,18 @@ export class WebClientServer {
329327

330328
// Service Worker
331329
serviceWorker: {
332-
scope: './',
333-
url: `./${this._environmentService.serviceWorkerFileName}`
330+
scope: vscodeBase + '/',
331+
url: vscodeBase + '/' + this._environmentService.serviceWorkerFileName,
334332
},
335333

336334
// Endpoints
337-
logoutEndpointUrl: './logout',
338-
webEndpointUrl: './static',
339-
webEndpointUrlTemplate: './static',
340-
webviewContentExternalBaseUrlTemplate: './webview/{{uuid}}/',
335+
base,
336+
logoutEndpointUrl: base + '/logout',
337+
webEndpointUrl: vscodeBase + '/static',
338+
webEndpointUrlTemplate: vscodeBase + '/static',
339+
webviewContentExternalBaseUrlTemplate: vscodeBase + '/webview/{{uuid}}/',
341340

342-
updateUrl: './update/check'
341+
updateUrl: base + '/update/check'
343342
},
344343
folderUri: (workspacePath && isFolder) ? transformer.transformOutgoing(URI.file(workspacePath)) : undefined,
345344
workspaceUri: (workspacePath && !isFolder) ? transformer.transformOutgoing(URI.file(workspacePath)) : undefined,
@@ -355,7 +354,8 @@ export class WebClientServer {
355354
})))
356355
.replace(/{{CLIENT_BACKGROUND_COLOR}}/g, () => backgroundColor)
357356
.replace(/{{CLIENT_FOREGROUND_COLOR}}/g, () => foregroundColor)
358-
.replace('{{WORKBENCH_AUTH_SESSION}}', () => authSessionInfo ? escapeAttribute(JSON.stringify(authSessionInfo)) : '');
357+
.replace('{{WORKBENCH_AUTH_SESSION}}', () => authSessionInfo ? escapeAttribute(JSON.stringify(authSessionInfo)) : '')
358+
.replace(/{{BASE}}/g, () => vscodeBase);
359359

360360
const cspDirectives = [
361361
'default-src \'self\';',
@@ -504,7 +504,7 @@ export class WebClientServer {
504504
return res.end(JSON.stringify(knownCallbackUri));
505505
}
506506

507-
serveError = async (_req: http.IncomingMessage, res: http.ServerResponse, code: number, message: string, parsedUrl?: url.UrlWithParsedQuery): Promise<void> => {
507+
serveError = async (req: http.IncomingMessage, res: http.ServerResponse, code: number, message: string, parsedUrl?: url.UrlWithParsedQuery): Promise<void> => {
508508
const { applicationName, commit = 'development', version } = this._productService;
509509

510510
res.statusCode = code;
@@ -532,7 +532,8 @@ export class WebClientServer {
532532
.replace(/{{ERROR_MESSAGE}}/g, () => message)
533533
.replace(/{{ERROR_FOOTER}}/g, () => `${version} - ${commit}`)
534534
.replace(/{{CLIENT_BACKGROUND_COLOR}}/g, () => clientTheme.backgroundColor)
535-
.replace(/{{CLIENT_FOREGROUND_COLOR}}/g, () => clientTheme.foregroundColor);
535+
.replace(/{{CLIENT_FOREGROUND_COLOR}}/g, () => clientTheme.foregroundColor)
536+
.replace(/{{BASE}}/g, () => relativePath(getOriginalUrl(req)));
536537

537538
res.end(data);
538539
};

src/vs/workbench/browser/client.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export class CodeServerClientAdditions extends Disposable {
176176
}
177177

178178
private appendSessionCommands() {
179-
const { auth, logoutEndpointUrl } = this.productConfiguration;
179+
const { auth, base, logoutEndpointUrl } = this.productConfiguration;
180180

181181
// Use to show or hide logout commands and menu options.
182182
this.contextKeyService.createKey(CodeServerClientAdditions.AUTH_KEY, auth === AuthType.Password);
@@ -190,9 +190,10 @@ export class CodeServerClientAdditions extends Disposable {
190190
* @file 'code-server/src/node/route/logout.ts'
191191
*/
192192
const logoutUrl = new URL(logoutEndpointUrl!, window.location.href);
193-
// Add base param as this session may be stored within a nested path.
194-
logoutUrl.searchParams.set('base', window.location.pathname);
195-
193+
// Inform the backend about the path since the proxy might have rewritten
194+
// it out of the headers and cookies must be set with absolute paths.
195+
logoutUrl.searchParams.set('base', base || ".");
196+
logoutUrl.searchParams.set('href', window.location.href);
196197
window.location.assign(logoutUrl);
197198
});
198199

0 commit comments

Comments
 (0)