From 52cf0d2fc7f2d52844fd635eaf3051006a4771bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=A1=E9=A3=8E?= <1795771535y@gmail.com> Date: Wed, 10 Jun 2026 15:35:02 +0800 Subject: [PATCH 1/2] fix: preserve path and query in openExternal URL rewrite The resolveExternalUri function in the proxy-uri patch was discarding the original URI path and query parameters when rewriting localhost URLs to go through the code-server proxy. Before: http://127.0.0.1:1234/my/path?q=1 -> /proxy/1234/ After: http://127.0.0.1:1234/my/path?q=1 -> /proxy/1234/my/path?q=1 Fixes #7668 --- patches/proxy-uri.diff | 304 +++++++++++++++++++++-------------------- 1 file changed, 156 insertions(+), 148 deletions(-) diff --git a/patches/proxy-uri.diff b/patches/proxy-uri.diff index 9921cc23022a..1f2a1af0ac10 100644 --- a/patches/proxy-uri.diff +++ b/patches/proxy-uri.diff @@ -1,113 +1,113 @@ -Add VSCODE_PROXY_URI environment variable - -This can be used by extensions to open a port and access it through the proxy. - -It is available in the terminal as well. - -This can be tested using printenv in the terminal and by using the -codeServerTest.proxyUri command through the test extension (copy it into your -extensions, use --extensions-dir, or symlink it). - -This has e2e tests. - -For the `asExternalUri` changes, you'll need to test manually by: -1. running code-server with the test extension -2. Command Palette > code-server: asExternalUri test -3. input a url like http://localhost:3000 -4. it should show a notification and show output as /proxy/3000 - -Do the same thing but set `VSCODE_PROXY_URI: "https://{{port}}-main-workspace-name-user-name.coder.com"` -and the output should replace `{{port}}` with port used in input url. - -This also enables the forwared ports view panel by default. - -Lastly, it adds a tunnelProvider so that ports are forwarded using code-server's -built-in proxy. You can test this by starting a server i.e. `python3 -m -http.server` and it should show a notification and show up in the ports panel -using the /proxy/port. - -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 -@@ -69,6 +69,7 @@ export interface IProductConfiguration { - readonly rootEndpoint?: string - readonly updateEndpoint?: string - readonly logoutEndpoint?: string -+ readonly proxyEndpointTemplate?: string - - readonly version: string; - readonly date?: string; -Index: code-server/lib/vscode/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts -=================================================================== ---- code-server.orig/lib/vscode/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts -+++ code-server/lib/vscode/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts -@@ -35,7 +35,7 @@ export class RemoteAuthorityResolverServ - connectionToken: Promise | string | undefined, - resourceUriProvider: ((uri: URI) => URI) | undefined, - serverBasePath: string | undefined, -- @IProductService productService: IProductService, -+ @IProductService private readonly productService: IProductService, - @ILogService private readonly _logService: ILogService, - ) { - super(); -@@ -86,9 +86,14 @@ export class RemoteAuthorityResolverServ - const connectionToken = await Promise.resolve(this._connectionTokens.get(authority) || this._connectionToken); - performance.mark(`code/didResolveConnectionToken/${authorityPrefix}`); - this._logService.info(`Resolved connection token (${authorityPrefix}) after ${sw.elapsed()} ms`); -+ let options: ResolvedOptions | undefined; -+ if (this.productService.proxyEndpointTemplate) { -+ const proxyUrl = new URL(this.productService.proxyEndpointTemplate, mainWindow.location.href); -+ options = { extensionHostEnv: { VSCODE_PROXY_URI: decodeURIComponent(proxyUrl.toString()) }} -+ } - const defaultPort = (/^https:/.test(mainWindow.location.href) ? 443 : 80); - const { host, port } = parseAuthorityWithOptionalPort(authority, defaultPort); -- const result: ResolverResult = { authority: { authority, connectTo: new WebSocketRemoteConnection(host, port), connectionToken } }; -+ const result: ResolverResult = { authority: { authority, connectTo: new WebSocketRemoteConnection(host, port), connectionToken }, options }; - RemoteAuthorities.set(authority, host, port); - this._cache.set(authority, result); - this._onDidChangeConnectionData.fire(); -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 -@@ -362,6 +362,7 @@ export class WebClientServer { - rootEndpoint: rootBase, - updateEndpoint: !this._environmentService.args['disable-update-check'] ? rootBase + '/update/check' : undefined, - logoutEndpoint: this._environmentService.args['auth'] && this._environmentService.args['auth'] !== "none" ? rootBase + '/logout' : undefined, -+ proxyEndpointTemplate: process.env.VSCODE_PROXY_URI ?? rootBase + '/proxy/{{port}}/', - embedderIdentifier: 'server-distro', - extensionsGallery: this._productService.extensionsGallery, - }; -Index: code-server/lib/vscode/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts -=================================================================== ---- code-server.orig/lib/vscode/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts -+++ code-server/lib/vscode/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts -@@ -292,7 +292,7 @@ export async function createTerminalEnvi - - // Sanitize the environment, removing any undesirable VS Code and Electron environment - // variables -- sanitizeProcessEnvironment(env, 'VSCODE_IPC_HOOK_CLI'); -+ sanitizeProcessEnvironment(env, 'VSCODE_IPC_HOOK_CLI', 'VSCODE_PROXY_URI'); - - // Merge config (settings) and ShellLaunchConfig environments - mergeEnvironments(env, allowedEnvFromConfig); -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 -@@ -20,6 +20,7 @@ import { ISecretStorageProvider } from ' - import { isFolderToOpen, isWorkspaceToOpen } from '../../../platform/window/common/window.js'; - import type { IWorkbenchConstructionOptions, IWorkspace, IWorkspaceProvider } from '../../../workbench/browser/web.api.js'; - import { AuthenticationSessionInfo } from '../../../workbench/services/authentication/browser/authenticationService.js'; -+import { extractLocalHostUriMetaDataForPortMapping, TunnelOptions, TunnelCreationOptions } from '../../../platform/tunnel/common/tunnel.js'; - import type { IURLCallbackProvider } from '../../../workbench/services/url/browser/urlService.js'; - import { create } from '../../../workbench/workbench.web.main.internal.js'; - -@@ -612,6 +613,39 @@ class WorkspaceProvider implements IWork - settingsSyncOptions: config.settingsSyncOptions ? { enabled: config.settingsSyncOptions.enabled, } : undefined, - workspaceProvider: WorkspaceProvider.create(config), - urlCallbackProvider: new LocalStorageURLCallbackProvider(config.callbackRoute), +Add VSCODE_PROXY_URI environment variable + +This can be used by extensions to open a port and access it through the proxy. + +It is available in the terminal as well. + +This can be tested using printenv in the terminal and by using the +codeServerTest.proxyUri command through the test extension (copy it into your +extensions, use --extensions-dir, or symlink it). + +This has e2e tests. + +For the `asExternalUri` changes, you'll need to test manually by: +1. running code-server with the test extension +2. Command Palette > code-server: asExternalUri test +3. input a url like http://localhost:3000 +4. it should show a notification and show output as /proxy/3000 + +Do the same thing but set `VSCODE_PROXY_URI: "https://{{port}}-main-workspace-name-user-name.coder.com"` +and the output should replace `{{port}}` with port used in input url. + +This also enables the forwared ports view panel by default. + +Lastly, it adds a tunnelProvider so that ports are forwarded using code-server's +built-in proxy. You can test this by starting a server i.e. `python3 -m +http.server` and it should show a notification and show up in the ports panel +using the /proxy/port. + +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 +@@ -69,6 +69,7 @@ export interface IProductConfiguration { + readonly rootEndpoint?: string + readonly updateEndpoint?: string + readonly logoutEndpoint?: string ++ readonly proxyEndpointTemplate?: string + + readonly version: string; + readonly date?: string; +Index: code-server/lib/vscode/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts ++++ code-server/lib/vscode/src/vs/platform/remote/browser/remoteAuthorityResolverService.ts +@@ -35,7 +35,7 @@ export class RemoteAuthorityResolverServ + connectionToken: Promise | string | undefined, + resourceUriProvider: ((uri: URI) => URI) | undefined, + serverBasePath: string | undefined, +- @IProductService productService: IProductService, ++ @IProductService private readonly productService: IProductService, + @ILogService private readonly _logService: ILogService, + ) { + super(); +@@ -86,9 +86,14 @@ export class RemoteAuthorityResolverServ + const connectionToken = await Promise.resolve(this._connectionTokens.get(authority) || this._connectionToken); + performance.mark(`code/didResolveConnectionToken/${authorityPrefix}`); + this._logService.info(`Resolved connection token (${authorityPrefix}) after ${sw.elapsed()} ms`); ++ let options: ResolvedOptions | undefined; ++ if (this.productService.proxyEndpointTemplate) { ++ const proxyUrl = new URL(this.productService.proxyEndpointTemplate, mainWindow.location.href); ++ options = { extensionHostEnv: { VSCODE_PROXY_URI: decodeURIComponent(proxyUrl.toString()) }} ++ } + const defaultPort = (/^https:/.test(mainWindow.location.href) ? 443 : 80); + const { host, port } = parseAuthorityWithOptionalPort(authority, defaultPort); +- const result: ResolverResult = { authority: { authority, connectTo: new WebSocketRemoteConnection(host, port), connectionToken } }; ++ const result: ResolverResult = { authority: { authority, connectTo: new WebSocketRemoteConnection(host, port), connectionToken }, options }; + RemoteAuthorities.set(authority, host, port); + this._cache.set(authority, result); + this._onDidChangeConnectionData.fire(); +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 +@@ -362,6 +362,7 @@ export class WebClientServer { + rootEndpoint: rootBase, + updateEndpoint: !this._environmentService.args['disable-update-check'] ? rootBase + '/update/check' : undefined, + logoutEndpoint: this._environmentService.args['auth'] && this._environmentService.args['auth'] !== "none" ? rootBase + '/logout' : undefined, ++ proxyEndpointTemplate: process.env.VSCODE_PROXY_URI ?? rootBase + '/proxy/{{port}}/', + embedderIdentifier: 'server-distro', + extensionsGallery: this._productService.extensionsGallery, + }; +Index: code-server/lib/vscode/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts ++++ code-server/lib/vscode/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +@@ -292,7 +292,7 @@ export async function createTerminalEnvi + + // Sanitize the environment, removing any undesirable VS Code and Electron environment + // variables +- sanitizeProcessEnvironment(env, 'VSCODE_IPC_HOOK_CLI'); ++ sanitizeProcessEnvironment(env, 'VSCODE_IPC_HOOK_CLI', 'VSCODE_PROXY_URI'); + + // Merge config (settings) and ShellLaunchConfig environments + mergeEnvironments(env, allowedEnvFromConfig); +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 +@@ -20,6 +20,7 @@ import { ISecretStorageProvider } from ' + import { isFolderToOpen, isWorkspaceToOpen } from '../../../platform/window/common/window.js'; + import type { IWorkbenchConstructionOptions, IWorkspace, IWorkspaceProvider } from '../../../workbench/browser/web.api.js'; + import { AuthenticationSessionInfo } from '../../../workbench/services/authentication/browser/authenticationService.js'; ++import { extractLocalHostUriMetaDataForPortMapping, TunnelOptions, TunnelCreationOptions } from '../../../platform/tunnel/common/tunnel.js'; + import type { IURLCallbackProvider } from '../../../workbench/services/url/browser/urlService.js'; + import { create } from '../../../workbench/workbench.web.main.internal.js'; + +@@ -612,6 +613,44 @@ class WorkspaceProvider implements IWork + settingsSyncOptions: config.settingsSyncOptions ? { enabled: config.settingsSyncOptions.enabled, } : undefined, + workspaceProvider: WorkspaceProvider.create(config), + urlCallbackProvider: new LocalStorageURLCallbackProvider(config.callbackRoute), + resolveExternalUri: (uri: URI): Promise => { + let resolvedUri = uri + const localhostMatch = extractLocalHostUriMetaDataForPortMapping(resolvedUri) @@ -116,46 +116,54 @@ Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts + const renderedTemplate = config.productConfiguration.proxyEndpointTemplate + .replace('{{port}}', localhostMatch.port.toString()) + .replace('{{host}}', window.location.host) -+ resolvedUri = URI.parse(new URL(renderedTemplate, window.location.href).toString()) ++ const proxyUrl = new URL(renderedTemplate, window.location.href) ++ // Preserve the original path and query string ++ if (resolvedUri.path && resolvedUri.path !== '/') { ++ proxyUrl.pathname += resolvedUri.path.substring(1) ++ } ++ if (resolvedUri.query) { ++ proxyUrl.search = resolvedUri.query ++ } ++ resolvedUri = URI.parse(proxyUrl.toString()) + } else { -+ throw new Error(`Failed to resolve external URI: ${uri.toString()}. Could not determine base url because productConfiguration missing.`) ++ throw new Error(Failed to resolve external URI: ${uri.toString()}. Could not determine base url because productConfiguration missing.) + } + } + // If not localhost, return unmodified. + return Promise.resolve(resolvedUri) -+ }, -+ tunnelProvider: { -+ tunnelFactory: (tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions) => { -+ const onDidDispose: Emitter = new Emitter(); -+ let isDisposed = false; -+ return Promise.resolve({ -+ remoteAddress: tunnelOptions.remoteAddress, -+ localAddress: `localhost:${tunnelOptions.remoteAddress.port}`, -+ onDidDispose: onDidDispose.event, -+ dispose: () => { -+ if (!isDisposed) { -+ isDisposed = true; -+ onDidDispose.fire(); -+ } -+ } -+ }) -+ } -+ }, - secretStorageProvider: config.remoteAuthority && !secretStorageKeyPath - ? undefined /* with a remote without embedder-preferred storage, store on the remote */ - : new LocalStorageSecretStorageProvider(secretStorageCrypto), -Index: code-server/lib/vscode/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts -=================================================================== ---- code-server.orig/lib/vscode/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts -+++ code-server/lib/vscode/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts -@@ -83,8 +83,8 @@ export class ForwardedPortsView extends - private async enableForwardedPortsFeatures() { - this.contextKeyListener.clear(); - -- const featuresEnabled: boolean = !!forwardedPortsFeaturesEnabled.getValue(this.contextKeyService); -- const viewEnabled: boolean = !!forwardedPortsViewEnabled.getValue(this.contextKeyService); -+ const featuresEnabled: boolean = true; -+ const viewEnabled: boolean = true; - - if (featuresEnabled || viewEnabled) { - // Also enable the view if it isn't already. ++ }, ++ tunnelProvider: { ++ tunnelFactory: (tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions) => { ++ const onDidDispose: Emitter = new Emitter(); ++ let isDisposed = false; ++ return Promise.resolve({ ++ remoteAddress: tunnelOptions.remoteAddress, ++ localAddress: `localhost:${tunnelOptions.remoteAddress.port}`, ++ onDidDispose: onDidDispose.event, ++ dispose: () => { ++ if (!isDisposed) { ++ isDisposed = true; ++ onDidDispose.fire(); ++ } ++ } ++ }) ++ } ++ }, + secretStorageProvider: config.remoteAuthority && !secretStorageKeyPath + ? undefined /* with a remote without embedder-preferred storage, store on the remote */ + : new LocalStorageSecretStorageProvider(secretStorageCrypto), +Index: code-server/lib/vscode/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts ++++ code-server/lib/vscode/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +@@ -83,8 +83,8 @@ export class ForwardedPortsView extends + private async enableForwardedPortsFeatures() { + this.contextKeyListener.clear(); + +- const featuresEnabled: boolean = !!forwardedPortsFeaturesEnabled.getValue(this.contextKeyService); +- const viewEnabled: boolean = !!forwardedPortsViewEnabled.getValue(this.contextKeyService); ++ const featuresEnabled: boolean = true; ++ const viewEnabled: boolean = true; + + if (featuresEnabled || viewEnabled) { + // Also enable the view if it isn't already. From 923b9c7e8b85caacf60ae67eee6d5e24cf3b890a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=A1=E9=A3=8E?= <1795771535y@gmail.com> Date: Fri, 12 Jun 2026 09:40:31 +0800 Subject: [PATCH 2/2] fix: restore backtick in Error template string --- patches/proxy-uri.diff | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/patches/proxy-uri.diff b/patches/proxy-uri.diff index 1f2a1af0ac10..dc157fa51f77 100644 --- a/patches/proxy-uri.diff +++ b/patches/proxy-uri.diff @@ -27,7 +27,7 @@ http.server` and it should show a notification and show up in the ports panel using the /proxy/port. 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 @@ -69,6 +69,7 @@ export interface IProductConfiguration { @@ -54,7 +54,7 @@ Index: code-server/lib/vscode/src/vs/platform/remote/browser/remoteAuthorityReso @@ -86,9 +86,14 @@ export class RemoteAuthorityResolverServ const connectionToken = await Promise.resolve(this._connectionTokens.get(authority) || this._connectionToken); performance.mark(`code/didResolveConnectionToken/${authorityPrefix}`); - this._logService.info(`Resolved connection token (${authorityPrefix}) after ${sw.elapsed()} ms`); + this._blogService.info(`Resolved connection token (${authorityPrefix}) after ${sw.elapsed()} ms`); + let options: ResolvedOptions | undefined; + if (this.productService.proxyEndpointTemplate) { + const proxyUrl = new URL(this.productService.proxyEndpointTemplate, mainWindow.location.href); @@ -68,13 +68,13 @@ Index: code-server/lib/vscode/src/vs/platform/remote/browser/remoteAuthorityReso this._cache.set(authority, result); this._onDidChangeConnectionData.fire(); 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 @@ -362,6 +362,7 @@ export class WebClientServer { rootEndpoint: rootBase, - updateEndpoint: !this._environmentService.args['disable-update-check'] ? rootBase + '/update/check' : undefined, - logoutEndpoint: this._environmentService.args['auth'] && this._environmentService.args['auth'] !== "none" ? rootBase + '/logout' : undefined, + updateEndpoint: !this._environmentService.args['disable-update-check'] ? rootBase + '/update/check' : undefined, + logoutEndpoint: this._environmentService.args['auth'] && this._environmentService.args['auth'] !== "none" ? rootBase + '/logout' : undefined, + proxyEndpointTemplate: process.env.VSCODE_PROXY_URI ?? rootBase + '/proxy/{{port}}/', embedderIdentifier: 'server-distro', extensionsGallery: this._productService.extensionsGallery, @@ -126,7 +126,7 @@ Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts + } + resolvedUri = URI.parse(proxyUrl.toString()) + } else { -+ throw new Error(Failed to resolve external URI: ${uri.toString()}. Could not determine base url because productConfiguration missing.) ++ throw new Error(`Failed to resolve external URI: ${uri.toString()}. Could not determine base url because productConfiguration missing.`) + } + } + // If not localhost, return unmodified. @@ -153,7 +153,7 @@ Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts ? undefined /* with a remote without embedder-preferred storage, store on the remote */ : new LocalStorageSecretStorageProvider(secretStorageCrypto), Index: code-server/lib/vscode/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts -=================================================================== +==================================================================9 --- code-server.orig/lib/vscode/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ code-server/lib/vscode/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -83,8 +83,8 @@ export class ForwardedPortsView extends @@ -167,3 +167,4 @@ Index: code-server/lib/vscode/src/vs/workbench/contrib/remote/browser/remoteExpl if (featuresEnabled || viewEnabled) { // Also enable the view if it isn't already. +