Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-jupyter-server for 
openSUSE:Factory checked in at 2026-10-18 19:25:31
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-jupyter-server (Old)
 and      /work/SRC/openSUSE:Factory/.python-jupyter-server.new.3698736 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-jupyter-server"

Sun Oct 18 19:25:31 2026 rev:49 rq:1385851 version:2.21.1

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-jupyter-server/python-jupyter-server.changes  
    2026-06-30 15:13:53.864984712 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-jupyter-server.new.3698736/python-jupyter-server.changes
 2026-10-18 19:25:45.992890118 +0200
@@ -1,0 +2,6 @@
+Sat Oct 17 15:17:15 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 2.21.1:
+  * Fix compatibility with Tornado 6.5.9 and newer
+
+-------------------------------------------------------------------

Old:
----
  jupyter_server-2.20.0.tar.gz

New:
----
  jupyter_server-2.21.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-jupyter-server.spec ++++++
--- /var/tmp/diff_new_pack.fFMeKQ/_old  2026-10-18 19:25:47.124937213 +0200
+++ /var/tmp/diff_new_pack.fFMeKQ/_new  2026-10-18 19:25:47.129937421 +0200
@@ -24,20 +24,17 @@
 %define psuffix %{nil}
 %bcond_with test
 %endif
-
 %if 0%{?suse_version} > 1500
 %bcond_without libalternatives
 %else
 %bcond_with libalternatives
 %endif
-
 %{?sle15_python_module_pythons}
 Name:           python-jupyter-server%{psuffix}
-Version:        2.20.0
+Version:        2.21.1
 Release:        0
 Summary:        The backend to Jupyter web applications
 License:        BSD-3-Clause
-Group:          Development/Languages/Python
 URL:            https://jupyter-server.readthedocs.io
 # SourceRepository: https://github.com/jupyter-server/jupyter_server
 Source:         
https://files.pythonhosted.org/packages/source/j/jupyter_server/jupyter_server-%{version}.tar.gz
@@ -96,7 +93,6 @@
 
 %package test
 Summary:        The backend to Jupyter web applications - test requirements
-Group:          Development/Languages/Python
 Requires:       python-flaky
 Requires:       python-ipykernel
 Requires:       python-jupyter-server = %{version}

++++++ jupyter_server-2.20.0.tar.gz -> jupyter_server-2.21.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.20.0/.github/workflows/publish-release.yml 
new/jupyter_server-2.21.1/.github/workflows/publish-release.yml
--- old/jupyter_server-2.20.0/.github/workflows/publish-release.yml     
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/.github/workflows/publish-release.yml     
2020-02-02 01:00:00.000000000 +0100
@@ -54,8 +54,9 @@
         run: |
           echo "Failed to Publish the Draft Release Url:"
           echo ${{ steps.populate-release.outputs.release_url }}
+
       - name: Send Zulip notification
-        if: startsWith(github.ref, 'refs/tags/')
+        if: ${{ success() }}
         continue-on-error: true
         uses: 
zulip/github-actions-zulip/send-message@08b6fbd07f5834e5b930a85bc7740e9fd44ab2e7
         with:
@@ -65,6 +66,6 @@
           to: "Releases"
           type: "stream"
           topic: "jupyter_server"
-          content: | # pypi strips the v from the tag name in urls
-            Jupyter Server ${{ github.ref_name }} was just released on PyPI! 🎉
-            https://pypi.org/project/jupyter-server/${{ github.ref_name }}/
+          content: |
+            Jupyter Server was just released! 🎉
+            ${{ steps.finalize-release.outputs.release_url }}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.20.0/.github/workflows/python-tests.yml 
new/jupyter_server-2.21.1/.github/workflows/python-tests.yml
--- old/jupyter_server-2.20.0/.github/workflows/python-tests.yml        
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/.github/workflows/python-tests.yml        
2020-02-02 01:00:00.000000000 +0100
@@ -131,8 +131,7 @@
         with:
           dependency_type: pre
           python_package_manager: pip
-          # pyO3 dependencies can't build on dev python
-          python_version: "3.14"
+          python_version: "3.15"
       - name: Run the tests
         run: |
           hatch run test:nowarn || hatch -v run test:nowarn --lf
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.20.0/CHANGELOG.md 
new/jupyter_server-2.21.1/CHANGELOG.md
--- old/jupyter_server-2.20.0/CHANGELOG.md      2020-02-02 01:00:00.000000000 
+0100
+++ new/jupyter_server-2.21.1/CHANGELOG.md      2020-02-02 01:00:00.000000000 
+0100
@@ -4,6 +4,61 @@
 
 <!-- <START NEW CHANGELOG ENTRY> -->
 
+## 2.21.1
+
+([Full 
Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.21.0...00d45c80eeb0f50f191b236ee47cc1d82a7db9ca))
+
+### Bugs fixed
+
+- Fix compatibility with Tornado v6.5.9+ Jupyter 
[#1703](https://github.com/jupyter-server/jupyter_server/pull/1703) 
([@krassowski](https://github.com/krassowski), 
[@andrii-i](https://github.com/andrii-i))
+
+### Documentation improvements
+
+- Fix broken link, link to actually useful resources on OWASP 
[#1704](https://github.com/jupyter-server/jupyter_server/pull/1704) 
([@krassowski](https://github.com/krassowski), 
[@jtpio](https://github.com/jtpio))
+
+### Contributors to this release
+
+The following people contributed discussions, new ideas, code and 
documentation contributions, and review.
+See [our definition of 
contributors](https://github-activity.readthedocs.io/en/latest/use/#how-does-this-tool-define-contributions-in-the-reports).
+
+([GitHub contributors page for this 
release](https://github.com/jupyter-server/jupyter_server/graphs/contributors?from=2026-08-27&to=2026-09-15&type=c))
+
+@andrii-i 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aandrii-i+updated%3A2026-08-27..2026-09-15&type=Issues))
 | @jtpio 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Ajtpio+updated%3A2026-08-27..2026-09-15&type=Issues))
 | @krassowski 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Akrassowski+updated%3A2026-08-27..2026-09-15&type=Issues))
+
+<!-- <END NEW CHANGELOG ENTRY> -->
+
+## 2.21.0
+
+([Full 
Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.20.0...14b40e6717b4093628a748fd46ca86570405bc70))
+
+### Enhancements made
+
+- Show connectable URL for password authentication 
[#1677](https://github.com/jupyter-server/jupyter_server/pull/1677) 
([@Sanjays2402](https://github.com/Sanjays2402), 
[@krassowski](https://github.com/krassowski))
+- Allow clients to supply a `kernel_id` on kernel/session creation 
[#1655](https://github.com/jupyter-server/jupyter_server/pull/1655) 
([@sfc-gh-sbirmiwal](https://github.com/sfc-gh-sbirmiwal), 
[@Zsailer](https://github.com/Zsailer), 
[@krassowski](https://github.com/krassowski), 
[@minrk](https://github.com/minrk))
+
+### Bugs fixed
+
+- Fix referer token leak in server logs when the request causes a 500 
[#1681](https://github.com/jupyter-server/jupyter_server/pull/1681) 
([@Yann-P](https://github.com/Yann-P), 
[@krassowski](https://github.com/krassowski))
+- Self-remove event subscriber on `WebSocketClosedError` 
[#1680](https://github.com/jupyter-server/jupyter_server/pull/1680) 
([@Zsailer](https://github.com/Zsailer), 
[@krassowski](https://github.com/krassowski))
+- Use hostname in wildcard browser redirect files 
[#1673](https://github.com/jupyter-server/jupyter_server/pull/1673) 
([@sjh9714](https://github.com/sjh9714), [@Yann-P](https://github.com/Yann-P), 
[@krassowski](https://github.com/krassowski))
+- Fix `resolvePath` handling for home-relative paths 
[#1670](https://github.com/jupyter-server/jupyter_server/pull/1670) 
([@MUFFANUJ](https://github.com/MUFFANUJ), 
[@krassowski](https://github.com/krassowski))
+
+### Maintenance and upkeep improvements
+
+- Fix downstream failure in traitlets, due to `packaging` change. 
[#1688](https://github.com/jupyter-server/jupyter_server/pull/1688) 
([@Carreau](https://github.com/Carreau), 
[@krassowski](https://github.com/krassowski))
+- Fix zulip notification on release 
[#1665](https://github.com/jupyter-server/jupyter_server/pull/1665) 
([@Yann-P](https://github.com/Yann-P), [@Zsailer](https://github.com/Zsailer))
+- Drop Pin preventing testing Python 3.15 pre-releases 
[#1662](https://github.com/jupyter-server/jupyter_server/pull/1662) 
([@krassowski](https://github.com/krassowski), 
[@Zsailer](https://github.com/Zsailer))
+- Remove condition to skip pending kernels on older `jupyter_client` versions 
(no longer needed) 
[#1656](https://github.com/jupyter-server/jupyter_server/pull/1656) 
([@krassowski](https://github.com/krassowski), 
[@Zsailer](https://github.com/Zsailer))
+
+### Contributors to this release
+
+The following people contributed discussions, new ideas, code and 
documentation contributions, and review.
+See [our definition of 
contributors](https://github-activity.readthedocs.io/en/latest/use/#how-does-this-tool-define-contributions-in-the-reports).
+
+([GitHub contributors page for this 
release](https://github.com/jupyter-server/jupyter_server/graphs/contributors?from=2026-06-17&to=2026-08-27&type=c))
+
+@Carreau 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3ACarreau+updated%3A2026-06-17..2026-08-27&type=Issues))
 | @krassowski 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Akrassowski+updated%3A2026-06-17..2026-08-27&type=Issues))
 | @minrk 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aminrk+updated%3A2026-06-17..2026-08-27&type=Issues))
 | @MUFFANUJ 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3AMUFFANUJ+updated%3A2026-06-17..2026-08-27&type=Issues))
 | @Sanjays2402 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3ASanjays2402+updated%3A2026-06-17..2026-08-27&type=Issues))
 | @sfc-gh-sbirmiwal 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Asfc-gh-sbirmiwal+updated%3A2026-06-17..2026-08-27&type=Issues))
 | @sjh9714 ([activity](https://gi
 
thub.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Asjh9714+updated%3A2026-06-17..2026-08-27&type=Issues))
 | @Yann-P 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3AYann-P+updated%3A2026-06-17..2026-08-27&type=Issues))
 | @Zsailer 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3AZsailer+updated%3A2026-06-17..2026-08-27&type=Issues))
+
 ## 2.20.0
 
 ([Full 
Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.19.0...333e700119ee0bcc0b5fcd4c158213d7c275c778))
@@ -38,8 +93,6 @@
 
 @Carreau 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3ACarreau+updated%3A2026-05-29..2026-06-17&type=Issues))
 | @choldgraf 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Acholdgraf+updated%3A2026-05-29..2026-06-17&type=Issues))
 | @Copilot 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3ACopilot+updated%3A2026-05-29..2026-06-17&type=Issues))
 | @ianthomas23 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aianthomas23+updated%3A2026-05-29..2026-06-17&type=Issues))
 | @krassowski 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Akrassowski+updated%3A2026-05-29..2026-06-17&type=Issues))
 | @minrk 
([activity](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aminrk+updated%3A2026-05-29..2026-06-17&type=Issues))
 | @Yann-P ([activity](https://github.com/search?q
 
=repo%3Ajupyter-server%2Fjupyter_server+involves%3AYann-P+updated%3A2026-05-29..2026-06-17&type=Issues))
 
-<!-- <END NEW CHANGELOG ENTRY> -->
-
 ## 2.19.0
 
 ([Full 
Changelog](https://github.com/jupyter-server/jupyter_server/compare/v2.18.2...664e2255c71efe963f397b9f803dbcf503b5a920))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.20.0/PKG-INFO 
new/jupyter_server-2.21.1/PKG-INFO
--- old/jupyter_server-2.20.0/PKG-INFO  2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/PKG-INFO  2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
-Metadata-Version: 2.4
+Metadata-Version: 2.5
 Name: jupyter_server
-Version: 2.20.0
+Version: 2.21.1
 Summary: The backend—i.e. core services, APIs, and REST endpoints—to Jupyter 
web applications.
 Project-URL: Homepage, https://jupyter-server.readthedocs.io
 Project-URL: Documentation, https://jupyter-server.readthedocs.io
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.20.0/docs/source/operators/public-server.rst 
new/jupyter_server-2.21.1/docs/source/operators/public-server.rst
--- old/jupyter_server-2.20.0/docs/source/operators/public-server.rst   
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/docs/source/operators/public-server.rst   
2020-02-02 01:00:00.000000000 +0100
@@ -180,7 +180,7 @@
 certificate and follow the steps in :ref:`using-lets-encrypt` to set up a
 public server.
 
-.. _OWASP: https://owasp.org/sitemap/
+.. _OWASP: 
https://owasp.org/projects?view=level&category=Documentation&level=flagship
 .. _tutorial: 
https://arstechnica.com/information-technology/2009/12/how-to-get-set-with-a-secure-sertificate-for-free/
 
 .. _jupyter_public_server:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.20.0/jupyter_server/_version.py 
new/jupyter_server-2.21.1/jupyter_server/_version.py
--- old/jupyter_server-2.20.0/jupyter_server/_version.py        2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/jupyter_server/_version.py        2020-02-02 
01:00:00.000000000 +0100
@@ -6,7 +6,7 @@
 import re
 
 # Version string must appear intact for automatic versioning
-__version__ = "2.20.0"
+__version__ = "2.21.1"
 
 # Build up version_info tuple for backwards compatibility
 pattern = r"(?P<major>\d+).(?P<minor>\d+).(?P<patch>\d+)(?P<rest>.*)"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.20.0/jupyter_server/base/handlers.py 
new/jupyter_server-2.21.1/jupyter_server/base/handlers.py
--- old/jupyter_server-2.20.0/jupyter_server/base/handlers.py   2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/jupyter_server/base/handlers.py   2020-02-02 
01:00:00.000000000 +0100
@@ -992,6 +992,8 @@
     # cache search results, don't search for files more than once
     _static_paths: dict[str, str] = {}
     root: tuple[str]  # type:ignore[assignment]
+    # default on the class, so a subclass with its own initialize() still has 
it
+    follow_dir_symlinks: bool = False
 
     def set_headers(self) -> None:
         """Set the headers."""
@@ -1009,14 +1011,23 @@
         ):
             self.set_header("Cache-Control", "no-cache")
 
-    def initialize(
+    # tornado 6.5.9 took argument 3 of StaticFileHandler.initialize for
+    # allowed_symlink_directory; this handler has had no_cache_paths there 
since the
+    # notebook days. Harmless in practice: tornado binds these by name from 
the route
+    # kwargs (self.initialize(**kwargs)), so neither is ever passed 
positionally, and
+    # validate_absolute_path below sets allowed_symlink_directory itself, per 
request,
+    # to the root that matched.
+    def initialize(  # type: ignore[override]
         self,
         path: str | list[str],
         default_filename: str | None = None,
         no_cache_paths: list[str] | None = None,
+        *,
+        follow_dir_symlinks: bool = False,
     ) -> None:
         """Initialize the file find handler."""
         self.no_cache_paths = no_cache_paths or []
+        self.follow_dir_symlinks = follow_dir_symlinks
 
         if isinstance(path, str):
             path = [path]
@@ -1066,8 +1077,30 @@
             if (absolute_path + os.sep).startswith(root):
                 break
 
+        # tornado >= 6.5.9 checks symlink targets against this attribute, 
which its
+        # own initialize() sets. The search path means the root that matched 
is only
+        # known here. Older tornado never reads it.
+        self.allowed_symlink_directory = self._symlink_directory(root, 
absolute_path)
+
         return super().validate_absolute_path(root, absolute_path)
 
+    def _symlink_directory(self, root: str, absolute_path: str) -> str:
+        """Directory that symlink targets are validated against.
+
+        The root the file was found under, unless ``follow_dir_symlinks`` is 
set and
+        a directory below it is a link, in which case that link's target is 
used and
+        anything below stays confined to it. The file name is never 
considered, so a
+        linked file cannot widen the check.
+        """
+        if not self.follow_dir_symlinks:
+            return root
+        current = root.rstrip(os.sep)
+        for part in absolute_path[len(root) :].split(os.sep)[:-1]:
+            current = os.path.join(current, part)
+            if os.path.islink(current):
+                return os.path.realpath(current) + os.sep
+        return root
+
 
 class APIVersionHandler(APIHandler):
     """An API handler for the server version."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.20.0/jupyter_server/kernelspecs/handlers.py 
new/jupyter_server-2.21.1/jupyter_server/kernelspecs/handlers.py
--- old/jupyter_server-2.20.0/jupyter_server/kernelspecs/handlers.py    
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/jupyter_server/kernelspecs/handlers.py    
2020-02-02 01:00:00.000000000 +0100
@@ -51,6 +51,9 @@
         try:
             kspec = await ensure_async(ksm.get_kernel_spec(kernel_name))
             self.root = kspec.resource_dir
+            # initialize() ran with an empty path, so tornado >= 6.5.9 would 
check
+            # symlink targets against the working directory. Older tornado 
ignores it.
+            self.allowed_symlink_directory = self.root
         except KeyError as e:
             raise web.HTTPError(404, "Kernel spec %s not found" % kernel_name) 
from e
         self.log.debug("Serving kernel resource from: %s", self.root)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.20.0/jupyter_server/log.py 
new/jupyter_server-2.21.1/jupyter_server/log.py
--- old/jupyter_server-2.20.0/jupyter_server/log.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/jupyter_server/log.py     2020-02-02 
01:00:00.000000000 +0100
@@ -102,7 +102,11 @@
         headers = {}
         for header in ["Host", "Accept", "Referer", "User-Agent"]:
             if header in request.headers:
-                headers[header] = request.headers[header]
+                value = request.headers[header]
+                if header == "Referer":
+                    # GHSA-c3mw-737p-c7g2, prevent leaking the Referer token.
+                    value = _scrub_uri(value, extra_param_keys)
+                headers[header] = value
         log_method(json.dumps(headers, indent=2))
     log_method(msg.format(**ns))
     if record_prometheus_metrics:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.20.0/jupyter_server/serverapp.py 
new/jupyter_server-2.21.1/jupyter_server/serverapp.py
--- old/jupyter_server-2.20.0/jupyter_server/serverapp.py       2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/jupyter_server/serverapp.py       2020-02-02 
01:00:00.000000000 +0100
@@ -207,6 +207,7 @@
 
 # Added for backwards compatibility from classic notebook server.
 DEFAULT_SERVER_PORT = DEFAULT_JUPYTER_SERVER_PORT
+WILDCARD_IPS = ("0.0.0.0", "::")  # noqa: S104
 
 # -----------------------------------------------------------------------------
 # Helper functions
@@ -2423,7 +2424,9 @@
 
     @property
     def connection_url(self) -> str:
-        urlparts = self._get_urlparts(path=self.base_url)
+        """Return a connectable URL, replacing wildcard addresses with the 
hostname."""
+        ip = socket.gethostname() if self.ip in WILDCARD_IPS else None
+        urlparts = self._get_urlparts(path=self.base_url, ip=ip)
         return urlparts.geturl()
 
     @property
@@ -2434,7 +2437,7 @@
         If `ip` is the wildcard address, add a text hint but keep
         the machine hostname in the link as 0.0.0.0 is not connectable.
         """
-        if self.ip not in ("0.0.0.0", "::"):  # noqa: S104
+        if self.ip not in WILDCARD_IPS:
             return self.display_url
         public = self._get_urlparts(include_token=True, 
ip=socket.gethostname()).geturl()
         hint = _i18n(
@@ -3149,9 +3152,16 @@
         if self.open_browser and not self.sock:
             self.launch_browser()
 
-        if self.identity_provider.token and 
self.identity_provider.token_generated:
-            # log full URL with generated token, so there's a copy/pasteable 
link
-            # with auth info.
+        generated_token = bool(
+            self.identity_provider.token and 
self.identity_provider.token_generated
+        )
+        non_token_auth_on_wildcard = (
+            not self.identity_provider.token
+            and self.identity_provider.auth_enabled
+            and self.ip in WILDCARD_IPS
+        )
+        if generated_token or non_token_auth_on_wildcard:
+            # Log the generated token or a connectable hostname for non-token 
authentication.
             if self.sock:
                 self.log.critical(
                     "\n".join(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.20.0/jupyter_server/services/contents/filemanager.py 
new/jupyter_server-2.21.1/jupyter_server/services/contents/filemanager.py
--- old/jupyter_server-2.20.0/jupyter_server/services/contents/filemanager.py   
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/jupyter_server/services/contents/filemanager.py   
2020-02-02 01:00:00.000000000 +0100
@@ -253,6 +253,19 @@
     def resolve_path(self, path: str) -> str | None:
         """Resolve path relative to root resource."""
         # transform OS path to API path
+        original_path = Path(path)
+        try:
+            expanded_path = original_path.expanduser()
+        except RuntimeError:
+            expanded_path = original_path
+        if expanded_path != original_path and expanded_path.is_absolute():
+            root = Path(os.path.abspath(self.root_dir))
+            expanded_abs = Path(os.path.abspath(expanded_path))
+            try:
+                expanded_abs.relative_to(root)
+            except ValueError:
+                return None
+            path = str(expanded_abs)
         relative_path = to_api_path(path, self.root_dir)
         # check if the API path is within contents directory
         try:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.20.0/jupyter_server/services/events/handlers.py 
new/jupyter_server-2.21.1/jupyter_server/services/events/handlers.py
--- old/jupyter_server-2.20.0/jupyter_server/services/events/handlers.py        
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/jupyter_server/services/events/handlers.py        
2020-02-02 01:00:00.000000000 +0100
@@ -56,9 +56,25 @@
     async def event_listener(
         self, logger: jupyter_events.logger.EventLogger, schema_id: str, data: 
dict[str, Any]
     ) -> None:
-        """Write an event message."""
+        """Write an event message to the subscribed WebSocket.
+
+        If the underlying socket has already been closed — typically
+        because the peer disappeared without a clean close handshake, so
+        ``on_close`` never fired — Tornado raises
+        :class:`~tornado.websocket.WebSocketClosedError` from
+        ``write_message``; in that case ``event_listener`` will remove itself 
from the logger
+        so subsequent events don't fan out one failing task per stale
+        subscriber.
+        """
         capsule = dict(schema_id=schema_id, **data)
-        self.write_message(json.dumps(capsule))
+        try:
+            self.write_message(json.dumps(capsule))
+        except websocket.WebSocketClosedError:
+            self.log.debug(
+                "Event WebSocket for %s is closed; removing stale listener",
+                schema_id,
+            )
+            self.event_logger.remove_listener(listener=self.event_listener)
 
     def open(self) -> None:  # type: ignore[override]
         """Routes events that are emitted by Jupyter Server's
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.20.0/jupyter_server/services/kernels/handlers.py 
new/jupyter_server-2.21.1/jupyter_server/services/kernels/handlers.py
--- old/jupyter_server-2.20.0/jupyter_server/services/kernels/handlers.py       
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/jupyter_server/services/kernels/handlers.py       
2020-02-02 01:00:00.000000000 +0100
@@ -53,7 +53,11 @@
             model.setdefault("name", km.default_kernel_name)
 
         kernel_id: str = await ensure_async(
-            km.start_kernel(kernel_name=model["name"], path=model.get("path"))
+            km.start_kernel(
+                kernel_name=model["name"],
+                path=model.get("path"),
+                kernel_id=model.get("kernel_id"),
+            )
         )
         model = await ensure_async(km.kernel_model(kernel_id))
         location = url_path_join(self.base_url, "api", "kernels", 
url_escape(kernel_id))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.20.0/jupyter_server/services/sessions/handlers.py 
new/jupyter_server-2.21.1/jupyter_server/services/sessions/handlers.py
--- old/jupyter_server-2.20.0/jupyter_server/services/sessions/handlers.py      
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/jupyter_server/services/sessions/handlers.py      
2020-02-02 01:00:00.000000000 +0100
@@ -14,6 +14,7 @@
     from jupyter_client.jsonutil import date_default as json_default
 
 from jupyter_client.kernelspec import NoSuchKernel
+from jupyter_client.multikernelmanager import DuplicateKernelError
 from jupyter_core.utils import ensure_async
 from tornado import web
 
@@ -104,6 +105,8 @@
                 self.set_status(501)
                 self.finish(json.dumps({"message": msg, "short_message": 
status_msg}))
                 return
+            except DuplicateKernelError as e:
+                raise web.HTTPError(409, str(e)) from e
             except Exception as e:
                 raise web.HTTPError(500, str(e)) from e
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.20.0/jupyter_server/services/sessions/sessionmanager.py 
new/jupyter_server-2.21.1/jupyter_server/services/sessions/sessionmanager.py
--- 
old/jupyter_server-2.20.0/jupyter_server/services/sessions/sessionmanager.py    
    2020-02-02 01:00:00.000000000 +0100
+++ 
new/jupyter_server-2.21.1/jupyter_server/services/sessions/sessionmanager.py    
    2020-02-02 01:00:00.000000000 +0100
@@ -283,7 +283,12 @@
             pass
         else:
             kernel_id = await self.start_kernel_for_session(
-                session_id, path, name, type, kernel_name
+                session_id=session_id,
+                path=path,
+                name=name,
+                type=type,
+                kernel_name=kernel_name,
+                kernel_id=kernel_id,
             )
         record.kernel_id = kernel_id
         self._pending_sessions.update(record)
@@ -317,6 +322,7 @@
         name: ModelName | None,
         type: str | None,
         kernel_name: KernelName | None,
+        kernel_id: str | None = None,
     ) -> str:
         """Start a new kernel for a given session.
 
@@ -333,6 +339,11 @@
             the type of the session
         kernel_name : str
             the name of the kernel specification to use.  The default kernel 
name will be used if not provided.
+        kernel_id : str, optional
+            client-supplied UUID to register the new kernel under.  When
+            provided, forwarded to ``kernel_manager.start_kernel`` so the
+            kernel is created at that exact id.  If omitted, the kernel
+            manager mints a fresh id.
         """
         # allow contents manager to specify kernels cwd
         kernel_path = await 
ensure_async(self.contents_manager.get_kernel_path(path=path))
@@ -342,6 +353,7 @@
             path=kernel_path,
             kernel_name=kernel_name,
             env=kernel_env,
+            kernel_id=kernel_id,
         )
         return cast("str", kernel_id)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.20.0/jupyter_server/utils.py 
new/jupyter_server-2.21.1/jupyter_server/utils.py
--- old/jupyter_server-2.20.0/jupyter_server/utils.py   2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/jupyter_server/utils.py   2020-02-02 
01:00:00.000000000 +0100
@@ -28,7 +28,7 @@
 from urllib.request import pathname2url as _pathname2url
 
 from jupyter_core.utils import ensure_async as _ensure_async
-from packaging.version import Version
+from packaging.version import InvalidVersion, Version
 from tornado.httpclient import AsyncHTTPClient, HTTPClient, HTTPRequest, 
HTTPResponse
 from tornado.netutil import Resolver
 
@@ -178,13 +178,15 @@
 def check_version(v: str, check: str) -> bool:
     """check version string v >= check
 
-    If dev/prerelease tags result in TypeError for string-number comparison,
+    If a version cannot be parsed (dev/prerelease tags, or a non-string),
     it is assumed that the dependency is satisfied.
     Users on dev branches are responsible for keeping their own packages up to 
date.
     """
     try:
         return bool(Version(v) >= Version(check))
-    except TypeError:
+    except (TypeError, InvalidVersion):
+        # packaging >= 26.3 raises InvalidVersion where it used to raise
+        # TypeError for non-string inputs.
         return True
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.20.0/tests/base/test_handlers.py 
new/jupyter_server-2.21.1/tests/base/test_handlers.py
--- old/jupyter_server-2.20.0/tests/base/test_handlers.py       2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/tests/base/test_handlers.py       2020-02-02 
01:00:00.000000000 +0100
@@ -5,6 +5,7 @@
 from unittest.mock import MagicMock, patch
 
 import pytest
+import tornado
 from tornado.httpclient import HTTPClientError
 from tornado.httpserver import HTTPRequest
 from tornado.httputil import HTTPHeaders
@@ -391,3 +392,46 @@
     handler.settings["static_immutable_cache"] = [str(tmpdir)]
     await handler.get("foo")
     assert handler._headers["Cache-Control"] == "public, max-age=31536000, 
immutable"
+
+
[email protected](os.name == "nt", reason="symlinks need extra permissions 
on Windows")
[email protected](
+    tornado.version_info < (6, 5, 9),
+    reason="tornado only validates symlink targets from 6.5.9",
+)
+async def test_static_handler_follow_dir_symlinks(jp_serverapp, tmp_path):
+    """A linked directory is served only when the handler opts in."""
+
+    async def async_magic():
+        pass
+
+    MagicMock.__await__ = lambda x: async_magic().__await__()
+
+    root = tmp_path / "root"
+    outside = tmp_path / "outside"
+    root.mkdir()
+    (outside / "ext").mkdir(parents=True)
+    (outside / "ext" / "index.js").write_text("hello")
+    (outside / "secret.txt").write_text("secret")
+    (root / "ext").symlink_to(outside / "ext")
+    (outside / "ext" / "escape.txt").symlink_to(outside / "secret.txt")
+
+    app: ServerApp = jp_serverapp
+
+    def handler(**kwargs):
+        request = HTTPRequest("GET", "/")
+        request.connection = MagicMock()
+        found = FileFindHandler(app.web_app, request, path=str(root), **kwargs)
+        found._transforms = []
+        return found
+
+    with pytest.raises(HTTPError) as excinfo:
+        await handler().get("ext/index.js")
+    assert excinfo.value.status_code == 403
+
+    await handler(follow_dir_symlinks=True).get("ext/index.js")
+
+    # a link below the linked directory still cannot leave it
+    with pytest.raises(HTTPError) as excinfo:
+        await handler(follow_dir_symlinks=True).get("ext/escape.txt")
+    assert excinfo.value.status_code == 403
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.20.0/tests/services/events/test_api.py 
new/jupyter_server-2.21.1/tests/services/events/test_api.py
--- old/jupyter_server-2.20.0/tests/services/events/test_api.py 2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/tests/services/events/test_api.py 2020-02-02 
01:00:00.000000000 +0100
@@ -42,6 +42,44 @@
     assert event_data.get("event_message") == "Hello, world!"
 
 
+async def test_subscribe_websocket_self_removes_on_closed_socket(event_logger, 
jp_ws_fetch):
+    """When the subscriber's socket has died without a clean close
+    handshake (so ``on_close`` never fired), the listener should remove
+    itself from the event logger the next time an event fires, instead
+    of remaining pinned in ``_modified_listeners`` and pinning its
+    handler instance with it.
+    """
+    schema_id = "http://event.mock.jupyter.org/message";
+
+    ws = await jp_ws_fetch("/api/events/subscribe")
+
+    # The ``open()`` hook adds our bound ``event_listener`` to the
+    # logger. Grab it back so we can reach the underlying handler
+    # instance and assert removal later.
+    listeners = event_logger._modified_listeners.get(schema_id, set())
+    assert len(listeners) == 1, "expected exactly one subscriber listener"
+    (listener,) = listeners
+    handler = listener.__self__
+
+    # Simulate an abrupt server-side teardown: clear ``ws_connection``
+    # without calling ``on_close``. Tornado's own ``write_message`` will
+    # then raise ``WebSocketClosedError`` on the next emit, exercising
+    # the self-removal path.
+    handler.ws_connection = None
+
+    event_logger.emit(schema_id=schema_id, data={"event_message": "hi"})
+    await event_logger.gather_listeners()
+
+    remaining = event_logger._modified_listeners.get(schema_id, set())
+    assert listener not in remaining, "listener should have removed itself"
+
+    # A second emit must be a no-op (no accumulated failing tasks).
+    event_logger.emit(schema_id=schema_id, data={"event_message": "hi again"})
+    await event_logger.gather_listeners()
+
+    ws.close()
+
+
 payload_1 = """\
 {
     "schema_id": "http://event.mock.jupyter.org/message";,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.20.0/tests/services/kernels/test_api.py 
new/jupyter_server-2.21.1/tests/services/kernels/test_api.py
--- old/jupyter_server-2.20.0/tests/services/kernels/test_api.py        
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/tests/services/kernels/test_api.py        
2020-02-02 01:00:00.000000000 +0100
@@ -103,6 +103,28 @@
 
 
 @pytest.mark.timeout(TEST_TIMEOUT)
+async def test_post_kernel_with_client_supplied_id(
+    jp_fetch, jp_base_url, jp_serverapp, pending_kernel_is_ready
+):
+    """``POST /api/kernels { "kernel_id": <uuid>, "name": <kernel_name> }``
+    should register a kernel at the supplied id.
+    """
+    supplied_kernel_id = "11111111-2222-3333-4444-555555555555"
+    body = json.dumps({"name": NATIVE_KERNEL_NAME, "kernel_id": 
supplied_kernel_id})
+    r = await jp_fetch("api", "kernels", method="POST", body=body)
+    assert r.code == 201
+    kernel = json.loads(r.body.decode())
+    assert kernel["id"] == supplied_kernel_id
+    assert r.headers["location"] == url_path_join(jp_base_url, 
"/api/kernels/", supplied_kernel_id)
+    await pending_kernel_is_ready(supplied_kernel_id)
+
+    # Confirm that ``GET /api/kernels/<id>`` resolves to the same id.
+    r = await jp_fetch("api", "kernels", supplied_kernel_id, method="GET")
+    assert r.code == 200
+    assert json.loads(r.body.decode())["id"] == supplied_kernel_id
+
+
[email protected](TEST_TIMEOUT)
 async def test_main_kernel_handler(jp_fetch, jp_base_url, jp_serverapp, 
pending_kernel_is_ready):
     # Start the first kernel
     r = await jp_fetch(
@@ -267,6 +289,86 @@
     assert path["path"] == server_path.name
 
 
[email protected](TEST_TIMEOUT)
+async def test_resolve_path_server_expands_user(jp_fetch, jp_serverapp, 
jp_root_dir, monkeypatch):
+    query_path = "~/my-preferred-dir/foo.py"
+    contents = "(irrelevant)"
+
+    monkeypatch.setenv("HOME", str(jp_root_dir))
+    monkeypatch.setenv("USERPROFILE", str(jp_root_dir))
+
+    # Put the file under the user's home, which is also the server root.
+    server_path = jp_root_dir / "my-preferred-dir" / "foo.py"
+    server_path.parent.mkdir(parents=True, exist_ok=True)
+    server_path.write_text(contents)
+
+    # Resolve the path
+    r = await jp_fetch("api", "resolvePath", params={"path": query_path}, 
method="GET")
+
+    assert r.code == 200
+    resolution = json.loads(r.body.decode())
+    assert len(resolution["resolved"]) == 1
+    path = resolution["resolved"][0]
+    assert path["scope"] == "server"
+
+    # Should resolve home-relative paths to API paths within the server root.
+    assert path["path"] == "my-preferred-dir/foo.py"
+
+
[email protected](TEST_TIMEOUT)
+async def test_resolve_path_server_expands_user_normalizes_path(
+    jp_fetch, jp_serverapp, jp_root_dir, monkeypatch
+):
+    query_path = "~/my-preferred-dir/../foo.py"
+    contents = "(irrelevant)"
+
+    monkeypatch.setenv("HOME", str(jp_root_dir))
+    monkeypatch.setenv("USERPROFILE", str(jp_root_dir))
+
+    # Put the file under the user's home, which is also the server root.
+    (jp_root_dir / "my-preferred-dir").mkdir()
+    server_path = jp_root_dir / "foo.py"
+    server_path.write_text(contents)
+
+    # Resolve the path
+    r = await jp_fetch("api", "resolvePath", params={"path": query_path}, 
method="GET")
+
+    assert r.code == 200
+    resolution = json.loads(r.body.decode())
+    assert len(resolution["resolved"]) == 1
+    path = resolution["resolved"][0]
+    assert path["scope"] == "server"
+
+    # Should return a normalized API path, not the original path with ".." 
segments.
+    assert path["path"] == "foo.py"
+
+
[email protected](TEST_TIMEOUT)
+async def test_resolve_path_server_expands_user_rejects_outside_root(
+    jp_fetch, jp_serverapp, jp_root_dir, monkeypatch
+):
+    query_path = "~/outside-root.py"
+    contents = "(irrelevant)"
+
+    home_dir = jp_root_dir.parent / f"{jp_root_dir.name}-home"
+    home_dir.mkdir(parents=True, exist_ok=True)
+    monkeypatch.setenv("HOME", str(home_dir))
+    monkeypatch.setenv("USERPROFILE", str(home_dir))
+
+    # Put the file under the user's home, but outside the server root.
+    server_path = home_dir / "outside-root.py"
+    server_path.write_text(contents)
+
+    # Resolve the path
+    r = await jp_fetch("api", "resolvePath", params={"path": query_path}, 
method="GET")
+
+    assert r.code == 200
+    resolution = json.loads(r.body.decode())
+
+    # Should NOT disclose file existence outside the server root after 
expansion.
+    assert len(resolution["resolved"]) == 0
+
+
 @pytest.mark.skipif(
     ipykernel.version_info < (7, 0),
     reason="requires ipykernel 7, which is not compatible with Python 3.10",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.20.0/tests/services/kernels/test_cull.py 
new/jupyter_server-2.21.1/tests/services/kernels/test_cull.py
--- old/jupyter_server-2.20.0/tests/services/kernels/test_cull.py       
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/tests/services/kernels/test_cull.py       
2020-02-02 01:00:00.000000000 +0100
@@ -6,7 +6,6 @@
 import uuid
 import warnings
 
-import jupyter_client
 import pytest
 from tornado.httpclient import HTTPClientError
 from traitlets.config import Config
@@ -191,12 +190,11 @@
     assert not culled
 
 
-# Pending kernels was released in Jupyter Client 7.1
 # It is currently broken on Windows (Jan 2022). When fixed, we can remove the 
Windows check.
 # See https://github.com/jupyter-server/jupyter_server/issues/672
 @pytest.mark.skipif(
-    os.name == "nt" or jupyter_client._version.version_info < (7, 1),
-    reason="Pending kernels require jupyter_client >= 7.1 on non-Windows",
+    os.name == "nt",
+    reason="Pending kernels fail on Windows",
 )
 @pytest.mark.parametrize(
     "jp_server_config",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/jupyter_server-2.20.0/tests/services/sessions/test_api.py 
new/jupyter_server-2.21.1/tests/services/sessions/test_api.py
--- old/jupyter_server-2.20.0/tests/services/sessions/test_api.py       
2020-02-02 01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/tests/services/sessions/test_api.py       
2020-02-02 01:00:00.000000000 +0100
@@ -392,6 +392,101 @@
 
 
 @pytest.mark.timeout(TEST_TIMEOUT)
+async def test_create_with_client_supplied_kernel_id(
+    session_client, jp_fetch, jp_ws_fetch, jp_base_url, jp_serverapp, 
session_is_ready
+):
+    """A client-supplied ``kernel.id`` for a kernel that doesn't exist yet
+    should cause the server to register the new kernel at that exact id.
+    """
+    supplied_kernel_id = "12345678-1234-5678-1234-567812345678"
+
+    resp = await session_client.create("foo/nb1.ipynb", 
kernel_id=supplied_kernel_id)
+    assert resp.code == 201
+    new_session = j(resp)
+    assert new_session["kernel"]["id"] == supplied_kernel_id
+    sid = new_session["id"]
+    await session_is_ready(sid)
+
+    # The kernel is registered at the supplied id and reachable via the
+    # kernels API.
+    resp = await jp_fetch("api", "kernels", supplied_kernel_id, method="GET")
+    assert resp.code == 200
+    kernel = j(resp)
+    assert kernel["id"] == supplied_kernel_id
+
+    # The kernel websocket also opens against the supplied id.
+    ws = await jp_ws_fetch("api", "kernels", supplied_kernel_id, "channels")
+    ws.close()
+
+
[email protected](TEST_TIMEOUT)
+async def test_create_session_duplicate_kernel_id_returns_409(session_client, 
jp_serverapp):
+    """``DuplicateKernelError`` raised from ``create_session`` should be
+    translated to HTTP 409 by the sessions handler (rather than the
+    catch-all 500 path).
+
+    Reproducing this end-to-end is awkward because ``create_session``'s
+    ``kernel_id in self.kernel_manager`` short-circuit attaches to the
+    existing kernel before ``start_kernel`` could raise.  In practice
+    the duplicate path fires on concurrent creates that race the
+    short-circuit.  We simulate that by patching ``create_session`` to
+    raise ``DuplicateKernelError`` directly and asserting the handler
+    maps it to 409.
+    """
+    from jupyter_client.multikernelmanager import DuplicateKernelError
+
+    sm = jp_serverapp.session_manager
+    original_create_session = sm.create_session
+    expected_message = "Kernel already exists: collision-test"
+
+    async def raising_create_session(*args, **kwargs):
+        raise DuplicateKernelError(expected_message)
+
+    sm.create_session = raising_create_session  # type: ignore[method-assign]
+    try:
+        with pytest.raises(tornado.httpclient.HTTPClientError) as exc_info:
+            await session_client.create("foo/nb1.ipynb", kernel_name="python3")
+        assert expected_http_error(exc_info, 409)
+        # The DuplicateKernelError message should make it to the response body.
+        body = json.loads(exc_info.value.response.body.decode())
+        assert expected_message in body["message"]
+    finally:
+        sm.create_session = original_create_session  # type: 
ignore[method-assign]
+
+
[email protected](TEST_TIMEOUT)
+async def test_create_attach_to_existing_kernel_via_kernel_id(
+    session_client, jp_fetch, jp_serverapp, session_is_ready
+):
+    """Existing behavior: ``kernel.id`` referencing a kernel already in
+    ``MultiKernelManager`` attaches the session to that kernel."""
+    # Create a kernel via the kernels API; let the server mint the id.
+    resp = await jp_fetch("api/kernels", method="POST", 
allow_nonstandard_methods=True)
+    existing_kernel = j(resp)
+
+    # Now create a session that attaches to it.
+    resp = await session_client.create("foo/nb1.ipynb", 
kernel_id=existing_kernel["id"])
+    assert resp.code == 201
+    new_session = j(resp)
+    assert new_session["kernel"]["id"] == existing_kernel["id"]
+
+
[email protected](TEST_TIMEOUT)
+async def test_create_session_without_kernel_id_mints_uuid(
+    session_client, jp_serverapp, session_is_ready
+):
+    """Backwards compat: omitting ``kernel.id`` lets the server mint one."""
+    resp = await session_client.create("foo/nb1.ipynb")
+    assert resp.code == 201
+    new_session = j(resp)
+    minted = new_session["kernel"]["id"]
+    # Server-minted: present and looks like a UUID.
+    assert minted
+    assert len(minted) == 36
+    assert minted.count("-") == 4
+
+
[email protected](TEST_TIMEOUT)
 async def test_delete(session_client, jp_serverapp, session_is_ready):
     resp = await session_client.create("foo/nb1.ipynb")
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.20.0/tests/test_log.py 
new/jupyter_server-2.21.1/tests/test_log.py
--- old/jupyter_server-2.20.0/tests/test_log.py 2020-02-02 01:00:00.000000000 
+0100
+++ new/jupyter_server-2.21.1/tests/test_log.py 2020-02-02 01:00:00.000000000 
+0100
@@ -72,3 +72,26 @@
     assert "default_token" not in call_args
     assert "[secret]" in call_args
     assert "normal=value" in call_args
+
+
+def 
test_log_request_scrubs_referer_in_5xx_header_dump(server_app_with_default_scrub_keys,
 caplog):
+    """Test that the 5xx JSON header dump scrubs sensitive params from the 
Referer header. GHSA-c3mw-737p-c7g2"""
+    handler = Mock()
+    handler.get_status.return_value = 500
+    handler.request.method = "POST"
+    handler.request.remote_ip = "127.0.0.1"
+    handler.request.uri = "http://example.com/api/kernels";
+    handler.request.request_time.return_value = 0.1
+    handler.request.headers = {"Referer": 
"http://example.com/tree?token=REFERTOKEN"}
+    handler.settings = {
+        "extra_log_scrub_param_keys": 
server_app_with_default_scrub_keys.extra_log_scrub_param_keys
+    }
+    handler.log = Mock()
+    handler.current_user = None
+
+    log_request(handler, record_prometheus_metrics=False)
+
+    call_args = handler.log.error.call_args_list[0][0][0]
+
+    assert "REFERTOKEN" not in call_args
+    assert "[secret]" in call_args
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jupyter_server-2.20.0/tests/test_serverapp.py 
new/jupyter_server-2.21.1/tests/test_serverapp.py
--- old/jupyter_server-2.20.0/tests/test_serverapp.py   2020-02-02 
01:00:00.000000000 +0100
+++ new/jupyter_server-2.21.1/tests/test_serverapp.py   2020-02-02 
01:00:00.000000000 +0100
@@ -328,14 +328,14 @@
             {"ip": "0.0.0.0"},
             "http://0.0.0.0:8888/?token=<generated>",
             "http://127.0.0.1:8888/?token=<generated>",
-            "http://0.0.0.0:8888/";,
+            "http://myhost:8888/";,
         ),
         (
             # https://github.com/jupyterlab/jupyterlab/issues/15520
             {"ip": "::"},
             "http://[::]:8888/?token=<generated>",
             "http://[::1]:8888/?token=<generated>",
-            "http://[::]:8888/";,
+            "http://myhost:8888/";,
         ),
     ],
 )
@@ -353,7 +353,8 @@
         connection_url = connection_url.replace("<generated>", token)
     assert serverapp.public_url == public_url
     assert serverapp.local_url == local_url
-    assert serverapp.connection_url == connection_url
+    with patch("socket.gethostname", return_value="myhost"):
+        assert serverapp.connection_url == connection_url
     # Cleanup singleton after test.
     ServerApp.clear_instance()
 
@@ -413,6 +414,30 @@
     ServerApp.clear_instance()
 
 
+def test_password_auth_logs_connect_url_for_wildcard():
+    config = Config()
+    config.PasswordIdentityProvider.hashed_password = 
"configured-password-hash"
+    app = ServerApp(
+        ip="0.0.0.0",
+        port=8889,
+        allow_root=True,
+        open_browser=False,
+        no_browser_open_file=True,
+        config=config,
+    )
+    app.init_configurables()
+    assert not app.identity_provider.token
+
+    with (
+        patch("socket.gethostname", return_value="myhost"),
+        patch.object(app, "write_server_info_file"),
+        patch.object(app.log, "critical") as log_critical,
+    ):
+        app.start_app()
+
+    assert any(f"http://myhost:{app.port}/"; in call.args[0] for call in 
log_critical.call_args_list)
+
+
 # Preferred dir tests
 # ----------------------------------------------------------------------------
 @pytest.mark.filterwarnings("ignore::FutureWarning")
@@ -687,6 +712,17 @@
     assert url_messages if should_exist else not url_messages
 
 
+def 
test_browser_open_file_uses_hostname_for_wildcard(jp_configurable_serverapp):
+    app = jp_configurable_serverapp(ip="0.0.0.0")
+
+    with patch("socket.gethostname", return_value="myhost"):
+        app.write_browser_open_file()
+
+    browser_open_file = 
pathlib.Path(app.browser_open_file).read_text(encoding="utf-8")
+    assert "http://myhost:"; in browser_open_file
+    assert "http://0.0.0.0:"; not in browser_open_file
+
+
 def test_deprecated_notebook_dir_priority(jp_configurable_serverapp, tmp_path):
     notebook_dir = tmp_path / "notebook"
     notebook_dir.mkdir()

Reply via email to