Compare commits

...

24 Commits

Author SHA1 Message Date
Asher
2018024810 Hash password
Fixes issues with unexpected characters breaking things when setting the
cookie (like semicolons).

This change as-is does not affect the security of code-server
itself (we've just replaced the static password with a static hash) but
if we were to add a salt in the future it would let us invalidate keys
by rehashing with a new salt which could be handy.
2019-11-07 15:57:57 -06:00
Asher
a1d6bcb8e5 Handle cookies more robustly
If you visit /login/ instead of /login the cookie will be set at /login
instead of / which means the cookie can't be read at the root. It will
redirect to the login page which *can* read the cookie at /login and
redirect back resulting in an infinite loop.

The previous solution relied on setting the cookie at / (any invalid
value works) which then overrode the login page cookie since
parseCookies only kept a single value. So the login page would see the
same cookie the root was seeing and not redirect back. However, that
behavior depends on the cookies being in the right order which I'm not
sure is guaranteed.

This new method tests all available cookies and always sets the cookie
so the root path will be able to read it in case the login page is
seeing a cookie the root can't.

It also goes a step further and explicitly sets the path on the cookie
which fixes the case where there is a permanent misconfiguration
redirecting /login to /login/. Otherwise the cookie would continually be
set on /login only and you'd have another loop. It also means you only
need to delete one cookie to log out.

Lastly add some properties to make the cookies a bit more secure.
2019-11-07 13:36:18 -06:00
ecrode
727ac6483b Clear password when redirecting to login
Should prevent endless redirects when the cookie is set on a different path or domain (like with a dot prefix).
2019-11-07 11:38:10 -06:00
Asher
2c15c09fc0 Add missing telemetry option 2019-11-06 15:47:34 -06:00
Asher
2ad2582cc0 Minor readme updates and fixes 2019-11-05 13:49:18 -06:00
Asher
cee0ac213c Fix error activating extensions on insecure domains
Doesn't affect Firefox but it does affect other browsers.

Fixes #1136.
2019-11-04 17:10:00 -06:00
Asher
780a673017 Add meta tag to allow full screen app on iOS
Fixes #933.
2019-11-04 16:01:01 -06:00
Asher
af71203955 Fix relaunching during an update 2019-11-01 10:51:23 -05:00
Asher
fc3acfabb2 Fix update check 2019-10-30 17:35:50 -05:00
Asher
3d5db8313a Add secure domain to requirements 2019-10-30 10:33:07 -05:00
Asher
73cf8f34e3 Fix outgoing scheme transformation
Accidentally used local instead of remote.

Fixes #1127.
2019-10-30 10:32:57 -05:00
dependabot[bot]
766efd6079 Bump mixin-deep from 1.3.1 to 1.3.2 (#1126)
Bumps [mixin-deep](https://github.com/jonschlinkert/mixin-deep) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/jonschlinkert/mixin-deep/releases)
- [Commits](https://github.com/jonschlinkert/mixin-deep/compare/1.3.1...1.3.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-10-29 15:20:12 -05:00
Asher
87485948ad Kill inner process if parent process dies
Fixes #1076.
2019-10-29 14:43:27 -05:00
Asher
7e4a73ce2d Fix schema matching against vscode-remote
Fixes #1104.
2019-10-29 11:42:28 -05:00
Asher
2f0878d9b7 Revert remote scheme change
It doesn't show in the explorer anymore so there's no point. Also remove
the local scheme transform which is no longer required with the latest
client-side extension implementation.
2019-10-29 11:26:50 -05:00
Marc-André Daigneault
f65c9b23fc Add docker-compose file (#680) 2019-10-29 11:08:01 -05:00
Asher
cd859d117f Start pushing to latest Docker tag 2019-10-29 11:04:38 -05:00
Asher
e22964915a Support opening workspaces from command line
Partly addresses #1121.
2019-10-28 16:25:51 -05:00
Asher
197d0b6ca9 Strip internal env vars when spawning the shell
This should fix all those reports of code-server dropping straight to
Node and things like #1121.
2019-10-28 16:08:32 -05:00
Asher
422503ef98 Proxy child exit code when exiting parent process
This fixes code-server exiting with zero on errors.
2019-10-28 14:57:01 -05:00
Asher
ea36345d2c Allow fetching any resource
Fixes #1118.
2019-10-28 14:29:51 -05:00
Asher
a89d83cbba Fix other incorrect usages of split 2019-10-28 14:03:13 -05:00
Asher
83ff31b620 Fix passwords that contain =
Fixes #1119.

Apparently `split` does not work the way I'd expect.
2019-10-28 13:47:31 -05:00
Asher
3a9b032c72 Add heartbeat file (#1115)
Fixes #1050.
2019-10-28 09:59:34 -05:00
15 changed files with 290 additions and 79 deletions

View File

@@ -61,7 +61,7 @@ deploy:
- provider: script - provider: script
skip_cleanup: true skip_cleanup: true
script: docker build -f ./scripts/ci.dockerfile -t codercom/code-server:"$TAG" -t codercom/code-server:v2 . && docker push codercom/code-server:"$TAG" && docker push codercom/code-server:v2 script: docker build -f ./scripts/ci.dockerfile -t codercom/code-server:"$TAG" -t codercom/code-server:v2 -t codercom/code-server . && docker push codercom/code-server:"$TAG" && docker push codercom/code-server:v2 && docker push codercom/code-server
on: on:
repo: cdr/code-server repo: cdr/code-server
branch: master branch: master

View File

@@ -22,10 +22,11 @@ docker run -it -p 127.0.0.1:8080:8080 -v "${HOME}/.local/share/code-server:/home
### Requirements ### Requirements
- Minimum GLIBC version of 2.17 and a minimum version of GLIBCXX of 3.4.15. - 64-bit host.
- This is the main requirement for building Visual Studio Code. We cannot go lower than this. - At least 1GB of RAM.
- A 64-bit host with at least 1GB RAM and 2 cores. - 2 cores or more are recommended (1 core works but not optimally).
- 1 core hosts would work but not optimally. - Secure connection over HTTPS or localhost (required for service workers).
- For Linux: GLIBC 2.17 or later and GLIBCXX 3.4.15 or later.
- Docker (for Docker versions of `code-server`). - Docker (for Docker versions of `code-server`).
### Run over SSH ### Run over SSH
@@ -59,14 +60,14 @@ arguments when launching code-server with Docker. See
### Build ### Build
See See
[VS Code prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites) [VS Code's prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites)
before building. before building.
```shell ```shell
export OUT=/path/to/output/build # Optional if only building. Required if also developing. export OUT=/path/to/output/build # Optional if only building. Required if also developing.
yarn build ${vscodeVersion} ${codeServerVersion} # See travis.yml for the VS Code version to use. yarn build ${vscodeVersion} ${codeServerVersion} # See travis.yml for the VS Code version to use.
# The code-server version can be anything you want. # The code-server version can be anything you want.
node ~/path/to/output/build/out/vs/server/main.js # You can run the built JavaScript with Node. node /path/to/output/build/out/vs/server/main.js # You can run the built JavaScript with Node.
yarn binary ${vscodeVersion} ${codeServerVersion} # Or you can package it into a binary. yarn binary ${vscodeVersion} ${codeServerVersion} # Or you can package it into a binary.
``` ```
@@ -134,7 +135,7 @@ data collected to improve code-server.
### Development ### Development
See See
[VS Code prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites) [VS Code's prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites)
before developing. before developing.
```shell ```shell
@@ -154,8 +155,7 @@ yarn start
``` ```
If you run into issues about a different version of Node being used, try running If you run into issues about a different version of Node being used, try running
`npm rebuild` in the VS Code directory and ignore the error at the end from `npm rebuild` in the VS Code directory.
`vscode-ripgrep`.
### Upgrading VS Code ### Upgrading VS Code
@@ -170,7 +170,6 @@ directory.
Our changes include: Our changes include:
- Change the remote schema to `code-server`.
- Allow multiple extension directories (both user and built-in). - Allow multiple extension directories (both user and built-in).
- Modify the loader, websocket, webview, service worker, and asset requests to - Modify the loader, websocket, webview, service worker, and asset requests to
use the URL of the page as a base (and TLS if necessary for the websocket). use the URL of the page as a base (and TLS if necessary for the websocket).

13
docker-compose.yml Normal file
View File

@@ -0,0 +1,13 @@
version: "3"
services:
code-server:
container_name: code-server
image: codercom/code-server
ports:
- "8080:8080"
volumes:
- "${PWD}:/home/coder/project"
- "${HOME}/.local/share/code-server:/home/coder/.local/share/code-server"
environment:
PASSWORD: ${PASSWORD}

View File

@@ -11,7 +11,7 @@
"patch:apply": "cd ../../../ && git apply ./src/vs/server/scripts/vscode.patch" "patch:apply": "cd ../../../ && git apply ./src/vs/server/scripts/vscode.patch"
}, },
"devDependencies": { "devDependencies": {
"@coder/nbin": "^1.2.2", "@coder/nbin": "^1.2.3",
"@types/fs-extra": "^8.0.1", "@types/fs-extra": "^8.0.1",
"@types/node": "^10.12.12", "@types/node": "^10.12.12",
"@types/pem": "^1.9.5", "@types/pem": "^1.9.5",

View File

@@ -1,19 +1,8 @@
diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts
index 6d41e85e42..f845d0bf9e 100644 index 6d41e85e42..64f39687a4 100644
--- a/src/vs/base/common/network.ts --- a/src/vs/base/common/network.ts
+++ b/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts
@@ -48,7 +48,9 @@ export namespace Schemas { @@ -96,12 +96,12 @@ class RemoteAuthoritiesImpl {
export const command: string = 'command';
- export const vscodeRemote: string = 'vscode-remote';
+ // NOTE@coder: Changed this so it'll be reflected in the explorer to prevent
+ // confusion with vscode-remote itself.
+ export const vscodeRemote: string = 'code-server';
export const vscodeRemoteResource: string = 'vscode-remote-resource';
@@ -96,12 +98,12 @@ class RemoteAuthoritiesImpl {
if (host && host.indexOf(':') !== -1) { if (host && host.indexOf(':') !== -1) {
host = `[${host}]`; host = `[${host}]`;
} }
@@ -50,6 +39,21 @@ index a657f4a4d9..66bd13dffa 100644
} else if (typeof process === 'object') { } else if (typeof process === 'object') {
_isWindows = (process.platform === 'win32'); _isWindows = (process.platform === 'win32');
_isMacintosh = (process.platform === 'darwin'); _isMacintosh = (process.platform === 'darwin');
diff --git a/src/vs/base/common/processes.ts b/src/vs/base/common/processes.ts
index c52f7b3774..5635cfac8a 100644
--- a/src/vs/base/common/processes.ts
+++ b/src/vs/base/common/processes.ts
@@ -110,7 +110,9 @@ export function sanitizeProcessEnvironment(env: IProcessEnvironment, ...preserve
/^ELECTRON_.+$/,
/^GOOGLE_API_KEY$/,
/^VSCODE_.+$/,
- /^SNAP(|_.*)$/
+ /^SNAP(|_.*)$/,
+ /^NBIN_BYPASS$/,
+ /^LAUNCH_VSCODE$/
];
const envKeys = Object.keys(env);
envKeys
diff --git a/src/vs/base/node/languagePacks.js b/src/vs/base/node/languagePacks.js diff --git a/src/vs/base/node/languagePacks.js b/src/vs/base/node/languagePacks.js
index 3ae24454cb..fac8679290 100644 index 3ae24454cb..fac8679290 100644
--- a/src/vs/base/node/languagePacks.js --- a/src/vs/base/node/languagePacks.js
@@ -615,6 +619,34 @@ index 84c46faa36..957e8412e1 100644
if (!this.configuration.userDataProvider) { if (!this.configuration.userDataProvider) {
const remoteUserDataUri = this.getRemoteUserDataUri(); const remoteUserDataUri = this.getRemoteUserDataUri();
diff --git a/src/vs/workbench/common/resources.ts b/src/vs/workbench/common/resources.ts
index 53de865d8f..df234821a9 100644
--- a/src/vs/workbench/common/resources.ts
+++ b/src/vs/workbench/common/resources.ts
@@ -15,6 +15,7 @@ import { ParsedExpression, IExpression, parse } from 'vs/base/common/glob';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { withNullAsUndefined } from 'vs/base/common/types';
+import { Schemas } from 'vs/base/common/network';
export class ResourceContextKey extends Disposable implements IContextKey<URI> {
@@ -63,7 +64,7 @@ export class ResourceContextKey extends Disposable implements IContextKey<URI> {
set(value: URI | null) {
if (!ResourceContextKey._uriEquals(this._resourceKey.get(), value)) {
this._resourceKey.set(value);
- this._schemeKey.set(value ? value.scheme : null);
+ this._schemeKey.set(value ? (value.scheme === Schemas.vscodeRemote ? Schemas.file : value.scheme) : null);
this._filenameKey.set(value ? basename(value) : null);
this._langIdKey.set(value ? this._modeService.getModeIdByFilepathOrFirstLine(value) : null);
this._extensionKey.set(value ? extname(value) : null);
@@ -200,4 +201,4 @@ export class ResourceGlobMatcher extends Disposable {
return !!expressionForRoot(resourcePathToMatch);
}
-}
\ No newline at end of file
+}
diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts
index 1f4cd95f65..061931cbde 100644 index 1f4cd95f65..061931cbde 100644
--- a/src/vs/workbench/contrib/files/browser/files.contribution.ts --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts
@@ -780,6 +812,21 @@ index 3bdfa1a79f..ded21cf9c6 100644
// register services that only throw errors // register services that only throw errors
function NotImplementedProxy<T>(name: ServiceIdentifier<T>): { new(): T } { function NotImplementedProxy<T>(name: ServiceIdentifier<T>): { new(): T } {
diff --git a/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts b/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts
index 3b5706ce76..f390ed35dc 100644
--- a/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts
+++ b/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts
@@ -36,7 +36,9 @@ const nativeAddEventLister = addEventListener.bind(self);
self.addEventLister = () => console.trace(`'addEventListener' has been blocked`);
self.indexedDB.open = () => console.trace(`'indexedDB.open' has been blocked`);
-self.caches.open = () => console.trace(`'indexedDB.caches' has been blocked`);
+if (self.caches) { // NOTE@coder: on insecure domains this exists in Firefox but not Chromium or Safari.
+ self.caches.open = () => console.trace(`'indexedDB.caches' has been blocked`);
+}
//#endregion ---
diff --git a/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts b/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts diff --git a/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts b/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts
index 99394090da..4891e0fece 100644 index 99394090da..4891e0fece 100644
--- a/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts --- a/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts

View File

@@ -1,17 +1,21 @@
import { Emitter } from "vs/base/common/event"; import { Emitter } from "vs/base/common/event";
import { URI } from "vs/base/common/uri"; import { URI } from "vs/base/common/uri";
import { localize } from "vs/nls";
import { Extensions, IConfigurationRegistry } from "vs/platform/configuration/common/configurationRegistry";
import { registerSingleton } from "vs/platform/instantiation/common/extensions"; import { registerSingleton } from "vs/platform/instantiation/common/extensions";
import { ServiceCollection } from "vs/platform/instantiation/common/serviceCollection"; import { ServiceCollection } from "vs/platform/instantiation/common/serviceCollection";
import { ILocalizationsService } from "vs/platform/localizations/common/localizations"; import { ILocalizationsService } from "vs/platform/localizations/common/localizations";
import { LocalizationsService } from "vs/workbench/services/localizations/electron-browser/localizationsService"; import { Registry } from "vs/platform/registry/common/platform";
import { PersistentConnectionEventType } from "vs/platform/remote/common/remoteAgentConnection";
import { ITelemetryService } from "vs/platform/telemetry/common/telemetry"; import { ITelemetryService } from "vs/platform/telemetry/common/telemetry";
import { coderApi, vscodeApi } from "vs/server/src/browser/api"; import { coderApi, vscodeApi } from "vs/server/src/browser/api";
import { IUploadService, UploadService } from "vs/server/src/browser/upload"; import { IUploadService, UploadService } from "vs/server/src/browser/upload";
import { INodeProxyService, NodeProxyChannelClient } from "vs/server/src/common/nodeProxy"; import { INodeProxyService, NodeProxyChannelClient } from "vs/server/src/common/nodeProxy";
import { TelemetryChannelClient } from "vs/server/src/common/telemetry"; import { TelemetryChannelClient } from "vs/server/src/common/telemetry";
import { split } from "vs/server/src/common/util";
import "vs/workbench/contrib/localizations/browser/localizations.contribution"; import "vs/workbench/contrib/localizations/browser/localizations.contribution";
import { LocalizationsService } from "vs/workbench/services/localizations/electron-browser/localizationsService";
import { IRemoteAgentService } from "vs/workbench/services/remote/common/remoteAgentService"; import { IRemoteAgentService } from "vs/workbench/services/remote/common/remoteAgentService";
import { PersistentConnectionEventType } from "vs/platform/remote/common/remoteAgentConnection";
class TelemetryService extends TelemetryChannelClient { class TelemetryService extends TelemetryChannelClient {
public constructor( public constructor(
@@ -21,6 +25,23 @@ class TelemetryService extends TelemetryChannelClient {
} }
} }
const TELEMETRY_SECTION_ID = "telemetry";
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
"id": TELEMETRY_SECTION_ID,
"order": 110,
"type": "object",
"title": localize("telemetryConfigurationTitle", "Telemetry"),
"properties": {
"telemetry.enableTelemetry": {
"type": "boolean",
"description": localize("telemetry.enableTelemetry", "Enable usage data and errors to be sent to a Microsoft online service."),
"default": true,
"tags": ["usesOnlineServices"]
}
}
});
class NodeProxyService extends NodeProxyChannelClient implements INodeProxyService { class NodeProxyService extends NodeProxyChannelClient implements INodeProxyService {
private readonly _onClose = new Emitter<void>(); private readonly _onClose = new Emitter<void>();
public readonly onClose = this._onClose.event; public readonly onClose = this._onClose.event;
@@ -79,7 +100,7 @@ export const withQuery = (url: string, replace: Query): string => {
const uri = URI.parse(url); const uri = URI.parse(url);
const query = { ...replace }; const query = { ...replace };
uri.query.split("&").forEach((kv) => { uri.query.split("&").forEach((kv) => {
const [key, value] = kv.split("=", 2); const [key, value] = split(kv, "=");
if (!(key in query)) { if (!(key in query)) {
query[key] = value; query[key] = value;
} }

View File

@@ -40,6 +40,7 @@
<link rel="manifest" href="./manifest.json"> <link rel="manifest" href="./manifest.json">
<link rel="apple-touch-icon" href="./static-{{COMMIT}}/out/vs/server/src/media/code-server.png" /> <link rel="apple-touch-icon" href="./static-{{COMMIT}}/out/vs/server/src/media/code-server.png" />
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="./static-{{COMMIT}}/out/vs/workbench/workbench.web.api.css"> <link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="./static-{{COMMIT}}/out/vs/workbench/workbench.web.api.css">
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- Prefetch to avoid waterfall --> <!-- Prefetch to avoid waterfall -->
<link rel="prefetch" href="./static-{{COMMIT}}/node_modules/semver-umd/lib/semver-umd.js"> <link rel="prefetch" href="./static-{{COMMIT}}/node_modules/semver-umd/lib/semver-umd.js">

10
src/common/util.ts Normal file
View File

@@ -0,0 +1,10 @@
/**
* Split a string up to the delimiter. If the delimiter doesn't exist the first
* item will have all the text and the second item will be an empty string.
*/
export const split = (str: string, delimiter: string): [string, string] => {
const index = str.indexOf(delimiter);
return index !== -1
? [str.substring(0, index).trim(), str.substring(index + 1)]
: [str, ""];
};

View File

@@ -90,7 +90,7 @@ const startVscode = async (): Promise<void | void[]> => {
basePath: args["base-path"], basePath: args["base-path"],
cert: args.cert, cert: args.cert,
certKey: args["cert-key"], certKey: args["cert-key"],
folderUri: extra.length > 1 ? extra[extra.length - 1] : undefined, openUri: extra.length > 1 ? extra[extra.length - 1] : undefined,
host: args.host, host: args.host,
password: process.env.PASSWORD, password: process.env.PASSWORD,
}; };
@@ -196,14 +196,17 @@ const startCli = (): boolean | Promise<void> => {
export class WrapperProcess { export class WrapperProcess {
private process?: cp.ChildProcess; private process?: cp.ChildProcess;
private started?: Promise<void>; private started?: Promise<void>;
private currentVersion = product.codeServerVersion;
public constructor() { public constructor() {
ipcMain.onMessage(async (message) => { ipcMain.onMessage(async (message) => {
switch (message) { switch (message.type) {
case "relaunch": case "relaunch":
logger.info("Relaunching..."); logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`);
this.currentVersion = message.version;
this.started = undefined; this.started = undefined;
if (this.process) { if (this.process) {
this.process.removeAllListeners();
this.process.kill(); this.process.kill();
} }
try { try {
@@ -223,17 +226,26 @@ export class WrapperProcess {
public start(): Promise<void> { public start(): Promise<void> {
if (!this.started) { if (!this.started) {
const child = this.spawn(); const child = this.spawn();
this.started = ipcMain.handshake(child); this.started = ipcMain.handshake(child).then(() => {
child.once("exit", (code) => exit(code!));
});
this.process = child; this.process = child;
} }
return this.started; return this.started;
} }
private spawn(): cp.ChildProcess { private spawn(): cp.ChildProcess {
return cp.spawn(process.argv[0], process.argv.slice(1), { // If we're using loose files then we need to specify the path. If we're in
// the binary we need to let the binary determine the path (via nbin) since
// it could be different between binaries which presents a problem when
// upgrading (different version numbers or different staging directories).
const isBinary = (global as any).NBIN_LOADED;
return cp.spawn(process.argv[0], process.argv.slice(isBinary ? 2 : 1), {
env: { env: {
...process.env, ...process.env,
LAUNCH_VSCODE: "true", LAUNCH_VSCODE: "true",
NBIN_BYPASS: undefined,
VSCODE_PARENT_PID: process.pid.toString(),
}, },
stdio: ["inherit", "inherit", "inherit", "ipc"], stdio: ["inherit", "inherit", "inherit", "ipc"],
}); });
@@ -254,6 +266,20 @@ process.exit = function (code?: number) {
console.warn(err.stack); console.warn(err.stack);
} as (code?: number) => never; } as (code?: number) => never;
// Copy the extension host behavior of killing oneself if the parent dies. This
// also exists in bootstrap-fork.js but spawning with that won't work because we
// override process.exit.
if (typeof process.env.VSCODE_PARENT_PID !== "undefined") {
const parentPid = parseInt(process.env.VSCODE_PARENT_PID, 10);
setInterval(() => {
try {
process.kill(parentPid, 0); // Throws an exception if the process doesn't exist anymore.
} catch (e) {
exit();
}
}, 5000);
}
// It's possible that the pipe has closed (for example if you run code-server // It's possible that the pipe has closed (for example if you run code-server
// --version | head -1). Assume that means we're done. // --version | head -1). Assume that means we're done.
if (!process.stdout.isTTY) { if (!process.stdout.isTTY) {

View File

@@ -6,7 +6,12 @@ enum ControlMessage {
okFromChild = "ok<", okFromChild = "ok<",
} }
export type Message = "relaunch"; interface RelaunchMessage {
type: "relaunch";
version: string;
}
export type Message = RelaunchMessage;
class IpcMain { class IpcMain {
protected readonly _onMessage = new Emitter<Message>(); protected readonly _onMessage = new Emitter<Message>();
@@ -41,11 +46,15 @@ class IpcMain {
}); });
} }
public relaunch(): void { public relaunch(version: string): void {
this.send({ type: "relaunch", version });
}
private send(message: Message): void {
if (!process.send) { if (!process.send) {
throw new Error("Not a child process with IPC enabled"); throw new Error("Not a child process with IPC enabled");
} }
process.send("relaunch"); process.send(message);
} }
} }

View File

@@ -56,13 +56,14 @@ import { resolveCommonProperties } from "vs/platform/telemetry/node/commonProper
import { UpdateChannel } from "vs/platform/update/electron-main/updateIpc"; import { UpdateChannel } from "vs/platform/update/electron-main/updateIpc";
import { INodeProxyService, NodeProxyChannel } from "vs/server/src/common/nodeProxy"; import { INodeProxyService, NodeProxyChannel } from "vs/server/src/common/nodeProxy";
import { TelemetryChannel } from "vs/server/src/common/telemetry"; import { TelemetryChannel } from "vs/server/src/common/telemetry";
import { split } from "vs/server/src/common/util";
import { ExtensionEnvironmentChannel, FileProviderChannel, NodeProxyService } from "vs/server/src/node/channel"; import { ExtensionEnvironmentChannel, FileProviderChannel, NodeProxyService } from "vs/server/src/node/channel";
import { Connection, ExtensionHostConnection, ManagementConnection } from "vs/server/src/node/connection"; import { Connection, ExtensionHostConnection, ManagementConnection } from "vs/server/src/node/connection";
import { TelemetryClient } from "vs/server/src/node/insights"; import { TelemetryClient } from "vs/server/src/node/insights";
import { getLocaleFromConfig, getNlsConfiguration } from "vs/server/src/node/nls"; import { getLocaleFromConfig, getNlsConfiguration } from "vs/server/src/node/nls";
import { Protocol } from "vs/server/src/node/protocol"; import { Protocol } from "vs/server/src/node/protocol";
import { UpdateService } from "vs/server/src/node/update"; import { UpdateService } from "vs/server/src/node/update";
import { AuthType, getMediaMime, getUriTransformer, localRequire, tmpdir } from "vs/server/src/node/util"; import { AuthType, getMediaMime, getUriTransformer, hash, localRequire, tmpdir } from "vs/server/src/node/util";
import { RemoteExtensionLogFileName } from "vs/workbench/services/remote/common/remoteAgentService"; import { RemoteExtensionLogFileName } from "vs/workbench/services/remote/common/remoteAgentService";
import { IWorkbenchConstructionOptions } from "vs/workbench/workbench.web.api"; import { IWorkbenchConstructionOptions } from "vs/workbench/workbench.web.api";
@@ -100,6 +101,10 @@ export interface LoginPayload {
password?: string; password?: string;
} }
export interface AuthPayload {
key?: string[];
}
export class HttpError extends Error { export class HttpError extends Error {
public constructor(message: string, public readonly code: number) { public constructor(message: string, public readonly code: number) {
super(message); super(message);
@@ -115,7 +120,7 @@ export interface ServerOptions {
readonly connectionToken?: string; readonly connectionToken?: string;
readonly cert?: string; readonly cert?: string;
readonly certKey?: string; readonly certKey?: string;
readonly folderUri?: string; readonly openUri?: string;
readonly host?: string; readonly host?: string;
readonly password?: string; readonly password?: string;
readonly port?: number; readonly port?: number;
@@ -136,6 +141,7 @@ export abstract class Server {
host: options.auth === "password" && options.cert ? "0.0.0.0" : "localhost", host: options.auth === "password" && options.cert ? "0.0.0.0" : "localhost",
...options, ...options,
basePath: options.basePath ? options.basePath.replace(/\/+$/, "") : "", basePath: options.basePath ? options.basePath.replace(/\/+$/, "") : "",
password: options.password ? hash(options.password) : undefined,
}; };
this.protocol = this.options.cert ? "https" : "http"; this.protocol = this.options.cert ? "https" : "http";
if (this.protocol === "https") { if (this.protocol === "https") {
@@ -212,8 +218,8 @@ export abstract class Server {
} }
protected withBase(request: http.IncomingMessage, path: string): string { protected withBase(request: http.IncomingMessage, path: string): string {
const split = request.url ? request.url.split("?", 2) : []; const [, query] = request.url ? split(request.url, "?") : [];
return `${this.protocol}://${request.headers.host}${this.options.basePath}${path}${split.length === 2 ? `?${split[1]}` : ""}`; return `${this.protocol}://${request.headers.host}${this.options.basePath}${path}${query ? `?${query}` : ""}`;
} }
private isAllowedRequestPath(path: string): boolean { private isAllowedRequestPath(path: string): boolean {
@@ -356,16 +362,25 @@ export abstract class Server {
} }
private async tryLogin(request: http.IncomingMessage): Promise<Response> { private async tryLogin(request: http.IncomingMessage): Promise<Response> {
if (this.authenticate(request) && (request.method === "GET" || request.method === "POST")) { const redirect = (password: string | true) => {
return { redirect: "/" }; return {
redirect: "/",
headers: typeof password === "string"
? { "Set-Cookie": `key=${password}; Path=${this.options.basePath || "/"}; HttpOnly; SameSite=strict` }
: {},
};
};
const providedPassword = this.authenticate(request);
if (providedPassword && (request.method === "GET" || request.method === "POST")) {
return redirect(providedPassword);
} }
if (request.method === "POST") { if (request.method === "POST") {
const data = await this.getData<LoginPayload>(request); const data = await this.getData<LoginPayload>(request);
if (this.authenticate(request, data)) { const password = this.authenticate(request, {
return { key: typeof data.password === "string" ? [hash(data.password)] : undefined,
redirect: "/", });
headers: { "Set-Cookie": `password=${data.password}` } if (password) {
}; return redirect(password);
} }
console.error("Failed login attempt", JSON.stringify({ console.error("Failed login attempt", JSON.stringify({
xForwardedFor: request.headers["x-forwarded-for"], xForwardedFor: request.headers["x-forwarded-for"],
@@ -425,23 +440,33 @@ export abstract class Server {
: Promise.resolve({} as T); : Promise.resolve({} as T);
} }
private authenticate(request: http.IncomingMessage, payload?: LoginPayload): boolean { private authenticate(request: http.IncomingMessage, payload?: AuthPayload): string | boolean {
if (this.options.auth !== "password") { if (this.options.auth === "none") {
return true; return true;
} }
const safeCompare = localRequire<typeof import("safe-compare")>("safe-compare/index"); const safeCompare = localRequire<typeof import("safe-compare")>("safe-compare/index");
if (typeof payload === "undefined") { if (typeof payload === "undefined") {
payload = this.parseCookies<LoginPayload>(request); payload = this.parseCookies<AuthPayload>(request);
} }
return !!this.options.password && safeCompare(payload.password || "", this.options.password); if (this.options.password && payload.key) {
for (let i = 0; i < payload.key.length; ++i) {
if (safeCompare(payload.key[i], this.options.password)) {
return payload.key[i];
}
}
}
return false;
} }
private parseCookies<T extends object>(request: http.IncomingMessage): T { private parseCookies<T extends object>(request: http.IncomingMessage): T {
const cookies: { [key: string]: string } = {}; const cookies: { [key: string]: string[] } = {};
if (request.headers.cookie) { if (request.headers.cookie) {
request.headers.cookie.split(";").forEach((keyValue) => { request.headers.cookie.split(";").forEach((keyValue) => {
const [key, value] = keyValue.split("=", 2); const [key, value] = split(keyValue, "=");
cookies[key.trim()] = decodeURI(value); if (!cookies[key]) {
cookies[key] = [];
}
cookies[key].push(decodeURI(value));
}); });
} }
return cookies as T; return cookies as T;
@@ -474,6 +499,9 @@ export class MainServer extends Server {
private readonly proxyTimeout = 5000; private readonly proxyTimeout = 5000;
private settings: Settings = {}; private settings: Settings = {};
private heartbeatTimer?: NodeJS.Timeout;
private heartbeatInterval = 60000;
private lastHeartbeat = 0;
public constructor(options: ServerOptions, args: ParsedArgs) { public constructor(options: ServerOptions, args: ParsedArgs) {
super(options); super(options);
@@ -491,6 +519,7 @@ export class MainServer extends Server {
} }
protected async handleWebSocket(socket: net.Socket, parsedUrl: url.UrlWithParsedQuery): Promise<void> { protected async handleWebSocket(socket: net.Socket, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
this.heartbeat();
if (!parsedUrl.query.reconnectionToken) { if (!parsedUrl.query.reconnectionToken) {
throw new Error("Reconnection token is missing from query parameters"); throw new Error("Reconnection token is missing from query parameters");
} }
@@ -514,12 +543,13 @@ export class MainServer extends Server {
parsedUrl: url.UrlWithParsedQuery, parsedUrl: url.UrlWithParsedQuery,
request: http.IncomingMessage, request: http.IncomingMessage,
): Promise<Response> { ): Promise<Response> {
this.heartbeat();
switch (base) { switch (base) {
case "/": return this.getRoot(request, parsedUrl); case "/": return this.getRoot(request, parsedUrl);
case "/resource": case "/resource":
case "/vscode-remote-resource": case "/vscode-remote-resource":
if (typeof parsedUrl.query.path === "string") { if (typeof parsedUrl.query.path === "string") {
return this.getResource(parsedUrl.query.path); return this.getAnyResource(parsedUrl.query.path);
} }
break; break;
case "/tar": case "/tar":
@@ -546,9 +576,9 @@ export class MainServer extends Server {
util.promisify(fs.readFile)(filePath, "utf8"), util.promisify(fs.readFile)(filePath, "utf8"),
this.getFirstValidPath([ this.getFirstValidPath([
{ path: parsedUrl.query.workspace, workspace: true }, { path: parsedUrl.query.workspace, workspace: true },
{ path: parsedUrl.query.folder }, { path: parsedUrl.query.folder, workspace: false },
(await this.readSettings()).lastVisited, (await this.readSettings()).lastVisited,
{ path: this.options.folderUri } { path: this.options.openUri }
]), ]),
this.servicesPromise, this.servicesPromise,
]); ]);
@@ -592,7 +622,9 @@ export class MainServer extends Server {
} }
/** /**
* Choose the first valid path. * 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.
*/ */
private async getFirstValidPath(startPaths: Array<StartPath | undefined>): Promise<{ uri: URI, workspace?: boolean} | undefined> { private async getFirstValidPath(startPaths: Array<StartPath | undefined>): Promise<{ uri: URI, workspace?: boolean} | undefined> {
const logger = this.services.get(ILogService) as ILogService; const logger = this.services.get(ILogService) as ILogService;
@@ -607,9 +639,8 @@ export class MainServer extends Server {
const uri = URI.file(sanitizeFilePath(paths[j], cwd)); const uri = URI.file(sanitizeFilePath(paths[j], cwd));
try { try {
const stat = await util.promisify(fs.stat)(uri.fsPath); const stat = await util.promisify(fs.stat)(uri.fsPath);
// Workspace must be a file. if (typeof startPath.workspace === "undefined" || startPath.workspace !== stat.isDirectory()) {
if (!!startPath.workspace !== stat.isDirectory()) { return { uri, workspace: !stat.isDirectory() };
return { uri, workspace: startPath.workspace };
} }
} catch (error) { } catch (error) {
logger.warn(error.message); logger.warn(error.message);
@@ -876,4 +907,48 @@ export class MainServer extends Server {
(this.services.get(ILogService) as ILogService).warn(error.message); (this.services.get(ILogService) as ILogService).warn(error.message);
} }
} }
/**
* Return the file path for the heartbeat file.
*/
private get heartbeatPath(): string {
const environment = this.services.get(IEnvironmentService) as IEnvironmentService;
return path.join(environment.userDataPath, "heartbeat");
}
/**
* Return all online connections regardless of type.
*/
private get onlineConnections(): Connection[] {
const online = <Connection[]>[];
this.connections.forEach((connections) => {
connections.forEach((connection) => {
if (typeof connection.offline === "undefined") {
online.push(connection);
}
});
});
return online;
}
/**
* Write to the heartbeat file if we haven't already done so within the
* timeout and start or reset a timer that keeps running as long as there are
* active connections. Failures are logged as warnings.
*/
private heartbeat(): void {
const now = Date.now();
if (now - this.lastHeartbeat >= this.heartbeatInterval) {
util.promisify(fs.writeFile)(this.heartbeatPath, "").catch((error) => {
(this.services.get(ILogService) as ILogService).warn(error.message);
});
this.lastHeartbeat = now;
clearTimeout(this.heartbeatTimer!); // We can clear undefined so ! is fine.
this.heartbeatTimer = setTimeout(() => {
if (this.onlineConnections.length > 0) {
this.heartbeat();
}
}, this.heartbeatInterval);
}
}
} }

View File

@@ -13,7 +13,7 @@ import { IFileService } from "vs/platform/files/common/files";
import { ILogService } from "vs/platform/log/common/log"; import { ILogService } from "vs/platform/log/common/log";
import product from "vs/platform/product/common/product"; import product from "vs/platform/product/common/product";
import { asJson, IRequestService } from "vs/platform/request/common/request"; import { asJson, IRequestService } from "vs/platform/request/common/request";
import { AvailableForDownload, State, UpdateType } from "vs/platform/update/common/update"; import { AvailableForDownload, State, UpdateType, StateType } from "vs/platform/update/common/update";
import { AbstractUpdateService } from "vs/platform/update/electron-main/abstractUpdateService"; import { AbstractUpdateService } from "vs/platform/update/electron-main/abstractUpdateService";
import { ipcMain } from "vs/server/src/node/ipc"; import { ipcMain } from "vs/server/src/node/ipc";
import { extract } from "vs/server/src/node/marketplace"; import { extract } from "vs/server/src/node/marketplace";
@@ -37,6 +37,9 @@ export class UpdateService extends AbstractUpdateService {
super(null, configurationService, environmentService, requestService, logService); super(null, configurationService, environmentService, requestService, logService);
} }
/**
* Return true if the currently installed version is the latest.
*/
public async isLatestVersion(latest?: IUpdate | null): Promise<boolean | undefined> { public async isLatestVersion(latest?: IUpdate | null): Promise<boolean | undefined> {
if (!latest) { if (!latest) {
latest = await this.getLatestVersion(); latest = await this.getLatestVersion();
@@ -44,8 +47,12 @@ export class UpdateService extends AbstractUpdateService {
if (latest) { if (latest) {
const latestMajor = parseInt(latest.name); const latestMajor = parseInt(latest.name);
const currentMajor = parseInt(product.codeServerVersion); const currentMajor = parseInt(product.codeServerVersion);
return !isNaN(latestMajor) && !isNaN(currentMajor) && // If these are invalid versions we can't compare meaningfully.
currentMajor <= latestMajor && latest.name === product.codeServerVersion; return isNaN(latestMajor) || isNaN(currentMajor) ||
// This can happen when there is a pre-release for a new major version.
currentMajor > latestMajor ||
// Otherwise assume that if it's not the same then we're out of date.
latest.name === product.codeServerVersion;
} }
return true; return true;
} }
@@ -55,14 +62,16 @@ export class UpdateService extends AbstractUpdateService {
} }
public async doQuitAndInstall(): Promise<void> { public async doQuitAndInstall(): Promise<void> {
ipcMain.relaunch(); if (this.state.type === StateType.Ready) {
ipcMain.relaunch(this.state.update.version);
}
} }
protected async doCheckForUpdates(context: any): Promise<void> { protected async doCheckForUpdates(context: any): Promise<void> {
this.setState(State.CheckingForUpdates(context)); this.setState(State.CheckingForUpdates(context));
try { try {
const update = await this.getLatestVersion(); const update = await this.getLatestVersion();
if (!update || this.isLatestVersion(update)) { if (!update || await this.isLatestVersion(update)) {
this.setState(State.Idle(UpdateType.Archive)); this.setState(State.Idle(UpdateType.Archive));
} else { } else {
this.setState(State.AvailableForDownload({ this.setState(State.AvailableForDownload({

View File

@@ -4,22 +4,19 @@ module.exports = (remoteAuthority) => {
return { return {
transformIncoming: (uri) => { transformIncoming: (uri) => {
switch (uri.scheme) { switch (uri.scheme) {
case "code-server": return { scheme: "file", path: uri.path }; case "vscode-remote": return { scheme: "file", path: uri.path };
case "file": return { scheme: "code-server", path: uri.path };
default: return uri; default: return uri;
} }
}, },
transformOutgoing: (uri) => { transformOutgoing: (uri) => {
switch (uri.scheme) { switch (uri.scheme) {
case "code-server": return { scheme: "file", path: uri.path }; case "file": return { scheme: "vscode-remote", authority: remoteAuthority, path: uri.path };
case "file": return { scheme: "code-server", authority: remoteAuthority, path: uri.path };
default: return uri; default: return uri;
} }
}, },
transformOutgoingScheme: (scheme) => { transformOutgoingScheme: (scheme) => {
switch (scheme) { switch (scheme) {
case "code-server": return "file"; case "file": return "vscode-remote";
case "file": return "code-server";
default: return scheme; default: return scheme;
} }
}, },

View File

@@ -67,6 +67,10 @@ export const generatePassword = async (length: number = 24): Promise<string> =>
return buffer.toString("hex").substring(0, length); return buffer.toString("hex").substring(0, length);
}; };
export const hash = (str: string): string => {
return crypto.createHash("sha256").update(str).digest("hex");
};
export const getMediaMime = (filePath?: string): string => { export const getMediaMime = (filePath?: string): string => {
return filePath && (vsGetMediaMime(filePath) || (<{[index: string]: string}>{ return filePath && (vsGetMediaMime(filePath) || (<{[index: string]: string}>{
".css": "text/css", ".css": "text/css",

View File

@@ -7,10 +7,10 @@
resolved "https://registry.yarnpkg.com/@coder/logger/-/logger-1.1.8.tgz#416a7221d84161ee35eca9cfa93ba9377639b4ee" resolved "https://registry.yarnpkg.com/@coder/logger/-/logger-1.1.8.tgz#416a7221d84161ee35eca9cfa93ba9377639b4ee"
integrity sha512-NJDC4rZTx0deVYqAxZtJWACq3IrVR59BjFeZebO3i7OfTZZMkkbLsGsCFMnJd5KnX6KjnvvFq4XXtwJ9yf8/YQ== integrity sha512-NJDC4rZTx0deVYqAxZtJWACq3IrVR59BjFeZebO3i7OfTZZMkkbLsGsCFMnJd5KnX6KjnvvFq4XXtwJ9yf8/YQ==
"@coder/nbin@^1.2.2": "@coder/nbin@^1.2.3":
version "1.2.2" version "1.2.3"
resolved "https://registry.yarnpkg.com/@coder/nbin/-/nbin-1.2.2.tgz#c5f9aaa2a0e84c2a13a4cce895547efbd66730b7" resolved "https://registry.yarnpkg.com/@coder/nbin/-/nbin-1.2.3.tgz#793061abc7e1f7e0a9d1b9f854fa8f4121ed4e90"
integrity sha512-1Z6aYBRZRY1AQ2xp0jmoz+TXR8M4WaHa9FfVkOPej0KPJjYtEp18I+/6CmffDtBLxSnIai0rc+AA0VhbjCN/rg== integrity sha512-JGJhkaqCrAF9hQ8e7m29/gbbKqDrBAOJCdjNZv9LKF+67lmHUoJ2QS+eHN+KOtpO4EJeEs4/uq7LSEdT+g3t5w==
dependencies: dependencies:
"@coder/logger" "^1.1.8" "@coder/logger" "^1.1.8"
fs-extra "^7.0.1" fs-extra "^7.0.1"
@@ -1256,9 +1256,9 @@ minizlib@^1.1.1:
minipass "^2.2.1" minipass "^2.2.1"
mixin-deep@^1.2.0: mixin-deep@^1.2.0:
version "1.3.1" version "1.3.2"
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ== integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
dependencies: dependencies:
for-in "^1.0.2" for-in "^1.0.2"
is-extendable "^1.0.1" is-extendable "^1.0.1"