def initialize_settings(self):
        """Add settings to the tornado app."""
        if self.ignore_minified_js:
            self.log.warning(
                _i18n(
                    """The `ignore_minified_js` flag is deprecated and no longer works."""
                ))
            self.log.warning(
                _i18n(
                    """Alternatively use `%s` when working on the notebook's Javascript and LESS"""
                ) % 'npm run build:watch')
            warnings.warn(
                _i18n(
                    "The `ignore_minified_js` flag is deprecated and will be removed in Notebook 6.0"
                ), DeprecationWarning)

        settings = dict(
            static_custom_path=self.static_custom_path,
            static_handler_args={
                # don't cache custom.js
                'no_cache_paths': [
                    url_path_join(self.serverapp.base_url, 'static', self.name,
                                  'custom')
                ],
            },
            ignore_minified_js=self.ignore_minified_js,
            mathjax_url=self.mathjax_url,
            mathjax_config=self.mathjax_config,
            nbextensions_path=self.nbextensions_path,
        )
        self.settings.update(**settings)
class ExtensionAppJinjaMixin(HasTraits):
    """Use Jinja templates for HTML templates on top of an ExtensionApp."""

    jinja2_options = Dict(
        help=_i18n("""Options to pass to the jinja2 environment for this
        """)).tag(config=True)

    def _prepare_templates(self):
        # Get templates defined in a subclass.
        self.initialize_templates()
        # Add templates to web app settings if extension has templates.
        if len(self.template_paths) > 0:
            self.settings.update(
                {"{}_template_paths".format(self.name): self.template_paths})

        # Create a jinja environment for logging html templates.
        self.jinja2_env = Environment(loader=FileSystemLoader(
            self.template_paths),
                                      extensions=['jinja2.ext.i18n'],
                                      autoescape=True,
                                      **self.jinja2_options)

        # Add the jinja2 environment for this extension to the tornado settings.
        self.settings.update(
            {"{}_jinja2_env".format(self.name): self.jinja2_env})
Exemple #3
0
 def validate_security(
     self,
     app: ServerApp,
     ssl_options: dict | None = None,
 ) -> None:
     if self.password_required and (not self.hashed_password):
         self.log.critical(
             _i18n(
                 "Jupyter servers are configured to only be run with a password."
             ))
         self.log.critical(
             _i18n("Hint: run the following command to set a password"))
         self.log.critical(
             _i18n("\t$ python -m jupyter_server.auth password"))
         sys.exit(1)
     return self.login_handler_class.validate_security(app, ssl_options)
Exemple #4
0
    def process_login_form(self, handler: JupyterHandler) -> User | None:
        """Process login form data

        Return authenticated User if successful, None if not.
        """
        typed_password = handler.get_argument("password", default="")
        new_password = handler.get_argument("new_password", default="")
        user = None
        if not self.auth_enabled:
            self.log.warning(
                "Accepting anonymous login because auth fully disabled!")
            return self.generate_anonymous_user(handler)

        if self.passwd_check(typed_password) and not new_password:
            return self.generate_anonymous_user(handler)
        elif self.token and self.token == typed_password:
            user = self.generate_anonymous_user(handler)
            if new_password and self.allow_password_change:
                config_dir = handler.settings.get("config_dir", "")
                config_file = os.path.join(config_dir,
                                           "jupyter_server_config.json")
                self.hashed_password = set_password(new_password,
                                                    config_file=config_file)
                self.log.info(_i18n(f"Wrote hashed password to {config_file}"))

        return user
Exemple #5
0
 def _update_mathjax_url(self, change):
     new = change['new']
     if new and not self.enable_mathjax:
         # enable_mathjax=False overrides mathjax_url
         self.mathjax_url = u''
     else:
         self.log.info(_i18n("Using MathJax: %s"), new)
Exemple #6
0
class Jupyterfs(Configurable):
    root_manager_class = Type(
        config=True,
        default_value=LargeFileManager,
        help=_i18n(
            "the root contents manager class to use. Used by the Jupyterlab default filebrowser and elsewhere"
        ),
        klass=ContentsManager,
    )

    resources = List(
        config=True,
        default_value=[],
        help=_i18n(
            "server-side definitions of fsspec resources for jupyter-fs"),
        # trait=Dict(traits={"name": Unicode, "url": Unicode}),
    )
Exemple #7
0
def get_frontend_exporters():
    from nbconvert.exporters.base import get_export_names, get_exporter

    # name=exporter_name, display=export_from_notebook+extension
    ExporterInfo = namedtuple('ExporterInfo', ['name', 'display'])

    default_exporters = [
        ExporterInfo(name='html', display='HTML (.html)'),
        ExporterInfo(name='latex', display='LaTeX (.tex)'),
        ExporterInfo(name='markdown', display='Markdown (.md)'),
        ExporterInfo(name='notebook', display='Notebook (.ipynb)'),
        ExporterInfo(name='pdf', display='PDF via LaTeX (.pdf)'),
        ExporterInfo(name='rst', display='reST (.rst)'),
        ExporterInfo(name='script', display='Script (.txt)'),
        ExporterInfo(name='slides', display='Reveal.js slides (.slides.html)')
    ]

    frontend_exporters = []
    for name in get_export_names():
        exporter_class = get_exporter(name)
        exporter_instance = exporter_class()
        ux_name = getattr(exporter_instance, 'export_from_notebook', None)
        super_uxname = getattr(super(exporter_class, exporter_instance),
                               'export_from_notebook', None)

        # Ensure export_from_notebook is explicitly defined & not inherited
        if ux_name is not None and ux_name != super_uxname:
            display = _i18n('{} ({})'.format(ux_name,
                                             exporter_instance.file_extension))
            frontend_exporters.append(ExporterInfo(name, display))

    # Ensure default_exporters are in frontend_exporters if not already
    # This protects against nbconvert versions lower than 5.5
    names = set(exporter.name.lower() for exporter in frontend_exporters)
    for exporter in default_exporters:
        if exporter.name not in names:
            frontend_exporters.append(exporter)

    # Protect against nbconvert 5.5.0
    python_exporter = ExporterInfo(name='python', display='python (.py)')
    if python_exporter in frontend_exporters:
        frontend_exporters.remove(python_exporter)

    # Protect against nbconvert 5.4.x
    template_exporter = ExporterInfo(name='custom', display='custom (.txt)')
    if template_exporter in frontend_exporters:
        frontend_exporters.remove(template_exporter)
    return sorted(frontend_exporters)
Exemple #8
0
class ContentsManager(LoggingConfigurable):
    """Base class for serving files and directories.

    This serves any text or binary file,
    as well as directories,
    with special handling for JSON notebook documents.

    Most APIs take a path argument,
    which is always an API-style unicode path,
    and always refers to a directory.

    - unicode, not url-escaped
    - '/'-separated
    - leading and trailing '/' will be stripped
    - if unspecified, path defaults to '',
      indicating the root path.

    """

    root_dir = Unicode("/", config=True)

    allow_hidden = Bool(False,
                        config=True,
                        help="Allow access to hidden files")

    notary = Instance(sign.NotebookNotary)

    def _notary_default(self):
        return sign.NotebookNotary(parent=self)

    hide_globs = List(
        Unicode(),
        [
            u"__pycache__",
            "*.pyc",
            "*.pyo",
            ".DS_Store",
            "*.so",
            "*.dylib",
            "*~",
        ],
        config=True,
        help="""
        Glob patterns to hide in file and directory listings.
    """,
    )

    untitled_notebook = Unicode(
        _i18n("Untitled"),
        config=True,
        help="The base name used when creating untitled notebooks.")

    untitled_file = Unicode(
        "untitled",
        config=True,
        help="The base name used when creating untitled files.")

    untitled_directory = Unicode(
        "Untitled Folder",
        config=True,
        help="The base name used when creating untitled directories.",
    )

    pre_save_hook = Any(
        None,
        config=True,
        allow_none=True,
        help="""Python callable or importstring thereof

        To be called on a contents model prior to save.

        This can be used to process the structure,
        such as removing notebook outputs or other side effects that
        should not be saved.

        It will be called as (all arguments passed by keyword)::

            hook(path=path, model=model, contents_manager=self)

        - model: the model to be saved. Includes file contents.
          Modifying this dict will affect the file that is stored.
        - path: the API path of the save destination
        - contents_manager: this ContentsManager instance
        """,
    )

    @validate("pre_save_hook")
    def _validate_pre_save_hook(self, proposal):
        value = proposal["value"]
        if isinstance(value, str):
            value = import_item(self.pre_save_hook)
        if not callable(value):
            raise TraitError("pre_save_hook must be callable")
        return value

    def run_pre_save_hook(self, model, path, **kwargs):
        """Run the pre-save hook if defined, and log errors"""
        if self.pre_save_hook:
            try:
                self.log.debug("Running pre-save hook on %s", path)
                self.pre_save_hook(model=model,
                                   path=path,
                                   contents_manager=self,
                                   **kwargs)
            except HTTPError:
                # allow custom HTTPErrors to raise,
                # rejecting the save with a message.
                raise
            except Exception:
                # unhandled errors don't prevent saving,
                # which could cause frustrating data loss
                self.log.error("Pre-save hook failed on %s",
                               path,
                               exc_info=True)

    checkpoints_class = Type(Checkpoints, config=True)
    checkpoints = Instance(Checkpoints, config=True)
    checkpoints_kwargs = Dict(config=True)

    @default("checkpoints")
    def _default_checkpoints(self):
        return self.checkpoints_class(**self.checkpoints_kwargs)

    @default("checkpoints_kwargs")
    def _default_checkpoints_kwargs(self):
        return dict(
            parent=self,
            log=self.log,
        )

    files_handler_class = Type(
        FilesHandler,
        klass=RequestHandler,
        allow_none=True,
        config=True,
        help="""handler class to use when serving raw file requests.

        Default is a fallback that talks to the ContentsManager API,
        which may be inefficient, especially for large files.

        Local files-based ContentsManagers can use a StaticFileHandler subclass,
        which will be much more efficient.

        Access to these files should be Authenticated.
        """,
    )

    files_handler_params = Dict(
        config=True,
        help="""Extra parameters to pass to files_handler_class.

        For example, StaticFileHandlers generally expect a `path` argument
        specifying the root directory from which to serve files.
        """,
    )

    def get_extra_handlers(self):
        """Return additional handlers

        Default: self.files_handler_class on /files/.*
        """
        handlers = []
        if self.files_handler_class:
            handlers.append((r"/files/(.*)", self.files_handler_class,
                             self.files_handler_params))
        return handlers

    # ContentsManager API part 1: methods that must be
    # implemented in subclasses.

    def dir_exists(self, path):
        """Does a directory exist at the given path?

        Like os.path.isdir

        Override this method in subclasses.

        Parameters
        ----------
        path : string
            The path to check

        Returns
        -------
        exists : bool
            Whether the path does indeed exist.
        """
        raise NotImplementedError

    def is_hidden(self, path):
        """Is path a hidden directory or file?

        Parameters
        ----------
        path : string
            The path to check. This is an API path (`/` separated,
            relative to root dir).

        Returns
        -------
        hidden : bool
            Whether the path is hidden.

        """
        raise NotImplementedError

    def file_exists(self, path=""):
        """Does a file exist at the given path?

        Like os.path.isfile

        Override this method in subclasses.

        Parameters
        ----------
        path : string
            The API path of a file to check for.

        Returns
        -------
        exists : bool
            Whether the file exists.
        """
        raise NotImplementedError("must be implemented in a subclass")

    def exists(self, path):
        """Does a file or directory exist at the given path?

        Like os.path.exists

        Parameters
        ----------
        path : string
            The API path of a file or directory to check for.

        Returns
        -------
        exists : bool
            Whether the target exists.
        """
        return self.file_exists(path) or self.dir_exists(path)

    def get(self, path, content=True, type=None, format=None):
        """Get a file or directory model."""
        raise NotImplementedError("must be implemented in a subclass")

    def save(self, model, path):
        """
        Save a file or directory model to path.

        Should return the saved model with no content.  Save implementations
        should call self.run_pre_save_hook(model=model, path=path) prior to
        writing any data.
        """
        raise NotImplementedError("must be implemented in a subclass")

    def delete_file(self, path):
        """Delete the file or directory at path."""
        raise NotImplementedError("must be implemented in a subclass")

    def rename_file(self, old_path, new_path):
        """Rename a file or directory."""
        raise NotImplementedError("must be implemented in a subclass")

    # ContentsManager API part 2: methods that have useable default
    # implementations, but can be overridden in subclasses.

    def delete(self, path):
        """Delete a file/directory and any associated checkpoints."""
        path = path.strip("/")
        if not path:
            raise HTTPError(400, "Can't delete root")
        self.delete_file(path)
        self.checkpoints.delete_all_checkpoints(path)

    def rename(self, old_path, new_path):
        """Rename a file and any checkpoints associated with that file."""
        self.rename_file(old_path, new_path)
        self.checkpoints.rename_all_checkpoints(old_path, new_path)

    def update(self, model, path):
        """Update the file's path

        For use in PATCH requests, to enable renaming a file without
        re-uploading its contents. Only used for renaming at the moment.
        """
        path = path.strip("/")
        new_path = model.get("path", path).strip("/")
        if path != new_path:
            self.rename(path, new_path)
        model = self.get(new_path, content=False)
        return model

    def info_string(self):
        return "Serving contents"

    def get_kernel_path(self, path, model=None):
        """Return the API path for the kernel

        KernelManagers can turn this value into a filesystem path,
        or ignore it altogether.

        The default value here will start kernels in the directory of the
        notebook server. FileContentsManager overrides this to use the
        directory containing the notebook.
        """
        return ""

    def increment_filename(self, filename, path="", insert=""):
        """Increment a filename until it is unique.

        Parameters
        ----------
        filename : unicode
            The name of a file, including extension
        path : unicode
            The API path of the target's directory
        insert : unicode
            The characters to insert after the base filename

        Returns
        -------
        name : unicode
            A filename that is unique, based on the input filename.
        """
        # Extract the full suffix from the filename (e.g. .tar.gz)
        path = path.strip("/")
        basename, dot, ext = filename.rpartition(".")
        if ext != "ipynb":
            basename, dot, ext = filename.partition(".")

        suffix = dot + ext

        for i in itertools.count():
            if i:
                insert_i = "{}{}".format(insert, i)
            else:
                insert_i = ""
            name = u"{basename}{insert}{suffix}".format(basename=basename,
                                                        insert=insert_i,
                                                        suffix=suffix)
            if not self.exists(u"{}/{}".format(path, name)):
                break
        return name

    def validate_notebook_model(self, model):
        """Add failed-validation message to model"""
        try:
            validate_nb(model["content"])
        except ValidationError as e:
            model["message"] = u"Notebook validation failed: {}:\n{}".format(
                e.message,
                json.dumps(e.instance,
                           indent=1,
                           default=lambda obj: "<UNKNOWN>"),
            )
        return model

    def new_untitled(self, path="", type="", ext=""):
        """Create a new untitled file or directory in path

        path must be a directory

        File extension can be specified.

        Use `new` to create files with a fully specified path (including filename).
        """
        path = path.strip("/")
        if not self.dir_exists(path):
            raise HTTPError(404, "No such directory: %s" % path)

        model = {}
        if type:
            model["type"] = type

        if ext == ".ipynb":
            model.setdefault("type", "notebook")
        else:
            model.setdefault("type", "file")

        insert = ""
        if model["type"] == "directory":
            untitled = self.untitled_directory
            insert = " "
        elif model["type"] == "notebook":
            untitled = self.untitled_notebook
            ext = ".ipynb"
        elif model["type"] == "file":
            untitled = self.untitled_file
        else:
            raise HTTPError(400, "Unexpected model type: %r" % model["type"])

        name = self.increment_filename(untitled + ext, path, insert=insert)
        path = u"{0}/{1}".format(path, name)
        return self.new(model, path)

    def new(self, model=None, path=""):
        """Create a new file or directory and return its model with no content.

        To create a new untitled entity in a directory, use `new_untitled`.
        """
        path = path.strip("/")
        if model is None:
            model = {}

        if path.endswith(".ipynb"):
            model.setdefault("type", "notebook")
        else:
            model.setdefault("type", "file")

        # no content, not a directory, so fill out new-file model
        if "content" not in model and model["type"] != "directory":
            if model["type"] == "notebook":
                model["content"] = new_notebook()
                model["format"] = "json"
            else:
                model["content"] = ""
                model["type"] = "file"
                model["format"] = "text"

        model = self.save(model, path)
        return model

    def copy(self, from_path, to_path=None):
        """Copy an existing file and return its new model.

        If to_path not specified, it will be the parent directory of from_path.
        If to_path is a directory, filename will increment `from_path-Copy#.ext`.
        Considering multi-part extensions, the Copy# part will be placed before the first dot for all the extensions except `ipynb`.
        For easier manual searching in case of notebooks, the Copy# part will be placed before the last dot.

        from_path must be a full path to a file.
        """
        path = from_path.strip("/")
        if to_path is not None:
            to_path = to_path.strip("/")

        if "/" in path:
            from_dir, from_name = path.rsplit("/", 1)
        else:
            from_dir = ""
            from_name = path

        model = self.get(path)
        model.pop("path", None)
        model.pop("name", None)
        if model["type"] == "directory":
            raise HTTPError(400, "Can't copy directories")

        if to_path is None:
            to_path = from_dir
        if self.dir_exists(to_path):
            name = copy_pat.sub(u".", from_name)
            to_name = self.increment_filename(name, to_path, insert="-Copy")
            to_path = u"{0}/{1}".format(to_path, to_name)

        model = self.save(model, to_path)
        return model

    def log_info(self):
        self.log.info(self.info_string())

    def trust_notebook(self, path):
        """Explicitly trust a notebook

        Parameters
        ----------
        path : string
            The path of a notebook
        """
        model = self.get(path)
        nb = model["content"]
        self.log.warning("Trusting notebook %s", path)
        self.notary.mark_cells(nb, True)
        self.check_and_sign(nb, path)

    def check_and_sign(self, nb, path=""):
        """Check for trusted cells, and sign the notebook.

        Called as a part of saving notebooks.

        Parameters
        ----------
        nb : dict
            The notebook dict
        path : string
            The notebook's path (for logging)
        """
        if self.notary.check_cells(nb):
            self.notary.sign(nb)
        else:
            self.log.warning("Notebook %s is not trusted", path)

    def mark_trusted_cells(self, nb, path=""):
        """Mark cells as trusted if the notebook signature matches.

        Called as a part of loading notebooks.

        Parameters
        ----------
        nb : dict
            The notebook object (in current nbformat)
        path : string
            The notebook's path (for logging)
        """
        trusted = self.notary.check_signature(nb)
        if not trusted:
            self.log.warning("Notebook %s is not trusted", path)
        self.notary.mark_cells(nb, trusted)

    def should_list(self, name):
        """Should this file/directory name be displayed in a listing?"""
        return not any(fnmatch(name, glob) for glob in self.hide_globs)

    # Part 3: Checkpoints API
    def create_checkpoint(self, path):
        """Create a checkpoint."""
        return self.checkpoints.create_checkpoint(self, path)

    def restore_checkpoint(self, checkpoint_id, path):
        """
        Restore a checkpoint.
        """
        self.checkpoints.restore_checkpoint(self, checkpoint_id, path)

    def list_checkpoints(self, path):
        return self.checkpoints.list_checkpoints(path)

    def delete_checkpoint(self, checkpoint_id, path):
        return self.checkpoints.delete_checkpoint(checkpoint_id, path)
class ExtensionApp(JupyterApp):
    """Base class for configurable Jupyter Server Extension Applications.

    ExtensionApp subclasses can be initialized two ways:
    1. Extension is listed as a jpserver_extension, and ServerApp calls
        its load_jupyter_server_extension classmethod. This is the
        classic way of loading a server extension.
    2. Extension is launched directly by calling its `launch_instance`
        class method. This method can be set as a entry_point in
        the extensions setup.py
    """
    # Subclasses should override this trait. Tells the server if
    # this extension allows other other extensions to be loaded
    # side-by-side when launched directly.
    load_other_extensions = True

    # A useful class property that subclasses can override to
    # configure the underlying Jupyter Server when this extension
    # is launched directly (using its `launch_instance` method).
    serverapp_config = {}

    # Some subclasses will likely override this trait to flip
    # the default value to False if they don't offer a browser
    # based frontend.
    open_browser = Bool(help="""Whether to open in a browser after starting.
        The specific browser used is platform dependent and
        determined by the python standard library `webbrowser`
        module, unless it is overridden using the --browser
        (ServerApp.browser) configuration option.
        """).tag(config=True)

    @default('open_browser')
    def _default_open_browser(self):
        return self.serverapp.config["ServerApp"].get("open_browser", True)

    # The extension name used to name the jupyter config
    # file, jupyter_{name}_config.
    # This should also match the jupyter subcommand used to launch
    # this extension from the CLI, e.g. `jupyter {name}`.
    name = None

    @classmethod
    def get_extension_package(cls):
        parts = cls.__module__.split('.')
        if is_namespace_package(parts[0]):
            # in this case the package name is `<namespace>.<package>`.
            return '.'.join(parts[0:2])
        return parts[0]

    @classmethod
    def get_extension_point(cls):
        return cls.__module__

    # Extension URL sets the default landing page for this extension.
    extension_url = "/"

    default_url = Unicode().tag(config=True)

    @default('default_url')
    def _default_url(self):
        return self.extension_url

    file_url_prefix = Unicode('notebooks')

    # Is this linked to a serverapp yet?
    _linked = Bool(False)

    # Extension can configure the ServerApp from the command-line
    classes = [
        ServerApp,
    ]

    # A ServerApp is not defined yet, but will be initialized below.
    serverapp = None

    _log_formatter_cls = LogFormatter

    @default('log_level')
    def _default_log_level(self):
        return logging.INFO

    @default('log_format')
    def _default_log_format(self):
        """override default log format to include date & time"""
        return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"

    static_url_prefix = Unicode(
        help="""Url where the static assets for the extension are served."""
    ).tag(config=True)

    @default('static_url_prefix')
    def _default_static_url_prefix(self):
        static_url = "static/{name}/".format(name=self.name)
        return url_path_join(self.serverapp.base_url, static_url)

    static_paths = List(Unicode(),
                        help="""paths to search for serving static files.

        This allows adding javascript/css to be available from the notebook server machine,
        or overriding individual files in the IPython
        """).tag(config=True)

    template_paths = List(
        Unicode(),
        help=_i18n("""Paths to search for serving jinja templates.

        Can be used to override templates from notebook.templates.""")).tag(
            config=True)

    settings = Dict(
        help=_i18n("""Settings that will passed to the server.""")).tag(
            config=True)

    handlers = List(help=_i18n("""Handlers appended to the server.""")).tag(
        config=True)

    def _config_file_name_default(self):
        """The default config file name."""
        if not self.name:
            return ''
        return 'jupyter_{}_config'.format(self.name.replace('-', '_'))

    def initialize_settings(self):
        """Override this method to add handling of settings."""
        pass

    def initialize_handlers(self):
        """Override this method to append handlers to a Jupyter Server."""
        pass

    def initialize_templates(self):
        """Override this method to add handling of template files."""
        pass

    def _prepare_config(self):
        """Builds a Config object from the extension's traits and passes
        the object to the webapp's settings as `<name>_config`.
        """
        traits = self.class_own_traits().keys()
        self.extension_config = Config({t: getattr(self, t) for t in traits})
        self.settings['{}_config'.format(self.name)] = self.extension_config

    def _prepare_settings(self):
        # Make webapp settings accessible to initialize_settings method
        webapp = self.serverapp.web_app
        self.settings.update(**webapp.settings)

        # Add static and template paths to settings.
        self.settings.update({
            "{}_static_paths".format(self.name): self.static_paths,
            "{}".format(self.name): self,
        })

        # Get setting defined by subclass using initialize_settings method.
        self.initialize_settings()

        # Update server settings with extension settings.
        webapp.settings.update(**self.settings)

    def _prepare_handlers(self):
        webapp = self.serverapp.web_app

        # Get handlers defined by extension subclass.
        self.initialize_handlers()

        # prepend base_url onto the patterns that we match
        new_handlers = []
        for handler_items in self.handlers:
            # Build url pattern including base_url
            pattern = url_path_join(webapp.settings['base_url'],
                                    handler_items[0])
            handler = handler_items[1]

            # Get handler kwargs, if given
            kwargs = {}
            if issubclass(handler, ExtensionHandlerMixin):
                kwargs['name'] = self.name

            try:
                kwargs.update(handler_items[2])
            except IndexError:
                pass

            new_handler = (pattern, handler, kwargs)
            new_handlers.append(new_handler)

        # Add static endpoint for this extension, if static paths are given.
        if len(self.static_paths) > 0:
            # Append the extension's static directory to server handlers.
            static_url = url_path_join(self.static_url_prefix, "(.*)")

            # Construct handler.
            handler = (static_url, webapp.settings['static_handler_class'], {
                'path': self.static_paths
            })
            new_handlers.append(handler)

        webapp.add_handlers('.*$', new_handlers)

    def _prepare_templates(self):
        # Add templates to web app settings if extension has templates.
        if len(self.template_paths) > 0:
            self.settings.update(
                {"{}_template_paths".format(self.name): self.template_paths})
        self.initialize_templates()

    def _jupyter_server_config(self):
        base_config = {
            "ServerApp": {
                "default_url": self.default_url,
                "open_browser": self.open_browser,
                "file_url_prefix": self.file_url_prefix
            }
        }
        base_config["ServerApp"].update(self.serverapp_config)
        return base_config

    def _link_jupyter_server_extension(self, serverapp):
        """Link the ExtensionApp to an initialized ServerApp.

        The ServerApp is stored as an attribute and config
        is exchanged between ServerApp and `self` in case
        the command line contains traits for the ExtensionApp
        or the ExtensionApp's config files have server
        settings.

        Note, the ServerApp has not initialized the Tornado
        Web Application yet, so do not try to affect the
        `web_app` attribute.
        """
        self.serverapp = serverapp
        # Load config from an ExtensionApp's config files.
        self.load_config_file()
        # ServerApp's config might have picked up
        # config for the ExtensionApp. We call
        # update_config to update ExtensionApp's
        # traits with these values found in ServerApp's
        # config.
        # ServerApp config ---> ExtensionApp traits
        self.update_config(self.serverapp.config)
        # Use ExtensionApp's CLI parser to find any extra
        # args that passed through ServerApp and
        # now belong to ExtensionApp.
        self.parse_command_line(self.serverapp.extra_args)
        # If any config should be passed upstream to the
        # ServerApp, do it here.
        # i.e. ServerApp traits <--- ExtensionApp config
        self.serverapp.update_config(self.config)
        # Acknowledge that this extension has been linked.
        self._linked = True

    def initialize(self):
        """Initialize the extension app. The
        corresponding server app and webapp should already
        be initialized by this step.

        1) Appends Handlers to the ServerApp,
        2) Passes config and settings from ExtensionApp
        to the Tornado web application
        3) Points Tornado Webapp to templates and
        static assets.
        """
        if not self.serverapp:
            msg = ("This extension has no attribute `serverapp`. "
                   "Try calling `.link_to_serverapp()` before calling "
                   "`.initialize()`.")
            raise JupyterServerExtensionException(msg)

        self._prepare_config()
        self._prepare_templates()
        self._prepare_settings()
        self._prepare_handlers()

    def start(self):
        """Start the underlying Jupyter server.

        Server should be started after extension is initialized.
        """
        super(ExtensionApp, self).start()
        # Start the server.
        self.serverapp.start()

    async def stop_extension(self):
        """Cleanup any resources managed by this extension."""

    def stop(self):
        """Stop the underlying Jupyter server.
        """
        self.serverapp.stop()
        self.serverapp.clear_instance()

    @classmethod
    def _load_jupyter_server_extension(cls, serverapp):
        """Initialize and configure this extension, then add the extension's
        settings and handlers to the server's web application.
        """
        extension_manager = serverapp.extension_manager
        try:
            # Get loaded extension from serverapp.
            point = extension_manager.extension_points[cls.name]
            extension = point.app
        except KeyError:
            extension = cls()
            extension._link_jupyter_server_extension(serverapp)
        extension.initialize()
        return extension

    @classmethod
    def load_classic_server_extension(cls, serverapp):
        """Enables extension to be loaded as classic Notebook (jupyter/notebook) extension.
        """
        extension = cls()
        extension.serverapp = serverapp
        extension.load_config_file()
        extension.update_config(serverapp.config)
        extension.parse_command_line(serverapp.extra_args)
        # Add redirects to get favicons from old locations in the classic notebook server
        extension.handlers.extend([
            (r"/static/favicons/favicon.ico", RedirectHandler, {
                "url":
                url_path_join(serverapp.base_url,
                              "static/base/images/favicon.ico")
            }),
            (r"/static/favicons/favicon-busy-1.ico", RedirectHandler, {
                "url":
                url_path_join(serverapp.base_url,
                              "static/base/images/favicon-busy-1.ico")
            }),
            (r"/static/favicons/favicon-busy-2.ico", RedirectHandler, {
                "url":
                url_path_join(serverapp.base_url,
                              "static/base/images/favicon-busy-2.ico")
            }),
            (r"/static/favicons/favicon-busy-3.ico", RedirectHandler, {
                "url":
                url_path_join(serverapp.base_url,
                              "static/base/images/favicon-busy-3.ico")
            }),
            (r"/static/favicons/favicon-file.ico", RedirectHandler, {
                "url":
                url_path_join(serverapp.base_url,
                              "static/base/images/favicon-file.ico")
            }),
            (r"/static/favicons/favicon-notebook.ico", RedirectHandler, {
                "url":
                url_path_join(serverapp.base_url,
                              "static/base/images/favicon-notebook.ico")
            }),
            (r"/static/favicons/favicon-terminal.ico", RedirectHandler, {
                "url":
                url_path_join(serverapp.base_url,
                              "static/base/images/favicon-terminal.ico")
            }),
            (r"/static/logo/logo.png", RedirectHandler, {
                "url":
                url_path_join(serverapp.base_url,
                              "static/base/images/logo.png")
            }),
        ])
        extension.initialize()

    @classmethod
    def initialize_server(cls, argv=[], load_other_extensions=True, **kwargs):
        """Creates an instance of ServerApp and explicitly sets
        this extension to enabled=True (i.e. superceding disabling
        found in other config from files).

        The `launch_instance` method uses this method to initialize
        and start a server.
        """
        jpserver_extensions = {cls.get_extension_package(): True}
        find_extensions = cls.load_other_extensions
        if 'jpserver_extensions' in cls.serverapp_config:
            jpserver_extensions.update(
                cls.serverapp_config['jpserver_extensions'])
            cls.serverapp_config['jpserver_extensions'] = jpserver_extensions
            find_extensions = False
        serverapp = ServerApp.instance(jpserver_extensions=jpserver_extensions,
                                       **kwargs)
        serverapp.initialize(
            argv=argv,
            starter_extension=cls.name,
            find_extensions=find_extensions,
        )
        return serverapp

    @classmethod
    def launch_instance(cls, argv=None, **kwargs):
        """Launch the extension like an application. Initializes+configs a stock server
        and appends the extension to the server. Then starts the server and routes to
        extension's landing page.
        """
        # Handle arguments.
        if argv is None:
            args = sys.argv[1:]  # slice out extension config.
        else:
            args = argv

        # Handle all "stops" that could happen before
        # continuing to launch a server+extension.
        subapp = _preparse_for_subcommand(cls, args)
        if subapp:
            subapp.start()
            return

        # Check for help, version, and generate-config arguments
        # before initializing server to make sure these
        # arguments trigger actions from the extension not the server.
        _preparse_for_stopping_flags(cls, args)
        serverapp = cls.initialize_server(argv=args)

        # Log if extension is blocking other extensions from loading.
        if not cls.load_other_extensions:
            serverapp.log.info("{ext_name} is running without loading "
                               "other extensions.".format(ext_name=cls.name))
        # Start the server.
        try:
            serverapp.start()
        except NoStart:
            pass
Exemple #10
0
class PasswordIdentityProvider(IdentityProvider):

    hashed_password = Unicode(
        "",
        config=True,
        help=_i18n("""
            Hashed password to use for web authentication.

            To generate, type in a python/IPython shell:

                from jupyter_server.auth import passwd; passwd()

            The string should be of the form type:salt:hashed-password.
            """),
    )

    password_required = Bool(
        False,
        config=True,
        help=_i18n("""
            Forces users to use a password for the Jupyter server.
            This is useful in a multi user environment, for instance when
            everybody in the LAN can access each other's machine through ssh.

            In such a case, serving on localhost is not secure since
            any user can connect to the Jupyter server via ssh.

            """),
    )

    allow_password_change = Bool(
        True,
        config=True,
        help=_i18n("""
            Allow password to be changed at login for the Jupyter server.

            While logging in with a token, the Jupyter server UI will give the opportunity to
            the user to enter a new password at the same time that will replace
            the token login mechanism.

            This can be set to False to prevent changing password from the UI/API.
            """),
    )

    @default("need_token")
    def _need_token_default(self):
        return not bool(self.hashed_password)

    @property
    def login_available(self) -> bool:
        """Whether a LoginHandler is needed - and therefore whether the login page should be displayed."""
        return self.auth_enabled

    @property
    def auth_enabled(self) -> bool:
        """Return whether any auth is enabled"""
        return bool(self.hashed_password or self.token)

    def passwd_check(self, password):
        """Check password against our stored hashed password"""
        return passwd_check(self.hashed_password, password)

    def process_login_form(self, handler: JupyterHandler) -> User | None:
        """Process login form data

        Return authenticated User if successful, None if not.
        """
        typed_password = handler.get_argument("password", default="")
        new_password = handler.get_argument("new_password", default="")
        user = None
        if not self.auth_enabled:
            self.log.warning(
                "Accepting anonymous login because auth fully disabled!")
            return self.generate_anonymous_user(handler)

        if self.passwd_check(typed_password) and not new_password:
            return self.generate_anonymous_user(handler)
        elif self.token and self.token == typed_password:
            user = self.generate_anonymous_user(handler)
            if new_password and self.allow_password_change:
                config_dir = handler.settings.get("config_dir", "")
                config_file = os.path.join(config_dir,
                                           "jupyter_server_config.json")
                self.hashed_password = set_password(new_password,
                                                    config_file=config_file)
                self.log.info(_i18n(f"Wrote hashed password to {config_file}"))

        return user

    def validate_security(
        self,
        app: ServerApp,
        ssl_options: dict | None = None,
    ) -> None:
        super().validate_security(app, ssl_options)
        if self.password_required and (not self.hashed_password):
            self.log.critical(
                _i18n(
                    "Jupyter servers are configured to only be run with a password."
                ))
            self.log.critical(
                _i18n("Hint: run the following command to set a password"))
            self.log.critical(
                _i18n("\t$ python -m jupyter_server.auth password"))
            sys.exit(1)
Exemple #11
0
class IdentityProvider(LoggingConfigurable):
    """
    Interface for providing identity management and authentication.

    Two principle methods:

    - :meth:`~.IdentityProvider.get_user` returns a :class:`~.User` object
      for successful authentication, or None for no-identity-found.
    - :meth:`~.IdentityProvider.identity_model` turns a :class:`~.User` into a JSONable dict.
      The default is to use :py:meth:`dataclasses.asdict`,
      and usually shouldn't need override.

    Additional methods can customize authentication.

    .. versionadded:: 2.0
    """

    cookie_name = Unicode(
        "",
        config=True,
        help=_i18n(
            "Name of the cookie to set for persisting login. Default: username-${Host}."
        ),
    )

    cookie_options = Dict(
        config=True,
        help=_i18n("Extra keyword arguments to pass to `set_secure_cookie`."
                   " See tornado's set_secure_cookie docs for details."),
    )

    secure_cookie = Bool(
        None,
        allow_none=True,
        config=True,
        help=_i18n(
            "Specify whether login cookie should have the `secure` property (HTTPS-only)."
            "Only needed when protocol-detection gives the wrong answer due to proxies."
        ),
    )

    get_secure_cookie_kwargs = Dict(
        config=True,
        help=_i18n("Extra keyword arguments to pass to `get_secure_cookie`."
                   " See tornado's get_secure_cookie docs for details."),
    )

    token = Unicode(
        "<generated>",
        help=_i18n(
            """Token used for authenticating first-time connections to the server.

        The token can be read from the file referenced by JUPYTER_TOKEN_FILE or set directly
        with the JUPYTER_TOKEN environment variable.

        When no password is enabled,
        the default is to generate a new, random token.

        Setting to an empty string disables authentication altogether, which is NOT RECOMMENDED.

        Prior to 2.0: configured as ServerApp.token
        """),
    ).tag(config=True)

    login_handler_class = Type(
        default_value="jupyter_server.auth.login.LoginFormHandler",
        klass=web.RequestHandler,
        config=True,
        help=_i18n("The login handler class to use, if any."),
    )

    logout_handler_class = Type(
        default_value="jupyter_server.auth.logout.LogoutHandler",
        klass=web.RequestHandler,
        config=True,
        help=_i18n("The logout handler class to use."),
    )

    token_generated = False

    @default("token")
    def _token_default(self):
        if os.getenv("JUPYTER_TOKEN"):
            self.token_generated = False
            return os.environ["JUPYTER_TOKEN"]
        if os.getenv("JUPYTER_TOKEN_FILE"):
            self.token_generated = False
            with open(os.environ["JUPYTER_TOKEN_FILE"]) as token_file:
                return token_file.read()
        if not self.need_token:
            # no token if password is enabled
            self.token_generated = False
            return ""
        else:
            self.token_generated = True
            return binascii.hexlify(os.urandom(24)).decode("ascii")

    need_token = Bool(True)

    def get_user(
            self,
            handler: JupyterHandler) -> User | None | Awaitable[User | None]:
        """Get the authenticated user for a request

        Must return a :class:`.jupyter_server.auth.User`,
        though it may be a subclass.

        Return None if the request is not authenticated.

        _may_ be a coroutine
        """
        return self._get_user(handler)

    # not sure how to have optional-async type signature
    # on base class with `async def` without splitting it into two methods

    async def _get_user(self, handler: JupyterHandler) -> User | None:
        if getattr(handler, "_jupyter_current_user", None):
            # already authenticated
            return handler._jupyter_current_user
        _token_user: User | None | Awaitable[User
                                             | None] = self.get_user_token(
                                                 handler)
        if isinstance(_token_user, Awaitable):
            _token_user = await _token_user
        token_user: User | None = _token_user  # need second variable name to collapse type
        _cookie_user = self.get_user_cookie(handler)
        if isinstance(_cookie_user, Awaitable):
            _cookie_user = await _cookie_user
        cookie_user: User | None = _cookie_user
        # prefer token to cookie if both given,
        # because token is always explicit
        user = token_user or cookie_user

        if user is not None and token_user is not None:
            # if token-authenticated, persist user_id in cookie
            # if it hasn't already been stored there
            if user != cookie_user:
                self.set_login_cookie(handler, user)
            # Record that the current request has been authenticated with a token.
            # Used in is_token_authenticated above.
            handler._token_authenticated = True

        if user is None:
            # If an invalid cookie was sent, clear it to prevent unnecessary
            # extra warnings. But don't do this on a request with *no* cookie,
            # because that can erroneously log you out (see gh-3365)
            cookie_name = self.get_cookie_name(handler)
            cookie = handler.get_cookie(cookie_name)
            if cookie is not None:
                self.log.warning(
                    f"Clearing invalid/expired login cookie {cookie_name}")
                self.clear_login_cookie(handler)
            if not self.auth_enabled:
                # Completely insecure! No authentication at all.
                # No need to warn here, though; validate_security will have already done that.
                user = self.generate_anonymous_user(handler)

        return user

    def identity_model(self, user: User) -> dict:
        """Return a User as an Identity model"""
        # TODO: validate?
        return asdict(user)

    def get_handlers(self) -> list:
        """Return list of additional handlers for this identity provider

        For example, an OAuth callback handler.
        """
        handlers = []
        if self.login_available:
            handlers.append((r"/login", self.login_handler_class))
        if self.logout_available:
            handlers.append((r"/logout", self.logout_handler_class))
        return handlers

    def user_to_cookie(self, user: User) -> str:
        """Serialize a user to a string for storage in a cookie

        If overriding in a subclass, make sure to define user_from_cookie as well.

        Default is just the user's username.
        """
        # default: username is enough
        cookie = json.dumps({
            "username": user.username,
            "name": user.name,
            "display_name": user.display_name,
            "initials": user.initials,
            "color": user.color,
        })
        return cookie

    def user_from_cookie(self, cookie_value: str) -> User | None:
        """Inverse of user_to_cookie"""
        user = json.loads(cookie_value)
        return User(
            user["username"],
            user["name"],
            user["display_name"],
            user["initials"],
            None,
            user["color"],
        )

    def get_cookie_name(self, handler: JupyterHandler) -> str:
        """Return the login cookie name

        Uses IdentityProvider.cookie_name, if defined.
        Default is to generate a string taking host into account to avoid
        collisions for multiple servers on one hostname with different ports.
        """
        if self.cookie_name:
            return self.cookie_name
        else:
            return _non_alphanum.sub("-", f"username-{handler.request.host}")

    def set_login_cookie(self, handler: JupyterHandler, user: User) -> None:
        """Call this on handlers to set the login cookie for success"""
        cookie_options = {}
        cookie_options.update(self.cookie_options)
        cookie_options.setdefault("httponly", True)
        # tornado <4.2 has a bug that considers secure==True as soon as
        # 'secure' kwarg is passed to set_secure_cookie
        secure_cookie = self.secure_cookie
        if secure_cookie is None:
            secure_cookie = handler.request.protocol == "https"
        if secure_cookie:
            cookie_options.setdefault("secure", True)
        cookie_options.setdefault("path", handler.base_url)
        cookie_name = self.get_cookie_name(handler)
        handler.set_secure_cookie(cookie_name, self.user_to_cookie(user),
                                  **cookie_options)

    def _force_clear_cookie(self,
                            handler: JupyterHandler,
                            name: str,
                            path: str = "/",
                            domain: str | None = None) -> None:
        """Deletes the cookie with the given name.

        Tornado's cookie handling currently (Jan 2018) stores cookies in a dict
        keyed by name, so it can only modify one cookie with a given name per
        response. The browser can store multiple cookies with the same name
        but different domains and/or paths. This method lets us clear multiple
        cookies with the same name.

        Due to limitations of the cookie protocol, you must pass the same
        path and domain to clear a cookie as were used when that cookie
        was set (but there is no way to find out on the server side
        which values were used for a given cookie).
        """
        name = escape.native_str(name)
        expires = datetime.datetime.utcnow() - datetime.timedelta(days=365)

        morsel: Morsel = Morsel()
        morsel.set(name, "", '""')
        morsel["expires"] = httputil.format_timestamp(expires)
        morsel["path"] = path
        if domain:
            morsel["domain"] = domain
        handler.add_header("Set-Cookie", morsel.OutputString())

    def clear_login_cookie(self, handler: JupyterHandler) -> None:
        """Clear the login cookie, effectively logging out the session."""
        cookie_options = {}
        cookie_options.update(self.cookie_options)
        path = cookie_options.setdefault("path", handler.base_url)
        cookie_name = self.get_cookie_name(handler)
        handler.clear_cookie(cookie_name, path=path)
        if path and path != "/":
            # also clear cookie on / to ensure old cookies are cleared
            # after the change in path behavior.
            # N.B. This bypasses the normal cookie handling, which can't update
            # two cookies with the same name. See the method above.
            self._force_clear_cookie(handler, cookie_name)

    def get_user_cookie(
            self,
            handler: JupyterHandler) -> User | None | Awaitable[User | None]:
        """Get user from a cookie

        Calls user_from_cookie to deserialize cookie value
        """
        _user_cookie = handler.get_secure_cookie(
            self.get_cookie_name(handler),
            **self.get_secure_cookie_kwargs,
        )
        if not _user_cookie:
            return None
        user_cookie = _user_cookie.decode()
        # TODO: try/catch in case of change in config?
        try:
            return self.user_from_cookie(user_cookie)
        except Exception as e:
            # log bad cookie itself, only at debug-level
            self.log.debug(
                f"Error unpacking user from cookie: cookie={user_cookie}",
                exc_info=True)
            self.log.error(f"Error unpacking user from cookie: {e}")
            return None

    auth_header_pat = re.compile(r"(token|bearer)\s+(.+)", re.IGNORECASE)

    def get_token(self, handler: JupyterHandler) -> str | None:
        """Get the user token from a request

        Default:

        - in URL parameters: ?token=<token>
        - in header: Authorization: token <token>
        """
        user_token = handler.get_argument("token", "")
        if not user_token:
            # get it from Authorization header
            m = self.auth_header_pat.match(
                handler.request.headers.get("Authorization", ""))
            if m:
                user_token = m.group(2)
        return user_token

    async def get_user_token(self, handler: JupyterHandler) -> User | None:
        """Identify the user based on a token in the URL or Authorization header

        Returns:
        - uuid if authenticated
        - None if not
        """
        token = handler.token
        if not token:
            return None
        # check login token from URL argument or Authorization header
        user_token = self.get_token(handler)
        authenticated = False
        if user_token == token:
            # token-authenticated, set the login cookie
            self.log.debug(
                "Accepting token-authenticated request from %s",
                handler.request.remote_ip,
            )
            authenticated = True

        if authenticated:
            # token does not correspond to user-id,
            # which is stored in a cookie.
            # still check the cookie for the user id
            _user = self.get_user_cookie(handler)
            if isinstance(_user, Awaitable):
                _user = await _user
            user: User | None = _user
            if user is None:
                user = self.generate_anonymous_user(handler)
            return user
        else:
            return None

    def generate_anonymous_user(self, handler: JupyterHandler) -> User:
        """Generate a random anonymous user.

        For use when a single shared token is used,
        but does not identify a user.
        """
        user_id = uuid.uuid4().hex
        moon = get_anonymous_username()
        name = display_name = f"Anonymous {moon}"
        initials = f"A{moon[0]}"
        color = None
        handler.log.info(
            f"Generating new user for token-authenticated request: {user_id}")
        return User(user_id, name, display_name, initials, None, color)

    def should_check_origin(self, handler: JupyterHandler) -> bool:
        """Should the Handler check for CORS origin validation?

        Origin check should be skipped for token-authenticated requests.

        Returns:
        - True, if Handler must check for valid CORS origin.
        - False, if Handler should skip origin check since requests are token-authenticated.
        """
        return not self.is_token_authenticated(handler)

    def is_token_authenticated(self, handler: JupyterHandler) -> bool:
        """Returns True if handler has been token authenticated. Otherwise, False.

        Login with a token is used to signal certain things, such as:

        - permit access to REST API
        - xsrf protection
        - skip origin-checks for scripts
        """
        # ensure get_user has been called, so we know if we're token-authenticated
        handler.current_user  # noqa
        return getattr(handler, "_token_authenticated", False)

    def validate_security(
        self,
        app: ServerApp,
        ssl_options: dict | None = None,
    ) -> None:
        """Check the application's security.

        Show messages, or abort if necessary, based on the security configuration.
        """
        if not app.ip:
            warning = "WARNING: The Jupyter server is listening on all IP addresses"
            if ssl_options is None:
                app.log.warning(
                    f"{warning} and not using encryption. This is not recommended."
                )
            if not self.auth_enabled:
                app.log.warning(f"{warning} and not using authentication. "
                                "This is highly insecure and not recommended.")
        else:
            if not self.auth_enabled:
                app.log.warning(
                    "All authentication is disabled."
                    "  Anyone who can connect to this server will be able to run code."
                )

    def process_login_form(self, handler: JupyterHandler) -> User | None:
        """Process login form data

        Return authenticated User if successful, None if not.
        """
        typed_password = handler.get_argument("password", default="")
        user = None
        if not self.auth_enabled:
            self.log.warning(
                "Accepting anonymous login because auth fully disabled!")
            return self.generate_anonymous_user(handler)

        if self.token and self.token == typed_password:
            return self.user_for_token(typed_password)

        return user

    @property
    def auth_enabled(self):
        """Is authentication enabled?

        Should always be True, but may be False in rare, insecure cases
        where requests with no auth are allowed.

        Previously: LoginHandler.get_login_available
        """
        return True

    @property
    def login_available(self):
        """Whether a LoginHandler is needed - and therefore whether the login page should be displayed."""
        return self.auth_enabled

    @property
    def logout_available(self):
        """Whether a LogoutHandler is needed."""
        return True
Exemple #12
0
 def _update_mathjax_config(self, change):
     self.log.info(_i18n("Using MathJax configuration file: %s"),
                   change['new'])
Exemple #13
0
class NotebookAppTraits(HasTraits):

    ignore_minified_js = Bool(
        False,
        config=True,
        help=_i18n(
            'Deprecated: Use minified JS file or not, mainly use during dev to avoid JS recompilation'
        ),
    )

    jinja_environment_options = Dict(
        config=True,
        help=_i18n(
            "Supply extra arguments that will be passed to Jinja environment.")
    )

    jinja_template_vars = Dict(
        config=True,
        help=_i18n(
            "Extra variables to supply to jinja templates when rendering."),
    )

    enable_mathjax = Bool(
        True,
        config=True,
        help="""Whether to enable MathJax for typesetting math/TeX

        MathJax is the javascript library Jupyter uses to render math/LaTeX. It is
        very large, so you may want to disable it if you have a slow internet
        connection, or for offline use of the notebook.

        When disabled, equations etc. will appear as their untransformed TeX source.
        """)

    @observe('enable_mathjax')
    def _update_enable_mathjax(self, change):
        """set mathjax url to empty if mathjax is disabled"""
        if not change['new']:
            self.mathjax_url = u''

    extra_static_paths = List(
        Unicode(),
        config=True,
        help="""Extra paths to search for serving static files.

        This allows adding javascript/css to be available from the notebook server machine,
        or overriding individual files in the IPython""")

    @property
    def static_file_path(self):
        """return extra paths + the default location"""
        return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]

    static_custom_path = List(
        Unicode(), help=_i18n("""Path to search for custom.js, css"""))

    @default('static_custom_path')
    def _default_static_custom_path(self):
        return [
            os.path.join(d, 'custom')
            for d in (self.config_dir, DEFAULT_STATIC_FILES_PATH)
        ]

    extra_template_paths = List(
        Unicode(),
        config=True,
        help=_i18n("""Extra paths to search for serving jinja templates.

        Can be used to override templates from notebook.templates."""))

    @property
    def template_file_path(self):
        """return extra paths + the default locations"""
        return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST

    extra_nbextensions_path = List(
        Unicode(),
        config=True,
        help=_i18n(
            """extra paths to look for Javascript notebook extensions"""))

    @property
    def nbextensions_path(self):
        """The path to look for Javascript notebook extensions"""
        path = self.extra_nbextensions_path + jupyter_path('nbextensions')
        # FIXME: remove IPython nbextensions path after a migration period
        try:
            from IPython.paths import get_ipython_dir
        except ImportError:
            pass
        else:
            path.append(os.path.join(get_ipython_dir(), 'nbextensions'))
        return path

    mathjax_url = Unicode("",
                          config=True,
                          help="""A custom url for MathJax.js.
        Should be in the form of a case-sensitive url to MathJax,
        for example:  /static/components/MathJax/MathJax.js
        """)

    @property
    def static_url_prefix(self):
        """Get the static url prefix for serving static files."""
        return super(NotebookAppTraits, self).static_url_prefix

    @default('mathjax_url')
    def _default_mathjax_url(self):
        if not self.enable_mathjax:
            return u''
        static_url_prefix = self.static_url_prefix
        return url_path_join(static_url_prefix, 'components', 'MathJax',
                             'MathJax.js')

    @observe('mathjax_url')
    def _update_mathjax_url(self, change):
        new = change['new']
        if new and not self.enable_mathjax:
            # enable_mathjax=False overrides mathjax_url
            self.mathjax_url = u''
        else:
            self.log.info(_i18n("Using MathJax: %s"), new)

    mathjax_config = Unicode(
        "TeX-AMS-MML_HTMLorMML-full,Safe",
        config=True,
        help=_i18n(
            """The MathJax.js configuration file that is to be used."""))

    @observe('mathjax_config')
    def _update_mathjax_config(self, change):
        self.log.info(_i18n("Using MathJax configuration file: %s"),
                      change['new'])

    quit_button = Bool(
        True,
        config=True,
        help="""If True, display a button in the dashboard to quit
        (shutdown the notebook server).""")

    nbserver_extensions = Dict(
        {},
        config=True,
        help=(_i18n(
            "Dict of Python modules to load as notebook server extensions."
            "Entry values can be used to enable and disable the loading of"
            "the extensions. The extensions will be loaded in alphabetical "
            "order.")))
 def info_string(self):
     return _i18n("Serving notebooks from local directory: %s") % self.root_dir
class NotebookApp(
        shim.NBClassicConfigShimMixin,
        ExtensionAppJinjaMixin,
        ExtensionApp,
        traits.NotebookAppTraits,
):

    name = 'notebook'
    version = __version__
    description = _i18n("""The Jupyter HTML Notebook.

    This launches a Tornado based HTML Notebook Server that serves up an HTML5/Javascript Notebook client."""
                        )

    aliases = aliases
    flags = flags
    extension_url = "/tree"
    subcommands = {}

    default_url = Unicode("/tree").tag(config=True)

    # Override the default open_Browser trait in ExtensionApp,
    # setting it to True.
    open_browser = Bool(True,
                        help="""Whether to open in a browser after starting.
        The specific browser used is platform dependent and
        determined by the python standard library `webbrowser`
        module, unless it is overridden using the --browser
        (ServerApp.browser) configuration option.
        """).tag(config=True)

    static_custom_path = List(
        Unicode(), help=_i18n("""Path to search for custom.js, css"""))

    @default('static_custom_path')
    def _default_static_custom_path(self):
        return [
            os.path.join(d, 'custom')
            for d in (self.config_dir, DEFAULT_STATIC_FILES_PATH)
        ]

    extra_nbextensions_path = List(
        Unicode(),
        config=True,
        help=_i18n(
            """extra paths to look for Javascript notebook extensions"""))

    @property
    def nbextensions_path(self):
        """The path to look for Javascript notebook extensions"""
        path = self.extra_nbextensions_path + jupyter_path('nbextensions')
        # FIXME: remove IPython nbextensions path after a migration period
        try:
            from IPython.paths import get_ipython_dir
        except ImportError:
            pass
        else:
            path.append(os.path.join(get_ipython_dir(), 'nbextensions'))
        return path

    @property
    def static_paths(self):
        """Rename trait in jupyter_server."""
        return self.static_file_path

    @property
    def template_paths(self):
        """Rename trait for Jupyter Server."""
        return self.template_file_path

    def _prepare_templates(self):
        super(NotebookApp, self)._prepare_templates()

        # Get translations from notebook package.
        base_dir = os.path.dirname(notebook.__file__)

        nbui = gettext.translation('nbui',
                                   localedir=os.path.join(
                                       base_dir, 'notebook/i18n'),
                                   fallback=True)
        self.jinja2_env.install_gettext_translations(nbui, newstyle=False)

    def initialize_settings(self):
        """Add settings to the tornado app."""
        if self.ignore_minified_js:
            self.log.warning(
                _i18n(
                    """The `ignore_minified_js` flag is deprecated and no longer works."""
                ))
            self.log.warning(
                _i18n(
                    """Alternatively use `%s` when working on the notebook's Javascript and LESS"""
                ) % 'npm run build:watch')
            warnings.warn(
                _i18n(
                    "The `ignore_minified_js` flag is deprecated and will be removed in Notebook 6.0"
                ), DeprecationWarning)

        settings = dict(
            static_custom_path=self.static_custom_path,
            static_handler_args={
                # don't cache custom.js
                'no_cache_paths': [
                    url_path_join(self.serverapp.base_url, 'static', self.name,
                                  'custom')
                ],
            },
            ignore_minified_js=self.ignore_minified_js,
            mathjax_url=self.mathjax_url,
            mathjax_config=self.mathjax_config,
            nbextensions_path=self.nbextensions_path,
        )
        self.settings.update(**settings)

    def initialize_handlers(self):
        """Load the (URL pattern, handler) tuples for each component."""
        # Order matters. The first handler to match the URL will handle the request.
        handlers = []

        # Add a redirect from /notebooks to /edit
        # for opening non-ipynb files in edit mode.
        handlers.append((rf"/{self.file_url_prefix}/((?!.*\.ipynb($|\?)).*)",
                         RedirectHandler, {
                             "url": self.serverapp.base_url + "edit/{0}"
                         }))

        # load extra services specified by users before default handlers
        for service in self.settings['extra_services']:
            handlers.extend(load_handlers(service))

        handlers.extend(load_handlers('nbclassic.tree.handlers'))
        handlers.extend(load_handlers('nbclassic.notebook.handlers'))
        handlers.extend(load_handlers('nbclassic.edit.handlers'))

        # Add terminal handlers
        handlers.append((r"/terminals/(\w+)", TerminalHandler))

        handlers.append(
            (
                r"/nbextensions/(.*)",
                FileFindHandler,
                {
                    'path': self.settings['nbextensions_path'],
                    'no_cache_paths':
                    ['/'],  # don't cache anything in nbextensions
                }), )
        handlers.append(
            (
                r"/custom/(.*)",
                FileFindHandler,
                {
                    'path': self.settings['static_custom_path'],
                    'no_cache_paths':
                    ['/'],  # don't cache anything in nbextensions
                }), )
        # Add new handlers to Jupyter server handlers.
        self.handlers.extend(handlers)
jupyter nbclassic                       # start the notebook
jupyter nbclassic --certfile=mycert.pem # use SSL/TLS certificate
jupyter nbclassic password              # enter a password to protect the server
"""

#-----------------------------------------------------------------------------
# Aliases and Flags
#-----------------------------------------------------------------------------

flags = {}
aliases = {}
flags['no-browser'] = ({
    'ServerApp': {
        'open_browser': False
    }
}, _i18n("Don't open the notebook in a browser after startup."))
flags['no-mathjax'] = ({
    'NotebookApp': {
        'enable_mathjax': False
    }
}, """Disable MathJax

    MathJax is the javascript library Jupyter uses to render math/LaTeX. It is
    very large, so you may want to disable it if you have a slow internet
    connection, or for offline use of the notebook.

    When disabled, equations etc. will appear as their untransformed TeX source.
    """)

flags['allow-root'] = ({
    'ServerApp': {