Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/2 Fixes/6265.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow for both password and self cert server to work together
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,8 @@ export class InteractiveWindow extends WebViewHost<IInteractiveWindowMapping> im
} else if (value === closeOption) {
sendTelemetryEvent(Telemetry.SelfCertsMessageClose);
}
// Don't leave our Interactive Window open in a non-connected state
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can remove this if we don't like it. I still have the bug for doing a reconnect that I'll try to fit into this release. But I felt this one liner made the scenario better than it currently is. So I stuck it in with this fix.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No sounds good to me. Better than what we have now.


In reply to: 296046769 [](ancestors = 296046769)

this.dispose();
});
throw e;
} else {
Expand Down
28 changes: 20 additions & 8 deletions src/client/datascience/jupyter/jupyterPasswordConnect.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';
import { Agent as HttpsAgent } from 'https';
import { inject, injectable } from 'inversify';
import * as nodeFetch from 'node-fetch';
import { URLSearchParams } from 'url';
Expand All @@ -19,7 +20,7 @@ export class JupyterPasswordConnect implements IJupyterPasswordConnect {
}

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

if (userPassword) {
// First get the xsrf cookie by hitting the initial login page
xsrfCookie = await this.getXSRFToken(url, fetchFunction);
xsrfCookie = await this.getXSRFToken(url, allowUnauthorized, fetchFunction);

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

// For HTTPS connections respect our allowUnauthorized setting by adding in an agent to enable that on the request
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our services connection stuff was using the allowUnauthorized flag, but the manual password requests were not. So you couldn't connect to a self cert password machine.

private addAllowUnauthorized(url: string, allowUnauthorized: boolean, options: nodeFetch.RequestInit): nodeFetch.RequestInit {
if (url.startsWith('https') && allowUnauthorized) {
const requestAgent = new HttpsAgent({rejectUnauthorized: false});
return {...options, agent: requestAgent};
}

return options;
}

private async getUserPassword() : Promise<string | undefined> {
// First get the proposed URI from the user
return this.appShell.showInputBox({
Expand All @@ -74,14 +85,14 @@ export class JupyterPasswordConnect implements IJupyterPasswordConnect {
});
}

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

const response = await fetchFunction(`${url}login?`, {
const response = await fetchFunction(`${url}login?`, this.addAllowUnauthorized(url, allowUnauthorized, {
method: 'get',
redirect: 'manual',
headers: { Connection: 'keep-alive' }
});
}));

if (response.ok) {
const cookies = this.getCookies(response);
Expand All @@ -98,6 +109,7 @@ export class JupyterPasswordConnect implements IJupyterPasswordConnect {
// 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
// That will return back the session cookie. This session cookie then needs to be added to our requests and websockets for @jupyterlab/services
private async getSessionCookie(url: string,
allowUnauthorized: boolean,
xsrfCookie: string,
password: string,
fetchFunction: (url: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) => Promise<nodeFetch.Response>): Promise<{sessionCookieName: string | undefined; sessionCookieValue: string | undefined}> {
Expand All @@ -108,12 +120,12 @@ export class JupyterPasswordConnect implements IJupyterPasswordConnect {
postParams.append('_xsrf', xsrfCookie);
postParams.append('password', password);

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

// Now from this result we need to extract the session cookie
if (response.status === 302) {
Expand Down
2 changes: 1 addition & 1 deletion src/client/datascience/jupyter/jupyterSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export class JupyterSession implements IJupyterSession {
// If no token is specified prompt for a password
if (connInfo.token === '' || connInfo.token === 'null') {
serverSettings = {...serverSettings, token: ''};
const pwSettings = await this.jupyterPasswordConnect.getPasswordConnectionInfo(connInfo.baseUrl);
const pwSettings = await this.jupyterPasswordConnect.getPasswordConnectionInfo(connInfo.baseUrl, connInfo.allowUnauthorized ? true : false);
if (pwSettings) {
cookieString = this.getSessionCookieString(pwSettings);
const requestHeaders = { Cookie: cookieString, 'X-XSRFToken': pwSettings.xsrfCookie };
Expand Down
2 changes: 1 addition & 1 deletion src/client/datascience/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export interface IJupyterPasswordConnectInfo {

export const IJupyterPasswordConnect = Symbol('IJupyterPasswordConnect');
export interface IJupyterPasswordConnect {
getPasswordConnectionInfo(url: string): Promise<IJupyterPasswordConnectInfo | undefined>;
getPasswordConnectionInfo(url: string, allowUnauthorized: boolean): Promise<IJupyterPasswordConnectInfo | undefined>;
}

export const IJupyterSession = Symbol('IJupyterSession');
Expand Down
52 changes: 50 additions & 2 deletions src/test/datascience/jupyterPasswordConnect.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,55 @@ suite('JupyterPasswordConnect', () => {
}))).returns(() => Promise.resolve(mockSessionResponse.object)).verifiable(typemoq.Times.once());

//tslint:disable-next-line:no-http-string
const result = await jupyterPasswordConnect.getPasswordConnectionInfo('http://TESTNAME:8888/', fetchMock.object);
const result = await jupyterPasswordConnect.getPasswordConnectionInfo('http://TESTNAME:8888/', false, fetchMock.object);
assert(result, 'Failed to get password');
if (result) {
assert(result.xsrfCookie === xsrfValue, 'Incorrect xsrf value');
assert(result.sessionCookieName === sessionName, 'Incorrect session name');
assert(result.sessionCookieValue === sessionValue, 'Incorrect session value');
}

// Verfiy calls
mockXsrfHeaders.verifyAll();
mockSessionHeaders.verifyAll();
mockXsrfResponse.verifyAll();
mockSessionResponse.verifyAll();
fetchMock.verifyAll();
});

test('getPasswordConnectionInfo allowUnauthorized', async() => {
// Set up our fake node fetch
const fetchMock: typemoq.IMock<typeof nodeFetch.default> = typemoq.Mock.ofInstance(nodeFetch.default);

// Mock our first call to get xsrf cookie
const mockXsrfResponse = typemoq.Mock.ofType(nodeFetch.Response);
const mockXsrfHeaders = typemoq.Mock.ofType(nodeFetch.Headers);
mockXsrfHeaders.setup(mh => mh.get('set-cookie')).returns(() => `_xsrf=${xsrfValue}`).verifiable(typemoq.Times.once());
mockXsrfResponse.setup(mr => mr.ok).returns(() => true).verifiable(typemoq.Times.once());
mockXsrfResponse.setup(mr => mr.headers).returns(() => mockXsrfHeaders.object).verifiable(typemoq.Times.once());

//tslint:disable-next-line:no-http-string
fetchMock.setup(fm => fm('https://TESTNAME:8888/login?', typemoq.It.isObjectWith({
method: 'get',
headers: { Connection: 'keep-alive' }
}))).returns(() => Promise.resolve(mockXsrfResponse.object)).verifiable(typemoq.Times.once());

// Mock our second call to get session cookie
const mockSessionResponse = typemoq.Mock.ofType(nodeFetch.Response);
const mockSessionHeaders = typemoq.Mock.ofType(nodeFetch.Headers);
mockSessionHeaders.setup(mh => mh.get('set-cookie')).returns(() => `${sessionName}=${sessionValue}`).verifiable(typemoq.Times.once());
mockSessionResponse.setup(mr => mr.status).returns(() => 302).verifiable(typemoq.Times.once());
mockSessionResponse.setup(mr => mr.headers).returns(() => mockSessionHeaders.object).verifiable(typemoq.Times.once());

// typemoq doesn't love this comparison, so generalize it a bit
//tslint:disable-next-line:no-http-string
fetchMock.setup(fm => fm('https://TESTNAME:8888/login?', typemoq.It.isObjectWith({
method: 'post',
headers: { Cookie: `_xsrf=${xsrfValue}`, Connection: 'keep-alive', 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8' }
}))).returns(() => Promise.resolve(mockSessionResponse.object)).verifiable(typemoq.Times.once());

//tslint:disable-next-line:no-http-string
const result = await jupyterPasswordConnect.getPasswordConnectionInfo('https://TESTNAME:8888/', true, fetchMock.object);
assert(result, 'Failed to get password');
if (result) {
assert(result.xsrfCookie === xsrfValue, 'Incorrect xsrf value');
Expand Down Expand Up @@ -93,7 +141,7 @@ suite('JupyterPasswordConnect', () => {
})).returns(() => Promise.resolve(mockXsrfResponse.object)).verifiable(typemoq.Times.once());

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

// Verfiy calls
Expand Down