|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Copyright (c) Gitpod. All rights reserved. |
| 3 | + *--------------------------------------------------------------------------------------------*/ |
| 4 | + |
| 5 | +import * as http from 'http'; |
1 | 6 | 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 | + |
10 | 10 | export function sanitizeString(str: string): string {
|
11 |
| - // Very basic sanitization of string |
12 |
| - // Credit: https://stackoverflow.com/a/46719000/3015595 |
13 | 11 | return typeof str === 'string' && str.trim().length > 0 ? str.trim() : '';
|
14 | 12 | }
|
15 | 13 |
|
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) { |
21 | 33 | return true;
|
22 | 34 | }
|
| 35 | + const cookies = parseCookies(req); |
23 | 36 | const isCookieValidArgs: IsCookieValidArgs = {
|
24 |
| - cookieKey: sanitizeString(req.cookies.key), |
| 37 | + cookieKey: sanitizeString(cookies.key), |
25 | 38 | passwordFromArgs: args.password || ''
|
26 | 39 | };
|
27 | 40 |
|
28 | 41 | return await isCookieValid(isCookieValidArgs);
|
29 | 42 | };
|
30 | 43 |
|
31 |
| -type PasswordValidation = { |
| 44 | +interface PasswordValidation { |
32 | 45 | isPasswordValid: boolean
|
33 | 46 | hashedPassword: string
|
34 |
| -}; |
| 47 | +} |
35 | 48 |
|
36 |
| -type HandlePasswordValidationArgs = { |
37 |
| - /** The password provided by the user */ |
| 49 | +interface HandlePasswordValidationArgs { |
38 | 50 | passwordFromRequestBody: string | undefined
|
39 |
| - /** The password set in PASSWORD or config */ |
40 | 51 | passwordFromArgs: string | undefined
|
41 |
| -}; |
| 52 | +} |
42 | 53 |
|
43 | 54 | 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; |
49 | 61 | }
|
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}`); |
51 | 65 | }
|
52 | 66 |
|
53 |
| -export const generatePassword = async (length = 24): Promise<string> => { |
| 67 | +export async function generatePassword(length = 24): Promise<string> { |
54 | 68 | const buffer = Buffer.alloc(Math.ceil(length / 2));
|
55 | 69 | await new Promise(resolve => {
|
56 | 70 | crypto.randomFill(buffer, (_, buf) => resolve(buf));
|
57 | 71 | });
|
58 | 72 | return buffer.toString('hex').substring(0, length);
|
59 |
| -}; |
| 73 | +} |
60 | 74 |
|
61 |
| -export const hash = (str: string): string => { |
| 75 | +export function hash(str: string): string { |
62 | 76 | return crypto.createHash('sha256').update(str).digest('hex');
|
63 |
| -}; |
| 77 | +} |
64 | 78 |
|
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 | +} |
69 | 83 |
|
70 | 84 | export async function handlePasswordValidation({
|
71 | 85 | passwordFromArgs,
|
|
0 commit comments