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 <code-server>/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
@@ -68,6 +68,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> | 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
@@ -342,6 +342,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
@@ -291,7 +291,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';
 
@@ -604,6 +605,39 @@ 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<URI> => {
+			let resolvedUri = uri
+			const localhostMatch = extractLocalHostUriMetaDataForPortMapping(resolvedUri)
+			if (localhostMatch && resolvedUri.authority !== location.host) {
+				if (config.productConfiguration && config.productConfiguration.proxyEndpointTemplate) {
+					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())
+				} else {
+					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<void> = 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.
