Made project use a callback provider to avoid letting almost the whole world on blank
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate CALLBACK_PROVIDER and render its safe Nginx proxy locations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ipaddress
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
|
||||
class ProviderValidationError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CallbackProvider:
|
||||
origin: str
|
||||
path_template: str
|
||||
|
||||
|
||||
def validate_callback_provider(value: str | None) -> CallbackProvider | None:
|
||||
if value is None or value == "":
|
||||
return None
|
||||
if value != value.strip() or len(value) > 2048 or any(character.isspace() for character in value):
|
||||
raise ProviderValidationError("CALLBACK_PROVIDER não pode conter espaços e deve ter até 2048 caracteres")
|
||||
parsed = urlsplit(value)
|
||||
if parsed.scheme != "https" or not parsed.netloc or parsed.username or parsed.password:
|
||||
raise ProviderValidationError("CALLBACK_PROVIDER deve ser uma URL HTTPS pública, sem usuário ou senha")
|
||||
if parsed.query or parsed.fragment or not parsed.path.startswith("/"):
|
||||
raise ProviderValidationError("CALLBACK_PROVIDER não aceita query, fragmento e exige um caminho absoluto")
|
||||
try:
|
||||
port = parsed.port
|
||||
except ValueError as error:
|
||||
raise ProviderValidationError("CALLBACK_PROVIDER possui porta inválida") from error
|
||||
host = parsed.hostname
|
||||
if not host or not re.fullmatch(r"[A-Za-z0-9.-]+", host) or ".." in host or host.startswith(".") or host.endswith("."):
|
||||
raise ProviderValidationError("CALLBACK_PROVIDER possui host inválido")
|
||||
try:
|
||||
address = ipaddress.ip_address(host)
|
||||
except ValueError:
|
||||
address = None
|
||||
if address is not None and not address.is_global:
|
||||
raise ProviderValidationError("CALLBACK_PROVIDER não pode apontar para endereço IP privado ou reservado")
|
||||
if port is not None and not 1 <= port <= 65535:
|
||||
raise ProviderValidationError("CALLBACK_PROVIDER possui porta inválida")
|
||||
if not re.fullmatch(r"/[A-Za-z0-9._~/%{}-]+", parsed.path) or "/../" in f"{parsed.path}/" or any(token in parsed.path for token in ("$", ";", "\\")):
|
||||
raise ProviderValidationError("CALLBACK_PROVIDER possui caminho não permitido")
|
||||
for token in ("{x}", "{y}", "{z}"):
|
||||
if parsed.path.count(token) != 1:
|
||||
raise ProviderValidationError("CALLBACK_PROVIDER deve conter exatamente uma ocorrência de {x}, {y} e {z}")
|
||||
remaining_placeholders = parsed.path
|
||||
for token in ("{x}", "{y}", "{z}"):
|
||||
remaining_placeholders = remaining_placeholders.replace(token, "")
|
||||
if "{" in remaining_placeholders or "}" in remaining_placeholders:
|
||||
raise ProviderValidationError("CALLBACK_PROVIDER contém placeholder desconhecido")
|
||||
return CallbackProvider(origin=f"https://{parsed.netloc}", path_template=parsed.path)
|
||||
|
||||
|
||||
def render_location(name: str, provider: CallbackProvider | None, x: str, y: str, z: str, label: str) -> str:
|
||||
headers = """ add_header Cache-Control \"public, max-age=86400, stale-while-revalidate=604800\" always;
|
||||
add_header Access-Control-Allow-Origin \"*\" always;
|
||||
add_header Access-Control-Allow-Methods \"GET, HEAD, OPTIONS\" always;
|
||||
add_header Access-Control-Allow-Headers \"Origin, Range, Accept\" always;"""
|
||||
if provider is None:
|
||||
return f"""location @{name} {{
|
||||
try_files /tiles/empty.png =500;
|
||||
{headers}
|
||||
}}
|
||||
"""
|
||||
rendered_path = provider.path_template.replace("{x}", f"${x}").replace("{y}", f"${y}").replace("{z}", f"${z}")
|
||||
return f"""location @{name} {{
|
||||
rewrite ^ {rendered_path} break;
|
||||
proxy_pass {provider.origin};
|
||||
proxy_set_header Host $proxy_host;
|
||||
proxy_ssl_server_name on;
|
||||
proxy_cache callback_provider;
|
||||
proxy_cache_valid 200 7d;
|
||||
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
|
||||
add_header X-Map-Source \"Callback provider {label}\" always;
|
||||
{headers}
|
||||
}}
|
||||
"""
|
||||
|
||||
|
||||
def render_nginx(provider: CallbackProvider | None) -> str:
|
||||
return (
|
||||
"# Generated at container startup. Do not edit.\n"
|
||||
+ render_location("callback_provider_tile", provider, "tile_x", "tile_y", "tile_z", "fallback")
|
||||
+ "\n"
|
||||
+ render_location("callback_provider_base", provider, "base_x", "base_y", "base_z", "base")
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--nginx-output", type=Path)
|
||||
arguments = parser.parse_args()
|
||||
provider = validate_callback_provider(__import__("os").environ.get("CALLBACK_PROVIDER"))
|
||||
if arguments.nginx_output:
|
||||
arguments.nginx_output.parent.mkdir(parents=True, exist_ok=True)
|
||||
arguments.nginx_output.write_text(render_nginx(provider), encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except ProviderValidationError as error:
|
||||
raise SystemExit(f"CALLBACK_PROVIDER inválido: {error}")
|
||||
Reference in New Issue
Block a user