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
@@ -272,6 +291,43 @@ def hook(req: tornado.web.RequestHandler,
272
291
),
273
292
)
274
293
294
+ cookie_secret = Bytes (
295
+ b"" ,
296
+ config = True ,
297
+ help = """The random bytes used to secure cookies.
298
+ By default this is a new random number every time you start the server.
299
+ Set it to a value in a config file to enable logins to persist across server sessions.
300
+
301
+ Note: Cookie secrets should be kept private, do not share config files with
302
+ cookie_secret stored in plaintext (you can read the value from a file).
303
+ """ ,
304
+ )
305
+
306
+ @default ("cookie_secret" )
307
+ def _default_cookie_secret (self ):
308
+ return os .urandom (32 )
309
+
310
+ authorizer_class = Type (
311
+ default_value = AllowAllAuthorizer ,
312
+ klass = Authorizer ,
313
+ config = True ,
314
+ help = _ ("The authorizer class to use." ),
315
+ )
316
+
317
+ identity_provider_class = Type (
318
+ default_value = PasswordIdentityProvider ,
319
+ klass = IdentityProvider ,
320
+ config = True ,
321
+ help = _ ("The identity provider class to use." ),
322
+ )
323
+
324
+ kernel_websocket_connection_class = Type (
325
+ default_value = ZMQChannelsWebsocketConnection ,
326
+ klass = BaseKernelWebsocketConnection ,
327
+ config = True ,
328
+ help = _ ("The kernel websocket connection class to use." ),
329
+ )
330
+
275
331
@property
276
332
def display_url (self ):
277
333
if self .custom_display_url :
@@ -282,13 +338,17 @@ def display_url(self):
282
338
ip = "%s" % socket .gethostname () if self .ip in ("" , "0.0.0.0" ) else self .ip
283
339
url = self ._url (ip )
284
340
# 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
341
+ if self .identity_provider .token :
342
+ # Don't log full token if it came from config
343
+ token = (
344
+ self .identity_provider .token
345
+ if self .identity_provider .token_generated
346
+ else "..."
347
+ )
348
+ query = f"?token={ token } "
349
+ else :
350
+ query = ""
351
+ return f"{ url } { query } "
292
352
293
353
@property
294
354
def connection_url (self ):
@@ -405,6 +465,7 @@ def setup_template_dirs(self):
405
465
self .static_paths = collect_static_paths (
406
466
["voila" , "nbconvert" ], template_name
407
467
)
468
+ self .static_paths .append (DEFAULT_STATIC_FILES_PATH )
408
469
conf_paths = [os .path .join (d , "conf.json" ) for d in self .template_paths ]
409
470
for p in conf_paths :
410
471
# see if config file exists
@@ -428,17 +489,8 @@ def setup_template_dirs(self):
428
489
if self .notebook_path and not os .path .exists (self .notebook_path ):
429
490
raise ValueError ("Notebook not found: %s" % self .notebook_path )
430
491
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
-
492
+ def init_settings (self ) -> Dict :
493
+ """Initialize settings for Voila application."""
442
494
# default server_url to base_url
443
495
self .server_url = self .server_url or self .base_url
444
496
@@ -486,28 +538,55 @@ def start(self):
486
538
extensions = ["jinja2.ext.i18n" ],
487
539
** jenv_opt ,
488
540
)
541
+ server_env = jinja2 .Environment (
542
+ loader = jinja2 .FileSystemLoader (DEFAULT_TEMPLATE_PATH_LIST ),
543
+ extensions = ["jinja2.ext.i18n" ],
544
+ ** jenv_opt ,
545
+ )
546
+
489
547
nbui = gettext .translation (
490
548
"nbui" , localedir = os .path .join (ROOT , "i18n" ), fallback = True
491
549
)
492
550
env .install_gettext_translations (nbui , newstyle = False )
551
+ server_env .install_gettext_translations (nbui , newstyle = False )
552
+
553
+ identity_provider_kwargs = {
554
+ "parent" : self ,
555
+ "log" : self .log ,
556
+ "login_handler_class" : VoilaLoginHandler ,
557
+ }
558
+ self .identity_provider = self .identity_provider_class (
559
+ ** identity_provider_kwargs
560
+ )
493
561
494
- self .app = tornado .web .Application (
562
+ self .authorizer = self .authorizer_class (
563
+ parent = self , log = self .log , identity_provider = self .identity_provider
564
+ )
565
+
566
+ settings = dict (
495
567
base_url = self .base_url ,
496
568
server_url = self .server_url or self .base_url ,
497
569
kernel_manager = self .kernel_manager ,
498
570
kernel_spec_manager = self .kernel_spec_manager ,
499
571
allow_remote_access = True ,
500
572
autoreload = self .autoreload ,
501
573
voila_jinja2_env = env ,
502
- jinja2_env = env ,
574
+ jinja2_env = server_env ,
503
575
static_path = "/" ,
504
576
server_root_dir = "/" ,
505
577
contents_manager = self .contents_manager ,
506
578
config_manager = self .config_manager ,
579
+ cookie_secret = self .cookie_secret ,
580
+ authorizer = self .authorizer ,
581
+ identity_provider = self .identity_provider ,
582
+ kernel_websocket_connection_class = self .kernel_websocket_connection_class ,
583
+ login_url = url_path_join (self .base_url , "/login" ),
507
584
)
508
585
509
- self . app . settings . update ( self . tornado_settings )
586
+ return settings
510
587
588
+ def init_handlers (self ) -> List :
589
+ """Initialize handlers for Voila application."""
511
590
handlers = []
512
591
513
592
handlers .extend (
@@ -522,7 +601,7 @@ def start(self):
522
601
url_path_join (
523
602
self .server_url , r"/api/kernels/%s/channels" % _kernel_id_regex
524
603
),
525
- ZMQChannelsHandler ,
604
+ KernelWebsocketHandler ,
526
605
),
527
606
(
528
607
url_path_join (self .server_url , r"/voila/templates/(.*)" ),
@@ -549,8 +628,8 @@ def start(self):
549
628
),
550
629
]
551
630
)
552
-
553
- if preheat_kernel :
631
+ handlers . extend ( self . identity_provider . get_handlers ())
632
+ if self . voila_configuration . preheat_kernel :
554
633
handlers .append (
555
634
(
556
635
url_path_join (
@@ -621,13 +700,30 @@ def start(self):
621
700
),
622
701
]
623
702
)
703
+ return handlers
704
+
705
+ def start (self ):
706
+ self .connection_dir = tempfile .mkdtemp (
707
+ prefix = "voila_" , dir = self .connection_dir_root
708
+ )
709
+ self .log .info ("Storing connection files in %s." % self .connection_dir )
710
+ self .log .info ("Serving static files from %s." % self .static_root )
711
+
712
+ settings = self .init_settings ()
624
713
714
+ self .app = tornado .web .Application (** settings )
715
+ self .app .settings .update (self .tornado_settings )
716
+ handlers = self .init_handlers ()
625
717
self .app .add_handlers (".*$" , handlers )
626
718
self .listen ()
627
719
720
+ def _handle_signal_stop (self , sig , frame ):
721
+ self .log .info ("Handle signal %s." % sig )
722
+ self .ioloop .add_callback_from_signal (self .ioloop .stop )
723
+
628
724
def stop (self ):
629
725
shutil .rmtree (self .connection_dir )
630
- run_sync (self .kernel_manager .shutdown_all () )
726
+ run_sync (self .kernel_manager .shutdown_all )( )
631
727
632
728
def random_ports (self , port , n ):
633
729
"""Generate a list of n random ports near the given port.
0 commit comments