"""Validation helpers for request data.""" from urllib.parse import urlparse # Special sentinel values allowed in allowed_cors_origins _CORS_SENTINELS = {"+", "*"} def validate_cors_origins(origins): """Validate a list of CORS origin values. Accepts: None - means "use global CORS config" (pass-through) ["+"] - derive origins from the client's redirect_uris ["*"] - allow any origin ["https://host"] - explicit allow-list of well-formed origins Each non-sentinel entry must be a well-formed origin: scheme (http or https) + host + optional port, with NO path, query string, or fragment. Returns: (validated_value, None) on success, or (None, error_message) on failure. """ if origins is None: return None, None if not isinstance(origins, list): return None, "allowed_cors_origins must be a list or null" validated = [] for i, entry in enumerate(origins): if not isinstance(entry, str): return None, f"allowed_cors_origins[{i}]: expected a string, got {type(entry).__name__}" entry = entry.strip() if not entry: return None, f"allowed_cors_origins[{i}]: empty string is not allowed" # Sentinel values are accepted as-is if entry in _CORS_SENTINELS: validated.append(entry) continue # Parse and validate as origin error = _validate_single_origin(entry, i) if error: return None, error validated.append(entry) return validated, None def _validate_single_origin(origin, index): """Validate that a string is a well-formed browser origin. A valid origin is: scheme://host[:port] with no path, query, or fragment. Only http and https schemes are accepted. Returns an error message string on failure, or None on success. """ try: parsed = urlparse(origin) except Exception: return f"allowed_cors_origins[{index}]: '{origin}' is not a valid URL" if parsed.scheme not in ("http", "https"): return ( f"allowed_cors_origins[{index}]: '{origin}' has an invalid scheme " f"'{parsed.scheme}'; only 'http' and 'https' are allowed" ) if not parsed.hostname: return f"allowed_cors_origins[{index}]: '{origin}' is missing a hostname" # Origins must not have a path (other than empty or "/"), query, or fragment if parsed.path and parsed.path != "/": return ( f"allowed_cors_origins[{index}]: '{origin}' must not contain a path " f"(got '{parsed.path}'). Specify only scheme://host[:port]" ) if parsed.query: return ( f"allowed_cors_origins[{index}]: '{origin}' must not contain a query string" ) if parsed.fragment: return ( f"allowed_cors_origins[{index}]: '{origin}' must not contain a fragment" ) return None