Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28e91ba70c | ||
|
|
5aded14b87 | ||
|
|
a288351ad4 | ||
|
|
3b39482420 | ||
|
|
a5c35af81b | ||
|
|
b78bdaf46e | ||
|
|
aefef5b0e8 | ||
|
|
ca998240a0 | ||
|
|
d2a31477c7 | ||
|
|
9c6581273e | ||
|
|
d1445a8135 | ||
|
|
5fc00acc39 | ||
|
|
363cdd02df | ||
|
|
a5d1d3b90e | ||
|
|
aaa6c279a1 | ||
|
|
498becd11f | ||
|
|
411c61fb02 | ||
|
|
74a0bacdcf | ||
|
|
e7e7b0ffb7 | ||
|
|
fd339a7433 | ||
|
|
561b6343c8 | ||
|
|
e68d72c4d6 | ||
|
|
737a8f5965 | ||
|
|
c0dd29c591 | ||
|
|
8aa5675ba2 | ||
|
|
2086648c87 | ||
|
|
3a98d856a5 | ||
|
|
90fd1f7dd1 | ||
|
|
77ad73d579 | ||
|
|
13534fa0c0 | ||
|
|
37299abcc9 | ||
|
|
e480f6527e | ||
|
|
26584f2060 | ||
|
|
a4c0fd1fdc | ||
|
|
6c104c016e | ||
|
|
599670136d | ||
|
|
ce637d318d | ||
|
|
d8654b5a19 | ||
|
|
12c3ccd6c7 | ||
|
|
7954656610 |
@@ -15,7 +15,7 @@ RUN yum update -y && yum install -y \
|
||||
RUN mkdir /usr/share/node && cd /usr/share/node \
|
||||
&& curl "https://nodejs.org/dist/v12.14.0/node-v12.14.0-linux-$(uname -m | sed 's/86_//; s/aarch/arm/').tar.xz" | tar xJ --strip-components=1 --
|
||||
ENV PATH "$PATH:/usr/share/node/bin"
|
||||
RUN npm install -g yarn
|
||||
RUN npm install -g yarn@1.22.4
|
||||
|
||||
RUN curl -L "https://github.com/mvdan/sh/releases/download/v3.0.1/shfmt_v3.0.1_linux_$(uname -m | sed 's/x86_/amd/; s/aarch64/arm/')" > /usr/local/bin/shfmt \
|
||||
&& chmod +x /usr/local/bin/shfmt
|
||||
|
||||
107
ci/vscode.patch
107
ci/vscode.patch
@@ -157,7 +157,7 @@ index 2d8b725ff2..a8d93a17ca 100644
|
||||
unique-stream@^2.0.2:
|
||||
version "2.2.1"
|
||||
diff --git a/package.json b/package.json
|
||||
index 6e9b9dc0a0..49b14e536a 100644
|
||||
index 29d3cb6677..d3788cb1ab 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -33,6 +33,9 @@
|
||||
@@ -170,6 +170,19 @@ index 6e9b9dc0a0..49b14e536a 100644
|
||||
"applicationinsights": "1.0.8",
|
||||
"chokidar": "3.2.3",
|
||||
"graceful-fs": "4.2.3",
|
||||
diff --git a/product.json b/product.json
|
||||
index 759d765533..e1c33a008a 100644
|
||||
--- a/product.json
|
||||
+++ b/product.json
|
||||
@@ -18,7 +18,7 @@
|
||||
"darwinBundleIdentifier": "com.visualstudio.code.oss",
|
||||
"linuxIconName": "com.visualstudio.code.oss",
|
||||
"licenseFileName": "LICENSE.txt",
|
||||
- "reportIssueUrl": "https://github.com/Microsoft/vscode/issues/new",
|
||||
+ "reportIssueUrl": "https://github.com/cdr/code-server/issues/new",
|
||||
"urlProtocol": "code-oss",
|
||||
"extensionAllowedProposedApi": [
|
||||
"ms-vscode.references-view"
|
||||
diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts
|
||||
index a68e020f9f..c31e7befa3 100644
|
||||
--- a/src/vs/base/common/network.ts
|
||||
@@ -248,16 +261,32 @@ index 2c64061da7..c0ef8faedd 100644
|
||||
// Do nothing. If we can't read the file we have no
|
||||
// language pack config.
|
||||
diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts
|
||||
index 45f6f17ce0..79fde0b92c 100644
|
||||
index 45f6f17ce0..546b4c24de 100644
|
||||
--- a/src/vs/code/browser/workbench/workbench.ts
|
||||
+++ b/src/vs/code/browser/workbench/workbench.ts
|
||||
@@ -16,6 +16,7 @@ import product from 'vs/platform/product/common/product';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { posix } from 'vs/base/common/path';
|
||||
import { localize } from 'vs/nls';
|
||||
+import { encodePath } from 'vs/server/node/util';
|
||||
|
||||
interface ICredential {
|
||||
service: string;
|
||||
@@ -237,7 +238,6 @@ class WorkspaceProvider implements IWorkspaceProvider {
|
||||
}
|
||||
|
||||
private createTargetUrl(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): string | undefined {
|
||||
-
|
||||
// Empty
|
||||
let targetHref: string | undefined = undefined;
|
||||
if (!workspace) {
|
||||
@@ -246,12 +246,18 @@ class WorkspaceProvider implements IWorkspaceProvider {
|
||||
|
||||
// Folder
|
||||
else if (isFolderToOpen(workspace)) {
|
||||
- targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${encodeURIComponent(workspace.folderUri.toString())}`;
|
||||
+ const target = workspace.folderUri.scheme === Schemas.vscodeRemote
|
||||
+ ? encodeURIComponent(workspace.folderUri.path).replace(/%2F/g, "/")
|
||||
+ ? encodePath(workspace.folderUri.path)
|
||||
+ : encodeURIComponent(workspace.folderUri.toString());
|
||||
+ targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${target}`;
|
||||
}
|
||||
@@ -266,13 +295,32 @@ index 45f6f17ce0..79fde0b92c 100644
|
||||
else if (isWorkspaceToOpen(workspace)) {
|
||||
- targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${encodeURIComponent(workspace.workspaceUri.toString())}`;
|
||||
+ const target = workspace.workspaceUri.scheme === Schemas.vscodeRemote
|
||||
+ ? encodeURIComponent(workspace.workspaceUri.path).replace(/%2F/g, "/")
|
||||
+ ? encodePath(workspace.workspaceUri.path)
|
||||
+ : encodeURIComponent(workspace.workspaceUri.toString());
|
||||
+ targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${target}`;
|
||||
}
|
||||
|
||||
// Append payload if any
|
||||
@@ -302,35 +308,6 @@ class WorkspaceProvider implements IWorkspaceProvider {
|
||||
@@ -290,6 +296,18 @@ class WorkspaceProvider implements IWorkspaceProvider {
|
||||
|
||||
const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents } = JSON.parse(configElementAttribute);
|
||||
|
||||
+ // Strip the protocol from the authority if it exists.
|
||||
+ const normalizeAuthority = (authority: string): string => authority.replace(/^https?:\/\//, "");
|
||||
+ if (config.remoteAuthority) {
|
||||
+ (config as any).remoteAuthority = normalizeAuthority(config.remoteAuthority);
|
||||
+ }
|
||||
+ if (config.workspaceUri && config.workspaceUri.authority) {
|
||||
+ config.workspaceUri.authority = normalizeAuthority(config.workspaceUri.authority);
|
||||
+ }
|
||||
+ if (config.folderUri && config.folderUri.authority) {
|
||||
+ config.folderUri.authority = normalizeAuthority(config.folderUri.authority);
|
||||
+ }
|
||||
+
|
||||
// Revive static extension locations
|
||||
if (Array.isArray(config.staticExtensions)) {
|
||||
config.staticExtensions.forEach(extension => {
|
||||
@@ -302,35 +320,6 @@ class WorkspaceProvider implements IWorkspaceProvider {
|
||||
let workspace: IWorkspace;
|
||||
let payload = Object.create(null);
|
||||
|
||||
@@ -493,10 +541,10 @@ index eab8591492..26668701f7 100644
|
||||
options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`);
|
||||
diff --git a/src/vs/server/browser/client.ts b/src/vs/server/browser/client.ts
|
||||
new file mode 100644
|
||||
index 0000000000..4042e32f74
|
||||
index 0000000000..4f8543d975
|
||||
--- /dev/null
|
||||
+++ b/src/vs/server/browser/client.ts
|
||||
@@ -0,0 +1,263 @@
|
||||
@@ -0,0 +1,266 @@
|
||||
+import { Emitter } from 'vs/base/common/event';
|
||||
+import { URI } from 'vs/base/common/uri';
|
||||
+import { localize } from 'vs/nls';
|
||||
@@ -515,6 +563,7 @@ index 0000000000..4042e32f74
|
||||
+import { LocalizationsService } from 'vs/workbench/services/localizations/electron-browser/localizationsService';
|
||||
+import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
+import { Options } from 'vs/server/ipc.d';
|
||||
+import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
+
|
||||
+class TelemetryService extends TelemetryChannelClient {
|
||||
+ public constructor(
|
||||
@@ -672,14 +721,10 @@ index 0000000000..4042e32f74
|
||||
+ headers: { "content-type": "application/json" },
|
||||
+ });
|
||||
+ if (response.status !== 200) {
|
||||
+ throw new Error("Unexpected response");
|
||||
+ throw new Error(response.statusText);
|
||||
+ }
|
||||
+
|
||||
+ const json = await response.json();
|
||||
+ if (!json.isLatest) {
|
||||
+ throw new Error("Update failed");
|
||||
+ }
|
||||
+
|
||||
+ (services.get(INotificationService) as INotificationService).info(`Updated to ${json.version}`);
|
||||
+ };
|
||||
+
|
||||
@@ -725,6 +770,12 @@ index 0000000000..4042e32f74
|
||||
+ };
|
||||
+
|
||||
+ updateLoop();
|
||||
+
|
||||
+ // This will be used to set the background color while VS Code loads.
|
||||
+ const theme = (services.get(IStorageService) as IStorageService).get("colorThemeData", StorageScope.GLOBAL);
|
||||
+ if (theme) {
|
||||
+ localStorage.setItem("colorThemeData", theme);
|
||||
+ }
|
||||
+};
|
||||
+
|
||||
+export interface Query {
|
||||
@@ -2311,10 +2362,10 @@ index 0000000000..3c74512192
|
||||
+}
|
||||
diff --git a/src/vs/server/node/server.ts b/src/vs/server/node/server.ts
|
||||
new file mode 100644
|
||||
index 0000000000..d6dcfe1fe7
|
||||
index 0000000000..52311bf756
|
||||
--- /dev/null
|
||||
+++ b/src/vs/server/node/server.ts
|
||||
@@ -0,0 +1,257 @@
|
||||
@@ -0,0 +1,269 @@
|
||||
+import * as net from 'net';
|
||||
+import * as path from 'path';
|
||||
+import { Emitter } from 'vs/base/common/event';
|
||||
@@ -2394,10 +2445,22 @@ index 0000000000..d6dcfe1fe7
|
||||
+ await this.servicesPromise;
|
||||
+ const environment = this.services.get(IEnvironmentService) as IEnvironmentService;
|
||||
+ const startPath = options.startPath;
|
||||
+ const parseUrl = (url: string): URI => {
|
||||
+ // This might be a fully-specified URL or just a path.
|
||||
+ try {
|
||||
+ return URI.parse(url, true);
|
||||
+ } catch (error) {
|
||||
+ return URI.from({
|
||||
+ scheme: Schemas.vscodeRemote,
|
||||
+ authority: options.remoteAuthority,
|
||||
+ path: url,
|
||||
+ });
|
||||
+ }
|
||||
+ };
|
||||
+ return {
|
||||
+ workbenchWebConfiguration: {
|
||||
+ workspaceUri: startPath && startPath.workspace ? URI.parse(startPath.url) : undefined,
|
||||
+ folderUri: startPath && !startPath.workspace ? URI.parse(startPath.url) : undefined,
|
||||
+ workspaceUri: startPath && startPath.workspace ? parseUrl(startPath.url) : undefined,
|
||||
+ folderUri: startPath && !startPath.workspace ? parseUrl(startPath.url) : undefined,
|
||||
+ remoteAuthority: options.remoteAuthority,
|
||||
+ logLevel: getLogLevel(environment),
|
||||
+ },
|
||||
@@ -2604,10 +2667,10 @@ index 0000000000..fc69441cf0
|
||||
+};
|
||||
diff --git a/src/vs/server/node/util.ts b/src/vs/server/node/util.ts
|
||||
new file mode 100644
|
||||
index 0000000000..06b080044c
|
||||
index 0000000000..dd7fdf7b58
|
||||
--- /dev/null
|
||||
+++ b/src/vs/server/node/util.ts
|
||||
@@ -0,0 +1,9 @@
|
||||
@@ -0,0 +1,17 @@
|
||||
+import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
+import { URITransformer, IRawURITransformer } from 'vs/base/common/uriIpc';
|
||||
+
|
||||
@@ -2617,6 +2680,14 @@ index 0000000000..06b080044c
|
||||
+ const rawURITransformer = <IRawURITransformer>rawURITransformerFactory(remoteAuthority);
|
||||
+ return new URITransformer(rawURITransformer);
|
||||
+};
|
||||
+
|
||||
+/**
|
||||
+ * Encode a path for opening via the folder or workspace query parameter. This
|
||||
+ * preserves slashes so it can be edited by hand more easily.
|
||||
+ */
|
||||
+export const encodePath = (path: string): string => {
|
||||
+ return path.split("/").map((p) => encodeURIComponent(p)).join("/");
|
||||
+};
|
||||
diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts
|
||||
index e69aa80159..71a899d37b 100644
|
||||
--- a/src/vs/workbench/api/browser/extensionHost.contribution.ts
|
||||
|
||||
40
doc/FAQ.md
40
doc/FAQ.md
@@ -19,7 +19,7 @@ As a result, Coder has created its own marketplace for open source extensions. I
|
||||
GitHub for VS Code extensions and building them. It's not perfect but getting better by the day with
|
||||
more and more extensions.
|
||||
|
||||
Issue [https://github.com/cdr/code-server/issues/1299](#1299) is a big one in making the experience here
|
||||
Issue [#1299](https://github.com/cdr/code-server/issues/1299) is a big one in making the experience here
|
||||
better by allowing the community to submit extensions and repos to avoid waiting until the scraper finds
|
||||
an extension.
|
||||
|
||||
@@ -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
|
||||
@@ -104,6 +133,15 @@ demand and will work on it when the time is right.
|
||||
Use the `--disable-telemetry` flag to completely disable telemetry. We use the
|
||||
data collected only to improve code-server.
|
||||
|
||||
## How does code-server decide what workspace or folder to open?
|
||||
|
||||
code-server tries the following in order:
|
||||
|
||||
1. The `workspace` query parameter.
|
||||
2. The `folder` query parameter.
|
||||
3. The directory passed on the command line.
|
||||
4. The last opened workspace or folder.
|
||||
|
||||
## Enterprise
|
||||
|
||||
Visit [our enterprise page](https://coder.com) for more information about our
|
||||
|
||||
Submodule lib/vscode updated: 78a4c91400...0ba0ca5295
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "code-server",
|
||||
"license": "MIT",
|
||||
"version": "3.0.1",
|
||||
"version": "3.1.1",
|
||||
"scripts": {
|
||||
"clean": "ci/clean.sh",
|
||||
"vscode": "ci/vscode.sh",
|
||||
@@ -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",
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -100,4 +100,11 @@
|
||||
<script>
|
||||
require(["vs/code/browser/workbench/workbench"], function() {})
|
||||
</script>
|
||||
<script>
|
||||
try {
|
||||
document.body.style.background = JSON.parse(localStorage.getItem("colorThemeData")).colorMap["editor.background"]
|
||||
} catch (error) {
|
||||
// Oh well.
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
43
src/node/app/proxy.ts
Normal file
43
src/node/app/proxy.ts
Normal file
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -221,8 +221,13 @@ export class UpdateHttpProvider extends HttpProvider {
|
||||
targetPath = path.resolve(__dirname, "../../../")
|
||||
}
|
||||
|
||||
logger.debug("Replacing files", field("target", targetPath))
|
||||
await fs.move(directoryPath, targetPath, { overwrite: true })
|
||||
// Move the old directory to prevent potential data loss.
|
||||
const backupPath = path.resolve(targetPath, `../${path.basename(targetPath)}.${Date.now().toString()}`)
|
||||
logger.debug("Replacing files", field("target", targetPath), field("backup", backupPath))
|
||||
await fs.move(targetPath, backupPath)
|
||||
|
||||
// Move the new directory.
|
||||
await fs.move(directoryPath, targetPath)
|
||||
|
||||
await fs.remove(downloadPath)
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import * as cp from "child_process"
|
||||
import * as crypto from "crypto"
|
||||
import * as fs from "fs-extra"
|
||||
import * as http from "http"
|
||||
import * as net from "net"
|
||||
import * as path from "path"
|
||||
import * as url from "url"
|
||||
import {
|
||||
CodeServerMessage,
|
||||
Options,
|
||||
@@ -128,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 } }
|
||||
@@ -168,15 +166,12 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
private async getRoot(request: http.IncomingMessage, route: Route): Promise<HttpResponse> {
|
||||
const remoteAuthority = request.headers.host as string
|
||||
const { lastVisited } = await settings.read()
|
||||
const startPath = await this.getFirstValidPath(
|
||||
[
|
||||
{ url: route.query.workspace, workspace: true },
|
||||
{ url: route.query.folder, workspace: false },
|
||||
this.args._ && this.args._.length > 0 ? { url: path.resolve(this.args._[this.args._.length - 1]) } : undefined,
|
||||
lastVisited,
|
||||
],
|
||||
remoteAuthority,
|
||||
)
|
||||
const startPath = await this.getFirstPath([
|
||||
{ url: route.query.workspace, workspace: true },
|
||||
{ url: route.query.folder, workspace: false },
|
||||
this.args._ && this.args._.length > 0 ? { url: path.resolve(this.args._[this.args._.length - 1]) } : undefined,
|
||||
lastVisited,
|
||||
])
|
||||
const [response, options] = await Promise.all([
|
||||
await this.getUtf8Resource(this.rootPath, "src/browser/pages/vscode.html"),
|
||||
this.initialize({
|
||||
@@ -209,41 +204,19 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose the first valid path. If `workspace` is undefined then either a
|
||||
* workspace or a directory are acceptable. Otherwise it must be a file if a
|
||||
* workspace or a directory otherwise.
|
||||
* Choose the first non-empty path.
|
||||
*/
|
||||
private async getFirstValidPath(
|
||||
private async getFirstPath(
|
||||
startPaths: Array<{ url?: string | string[]; workspace?: boolean } | undefined>,
|
||||
remoteAuthority: string,
|
||||
): Promise<StartPath | undefined> {
|
||||
for (let i = 0; i < startPaths.length; ++i) {
|
||||
const startPath = startPaths[i]
|
||||
if (!startPath) {
|
||||
continue
|
||||
}
|
||||
const paths = typeof startPath.url === "string" ? [startPath.url] : startPath.url || []
|
||||
for (let j = 0; j < paths.length; ++j) {
|
||||
const uri = url.parse(paths[j])
|
||||
try {
|
||||
if (!uri.pathname) {
|
||||
throw new Error(`${paths[j]} is not a valid URL`)
|
||||
}
|
||||
const stat = await fs.stat(uri.pathname)
|
||||
if (typeof startPath.workspace === "undefined" || startPath.workspace !== stat.isDirectory()) {
|
||||
return {
|
||||
url: url.format({
|
||||
protocol: uri.protocol || "vscode-remote",
|
||||
hostname: remoteAuthority.split(":")[0],
|
||||
port: remoteAuthority.split(":")[1],
|
||||
pathname: uri.pathname,
|
||||
slashes: true,
|
||||
}),
|
||||
workspace: !stat.isDirectory(),
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(error.message)
|
||||
const url =
|
||||
startPath && (typeof startPath.url === "string" ? [startPath.url] : startPath.url || []).find((p) => !!p)
|
||||
if (startPath && url) {
|
||||
return {
|
||||
url,
|
||||
workspace: !!startPath.workspace,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
@@ -98,8 +99,8 @@ const options: Options<Required<Args>> = {
|
||||
version: { type: "boolean", short: "v", description: "Display version information." },
|
||||
_: { type: "string[]" },
|
||||
|
||||
"disable-ssh": { type: "boolean" },
|
||||
"ssh-host-key": { type: "string", path: true },
|
||||
"disable-ssh": { type: "boolean", description: "Disable the SSH server." },
|
||||
"ssh-host-key": { type: "string", path: true, description: "SSH server host key." },
|
||||
|
||||
"user-data-dir": { type: "string", path: true, description: "Path to the user data directory." },
|
||||
"extensions-dir": { type: "string", path: true, description: "Path to the extensions directory." },
|
||||
@@ -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 },
|
||||
|
||||
@@ -5,87 +5,76 @@ 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"
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error(`Uncaught exception: ${error.message}`)
|
||||
if (typeof error.stack !== "undefined") {
|
||||
logger.error(error.stack)
|
||||
}
|
||||
})
|
||||
|
||||
let pkg: { version?: string; commit?: string } = {}
|
||||
try {
|
||||
pkg = require("../../package.json")
|
||||
} catch (error) {
|
||||
logger.warn(error.message)
|
||||
}
|
||||
|
||||
const version = pkg.version || "development"
|
||||
const commit = pkg.commit || "development"
|
||||
|
||||
const main = async (args: Args): Promise<void> => {
|
||||
const auth = args.auth || AuthType.Password
|
||||
const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword()))
|
||||
|
||||
let commit: string | undefined
|
||||
try {
|
||||
commit = require("../../package.json").commit
|
||||
} catch (error) {
|
||||
logger.warn(error.message)
|
||||
}
|
||||
|
||||
// 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: commit || "development",
|
||||
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)
|
||||
|
||||
ipcMain().onDispose(() => httpServer.dispose())
|
||||
|
||||
logger.info(`code-server ${require("../../package.json").version}`)
|
||||
|
||||
let sshPort = ""
|
||||
if (!args["disable-ssh"] && options.sshHostKey) {
|
||||
const sshProvider = httpServer.registerHttpProvider("/ssh", SshProvider, options.sshHostKey as string)
|
||||
try {
|
||||
sshPort = await sshProvider.listen()
|
||||
} catch (error) {
|
||||
logger.warn(`SSH server: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`code-server ${version} ${commit}`)
|
||||
const serverAddress = await httpServer.listen()
|
||||
logger.info(`Server listening on ${serverAddress}`)
|
||||
logger.info(`HTTP server listening on ${serverAddress}`)
|
||||
|
||||
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`")
|
||||
}
|
||||
@@ -97,7 +86,7 @@ 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`,
|
||||
)
|
||||
@@ -105,19 +94,44 @@ const main = async (args: Args): Promise<void> => {
|
||||
logger.info(" - Not serving HTTPS")
|
||||
}
|
||||
|
||||
logger.info(` - Automatic updates are ${update.enabled ? "enabled" : "disabled"}`)
|
||||
if (httpServer.proxyDomains.size > 0) {
|
||||
logger.info(` - Proxying the following domain${httpServer.proxyDomains.size === 1 ? "" : "s"}:`)
|
||||
httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
|
||||
}
|
||||
|
||||
if (sshPort) {
|
||||
logger.info(` - SSH Server - Listening :${sshPort}`)
|
||||
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"] && sshHostKey) {
|
||||
const sshProvider = httpServer.registerHttpProvider("/ssh", SshProvider, sshHostKey)
|
||||
try {
|
||||
sshPort = await sshProvider.listen()
|
||||
} catch (error) {
|
||||
logger.warn(`SSH server: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
logger.info("SSH server disabled")
|
||||
}
|
||||
|
||||
if (serverAddress && !options.socket && args.open) {
|
||||
// The web socket doesn't seem to work if browsing with 0.0.0.0.
|
||||
const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost")
|
||||
await open(openAddress).catch(console.error)
|
||||
logger.info(` - Opened ${openAddress}`)
|
||||
logger.info(`Opened ${openAddress}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +146,7 @@ const tryParse = (): Args => {
|
||||
|
||||
const args = tryParse()
|
||||
if (args.help) {
|
||||
console.log("code-server", require("../../package.json").version)
|
||||
console.log("code-server", version, commit)
|
||||
console.log("")
|
||||
console.log(`Usage: code-server [options] [path]`)
|
||||
console.log("")
|
||||
@@ -141,14 +155,14 @@ if (args.help) {
|
||||
console.log("", description)
|
||||
})
|
||||
} else if (args.version) {
|
||||
const version = require("../../package.json").version
|
||||
if (args.json) {
|
||||
console.log({
|
||||
codeServer: version,
|
||||
commit,
|
||||
vscode: require("../../lib/vscode/package.json").version,
|
||||
})
|
||||
} else {
|
||||
console.log(version)
|
||||
console.log(version, commit)
|
||||
}
|
||||
process.exit(0)
|
||||
} else if (args["list-extensions"] || args["install-extension"] || args["uninstall-extension"]) {
|
||||
|
||||
238
src/node/http.ts
238
src/node/http.ts
@@ -1,6 +1,7 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import * as fs from "fs-extra"
|
||||
import * as http from "http"
|
||||
import proxy from "http-proxy"
|
||||
import * as httpolyglot from "httpolyglot"
|
||||
import * as https from "https"
|
||||
import * as net from "net"
|
||||
@@ -18,6 +19,10 @@ import { getMediaMime, xdgLocalDir } from "./util"
|
||||
export type Cookies = { [key: string]: string[] | undefined }
|
||||
export type PostData = { [key: string]: string | string[] | undefined }
|
||||
|
||||
interface ProxyRequest extends http.IncomingMessage {
|
||||
base?: string
|
||||
}
|
||||
|
||||
interface AuthPayload extends Cookies {
|
||||
key?: string[]
|
||||
}
|
||||
@@ -29,6 +34,17 @@ export enum AuthType {
|
||||
|
||||
export type Query = { [key: string]: string | string[] | undefined }
|
||||
|
||||
export interface ProxyOptions {
|
||||
/**
|
||||
* A base path to strip from from the request before proxying if necessary.
|
||||
*/
|
||||
base?: string
|
||||
/**
|
||||
* The port to proxy.
|
||||
*/
|
||||
port: string
|
||||
}
|
||||
|
||||
export interface HttpResponse<T = string | Buffer | object> {
|
||||
/*
|
||||
* Whether to set cache-control headers for this response.
|
||||
@@ -77,6 +93,17 @@ export interface HttpResponse<T = string | Buffer | object> {
|
||||
* `undefined` to remove a query variable.
|
||||
*/
|
||||
query?: Query
|
||||
/**
|
||||
* Indicates the request should be proxied.
|
||||
*/
|
||||
proxy?: ProxyOptions
|
||||
}
|
||||
|
||||
export interface WsResponse {
|
||||
/**
|
||||
* Indicates the web socket should be proxied.
|
||||
*/
|
||||
proxy?: ProxyOptions
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,14 +127,31 @@ export interface HttpServerOptions {
|
||||
readonly host?: string
|
||||
readonly password?: string
|
||||
readonly port?: number
|
||||
readonly proxyDomains?: string[]
|
||||
readonly socket?: string
|
||||
}
|
||||
|
||||
export interface Route {
|
||||
/**
|
||||
* Base path part (in /test/path it would be "/test").
|
||||
*/
|
||||
base: string
|
||||
/**
|
||||
* Remaining part of the route (in /test/path it would be "/path"). It can be
|
||||
* blank.
|
||||
*/
|
||||
requestPath: string
|
||||
/**
|
||||
* Query variables included in the request.
|
||||
*/
|
||||
query: querystring.ParsedUrlQuery
|
||||
/**
|
||||
* Normalized version of `originalPath`.
|
||||
*/
|
||||
fullPath: string
|
||||
/**
|
||||
* Original path of the request without any modifications.
|
||||
*/
|
||||
originalPath: string
|
||||
}
|
||||
|
||||
@@ -136,7 +180,9 @@ export abstract class HttpProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle web sockets on the registered endpoint.
|
||||
* Handle web sockets on the registered endpoint. Normally the provider
|
||||
* handles the request itself but it can return a response when necessary. The
|
||||
* default is to throw a 404.
|
||||
*/
|
||||
public handleWebSocket(
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
@@ -145,7 +191,7 @@ export abstract class HttpProvider {
|
||||
_socket: net.Socket,
|
||||
_head: Buffer,
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
): Promise<void> {
|
||||
): Promise<WsResponse | void> {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
@@ -264,7 +310,7 @@ export abstract class HttpProvider {
|
||||
* Return the provided password value if the payload contains the right
|
||||
* password otherwise return false. If no payload is specified use cookies.
|
||||
*/
|
||||
protected authenticated(request: http.IncomingMessage, payload?: AuthPayload): string | boolean {
|
||||
public authenticated(request: http.IncomingMessage, payload?: AuthPayload): string | boolean {
|
||||
switch (this.options.auth) {
|
||||
case AuthType.None:
|
||||
return true
|
||||
@@ -335,6 +381,14 @@ export abstract class HttpProvider {
|
||||
}
|
||||
return cookies as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the route is for the root page. For example /base, /base/,
|
||||
* or /base/index.html but not /base/path or /base/file.js.
|
||||
*/
|
||||
protected isRoot(route: Route): boolean {
|
||||
return !route.requestPath || route.requestPath === "/index.html"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -407,7 +461,18 @@ export class HttpServer {
|
||||
private readonly heart: Heart
|
||||
private readonly socketProvider = new SocketProxyProvider()
|
||||
|
||||
/**
|
||||
* Proxy domains are stored here without the leading `*.`
|
||||
*/
|
||||
public readonly proxyDomains: Set<string>
|
||||
|
||||
/**
|
||||
* Provides the actual proxying functionality.
|
||||
*/
|
||||
private readonly proxy = proxy.createProxyServer({})
|
||||
|
||||
public constructor(private readonly options: HttpServerOptions) {
|
||||
this.proxyDomains = new Set((options.proxyDomains || []).map((d) => d.replace(/^\*\./, "")))
|
||||
this.heart = new Heart(path.join(xdgLocalDir, "heartbeat"), async () => {
|
||||
const connections = await this.getConnections()
|
||||
logger.trace(`${connections} active connection${plural(connections)}`)
|
||||
@@ -425,6 +490,16 @@ export class HttpServer {
|
||||
} else {
|
||||
this.server = http.createServer(this.onRequest)
|
||||
}
|
||||
this.proxy.on("error", (error, _request, response) => {
|
||||
response.writeHead(HttpCode.ServerError)
|
||||
response.end(error.message)
|
||||
})
|
||||
// Intercept the response to rewrite absolute redirects against the base path.
|
||||
this.proxy.on("proxyRes", (response, request: ProxyRequest) => {
|
||||
if (response.headers.location && response.headers.location.startsWith("/") && request.base) {
|
||||
response.headers.location = request.base + response.headers.location
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
@@ -525,9 +600,12 @@ export class HttpServer {
|
||||
"Set-Cookie": [
|
||||
`${payload.cookie.key}=${payload.cookie.value}`,
|
||||
`Path=${normalize(payload.cookie.path || "/", true)}`,
|
||||
this.getCookieDomain(request.headers.host || ""),
|
||||
// "HttpOnly",
|
||||
"SameSite=strict",
|
||||
].join(";"),
|
||||
"SameSite=lax",
|
||||
]
|
||||
.filter((l) => !!l)
|
||||
.join(";"),
|
||||
}
|
||||
: {}),
|
||||
...payload.headers,
|
||||
@@ -547,20 +625,27 @@ export class HttpServer {
|
||||
response.end()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = this.maybeRedirect(request, route) || (await route.provider.handleRequest(route, request))
|
||||
if (!payload) {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
const payload =
|
||||
this.maybeRedirect(request, route) ||
|
||||
(route.provider.authenticated(request) && this.maybeProxy(request)) ||
|
||||
(await route.provider.handleRequest(route, request))
|
||||
if (payload.proxy) {
|
||||
this.doProxy(route, request, response, payload.proxy)
|
||||
} else {
|
||||
write(payload)
|
||||
}
|
||||
write(payload)
|
||||
} catch (error) {
|
||||
let e = error
|
||||
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
||||
e = new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
logger.debug("Request error", field("url", request.url))
|
||||
logger.debug(error.stack)
|
||||
const code = typeof e.code === "number" ? e.code : HttpCode.ServerError
|
||||
logger.debug("Request error", field("url", request.url), field("code", code))
|
||||
if (code >= HttpCode.ServerError) {
|
||||
logger.error(error.stack)
|
||||
}
|
||||
const payload = await route.provider.getErrorRoot(route, code, code, e.message)
|
||||
write({
|
||||
code,
|
||||
@@ -625,7 +710,14 @@ export class HttpServer {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
await route.provider.handleWebSocket(route, request, await this.socketProvider.createProxy(socket), head)
|
||||
// The socket proxy is so we can pass them to child processes (TLS sockets
|
||||
// can't be transferred so we need an in-between).
|
||||
const socketProxy = await this.socketProvider.createProxy(socket)
|
||||
const payload =
|
||||
this.maybeProxy(request) || (await route.provider.handleWebSocket(route, request, socketProxy, head))
|
||||
if (payload && payload.proxy) {
|
||||
this.doProxy(route, request, { socket: socketProxy, head }, payload.proxy)
|
||||
}
|
||||
} catch (error) {
|
||||
socket.destroy(error)
|
||||
logger.warn(`discarding socket connection: ${error.message}`)
|
||||
@@ -647,7 +739,6 @@ export class HttpServer {
|
||||
// Happens if it's a plain `domain.com`.
|
||||
base = "/"
|
||||
}
|
||||
requestPath = requestPath || "/index.html"
|
||||
return { base, requestPath }
|
||||
}
|
||||
|
||||
@@ -670,4 +761,125 @@ export class HttpServer {
|
||||
}
|
||||
return { base, fullPath, requestPath, query: parsedUrl.query, provider, originalPath }
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy a request to the target.
|
||||
*/
|
||||
private doProxy(
|
||||
route: Route,
|
||||
request: http.IncomingMessage,
|
||||
response: http.ServerResponse,
|
||||
options: ProxyOptions,
|
||||
): void
|
||||
/**
|
||||
* Proxy a web socket to the target.
|
||||
*/
|
||||
private doProxy(
|
||||
route: Route,
|
||||
request: http.IncomingMessage,
|
||||
response: { socket: net.Socket; head: Buffer },
|
||||
options: ProxyOptions,
|
||||
): void
|
||||
/**
|
||||
* Proxy a request or web socket to the target.
|
||||
*/
|
||||
private doProxy(
|
||||
route: Route,
|
||||
request: http.IncomingMessage,
|
||||
response: http.ServerResponse | { socket: net.Socket; head: Buffer },
|
||||
options: ProxyOptions,
|
||||
): void {
|
||||
const port = parseInt(options.port, 10)
|
||||
if (isNaN(port)) {
|
||||
throw new HttpError(`"${options.port}" is not a valid number`, HttpCode.BadRequest)
|
||||
}
|
||||
|
||||
// REVIEW: Absolute redirects need to be based on the subpath but I'm not
|
||||
// sure how best to get this information to the `proxyRes` event handler.
|
||||
// For now I'm sticking it on the request object which is passed through to
|
||||
// the event.
|
||||
;(request as ProxyRequest).base = options.base
|
||||
|
||||
const isHttp = response instanceof http.ServerResponse
|
||||
const path = options.base ? route.fullPath.replace(options.base, "") : route.fullPath
|
||||
const proxyOptions: proxy.ServerOptions = {
|
||||
changeOrigin: true,
|
||||
ignorePath: true,
|
||||
target: `${isHttp ? "http" : "ws"}://127.0.0.1:${port}${path}${
|
||||
Object.keys(route.query).length > 0 ? `?${querystring.stringify(route.query)}` : ""
|
||||
}`,
|
||||
ws: !isHttp,
|
||||
}
|
||||
|
||||
if (response instanceof http.ServerResponse) {
|
||||
this.proxy.web(request, response, proxyOptions)
|
||||
} else {
|
||||
this.proxy.ws(request, response.socket, response.head, proxyOptions)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value that should be used for setting a cookie domain. This will
|
||||
* allow the user to authenticate only once. This will use the highest level
|
||||
* domain (e.g. `coder.com` over `test.coder.com` if both are specified).
|
||||
*/
|
||||
private getCookieDomain(host: string): string | undefined {
|
||||
const idx = host.lastIndexOf(":")
|
||||
host = idx !== -1 ? host.substring(0, idx) : host
|
||||
if (
|
||||
// Might be blank/missing, so there's nothing more to do.
|
||||
!host ||
|
||||
// IP addresses can't have subdomains so there's no value in setting the
|
||||
// domain for them. Assume anything with a : is ipv6 (valid domain name
|
||||
// characters are alphanumeric or dashes).
|
||||
host.includes(":") ||
|
||||
// Assume anything entirely numbers and dots is ipv4 (currently tlds
|
||||
// cannot be entirely numbers).
|
||||
!/[^0-9.]/.test(host) ||
|
||||
// localhost subdomains don't seem to work at all (browser bug?).
|
||||
host.endsWith(".localhost") ||
|
||||
// It might be localhost (or an IP, see above) if it's a proxy and it
|
||||
// isn't setting the host header to match the access domain.
|
||||
host === "localhost"
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
this.proxyDomains.forEach((domain) => {
|
||||
if (host.endsWith(domain) && domain.length < host.length) {
|
||||
host = domain
|
||||
}
|
||||
})
|
||||
|
||||
return host ? `Domain=${host}` : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a response if the request should be proxied. Anything that ends in a
|
||||
* proxy domain and has a *single* subdomain should be proxied. Anything else
|
||||
* should return `undefined` and will be handled as normal.
|
||||
*
|
||||
* For example if `coder.com` is specified `8080.coder.com` will be proxied
|
||||
* but `8080.test.coder.com` and `test.8080.coder.com` will not.
|
||||
*/
|
||||
public maybeProxy(request: http.IncomingMessage): HttpResponse | undefined {
|
||||
// Split into parts.
|
||||
const host = request.headers.host || ""
|
||||
const idx = host.indexOf(":")
|
||||
const domain = idx !== -1 ? host.substring(0, idx) : host
|
||||
const parts = domain.split(".")
|
||||
|
||||
// There must be an exact match.
|
||||
const port = parts.shift()
|
||||
const proxyDomain = parts.join(".")
|
||||
if (!port || !this.proxyDomains.has(proxyDomain)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
proxy: {
|
||||
port,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,11 @@ export class SshProvider extends HttpProvider {
|
||||
})
|
||||
}
|
||||
|
||||
public async listen(): Promise<string> {
|
||||
public async listen(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.sshServer.once("error", reject)
|
||||
this.sshServer.listen(() => {
|
||||
resolve(this.sshServer.address().port.toString())
|
||||
resolve(this.sshServer.address().port)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ describe("cli", () => {
|
||||
assert.throws(() => parse(["--auth=", "--log=debug"]), /--auth requires a value/)
|
||||
assert.throws(() => parse(["--auth", "--log"]), /--auth requires a value/)
|
||||
assert.throws(() => parse(["--auth", "--invalid"]), /--auth requires a value/)
|
||||
assert.throws(() => parse(["--ssh-host-key"]), /--ssh-host-key requires a value/)
|
||||
})
|
||||
|
||||
it("should error if value is invalid", () => {
|
||||
@@ -160,4 +161,19 @@ describe("cli", () => {
|
||||
auth: "none",
|
||||
})
|
||||
})
|
||||
|
||||
it("should support repeatable flags", () => {
|
||||
assert.deepEqual(parse(["--proxy-domain", "*.coder.com"]), {
|
||||
_: [],
|
||||
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
||||
"user-data-dir": xdgLocalDir,
|
||||
"proxy-domain": ["*.coder.com"],
|
||||
})
|
||||
assert.deepEqual(parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "test.com"]), {
|
||||
_: [],
|
||||
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
||||
"user-data-dir": xdgLocalDir,
|
||||
"proxy-domain": ["*.coder.com", "test.com"],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -214,13 +214,18 @@ describe("update", () => {
|
||||
await p.downloadAndApplyUpdate(update, destination)
|
||||
assert.equal(`console.log("UPDATED")`, await fs.readFile(entry, "utf8"))
|
||||
|
||||
// Should still work if there is no existing version somehow.
|
||||
await fs.remove(destination)
|
||||
await p.downloadAndApplyUpdate(update, destination)
|
||||
assert.equal(`console.log("UPDATED")`, await fs.readFile(entry, "utf8"))
|
||||
// There should be a backup.
|
||||
const dir = (await fs.readdir(path.join(tmpdir, "tests/updates"))).filter((dir) => {
|
||||
return dir.startsWith("code-server.")
|
||||
})
|
||||
assert.equal(dir.length, 1)
|
||||
assert.equal(
|
||||
`console.log("OLD")`,
|
||||
await fs.readFile(path.join(tmpdir, "tests/updates", dir[0], "code-server"), "utf8"),
|
||||
)
|
||||
|
||||
const archiveName = await p.getReleaseName(update)
|
||||
assert.deepEqual(spy, ["/latest", `/download/${version}/${archiveName}`, `/download/${version}/${archiveName}`])
|
||||
assert.deepEqual(spy, ["/latest", `/download/${version}/${archiveName}`])
|
||||
})
|
||||
|
||||
it("should not reject if unable to fetch", async () => {
|
||||
|
||||
35
yarn.lock
35
yarn.lock
@@ -871,6 +871,13 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/http-proxy@^1.17.4":
|
||||
version "1.17.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b"
|
||||
integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/json-schema@^7.0.3":
|
||||
version "7.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
|
||||
@@ -2240,7 +2247,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@3.2.6:
|
||||
debug@3.2.6, debug@^3.0.0:
|
||||
version "3.2.6"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
|
||||
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
|
||||
@@ -2745,6 +2752,11 @@ etag@~1.8.1:
|
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
|
||||
|
||||
eventemitter3@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb"
|
||||
integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==
|
||||
|
||||
events@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59"
|
||||
@@ -2980,6 +2992,13 @@ flatted@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08"
|
||||
integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==
|
||||
|
||||
follow-redirects@^1.0.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.10.0.tgz#01f5263aee921c6a54fb91667f08f4155ce169eb"
|
||||
integrity sha512-4eyLK6s6lH32nOvLLwlIOnr9zrL8Sm+OvW4pVTJNoXeGzYIkHVf+pADQi+OJ0E67hiuSLezPVPyBcIZO50TmmQ==
|
||||
dependencies:
|
||||
debug "^3.0.0"
|
||||
|
||||
for-in@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
|
||||
@@ -3403,6 +3422,15 @@ http-errors@~1.7.2:
|
||||
statuses ">= 1.5.0 < 2"
|
||||
toidentifier "1.0.0"
|
||||
|
||||
http-proxy@^1.18.0:
|
||||
version "1.18.0"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a"
|
||||
integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==
|
||||
dependencies:
|
||||
eventemitter3 "^4.0.0"
|
||||
follow-redirects "^1.0.0"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
http-signature@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
|
||||
@@ -5894,6 +5922,11 @@ require-main-filename@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
|
||||
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
|
||||
|
||||
requires-port@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
||||
|
||||
resolve-from@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
|
||||
|
||||
Reference in New Issue
Block a user