Skip to content

Commit f0ea2d6

Browse files
committed
Split auth away from default server
1 parent 3c22b7d commit f0ea2d6

File tree

11 files changed

+635
-1098
lines changed

11 files changed

+635
-1098
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@
6161
"@vscode/vscode-languagedetection": "1.0.15",
6262
"applicationinsights": "1.0.8",
6363
"chokidar": "3.5.1",
64-
"express": "^4.17.1",
6564
"graceful-fs": "4.2.6",
6665
"http-proxy-agent": "^2.1.0",
6766
"https-proxy-agent": "^2.2.3",

src/vs/server/browser/workbench/login.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,16 +161,16 @@
161161
<div class="card-box">
162162
<div class="header">
163163
<h1 class="main">Welcome to VSCode Server</h1>
164-
<div class="sub">Please log in below.</div>
164+
<div class="sub">Please log in below. If you haven't set a password on startup, an automatically generated password will be visible in the server logs.</div>
165165
</div>
166166
<div class="content">
167167
<form class="login-form" action="/login" method="post">
168168
<input class="user" type="text" autocomplete="username" />
169169
<input id="base" type="hidden" name="base" value="/" />
170170
<div class="field">
171-
<input required autofocus class="password" type="password" placeholder="PASSWORD" name="password"
171+
<input required autofocus class="password" type="password" placeholder="Enter password..." name="password"
172172
autocomplete="current-password" />
173-
<input class="submit -button" value="SUBMIT" type="submit" />
173+
<input class="submit -button" value="Login" type="submit" />
174174
</div>
175175
</form>
176176
</div>

src/vs/server/node/args.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Gitpod. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
15
import * as path from 'path';
26
import * as os from 'os';
37
import { URI } from 'vs/base/common/uri';
@@ -8,13 +12,11 @@ import product from 'vs/platform/product/common/product';
812
export interface ServerParsedArgs extends NativeParsedArgs {
913
port?: string
1014
password?: string
11-
hashedPassword?: string
1215
}
1316
const SERVER_OPTIONS: OptionDescriptions<Required<ServerParsedArgs>> = {
1417
...OPTIONS,
1518
port: { type: 'string' },
16-
password: { type: 'string' },
17-
hashedPassword: { type: 'string' }
19+
password: { type: 'string' }
1820
};
1921

2022
export const devMode = !!process.env['VSCODE_DEV'];

src/vs/server/node/auth-http.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Gitpod. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
import * as path from 'path';
6+
import * as http from 'http';
7+
import { ILogService } from 'vs/platform/log/common/log';
8+
import { parse } from 'querystring';
9+
import { args } from 'vs/server/node/args';
10+
import { authenticated, generateAndSetPassword, handlePasswordValidation } from 'vs/server/node/auth';
11+
import { APP_ROOT, serveError, serveFile } from 'vs/server/node/server.main';
12+
13+
const LOGIN = path.join(APP_ROOT, 'out', 'vs', 'server', 'browser', 'workbench', 'login.html');
14+
15+
export async function handleServerRequest(pathname: string | null, req: http.IncomingMessage, res: http.ServerResponse, logService: ILogService): Promise<boolean> {
16+
if (args.password === undefined) {
17+
await generateAndSetPassword(logService);
18+
}
19+
const auth = await authenticated(args, req);
20+
if (!auth) {
21+
if (pathname === '/') {
22+
serveFile(logService, req, res, LOGIN);
23+
return true;
24+
}
25+
if (pathname === '/login') {
26+
const password = (await collectRequestData(req)).password;
27+
const { isPasswordValid, hashedPassword } = await handlePasswordValidation({
28+
passwordFromRequestBody: password,
29+
passwordFromArgs: args.password,
30+
});
31+
32+
if (isPasswordValid) {
33+
res.writeHead(302, {
34+
'Location': '/',
35+
'Set-Cookie': `key=${hashedPassword}`,
36+
'Content-Type': 'text/plain'
37+
});
38+
} else {
39+
res.writeHead(302, {
40+
'Location': '/',
41+
'Content-Type': 'text/plain'
42+
});
43+
}
44+
res.end('');
45+
return true;
46+
}
47+
serveError(req, res, 401, 'Unauthorized');
48+
return true;
49+
}
50+
return false;
51+
}
52+
53+
function collectRequestData(request: http.IncomingMessage): Promise<Record<string, string>> {
54+
return new Promise(resolve => {
55+
const FORM_URLENCODED = 'application/x-www-form-urlencoded';
56+
if (request.headers['content-type'] === FORM_URLENCODED) {
57+
let body = '';
58+
request.on('data', chunk => {
59+
body += chunk.toString();
60+
});
61+
request.on('end', () => {
62+
const item = parse(body) as Record<string, string>;
63+
resolve(item);
64+
});
65+
}
66+
else {
67+
resolve({});
68+
}
69+
});
70+
}

src/vs/server/node/auth.ts

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,85 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Gitpod. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
import * as http from 'http';
16
import * as crypto from 'crypto';
2-
import * as express from 'express';
3-
import { ServerParsedArgs } from 'vs/server/node/args';
4-
5-
/** Ensures that the input is sanitized by checking
6-
* - it's a string
7-
* - greater than 0 characters
8-
* - trims whitespace
9-
*/
7+
import { args, ServerParsedArgs } from 'vs/server/node/args';
8+
import { ILogService } from 'vs/platform/log/common/log';
9+
1010
export function sanitizeString(str: string): string {
11-
// Very basic sanitization of string
12-
// Credit: https://stackoverflow.com/a/46719000/3015595
1311
return typeof str === 'string' && str.trim().length > 0 ? str.trim() : '';
1412
}
1513

16-
/**
17-
* Return true if authenticated via cookies.
18-
*/
19-
export const authenticated = async (args: ServerParsedArgs, req: express.Request): Promise<boolean> => {
20-
if (!args.password && !args.hashedPassword) {
14+
function parseCookies(request: http.IncomingMessage): Record<string, string> {
15+
const cookies: Record<string, string> = {},
16+
rc = request.headers.cookie;
17+
18+
// eslint-disable-next-line code-no-unused-expressions
19+
rc && rc.split(';').forEach(cookie => {
20+
let parts = cookie.split('=');
21+
if (parts.length > 0) {
22+
const name = parts.shift()!.trim();
23+
let value = decodeURI(parts.join('='));
24+
cookies[name] = value;
25+
}
26+
});
27+
28+
return cookies;
29+
}
30+
31+
export const authenticated = async (args: ServerParsedArgs, req: http.IncomingMessage): Promise<boolean> => {
32+
if (!args.password) {
2133
return true;
2234
}
35+
const cookies = parseCookies(req);
2336
const isCookieValidArgs: IsCookieValidArgs = {
24-
cookieKey: sanitizeString(req.cookies.key),
37+
cookieKey: sanitizeString(cookies.key),
2538
passwordFromArgs: args.password || ''
2639
};
2740

2841
return await isCookieValid(isCookieValidArgs);
2942
};
3043

31-
type PasswordValidation = {
44+
interface PasswordValidation {
3245
isPasswordValid: boolean
3346
hashedPassword: string
34-
};
47+
}
3548

36-
type HandlePasswordValidationArgs = {
37-
/** The password provided by the user */
49+
interface HandlePasswordValidationArgs {
3850
passwordFromRequestBody: string | undefined
39-
/** The password set in PASSWORD or config */
4051
passwordFromArgs: string | undefined
41-
};
52+
}
4253

4354
function safeCompare(a: string, b: string): boolean {
44-
if (b.length > a.length) {
45-
a = a.padEnd(b.length, '0');
46-
}
47-
if (a.length > b.length) {
48-
b = b.padEnd(a.length, '0');
55+
return a.length === b.length && crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
56+
}
57+
58+
export async function generateAndSetPassword(logService: ILogService, length = 24): Promise<void> {
59+
if (args.password || !length) {
60+
return;
4961
}
50-
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
62+
const password = await generatePassword(length);
63+
args.password = password;
64+
logService.info(`Automatically generated password\r\n ${password}`);
5165
}
5266

53-
export const generatePassword = async (length = 24): Promise<string> => {
67+
export async function generatePassword(length = 24): Promise<string> {
5468
const buffer = Buffer.alloc(Math.ceil(length / 2));
5569
await new Promise(resolve => {
5670
crypto.randomFill(buffer, (_, buf) => resolve(buf));
5771
});
5872
return buffer.toString('hex').substring(0, length);
59-
};
73+
}
6074

61-
export const hash = (str: string): string => {
75+
export function hash(str: string): string {
6276
return crypto.createHash('sha256').update(str).digest('hex');
63-
};
77+
}
6478

65-
export const isHashMatch = (password: string, hashPassword: string) => {
66-
const hashedWithLegacy = hash(password);
67-
return safeCompare(hashedWithLegacy, hashPassword);
68-
};
79+
export function isHashMatch(password: string, hashPassword: string): boolean {
80+
const hashed = hash(password);
81+
return safeCompare(hashed, hashPassword);
82+
}
6983

7084
export async function handlePasswordValidation({
7185
passwordFromArgs,

src/vs/server/node/http.ts

Lines changed: 0 additions & 88 deletions
This file was deleted.

0 commit comments

Comments
 (0)