Skip to content

Commit 5aded14

Browse files
authored
Merge pull request #1453 from cdr/proxy
HTTP proxy
2 parents 3b39482 + a288351 commit 5aded14

File tree

14 files changed

+373
-47
lines changed

14 files changed

+373
-47
lines changed

doc/FAQ.md

+29
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,35 @@ only to HTTP requests.
6565
You can use [Let's Encrypt](https://letsencrypt.org/) to get an SSL certificate
6666
for free.
6767

68+
## How do I securely access web services?
69+
70+
code-server is capable of proxying to any port using either a subdomain or a
71+
subpath which means you can securely access these services using code-server's
72+
built-in authentication.
73+
74+
### Sub-domains
75+
76+
You will need a DNS entry that points to your server for each port you want to
77+
access. You can either set up a wildcard DNS entry for `*.<domain>` if your domain
78+
name registrar supports it or you can create one for every port you want to
79+
access (`3000.<domain>`, `8080.<domain>`, etc).
80+
81+
You should also set up TLS certificates for these subdomains, either using a
82+
wildcard certificate for `*.<domain>` or individual certificates for each port.
83+
84+
Start code-server with the `--proxy-domain` flag set to your domain.
85+
86+
```
87+
code-server --proxy-domain <domain>
88+
```
89+
90+
Now you can browse to `<port>.<domain>`. Note that this uses the host header so
91+
ensure your reverse proxy forwards that information if you are using one.
92+
93+
### Sub-paths
94+
95+
Just browse to `/proxy/<port>/`.
96+
6897
## x86 releases?
6998

7099
node has dropped support for x86 and so we decided to as well. See

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"devDependencies": {
1818
"@types/adm-zip": "^0.4.32",
1919
"@types/fs-extra": "^8.0.1",
20+
"@types/http-proxy": "^1.17.4",
2021
"@types/mocha": "^5.2.7",
2122
"@types/node": "^12.12.7",
2223
"@types/parcel-bundler": "^1.12.1",
@@ -52,13 +53,14 @@
5253
"@coder/logger": "1.1.11",
5354
"adm-zip": "^0.4.14",
5455
"fs-extra": "^8.1.0",
56+
"http-proxy": "^1.18.0",
5557
"httpolyglot": "^0.1.2",
5658
"node-pty": "^0.9.0",
5759
"pem": "^1.14.2",
5860
"safe-compare": "^1.1.4",
5961
"semver": "^7.1.3",
60-
"tar": "^6.0.1",
6162
"ssh2": "^0.8.7",
63+
"tar": "^6.0.1",
6264
"tar-fs": "^2.0.0",
6365
"ws": "^7.2.0"
6466
}

src/browser/pages/home.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
1818
crossorigin="use-credentials"
1919
/>
20-
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.pnggg" />
20+
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
2121
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
2222
<meta id="coder-options" data-settings="{{OPTIONS}}" />
2323
</head>

src/node/app/api.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class ApiHttpProvider extends HttpProvider {
4343

4444
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
4545
this.ensureAuthenticated(request)
46-
if (route.requestPath !== "/index.html") {
46+
if (!this.isRoot(route)) {
4747
throw new HttpError("Not found", HttpCode.NotFound)
4848
}
4949

src/node/app/dashboard.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class DashboardHttpProvider extends HttpProvider {
2020
}
2121

2222
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
23-
if (route.requestPath !== "/index.html") {
23+
if (!this.isRoot(route)) {
2424
throw new HttpError("Not found", HttpCode.NotFound)
2525
}
2626

src/node/app/login.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ interface LoginPayload {
1818
*/
1919
export class LoginHttpProvider extends HttpProvider {
2020
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
21-
if (this.options.auth !== AuthType.Password || route.requestPath !== "/index.html") {
21+
if (this.options.auth !== AuthType.Password || !this.isRoot(route)) {
2222
throw new HttpError("Not found", HttpCode.NotFound)
2323
}
2424
switch (route.base) {

src/node/app/proxy.ts

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import * as http from "http"
2+
import { HttpCode, HttpError } from "../../common/http"
3+
import { HttpProvider, HttpResponse, Route, WsResponse } from "../http"
4+
5+
/**
6+
* Proxy HTTP provider.
7+
*/
8+
export class ProxyHttpProvider extends HttpProvider {
9+
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
10+
if (!this.authenticated(request)) {
11+
if (this.isRoot(route)) {
12+
return { redirect: "/login", query: { to: route.fullPath } }
13+
}
14+
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
15+
}
16+
17+
// Ensure there is a trailing slash so relative paths work correctly.
18+
if (this.isRoot(route) && !route.fullPath.endsWith("/")) {
19+
return {
20+
redirect: `${route.fullPath}/`,
21+
}
22+
}
23+
24+
const port = route.base.replace(/^\//, "")
25+
return {
26+
proxy: {
27+
base: `${this.options.base}/${port}`,
28+
port,
29+
},
30+
}
31+
}
32+
33+
public async handleWebSocket(route: Route, request: http.IncomingMessage): Promise<WsResponse> {
34+
this.ensureAuthenticated(request)
35+
const port = route.base.replace(/^\//, "")
36+
return {
37+
proxy: {
38+
base: `${this.options.base}/${port}`,
39+
port,
40+
},
41+
}
42+
}
43+
}

src/node/app/update.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export class UpdateHttpProvider extends HttpProvider {
6161
this.ensureAuthenticated(request)
6262
this.ensureMethod(request)
6363

64-
if (route.requestPath !== "/index.html") {
64+
if (!this.isRoot(route)) {
6565
throw new HttpError("Not found", HttpCode.NotFound)
6666
}
6767

src/node/app/vscode.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export class VscodeHttpProvider extends HttpProvider {
126126

127127
switch (route.base) {
128128
case "/":
129-
if (route.requestPath !== "/index.html") {
129+
if (!this.isRoot(route)) {
130130
throw new HttpError("Not found", HttpCode.NotFound)
131131
} else if (!this.authenticated(request)) {
132132
return { redirect: "/login", query: { to: this.options.base } }

src/node/cli.ts

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface Args extends VsArgs {
3939
readonly "install-extension"?: string[]
4040
readonly "show-versions"?: boolean
4141
readonly "uninstall-extension"?: string[]
42+
readonly "proxy-domain"?: string[]
4243
readonly locale?: string
4344
readonly _: string[]
4445
}
@@ -111,6 +112,7 @@ const options: Options<Required<Args>> = {
111112
"install-extension": { type: "string[]", description: "Install or update a VS Code extension by id or vsix." },
112113
"uninstall-extension": { type: "string[]", description: "Uninstall a VS Code extension by id." },
113114
"show-versions": { type: "boolean", description: "Show VS Code extension versions." },
115+
"proxy-domain": { type: "string[]", description: "Domain used for proxying ports." },
114116

115117
locale: { type: "string" },
116118
log: { type: LogLevel },

src/node/entry.ts

+31-26
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import { CliMessage } from "../../lib/vscode/src/vs/server/ipc"
55
import { ApiHttpProvider } from "./app/api"
66
import { DashboardHttpProvider } from "./app/dashboard"
77
import { LoginHttpProvider } from "./app/login"
8+
import { ProxyHttpProvider } from "./app/proxy"
89
import { StaticHttpProvider } from "./app/static"
910
import { UpdateHttpProvider } from "./app/update"
1011
import { VscodeHttpProvider } from "./app/vscode"
1112
import { Args, optionDescriptions, parse } from "./cli"
12-
import { AuthType, HttpServer } from "./http"
13+
import { AuthType, HttpServer, HttpServerOptions } from "./http"
1314
import { SshProvider } from "./ssh/server"
1415
import { generateCertificate, generatePassword, generateSshHostKey, hash, open } from "./util"
1516
import { ipcMain, wrap } from "./wrapper"
@@ -36,42 +37,31 @@ const main = async (args: Args): Promise<void> => {
3637
const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword()))
3738

3839
// Spawn the main HTTP server.
39-
const options = {
40+
const options: HttpServerOptions = {
4041
auth,
41-
cert: args.cert ? args.cert.value : undefined,
42-
certKey: args["cert-key"],
43-
sshHostKey: args["ssh-host-key"],
4442
commit,
4543
host: args.host || (args.auth === AuthType.Password && typeof args.cert !== "undefined" ? "0.0.0.0" : "localhost"),
4644
password: originalPassword ? hash(originalPassword) : undefined,
4745
port: typeof args.port !== "undefined" ? args.port : process.env.PORT ? parseInt(process.env.PORT, 10) : 8080,
46+
proxyDomains: args["proxy-domain"],
4847
socket: args.socket,
48+
...(args.cert && !args.cert.value
49+
? await generateCertificate()
50+
: {
51+
cert: args.cert && args.cert.value,
52+
certKey: args["cert-key"],
53+
}),
4954
}
5055

51-
if (!options.cert && args.cert) {
52-
const { cert, certKey } = await generateCertificate()
53-
options.cert = cert
54-
options.certKey = certKey
55-
} else if (args.cert && !args["cert-key"]) {
56+
if (options.cert && !options.certKey) {
5657
throw new Error("--cert-key is missing")
5758
}
5859

59-
if (!args["disable-ssh"]) {
60-
if (!options.sshHostKey && typeof options.sshHostKey !== "undefined") {
61-
throw new Error("--ssh-host-key cannot be blank")
62-
} else if (!options.sshHostKey) {
63-
try {
64-
options.sshHostKey = await generateSshHostKey()
65-
} catch (error) {
66-
logger.error("Unable to start SSH server", field("error", error.message))
67-
}
68-
}
69-
}
70-
7160
const httpServer = new HttpServer(options)
7261
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
7362
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
7463
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"])
64+
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
7565
httpServer.registerHttpProvider("/login", LoginHttpProvider)
7666
httpServer.registerHttpProvider("/static", StaticHttpProvider)
7767
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
@@ -84,7 +74,7 @@ const main = async (args: Args): Promise<void> => {
8474

8575
if (auth === AuthType.Password && !process.env.PASSWORD) {
8676
logger.info(` - Password is ${originalPassword}`)
87-
logger.info(" - To use your own password, set the PASSWORD environment variable")
77+
logger.info(" - To use your own password set the PASSWORD environment variable")
8878
if (!args.auth) {
8979
logger.info(" - To disable use `--auth none`")
9080
}
@@ -96,19 +86,33 @@ const main = async (args: Args): Promise<void> => {
9686

9787
if (httpServer.protocol === "https") {
9888
logger.info(
99-
typeof args.cert === "string"
89+
args.cert && args.cert.value
10090
? ` - Using provided certificate and key for HTTPS`
10191
: ` - Using generated certificate and key for HTTPS`,
10292
)
10393
} else {
10494
logger.info(" - Not serving HTTPS")
10595
}
10696

97+
if (httpServer.proxyDomains.size > 0) {
98+
logger.info(` - Proxying the following domain${httpServer.proxyDomains.size === 1 ? "" : "s"}:`)
99+
httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
100+
}
101+
107102
logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`)
108103

104+
let sshHostKey = args["ssh-host-key"]
105+
if (!args["disable-ssh"] && !sshHostKey) {
106+
try {
107+
sshHostKey = await generateSshHostKey()
108+
} catch (error) {
109+
logger.error("Unable to start SSH server", field("error", error.message))
110+
}
111+
}
112+
109113
let sshPort: number | undefined
110-
if (!args["disable-ssh"] && options.sshHostKey) {
111-
const sshProvider = httpServer.registerHttpProvider("/ssh", SshProvider, options.sshHostKey as string)
114+
if (!args["disable-ssh"] && sshHostKey) {
115+
const sshProvider = httpServer.registerHttpProvider("/ssh", SshProvider, sshHostKey)
112116
try {
113117
sshPort = await sshProvider.listen()
114118
} catch (error) {
@@ -118,6 +122,7 @@ const main = async (args: Args): Promise<void> => {
118122

119123
if (typeof sshPort !== "undefined") {
120124
logger.info(`SSH server listening on localhost:${sshPort}`)
125+
logger.info(" - To disable use `--disable-ssh`")
121126
} else {
122127
logger.info("SSH server disabled")
123128
}

0 commit comments

Comments
 (0)