Skip to content

Commit 4bd512e

Browse files
authored
Merge branch 'main' into patch-2
2 parents 1bc9e0d + 4b4ec37 commit 4bd512e

File tree

14 files changed

+91
-153
lines changed

14 files changed

+91
-153
lines changed

ci/dev/watch.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { spawn, fork, ChildProcess } from "child_process"
2-
import del from "del"
32
import { promises as fs } from "fs"
43
import * as path from "path"
54
import { CompilationStats, onLine, OnLineCallback } from "../../src/node/util"
@@ -57,8 +56,6 @@ class Watcher {
5756
process.on(event, () => this.dispose(0))
5857
}
5958

60-
this.cleanFiles()
61-
6259
for (const [processName, devProcess] of Object.entries(this.compilers)) {
6360
if (!devProcess) continue
6461

@@ -121,15 +118,6 @@ class Watcher {
121118

122119
//#region Utilities
123120

124-
/**
125-
* Cleans files from previous builds.
126-
*/
127-
private cleanFiles(): Promise<string[]> {
128-
console.log("[Watcher]", "Cleaning files from previous builds...")
129-
130-
return del(["out/**/*"])
131-
}
132-
133121
/**
134122
* Emits a file containing compilation data.
135123
* This is especially useful when Express needs to determine if VS Code is still compiling.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@
5252
"@typescript-eslint/parser": "^5.0.0",
5353
"audit-ci": "^5.0.0",
5454
"codecov": "^3.8.3",
55-
"del": "^6.0.0",
5655
"doctoc": "^2.0.0",
5756
"eslint": "^7.7.0",
5857
"eslint-config-prettier": "^8.1.0",

src/browser/pages/login.html

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ <h1 class="main">Welcome to code-server</h1>
3030
<div class="content">
3131
<form class="login-form" method="post">
3232
<input class="user" type="text" autocomplete="username" />
33-
<input id="base" type="hidden" name="base" value="/" />
33+
<input id="base" type="hidden" name="base" value="{{BASE}}" />
34+
<input id="href" type="hidden" name="href" value="" />
3435
<div class="field">
3536
<input
3637
required
@@ -51,9 +52,9 @@ <h1 class="main">Welcome to code-server</h1>
5152
<script>
5253
// Inform the backend about the path since the proxy might have rewritten
5354
// it out of the headers and cookies must be set with absolute paths.
54-
const el = document.getElementById("base")
55+
const el = document.getElementById("href")
5556
if (el) {
56-
el.value = window.location.pathname
57+
el.value = location.href
5758
}
5859
</script>
5960
</body>

src/common/util.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ export const generateUuid = (length = 24): string => {
2323

2424
/**
2525
* Remove extra slashes in a URL.
26+
*
27+
* This is meant to fill the job of `path.join` so you can concatenate paths and
28+
* then normalize out any extra slashes.
29+
*
30+
* If you are using `path.join` you do not need this but note that `path` is for
31+
* file system paths, not URLs.
2632
*/
2733
export const normalize = (url: string, keepTrailing = false): string => {
2834
return url.replace(/\/\/+/g, "/").replace(/\/+$/, keepTrailing ? "/" : "")
@@ -35,21 +41,6 @@ export const trimSlashes = (url: string): string => {
3541
return url.replace(/^\/+|\/+$/g, "")
3642
}
3743

38-
/**
39-
* Resolve a relative base against the window location. This is used for
40-
* anything that doesn't work with a relative path.
41-
*/
42-
export const resolveBase = (base?: string): string => {
43-
// After resolving the base will either start with / or be an empty string.
44-
if (!base || base.startsWith("/")) {
45-
return base ?? ""
46-
}
47-
const parts = location.pathname.split("/")
48-
parts[parts.length - 1] = base
49-
const url = new URL(location.origin + "/" + parts.join("/"))
50-
return normalize(url.pathname)
51-
}
52-
5344
/**
5445
* Wrap the value in an array if it's not already an array. If the value is
5546
* undefined return an empty array.

src/node/http.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import * as express from "express"
33
import * as expressCore from "express-serve-static-core"
44
import * as http from "http"
55
import * as net from "net"
6-
import path from "path"
76
import * as qs from "qs"
87
import { Disposable } from "../common/emitter"
98
import { CookieKeys, HttpCode, HttpError } from "../common/http"
@@ -18,7 +17,9 @@ import { getPasswordMethod, IsCookieValidArgs, isCookieValid, sanitizeString, es
1817
*/
1918
export interface ClientConfiguration {
2019
codeServerVersion: string
20+
/** Relative path from this page to the root. No trailing slash. */
2121
base: string
22+
/** Relative path from this page to the static root. No trailing slash. */
2223
csStaticBase: string
2324
}
2425

@@ -33,11 +34,11 @@ declare global {
3334
}
3435

3536
export const createClientConfiguration = (req: express.Request): ClientConfiguration => {
36-
const base = relativeRoot(req)
37+
const base = relativeRoot(req.originalUrl)
3738

3839
return {
3940
base,
40-
csStaticBase: normalize(path.posix.join(base, "_static/")),
41+
csStaticBase: base + "/_static",
4142
codeServerVersion,
4243
}
4344
}
@@ -108,15 +109,28 @@ export const authenticated = async (req: express.Request): Promise<boolean> => {
108109

109110
/**
110111
* Get the relative path that will get us to the root of the page. For each
111-
* slash we need to go up a directory. For example:
112+
* slash we need to go up a directory. Will not have a trailing slash.
113+
*
114+
* For example:
115+
*
112116
* / => .
113117
* /foo => .
114118
* /foo/ => ./..
115119
* /foo/bar => ./..
116120
* /foo/bar/ => ./../..
121+
*
122+
* All paths must be relative in order to work behind a reverse proxy since we
123+
* we do not know the base path. Anything that needs to be absolute (for
124+
* example cookies) must get the base path from the frontend.
125+
*
126+
* All relative paths must be prefixed with the relative root to ensure they
127+
* work no matter the depth at which they happen to appear.
128+
*
129+
* For Express `req.originalUrl` should be used as they remove the base from the
130+
* standard `url` property making it impossible to get the true depth.
117131
*/
118-
export const relativeRoot = (req: express.Request): string => {
119-
const depth = (req.originalUrl.split("?", 1)[0].match(/\//g) || []).length
132+
export const relativeRoot = (originalUrl: string): string => {
133+
const depth = (originalUrl.split("?", 1)[0].match(/\//g) || []).length
120134
return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : ""))
121135
}
122136

@@ -138,7 +152,7 @@ export const redirect = (
138152
}
139153
})
140154

141-
const relativePath = normalize(`${relativeRoot(req)}/${to}`, true)
155+
const relativePath = normalize(`${relativeRoot(req.originalUrl)}/${to}`, true)
142156
const queryString = qs.stringify(query)
143157
const redirectPath = `${relativePath}${queryString ? `?${queryString}` : ""}`
144158
logger.debug(`redirecting from ${req.originalUrl} to ${redirectPath}`)
@@ -241,3 +255,32 @@ export function disposer(server: http.Server): Disposable["dispose"] {
241255
})
242256
}
243257
}
258+
259+
/**
260+
* Get the options for setting a cookie. The options must be identical for
261+
* setting and unsetting cookies otherwise they are considered separate.
262+
*/
263+
export const getCookieOptions = (req: express.Request): express.CookieOptions => {
264+
// Normally we set paths relatively. However browsers do not appear to allow
265+
// cookies to be set relatively which means we need an absolute path. We
266+
// cannot be guaranteed we know the path since a reverse proxy might have
267+
// rewritten it. That means we need to get the path from the frontend.
268+
269+
// The reason we need to set the path (as opposed to defaulting to /) is to
270+
// avoid code-server instances on different sub-paths clobbering each other or
271+
// from accessing each other's tokens (and to prevent other services from
272+
// accessing code-server's tokens).
273+
274+
// When logging in or out the request must include the href (the full current
275+
// URL of that page) and the relative path to the root as given to it by the
276+
// backend. Using these two we can determine the true absolute root.
277+
const url = new URL(
278+
req.query.base || req.body.base || "/",
279+
req.query.href || req.body.href || "http://" + (req.headers.host || "localhost"),
280+
)
281+
return {
282+
domain: getCookieDomain(url.host, req.args["proxy-domain"]),
283+
path: normalize(url.pathname) || "/",
284+
sameSite: "lax",
285+
}
286+
}

src/node/routes/login.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as os from "os"
55
import * as path from "path"
66
import { CookieKeys } from "../../common/http"
77
import { rootPath } from "../constants"
8-
import { authenticated, getCookieDomain, redirect, replaceTemplates } from "../http"
8+
import { authenticated, getCookieOptions, redirect, replaceTemplates } from "../http"
99
import { getPasswordMethod, handlePasswordValidation, humanPath, sanitizeString, escapeHtml } from "../util"
1010

1111
// RateLimiter wraps around the limiter library for logins.
@@ -84,15 +84,7 @@ router.post<{}, string, { password: string; base?: string }, { to?: string }>("/
8484
if (isPasswordValid) {
8585
// The hash does not add any actual security but we do it for
8686
// obfuscation purposes (and as a side effect it handles escaping).
87-
res.cookie(CookieKeys.Session, hashedPassword, {
88-
domain: getCookieDomain(req.headers.host || "", req.args["proxy-domain"]),
89-
// Browsers do not appear to allow cookies to be set relatively so we
90-
// need to get the root path from the browser since the proxy rewrites
91-
// it out of the path. Otherwise code-server instances hosted on
92-
// separate sub-paths will clobber each other.
93-
path: req.body.base ? path.posix.join(req.body.base, "..", "/") : "/",
94-
sameSite: "lax",
95-
})
87+
res.cookie(CookieKeys.Session, hashedPassword, getCookieOptions(req))
9688

9789
const to = (typeof req.query.to === "string" && req.query.to) || "/"
9890
return redirect(req, res, to, { to: undefined })

src/node/routes/logout.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,14 @@
11
import { Router } from "express"
22
import { CookieKeys } from "../../common/http"
3-
import { getCookieDomain, redirect } from "../http"
4-
3+
import { getCookieOptions, redirect } from "../http"
54
import { sanitizeString } from "../util"
65

76
export const router = Router()
87

98
router.get<{}, undefined, undefined, { base?: string; to?: string }>("/", async (req, res) => {
10-
const path = sanitizeString(req.query.base) || "/"
11-
const to = sanitizeString(req.query.to) || "/"
12-
139
// Must use the *identical* properties used to set the cookie.
14-
res.clearCookie(CookieKeys.Session, {
15-
domain: getCookieDomain(req.headers.host || "", req.args["proxy-domain"]),
16-
path: decodeURIComponent(path),
17-
sameSite: "lax",
18-
})
10+
res.clearCookie(CookieKeys.Session, getCookieOptions(req))
1911

20-
return redirect(req, res, to, { to: undefined, base: undefined })
12+
const to = sanitizeString(req.query.to) || "/"
13+
return redirect(req, res, to, { to: undefined, base: undefined, href: undefined })
2114
})

src/node/routes/vscode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export class CodeServerRouteWrapper {
2424
const isAuthenticated = await authenticated(req)
2525

2626
if (!isAuthenticated) {
27-
return redirect(req, res, "login/", {
27+
return redirect(req, res, "login", {
2828
// req.baseUrl can be blank if already at the root.
2929
to: req.baseUrl && req.baseUrl !== "/" ? req.baseUrl : undefined,
3030
})

src/node/util.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ export async function isCookieValid({
324324
export function sanitizeString(str: unknown): string {
325325
// Very basic sanitization of string
326326
// Credit: https://stackoverflow.com/a/46719000/3015595
327-
return typeof str === "string" && str.trim().length > 0 ? str.trim() : ""
327+
return typeof str === "string" ? str.trim() : ""
328328
}
329329

330330
const mimeTypes: { [key: string]: string } = {

test/unit/common/util.test.ts

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -74,42 +74,6 @@ describe("util", () => {
7474
})
7575
})
7676

77-
describe("resolveBase", () => {
78-
beforeEach(() => {
79-
const location: LocationLike = {
80-
pathname: "/healthz",
81-
origin: "http://localhost:8080",
82-
}
83-
84-
// Because resolveBase is not a pure function
85-
// and relies on the global location to be set
86-
// we set it before all the tests
87-
// and tell TS that our location should be looked at
88-
// as Location (even though it's missing some properties)
89-
global.location = location as Location
90-
})
91-
92-
it("should resolve a base", () => {
93-
expect(util.resolveBase("localhost:8080")).toBe("/localhost:8080")
94-
})
95-
96-
it("should resolve a base with a forward slash at the beginning", () => {
97-
expect(util.resolveBase("/localhost:8080")).toBe("/localhost:8080")
98-
})
99-
100-
it("should resolve a base with query params", () => {
101-
expect(util.resolveBase("localhost:8080?folder=hello-world")).toBe("/localhost:8080")
102-
})
103-
104-
it("should resolve a base with a path", () => {
105-
expect(util.resolveBase("localhost:8080/hello/world")).toBe("/localhost:8080/hello/world")
106-
})
107-
108-
it("should resolve a base to an empty string when not provided", () => {
109-
expect(util.resolveBase()).toBe("")
110-
})
111-
})
112-
11377
describe("arrayify", () => {
11478
it("should return value it's already an array", () => {
11579
expect(util.arrayify(["hello", "world"])).toStrictEqual(["hello", "world"])

test/unit/node/http.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { relativeRoot } from "../../../src/node/http"
2+
3+
describe("http", () => {
4+
it("should construct a relative path to the root", () => {
5+
expect(relativeRoot("/")).toStrictEqual(".")
6+
expect(relativeRoot("/foo")).toStrictEqual(".")
7+
expect(relativeRoot("/foo/")).toStrictEqual("./..")
8+
expect(relativeRoot("/foo/bar ")).toStrictEqual("./..")
9+
expect(relativeRoot("/foo/bar/")).toStrictEqual("./../..")
10+
})
11+
})

vendor/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
"postinstall": "./postinstall.sh"
88
},
99
"devDependencies": {
10-
"code-oss-dev": "cdr/vscode#c2a251c6afaa13fbebf97fcd8a68192f8cf46031"
10+
"code-oss-dev": "cdr/vscode#478224aa345e9541f2427b30142dd13ee7e14d39"
1111
}
1212
}

vendor/yarn.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,9 +296,9 @@ clone-response@^1.0.2:
296296
dependencies:
297297
mimic-response "^1.0.0"
298298

299-
code-oss-dev@cdr/vscode#c2a251c6afaa13fbebf97fcd8a68192f8cf46031:
299+
code-oss-dev@cdr/vscode#478224aa345e9541f2427b30142dd13ee7e14d39:
300300
version "1.61.1"
301-
resolved "https://codeload.github.com/cdr/vscode/tar.gz/c2a251c6afaa13fbebf97fcd8a68192f8cf46031"
301+
resolved "https://codeload.github.com/cdr/vscode/tar.gz/478224aa345e9541f2427b30142dd13ee7e14d39"
302302
dependencies:
303303
"@microsoft/applicationinsights-web" "^2.6.4"
304304
"@vscode/sqlite3" "4.0.12"

0 commit comments

Comments
 (0)