Skip to content

Commit

Permalink
Merge pull request #1453 from cdr/proxy
Browse files Browse the repository at this point in the history
HTTP proxy
  • Loading branch information
code-asher authored Apr 8, 2020
2 parents 3b39482 + a288351 commit 5aded14
Show file tree
Hide file tree
Showing 14 changed files with 373 additions and 47 deletions.
29 changes: 29 additions & 0 deletions doc/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,35 @@ only to HTTP requests.
You can use [Let's Encrypt](https://letsencrypt.org/) to get an SSL certificate
for free.

## How do I securely access web services?

code-server is capable of proxying to any port using either a subdomain or a
subpath which means you can securely access these services using code-server's
built-in authentication.

### Sub-domains

You will need a DNS entry that points to your server for each port you want to
access. You can either set up a wildcard DNS entry for `*.<domain>` if your domain
name registrar supports it or you can create one for every port you want to
access (`3000.<domain>`, `8080.<domain>`, etc).

You should also set up TLS certificates for these subdomains, either using a
wildcard certificate for `*.<domain>` or individual certificates for each port.

Start code-server with the `--proxy-domain` flag set to your domain.

```
code-server --proxy-domain <domain>
```

Now you can browse to `<port>.<domain>`. Note that this uses the host header so
ensure your reverse proxy forwards that information if you are using one.

### Sub-paths

Just browse to `/proxy/<port>/`.

## x86 releases?

node has dropped support for x86 and so we decided to as well. See
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"devDependencies": {
"@types/adm-zip": "^0.4.32",
"@types/fs-extra": "^8.0.1",
"@types/http-proxy": "^1.17.4",
"@types/mocha": "^5.2.7",
"@types/node": "^12.12.7",
"@types/parcel-bundler": "^1.12.1",
Expand Down Expand Up @@ -52,13 +53,14 @@
"@coder/logger": "1.1.11",
"adm-zip": "^0.4.14",
"fs-extra": "^8.1.0",
"http-proxy": "^1.18.0",
"httpolyglot": "^0.1.2",
"node-pty": "^0.9.0",
"pem": "^1.14.2",
"safe-compare": "^1.1.4",
"semver": "^7.1.3",
"tar": "^6.0.1",
"ssh2": "^0.8.7",
"tar": "^6.0.1",
"tar-fs": "^2.0.0",
"ws": "^7.2.0"
}
Expand Down
2 changes: 1 addition & 1 deletion src/browser/pages/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
crossorigin="use-credentials"
/>
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.pnggg" />
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
<meta id="coder-options" data-settings="{{OPTIONS}}" />
</head>
Expand Down
2 changes: 1 addition & 1 deletion src/node/app/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class ApiHttpProvider extends HttpProvider {

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

Expand Down
2 changes: 1 addition & 1 deletion src/node/app/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class DashboardHttpProvider extends HttpProvider {
}

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

Expand Down
2 changes: 1 addition & 1 deletion src/node/app/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface LoginPayload {
*/
export class LoginHttpProvider extends HttpProvider {
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
if (this.options.auth !== AuthType.Password || route.requestPath !== "/index.html") {
if (this.options.auth !== AuthType.Password || !this.isRoot(route)) {
throw new HttpError("Not found", HttpCode.NotFound)
}
switch (route.base) {
Expand Down
43 changes: 43 additions & 0 deletions src/node/app/proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as http from "http"
import { HttpCode, HttpError } from "../../common/http"
import { HttpProvider, HttpResponse, Route, WsResponse } from "../http"

/**
* Proxy HTTP provider.
*/
export class ProxyHttpProvider extends HttpProvider {
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
if (!this.authenticated(request)) {
if (this.isRoot(route)) {
return { redirect: "/login", query: { to: route.fullPath } }
}
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
}

// Ensure there is a trailing slash so relative paths work correctly.
if (this.isRoot(route) && !route.fullPath.endsWith("/")) {
return {
redirect: `${route.fullPath}/`,
}
}

const port = route.base.replace(/^\//, "")
return {
proxy: {
base: `${this.options.base}/${port}`,
port,
},
}
}

public async handleWebSocket(route: Route, request: http.IncomingMessage): Promise<WsResponse> {
this.ensureAuthenticated(request)
const port = route.base.replace(/^\//, "")
return {
proxy: {
base: `${this.options.base}/${port}`,
port,
},
}
}
}
2 changes: 1 addition & 1 deletion src/node/app/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class UpdateHttpProvider extends HttpProvider {
this.ensureAuthenticated(request)
this.ensureMethod(request)

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

Expand Down
2 changes: 1 addition & 1 deletion src/node/app/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export class VscodeHttpProvider extends HttpProvider {

switch (route.base) {
case "/":
if (route.requestPath !== "/index.html") {
if (!this.isRoot(route)) {
throw new HttpError("Not found", HttpCode.NotFound)
} else if (!this.authenticated(request)) {
return { redirect: "/login", query: { to: this.options.base } }
Expand Down
2 changes: 2 additions & 0 deletions src/node/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface Args extends VsArgs {
readonly "install-extension"?: string[]
readonly "show-versions"?: boolean
readonly "uninstall-extension"?: string[]
readonly "proxy-domain"?: string[]
readonly locale?: string
readonly _: string[]
}
Expand Down Expand Up @@ -111,6 +112,7 @@ const options: Options<Required<Args>> = {
"install-extension": { type: "string[]", description: "Install or update a VS Code extension by id or vsix." },
"uninstall-extension": { type: "string[]", description: "Uninstall a VS Code extension by id." },
"show-versions": { type: "boolean", description: "Show VS Code extension versions." },
"proxy-domain": { type: "string[]", description: "Domain used for proxying ports." },

locale: { type: "string" },
log: { type: LogLevel },
Expand Down
57 changes: 31 additions & 26 deletions src/node/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import { CliMessage } from "../../lib/vscode/src/vs/server/ipc"
import { ApiHttpProvider } from "./app/api"
import { DashboardHttpProvider } from "./app/dashboard"
import { LoginHttpProvider } from "./app/login"
import { ProxyHttpProvider } from "./app/proxy"
import { StaticHttpProvider } from "./app/static"
import { UpdateHttpProvider } from "./app/update"
import { VscodeHttpProvider } from "./app/vscode"
import { Args, optionDescriptions, parse } from "./cli"
import { AuthType, HttpServer } from "./http"
import { AuthType, HttpServer, HttpServerOptions } from "./http"
import { SshProvider } from "./ssh/server"
import { generateCertificate, generatePassword, generateSshHostKey, hash, open } from "./util"
import { ipcMain, wrap } from "./wrapper"
Expand All @@ -36,42 +37,31 @@ const main = async (args: Args): Promise<void> => {
const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword()))

// Spawn the main HTTP server.
const options = {
const options: HttpServerOptions = {
auth,
cert: args.cert ? args.cert.value : undefined,
certKey: args["cert-key"],
sshHostKey: args["ssh-host-key"],
commit,
host: args.host || (args.auth === AuthType.Password && typeof args.cert !== "undefined" ? "0.0.0.0" : "localhost"),
password: originalPassword ? hash(originalPassword) : undefined,
port: typeof args.port !== "undefined" ? args.port : process.env.PORT ? parseInt(process.env.PORT, 10) : 8080,
proxyDomains: args["proxy-domain"],
socket: args.socket,
...(args.cert && !args.cert.value
? await generateCertificate()
: {
cert: args.cert && args.cert.value,
certKey: args["cert-key"],
}),
}

if (!options.cert && args.cert) {
const { cert, certKey } = await generateCertificate()
options.cert = cert
options.certKey = certKey
} else if (args.cert && !args["cert-key"]) {
if (options.cert && !options.certKey) {
throw new Error("--cert-key is missing")
}

if (!args["disable-ssh"]) {
if (!options.sshHostKey && typeof options.sshHostKey !== "undefined") {
throw new Error("--ssh-host-key cannot be blank")
} else if (!options.sshHostKey) {
try {
options.sshHostKey = await generateSshHostKey()
} catch (error) {
logger.error("Unable to start SSH server", field("error", error.message))
}
}
}

const httpServer = new HttpServer(options)
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"])
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
httpServer.registerHttpProvider("/login", LoginHttpProvider)
httpServer.registerHttpProvider("/static", StaticHttpProvider)
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
Expand All @@ -84,7 +74,7 @@ const main = async (args: Args): Promise<void> => {

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

if (httpServer.protocol === "https") {
logger.info(
typeof args.cert === "string"
args.cert && args.cert.value
? ` - Using provided certificate and key for HTTPS`
: ` - Using generated certificate and key for HTTPS`,
)
} else {
logger.info(" - Not serving HTTPS")
}

if (httpServer.proxyDomains.size > 0) {
logger.info(` - Proxying the following domain${httpServer.proxyDomains.size === 1 ? "" : "s"}:`)
httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
}

logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`)

let sshHostKey = args["ssh-host-key"]
if (!args["disable-ssh"] && !sshHostKey) {
try {
sshHostKey = await generateSshHostKey()
} catch (error) {
logger.error("Unable to start SSH server", field("error", error.message))
}
}

let sshPort: number | undefined
if (!args["disable-ssh"] && options.sshHostKey) {
const sshProvider = httpServer.registerHttpProvider("/ssh", SshProvider, options.sshHostKey as string)
if (!args["disable-ssh"] && sshHostKey) {
const sshProvider = httpServer.registerHttpProvider("/ssh", SshProvider, sshHostKey)
try {
sshPort = await sshProvider.listen()
} catch (error) {
Expand All @@ -118,6 +122,7 @@ const main = async (args: Args): Promise<void> => {

if (typeof sshPort !== "undefined") {
logger.info(`SSH server listening on localhost:${sshPort}`)
logger.info(" - To disable use `--disable-ssh`")
} else {
logger.info("SSH server disabled")
}
Expand Down
Loading

0 comments on commit 5aded14

Please sign in to comment.