Skip to content

Commit 470ba92

Browse files
committed
Implement full login workflow
1 parent 1bcb11e commit 470ba92

2 files changed

Lines changed: 243 additions & 14 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport"
6+
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" />
7+
<meta http-equiv="Content-Security-Policy"
8+
content="script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;" />
9+
<title>vscode-server login</title>
10+
<style type="text/css" title="text/css" media="screen">
11+
html,
12+
body,
13+
#root {
14+
height: 100%;
15+
width: 100%;
16+
}
17+
18+
body {
19+
background: rgb(244, 247, 252);
20+
color: #111;
21+
margin: 0;
22+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji",
23+
"Segoe UI Emoji", "Segoe UI Symbol";
24+
overflow: hidden;
25+
}
26+
27+
input,
28+
button {
29+
font-family: inherit;
30+
font-size: 1rem;
31+
line-height: 1rem;
32+
}
33+
34+
.-button {
35+
background-color: rgb(87, 114, 245);
36+
border-radius: 5px;
37+
border: none;
38+
box-sizing: border-box;
39+
color: white;
40+
cursor: pointer;
41+
padding: 18px 20px;
42+
text-decoration: none;
43+
}
44+
45+
.center-container {
46+
align-items: center;
47+
box-sizing: border-box;
48+
display: flex;
49+
flex-direction: column;
50+
justify-content: center;
51+
min-height: 100%;
52+
padding: 20px;
53+
width: 100%;
54+
}
55+
56+
.card-box {
57+
background-color: rgb(250, 253, 258);
58+
border-radius: 5px;
59+
box-shadow: rgba(60, 66, 87, 0.117647) 0px 7px 14px 0px, rgba(0, 0, 0, 0.117647) 0px 3px 6px 0px;
60+
max-width: 650px;
61+
width: 100%;
62+
}
63+
64+
.card-box>.header {
65+
border-bottom: 1px solid #ddd;
66+
color: #444;
67+
padding: 30px;
68+
}
69+
70+
.card-box>.header>.main {
71+
margin: 0;
72+
font-size: 1.5rem;
73+
}
74+
75+
.card-box>.header>.sub {
76+
color: #555;
77+
margin-top: 10px;
78+
}
79+
80+
.card-box>.content {
81+
padding: 40px;
82+
}
83+
84+
.card-box>.content>.none {
85+
margin: 2px 0;
86+
}
87+
88+
.card-box+.card-box {
89+
margin-top: 26px;
90+
}
91+
92+
canvas {
93+
top: 0;
94+
left: 0;
95+
}
96+
</style>
97+
<style>
98+
body {
99+
min-height: 568px;
100+
min-width: 320px;
101+
overflow: auto;
102+
}
103+
104+
.login-form {
105+
display: flex;
106+
flex-direction: column;
107+
flex: 1;
108+
justify-content: center;
109+
}
110+
111+
.login-form>.field {
112+
display: flex;
113+
flex-direction: row;
114+
width: 100%;
115+
}
116+
117+
@media (max-width: 600px) {
118+
.login-form>.field {
119+
flex-direction: column;
120+
}
121+
}
122+
123+
.login-form>.error {
124+
color: red;
125+
margin-top: 16px;
126+
}
127+
128+
.login-form>.field>.password {
129+
background-color: rgb(244, 247, 252);
130+
border-radius: 5px;
131+
border: 1px solid #ddd;
132+
box-sizing: border-box;
133+
color: black;
134+
flex: 1;
135+
padding: 16px;
136+
}
137+
138+
.login-form>.user {
139+
display: none;
140+
}
141+
142+
.login-form>.field>.submit {
143+
margin-left: 20px;
144+
}
145+
146+
@media (max-width: 600px) {
147+
.login-form>.field>.submit {
148+
margin-left: 0px;
149+
margin-top: 16px;
150+
}
151+
}
152+
153+
input {
154+
-webkit-appearance: none;
155+
}
156+
</style>
157+
</head>
158+
159+
<body>
160+
<div class="center-container">
161+
<div class="card-box">
162+
<div class="header">
163+
<h1 class="main">Welcome to VSCode Server</h1>
164+
<div class="sub">Please log in below.</div>
165+
</div>
166+
<div class="content">
167+
<form class="login-form" action="/login" method="post">
168+
<input class="user" type="text" autocomplete="username" />
169+
<input id="base" type="hidden" name="base" value="/" />
170+
<div class="field">
171+
<input required autofocus class="password" type="password" placeholder="PASSWORD" name="password"
172+
autocomplete="current-password" />
173+
<input class="submit -button" value="SUBMIT" type="submit" />
174+
</div>
175+
</form>
176+
</div>
177+
</div>
178+
</div>
179+
</body>
180+
181+
</html>

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

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import { ExtensionScanner, ExtensionScannerInput, IExtensionReference } from 'vs
7070
import { IGetEnvironmentDataArguments, IRemoteAgentEnvironmentDTO, IScanExtensionsArguments, IScanSingleExtensionArguments } from 'vs/workbench/services/remote/common/remoteAgentEnvironmentChannel';
7171
import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/workbench/services/remote/common/remoteAgentFileSystemChannel';
7272
import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService';
73+
import { parse } from 'querystring';
7374

7475
export type IRawURITransformerFactory = (remoteAuthority: string) => IRawURITransformer;
7576
export const IRawURITransformerFactory = createDecorator<IRawURITransformerFactory>('rawURITransformerFactory');
@@ -583,10 +584,36 @@ export async function main(options: IServerOptions): Promise<void> {
583584
return;
584585
}
585586

586-
//#region static
587587
if (pathname === '/') {
588588
return serveFile(logService, req, res, await authenticated(req) ? devMode ? options.mainDev || WEB_MAIN_DEV : options.main || WEB_MAIN : LOGIN);
589589
}
590+
if (pathname === '/login') {
591+
const password = (await collectRequestData(req)).password;
592+
const passwordMethod = getPasswordMethod(parsedArgs.hashedPassword);
593+
const { isPasswordValid, hashedPassword } = await handlePasswordValidation({
594+
passwordMethod,
595+
hashedPasswordFromArgs: parsedArgs.hashedPassword,
596+
passwordFromRequestBody: password,
597+
passwordFromArgs: parsedArgs.password,
598+
});
599+
600+
if (isPasswordValid) {
601+
// The hash does not add any actual security but we do it for
602+
// obfuscation purposes (and as a side effect it handles escaping).
603+
res.writeHead(302, {
604+
'Location': '/',
605+
'Set-Cookie': `key=${hashedPassword}`,
606+
'Content-Type': 'text/plain'
607+
});
608+
return res.end('');
609+
} else {
610+
res.writeHead(302, {
611+
'Location': '/',
612+
'Content-Type': 'text/plain'
613+
});
614+
return res.end('');
615+
}
616+
}
590617
if (!await ensureAuthenticated(req, res)) {
591618
return;
592619
}
@@ -600,6 +627,14 @@ export async function main(options: IServerOptions): Promise<void> {
600627
'display': 'standalone'
601628
}));
602629
}
630+
if (pathname === '/vscode-remote-resource') {
631+
const filePath = parsedUrl.query['path'];
632+
const fsPath = typeof filePath === 'string' && URI.from({ scheme: 'file', path: filePath }).fsPath;
633+
if (!fsPath) {
634+
return serveError(req, res, 400, 'Bad Request.');
635+
}
636+
return serveFile(logService, req, res, fsPath);
637+
}
603638
if (pathname) {
604639
let relativeFilePath;
605640
if (/^\/static\//.test(pathname)) {
@@ -609,18 +644,8 @@ export async function main(options: IServerOptions): Promise<void> {
609644
}
610645
return serveFile(logService, req, res, path.join(APP_ROOT, relativeFilePath));
611646
}
612-
//#region static end
613647

614-
//#region headless
615-
if (pathname === '/vscode-remote-resource') {
616-
const filePath = parsedUrl.query['path'];
617-
const fsPath = typeof filePath === 'string' && URI.from({ scheme: 'file', path: filePath }).fsPath;
618-
if (!fsPath) {
619-
return serveError(req, res, 400, 'Bad Request.');
620-
}
621-
return serveFile(logService, req, res, fsPath);
622-
}
623-
//#region headless end
648+
624649

625650
// TODO uri callbacks ?
626651
logService.error(`${req.method} ${req.url} not found`);
@@ -922,6 +947,25 @@ export async function main(options: IServerOptions): Promise<void> {
922947
});
923948
}
924949

950+
function collectRequestData(request: http.IncomingMessage): Promise<Record<string, string>> {
951+
return new Promise(resolve => {
952+
const FORM_URLENCODED = 'application/x-www-form-urlencoded';
953+
if (request.headers['content-type'] === FORM_URLENCODED) {
954+
let body = '';
955+
request.on('data', chunk => {
956+
body += chunk.toString();
957+
});
958+
request.on('end', () => {
959+
const item = parse(body) as Record<string, string>;
960+
resolve(item);
961+
});
962+
}
963+
else {
964+
resolve({});
965+
}
966+
});
967+
}
968+
925969
/** Ensures that the input is sanitized by checking
926970
* - it's a string
927971
* - greater than 0 characters
@@ -949,9 +993,10 @@ export const authenticated = async (req: http.IncomingMessage): Promise<boolean>
949993
return true;
950994
}
951995
const passwordMethod = getPasswordMethod(parsedArgs.hashedPassword);
996+
const cookies = parseCookies(req);
952997
const isCookieValidArgs: IsCookieValidArgs = {
953998
passwordMethod,
954-
cookieKey: sanitizeString(parseCookies(req).key),
999+
cookieKey: sanitizeString(cookies.key),
9551000
passwordFromArgs: parsedArgs.password || '',
9561001
hashedPasswordFromArgs: parsedArgs.hashedPassword,
9571002
};
@@ -967,7 +1012,10 @@ function parseCookies(request: http.IncomingMessage): Record<string, string> {
9671012
rc && rc.split(';').forEach(cookie => {
9681013
let parts = cookie.split('=');
9691014
if (parts.length > 0) {
970-
cookies[parts.shift()!.trim()] = decodeURI(parts.join('='));
1015+
const name = parts.shift()!.trim();
1016+
let value = decodeURI(parts.join('='));
1017+
value = value.substring(1, value.length - 1);
1018+
cookies[name] = value;
9711019
}
9721020
});
9731021

0 commit comments

Comments
 (0)