@@ -1452,32 +1452,45 @@ def close(self):
14521452_CLOUD_SCHEMES = ('s3://' , 'gs://' , 'az://' , 'abfs://' )
14531453
14541454
1455- def _is_http_url (path ) -> bool :
1456- """Return True if *path* is an ``http://`` or ``https://`` URL.
1457-
1458- Case-insensitive: URL schemes are case-insensitive per RFC 3986, so an
1459- uppercase ``HTTP://`` or mixed-case ``Http://`` must dispatch to the
1460- SSRF-validating :class:`_HTTPSource`, not to the fsspec branch. See
1461- issue #2323.
1455+ def _is_http_source (source ) -> bool :
1456+ """Return True if ``source`` is an HTTP(S) URL, case-insensitively.
1457+
1458+ Centralized so every routing call site in ``xrspatial/geotiff/``
1459+ classifies the scheme the same way. Before this helper existed,
1460+ each call site did ``source.startswith(('http://', 'https://'))``,
1461+ which is case-sensitive and let ``HTTP://example.internal/...``
1462+ (uppercase) slip past :class:`_HTTPSource` and the SSRF allow-list
1463+ in :func:`_validate_http_url`. Per RFC 3986 section 3.1 URI schemes
1464+ are case-insensitive, so any uppercase / mixed-case variant has to
1465+ route through the same validator as ``http`` / ``https``.
1466+
1467+ Non-string inputs (``None``, ``bytes``, ``os.PathLike``, file-like
1468+ objects) return ``False`` so callers can drop the surrounding
1469+ ``isinstance(_, str)`` check where they want to. Issues #2323 / #2332.
14621470 """
1463- if not isinstance (path , str ):
1464- return False
1465- try :
1466- scheme = urlparse (path ).scheme
1467- except (ValueError , TypeError ):
1471+ if not isinstance (source , str ) or not source :
14681472 return False
1469- return scheme .lower () in ('http' , 'https' )
1473+ # ``urlparse`` strips off the scheme cleanly even for unusual inputs
1474+ # (e.g. ``HTTP:`` with no ``//``) and avoids the prefix-tuple trap.
1475+ return urlparse (source ).scheme .lower () in ('http' , 'https' )
1476+
1477+
1478+ # Back-compat alias: earlier patches (#2323) shipped this same helper under
1479+ # the name ``_is_http_url`` and downstream tests / re-exports still use that
1480+ # name. Keep the alias so importers and the regression tests stay green.
1481+ _is_http_url = _is_http_source
14701482
14711483
14721484def _is_fsspec_uri (path : str ) -> bool :
14731485 """Check if a path is a fsspec-compatible URI (not http/https/local).
14741486
14751487 Excludes http(s) case-insensitively so uppercase URLs cannot dodge the
1476- SSRF allow-list and pinned DNS in :class:`_HTTPSource` (issue #2323).
1488+ SSRF allow-list and pinned DNS in :class:`_HTTPSource` (issues #2323 /
1489+ #2332).
14771490 """
14781491 if not isinstance (path , str ):
14791492 return False
1480- if _is_http_url (path ):
1493+ if _is_http_source (path ):
14811494 return False
14821495 return '://' in path
14831496
@@ -1658,7 +1671,7 @@ def _open_source(source):
16581671 raise TypeError (
16591672 f"source must be a str path/URL or a binary file-like object "
16601673 f"with read+seek methods, got { type (source ).__name__ } " )
1661- if _is_http_url (source ):
1674+ if _is_http_source (source ):
16621675 return _HTTPSource (source )
16631676 if _is_fsspec_uri (source ):
16641677 return _CloudSource (source )
0 commit comments