20
20
import threading
21
21
import webbrowser
22
22
23
+ from .voila_identity_provider import VoilaLoginHandler
24
+
23
25
try :
24
26
from urllib .parse import urljoin
25
27
from urllib .request import pathname2url
35
37
from jupyter_core .paths import jupyter_config_path , jupyter_path
36
38
from jupyter_server .base .handlers import FileFindHandler , path_regex
37
39
from jupyter_server .config_manager import recursive_update
38
- from jupyter_server .services .config import ConfigManager
40
+ from jupyter_server .services .config . manager import ConfigManager
39
41
from jupyter_server .services .contents .largefilemanager import LargeFileManager
40
- from jupyter_server .services .kernels .handlers import KernelHandler , ZMQChannelsHandler
41
- from jupyter_server .utils import run_sync , url_path_join
42
+ from jupyter_server .services .kernels .handlers import KernelHandler
43
+ from jupyter_server .services .kernels .websocket import KernelWebsocketHandler
44
+ from jupyter_server .auth .authorizer import AllowAllAuthorizer , Authorizer
45
+ from jupyter_server .auth .identity import PasswordIdentityProvider
46
+ from jupyter_server import DEFAULT_TEMPLATE_PATH_LIST , DEFAULT_STATIC_FILES_PATH
47
+ from jupyter_server .services .kernels .connection .base import (
48
+ BaseKernelWebsocketConnection ,
49
+ )
50
+ from jupyter_server .services .kernels .connection .channels import (
51
+ ZMQChannelsWebsocketConnection ,
52
+ )
53
+ from jupyter_server .auth .identity import (
54
+ IdentityProvider ,
55
+ )
56
+ from jupyter_server .utils import url_path_join
57
+ from jupyter_core .utils import run_sync
58
+
42
59
from jupyterlab_server .themes_handler import ThemesHandler
43
- from traitlets import Bool , Callable , Dict , Integer , List , Unicode , default
60
+
61
+
62
+ from traitlets import Bool , Callable , Dict , Integer , List , Unicode , default , Type , Bytes
44
63
from traitlets .config .application import Application
45
64
from traitlets .config .loader import Config
46
65
@@ -130,6 +149,7 @@ class Voila(Application):
130
149
"port" : "Voila.port" ,
131
150
"static" : "Voila.static_root" ,
132
151
"server_url" : "Voila.server_url" ,
152
+ "token" : "Voila.token" ,
133
153
"pool_size" : "VoilaConfiguration.default_pool_size" ,
134
154
"show_tracebacks" : "VoilaConfiguration.show_tracebacks" ,
135
155
"preheat_kernel" : "VoilaConfiguration.preheat_kernel" ,
@@ -272,6 +292,47 @@ def hook(req: tornado.web.RequestHandler,
272
292
),
273
293
)
274
294
295
+ cookie_secret = Bytes (
296
+ b"" ,
297
+ config = True ,
298
+ help = """The random bytes used to secure cookies.
299
+ By default this is a new random number every time you start the server.
300
+ Set it to a value in a config file to enable logins to persist across server sessions.
301
+
302
+ Note: Cookie secrets should be kept private, do not share config files with
303
+ cookie_secret stored in plaintext (you can read the value from a file).
304
+ """ ,
305
+ )
306
+
307
+ token = Unicode (None , help = """Token for identity provider """ , allow_none = True ).tag (
308
+ config = True
309
+ )
310
+
311
+ @default ("cookie_secret" )
312
+ def _default_cookie_secret (self ):
313
+ return os .urandom (32 )
314
+
315
+ authorizer_class = Type (
316
+ default_value = AllowAllAuthorizer ,
317
+ klass = Authorizer ,
318
+ config = True ,
319
+ help = _ ("The authorizer class to use." ),
320
+ )
321
+
322
+ identity_provider_class = Type (
323
+ default_value = PasswordIdentityProvider ,
324
+ klass = IdentityProvider ,
325
+ config = True ,
326
+ help = _ ("The identity provider class to use." ),
327
+ )
328
+
329
+ kernel_websocket_connection_class = Type (
330
+ default_value = ZMQChannelsWebsocketConnection ,
331
+ klass = BaseKernelWebsocketConnection ,
332
+ config = True ,
333
+ help = _ ("The kernel websocket connection class to use." ),
334
+ )
335
+
275
336
@property
276
337
def display_url (self ):
277
338
if self .custom_display_url :
@@ -282,13 +343,17 @@ def display_url(self):
282
343
ip = "%s" % socket .gethostname () if self .ip in ("" , "0.0.0.0" ) else self .ip
283
344
url = self ._url (ip )
284
345
# TODO: do we want to have the token?
285
- # if self.token:
286
- # # Don't log full token if it came from config
287
- # token = self.token if self._token_generated else '...'
288
- # url = (url_concat(url, {'token': token})
289
- # + '\n or '
290
- # + url_concat(self._url('127.0.0.1'), {'token': token}))
291
- return url
346
+ if self .identity_provider .token :
347
+ # Don't log full token if it came from config
348
+ token = (
349
+ self .identity_provider .token
350
+ if self .identity_provider .token_generated
351
+ else "..."
352
+ )
353
+ query = f"?token={ token } "
354
+ else :
355
+ query = ""
356
+ return f"{ url } { query } "
292
357
293
358
@property
294
359
def connection_url (self ):
@@ -405,6 +470,7 @@ def setup_template_dirs(self):
405
470
self .static_paths = collect_static_paths (
406
471
["voila" , "nbconvert" ], template_name
407
472
)
473
+ self .static_paths .append (DEFAULT_STATIC_FILES_PATH )
408
474
conf_paths = [os .path .join (d , "conf.json" ) for d in self .template_paths ]
409
475
for p in conf_paths :
410
476
# see if config file exists
@@ -428,17 +494,8 @@ def setup_template_dirs(self):
428
494
if self .notebook_path and not os .path .exists (self .notebook_path ):
429
495
raise ValueError ("Notebook not found: %s" % self .notebook_path )
430
496
431
- def _handle_signal_stop (self , sig , frame ):
432
- self .log .info ("Handle signal %s." % sig )
433
- self .ioloop .add_callback_from_signal (self .ioloop .stop )
434
-
435
- def start (self ):
436
- self .connection_dir = tempfile .mkdtemp (
437
- prefix = "voila_" , dir = self .connection_dir_root
438
- )
439
- self .log .info ("Storing connection files in %s." % self .connection_dir )
440
- self .log .info ("Serving static files from %s." % self .static_root )
441
-
497
+ def init_settings (self ) -> Dict :
498
+ """Initialize settings for Voila application."""
442
499
# default server_url to base_url
443
500
self .server_url = self .server_url or self .base_url
444
501
@@ -486,28 +543,58 @@ def start(self):
486
543
extensions = ["jinja2.ext.i18n" ],
487
544
** jenv_opt ,
488
545
)
546
+ server_env = jinja2 .Environment (
547
+ loader = jinja2 .FileSystemLoader (DEFAULT_TEMPLATE_PATH_LIST ),
548
+ extensions = ["jinja2.ext.i18n" ],
549
+ ** jenv_opt ,
550
+ )
551
+
489
552
nbui = gettext .translation (
490
553
"nbui" , localedir = os .path .join (ROOT , "i18n" ), fallback = True
491
554
)
492
555
env .install_gettext_translations (nbui , newstyle = False )
556
+ server_env .install_gettext_translations (nbui , newstyle = False )
557
+
558
+ identity_provider_kwargs = {
559
+ "parent" : self ,
560
+ "log" : self .log ,
561
+ "login_handler_class" : VoilaLoginHandler ,
562
+ }
563
+ if self .token is None :
564
+ identity_provider_kwargs ["token" ] = ""
565
+
566
+ self .identity_provider = self .identity_provider_class (
567
+ ** identity_provider_kwargs
568
+ )
493
569
494
- self .app = tornado .web .Application (
570
+ self .authorizer = self .authorizer_class (
571
+ parent = self , log = self .log , identity_provider = self .identity_provider
572
+ )
573
+
574
+ settings = dict (
495
575
base_url = self .base_url ,
496
576
server_url = self .server_url or self .base_url ,
497
577
kernel_manager = self .kernel_manager ,
498
578
kernel_spec_manager = self .kernel_spec_manager ,
499
579
allow_remote_access = True ,
500
580
autoreload = self .autoreload ,
501
581
voila_jinja2_env = env ,
502
- jinja2_env = env ,
582
+ jinja2_env = server_env ,
503
583
static_path = "/" ,
504
584
server_root_dir = "/" ,
505
585
contents_manager = self .contents_manager ,
506
586
config_manager = self .config_manager ,
587
+ cookie_secret = self .cookie_secret ,
588
+ authorizer = self .authorizer ,
589
+ identity_provider = self .identity_provider ,
590
+ kernel_websocket_connection_class = self .kernel_websocket_connection_class ,
591
+ login_url = url_path_join (self .base_url , "/login" ),
507
592
)
508
593
509
- self . app . settings . update ( self . tornado_settings )
594
+ return settings
510
595
596
+ def init_handlers (self ) -> List :
597
+ """Initialize handlers for Voila application."""
511
598
handlers = []
512
599
513
600
handlers .extend (
@@ -522,7 +609,7 @@ def start(self):
522
609
url_path_join (
523
610
self .server_url , r"/api/kernels/%s/channels" % _kernel_id_regex
524
611
),
525
- ZMQChannelsHandler ,
612
+ KernelWebsocketHandler ,
526
613
),
527
614
(
528
615
url_path_join (self .server_url , r"/voila/templates/(.*)" ),
@@ -549,8 +636,8 @@ def start(self):
549
636
),
550
637
]
551
638
)
552
-
553
- if preheat_kernel :
639
+ handlers . extend ( self . identity_provider . get_handlers ())
640
+ if self . voila_configuration . preheat_kernel :
554
641
handlers .append (
555
642
(
556
643
url_path_join (
@@ -621,13 +708,30 @@ def start(self):
621
708
),
622
709
]
623
710
)
711
+ return handlers
712
+
713
+ def start (self ):
714
+ self .connection_dir = tempfile .mkdtemp (
715
+ prefix = "voila_" , dir = self .connection_dir_root
716
+ )
717
+ self .log .info ("Storing connection files in %s." % self .connection_dir )
718
+ self .log .info ("Serving static files from %s." % self .static_root )
719
+
720
+ settings = self .init_settings ()
624
721
722
+ self .app = tornado .web .Application (** settings )
723
+ self .app .settings .update (self .tornado_settings )
724
+ handlers = self .init_handlers ()
625
725
self .app .add_handlers (".*$" , handlers )
626
726
self .listen ()
627
727
728
+ def _handle_signal_stop (self , sig , frame ):
729
+ self .log .info ("Handle signal %s." % sig )
730
+ self .ioloop .add_callback_from_signal (self .ioloop .stop )
731
+
628
732
def stop (self ):
629
733
shutil .rmtree (self .connection_dir )
630
- run_sync (self .kernel_manager .shutdown_all () )
734
+ run_sync (self .kernel_manager .shutdown_all )( )
631
735
632
736
def random_ports (self , port , n ):
633
737
"""Generate a list of n random ports near the given port.
0 commit comments