Compare commits

..

13 Commits

Author SHA1 Message Date
Asher
e14362f322 Pass along Node options 2019-11-14 17:20:23 -06:00
Asher
917aa48072 Update enterprise link
Fixes #1172.
2019-11-14 11:16:08 -06:00
Asher
938c6ef829 Update fail2ban configuration
Fixes #1177.
2019-11-14 11:14:27 -06:00
Sandro
0add01d383 Delete apt lists from final image (#1174) 2019-11-14 11:12:21 -06:00
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
14 changed files with 146 additions and 50 deletions

View File

@@ -33,7 +33,8 @@ RUN apt-get update && apt-get install -y \
dumb-init \
vim \
curl \
wget
wget \
&& rm -rf /var/lib/apt/lists/*
RUN locale-gen en_US.UTF-8
# We cannot use update-locale because docker will not use the env variables

View File

@@ -60,14 +60,14 @@ arguments when launching code-server with Docker. See
### Build
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.
```shell
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.
# 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.
```
@@ -135,7 +135,7 @@ data collected to improve code-server.
### Development
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.
```shell
@@ -155,8 +155,7 @@ yarn start
```
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
`vscode-ripgrep`.
`npm rebuild` in the VS Code directory.
### Upgrading VS Code
@@ -171,7 +170,6 @@ directory.
Our changes include:
- Change the remote schema to `code-server`.
- Allow multiple extension directories (both user and built-in).
- 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).
@@ -190,8 +188,8 @@ Our changes include:
## Enterprise
Visit [our enterprise page](https://coder.com/enterprise) for more information
about our enterprise offering.
Visit [our enterprise page](https://coder.com) for more information about our
enterprise offering.
## Commercialization

View File

@@ -2,11 +2,11 @@
[Definition]
failregex = ^INFO\s+Failed login attempt\s+{\"password\":\"(\\.|[^"])*\",\"remoteAddress\":\"<HOST>\"
failregex = ^Failed login attempt\s+{\"remoteAddress\":\"<HOST>\"
# Use this instead for proxies (ensure the proxy is configured to send the
# X-Forwarded-For header).
# failregex = ^INFO\s+Failed login attempt\s+{\"password\":\"(\\.|[^"])*\",\"xForwardedFor\":\"<HOST>\"
# failregex = ^Failed login attempt\s+{\"xForwardedFor\":\"<HOST>\"
ignoreregex =

View File

@@ -30,6 +30,6 @@ accessible from the internet (use localhost or block it in your firewall).
## Fail2Ban
Fail2Ban allows for automatically banning and logging repeated failed
authentication attempts for many applications through regex filters. A working
filter for code-server can be found in `./code-server.fail2ban.conf`. Once this
filter for code-server can be found in `./examples/fail2ban.conf`. Once this
is installed and configured correctly, repeated failed login attempts should
automatically be banned from connecting to your server.

View File

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

View File

@@ -812,6 +812,21 @@ index 3bdfa1a79f..ded21cf9c6 100644
// register services that only throw errors
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
index 99394090da..4891e0fece 100644
--- a/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts

View File

@@ -1,8 +1,11 @@
import { Emitter } from "vs/base/common/event";
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 { ServiceCollection } from "vs/platform/instantiation/common/serviceCollection";
import { ILocalizationsService } from "vs/platform/localizations/common/localizations";
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 { coderApi, vscodeApi } from "vs/server/src/browser/api";
@@ -22,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 {
private readonly _onClose = new Emitter<void>();
public readonly onClose = this._onClose.event;

View File

@@ -40,6 +40,7 @@
<link rel="manifest" href="./manifest.json">
<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">
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- Prefetch to avoid waterfall -->
<link rel="prefetch" href="./static-{{COMMIT}}/node_modules/semver-umd/lib/semver-umd.js">

View File

@@ -47,7 +47,6 @@ const getArgs = (): Args => {
case "wait":
case "disable-gpu":
// TODO: pretty sure these don't work but not 100%.
case "max-memory":
case "prof-startup":
case "inspect-extensions":
case "inspect-brk-extensions":
@@ -82,8 +81,7 @@ const getArgs = (): Args => {
return validatePaths(args);
};
const startVscode = async (): Promise<void | void[]> => {
const args = getArgs();
const startVscode = async (args: Args): Promise<void | void[]> => {
const extra = args["_"] || [];
const options = {
auth: args.auth || AuthType.Password,
@@ -155,8 +153,7 @@ const startVscode = async (): Promise<void | void[]> => {
}
};
const startCli = (): boolean | Promise<void> => {
const args = getArgs();
const startCli = (args: Args): boolean | Promise<void> => {
if (args.help) {
const executable = `${product.applicationName}${os.platform() === "win32" ? ".exe" : ""}`;
console.log(buildHelpMessage(product.nameLong, executable, product.codeServerVersion, OPTIONS, false));
@@ -196,12 +193,14 @@ const startCli = (): boolean | Promise<void> => {
export class WrapperProcess {
private process?: cp.ChildProcess;
private started?: Promise<void>;
private currentVersion = product.codeServerVersion;
public constructor() {
public constructor(private readonly args: Args) {
ipcMain.onMessage(async (message) => {
switch (message) {
switch (message.type) {
case "relaunch":
logger.info("Relaunching...");
logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`);
this.currentVersion = message.version;
this.started = undefined;
if (this.process) {
this.process.removeAllListeners();
@@ -233,11 +232,26 @@ export class WrapperProcess {
}
private spawn(): cp.ChildProcess {
return cp.spawn(process.argv[0], process.argv.slice(1), {
// Flags to pass along to the Node binary. We use the environment variable
// since otherwise the code-server binary will swallow them.
const maxMemory = this.args["max-memory"] || 2048;
let nodeOptions = `${process.env.NODE_OPTIONS || ""} ${this.args["js-flags"] || ""}`;
if (!/max_old_space_size=(\d+)/g.exec(nodeOptions)) {
nodeOptions += ` --max_old_space_size=${maxMemory}`;
}
// 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: {
...process.env,
LAUNCH_VSCODE: "true",
NBIN_BYPASS: undefined,
VSCODE_PARENT_PID: process.pid.toString(),
NODE_OPTIONS: nodeOptions,
},
stdio: ["inherit", "inherit", "inherit", "ipc"],
});
@@ -245,11 +259,12 @@ export class WrapperProcess {
}
const main = async(): Promise<boolean | void | void[]> => {
const args = getArgs();
if (process.env.LAUNCH_VSCODE) {
await ipcMain.handshake();
return startVscode();
return startVscode(args);
}
return startCli() || new WrapperProcess().start();
return startCli(args) || new WrapperProcess(args).start();
};
const exit = process.exit;

View File

@@ -6,7 +6,12 @@ enum ControlMessage {
okFromChild = "ok<",
}
export type Message = "relaunch";
interface RelaunchMessage {
type: "relaunch";
version: string;
}
export type Message = RelaunchMessage;
class IpcMain {
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) {
throw new Error("Not a child process with IPC enabled");
}
process.send("relaunch");
process.send(message);
}
}

View File

@@ -63,7 +63,7 @@ import { TelemetryClient } from "vs/server/src/node/insights";
import { getLocaleFromConfig, getNlsConfiguration } from "vs/server/src/node/nls";
import { Protocol } from "vs/server/src/node/protocol";
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 { IWorkbenchConstructionOptions } from "vs/workbench/workbench.web.api";
@@ -101,6 +101,10 @@ export interface LoginPayload {
password?: string;
}
export interface AuthPayload {
key?: string[];
}
export class HttpError extends Error {
public constructor(message: string, public readonly code: number) {
super(message);
@@ -137,6 +141,7 @@ export abstract class Server {
host: options.auth === "password" && options.cert ? "0.0.0.0" : "localhost",
...options,
basePath: options.basePath ? options.basePath.replace(/\/+$/, "") : "",
password: options.password ? hash(options.password) : undefined,
};
this.protocol = this.options.cert ? "https" : "http";
if (this.protocol === "https") {
@@ -357,16 +362,25 @@ export abstract class Server {
}
private async tryLogin(request: http.IncomingMessage): Promise<Response> {
if (this.authenticate(request) && (request.method === "GET" || request.method === "POST")) {
return { redirect: "/" };
const redirect = (password: string | true) => {
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") {
const data = await this.getData<LoginPayload>(request);
if (this.authenticate(request, data)) {
return {
redirect: "/",
headers: { "Set-Cookie": `password=${data.password}` }
};
const password = this.authenticate(request, {
key: typeof data.password === "string" ? [hash(data.password)] : undefined,
});
if (password) {
return redirect(password);
}
console.error("Failed login attempt", JSON.stringify({
xForwardedFor: request.headers["x-forwarded-for"],
@@ -426,23 +440,33 @@ export abstract class Server {
: Promise.resolve({} as T);
}
private authenticate(request: http.IncomingMessage, payload?: LoginPayload): boolean {
if (this.options.auth !== "password") {
private authenticate(request: http.IncomingMessage, payload?: AuthPayload): string | boolean {
if (this.options.auth === "none") {
return true;
}
const safeCompare = localRequire<typeof import("safe-compare")>("safe-compare/index");
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 {
const cookies: { [key: string]: string } = {};
const cookies: { [key: string]: string[] } = {};
if (request.headers.cookie) {
request.headers.cookie.split(";").forEach((keyValue) => {
const [key, value] = split(keyValue, "=");
cookies[key] = decodeURI(value);
if (!cookies[key]) {
cookies[key] = [];
}
cookies[key].push(decodeURI(value));
});
}
return cookies as T;

View File

@@ -13,7 +13,7 @@ import { IFileService } from "vs/platform/files/common/files";
import { ILogService } from "vs/platform/log/common/log";
import product from "vs/platform/product/common/product";
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 { ipcMain } from "vs/server/src/node/ipc";
import { extract } from "vs/server/src/node/marketplace";
@@ -37,6 +37,9 @@ export class UpdateService extends AbstractUpdateService {
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> {
if (!latest) {
latest = await this.getLatestVersion();
@@ -44,8 +47,12 @@ export class UpdateService extends AbstractUpdateService {
if (latest) {
const latestMajor = parseInt(latest.name);
const currentMajor = parseInt(product.codeServerVersion);
return !isNaN(latestMajor) && !isNaN(currentMajor) &&
currentMajor <= latestMajor && latest.name === product.codeServerVersion;
// If these are invalid versions we can't compare meaningfully.
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;
}
@@ -55,14 +62,16 @@ export class UpdateService extends AbstractUpdateService {
}
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> {
this.setState(State.CheckingForUpdates(context));
try {
const update = await this.getLatestVersion();
if (!update || this.isLatestVersion(update)) {
if (!update || await this.isLatestVersion(update)) {
this.setState(State.Idle(UpdateType.Archive));
} else {
this.setState(State.AvailableForDownload({

View File

@@ -67,6 +67,10 @@ export const generatePassword = async (length: number = 24): Promise<string> =>
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 => {
return filePath && (vsGetMediaMime(filePath) || (<{[index: string]: string}>{
".css": "text/css",

View File

@@ -7,10 +7,10 @@
resolved "https://registry.yarnpkg.com/@coder/logger/-/logger-1.1.8.tgz#416a7221d84161ee35eca9cfa93ba9377639b4ee"
integrity sha512-NJDC4rZTx0deVYqAxZtJWACq3IrVR59BjFeZebO3i7OfTZZMkkbLsGsCFMnJd5KnX6KjnvvFq4XXtwJ9yf8/YQ==
"@coder/nbin@^1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@coder/nbin/-/nbin-1.2.2.tgz#c5f9aaa2a0e84c2a13a4cce895547efbd66730b7"
integrity sha512-1Z6aYBRZRY1AQ2xp0jmoz+TXR8M4WaHa9FfVkOPej0KPJjYtEp18I+/6CmffDtBLxSnIai0rc+AA0VhbjCN/rg==
"@coder/nbin@^1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@coder/nbin/-/nbin-1.2.3.tgz#793061abc7e1f7e0a9d1b9f854fa8f4121ed4e90"
integrity sha512-JGJhkaqCrAF9hQ8e7m29/gbbKqDrBAOJCdjNZv9LKF+67lmHUoJ2QS+eHN+KOtpO4EJeEs4/uq7LSEdT+g3t5w==
dependencies:
"@coder/logger" "^1.1.8"
fs-extra "^7.0.1"