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()