@@ -5,9 +5,10 @@ import type { ContentsManager, ServerConnection, Session, SessionManager } from
55import { Agent as HttpsAgent } from 'https' ;
66import * as nodeFetch from 'node-fetch' ;
77import { CancellationToken } from 'vscode-jsonrpc' ;
8+ import { IApplicationShell } from '../../common/application/types' ;
89
910import { traceError , traceInfo } from '../../common/logger' ;
10- import { IConfigurationService , IOutputChannel } from '../../common/types' ;
11+ import { IConfigurationService , IOutputChannel , IPersistentState , IPersistentStateFactory } from '../../common/types' ;
1112import { sleep } from '../../common/utils/async' ;
1213import * as localize from '../../common/utils/localize' ;
1314import { noop } from '../../common/utils/misc' ;
@@ -27,14 +28,19 @@ import { JupyterKernelSpec } from './kernels/jupyterKernelSpec';
2728import { KernelSelector } from './kernels/kernelSelector' ;
2829import { LiveKernelModel } from './kernels/types' ;
2930
31+ // Key for our insecure connection global state
32+ const GlobalStateUserAllowsInsecureConnections = 'DataScienceAllowInsecureConnections' ;
33+
3034// tslint:disable: no-any
3135
3236export class JupyterSessionManager implements IJupyterSessionManager {
37+ private static secureServers = new Map < string , Promise < boolean > > ( ) ;
3338 private sessionManager : SessionManager | undefined ;
3439 private contentsManager : ContentsManager | undefined ;
3540 private connInfo : IJupyterConnection | undefined ;
3641 private serverSettings : ServerConnection . ISettings | undefined ;
3742 private _jupyterlab ?: typeof import ( '@jupyterlab/services' ) ;
43+ private readonly userAllowsInsecureConnections : IPersistentState < boolean > ;
3844 private get jupyterlab ( ) : typeof import ( '@jupyterlab/services' ) {
3945 if ( ! this . _jupyterlab ) {
4046 // tslint:disable-next-line: no-require-imports
@@ -48,8 +54,15 @@ export class JupyterSessionManager implements IJupyterSessionManager {
4854 private failOnPassword : boolean | undefined ,
4955 private kernelSelector : KernelSelector ,
5056 private outputChannel : IOutputChannel ,
51- private configService : IConfigurationService
52- ) { }
57+ private configService : IConfigurationService ,
58+ private readonly appShell : IApplicationShell ,
59+ private readonly stateFactory : IPersistentStateFactory
60+ ) {
61+ this . userAllowsInsecureConnections = this . stateFactory . createGlobalPersistentState < boolean > (
62+ GlobalStateUserAllowsInsecureConnections ,
63+ false
64+ ) ;
65+ }
5366
5467 public async dispose ( ) {
5568 traceInfo ( `Disposing session manager` ) ;
@@ -229,6 +242,9 @@ export class JupyterSessionManager implements IJupyterSessionManager {
229242 wsUrl : connInfo . baseUrl . replace ( 'http' , 'ws' )
230243 } ;
231244
245+ // Before we connect, see if we are trying to make an insecure connection, if we are, warn the user
246+ await this . secureConnectionCheck ( connInfo ) ;
247+
232248 // Agent is allowed to be set on this object, but ts doesn't like it on RequestInit, so any
233249 // tslint:disable-next-line:no-any
234250 let requestInit : any = { cache : 'no-store' , credentials : 'same-origin' } ;
@@ -305,4 +321,58 @@ export class JupyterSessionManager implements IJupyterSessionManager {
305321 traceInfo ( `Creating server with settings : ${ JSON . stringify ( serverSettings ) } ` ) ;
306322 return this . jupyterlab . ServerConnection . makeSettings ( serverSettings ) ;
307323 }
324+
325+ // If connecting on HTTP without a token prompt the user that this connection may not be secure
326+ private async insecureServerWarningPrompt ( ) : Promise < boolean > {
327+ const insecureMessage = localize . DataScience . insecureSessionMessage ( ) ;
328+ const insecureLabels = [
329+ localize . Common . bannerLabelYes ( ) ,
330+ localize . Common . bannerLabelNo ( ) ,
331+ localize . Common . doNotShowAgain ( )
332+ ] ;
333+ const response = await this . appShell . showWarningMessage ( insecureMessage , ...insecureLabels ) ;
334+
335+ switch ( response ) {
336+ case localize . Common . bannerLabelYes ( ) :
337+ // On yes just proceed as normal
338+ return true ;
339+
340+ case localize . Common . doNotShowAgain ( ) :
341+ // For don't ask again turn on the global true
342+ await this . userAllowsInsecureConnections . updateValue ( true ) ;
343+ return true ;
344+
345+ case localize . Common . bannerLabelNo ( ) :
346+ default :
347+ // No or for no choice return back false to block
348+ return false ;
349+ }
350+ }
351+
352+ // Check if our server connection is considered secure. If it is not, ask the user if they want to connect
353+ // If not, throw to bail out on the process
354+ private async secureConnectionCheck ( connInfo : IJupyterConnection ) : Promise < void > {
355+ // If they have turned on global server trust then everything is secure
356+ if ( this . userAllowsInsecureConnections . value ) {
357+ return ;
358+ }
359+
360+ // If they are local launch, https, or have a token, then they are secure
361+ if ( connInfo . localLaunch || connInfo . baseUrl . startsWith ( 'https' ) || connInfo . token !== 'null' ) {
362+ return ;
363+ }
364+
365+ // At this point prompt the user, cache the promise so we don't ask multiple times for the same server
366+ let serverSecurePromise = JupyterSessionManager . secureServers . get ( connInfo . baseUrl ) ;
367+
368+ if ( serverSecurePromise === undefined ) {
369+ serverSecurePromise = this . insecureServerWarningPrompt ( ) ;
370+ JupyterSessionManager . secureServers . set ( connInfo . baseUrl , serverSecurePromise ) ;
371+ }
372+
373+ // If our server is not secure, throw here to bail out on the process
374+ if ( ! ( await serverSecurePromise ) ) {
375+ throw new Error ( localize . DataScience . insecureSessionDenied ( ) ) ;
376+ }
377+ }
308378}
0 commit comments