Skip to content

Commit cc6782d

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

File tree

6 files changed

+223
-28
lines changed

6 files changed

+223
-28
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: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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.setHeader('Set-Cookie', `key=${hashed}; HttpOnly`);
30+
return true;
31+
} else {
32+
serveFile(logService, req, res, LOGIN);
33+
}
34+
} else {
35+
serveFile(logService, req, res, LOGIN);
36+
}
37+
return false;
38+
}
39+
return auth;
40+
}
41+
42+
function collectRequestData(request: http.IncomingMessage): Promise<Record<string, string>> {
43+
return new Promise(resolve => {
44+
const FORM_URLENCODED = 'application/x-www-form-urlencoded';
45+
if (request.headers['content-type'] === FORM_URLENCODED) {
46+
let body = '';
47+
request.on('data', chunk => {
48+
body += chunk.toString();
49+
});
50+
request.on('end', () => {
51+
const item = parse(body) as Record<string, string>;
52+
resolve(item);
53+
});
54+
}
55+
else {
56+
resolve({});
57+
}
58+
});
59+
}

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

src/vs/server/node/server.main.ts

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import * as crypto from 'crypto';
77
import * as fs from 'fs';
88
import * as http from 'http';
99
import * as net from 'net';
10-
import * as os from 'os';
1110
import * as path from 'path';
1211
import * as url from 'url';
1312
import { RunOnceScheduler } from 'vs/base/common/async';
@@ -33,9 +32,7 @@ import { ConfigurationService } from 'vs/platform/configuration/common/configura
3332
import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc';
3433
import { IDownloadService } from 'vs/platform/download/common/download';
3534
import { DownloadService } from 'vs/platform/download/common/downloadService';
36-
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
3735
import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
38-
import { OptionDescriptions, OPTIONS, parseArgs } from 'vs/platform/environment/node/argv';
3936
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
4037
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
4138
import { IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
@@ -62,6 +59,7 @@ import { RequestChannel } from 'vs/platform/request/common/requestIpc';
6259
import { RequestService } from 'vs/platform/request/node/requestService';
6360
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
6461
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
62+
import { args } from 'vs/server/node/args';
6563
import { IFileChangeDto } from 'vs/workbench/api/common/extHost.protocol';
6664
import { IExtHostReadyMessage, IExtHostSocketMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
6765
import { Logger } from 'vs/workbench/services/extensions/common/extensionPoints';
@@ -73,7 +71,7 @@ import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/
7371
export type IRawURITransformerFactory = (remoteAuthority: string) => IRawURITransformer;
7472
export const IRawURITransformerFactory = createDecorator<IRawURITransformerFactory>('rawURITransformerFactory');
7573

76-
const APP_ROOT = path.join(__dirname, '..', '..', '..', '..');
74+
export const APP_ROOT = path.join(__dirname, '..', '..', '..', '..');
7775
const uriTransformerPath = path.join(APP_ROOT, 'out/serverUriTransformer');
7876
const rawURITransformerFactory: IRawURITransformerFactory = <any>require.__$__nodeRequire(uriTransformerPath);
7977

@@ -226,14 +224,6 @@ async function handleRoot(req: http.IncomingMessage, resp: http.ServerResponse,
226224
return resp.end(entryPointContent);
227225
}
228226

229-
interface ServerParsedArgs extends NativeParsedArgs {
230-
port?: string
231-
}
232-
const SERVER_OPTIONS: OptionDescriptions<Required<ServerParsedArgs>> = {
233-
...OPTIONS,
234-
port: { type: 'string' }
235-
};
236-
237227
export interface IStartServerResult {
238228
installingInitialExtensions?: Promise<void>
239229
}
@@ -249,17 +239,16 @@ export interface IServerOptions {
249239
configureExtensionHostForkOptions?(opts: cp.ForkOptions, accessor: ServicesAccessor, channelServer: IPCServer<RemoteAgentConnectionContext>): void;
250240
configureExtensionHostProcess?(extensionHost: cp.ChildProcess, accessor: ServicesAccessor, channelServer: IPCServer<RemoteAgentConnectionContext>): IDisposable;
251241

242+
verifyRequest?(req: http.IncomingMessage, res: http.ServerResponse | undefined, accessor: ServicesAccessor): Promise<boolean>;
252243
handleRequest?(pathname: string | null, req: http.IncomingMessage, res: http.ServerResponse, accessor: ServicesAccessor, channelServer: IPCServer<RemoteAgentConnectionContext>): Promise<boolean>;
253244
}
254245

255246
export async function main(options: IServerOptions): Promise<void> {
256247
const devMode = !!process.env['VSCODE_DEV'];
257248
const connectionToken = generateUuid();
258249

259-
const parsedArgs = parseArgs(process.argv, SERVER_OPTIONS);
260-
parsedArgs['user-data-dir'] = URI.file(path.join(os.homedir(), product.dataFolderName)).fsPath;
261250
const productService = { _serviceBrand: undefined, ...product };
262-
const environmentService = new NativeEnvironmentService(parsedArgs, productService);
251+
const environmentService = new NativeEnvironmentService(args, productService);
263252

264253
// see src/vs/code/electron-main/main.ts#142
265254
const bufferLogService = new BufferLogService();
@@ -593,10 +582,12 @@ export async function main(options: IServerOptions): Promise<void> {
593582
const parsedUrl = url.parse(req.url, true);
594583
const pathname = parsedUrl.pathname;
595584

585+
if (options.verifyRequest && !await instantiationService.invokeFunction(accessor => options.verifyRequest!(req, res, accessor))) {
586+
return;
587+
}
596588
if (options.handleRequest && await instantiationService.invokeFunction(accessor => options.handleRequest!(pathname, req, res, accessor, channelServer))) {
597589
return;
598590
}
599-
600591
//#region headless
601592
if (pathname === '/vscode-remote-resource') {
602593
const filePath = parsedUrl.query['path'];
@@ -643,12 +634,16 @@ export async function main(options: IServerOptions): Promise<void> {
643634
}
644635
});
645636
server.on('error', e => logService.error(e));
646-
server.on('upgrade', (req: http.IncomingMessage, socket: net.Socket) => {
637+
server.on('upgrade', async (req: http.IncomingMessage, socket: net.Socket) => {
647638
if (req.headers['upgrade'] !== 'websocket' || !req.url) {
648639
logService.error(`failed to upgrade for header "${req.headers['upgrade']}" and url: "${req.url}".`);
649640
socket.end('HTTP/1.1 400 Bad Request');
650641
return;
651642
}
643+
if (options.verifyRequest && !await instantiationService.invokeFunction(accessor => options.verifyRequest!(req, undefined, accessor))) {
644+
socket.end('HTTP/1.1 401 Unauthorized');
645+
return;
646+
}
652647
const { query } = url.parse(req.url, true);
653648
// /?reconnectionToken=c0e3a8af-6838-44fb-851b-675401030831&reconnection=false&skipWebSocketFrames=false
654649
const reconnection = 'reconnection' in query && query['reconnection'] === 'true';
@@ -758,7 +753,7 @@ export async function main(options: IServerOptions): Promise<void> {
758753
logService.info(`[${token}] Management connection is connected.`);
759754
} else {
760755
if (!client.management) {
761-
logService.error(`[${token}] Failed to reconnect: management connection is not running.`);
756+
logService.error(`[${token}] Failed to reconnect: management connection is not running.`);
762757
protocol.sendControl(VSBuffer.fromString(JSON.stringify({ type: 'error', reason: 'Management connection is not running.' } as ErrorMessage)));
763758
safeDisposeProtocolAndSocket(protocol);
764759
return;
@@ -848,17 +843,17 @@ export async function main(options: IServerOptions): Promise<void> {
848843
socket.end();
849844
extensionHost.kill();
850845
client.extensionHost = undefined;
851-
logService.info(`[${token}] Extension host is disconnected.`);
846+
logService.info(`[${token}] Extension host is disconnected.`);
852847
}
853848

854849
extensionHost.on('error', err => {
855850
dispose();
856-
logService.error(`[${token}] Extension host failed with: `, err);
851+
logService.error(`[${token}] Extension host failed with: `, err);
857852
});
858853
extensionHost.on('exit', (code: number, signal: string) => {
859854
dispose();
860855
if (code !== 0 && signal !== 'SIGTERM') {
861-
logService.error(`[${token}] Extension host exited with code: ${code} and signal: ${signal}.`);
856+
logService.error(`[${token}] Extension host exited with code: ${code} and signal: ${signal}.`);
862857
}
863858
});
864859

@@ -873,7 +868,7 @@ export async function main(options: IServerOptions): Promise<void> {
873868
permessageDeflate,
874869
inflateBytes
875870
} as IExtHostSocketMessage, socket);
876-
logService.info(`[${token}] Extension host is connected.`);
871+
logService.info(`[${token}] Extension host is connected.`);
877872
}
878873
};
879874
extensionHost.on('message', readyListener);
@@ -882,13 +877,13 @@ export async function main(options: IServerOptions): Promise<void> {
882877
toDispose = instantiationService.invokeFunction(accessor => options.configureExtensionHostProcess!(extensionHost, accessor, channelServer));
883878
}
884879
client.extensionHost = extensionHost;
885-
logService.info(`[${token}] Extension host is started.`);
880+
logService.info(`[${token}] Extension host is started.`);
886881
} catch (e) {
887-
logService.error(`[${token}] Failed to start the extension host process: `, e);
882+
logService.error(`[${token}] Failed to start the extension host process: `, e);
888883
}
889884
} else {
890885
if (!client.extensionHost) {
891-
logService.error(`[${token}] Failed to reconnect: extension host is not running.`);
886+
logService.error(`[${token}] Failed to reconnect: extension host is not running.`);
892887
protocol.sendControl(VSBuffer.fromString(JSON.stringify({ type: 'error', reason: 'Extension host is not running.' } as ErrorMessage)));
893888
safeDisposeProtocolAndSocket(protocol);
894889
return;
@@ -908,7 +903,7 @@ export async function main(options: IServerOptions): Promise<void> {
908903
permessageDeflate,
909904
inflateBytes
910905
} as IExtHostSocketMessage, socket);
911-
logService.info(`[${token}] Extension host is reconnected.`);
906+
logService.info(`[${token}] Extension host is reconnected.`);
912907
}
913908
} else {
914909
logService.error(`[${token}] Unexpected connection type:`, msg.desiredConnectionType);
@@ -921,8 +916,8 @@ export async function main(options: IServerOptions): Promise<void> {
921916
});
922917
});
923918
let port = 3000;
924-
if (parsedArgs.port) {
925-
port = Number(parsedArgs.port);
919+
if (args.port) {
920+
port = Number(args.port);
926921
} else if (typeof options.port === 'number') {
927922
port = options.port;
928923
}

src/vs/server/node/server.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@
22
* Copyright (c) Gitpod. All rights reserved.
33
*--------------------------------------------------------------------------------------------*/
44

5+
import { ILogService } from 'vs/platform/log/common/log';
6+
import { handleVerification } from 'vs/server/node/auth-http';
57
import { registerRemoteTerminal } from 'vs/server/node/remote-terminal';
68
import { main } from 'vs/server/node/server.main';
79

810
main({
911
start: (services, channelServer) => {
1012
registerRemoteTerminal(services, channelServer);
13+
},
14+
verifyRequest: (req, res, accessor): Promise<boolean> => {
15+
const logService = accessor.get(ILogService);
16+
return handleVerification(req, res, logService);
1117
}
1218
});
19+

0 commit comments

Comments
 (0)