|
| 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