Skip to content

Commit afe33ea

Browse files
Merge pull request #6284 from IanMatthewHuff/dev/ianhu/portPassword
Port password fix to release
2 parents e5b39ab + 94093d4 commit afe33ea

File tree

6 files changed

+75
-12
lines changed

6 files changed

+75
-12
lines changed

news/2 Fixes/6265.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow for both password and self cert server to work together

src/client/datascience/interactive-window/interactiveWindow.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,8 @@ export class InteractiveWindow extends WebViewHost<IInteractiveWindowMapping> im
11691169
} else if (value === closeOption) {
11701170
sendTelemetryEvent(Telemetry.SelfCertsMessageClose);
11711171
}
1172+
// Don't leave our Interactive Window open in a non-connected state
1173+
this.dispose();
11721174
});
11731175
throw e;
11741176
} else {

src/client/datascience/jupyter/jupyterPasswordConnect.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33
'use strict';
4+
import { Agent as HttpsAgent } from 'https';
45
import { inject, injectable } from 'inversify';
56
import * as nodeFetch from 'node-fetch';
67
import { URLSearchParams } from 'url';
@@ -19,7 +20,7 @@ export class JupyterPasswordConnect implements IJupyterPasswordConnect {
1920
}
2021

2122
@captureTelemetry(Telemetry.GetPasswordAttempt)
22-
public async getPasswordConnectionInfo(url: string, fetchFunction?: (url: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) => Promise<nodeFetch.Response>): Promise<IJupyterPasswordConnectInfo | undefined> {
23+
public async getPasswordConnectionInfo(url: string, allowUnauthorized: boolean, fetchFunction?: (url: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) => Promise<nodeFetch.Response>): Promise<IJupyterPasswordConnectInfo | undefined> {
2324
// For testing allow for our fetch function to be overridden
2425
if (!fetchFunction) {
2526
fetchFunction = nodeFetch.default;
@@ -44,11 +45,11 @@ export class JupyterPasswordConnect implements IJupyterPasswordConnect {
4445

4546
if (userPassword) {
4647
// First get the xsrf cookie by hitting the initial login page
47-
xsrfCookie = await this.getXSRFToken(url, fetchFunction);
48+
xsrfCookie = await this.getXSRFToken(url, allowUnauthorized, fetchFunction);
4849

4950
// Then get the session cookie by hitting that same page with the xsrftoken and the password
5051
if (xsrfCookie) {
51-
const sessionResult = await this.getSessionCookie(url, xsrfCookie, userPassword, fetchFunction);
52+
const sessionResult = await this.getSessionCookie(url, allowUnauthorized, xsrfCookie, userPassword, fetchFunction);
5253
sessionCookieName = sessionResult.sessionCookieName;
5354
sessionCookieValue = sessionResult.sessionCookieValue;
5455
}
@@ -65,6 +66,16 @@ export class JupyterPasswordConnect implements IJupyterPasswordConnect {
6566
}
6667
}
6768

69+
// For HTTPS connections respect our allowUnauthorized setting by adding in an agent to enable that on the request
70+
private addAllowUnauthorized(url: string, allowUnauthorized: boolean, options: nodeFetch.RequestInit): nodeFetch.RequestInit {
71+
if (url.startsWith('https') && allowUnauthorized) {
72+
const requestAgent = new HttpsAgent({rejectUnauthorized: false});
73+
return {...options, agent: requestAgent};
74+
}
75+
76+
return options;
77+
}
78+
6879
private async getUserPassword() : Promise<string | undefined> {
6980
// First get the proposed URI from the user
7081
return this.appShell.showInputBox({
@@ -74,14 +85,14 @@ export class JupyterPasswordConnect implements IJupyterPasswordConnect {
7485
});
7586
}
7687

77-
private async getXSRFToken(url: string, fetchFunction: (url: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) => Promise<nodeFetch.Response>): Promise<string | undefined> {
88+
private async getXSRFToken(url: string, allowUnauthorized: boolean, fetchFunction: (url: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) => Promise<nodeFetch.Response>): Promise<string | undefined> {
7889
let xsrfCookie: string | undefined;
7990

80-
const response = await fetchFunction(`${url}login?`, {
91+
const response = await fetchFunction(`${url}login?`, this.addAllowUnauthorized(url, allowUnauthorized, {
8192
method: 'get',
8293
redirect: 'manual',
8394
headers: { Connection: 'keep-alive' }
84-
});
95+
}));
8596

8697
if (response.ok) {
8798
const cookies = this.getCookies(response);
@@ -98,6 +109,7 @@ export class JupyterPasswordConnect implements IJupyterPasswordConnect {
98109
// First you need a get at the login page to get the xsrf token, then you send back that token along with the password in a post
99110
// That will return back the session cookie. This session cookie then needs to be added to our requests and websockets for @jupyterlab/services
100111
private async getSessionCookie(url: string,
112+
allowUnauthorized: boolean,
101113
xsrfCookie: string,
102114
password: string,
103115
fetchFunction: (url: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) => Promise<nodeFetch.Response>): Promise<{sessionCookieName: string | undefined; sessionCookieValue: string | undefined}> {
@@ -108,12 +120,12 @@ export class JupyterPasswordConnect implements IJupyterPasswordConnect {
108120
postParams.append('_xsrf', xsrfCookie);
109121
postParams.append('password', password);
110122

111-
const response = await fetchFunction(`${url}login?`, {
123+
const response = await fetchFunction(`${url}login?`, this.addAllowUnauthorized(url, allowUnauthorized, {
112124
method: 'post',
113125
headers: { Cookie: `_xsrf=${xsrfCookie}`, Connection: 'keep-alive', 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8' },
114126
body: postParams.toString(),
115127
redirect: 'manual'
116-
});
128+
}));
117129

118130
// Now from this result we need to extract the session cookie
119131
if (response.status === 302) {

src/client/datascience/jupyter/jupyterSession.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ export class JupyterSession implements IJupyterSession {
210210
// If no token is specified prompt for a password
211211
if (connInfo.token === '' || connInfo.token === 'null') {
212212
serverSettings = {...serverSettings, token: ''};
213-
const pwSettings = await this.jupyterPasswordConnect.getPasswordConnectionInfo(connInfo.baseUrl);
213+
const pwSettings = await this.jupyterPasswordConnect.getPasswordConnectionInfo(connInfo.baseUrl, connInfo.allowUnauthorized ? true : false);
214214
if (pwSettings) {
215215
cookieString = this.getSessionCookieString(pwSettings);
216216
const requestHeaders = { Cookie: cookieString, 'X-XSRFToken': pwSettings.xsrfCookie };

src/client/datascience/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export interface IJupyterPasswordConnectInfo {
118118

119119
export const IJupyterPasswordConnect = Symbol('IJupyterPasswordConnect');
120120
export interface IJupyterPasswordConnect {
121-
getPasswordConnectionInfo(url: string): Promise<IJupyterPasswordConnectInfo | undefined>;
121+
getPasswordConnectionInfo(url: string, allowUnauthorized: boolean): Promise<IJupyterPasswordConnectInfo | undefined>;
122122
}
123123

124124
export const IJupyterSession = Symbol('IJupyterSession');

src/test/datascience/jupyterPasswordConnect.unit.test.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,55 @@ suite('JupyterPasswordConnect', () => {
5757
}))).returns(() => Promise.resolve(mockSessionResponse.object)).verifiable(typemoq.Times.once());
5858

5959
//tslint:disable-next-line:no-http-string
60-
const result = await jupyterPasswordConnect.getPasswordConnectionInfo('http://TESTNAME:8888/', fetchMock.object);
60+
const result = await jupyterPasswordConnect.getPasswordConnectionInfo('http://TESTNAME:8888/', false, fetchMock.object);
61+
assert(result, 'Failed to get password');
62+
if (result) {
63+
assert(result.xsrfCookie === xsrfValue, 'Incorrect xsrf value');
64+
assert(result.sessionCookieName === sessionName, 'Incorrect session name');
65+
assert(result.sessionCookieValue === sessionValue, 'Incorrect session value');
66+
}
67+
68+
// Verfiy calls
69+
mockXsrfHeaders.verifyAll();
70+
mockSessionHeaders.verifyAll();
71+
mockXsrfResponse.verifyAll();
72+
mockSessionResponse.verifyAll();
73+
fetchMock.verifyAll();
74+
});
75+
76+
test('getPasswordConnectionInfo allowUnauthorized', async() => {
77+
// Set up our fake node fetch
78+
const fetchMock: typemoq.IMock<typeof nodeFetch.default> = typemoq.Mock.ofInstance(nodeFetch.default);
79+
80+
// Mock our first call to get xsrf cookie
81+
const mockXsrfResponse = typemoq.Mock.ofType(nodeFetch.Response);
82+
const mockXsrfHeaders = typemoq.Mock.ofType(nodeFetch.Headers);
83+
mockXsrfHeaders.setup(mh => mh.get('set-cookie')).returns(() => `_xsrf=${xsrfValue}`).verifiable(typemoq.Times.once());
84+
mockXsrfResponse.setup(mr => mr.ok).returns(() => true).verifiable(typemoq.Times.once());
85+
mockXsrfResponse.setup(mr => mr.headers).returns(() => mockXsrfHeaders.object).verifiable(typemoq.Times.once());
86+
87+
//tslint:disable-next-line:no-http-string
88+
fetchMock.setup(fm => fm('https://TESTNAME:8888/login?', typemoq.It.isObjectWith({
89+
method: 'get',
90+
headers: { Connection: 'keep-alive' }
91+
}))).returns(() => Promise.resolve(mockXsrfResponse.object)).verifiable(typemoq.Times.once());
92+
93+
// Mock our second call to get session cookie
94+
const mockSessionResponse = typemoq.Mock.ofType(nodeFetch.Response);
95+
const mockSessionHeaders = typemoq.Mock.ofType(nodeFetch.Headers);
96+
mockSessionHeaders.setup(mh => mh.get('set-cookie')).returns(() => `${sessionName}=${sessionValue}`).verifiable(typemoq.Times.once());
97+
mockSessionResponse.setup(mr => mr.status).returns(() => 302).verifiable(typemoq.Times.once());
98+
mockSessionResponse.setup(mr => mr.headers).returns(() => mockSessionHeaders.object).verifiable(typemoq.Times.once());
99+
100+
// typemoq doesn't love this comparison, so generalize it a bit
101+
//tslint:disable-next-line:no-http-string
102+
fetchMock.setup(fm => fm('https://TESTNAME:8888/login?', typemoq.It.isObjectWith({
103+
method: 'post',
104+
headers: { Cookie: `_xsrf=${xsrfValue}`, Connection: 'keep-alive', 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8' }
105+
}))).returns(() => Promise.resolve(mockSessionResponse.object)).verifiable(typemoq.Times.once());
106+
107+
//tslint:disable-next-line:no-http-string
108+
const result = await jupyterPasswordConnect.getPasswordConnectionInfo('https://TESTNAME:8888/', true, fetchMock.object);
61109
assert(result, 'Failed to get password');
62110
if (result) {
63111
assert(result.xsrfCookie === xsrfValue, 'Incorrect xsrf value');
@@ -93,7 +141,7 @@ suite('JupyterPasswordConnect', () => {
93141
})).returns(() => Promise.resolve(mockXsrfResponse.object)).verifiable(typemoq.Times.once());
94142

95143
//tslint:disable-next-line:no-http-string
96-
const result = await jupyterPasswordConnect.getPasswordConnectionInfo('http://TESTNAME:8888/', fetchMock.object);
144+
const result = await jupyterPasswordConnect.getPasswordConnectionInfo('http://TESTNAME:8888/', false, fetchMock.object);
97145
assert(!result);
98146

99147
// Verfiy calls

0 commit comments

Comments
 (0)