mirror of
https://github.com/coder/code-server.git
synced 2026-09-19 14:01:28 +02:00
Preserve path, query, and fragment in proxy URL rewrite (#8012)
This commit is contained in:
@@ -22,6 +22,11 @@ Code v99.99.999
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Preserve the original path, query parameters, and fragment when rewriting
|
||||||
|
localhost URLs through the port proxy (#7668).
|
||||||
|
|
||||||
## [4.137.0](https://github.com/coder/code-server/releases/tag/v4.137.0) - 2026-09-11
|
## [4.137.0](https://github.com/coder/code-server/releases/tag/v4.137.0) - 2026-09-11
|
||||||
|
|
||||||
Code v1.137.0
|
Code v1.137.0
|
||||||
|
|||||||
@@ -13,12 +13,18 @@ This has e2e tests.
|
|||||||
For the `asExternalUri` changes, you'll need to test manually by:
|
For the `asExternalUri` changes, you'll need to test manually by:
|
||||||
1. running code-server with the test extension
|
1. running code-server with the test extension
|
||||||
2. Command Palette > code-server: asExternalUri test
|
2. Command Palette > code-server: asExternalUri test
|
||||||
3. input a url like http://localhost:3000
|
3. input a url like http://localhost:3000/my/path?token=abc#section
|
||||||
4. it should show a notification and show output as <code-server>/proxy/3000
|
4. it should show a notification and show output as
|
||||||
|
<code-server>/proxy/3000/my/path?token%3Dabc#section
|
||||||
|
|
||||||
Do the same thing but set `VSCODE_PROXY_URI: "https://{{port}}-main-workspace-name-user-name.coder.com"`
|
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.
|
and the output should replace `{{port}}` with port used in input url.
|
||||||
|
|
||||||
|
The rewritten URI must preserve the original path, query, and fragment (see
|
||||||
|
#7668). Append the original path to the proxy path using URI components so
|
||||||
|
encoded characters are not decoded or encoded twice. This also works with
|
||||||
|
proxy templates that do not have a trailing slash.
|
||||||
|
|
||||||
This also enables the forwared ports view panel by default.
|
This also enables the forwared ports view panel by default.
|
||||||
|
|
||||||
Lastly, it adds a tunnelProvider so that ports are forwarded using code-server's
|
Lastly, it adds a tunnelProvider so that ports are forwarded using code-server's
|
||||||
@@ -104,7 +110,7 @@ Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts
|
|||||||
import type { IURLCallbackProvider } from '../../../workbench/services/url/browser/urlService.js';
|
import type { IURLCallbackProvider } from '../../../workbench/services/url/browser/urlService.js';
|
||||||
import { create } from '../../../workbench/workbench.web.main.internal.js';
|
import { create } from '../../../workbench/workbench.web.main.internal.js';
|
||||||
|
|
||||||
@@ -612,6 +613,39 @@ class WorkspaceProvider implements IWork
|
@@ -612,6 +613,44 @@ class WorkspaceProvider implements IWork
|
||||||
settingsSyncOptions: config.settingsSyncOptions ? { enabled: config.settingsSyncOptions.enabled, } : undefined,
|
settingsSyncOptions: config.settingsSyncOptions ? { enabled: config.settingsSyncOptions.enabled, } : undefined,
|
||||||
workspaceProvider: WorkspaceProvider.create(config),
|
workspaceProvider: WorkspaceProvider.create(config),
|
||||||
urlCallbackProvider: new LocalStorageURLCallbackProvider(config.callbackRoute),
|
urlCallbackProvider: new LocalStorageURLCallbackProvider(config.callbackRoute),
|
||||||
@@ -116,7 +122,12 @@ Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts
|
|||||||
+ const renderedTemplate = config.productConfiguration.proxyEndpointTemplate
|
+ const renderedTemplate = config.productConfiguration.proxyEndpointTemplate
|
||||||
+ .replace('{{port}}', localhostMatch.port.toString())
|
+ .replace('{{port}}', localhostMatch.port.toString())
|
||||||
+ .replace('{{host}}', window.location.host)
|
+ .replace('{{host}}', window.location.host)
|
||||||
+ resolvedUri = URI.parse(new URL(renderedTemplate, window.location.href).toString())
|
+ const proxyUri = URI.parse(new URL(renderedTemplate, window.location.href).toString())
|
||||||
|
+ resolvedUri = proxyUri.with({
|
||||||
|
+ path: proxyUri.path.replace(/\/$/, '') + uri.path,
|
||||||
|
+ query: uri.query,
|
||||||
|
+ fragment: uri.fragment,
|
||||||
|
+ })
|
||||||
+ } else {
|
+ } 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.`)
|
||||||
+ }
|
+ }
|
||||||
|
|||||||
@@ -15,6 +15,56 @@ function runTestExtensionTests() {
|
|||||||
const normalizedAddress = address.replace(/\/+$/, "")
|
const normalizedAddress = address.replace(/\/+$/, "")
|
||||||
await expect(codeServerPage.page.getByText(`Info: proxyUri: ${normalizedAddress}/proxy/{{port}}/`)).toBeVisible()
|
await expect(codeServerPage.page.getByText(`Info: proxyUri: ${normalizedAddress}/proxy/{{port}}/`)).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
runExternalUriTests()
|
||||||
|
}
|
||||||
|
|
||||||
|
function runExternalUriTests(proxyEndpointTemplate?: string) {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
name: "path",
|
||||||
|
input: "http://127.0.0.1:1234/my/path",
|
||||||
|
suffix: "/my/path",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "query and fragment",
|
||||||
|
input: "http://localhost:1234/my/path?token=abc&mode=preview#section",
|
||||||
|
suffix: "/my/path?token%3Dabc%26mode%3Dpreview#section",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "encoded components",
|
||||||
|
input: "http://0.0.0.0:1234/my%20path/%23file?token=a%20b#my%20section",
|
||||||
|
suffix: "/my%20path/%23file?token%3Da%20b#my%20section",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "root path",
|
||||||
|
input: "http://127.0.0.1:1234/",
|
||||||
|
suffix: "/",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const { name, input, suffix } of cases) {
|
||||||
|
test(`asExternalUri should preserve ${name}`, async ({ codeServerPage }) => {
|
||||||
|
const address = await getMaybeProxiedCodeServer(codeServerPage)
|
||||||
|
const normalizedAddress = address.replace(/\/+$/, "")
|
||||||
|
const proxyBase = proxyEndpointTemplate
|
||||||
|
? new URL(proxyEndpointTemplate.replace("{{port}}", "1234"), `${normalizedAddress}/`).toString()
|
||||||
|
: `${normalizedAddress}/proxy/1234/`
|
||||||
|
|
||||||
|
await codeServerPage.waitForTestExtensionLoaded()
|
||||||
|
await codeServerPage.executeCommandViaMenus("code-server: asExternalUri test")
|
||||||
|
|
||||||
|
const inputBox = codeServerPage.page.locator(".quick-input-widget input")
|
||||||
|
await inputBox.fill(input)
|
||||||
|
await inputBox.press("Enter")
|
||||||
|
|
||||||
|
// The test extension displays URI.toString(), which also encodes query delimiters.
|
||||||
|
const output = `${proxyBase.replace(/\/$/, "")}${suffix}`
|
||||||
|
await expect(
|
||||||
|
codeServerPage.page.getByText(`Info: input: ${input} output: ${output}`, { exact: true }),
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const flags = ["--disable-workspace-trust", "--extensions-dir", path.join(__dirname, "./extensions")]
|
const flags = ["--disable-workspace-trust", "--extensions-dir", path.join(__dirname, "./extensions")]
|
||||||
@@ -23,6 +73,17 @@ describe("Extensions", flags, {}, () => {
|
|||||||
runTestExtensionTests()
|
runTestExtensionTests()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for (const proxyEndpointTemplate of ["./proxy/{{port}}", "https://{{port}}-workspace.example.com"]) {
|
||||||
|
describe(
|
||||||
|
`Extensions with VSCODE_PROXY_URI=${proxyEndpointTemplate}`,
|
||||||
|
flags,
|
||||||
|
{ VSCODE_PROXY_URI: proxyEndpointTemplate },
|
||||||
|
() => {
|
||||||
|
runExternalUriTests(proxyEndpointTemplate)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.USE_PROXY !== "1") {
|
if (process.env.USE_PROXY !== "1") {
|
||||||
describe("Extensions with --cert", [...flags, "--cert"], {}, () => {
|
describe("Extensions with --cert", [...flags, "--cert"], {}, () => {
|
||||||
runTestExtensionTests()
|
runTestExtensionTests()
|
||||||
|
|||||||
Reference in New Issue
Block a user