Skip to content

Commit 7035e0c

Browse files
committed
Implement basic authentification
1 parent 5c9fe49 commit 7035e0c

File tree

6 files changed

+243
-32
lines changed

6 files changed

+243
-32
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>vscode-web-server login</title>
6+
</head>
7+
<body>
8+
<form action="/" method="post">
9+
<input style="display: none;" type="text" autocomplete="username" />
10+
<div class="field">
11+
<input required autofocus type="password" placeholder="Enter password..." name="password" autocomplete="current-password" />
12+
<input value="Login" type="submit" />
13+
</div>
14+
</form>
15+
</body>
16+
</html>

src/vs/server/node/args.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Gitpod. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
import * as path from 'path';
6+
import * as os from 'os';
7+
import { URI } from 'vs/base/common/uri';
8+
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
9+
import { OptionDescriptions, OPTIONS, parseArgs } from 'vs/platform/environment/node/argv';
10+
import product from 'vs/platform/product/common/product';
11+
12+
export interface ServerParsedArgs extends NativeParsedArgs {
13+
port?: string
14+
password?: string
15+
}
16+
const SERVER_OPTIONS: OptionDescriptions<Required<ServerParsedArgs>> = {
17+
...OPTIONS,
18+
port: { type: 'string' },
19+
password: { type: 'string' }
20+
};
21+
22+
export const devMode = !!process.env['VSCODE_DEV'];
23+
export const args = parseArgs(process.argv, SERVER_OPTIONS);
24+
args['user-data-dir'] = URI.file(path.join(os.homedir(), product.dataFolderName)).fsPath;

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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, serveFile, VerifiedIncomingMessage } 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 handleVerification(req: http.IncomingMessage, logService: ILogService): Promise<boolean> {
16+
if (args.password === undefined) {
17+
await generateAndSetPassword(logService);
18+
}
19+
return authenticated(args, req);
20+
}
21+
22+
export async function handleAuthRequest(pathname: string | null, req: VerifiedIncomingMessage, res: http.ServerResponse, logService: ILogService): Promise<boolean> {
23+
if (!req.verified) {
24+
const password = (await collectRequestData(req)).password;
25+
if (password !== undefined) {
26+
const { valid, hashed } = await handlePasswordValidation({
27+
reqPassword: password,
28+
argsPassword: args.password,
29+
});
30+
31+
if (valid) {
32+
req.verified = true;
33+
res.setHeader('Set-Cookie', `key=${hashed}; HttpOnly`);
34+
return false;
35+
} else {
36+
serveFile(logService, req, res, LOGIN);
37+
}
38+
} else {
39+
serveFile(logService, req, res, LOGIN);
40+
}
41+
return true;
42+
}
43+
return false;
44+
}
45+
46+
function collectRequestData(request: http.IncomingMessage): Promise<Record<string, string>> {
47+
return new Promise(resolve => {
48+
const FORM_URLENCODED = 'application/x-www-form-urlencoded';
49+
if (request.headers['content-type'] === FORM_URLENCODED) {
50+
let body = '';
51+
request.on('data', chunk => {
52+
body += chunk.toString();
53+
});
54+
request.on('end', () => {
55+
const item = parse(body) as Record<string, string>;
56+
resolve(item);
57+
});
58+
}
59+
else {
60+
resolve({});
61+
}
62+
});
63+
}

src/vs/server/node/auth.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Gitpod. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
import * as http from 'http';
6+
import * as crypto from 'crypto';
7+
import { args, ServerParsedArgs } from 'vs/server/node/args';
8+
import { ILogService } from 'vs/platform/log/common/log';
9+
10+
export function sanitizeString(str: string): string {
11+
return typeof str === 'string' && str.trim().length > 0 ? str.trim() : '';
12+
}
13+
14+
export function parseCookies(request: http.IncomingMessage): Record<string, string> {
15+
const cookies: Record<string, string> = {};
16+
const rc = request.headers.cookie;
17+
18+
if (rc) {
19+
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+
29+
return cookies;
30+
}
31+
32+
export async function authenticated(args: ServerParsedArgs, req: http.IncomingMessage): Promise<boolean> {
33+
if (!args.password) {
34+
return true;
35+
}
36+
const cookies = parseCookies(req);
37+
return isHashMatch(args.password || '', sanitizeString(cookies.key));
38+
};
39+
40+
interface PasswordValidation {
41+
valid: boolean
42+
hashed: string
43+
}
44+
45+
interface HandlePasswordValidationArgs {
46+
reqPassword: string | undefined
47+
argsPassword: string | undefined
48+
}
49+
50+
function safeCompare(a: string, b: string): boolean {
51+
return a.length === b.length && crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
52+
}
53+
54+
export async function generateAndSetPassword(logService: ILogService, length = 24): Promise<void> {
55+
if (args.password || !length) {
56+
return;
57+
}
58+
const password = await generatePassword(length);
59+
args.password = password;
60+
logService.info(`Automatically generated password\r\n ${password}`);
61+
}
62+
63+
export async function generatePassword(length = 24): Promise<string> {
64+
const buffer = Buffer.alloc(Math.ceil(length / 2));
65+
await new Promise(resolve => {
66+
crypto.randomFill(buffer, (_, buf) => resolve(buf));
67+
});
68+
return buffer.toString('hex').substring(0, length);
69+
}
70+
71+
export function hash(str: string): string {
72+
return crypto.createHash('sha256').update(str).digest('hex');
73+
}
74+
75+
export function isHashMatch(password: string, hashPassword: string): boolean {
76+
const hashed = hash(password);
77+
return safeCompare(hashed, hashPassword);
78+
}
79+
80+
export async function handlePasswordValidation({ argsPassword: passwordFromArgs, reqPassword: passwordFromRequestBody }: HandlePasswordValidationArgs): Promise<PasswordValidation> {
81+
if (passwordFromRequestBody) {
82+
const valid = passwordFromArgs ? safeCompare(passwordFromRequestBody, passwordFromArgs) : false;
83+
const hashed = hash(passwordFromRequestBody);
84+
return {
85+
valid,
86+
hashed
87+
};
88+
}
89+
90+
return {
91+
valid: false,
92+
hashed: ''
93+
}
94+
}

0 commit comments

Comments
 (0)