From 0d429c58b02db2fdecc7565385027bacfe715d21 Mon Sep 17 00:00:00 2001 From: Ian Huff Date: Thu, 20 Jun 2019 16:07:01 -0700 Subject: [PATCH 1/4] all for both self certs and password at the same time --- .../interactive-window/interactiveWindow.ts | 2 ++ .../jupyter/jupyterPasswordConnect.ts | 28 +++++++++++++------ .../datascience/jupyter/jupyterSession.ts | 2 +- src/client/datascience/types.ts | 2 +- .../jupyterPasswordConnect.unit.test.ts | 4 +-- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/client/datascience/interactive-window/interactiveWindow.ts b/src/client/datascience/interactive-window/interactiveWindow.ts index 04e6167613cd..6e099aa5b8f3 100644 --- a/src/client/datascience/interactive-window/interactiveWindow.ts +++ b/src/client/datascience/interactive-window/interactiveWindow.ts @@ -1169,6 +1169,8 @@ export class InteractiveWindow extends WebViewHost im } else if (value === closeOption) { sendTelemetryEvent(Telemetry.SelfCertsMessageClose); } + // Don't leave our Interactive Window open in a non-connected state + this.dispose(); }); throw e; } else { diff --git a/src/client/datascience/jupyter/jupyterPasswordConnect.ts b/src/client/datascience/jupyter/jupyterPasswordConnect.ts index 0f32f43bdd36..ebf56457e2bc 100644 --- a/src/client/datascience/jupyter/jupyterPasswordConnect.ts +++ b/src/client/datascience/jupyter/jupyterPasswordConnect.ts @@ -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'; @@ -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): Promise { + public async getPasswordConnectionInfo(url: string, allowUnauthorized: boolean, fetchFunction?: (url: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) => Promise): Promise { // For testing allow for our fetch function to be overridden if (!fetchFunction) { fetchFunction = nodeFetch.default; @@ -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; } @@ -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 + private allowUnauthorized(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 { // First get the proposed URI from the user return this.appShell.showInputBox({ @@ -74,14 +85,14 @@ export class JupyterPasswordConnect implements IJupyterPasswordConnect { }); } - private async getXSRFToken(url: string, fetchFunction: (url: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) => Promise): Promise { + private async getXSRFToken(url: string, allowUnauthorized: boolean, fetchFunction: (url: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) => Promise): Promise { let xsrfCookie: string | undefined; - const response = await fetchFunction(`${url}login?`, { + const response = await fetchFunction(`${url}login?`, this.allowUnauthorized(url, allowUnauthorized, { method: 'get', redirect: 'manual', headers: { Connection: 'keep-alive' } - }); + })); if (response.ok) { const cookies = this.getCookies(response); @@ -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): Promise<{sessionCookieName: string | undefined; sessionCookieValue: string | undefined}> { @@ -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.allowUnauthorized(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) { diff --git a/src/client/datascience/jupyter/jupyterSession.ts b/src/client/datascience/jupyter/jupyterSession.ts index a2fe512ba330..330f30795506 100644 --- a/src/client/datascience/jupyter/jupyterSession.ts +++ b/src/client/datascience/jupyter/jupyterSession.ts @@ -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 }; diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index a67fda3932f7..6dc5a2ce706a 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -118,7 +118,7 @@ export interface IJupyterPasswordConnectInfo { export const IJupyterPasswordConnect = Symbol('IJupyterPasswordConnect'); export interface IJupyterPasswordConnect { - getPasswordConnectionInfo(url: string): Promise; + getPasswordConnectionInfo(url: string, allowUnauthorized: boolean): Promise; } export const IJupyterSession = Symbol('IJupyterSession'); diff --git a/src/test/datascience/jupyterPasswordConnect.unit.test.ts b/src/test/datascience/jupyterPasswordConnect.unit.test.ts index 8f6263bd98e3..aa8f2c267ff3 100644 --- a/src/test/datascience/jupyterPasswordConnect.unit.test.ts +++ b/src/test/datascience/jupyterPasswordConnect.unit.test.ts @@ -57,7 +57,7 @@ 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'); @@ -93,7 +93,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 From 79b1ec20ba5f0a452ca5e58ad8d8a5e305375073 Mon Sep 17 00:00:00 2001 From: Ian Huff Date: Thu, 20 Jun 2019 16:11:40 -0700 Subject: [PATCH 2/4] add news --- news/2 Fixes/6265.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/2 Fixes/6265.md diff --git a/news/2 Fixes/6265.md b/news/2 Fixes/6265.md new file mode 100644 index 000000000000..ea79dc3f953b --- /dev/null +++ b/news/2 Fixes/6265.md @@ -0,0 +1 @@ +Allow for both password and self cert server to work together \ No newline at end of file From 579f07f9a7a3eb4aa4027d43dd7ab73d1f2b087f Mon Sep 17 00:00:00 2001 From: Ian Huff Date: Thu, 20 Jun 2019 16:55:15 -0700 Subject: [PATCH 3/4] add unit test --- .../jupyterPasswordConnect.unit.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/test/datascience/jupyterPasswordConnect.unit.test.ts b/src/test/datascience/jupyterPasswordConnect.unit.test.ts index aa8f2c267ff3..43766ef86ff3 100644 --- a/src/test/datascience/jupyterPasswordConnect.unit.test.ts +++ b/src/test/datascience/jupyterPasswordConnect.unit.test.ts @@ -73,6 +73,54 @@ suite('JupyterPasswordConnect', () => { fetchMock.verifyAll(); }); + test('getPasswordConnectionInfo allowUnauthorized', async() => { + // Set up our fake node fetch + const fetchMock: typemoq.IMock = 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'); + 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 failure', async() => { // Set up our fake node fetch const fetchMock: typemoq.IMock = typemoq.Mock.ofInstance(nodeFetch.default); From de6ffe16d090a4cf84f369af2d6ff9699e68ad2a Mon Sep 17 00:00:00 2001 From: Ian Huff Date: Fri, 21 Jun 2019 08:44:01 -0700 Subject: [PATCH 4/4] function rename --- src/client/datascience/jupyter/jupyterPasswordConnect.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/datascience/jupyter/jupyterPasswordConnect.ts b/src/client/datascience/jupyter/jupyterPasswordConnect.ts index ebf56457e2bc..1b2f1f857791 100644 --- a/src/client/datascience/jupyter/jupyterPasswordConnect.ts +++ b/src/client/datascience/jupyter/jupyterPasswordConnect.ts @@ -67,7 +67,7 @@ export class JupyterPasswordConnect implements IJupyterPasswordConnect { } // For HTTPS connections respect our allowUnauthorized setting by adding in an agent to enable that on the request - private allowUnauthorized(url: string, allowUnauthorized: boolean, options: nodeFetch.RequestInit): nodeFetch.RequestInit { + 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}; @@ -88,7 +88,7 @@ export class JupyterPasswordConnect implements IJupyterPasswordConnect { private async getXSRFToken(url: string, allowUnauthorized: boolean, fetchFunction: (url: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) => Promise): Promise { let xsrfCookie: string | undefined; - const response = await fetchFunction(`${url}login?`, this.allowUnauthorized(url, allowUnauthorized, { + const response = await fetchFunction(`${url}login?`, this.addAllowUnauthorized(url, allowUnauthorized, { method: 'get', redirect: 'manual', headers: { Connection: 'keep-alive' } @@ -120,7 +120,7 @@ export class JupyterPasswordConnect implements IJupyterPasswordConnect { postParams.append('_xsrf', xsrfCookie); postParams.append('password', password); - const response = await fetchFunction(`${url}login?`, this.allowUnauthorized(url, allowUnauthorized, { + 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(),