Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 28e98c0

Browse files
authored
Merge pull request #2563 from cdr/proxy-path-passthrough-0bb9
pathProxy.ts: Implement --proxy-path-passthrough
2 parents c17f3ff + c32d8b1 commit 28e98c0

File tree

13 files changed

+277
-95
lines changed

13 files changed

+277
-95
lines changed

‎doc/FAQ.md‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- [How do I securely access web services?](#how-do-i-securely-access-web-services)
1616
- [Sub-paths](#sub-paths)
1717
- [Sub-domains](#sub-domains)
18+
- [Why does the code-server proxy strip `/proxy/<port>` from the request path?](#why-does-the-code-server-proxy-strip-proxyport-from-the-request-path)
1819
- [Multi-tenancy](#multi-tenancy)
1920
- [Docker in code-server container?](#docker-in-code-server-container)
2021
- [How can I disable telemetry?](#how-can-i-disable-telemetry)
@@ -208,6 +209,42 @@ code-server --proxy-domain <domain>
208209
Now you can browse to `<port>.<domain>`. Note that this uses the host header so
209210
ensure your reverse proxy forwards that information if you are using one.
210211
212+
## Why does the code-server proxy strip `/proxy/<port>` from the request path?
213+
214+
HTTP servers should strive to use relative URLs to avoid needed to be coupled to the
215+
absolute path at which they are served. This means you must use trailing slashes on all
216+
paths with subpaths. See https://blog.cdivilly.com/2019/02/28/uri-trailing-slashes
217+
218+
This is really the "correct" way things work and why the striping of the base path is the
219+
default. If your application uses relative URLs and does not assume the absolute path at
220+
which it is being served, it will just work no matter what port you decide to serve it off
221+
or if you put it in behind code-server or any other proxy!
222+
223+
However many people prefer the cleaner aesthetic of no trailing slashes. This couples you
224+
to the base path as you cannot use relative redirects correctly anymore. See the above
225+
link.
226+
227+
For users who are ok with this tradeoff, pass `--proxy-path-passthrough` to code-server
228+
and the path will be passed as is.
229+
230+
This is particularly a problem with the `start` script in create-react-app. See
231+
[#2222](https://github.com/cdr/code-server/issues/2222). You will need to inform
232+
create-react-app of the path at which you are serving via `homepage` field in your
233+
`package.json`. e.g. you'd add the following for the default CRA port:
234+
235+
```json
236+
"homepage": "/proxy/3000",
237+
```
238+
239+
Then visit `https://my-code-server-address.io/proxy/3000` to see your app exposed through
240+
code-server!
241+
242+
Unfortunately `webpack-dev-server`'s websocket connections will not go through as it
243+
always uses `/sockjs-node`. So hot reloading will not work until the `create-react-app`
244+
team fix this bug.
245+
246+
Highly recommend using the subdomain approach instead to avoid this class of issue.
247+
211248
## Multi-tenancy
212249

213250
If you want to run multiple code-servers on shared infrastructure, we recommend using virtual

‎package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@
3838
"@types/js-yaml": "^3.12.3",
3939
"@types/mocha": "^8.0.3",
4040
"@types/node": "^12.12.7",
41+
"@types/node-fetch": "^2.5.7",
4142
"@types/parcel-bundler": "^1.12.1",
4243
"@types/pem": "^1.9.5",
4344
"@types/proxy-from-env": "^1.0.1",
4445
"@types/safe-compare": "^1.1.0",
4546
"@types/semver": "^7.1.0",
4647
"@types/split2": "^2.1.6",
47-
"@types/supertest": "^2.0.10",
4848
"@types/tar-fs": "^2.0.0",
4949
"@types/tar-stream": "^2.1.0",
5050
"@types/ws": "^7.2.6",
@@ -61,7 +61,6 @@
6161
"prettier": "^2.0.5",
6262
"stylelint": "^13.0.0",
6363
"stylelint-config-recommended": "^3.0.0",
64-
"supertest": "^6.0.1",
6564
"ts-node": "^9.0.0",
6665
"typescript": "4.0.2"
6766
},
@@ -81,6 +80,7 @@
8180
"httpolyglot": "^0.1.2",
8281
"js-yaml": "^3.13.1",
8382
"limiter": "^1.1.5",
83+
"node-fetch": "^2.6.1",
8484
"pem": "^1.14.2",
8585
"proxy-agent": "^4.0.0",
8686
"proxy-from-env": "^1.1.0",

‎src/common/util.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,11 @@ export const getFirstString = (value: string | string[] | object | undefined): s
112112

113113
return typeof value === "string" ? value : undefined
114114
}
115+
116+
export function logError(prefix: string, err: any): void {
117+
if (err instanceof Error) {
118+
logger.error(`${prefix}: ${err.message} ${err.stack}`)
119+
} else {
120+
logger.error(`${prefix}: ${err}`)
121+
}
122+
}

‎src/node/app.ts‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import express, { Express } from "express"
33
import { promises as fs } from "fs"
44
import http from "http"
55
import * as httpolyglot from "httpolyglot"
6+
import * as util from "../common/util"
67
import { DefaultedArgs } from "./cli"
78
import { handleUpgrade } from "./wsRouter"
89

@@ -22,8 +23,21 @@ export const createApp = async (args: DefaultedArgs): Promise<[Express, Express,
2223
)
2324
: http.createServer(app)
2425

25-
await new Promise<http.Server>(async (resolve, reject) => {
26-
server.on("error", reject)
26+
let resolved = false
27+
await new Promise<http.Server>(async (resolve2, reject) => {
28+
const resolve = () => {
29+
resolved = true
30+
resolve2()
31+
}
32+
server.on("error", (err) => {
33+
if (!resolved) {
34+
reject(err)
35+
} else {
36+
// Promise resolved earlier so this is an unrelated error.
37+
util.logError("http server error", err)
38+
}
39+
})
40+
2741
if (args.socket) {
2842
try {
2943
await fs.unlink(args.socket)

‎src/node/cli.ts‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export interface Args extends VsArgs {
5050
"show-versions"?: boolean
5151
"uninstall-extension"?: string[]
5252
"proxy-domain"?: string[]
53+
"proxy-path-passthrough"?: boolean
5354
locale?: string
5455
_: string[]
5556
"reuse-window"?: boolean
@@ -172,6 +173,10 @@ const options: Options<Required<Args>> = {
172173
"uninstall-extension": { type: "string[]", description: "Uninstall a VS Code extension by id." },
173174
"show-versions": { type: "boolean", description: "Show VS Code extension versions." },
174175
"proxy-domain": { type: "string[]", description: "Domain used for proxying ports." },
176+
"proxy-path-passthrough": {
177+
type: "boolean",
178+
description: "Whether the path proxy should leave the /proxy/<port> in the request path when proxying.",
179+
},
175180
"ignore-last-opened": {
176181
type: "boolean",
177182
short: "e",
@@ -239,7 +244,7 @@ export const optionDescriptions = (): string[] => {
239244
export const parse = (
240245
argv: string[],
241246
opts?: {
242-
configFile: string
247+
configFile?: string
243248
},
244249
): Args => {
245250
const error = (msg: string): Error => {
@@ -516,7 +521,19 @@ export async function readConfigFile(configPath?: string): Promise<ConfigArgs> {
516521
}
517522

518523
const configFile = await fs.readFile(configPath)
519-
const config = yaml.safeLoad(configFile.toString(), {
524+
return parseConfigFile(configFile.toString(), configPath)
525+
}
526+
527+
/**
528+
* parseConfigFile parses configFile into ConfigArgs.
529+
* configPath is used as the filename in error messages
530+
*/
531+
export function parseConfigFile(configFile: string, configPath: string): ConfigArgs {
532+
if (!configFile) {
533+
return { _: [], config: configPath }
534+
}
535+
536+
const config = yaml.safeLoad(configFile, {
520537
filename: configPath,
521538
})
522539
if (!config || typeof config === "string") {

‎src/node/heart.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,13 @@ export class Heart {
4545
})
4646
}, this.heartbeatInterval)
4747
}
48+
49+
/**
50+
* Call to clear any heartbeatTimer for shutdown.
51+
*/
52+
public dispose(): void {
53+
if (typeof this.heartbeatTimer !== "undefined") {
54+
clearTimeout(this.heartbeatTimer)
55+
}
56+
}
4857
}

‎src/node/routes/index.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ export const register = async (
5555
})
5656
})
5757
})
58+
server.on("close", () => {
59+
heart.dispose()
60+
})
5861

5962
app.disable("x-powered-by")
6063
wsApp.disable("x-powered-by")
@@ -165,7 +168,7 @@ export const register = async (
165168

166169
app.use(errorHandler)
167170

168-
const wsErrorHandler: express.ErrorRequestHandler = async (err, req) => {
171+
const wsErrorHandler: express.ErrorRequestHandler = async (err, req,res,next) => {
169172
logger.error(`${err.message} ${err.stack}`)
170173
;(req as WebsocketRequest).ws.end()
171174
}

‎src/node/routes/pathProxy.ts‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import { Router as WsRouter } from "../wsRouter"
88

99
export const router = Router()
1010

11-
const getProxyTarget = (req: Request, rewrite: boolean): string => {
12-
if (rewrite) {
13-
const query = qs.stringify(req.query)
14-
return `http://0.0.0.0:${req.params.port}/${req.params[0] || ""}${query ? `?${query}` : ""}`
11+
const getProxyTarget = (req: Request, passthroughPath: boolean): string => {
12+
if (passthroughPath) {
13+
return `http://0.0.0.0:${req.params.port}/${req.originalUrl}`
1514
}
16-
return `http://0.0.0.0:${req.params.port}/${req.originalUrl}`
15+
const query = qs.stringify(req.query)
16+
return `http://0.0.0.0:${req.params.port}/${req.params[0] || ""}${query ? `?${query}` : ""}`
1717
}
1818

1919
router.all("/(:port)(/*)?", (req, res) => {
@@ -33,7 +33,7 @@ router.all("/(:port)(/*)?", (req, res) => {
3333

3434
proxy.web(req, res, {
3535
ignorePath: true,
36-
target: getProxyTarget(req, true),
36+
target: getProxyTarget(req, req.args["proxy-path-passthrough"]||false),
3737
})
3838
})
3939

@@ -42,6 +42,6 @@ export const wsRouter = WsRouter()
4242
wsRouter.ws("/(:port)(/*)?", ensureAuthenticated, (req) => {
4343
proxy.ws(req, req.ws, req.head, {
4444
ignorePath: true,
45-
target: getProxyTarget(req, true),
45+
target: getProxyTarget(req, req.args["proxy-path-passthrough"]||false),
4646
})
4747
})

‎test/httpserver.ts‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as http from "http"
2+
import * as nodeFetch from "node-fetch"
3+
import * as util from "../src/common/util"
4+
import { ensureAddress } from "../src/node/app"
5+
6+
// Perhaps an abstraction similar to this should be used in app.ts as well.
7+
export class HttpServer {
8+
private hs = http.createServer()
9+
10+
public constructor(hs?: http.Server) {
11+
// See usage in test/integration.ts
12+
if (hs) {
13+
this.hs = hs
14+
}
15+
}
16+
17+
/**
18+
* listen starts the server on a random localhost port.
19+
* Use close to cleanup when done.
20+
*/
21+
public listen(fn: http.RequestListener): Promise<void> {
22+
this.hs.on("request", fn)
23+
24+
let resolved = false
25+
return new Promise((res, rej) => {
26+
this.hs.listen(0, "localhost", () => {
27+
res()
28+
resolved = true
29+
})
30+
31+
this.hs.on("error", (err) => {
32+
if (!resolved) {
33+
rej(err)
34+
} else {
35+
// Promise resolved earlier so this is some other error.
36+
util.logError("http server error", err)
37+
}
38+
})
39+
})
40+
}
41+
42+
/**
43+
* close cleans up the server.
44+
*/
45+
public close(): Promise<void> {
46+
return new Promise((res, rej) => {
47+
this.hs.close((err) => {
48+
if (err) {
49+
rej(err)
50+
return
51+
}
52+
res()
53+
})
54+
})
55+
}
56+
57+
/**
58+
* fetch fetches the request path.
59+
* The request path must be rooted!
60+
*/
61+
public fetch(requestPath: string, opts?: nodeFetch.RequestInit): Promise<nodeFetch.Response> {
62+
return nodeFetch.default(`${ensureAddress(this.hs)}${requestPath}`, opts)
63+
}
64+
65+
public port(): number {
66+
const addr = this.hs.address()
67+
if (addr && typeof addr === "object") {
68+
return addr.port
69+
}
70+
throw new Error("server not listening or listening on unix socket")
71+
}
72+
}

‎test/integration.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as express from "express"
2+
import { createApp } from "../src/node/app"
3+
import { parse, setDefaults, parseConfigFile, DefaultedArgs } from "../src/node/cli"
4+
import { register } from "../src/node/routes"
5+
import * as httpserver from "./httpserver"
6+
7+
export async function setup(
8+
argv: string[],
9+
configFile?: string,
10+
): Promise<[express.Application, express.Application, httpserver.HttpServer, DefaultedArgs]> {
11+
argv = ["--bind-addr=localhost:0", ...argv]
12+
13+
const cliArgs = parse(argv)
14+
const configArgs = parseConfigFile(configFile || "", "test/integration.ts")
15+
const args = await setDefaults(cliArgs, configArgs)
16+
17+
const [app, wsApp, server] = await createApp(args)
18+
await register(app, wsApp, server, args)
19+
20+
return [app, wsApp, new httpserver.HttpServer(server), args]
21+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /