217 lines
5.6 KiB
Python
217 lines
5.6 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
Domain Whitelist Validation Test Script
|
||
|
|
|
||
|
|
This script demonstrates and tests the domain whitelist validation logic
|
||
|
|
used in OpenWebUI Skills Manager Tool.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import urllib.parse
|
||
|
|
from typing import Tuple
|
||
|
|
|
||
|
|
|
||
|
|
def validate_domain_whitelist(url: str, trusted_domains: str) -> Tuple[bool, str]:
|
||
|
|
"""
|
||
|
|
Validate if a URL's domain is in the trusted domains whitelist.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
url: The URL to validate
|
||
|
|
trusted_domains: Comma-separated list of trusted primary domains
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Tuple of (is_valid, reason)
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
parsed = urllib.parse.urlparse(url)
|
||
|
|
hostname = parsed.hostname or parsed.netloc
|
||
|
|
|
||
|
|
if not hostname:
|
||
|
|
return False, "No hostname found in URL"
|
||
|
|
|
||
|
|
# Check scheme
|
||
|
|
if parsed.scheme not in ("http", "https"):
|
||
|
|
return (
|
||
|
|
False,
|
||
|
|
f"Unsupported scheme: {parsed.scheme} (only http/https allowed)",
|
||
|
|
)
|
||
|
|
|
||
|
|
# Parse trusted domains
|
||
|
|
trusted_list = [
|
||
|
|
d.strip().lower() for d in (trusted_domains or "").split(",") if d.strip()
|
||
|
|
]
|
||
|
|
|
||
|
|
if not trusted_list:
|
||
|
|
return False, "No trusted domains configured"
|
||
|
|
|
||
|
|
hostname_lower = hostname.lower()
|
||
|
|
|
||
|
|
# Check exact match or subdomain match
|
||
|
|
for trusted_domain in trusted_list:
|
||
|
|
# Exact match
|
||
|
|
if hostname_lower == trusted_domain:
|
||
|
|
return True, f"✓ Exact match: {hostname_lower} == {trusted_domain}"
|
||
|
|
|
||
|
|
# Subdomain match
|
||
|
|
if hostname_lower.endswith("." + trusted_domain):
|
||
|
|
return (
|
||
|
|
True,
|
||
|
|
f"✓ Subdomain match: {hostname_lower}.endswith('.{trusted_domain}')",
|
||
|
|
)
|
||
|
|
|
||
|
|
# Not trusted
|
||
|
|
reason = f"✗ Not in whitelist: {hostname} not matched by {trusted_list}"
|
||
|
|
return False, reason
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
return False, f"Validation error: {e}"
|
||
|
|
|
||
|
|
|
||
|
|
def print_test_result(test_name: str, url: str, trusted_domains: str, expected: bool):
|
||
|
|
"""Pretty print a test result."""
|
||
|
|
is_valid, reason = validate_domain_whitelist(url, trusted_domains)
|
||
|
|
status = "✓ PASS" if is_valid == expected else "✗ FAIL"
|
||
|
|
|
||
|
|
print(f"\n{status} | {test_name}")
|
||
|
|
print(f" URL: {url}")
|
||
|
|
print(f" Domains: {trusted_domains}")
|
||
|
|
print(f" Result: {reason}")
|
||
|
|
|
||
|
|
|
||
|
|
# Test Cases
|
||
|
|
if __name__ == "__main__":
|
||
|
|
print("=" * 70)
|
||
|
|
print("Domain Whitelist Validation Tests")
|
||
|
|
print("=" * 70)
|
||
|
|
|
||
|
|
# ========== Scenario 1: GitHub Only ==========
|
||
|
|
print("\n" + "🔹" * 35)
|
||
|
|
print("Scenario 1: GitHub Domain Only")
|
||
|
|
print("🔹" * 35)
|
||
|
|
|
||
|
|
github_domains = "github.com"
|
||
|
|
|
||
|
|
print_test_result(
|
||
|
|
"GitHub exact domain",
|
||
|
|
"https://github.com/Fu-Jie/openwebui-extensions",
|
||
|
|
github_domains,
|
||
|
|
expected=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test_result(
|
||
|
|
"GitHub API subdomain",
|
||
|
|
"https://api.github.com/repos/Fu-Jie/openwebui-extensions",
|
||
|
|
github_domains,
|
||
|
|
expected=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test_result(
|
||
|
|
"GitHub Gist subdomain",
|
||
|
|
"https://gist.github.com/Fu-Jie/test",
|
||
|
|
github_domains,
|
||
|
|
expected=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test_result(
|
||
|
|
"GitHub Raw (wrong domain)",
|
||
|
|
"https://raw.githubusercontent.com/Fu-Jie/openwebui-extensions/main/test.py",
|
||
|
|
github_domains,
|
||
|
|
expected=False,
|
||
|
|
)
|
||
|
|
|
||
|
|
# ========== Scenario 2: GitHub + GitHub Raw ==========
|
||
|
|
print("\n" + "🔹" * 35)
|
||
|
|
print("Scenario 2: GitHub + GitHub Raw Content")
|
||
|
|
print("🔹" * 35)
|
||
|
|
|
||
|
|
github_all_domains = "github.com,githubusercontent.com"
|
||
|
|
|
||
|
|
print_test_result(
|
||
|
|
"GitHub Raw (now allowed)",
|
||
|
|
"https://raw.githubusercontent.com/Fu-Jie/openwebui-extensions/main/test.py",
|
||
|
|
github_all_domains,
|
||
|
|
expected=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test_result(
|
||
|
|
"GitHub Raw with subdomain",
|
||
|
|
"https://cdn.jsdelivr.net/gh/Fu-Jie/openwebui-extensions/test.py",
|
||
|
|
github_all_domains,
|
||
|
|
expected=False,
|
||
|
|
)
|
||
|
|
|
||
|
|
# ========== Scenario 3: Multiple Trusted Domains ==========
|
||
|
|
print("\n" + "🔹" * 35)
|
||
|
|
print("Scenario 3: Multiple Trusted Domains")
|
||
|
|
print("🔹" * 35)
|
||
|
|
|
||
|
|
multi_domains = "github.com,huggingface.co,anthropic.com"
|
||
|
|
|
||
|
|
print_test_result(
|
||
|
|
"GitHub domain", "https://github.com/Fu-Jie/test", multi_domains, expected=True
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test_result(
|
||
|
|
"HuggingFace domain",
|
||
|
|
"https://huggingface.co/models/gpt-4",
|
||
|
|
multi_domains,
|
||
|
|
expected=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test_result(
|
||
|
|
"HuggingFace Hub subdomain",
|
||
|
|
"https://hub.huggingface.co/models/gpt-4",
|
||
|
|
multi_domains,
|
||
|
|
expected=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test_result(
|
||
|
|
"Anthropic domain",
|
||
|
|
"https://anthropic.com/research",
|
||
|
|
multi_domains,
|
||
|
|
expected=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test_result(
|
||
|
|
"Untrusted domain",
|
||
|
|
"https://bitbucket.org/Fu-Jie/test",
|
||
|
|
multi_domains,
|
||
|
|
expected=False,
|
||
|
|
)
|
||
|
|
|
||
|
|
# ========== Edge Cases ==========
|
||
|
|
print("\n" + "🔹" * 35)
|
||
|
|
print("Edge Cases")
|
||
|
|
print("🔹" * 35)
|
||
|
|
|
||
|
|
print_test_result(
|
||
|
|
"FTP scheme (not allowed)",
|
||
|
|
"ftp://github.com/Fu-Jie/test",
|
||
|
|
github_domains,
|
||
|
|
expected=False,
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test_result(
|
||
|
|
"File scheme (not allowed)",
|
||
|
|
"file:///etc/passwd",
|
||
|
|
github_domains,
|
||
|
|
expected=False,
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test_result(
|
||
|
|
"Case insensitive domain",
|
||
|
|
"HTTPS://GITHUB.COM/Fu-Jie/test",
|
||
|
|
github_domains,
|
||
|
|
expected=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
print_test_result(
|
||
|
|
"Deep subdomain",
|
||
|
|
"https://api.v2.github.com/repos",
|
||
|
|
github_domains,
|
||
|
|
expected=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
print("\n" + "=" * 70)
|
||
|
|
print("✓ All tests completed!")
|
||
|
|
print("=" * 70)
|