diff --git a/docs/source/standalone.md b/docs/source/standalone.md index 4ad1caaa..4b09b026 100644 --- a/docs/source/standalone.md +++ b/docs/source/standalone.md @@ -51,7 +51,7 @@ jupyter standaloneproxy --address=localhost --port=8000 ... ### Disable Authentication -For testing, it can be useful to disable the authentication with JupyterHub. Passing `--skip-authentication` will +For testing, it can be useful to disable the authentication with JupyterHub. Passing `--no-authentication` will not trigger the login process when accessing the application. ```{warning} Disabling authentication will leave the application open to anyone! Be careful with it, @@ -76,7 +76,7 @@ c.StandaloneProxyServer.address = "localhost" c.StandaloneProxyServer.port = 8000 # Disable authentication -c.StandaloneProxyServer.skip_authentication = True +c.StandaloneProxyServer.no_authentication = True ``` A default config file can be emitted by running `jupyter standaloneproxy --generate-config` diff --git a/jupyter_server_proxy/__init__.py b/jupyter_server_proxy/__init__.py index cbce0d5f..45f77475 100644 --- a/jupyter_server_proxy/__init__.py +++ b/jupyter_server_proxy/__init__.py @@ -3,7 +3,7 @@ from ._version import __version__ # noqa from .api import IconHandler, ServersInfoHandler from .config import ServerProxy as ServerProxyConfig -from .config import get_entrypoint_server_processes, make_handlers, make_server_process +from .config import get_entrypoint_server_processes, make_handlers from .handlers import setup_handlers @@ -41,11 +41,8 @@ def _load_jupyter_server_extension(nbapp): base_url = nbapp.web_app.settings["base_url"] serverproxy_config = ServerProxyConfig(parent=nbapp) - server_processes = [ - make_server_process(name, server_process_config, serverproxy_config) - for name, server_process_config in serverproxy_config.servers.items() - ] - server_processes += get_entrypoint_server_processes(serverproxy_config) + server_processes = list(serverproxy_config.servers.values()) + server_processes += get_entrypoint_server_processes() server_handlers = make_handlers(base_url, server_processes) nbapp.web_app.add_handlers(".*", server_handlers) diff --git a/jupyter_server_proxy/config.py b/jupyter_server_proxy/config.py index 67132432..46d5b640 100644 --- a/jupyter_server_proxy/config.py +++ b/jupyter_server_proxy/config.py @@ -19,6 +19,7 @@ Callable, Dict, Float, + HasTraits, Instance, Int, List, @@ -35,7 +36,7 @@ from .rawsocket import RawSocketHandler, SuperviseAndRawSocketHandler -class LauncherEntry(Configurable): +class LauncherEntry(HasTraits): enabled = Bool( True, help=""" @@ -76,7 +77,7 @@ class LauncherEntry(Configurable): class ServerProcess(Configurable): - name = Unicode(help="Name of the server").tag(config=True) + name = Unicode(help="Name of the server") command = Union( [List(Unicode()), Callable()], @@ -92,7 +93,7 @@ class ServerProcess(Configurable): process is assumed to be started ahead of time and already available to be proxied to. """, - ).tag(config=True) + ) environment = Union( [Dict(Unicode()), Callable()], @@ -115,14 +116,14 @@ class ServerProcess(Configurable): Proxy requests default to being rewritten to ``/``. If this is True, the absolute URL will be sent to the backend instead. """, - ).tag(config=True) + ) port = Int( 0, help=""" Set the port that the service will listen on. The default is to automatically select an unused port. """, - ).tag(config=True) + ) unix_socket = Union( [Bool(False), Unicode()], @@ -135,7 +136,7 @@ class ServerProcess(Configurable): Proxying websockets over a Unix socket requires Tornado >= 6.3. """, - ).tag(config=True) + ) mappath = Union( [Dict(Unicode()), Callable()], @@ -145,15 +146,15 @@ class ServerProcess(Configurable): Either a dictionary of request paths to proxied paths, or a callable that takes parameter ``path`` and returns the proxied path. """, - ).tag(config=True) + ) launcher_entry = Union( [Instance(LauncherEntry), Dict()], allow_none=False, help=""" - A dictionary of various options for entries in classic notebook / jupyterlab launchers. + Specify various options for entries in classic notebook / jupyterlab launchers. - Keys recognized are: + Must be an instance of ``LauncherEntry`` or a dictionary with the following keys: ``enabled`` Set to True (default) to make an entry in the launchers. Set to False to have no @@ -174,13 +175,18 @@ class ServerProcess(Configurable): The category for the launcher item. Currently only used by the JupyterLab launcher. By default it is "Notebook". """, - ).tag(config=True) + ) @validate("launcher_entry") def _validate_launcher_entry(self, proposal): - kwargs = {"title": self.name, "path_info": self.name + "/"} - kwargs.update(proposal["value"]) - return LauncherEntry(**kwargs) + if isinstance(proposal["value"], LauncherEntry): + proposal["value"].title = self.name + proposal["value"].path_info = self.name + "/" + return proposal["value"] + else: + kwargs = {"title": self.name, "path_info": self.name + "/"} + kwargs.update(proposal["value"]) + return LauncherEntry(**kwargs) @default("launcher_entry") def _default_launcher_entry(self): @@ -201,7 +207,7 @@ def _default_launcher_entry(self): A dictionary of additional HTTP headers for the proxy request. As with the command traitlet, ``{port}``, ``{unix_socket}`` and ``{base_url}`` will be substituted. """, - ).tag(config=True) + ) rewrite_response = Union( [Callable(), List(Callable())], @@ -260,7 +266,7 @@ def cats_only(response, path): similar to running a websockify layer (https://github.com/novnc/websockify). All other HTTP requests return 405 (and thus this will also bypass rewrite_response). """, - ).tag(config=True) + ) def get_proxy_base_class(self) -> tuple[type | None, dict]: """ @@ -341,8 +347,8 @@ def get_timeout(self): return _Proxy, proxy_kwargs -def get_entrypoint_server_processes(serverproxy_config): - sps = [] +def get_entrypoint_server_processes(): + processes = [] for entry_point in entry_points(group="jupyter_serverproxy_servers"): name = entry_point.name try: @@ -350,8 +356,8 @@ def get_entrypoint_server_processes(serverproxy_config): except Exception as e: warn(f"entry_point {name} was unable to be loaded: {str(e)}") continue - sps.append(make_server_process(name, server_process_config, serverproxy_config)) - return sps + processes.append(ServerProcess(name=name, **server_process_config)) + return processes def make_handlers(base_url: str, server_processes: list[ServerProcess]): @@ -384,20 +390,36 @@ def _serverproxy_servers_help(): class ServerProxy(Configurable): servers = Dict( - {}, + key_trait=Unicode(), + value_trait=Union([Dict(), Instance(ServerProcess)]), help=""" Dictionary of processes to supervise & proxy. Key should be the name of the process. This is also used by default as the URL prefix, and all requests matching this prefix are routed to this process. - Value should be a dictionary with the following keys: + Value should be an instance of ``ServerProcess`` or a dictionary with the following keys: """ + indent(_serverproxy_servers_help(), " "), config=True, ) + @validate("servers") + def _validate_servers(self, proposal): + servers = {} + + for name, server_process in proposal["value"].items(): + if isinstance(server_process, ServerProcess): + server_process.name = server_process.name or name + servers[name] = server_process + else: + kwargs = {"name": name} + kwargs.update(**server_process) + servers[name] = ServerProcess(**kwargs) + + return servers + non_service_rewrite_response = Union( default_value=tuple(), trait_types=[List(), Tuple(), Callable()], diff --git a/jupyter_server_proxy/standalone/app.py b/jupyter_server_proxy/standalone/app.py index 0b0fbfd8..187061ce 100644 --- a/jupyter_server_proxy/standalone/app.py +++ b/jupyter_server_proxy/standalone/app.py @@ -58,7 +58,7 @@ def _validate_prefix(self, proposal): prefix = prefix[:-1] return prefix - skip_authentication = Bool( + no_authentication = Bool( default=False, help=""" Do not authenticate access to the server via JupyterHub. When set, @@ -146,11 +146,16 @@ def __init__(self, **kwargs): {"ServerProcess": {"raw_socket_proxy": True}}, dedent(ServerProcess.raw_socket_proxy.help), ), - "skip-authentication": ( - {"StandaloneProxyServer": {"skip_authentication": True}}, - dedent(self.__class__.skip_authentication.help), + "no-authentication": ( + {"StandaloneProxyServer": {"no_authentication": True}}, + dedent(self.__class__.no_authentication.help), ), } + self.flags.pop("y") + + # Some traits in ServerProcess are not defined to be configurable, but we need that for the standalone proxy + for name, trait in ServerProcess.class_own_traits().items(): + trait.tag(config=True) # Create an Alias to all Traits defined in ServerProcess, with some # exceptions we do not need, for easier use of the CLI @@ -164,7 +169,7 @@ def __init__(self, **kwargs): "command", ] server_process_aliases = { - trait: f"StandaloneProxyServer.{trait}" + trait.replace("_", "-"): f"StandaloneProxyServer.{trait}" for trait in ServerProcess.class_traits(config=True) if trait not in ignore_traits and trait not in self.flags } @@ -172,12 +177,12 @@ def __init__(self, **kwargs): self.aliases = { **super().aliases, **server_process_aliases, - "base_url": "StandaloneProxyServer.base_url", + "base-url": "StandaloneProxyServer.base_url", "address": "StandaloneProxyServer.address", "port": "StandaloneProxyServer.port", - "server_port": "StandaloneProxyServer.server_port", - "activity_interval": "StandaloneProxyServer.activity_interval", - "websocket_max_message_size": "StandaloneProxyServer.websocket_max_message_size", + "server-port": "StandaloneProxyServer.server_port", + "activity-interval": "StandaloneProxyServer.activity_interval", + "websocket-max-message-size": "StandaloneProxyServer.websocket_max_message_size", } def emit_alias_help(self): @@ -206,7 +211,7 @@ def get_proxy_attributes(self) -> dict: attributes["proxy_base"] = "/" attributes["requested_port"] = self.server_port - attributes["skip_authentication"] = self.skip_authentication + attributes["no_authentication"] = self.no_authentication return attributes @@ -278,7 +283,7 @@ def _configure_ssl(self) -> dict | None: return ssl_options def start(self): - if self.skip_authentication: + if self.no_authentication: self.log.warn("Disabling Authentication with JuypterHub Server!") app = self.create_app() diff --git a/jupyter_server_proxy/standalone/proxy.py b/jupyter_server_proxy/standalone/proxy.py index 35c30991..cc266102 100644 --- a/jupyter_server_proxy/standalone/proxy.py +++ b/jupyter_server_proxy/standalone/proxy.py @@ -32,7 +32,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.environment = {} self.timeout = 60 - self.skip_authentication = False + self.no_authentication = False @property def log(self) -> Logger: @@ -69,7 +69,7 @@ def write_error(self, status_code: int, **kwargs): return RequestHandler.write_error(self, status_code, **kwargs) async def proxy(self, port, path): - if self.skip_authentication: + if self.no_authentication: return await super().proxy(port, path) else: return await ensure_async(self.oauth_proxy(port, path)) diff --git a/tests/test_standalone.py b/tests/test_standalone.py index cc55548d..fbc0066a 100644 --- a/tests/test_standalone.py +++ b/tests/test_standalone.py @@ -40,7 +40,7 @@ class _TestStandaloneBase(testing.AsyncHTTPTestCase): runTest = None # Required for Tornado 6.1 unix_socket: bool - skip_authentication: bool + no_authentication: bool def get_app(self): command = [ @@ -55,7 +55,7 @@ def get_app(self): base_url="/some/prefix", unix_socket=self.unix_socket, timeout=60, - skip_authentication=self.skip_authentication, + no_authentication=self.no_authentication, log_level=logging.DEBUG, ) @@ -69,7 +69,7 @@ class TestStandaloneProxyRedirect(_TestStandaloneBase): """ unix_socket = False - skip_authentication = True + no_authentication = True def test_add_slash(self): response = self.fetch("/some/prefix", follow_redirects=False) @@ -97,7 +97,7 @@ def test_on_prefix(self): ) class TestStandaloneProxyWithUnixSocket(_TestStandaloneBase): unix_socket = True - skip_authentication = True + no_authentication = True def test_with_unix_socket(self): response = self.fetch("/some/prefix/") @@ -115,7 +115,7 @@ class TestStandaloneProxyLogin(_TestStandaloneBase): """ unix_socket = False - skip_authentication = False + no_authentication = False def test_redirect_to_login_url(self): response = self.fetch("/some/prefix/", follow_redirects=False)