Skip to content

Commit 6210d09

Browse files
committed
Refactor template string with handlebars.
- Fixed issue where JSON entities may be unescaped - Updated web manifest schema, route handler.
1 parent f2f1fee commit 6210d09

13 files changed

+221
-155
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
"devDependencies": {
3333
"@types/body-parser": "^1.19.0",
3434
"@types/cookie-parser": "^1.4.2",
35+
"@types/entities": "^1.1.1",
3536
"@types/express": "^4.17.8",
37+
"@types/express-handlebars": "^3.1.0",
3638
"@types/fs-extra": "^8.0.1",
3739
"@types/http-proxy": "^1.17.4",
3840
"@types/js-yaml": "^3.12.3",
@@ -71,8 +73,10 @@
7173
"@coder/logger": "1.1.16",
7274
"body-parser": "^1.19.0",
7375
"cookie-parser": "^1.4.5",
76+
"entities": "^2.1.0",
7477
"env-paths": "^2.2.0",
7578
"express": "^4.17.1",
79+
"express-handlebars": "^5.2.0",
7680
"fs-extra": "^9.0.1",
7781
"http-proxy": "^1.18.0",
7882
"httpolyglot": "^0.1.2",

src/browser/media/manifest.json

Lines changed: 0 additions & 40 deletions
This file was deleted.

src/browser/pages/error.html renamed to src/browser/pages/error.handlebars

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,20 @@
1010
http-equiv="Content-Security-Policy"
1111
content="style-src 'self'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
1212
/>
13-
<title>{{ERROR_TITLE}} - code-server</title>
13+
<title>{{ERROR_TITLE}} — Code Server</title>
1414
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
15-
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
15+
<link rel="manifest" href="/code-server.webmanifest" crossorigin="use-credentials" />
1616
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
1717
<link href="{{CS_STATIC_BASE}}/dist/register.css" rel="stylesheet" />
18-
<meta id="coder-options" data-settings="{{OPTIONS}}" />
18+
<meta id="coder-options" data-settings="{{{json coderOptions}}}" />
1919
</head>
2020
<body>
2121
<div class="center-container">
2222
<div class="error-display">
2323
<h2 class="header">{{ERROR_HEADER}}</h2>
2424
<div class="body">{{ERROR_BODY}}</div>
2525
<div class="links">
26-
<a class="link" href="{{BASE}}{{TO}}">go home</a>
26+
<a class="link" href="{{BASE}}{{TO}}">Go home</a>
2727
</div>
2828
</div>
2929
</div>

src/browser/pages/login.html renamed to src/browser/pages/login.handlebars

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,20 @@
1010
http-equiv="Content-Security-Policy"
1111
content="style-src 'self'; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
1212
/>
13-
<title>code-server login</title>
13+
<title>Login — Code Server</title>
1414
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
15-
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
15+
<link rel="manifest" href="/code-server.webmanifest" crossorigin="use-credentials" />
1616
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
1717
<link href="{{CS_STATIC_BASE}}/dist/register.css" rel="stylesheet" />
18-
<meta id="coder-options" data-settings="{{OPTIONS}}" />
18+
<meta id="coder-options" data-settings="{{{json coderOptions}}}" />
1919
</head>
2020
<body>
2121
<div class="center-container">
2222
<div class="card-box">
2323
<div class="header">
2424
<h1 class="main">Welcome to code-server</h1>
25-
<div class="sub">Please log in below. {{PASSWORD_MSG}}</div>
25+
<div class="sub">Please log in below.</div>
26+
<div class="sub">{{PASSWORD_MSG}}</div>
2627
</div>
2728
<div class="content">
2829
<form class="login-form" method="post">
@@ -40,7 +41,10 @@ <h1 class="main">Welcome to code-server</h1>
4041
/>
4142
<input class="submit -button" value="SUBMIT" type="submit" />
4243
</div>
43-
{{ERROR}}
44+
45+
{{#if ERROR}}
46+
<div class="error">{{ERROR}}</div>
47+
{{/if}}
4448
</form>
4549
</div>
4650
</div>

src/browser/pages/vscode.html renamed to src/browser/pages/vscode.handlebars

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
<html>
44
<head>
55
<script>
6-
globalThis.MonacoPerformanceMarks = globalThis.MonacoPerformanceMarks || []
7-
globalThis.MonacoPerformanceMarks.push("renderer/started", Date.now())
6+
globalThis.MonacoPerformanceMarks = globalThis.MonacoPerformanceMarks || [];
7+
globalThis.MonacoPerformanceMarks.push("renderer/started", Date.now());
88
</script>
99

1010
<meta charset="utf-8" />
11+
<title>Code Server</title>
1112

1213
<!-- Disable pinch zooming -->
1314
<meta
@@ -16,28 +17,30 @@
1617
/>
1718

1819
<!-- Workbench Configuration -->
19-
<meta id="vscode-workbench-web-configuration" data-settings="{{WORKBENCH_WEB_CONFIGURATION}}" />
20+
<meta id="vscode-workbench-web-configuration" data-settings="{{{json workbenchOptions.workbenchWebConfiguration}}}" />
2021

2122
<!-- Workarounds/Hacks (remote user data uri) -->
22-
<meta id="vscode-remote-user-data-uri" data-settings="{{REMOTE_USER_DATA_URI}}" />
23-
<meta id="vscode-remote-product-configuration" data-settings="{{PRODUCT_CONFIGURATION}}" />
24-
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}" />
23+
<meta id="vscode-remote-user-data-uri" data-settings="{{{json workbenchOptions.remoteUserDataUri}}}" />
24+
<meta id="vscode-remote-product-configuration" data-settings="{{{json workbenchOptions.productConfiguration}}}" />
25+
<meta id="vscode-remote-nls-configuration" data-settings="{{{json workbenchOptions.nlsConfiguration}}}" />
2526

2627
<!-- Workbench Icon/Manifest/CSS -->
2728
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
28-
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
29-
<!-- PROD_ONLY
30-
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="{{CS_STATIC_BASE}}/lib/vscode/out/vs/workbench/workbench.web.api.css">
31-
END_PROD_ONLY -->
29+
<link rel="manifest" href="/code-server.webmanifest" crossorigin="use-credentials" />
30+
31+
{{#if (prod)}}
32+
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="{{CS_STATIC_BASE}}/lib/vscode/out/vs/workbench/workbench.web.api.css">
33+
{{/if}}
34+
3235
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
3336
<meta name="apple-mobile-web-app-capable" content="yes" />
3437

3538
<!-- Prefetch to avoid waterfall -->
36-
<!-- PROD_ONLY
37-
<link rel="prefetch" href="{{CS_STATIC_BASE}}/lib/vscode/node_modules/semver-umd/lib/semver-umd.js">
38-
END_PROD_ONLY -->
39+
{{#if (prod)}}
40+
<link rel="prefetch" href="{{CS_STATIC_BASE}}/lib/vscode/node_modules/semver-umd/lib/semver-umd.js">
41+
{{/if}}
3942

40-
<meta id="coder-options" data-settings="{{OPTIONS}}" />
43+
<meta id="coder-options" data-settings="{{{json coderOptions}}}" />
4144
</head>
4245

4346
<body aria-label=""></body>
@@ -46,14 +49,17 @@
4649
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/pages/vscode.js"></script>
4750
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/register.js"></script>
4851
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/lib/vscode/out/vs/loader.js"></script>
52+
4953
<script>
50-
globalThis.MonacoPerformanceMarks.push("willLoadWorkbenchMain", Date.now())
54+
globalThis.MonacoPerformanceMarks.push("willLoadWorkbenchMain", Date.now());
5155
</script>
52-
<!-- PROD_ONLY
53-
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/lib/vscode/out/vs/workbench/workbench.web.api.nls.js"></script>
54-
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/lib/vscode/out/vs/workbench/workbench.web.api.js"></script>
55-
END_PROD_ONLY -->
56+
57+
{{#if (prod)}}
58+
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/lib/vscode/out/vs/workbench/workbench.web.api.nls.js"></script>
59+
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/lib/vscode/out/vs/workbench/workbench.web.api.js"></script>
60+
{{/if}}
61+
5662
<script>
57-
require(["vs/code/browser/workbench/workbench"], function () {})
63+
require(["vs/code/browser/workbench/workbench"], function () {});
5864
</script>
5965
</html>

src/node/app.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,40 @@
11
import { logger } from "@coder/logger"
2+
import { encodeXML } from "entities"
23
import express, { Express } from "express"
4+
import exphbs from "express-handlebars"
35
import { promises as fs } from "fs"
46
import http from "http"
57
import * as httpolyglot from "httpolyglot"
8+
import { resolve } from "path"
69
import { DefaultedArgs } from "./cli"
10+
import { rootPath } from "./constants"
711
import { handleUpgrade } from "./http"
812

913
/**
1014
* Create an Express app and an HTTP/S server to serve it.
1115
*/
1216
export const createApp = async (args: DefaultedArgs): Promise<[Express, http.Server]> => {
17+
const prod = process.env.NODE_ENV === "production"
1318
const app = express()
19+
app.set("json spaces", prod ? 0 : 2)
20+
21+
app.engine(
22+
"handlebars",
23+
exphbs({
24+
helpers: {
25+
prod: () => prod,
26+
/**
27+
* Converts to JSON string and encodes entities for use in HTML.
28+
* @TODO we can likely move JSON attributes to <script type="text/json">
29+
* and reduce the escaped output.
30+
*/
31+
json: (content: any): string => encodeXML(JSON.stringify(content)),
32+
},
33+
}),
34+
)
35+
36+
app.set("views", resolve(rootPath, "src/browser/pages"))
37+
app.set("view engine", "handlebars")
1438

1539
const server = args.cert
1640
? httpolyglot.createServer(

src/node/http.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,33 @@ export interface Locals {
1616
heart: Heart
1717
}
1818

19-
/**
20-
* Replace common variable strings in HTML templates.
21-
*/
22-
export const replaceTemplates = <T extends object>(
19+
export interface CommonTemplateVars {
20+
layout: boolean
21+
TO: string
22+
BASE: string
23+
CS_STATIC_BASE: string
24+
coderOptions: Options
25+
}
26+
27+
export const commonTemplateVars = <T extends Options>(
2328
req: express.Request,
24-
content: string,
2529
extraOpts?: Omit<T, "base" | "csStaticBase" | "logLevel">,
26-
): string => {
30+
): CommonTemplateVars => {
2731
const base = relativeRoot(req)
28-
const options: Options = {
32+
const coderOptions: Options = {
2933
base,
3034
csStaticBase: base + "/static/" + commit + rootPath,
3135
logLevel: logger.level,
3236
...extraOpts,
3337
}
34-
return content
35-
.replace(/{{TO}}/g, (typeof req.query.to === "string" && req.query.to) || "/")
36-
.replace(/{{BASE}}/g, options.base)
37-
.replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase)
38-
.replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`)
38+
39+
return {
40+
TO: (typeof req.query.to === "string" && req.query.to) || "/",
41+
BASE: coderOptions.base,
42+
CS_STATIC_BASE: coderOptions.csStaticBase,
43+
coderOptions,
44+
layout: false,
45+
}
3946
}
4047

4148
/**

src/node/routes/index.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ import { plural } from "../../common/util"
1111
import { AuthType, DefaultedArgs } from "../cli"
1212
import { rootPath } from "../constants"
1313
import { Heart } from "../heart"
14-
import { replaceTemplates } from "../http"
14+
import { commonTemplateVars } from "../http"
1515
import { loadPlugins } from "../plugin"
1616
import * as domainProxy from "../proxy"
1717
import { getMediaMime, paths } from "../util"
1818
import * as health from "./health"
1919
import * as login from "./login"
20+
import * as manifest from "./manifest"
2021
import * as proxy from "./proxy"
2122
// static is a reserved keyword.
2223
import * as _static from "./static"
@@ -85,6 +86,7 @@ export const register = async (app: Express, server: http.Server, args: Defaulte
8586

8687
app.use("/", domainProxy.router)
8788
app.use("/", vscode.router)
89+
app.use("/", manifest.router)
8890
app.use("/healthz", health.router)
8991
if (args.auth === AuthType.Password) {
9092
app.use("/login", login.router)
@@ -101,20 +103,19 @@ export const register = async (app: Express, server: http.Server, args: Defaulte
101103
})
102104

103105
const errorHandler: ErrorRequestHandler = async (err, req, res, next) => {
104-
const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html")
105-
res.set("Content-Type", getMediaMime(resourcePath))
106+
if (err.code === "ENOENT" || err.code === "EISDIR") {
107+
err.status = HttpCode.NotFound
108+
}
109+
110+
const status = err.status ?? err.statusCode ?? 500
111+
106112
try {
107-
const content = await fs.readFile(resourcePath, "utf8")
108-
if (err.code === "ENOENT" || err.code === "EISDIR") {
109-
err.status = HttpCode.NotFound
110-
}
111-
const status = err.status ?? err.statusCode ?? 500
112-
res.status(status).send(
113-
replaceTemplates(req, content)
114-
.replace(/{{ERROR_TITLE}}/g, status)
115-
.replace(/{{ERROR_HEADER}}/g, status)
116-
.replace(/{{ERROR_BODY}}/g, err.message),
117-
)
113+
res.status(status).render("error", {
114+
...commonTemplateVars(req),
115+
ERROR_TITLE: status,
116+
ERROR_HEADER: status,
117+
ERROR_BODY: err.message,
118+
})
118119
} catch (error) {
119120
next(error)
120121
}

0 commit comments

Comments
 (0)