Skip to content

Commit b3890fe

Browse files
committed
Implement basic authentification
1 parent 8e0cd66 commit b3890fe

File tree

6 files changed

+244
-30
lines changed

6 files changed

+244
-30
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>vscode-web-server login</title>
6+
<style>
7+
form {
8+
position: absolute;
9+
left: 50%;
10+
top: 50%;
11+
-webkit-transform: translate(-50%, -50%);
12+
transform: translate(-50%, -50%);
13+
}
14+
.text {
15+
max-width: 400px;
16+
}
17+
.password {
18+
width: 300px;
19+
}
20+
</style>
21+
</head>
22+
<body>
23+
<form action="/" method="post">
24+
<input style="display: none;" type="text" autocomplete="username" />
25+
<p class="text">Please login with your password. If you haven't set one yourself, you can find it in the server logs.</p>
26+
<div class="field">
27+
<input class="password" required autofocus type="password" placeholder="Enter password..." name="password" autocomplete="current-password" />
28+
<input value="Login" type="submit" />
29+
</div>
30+
</form>
31+
</body>
32+
</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: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 } 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, res: http.ServerResponse | undefined, logService: ILogService): Promise<boolean> {
16+
if (args.password === undefined) {
17+
await generateAndSetPassword(logService);
18+
}
19+
const auth = await authenticated(args, req);
20+
if (!auth && res) {
21+
const password = (await collectRequestData(req)).password;
22+
if (password !== undefined) {
23+
const { valid, hashed } = await handlePasswordValidation({
24+
reqPassword: password,
25+
argsPassword: args.password,
26+
});
27+
28+
if (valid) {
29+
res.writeHead(302, {
30+
'Set-Cookie': `key=${hashed}; HttpOnly`,
31+
'Location': '/'
32+
});
33+
res.end();
34+
} else {
35+
serveFile(logService, req, res, LOGIN);
36+
}
37+
} else {
38+
serveFile(logService, req, res, LOGIN);
39+
}
40+
return false;
41+
}
42+
return auth;
43+
}
44+
45+
function collectRequestData(request: http.IncomingMessage): Promise<Record<string, string>> {
46+
return new Promise(resolve => {
47+
const FORM_URLENCODED = 'application/x-www-form-urlencoded';
48+
if (request.headers['content-type'] === FORM_URLENCODED) {
49+
let body = '';
50+
request.on('data', chunk => {
51+
body += chunk.toString();
52+
});
53+
request.on('end', () => {
54+
const item = parse(body) as Record<string, string>;
55+
resolve(item);
56+
});
57+
}
58+
else {
59+
resolve({});
60+
}
61+
});
62+
}

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)