-
Notifications
You must be signed in to change notification settings - Fork 0
Add TiTiler endpoints for zarr datasets #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3737d7f
2464216
bef4fd2
e694150
7a64477
f62cabf
f0d99e5
7121f57
81ee5c3
622c5ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ | |
| from eo_api.ingestions import routes as ingestion_routes | ||
| from eo_api.pygeoapi_app import mount_pygeoapi | ||
| from eo_api.system import routes as system_routes | ||
| from eo_api.tiles import titiler_routes | ||
|
|
||
| app = FastAPI() | ||
|
|
||
|
|
@@ -27,5 +28,6 @@ | |
| app.include_router(ingestion_routes.ingestions_router, prefix="/ingestions", tags=["Ingestions"]) | ||
| app.include_router(ingestion_routes.zarr_router, prefix="/zarr", tags=["Zarr"]) | ||
| app.include_router(ingestion_routes.sync_router, prefix="/sync", tags=["Sync"]) | ||
| app.include_router(titiler_routes.router, prefix='/titiler', tags=["TiTiler"]) | ||
|
Comment on lines
28
to
+31
|
||
|
|
||
| mount_pygeoapi(app) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| """Tiles package.""" | ||
|
|
||
| from . import titiler as titiler_routes | ||
|
|
||
| __all__ = ["titiler_routes"] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,10 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Xarray-backed TiTiler router configuration""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from titiler.xarray.factory import TilerFactory | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from titiler.xarray.io import Reader | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from titiler.xarray.extensions import VariablesExtension | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| router = TilerFactory( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reader=Reader, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+3
to
+8
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from titiler.xarray.factory import TilerFactory | |
| from titiler.xarray.io import Reader | |
| from titiler.xarray.extensions import VariablesExtension | |
| router = TilerFactory( | |
| reader=Reader, | |
| import ipaddress | |
| import os | |
| import socket | |
| from pathlib import Path | |
| from urllib.parse import urlparse | |
| from fastapi import HTTPException | |
| from titiler.xarray.factory import TilerFactory | |
| from titiler.xarray.io import Reader | |
| from titiler.xarray.extensions import VariablesExtension | |
| _ALLOWED_DATASET_ROOTS_ENV = "EO_API_TITILER_ALLOWED_DATASET_ROOTS" | |
| _ALLOWED_REMOTE_HOSTS_ENV = "EO_API_TITILER_ALLOWED_REMOTE_HOSTS" | |
| _ALLOWED_REMOTE_SCHEMES_ENV = "EO_API_TITILER_ALLOWED_REMOTE_SCHEMES" | |
| def _get_env_list(name: str) -> list[str]: | |
| value = os.getenv(name, "") | |
| return [item.strip() for item in value.split(",") if item.strip()] | |
| def _path_is_within(path: Path, root: Path) -> bool: | |
| try: | |
| path.relative_to(root) | |
| return True | |
| except ValueError: | |
| return False | |
| def _hostname_targets_private_network(hostname: str) -> bool: | |
| try: | |
| parsed_ip = ipaddress.ip_address(hostname) | |
| return ( | |
| parsed_ip.is_private | |
| or parsed_ip.is_loopback | |
| or parsed_ip.is_link_local | |
| or parsed_ip.is_multicast | |
| or parsed_ip.is_reserved | |
| or parsed_ip.is_unspecified | |
| ) | |
| except ValueError: | |
| pass | |
| try: | |
| address_info = socket.getaddrinfo(hostname, None) | |
| except socket.gaierror: | |
| return True | |
| for entry in address_info: | |
| candidate_ip = ipaddress.ip_address(entry[4][0]) | |
| if ( | |
| candidate_ip.is_private | |
| or candidate_ip.is_loopback | |
| or candidate_ip.is_link_local | |
| or candidate_ip.is_multicast | |
| or candidate_ip.is_reserved | |
| or candidate_ip.is_unspecified | |
| ): | |
| return True | |
| return False | |
| def _validate_source(source: str) -> None: | |
| parsed = urlparse(source) | |
| if parsed.scheme: | |
| scheme = parsed.scheme.lower() | |
| if scheme == "file": | |
| raise HTTPException( | |
| status_code=400, | |
| detail="file:// sources are not allowed for tile requests.", | |
| ) | |
| if scheme not in {"http", "https"}: | |
| raise HTTPException( | |
| status_code=400, | |
| detail=f"Unsupported source scheme '{scheme}'.", | |
| ) | |
| allowed_schemes = {item.lower() for item in _get_env_list(_ALLOWED_REMOTE_SCHEMES_ENV)} | |
| if not allowed_schemes or scheme not in allowed_schemes: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Remote sources are not allowed for this deployment.", | |
| ) | |
| hostname = parsed.hostname | |
| if not hostname: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Remote source URL must include a hostname.", | |
| ) | |
| allowed_hosts = {item.lower() for item in _get_env_list(_ALLOWED_REMOTE_HOSTS_ENV)} | |
| if not allowed_hosts or hostname.lower() not in allowed_hosts: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Remote source host is not allowed.", | |
| ) | |
| if _hostname_targets_private_network(hostname): | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Remote source resolves to a private or otherwise disallowed network address.", | |
| ) | |
| return | |
| allowed_roots = [ | |
| Path(item).expanduser().resolve(strict=False) | |
| for item in _get_env_list(_ALLOWED_DATASET_ROOTS_ENV) | |
| ] | |
| if not allowed_roots: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Local sources are not allowed for this deployment.", | |
| ) | |
| resolved_source = Path(source).expanduser().resolve(strict=False) | |
| if not any(_path_is_within(resolved_source, root) for root in allowed_roots): | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Local source path is outside the configured allowed dataset roots.", | |
| ) | |
| class RestrictedReader(Reader): | |
| def __init__(self, *args, **kwargs) -> None: | |
| source = kwargs.get("src_path") or kwargs.get("url") or (args[0] if args else None) | |
| if not isinstance(source, str) or not source: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="A valid source path or URL is required.", | |
| ) | |
| _validate_source(source) | |
| super().__init__(*args, **kwargs) | |
| router = TilerFactory( | |
| reader=RestrictedReader, |
Uh oh!
There was an error while loading. Please reload this page.