Skip to content

HTTP proxy #1453

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Apr 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -128,7 +128,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") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have a SSH server?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this has something to do with the Chrome extension that was planned but I'm not 100% sure on the status of that. @kylecarbs @wbobeirne

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this was for the browser extension. That's pretty much on ice at this point, so I think it's probably best for safety / code cleanliness that we take it out since nothing is using it right now, and it's probably a liability to have such a powerful thing up by default. Or at the very least, disable by default.

Would be happy to make a PR to remove that if we're definitely not going to pursue the extension, but I'll defer to mr @kylecarbs on that.

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