Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9afe335384 | |||
| c14a28b093 | |||
| 568bccb37e | |||
| e5a24df13d | |||
| 6f34fdc782 | |||
| a5a460e765 |
3
.standalone.pid
Normal file
3
.standalone.pid
Normal file
@@ -0,0 +1,3 @@
|
||||
830554
|
||||
830555
|
||||
830569
|
||||
@@ -201,7 +201,38 @@ def load_config() -> dict:
|
||||
except Exception as e:
|
||||
log.warning(f"⚠️ Failed to load {secrets_yaml_path}: {e}")
|
||||
|
||||
# 4. Apply Environment Overrides (D-06)
|
||||
# 4. Load network.yaml (SSOT for infrastructure)
|
||||
network_yaml_path = os.path.join(config_dir, "network.yaml")
|
||||
if os.path.exists(network_yaml_path):
|
||||
try:
|
||||
with open(network_yaml_path, 'r') as f:
|
||||
network_data = yaml.safe_load(f) or {}
|
||||
# Map infrastructure settings to application domain
|
||||
infra = network_data.get("application", {})
|
||||
if "server_ip" in infra:
|
||||
config["application"]["server_ip"] = infra["server_ip"]
|
||||
if "cors_origins" in infra:
|
||||
# Append or override? Plan says establish as master.
|
||||
# We merge with any existing ones.
|
||||
existing = config["application"].get("cors_origins", "")
|
||||
if existing:
|
||||
config["application"]["cors_origins"] = f"{infra['cors_origins']},{existing}"
|
||||
else:
|
||||
config["application"]["cors_origins"] = infra["cors_origins"]
|
||||
|
||||
# Map ports to application domain for CORS logic
|
||||
ports = network_data.get("ports", {})
|
||||
for key, val in ports.items():
|
||||
config["application"][key] = val
|
||||
|
||||
# Map SSL state
|
||||
config["application"]["ssl_enabled"] = network_data.get("ssl", {}).get("ssl_enabled", False)
|
||||
|
||||
log.info(f"✅ Loaded network topology from {network_yaml_path}")
|
||||
except Exception as e:
|
||||
log.warning(f"⚠️ Failed to load {network_yaml_path}: {e}")
|
||||
|
||||
# 5. Apply Environment Overrides (D-06)
|
||||
_apply_env_overrides(config)
|
||||
|
||||
_config = config
|
||||
|
||||
@@ -72,6 +72,74 @@ class ConfigManager:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_secrets_path():
|
||||
"""Returns the absolute path to secrets.yaml."""
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(base_dir)
|
||||
return os.path.join(project_root, "config", "secrets.yaml")
|
||||
|
||||
@staticmethod
|
||||
def update_secrets(updates: dict) -> dict:
|
||||
"""Update secrets.yaml with new values (flattened structure)."""
|
||||
path = ConfigManager.get_secrets_path()
|
||||
|
||||
current_secrets = {}
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
current_secrets = yaml.safe_load(f) or {}
|
||||
except Exception as e:
|
||||
log.error(f"❌ Failed to read {path}: {e}")
|
||||
|
||||
# Update flattened secrets
|
||||
current_secrets.update(updates)
|
||||
|
||||
try:
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
# Header for clarity
|
||||
f.write("# TFM aInventory - Managed Secrets (Updated via Admin UI)\n")
|
||||
yaml.dump(current_secrets, f, default_flow_style=False, sort_keys=True)
|
||||
log.info(f"✅ Updated {path} with new secrets.")
|
||||
|
||||
# Reload the global config
|
||||
load_config()
|
||||
return get_config()
|
||||
except Exception as e:
|
||||
log.error(f"❌ Failed to write {path}: {e}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def update_keys(updates: dict) -> dict:
|
||||
"""
|
||||
Intelligent key update: routes sensitive AI keys to secrets.yaml
|
||||
and others to backend.yaml.
|
||||
"""
|
||||
secrets_updates = {}
|
||||
backend_updates = {}
|
||||
|
||||
sensitive_keys = ["GEMINI_API_KEY", "CLAUDE_API_KEY", "JWT_SECRET_KEY", "LDAP_PASSWORD"]
|
||||
|
||||
for key, val in updates.items():
|
||||
if key in sensitive_keys:
|
||||
secrets_updates[key] = val
|
||||
else:
|
||||
# Non-sensitive or complex nested settings
|
||||
backend_updates[key] = val
|
||||
|
||||
if secrets_updates:
|
||||
ConfigManager.update_secrets(secrets_updates)
|
||||
|
||||
if backend_updates:
|
||||
ConfigManager.update_config(backend_updates)
|
||||
|
||||
return get_config()
|
||||
|
||||
@staticmethod
|
||||
def get_config() -> dict:
|
||||
"""Return the current global configuration."""
|
||||
return get_config()
|
||||
|
||||
@staticmethod
|
||||
def get_masked_key(key_name: str):
|
||||
"""Returns a masked version of a configuration value or env var."""
|
||||
@@ -113,6 +181,10 @@ def update_config(updates: dict) -> dict:
|
||||
"""Update backend.yaml with new values and return updated config."""
|
||||
return ConfigManager.update_config(updates)
|
||||
|
||||
def get_config() -> dict:
|
||||
"""Return the current global configuration."""
|
||||
return ConfigManager.get_config()
|
||||
|
||||
def validate_config_file() -> bool:
|
||||
"""Validate backend.yaml syntax and required fields."""
|
||||
return ConfigManager.validate_config_file()
|
||||
|
||||
@@ -67,8 +67,11 @@ def get_ai_config(
|
||||
current_admin: auth.TokenData = Depends(auth.get_current_admin)
|
||||
):
|
||||
"""Check AI provider status and active provider."""
|
||||
gemini_key = os.environ.get("GEMINI_API_KEY")
|
||||
claude_key = os.environ.get("CLAUDE_API_KEY")
|
||||
config = ConfigManager.get_config() # Alias for config_loader.get_config
|
||||
ai_config = config.get("ai", {})
|
||||
|
||||
gemini_key = ai_config.get("gemini_api_key")
|
||||
claude_key = ai_config.get("claude_api_key")
|
||||
|
||||
provider_setting = db.query(models.SystemSetting).filter(models.SystemSetting.key == "ai_provider").first()
|
||||
active_provider = provider_setting.value if provider_setting else "gemini"
|
||||
@@ -112,10 +115,14 @@ def update_ai_keys(
|
||||
if updates:
|
||||
ConfigManager.update_keys(updates)
|
||||
|
||||
# Re-load config to return accurate status
|
||||
config = ConfigManager.get_config()
|
||||
ai_cfg = config.get("ai", {})
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"gemini_configured": bool(os.environ.get("GEMINI_API_KEY")),
|
||||
"claude_configured": bool(os.environ.get("CLAUDE_API_KEY"))
|
||||
"gemini_configured": bool(ai_cfg.get("gemini_api_key")),
|
||||
"claude_configured": bool(ai_cfg.get("claude_api_key"))
|
||||
}
|
||||
|
||||
|
||||
|
||||
BIN
backups/ainventory_2026-04-23_12-58-31.tar.gz
Normal file
BIN
backups/ainventory_2026-04-23_12-58-31.tar.gz
Normal file
Binary file not shown.
BIN
backups/ainventory_2026-04-23_13-58-34.tar.gz
Normal file
BIN
backups/ainventory_2026-04-23_13-58-34.tar.gz
Normal file
Binary file not shown.
BIN
backups/ainventory_2026-04-23_14-04-08.tar.gz
Normal file
BIN
backups/ainventory_2026-04-23_14-04-08.tar.gz
Normal file
Binary file not shown.
41
config/Caddyfile.standalone
Normal file
41
config/Caddyfile.standalone
Normal file
@@ -0,0 +1,41 @@
|
||||
# TFM aInventory - Caddy Patched IP Configuration
|
||||
# Version 1.9.17 - The Dynamic Shield (Production Polish)
|
||||
{
|
||||
admin off
|
||||
# Global TLS options for self-signed certificates
|
||||
local_certs
|
||||
skip_install_trust
|
||||
|
||||
# Configure on-demand TLS for private network IPs
|
||||
on_demand_tls {
|
||||
# Pointing to the backend root which returns 200 OK
|
||||
# This allows Caddy to generate internal certs for any IP/domain.
|
||||
ask http://backend:8000/
|
||||
}
|
||||
}
|
||||
|
||||
# Dynamic SSL Proxy (Matches ANY IP or hostname)
|
||||
https:// {
|
||||
tls internal {
|
||||
on_demand
|
||||
}
|
||||
|
||||
reverse_proxy frontend:3000
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-XSS-Protection "1; mode=block"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
}
|
||||
}
|
||||
|
||||
# Specific port listener for backend (8918 -> 444)
|
||||
https://:444 {
|
||||
tls internal {
|
||||
on_demand
|
||||
}
|
||||
reverse_proxy backend:8000
|
||||
}
|
||||
}
|
||||
@@ -75,14 +75,17 @@ Controls the Next.js frontend application behavior and PWA settings.
|
||||
| `pwa.app_name` | Display name of the PWA | `TFM aInventory` |
|
||||
|
||||
### network.yaml
|
||||
Defines host-side port mappings and SSL/CORS policies.
|
||||
**SINGLE SOURCE OF TRUTH (SSOT):** This is the master file for your server's network topology. All other domains (Backend CORS, Frontend API URL) are dynamically derived from these settings.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `application.server_ip` | **MASTER IP** of your server | `localhost` |
|
||||
| `application.cors_origins`| Extra allowed origins (Subnets/IPs) | - |
|
||||
| `ports.backend_port` | Host port for backend | `8916` |
|
||||
| `ports.frontend_port` | Host port for frontend | `8917` |
|
||||
| `ssl.ssl_enabled` | Enable HTTPS via Caddy | `true` |
|
||||
| `cors.allowed_origins` | List of allowed origins | `*` |
|
||||
|
||||
> **Note:** You only need to change the IP address in **this file**. The system will automatically update the frontend's API URL and the backend's CORS policies upon restart.
|
||||
|
||||
### docker.yaml
|
||||
Resource limits and container orchestration settings.
|
||||
|
||||
@@ -35,21 +35,8 @@ ai:
|
||||
# Environment: BACKEND_AI_FALLBACK_PROVIDER
|
||||
fallback_provider: "claude"
|
||||
|
||||
# Gemini API Key (obtain from https://aistudio.google.com/)
|
||||
# Environment: BACKEND_AI_GEMINI_API_KEY or GEMINI_API_KEY
|
||||
gemini_api_key: "your-gemini-api-key"
|
||||
|
||||
# Claude API Key (obtain from Anthropic Console)
|
||||
# Environment: BACKEND_AI_CLAUDE_API_KEY or CLAUDE_API_KEY
|
||||
claude_api_key: "your-claude-api-key"
|
||||
|
||||
# --- Authentication & LDAP ---
|
||||
auth:
|
||||
# JWT Secret Key (min 32 chars)
|
||||
# Generate with: openssl rand -hex 32
|
||||
# Environment: BACKEND_AUTH_JWT_SECRET_KEY or JWT_SECRET_KEY
|
||||
jwt_secret_key: "change_me_in_production"
|
||||
|
||||
# Enable LDAP Authentication?
|
||||
# Default: false
|
||||
# Environment: BACKEND_AUTH_LDAP_ENABLED or LDAP_ENABLED
|
||||
@@ -124,10 +111,6 @@ application:
|
||||
# Environment: BACKEND_APPLICATION_LOGS_DIR or LOGS_DIR
|
||||
logs_dir: "./logs"
|
||||
|
||||
# Comma-separated list of extra allowed CORS origins (IPs/FQDNs)
|
||||
# Environment: BACKEND_APPLICATION_CORS_ORIGINS or EXTRA_ALLOWED_ORIGINS
|
||||
cors_origins: "http://localhost:8917"
|
||||
|
||||
# --- Feature Flags ---
|
||||
features:
|
||||
# Enable AI image extraction and OCR?
|
||||
|
||||
@@ -6,11 +6,9 @@
|
||||
|
||||
# --- API Connection ---
|
||||
api:
|
||||
# Base URL of the backend API
|
||||
# Default: http://localhost:8916
|
||||
# Environment: FRONTEND_API_BACKEND_URL or BACKEND_URL
|
||||
backend_url: "http://localhost:8916"
|
||||
|
||||
# Note: backend_url is dynamically injected by the launcher script
|
||||
# based on network.yaml settings to ensure SSOT integrity.
|
||||
|
||||
# API timeout in milliseconds
|
||||
# Default: 30000 (30 seconds)
|
||||
# Environment: FRONTEND_API_TIMEOUT_MS
|
||||
|
||||
@@ -38,36 +38,18 @@ ssl:
|
||||
certificate_path: ""
|
||||
|
||||
# Path to SSL private key
|
||||
# Environment: NETWORK_SSL_KEY_PATH
|
||||
# Path to SSL private key
|
||||
key_path: ""
|
||||
|
||||
# --- Proxy (Caddy) Configuration ---
|
||||
proxy:
|
||||
# Caddy log level (debug|info|warn|error)
|
||||
# Default: info
|
||||
# Environment: NETWORK_PROXY_CADDY_LOG_LEVEL
|
||||
caddy_log_level: "info"
|
||||
# --- Master Infrastructure Settings ---
|
||||
# Establish this file as the SSOT for network topology.
|
||||
application:
|
||||
# The IP address or hostname of the server (Used for CORS and Frontend API)
|
||||
# Default: localhost
|
||||
# Environment: SERVER_IP
|
||||
server_ip: "localhost"
|
||||
|
||||
# Maximum timeout for reading requests in seconds
|
||||
# Default: 60
|
||||
# Environment: NETWORK_PROXY_READ_TIMEOUT_S
|
||||
proxy_read_timeout_s: 60
|
||||
# Comma-separated list of extra allowed CORS origins (IPs/FQDNs/Subnets)
|
||||
# Environment: EXTRA_ALLOWED_ORIGINS
|
||||
cors_origins: ""
|
||||
|
||||
# Maximum request body size in MB
|
||||
# Default: 10
|
||||
# Environment: NETWORK_PROXY_MAX_REQUEST_SIZE_MB
|
||||
max_request_size_mb: 10
|
||||
|
||||
# --- CORS Policies ---
|
||||
cors:
|
||||
# Comma-separated list of allowed origins
|
||||
# Environment: NETWORK_CORS_ALLOWED_ORIGINS or EXTRA_ALLOWED_ORIGINS
|
||||
allowed_origins: "*"
|
||||
|
||||
# Allowed HTTP methods
|
||||
# Environment: NETWORK_CORS_ALLOWED_METHODS
|
||||
allowed_methods: "GET,POST,PUT,DELETE,OPTIONS"
|
||||
|
||||
# Allowed HTTP headers
|
||||
# Environment: NETWORK_CORS_ALLOWED_HEADERS
|
||||
allowed_headers: "Authorization,Content-Type,Accept"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": "1.14.9",
|
||||
"last_build": "2026-04-23-1258",
|
||||
"version": "1.14.12",
|
||||
"last_build": "2026-04-23-1445",
|
||||
"codename": "ConfigCore",
|
||||
"commit": "33805cb7"
|
||||
"commit": "c14a28b0"
|
||||
}
|
||||
@@ -210,12 +210,25 @@ def main():
|
||||
|
||||
# Prepare environment variables for Docker Compose
|
||||
# We map YAML values to the environment variables expected by docker-compose.yml
|
||||
server_ip = network_cfg.get("application", {}).get("server_ip", "localhost")
|
||||
ssl_enabled = network_cfg.get("ssl", {}).get("ssl_enabled", False)
|
||||
|
||||
# Dynamically construct API URL from master settings
|
||||
if ssl_enabled:
|
||||
api_url = f"https://{server_ip}:{ports_to_check['Backend HTTPS']}"
|
||||
else:
|
||||
api_url = f"http://{server_ip}:{ports_to_check['Backend HTTP']}"
|
||||
|
||||
logger.info(f"Dynamically injecting API URL into Docker: {api_url}")
|
||||
|
||||
env = os.environ.copy()
|
||||
env["BACKEND_PORT"] = str(ports_to_check["Backend HTTP"])
|
||||
env["FRONTEND_PORT"] = str(ports_to_check["Frontend HTTP"])
|
||||
env["BACKEND_SSL_PORT"] = str(ports_to_check["Backend HTTPS"])
|
||||
env["FRONTEND_SSL_PORT"] = str(ports_to_check["Frontend HTTPS"])
|
||||
env["JWT_SECRET_KEY"] = jwt_secret or "change_me_in_production"
|
||||
env["NEXT_PUBLIC_API_URL"] = api_url
|
||||
env["SERVER_IP"] = server_ip
|
||||
|
||||
# 8. Docker Compose Build
|
||||
logger.info("Step 8/12: Building/Pulling Docker images...")
|
||||
|
||||
@@ -64,6 +64,37 @@ def load_yaml(file_path: str) -> Dict[str, Any]:
|
||||
logger.error(f"Failed to parse {file_path}: {e}")
|
||||
return {}
|
||||
|
||||
# PID file for tracking processes
|
||||
PID_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".standalone.pid")
|
||||
|
||||
def get_running_pids() -> List[int]:
|
||||
if not os.path.exists(PID_FILE):
|
||||
return []
|
||||
try:
|
||||
with open(PID_FILE, 'r') as f:
|
||||
return [int(line.strip()) for line in f if line.strip()]
|
||||
except:
|
||||
return []
|
||||
|
||||
def save_pids(pids: List[int]):
|
||||
with open(PID_FILE, 'w') as f:
|
||||
for pid in pids:
|
||||
f.write(f"{pid}\n")
|
||||
|
||||
def clear_pids():
|
||||
if os.path.exists(PID_FILE):
|
||||
try:
|
||||
os.remove(PID_FILE)
|
||||
except:
|
||||
pass
|
||||
|
||||
def is_pid_running(pid: int) -> bool:
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def is_port_in_use(port: int) -> bool:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
return s.connect_ex(('localhost', port)) == 0
|
||||
@@ -76,7 +107,7 @@ def log_stream(stream, prefix, color):
|
||||
print(f"{color}[{prefix}]{Colors.NC} {line.strip()}")
|
||||
stream.close()
|
||||
|
||||
def start_backend(backend_port: int, backend_cfg: Dict[str, Any]):
|
||||
def start_backend(backend_port: int, backend_cfg: Dict[str, Any], background: bool = False):
|
||||
logger.info(f"Starting Backend on port {backend_port}...")
|
||||
|
||||
app_cfg = backend_cfg.get("application", {})
|
||||
@@ -96,73 +127,231 @@ def start_backend(backend_port: int, backend_cfg: Dict[str, Any]):
|
||||
"--port", str(backend_port)
|
||||
]
|
||||
|
||||
# Add --reload if in dev mode (detectable via environment or flag,
|
||||
# but the script is for 'run_standalone' which might be used for prod too)
|
||||
# For now, let's keep it simple.
|
||||
|
||||
log_file = None
|
||||
if background:
|
||||
log_path = os.path.join(env["LOGS_DIR"], "backend_standalone.log")
|
||||
log_file = open(log_path, "a")
|
||||
logger.info(f"Backend logs redirected to {log_path}")
|
||||
|
||||
try:
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=log_file if background else subprocess.PIPE,
|
||||
stderr=log_file if background else subprocess.PIPE,
|
||||
text=True,
|
||||
env=env,
|
||||
bufsize=1,
|
||||
universal_newlines=True
|
||||
universal_newlines=True,
|
||||
start_new_session=background
|
||||
)
|
||||
processes.append(p)
|
||||
|
||||
threading.Thread(target=log_stream, args=(p.stdout, "BACKEND", Colors.GREEN), daemon=True).start()
|
||||
threading.Thread(target=log_stream, args=(p.stderr, "BACKEND", Colors.GREEN), daemon=True).start()
|
||||
if not background:
|
||||
threading.Thread(target=log_stream, args=(p.stdout, "BACKEND", Colors.GREEN), daemon=True).start()
|
||||
threading.Thread(target=log_stream, args=(p.stderr, "BACKEND", Colors.GREEN), daemon=True).start()
|
||||
|
||||
return p
|
||||
except FileNotFoundError:
|
||||
logger.error("uvicorn not found. Please install it with: pip install uvicorn")
|
||||
sys.exit(1)
|
||||
|
||||
def start_frontend(frontend_port: int, frontend_cfg: Dict[str, Any]):
|
||||
def start_frontend(frontend_port: int, frontend_cfg: Dict[str, Any], network_cfg: Dict[str, Any], background: bool = False):
|
||||
logger.info(f"Starting Frontend on port {frontend_port}...")
|
||||
|
||||
api_cfg = frontend_cfg.get("api", {})
|
||||
env = os.environ.copy()
|
||||
env["NEXT_PUBLIC_API_URL"] = api_cfg.get("backend_url", "http://localhost:8916")
|
||||
env["PORT"] = str(frontend_port)
|
||||
server_ip = network_cfg.get("application", {}).get("server_ip", "localhost")
|
||||
ssl_enabled = network_cfg.get("ssl", {}).get("ssl_enabled", False)
|
||||
ports = network_cfg.get("ports", {})
|
||||
|
||||
# Check if we should use 'npm run dev' or 'node server.js'
|
||||
# For standalone, we might prefer 'npm run start' if built, or 'npm run dev'
|
||||
# Bash script used 'npm run start'
|
||||
# Dynamically construct API URL from master settings
|
||||
if ssl_enabled:
|
||||
back_ssl_port = ports.get("backend_ssl_port", 8918)
|
||||
api_url = f"https://{server_ip}:{back_ssl_port}"
|
||||
else:
|
||||
back_port = ports.get("backend_port", 8916)
|
||||
api_url = f"http://{server_ip}:{back_port}"
|
||||
|
||||
logger.info(f"Dynamically injecting API URL: {api_url}")
|
||||
|
||||
env = os.environ.copy()
|
||||
env["NEXT_PUBLIC_API_URL"] = api_url
|
||||
env["PORT"] = str(frontend_port)
|
||||
|
||||
cmd = ["npm", "run", "dev"]
|
||||
if os.path.exists("frontend/server.js"):
|
||||
# If built for production
|
||||
cmd = ["node", "frontend/server.js"]
|
||||
|
||||
log_file = None
|
||||
if background:
|
||||
logs_dir = os.environ.get("LOGS_DIR", "./logs")
|
||||
log_path = os.path.join(logs_dir, "frontend_standalone.log")
|
||||
log_file = open(log_path, "a")
|
||||
logger.info(f"Frontend logs redirected to {log_path}")
|
||||
|
||||
try:
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
cwd="frontend" if os.path.isdir("frontend") else None,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=log_file if background else subprocess.PIPE,
|
||||
stderr=log_file if background else subprocess.PIPE,
|
||||
text=True,
|
||||
env=env,
|
||||
bufsize=1,
|
||||
universal_newlines=True
|
||||
universal_newlines=True,
|
||||
start_new_session=background
|
||||
)
|
||||
processes.append(p)
|
||||
|
||||
threading.Thread(target=log_stream, args=(p.stdout, "FRONTEND", Colors.MAGENTA), daemon=True).start()
|
||||
threading.Thread(target=log_stream, args=(p.stderr, "FRONTEND", Colors.MAGENTA), daemon=True).start()
|
||||
if not background:
|
||||
threading.Thread(target=log_stream, args=(p.stdout, "FRONTEND", Colors.MAGENTA), daemon=True).start()
|
||||
threading.Thread(target=log_stream, args=(p.stderr, "FRONTEND", Colors.MAGENTA), daemon=True).start()
|
||||
|
||||
return p
|
||||
except FileNotFoundError:
|
||||
logger.error("npm or node not found. Please install Node.js")
|
||||
sys.exit(1)
|
||||
|
||||
def start_proxy(network_cfg: Dict[str, Any], background: bool = False):
|
||||
ssl_cfg = network_cfg.get("ssl", {})
|
||||
if not ssl_cfg.get("ssl_enabled", False):
|
||||
return None
|
||||
|
||||
caddyfile = "Caddyfile.standalone"
|
||||
if not os.path.exists(caddyfile):
|
||||
logger.warning(f"{caddyfile} not found. SSL proxy will not start.")
|
||||
return None
|
||||
|
||||
logger.info("Starting SSL Proxy (Caddy)...")
|
||||
|
||||
# Check if caddy is installed
|
||||
try:
|
||||
subprocess.run(["caddy", "version"], capture_output=True, check=True)
|
||||
except:
|
||||
logger.error("Caddy not found. Please install Caddy to use SSL proxy.")
|
||||
return None
|
||||
|
||||
cmd = ["caddy", "run", "--config", caddyfile, "--adapter", "caddyfile"]
|
||||
|
||||
log_file = None
|
||||
if background:
|
||||
logs_dir = os.environ.get("LOGS_DIR", "./logs")
|
||||
log_path = os.path.join(logs_dir, "proxy_standalone.log")
|
||||
log_file = open(log_path, "a")
|
||||
logger.info(f"Proxy logs redirected to {log_path}")
|
||||
|
||||
try:
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=log_file if background else subprocess.PIPE,
|
||||
stderr=log_file if background else subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
universal_newlines=True,
|
||||
start_new_session=background
|
||||
)
|
||||
processes.append(p)
|
||||
|
||||
if not background:
|
||||
threading.Thread(target=log_stream, args=(p.stdout, "PROXY", Colors.CYAN), daemon=True).start()
|
||||
threading.Thread(target=log_stream, args=(p.stderr, "PROXY", Colors.CYAN), daemon=True).start()
|
||||
|
||||
return p
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start Caddy: {e}")
|
||||
return None
|
||||
|
||||
def action_status(backend_port: int, frontend_port: int, network_cfg: Dict[str, Any]):
|
||||
logger.info("Checking status...")
|
||||
backend_up = is_port_in_use(backend_port)
|
||||
frontend_up = is_port_in_use(frontend_port)
|
||||
|
||||
ssl_cfg = network_cfg.get("ssl", {})
|
||||
ssl_enabled = ssl_cfg.get("ssl_enabled", False)
|
||||
proxy_up = False
|
||||
if ssl_enabled:
|
||||
proxy_up = is_port_in_use(network_cfg.get("ports", {}).get("frontend_ssl_port", 8919))
|
||||
|
||||
pids = get_running_pids()
|
||||
running_pids = [pid for pid in pids if is_pid_running(pid)]
|
||||
|
||||
if backend_up or frontend_up or running_pids:
|
||||
print(f"{Colors.GREEN}● aInventory Standalone is RUNNING{Colors.NC}")
|
||||
print(f" - Backend (Port {backend_port}): {'UP' if backend_up else 'DOWN'}")
|
||||
print(f" - Frontend (Port {frontend_port}): {'UP' if frontend_up else 'DOWN'}")
|
||||
if ssl_enabled:
|
||||
print(f" - SSL Proxy: {'UP' if proxy_up else 'DOWN'}")
|
||||
if running_pids:
|
||||
print(f" - Active PIDs: {', '.join(map(str, running_pids))}")
|
||||
return True
|
||||
else:
|
||||
print(f"{Colors.RED}○ aInventory Standalone is STOPPED{Colors.NC}")
|
||||
return False
|
||||
|
||||
def action_stop(backend_port: int, frontend_port: int, network_cfg: Dict[str, Any]):
|
||||
pids = get_running_pids()
|
||||
|
||||
if pids:
|
||||
logger.info(f"Stopping processes with PIDs: {', '.join(map(str, pids))}...")
|
||||
for pid in pids:
|
||||
if is_pid_running(pid):
|
||||
try:
|
||||
# Kill the whole process group if possible
|
||||
os.killpg(os.getpgid(pid), signal.SIGTERM)
|
||||
except:
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
for pid in pids:
|
||||
if is_pid_running(pid):
|
||||
try:
|
||||
os.killpg(os.getpgid(pid), signal.SIGKILL)
|
||||
except:
|
||||
try:
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
clear_pids()
|
||||
|
||||
# Port-based cleanup (handles orphaned children like npm -> node)
|
||||
cleanup_ports = [backend_port, frontend_port]
|
||||
ports_cfg = network_cfg.get("ports", {})
|
||||
if network_cfg.get("ssl", {}).get("ssl_enabled"):
|
||||
cleanup_ports.append(ports_cfg.get("backend_ssl_port", 8918))
|
||||
cleanup_ports.append(ports_cfg.get("frontend_ssl_port", 8919))
|
||||
|
||||
for port in cleanup_ports:
|
||||
if is_port_in_use(port):
|
||||
logger.info(f"Cleaning up orphaned process on port {port}...")
|
||||
try:
|
||||
# Use fuser-like logic to find the PID on that port
|
||||
# Note: This is Linux specific
|
||||
cmd = ["lsof", "-t", f"-i:{port}"]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
orphans = [int(p) for p in result.stdout.splitlines() if p.strip()]
|
||||
for o_pid in orphans:
|
||||
if o_pid != os.getpid():
|
||||
logger.info(f" - Killing orphan PID {o_pid}")
|
||||
try:
|
||||
os.kill(o_pid, signal.SIGKILL)
|
||||
except OSError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not perform port-based cleanup: {e}")
|
||||
|
||||
logger.info("Stopped.")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="TFM aInventory Standalone Launcher")
|
||||
parser.add_argument("action", nargs="?", choices=["start", "stop", "restart", "status", "run"], default="run",
|
||||
help="Action to perform: run (foreground), start (background), stop, restart, status")
|
||||
parser.add_argument("--no-proxy", action="store_true", help="Disable SSL proxy even if enabled in config")
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument("--backend-only", action="store_true", help="Start only the backend")
|
||||
group.add_argument("--frontend-only", action="store_true", help="Start only the frontend")
|
||||
group.add_argument("--backend-only", action="store_true", help="Operation on backend only")
|
||||
group.add_argument("--frontend-only", action="store_true", help="Operation on frontend only")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -174,43 +363,71 @@ def main():
|
||||
ports_cfg = network_cfg.get("ports", {})
|
||||
backend_port = ports_cfg.get("backend_port", 8916)
|
||||
frontend_port = ports_cfg.get("frontend_port", 8917)
|
||||
|
||||
if not args.frontend_only:
|
||||
if is_port_in_use(backend_port):
|
||||
logger.error(f"Port {backend_port} is already in use. Cannot start backend.")
|
||||
if not args.backend_only:
|
||||
# If we are supposed to start both, we fail.
|
||||
sys.exit(1)
|
||||
else:
|
||||
start_backend(backend_port, backend_cfg)
|
||||
|
||||
if not args.backend_only:
|
||||
if is_port_in_use(frontend_port):
|
||||
logger.error(f"Port {frontend_port} is already in use. Cannot start frontend.")
|
||||
if not args.frontend_only:
|
||||
sys.exit(1)
|
||||
else:
|
||||
start_frontend(frontend_port, frontend_cfg)
|
||||
|
||||
if not processes:
|
||||
logger.error("No processes started. Check configuration and port availability.")
|
||||
sys.exit(1)
|
||||
|
||||
if args.action == "status":
|
||||
action_status(backend_port, frontend_port, network_cfg)
|
||||
return
|
||||
|
||||
if args.action == "stop":
|
||||
action_stop(backend_port, frontend_port, network_cfg)
|
||||
return
|
||||
|
||||
if args.action == "restart":
|
||||
action_stop(backend_port, frontend_port, network_cfg)
|
||||
# Fall through to start/run logic
|
||||
args.action = "start" if args.action == "restart" else args.action
|
||||
|
||||
if args.action in ["start", "run"]:
|
||||
background = (args.action == "start")
|
||||
|
||||
logger.info("All services started. Press Ctrl+C to stop.")
|
||||
|
||||
# Monitor processes
|
||||
try:
|
||||
while not shutdown_event.is_set():
|
||||
time.sleep(1)
|
||||
for p in processes:
|
||||
if p.poll() is not None:
|
||||
logger.error(f"Process {p.pid} exited with code {p.returncode}")
|
||||
shutdown_event.set()
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
signal_handler(None, None)
|
||||
if not args.frontend_only:
|
||||
if is_port_in_use(backend_port):
|
||||
logger.error(f"Port {backend_port} is already in use. Backend cannot start.")
|
||||
if not args.backend_only and args.action == "run":
|
||||
sys.exit(1)
|
||||
else:
|
||||
start_backend(backend_port, backend_cfg, background=background)
|
||||
|
||||
if not args.backend_only:
|
||||
if is_port_in_use(frontend_port):
|
||||
logger.error(f"Port {frontend_port} is already in use. Frontend cannot start.")
|
||||
if not args.frontend_only and args.action == "run":
|
||||
sys.exit(1)
|
||||
else:
|
||||
start_frontend(frontend_port, frontend_cfg, network_cfg, background=background)
|
||||
|
||||
# Start SSL Proxy if enabled
|
||||
if not args.no_proxy and not args.backend_only and not args.frontend_only:
|
||||
start_proxy(network_cfg, background=background)
|
||||
|
||||
if not processes:
|
||||
logger.error("No processes started. Check configuration and port availability.")
|
||||
sys.exit(1)
|
||||
|
||||
# Save PIDs
|
||||
save_pids([p.pid for p in processes])
|
||||
|
||||
if background:
|
||||
logger.info(f"All services started in BACKGROUND. PIDs: {', '.join(str(p.pid) for p in processes)}")
|
||||
logger.info(f"Use 'python3 {sys.argv[0]} status' to check state.")
|
||||
return
|
||||
|
||||
logger.info("All services started in FOREGROUND. Press Ctrl+C to stop.")
|
||||
|
||||
# Monitor processes
|
||||
try:
|
||||
while not shutdown_event.is_set():
|
||||
time.sleep(1)
|
||||
for p in processes:
|
||||
if p.poll() is not None:
|
||||
logger.error(f"Process {p.pid} exited with code {p.returncode}")
|
||||
shutdown_event.set()
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
signal_handler(None, None)
|
||||
clear_pids()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -39,10 +39,10 @@ deactivate nondestructive
|
||||
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
|
||||
# transform D:\path\to\venv to /d/path/to/venv on MSYS
|
||||
# and to /cygdrive/d/path/to/venv on Cygwin
|
||||
export VIRTUAL_ENV=$(cygpath /tmp/image-system-phase1/venv)
|
||||
export VIRTUAL_ENV=$(cygpath /data/programare_AI/tfm_ainventory/venv)
|
||||
else
|
||||
# use the path as-is
|
||||
export VIRTUAL_ENV=/tmp/image-system-phase1/venv
|
||||
export VIRTUAL_ENV=/data/programare_AI/tfm_ainventory/venv
|
||||
fi
|
||||
|
||||
_OLD_VIRTUAL_PATH="$PATH"
|
||||
|
||||
@@ -9,7 +9,7 @@ alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PA
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
setenv VIRTUAL_ENV /tmp/image-system-phase1/venv
|
||||
setenv VIRTUAL_ENV /data/programare_AI/tfm_ainventory/venv
|
||||
|
||||
set _OLD_VIRTUAL_PATH="$PATH"
|
||||
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
|
||||
|
||||
@@ -33,7 +33,7 @@ end
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
set -gx VIRTUAL_ENV /tmp/image-system-phase1/venv
|
||||
set -gx VIRTUAL_ENV /data/programare_AI/tfm_ainventory/venv
|
||||
|
||||
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from coverage.cmdline import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from coverage.cmdline import main_deprecated
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main_deprecated())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from coverage.cmdline import main_deprecated
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main_deprecated())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from distro.distro import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from dotenv.__main__ import cli
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(cli())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from numpy.f2py.f2py2e import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from fastapi.cli import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from httpx import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from charset_normalizer.cli import cli_detect
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(cli_detect())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from numpy._configtool import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from pytest import console_main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(console_main())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from pygments.cmdline import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import decrypt
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(decrypt())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import encrypt
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(encrypt())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import keygen
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(keygen())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from rsa.util import private_to_public
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(private_to_public())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import sign
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(sign())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import verify
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(verify())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from pytest import console_main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(console_main())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from uvicorn.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from watchfiles.cli import cli
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(cli())
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/tmp/image-system-phase1/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
#!/data/programare_AI/tfm_ainventory/venv/bin/python3
|
||||
import sys
|
||||
from websockets.cli import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
A. HISTORY OF THE SOFTWARE
|
||||
==========================
|
||||
|
||||
Python was created in the early 1990s by Guido van Rossum at Stichting
|
||||
Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
|
||||
as a successor of a language called ABC. Guido remains Python's
|
||||
principal author, although it includes many contributions from others.
|
||||
|
||||
In 1995, Guido continued his work on Python at the Corporation for
|
||||
National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
|
||||
in Reston, Virginia where he released several versions of the
|
||||
software.
|
||||
|
||||
In May 2000, Guido and the Python core development team moved to
|
||||
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
|
||||
year, the PythonLabs team moved to Digital Creations, which became
|
||||
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
|
||||
https://www.python.org/psf/) was formed, a non-profit organization
|
||||
created specifically to own Python-related Intellectual Property.
|
||||
Zope Corporation was a sponsoring member of the PSF.
|
||||
|
||||
All Python releases are Open Source (see https://opensource.org for
|
||||
the Open Source Definition). Historically, most, but not all, Python
|
||||
releases have also been GPL-compatible; the table below summarizes
|
||||
the various releases.
|
||||
|
||||
Release Derived Year Owner GPL-
|
||||
from compatible? (1)
|
||||
|
||||
0.9.0 thru 1.2 1991-1995 CWI yes
|
||||
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
|
||||
1.6 1.5.2 2000 CNRI no
|
||||
2.0 1.6 2000 BeOpen.com no
|
||||
1.6.1 1.6 2001 CNRI yes (2)
|
||||
2.1 2.0+1.6.1 2001 PSF no
|
||||
2.0.1 2.0+1.6.1 2001 PSF yes
|
||||
2.1.1 2.1+2.0.1 2001 PSF yes
|
||||
2.1.2 2.1.1 2002 PSF yes
|
||||
2.1.3 2.1.2 2002 PSF yes
|
||||
2.2 and above 2.1.1 2001-now PSF yes
|
||||
|
||||
Footnotes:
|
||||
|
||||
(1) GPL-compatible doesn't mean that we're distributing Python under
|
||||
the GPL. All Python licenses, unlike the GPL, let you distribute
|
||||
a modified version without making your changes open source. The
|
||||
GPL-compatible licenses make it possible to combine Python with
|
||||
other software that is released under the GPL; the others don't.
|
||||
|
||||
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
|
||||
because its license has a choice of law clause. According to
|
||||
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
|
||||
is "not incompatible" with the GPL.
|
||||
|
||||
Thanks to the many outside volunteers who have worked under Guido's
|
||||
direction to make these releases possible.
|
||||
|
||||
|
||||
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
|
||||
===============================================================
|
||||
|
||||
Python software and documentation are licensed under the
|
||||
Python Software Foundation License Version 2.
|
||||
|
||||
Starting with Python 3.8.6, examples, recipes, and other code in
|
||||
the documentation are dual licensed under the PSF License Version 2
|
||||
and the Zero-Clause BSD license.
|
||||
|
||||
Some software incorporated into Python is under different licenses.
|
||||
The licenses are listed with code falling under that license.
|
||||
|
||||
|
||||
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||
--------------------------------------------
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Python Software Foundation
|
||||
("PSF"), and the Individual or Organization ("Licensee") accessing and
|
||||
otherwise using this software ("Python") in source or binary form and
|
||||
its associated documentation.
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, PSF hereby
|
||||
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
|
||||
analyze, test, perform and/or display publicly, prepare derivative works,
|
||||
distribute, and otherwise use Python alone or in any derivative version,
|
||||
provided, however, that PSF's License Agreement and PSF's notice of copyright,
|
||||
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
|
||||
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation;
|
||||
All Rights Reserved" are retained in Python alone or in any derivative version
|
||||
prepared by Licensee.
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on
|
||||
or incorporates Python or any part thereof, and wants to make
|
||||
the derivative work available to others as provided herein, then
|
||||
Licensee hereby agrees to include in any such work a brief summary of
|
||||
the changes made to Python.
|
||||
|
||||
4. PSF is making Python available to Licensee on an "AS IS"
|
||||
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
|
||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
7. Nothing in this License Agreement shall be deemed to create any
|
||||
relationship of agency, partnership, or joint venture between PSF and
|
||||
Licensee. This License Agreement does not grant permission to use PSF
|
||||
trademarks or trade name in a trademark sense to endorse or promote
|
||||
products or services of Licensee, or any third party.
|
||||
|
||||
8. By copying, installing or otherwise using Python, Licensee
|
||||
agrees to be bound by the terms and conditions of this License
|
||||
Agreement.
|
||||
|
||||
|
||||
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
|
||||
-------------------------------------------
|
||||
|
||||
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
|
||||
|
||||
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
|
||||
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
|
||||
Individual or Organization ("Licensee") accessing and otherwise using
|
||||
this software in source or binary form and its associated
|
||||
documentation ("the Software").
|
||||
|
||||
2. Subject to the terms and conditions of this BeOpen Python License
|
||||
Agreement, BeOpen hereby grants Licensee a non-exclusive,
|
||||
royalty-free, world-wide license to reproduce, analyze, test, perform
|
||||
and/or display publicly, prepare derivative works, distribute, and
|
||||
otherwise use the Software alone or in any derivative version,
|
||||
provided, however, that the BeOpen Python License is retained in the
|
||||
Software, alone or in any derivative version prepared by Licensee.
|
||||
|
||||
3. BeOpen is making the Software available to Licensee on an "AS IS"
|
||||
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
|
||||
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
|
||||
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
|
||||
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
5. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
6. This License Agreement shall be governed by and interpreted in all
|
||||
respects by the law of the State of California, excluding conflict of
|
||||
law provisions. Nothing in this License Agreement shall be deemed to
|
||||
create any relationship of agency, partnership, or joint venture
|
||||
between BeOpen and Licensee. This License Agreement does not grant
|
||||
permission to use BeOpen trademarks or trade names in a trademark
|
||||
sense to endorse or promote products or services of Licensee, or any
|
||||
third party. As an exception, the "BeOpen Python" logos available at
|
||||
http://www.pythonlabs.com/logos.html may be used according to the
|
||||
permissions granted on that web page.
|
||||
|
||||
7. By copying, installing or otherwise using the software, Licensee
|
||||
agrees to be bound by the terms and conditions of this License
|
||||
Agreement.
|
||||
|
||||
|
||||
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
|
||||
---------------------------------------
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Corporation for National
|
||||
Research Initiatives, having an office at 1895 Preston White Drive,
|
||||
Reston, VA 20191 ("CNRI"), and the Individual or Organization
|
||||
("Licensee") accessing and otherwise using Python 1.6.1 software in
|
||||
source or binary form and its associated documentation.
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, CNRI
|
||||
hereby grants Licensee a nonexclusive, royalty-free, world-wide
|
||||
license to reproduce, analyze, test, perform and/or display publicly,
|
||||
prepare derivative works, distribute, and otherwise use Python 1.6.1
|
||||
alone or in any derivative version, provided, however, that CNRI's
|
||||
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
|
||||
1995-2001 Corporation for National Research Initiatives; All Rights
|
||||
Reserved" are retained in Python 1.6.1 alone or in any derivative
|
||||
version prepared by Licensee. Alternately, in lieu of CNRI's License
|
||||
Agreement, Licensee may substitute the following text (omitting the
|
||||
quotes): "Python 1.6.1 is made available subject to the terms and
|
||||
conditions in CNRI's License Agreement. This Agreement together with
|
||||
Python 1.6.1 may be located on the internet using the following
|
||||
unique, persistent identifier (known as a handle): 1895.22/1013. This
|
||||
Agreement may also be obtained from a proxy server on the internet
|
||||
using the following URL: http://hdl.handle.net/1895.22/1013".
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on
|
||||
or incorporates Python 1.6.1 or any part thereof, and wants to make
|
||||
the derivative work available to others as provided herein, then
|
||||
Licensee hereby agrees to include in any such work a brief summary of
|
||||
the changes made to Python 1.6.1.
|
||||
|
||||
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
|
||||
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
|
||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
7. This License Agreement shall be governed by the federal
|
||||
intellectual property law of the United States, including without
|
||||
limitation the federal copyright law, and, to the extent such
|
||||
U.S. federal law does not apply, by the law of the Commonwealth of
|
||||
Virginia, excluding Virginia's conflict of law provisions.
|
||||
Notwithstanding the foregoing, with regard to derivative works based
|
||||
on Python 1.6.1 that incorporate non-separable material that was
|
||||
previously distributed under the GNU General Public License (GPL), the
|
||||
law of the Commonwealth of Virginia shall govern this License
|
||||
Agreement only as to issues arising under or with respect to
|
||||
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
|
||||
License Agreement shall be deemed to create any relationship of
|
||||
agency, partnership, or joint venture between CNRI and Licensee. This
|
||||
License Agreement does not grant permission to use CNRI trademarks or
|
||||
trade name in a trademark sense to endorse or promote products or
|
||||
services of Licensee, or any third party.
|
||||
|
||||
8. By clicking on the "ACCEPT" button where indicated, or by copying,
|
||||
installing or otherwise using Python 1.6.1, Licensee agrees to be
|
||||
bound by the terms and conditions of this License Agreement.
|
||||
|
||||
ACCEPT
|
||||
|
||||
|
||||
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
|
||||
--------------------------------------------------
|
||||
|
||||
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
|
||||
The Netherlands. All rights reserved.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software and its
|
||||
documentation for any purpose and without fee is hereby granted,
|
||||
provided that the above copyright notice appear in all copies and that
|
||||
both that copyright notice and this permission notice appear in
|
||||
supporting documentation, and that the name of Stichting Mathematisch
|
||||
Centrum or CWI not be used in advertising or publicity pertaining to
|
||||
distribution of the software without specific, written prior
|
||||
permission.
|
||||
|
||||
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
|
||||
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
@@ -0,0 +1,123 @@
|
||||
Metadata-Version: 2.3
|
||||
Name: aiohappyeyeballs
|
||||
Version: 2.6.1
|
||||
Summary: Happy Eyeballs for asyncio
|
||||
License: PSF-2.0
|
||||
Author: J. Nick Koston
|
||||
Author-email: nick@koston.org
|
||||
Requires-Python: >=3.9
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Natural Language :: English
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Topic :: Software Development :: Libraries
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Programming Language :: Python :: 3.13
|
||||
Classifier: License :: OSI Approved :: Python Software Foundation License
|
||||
Project-URL: Bug Tracker, https://github.com/aio-libs/aiohappyeyeballs/issues
|
||||
Project-URL: Changelog, https://github.com/aio-libs/aiohappyeyeballs/blob/main/CHANGELOG.md
|
||||
Project-URL: Documentation, https://aiohappyeyeballs.readthedocs.io
|
||||
Project-URL: Repository, https://github.com/aio-libs/aiohappyeyeballs
|
||||
Description-Content-Type: text/markdown
|
||||
|
||||
# aiohappyeyeballs
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/aio-libs/aiohappyeyeballs/actions/workflows/ci.yml?query=branch%3Amain">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/aio-libs/aiohappyeyeballs/ci-cd.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >
|
||||
</a>
|
||||
<a href="https://aiohappyeyeballs.readthedocs.io">
|
||||
<img src="https://img.shields.io/readthedocs/aiohappyeyeballs.svg?logo=read-the-docs&logoColor=fff&style=flat-square" alt="Documentation Status">
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/aio-libs/aiohappyeyeballs">
|
||||
<img src="https://img.shields.io/codecov/c/github/aio-libs/aiohappyeyeballs.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://python-poetry.org/">
|
||||
<img src="https://img.shields.io/badge/packaging-poetry-299bd7?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAASCAYAAABrXO8xAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJJSURBVHgBfZLPa1NBEMe/s7tNXoxW1KJQKaUHkXhQvHgW6UHQQ09CBS/6V3hKc/AP8CqCrUcpmop3Cx48eDB4yEECjVQrlZb80CRN8t6OM/teagVxYZi38+Yz853dJbzoMV3MM8cJUcLMSUKIE8AzQ2PieZzFxEJOHMOgMQQ+dUgSAckNXhapU/NMhDSWLs1B24A8sO1xrN4NECkcAC9ASkiIJc6k5TRiUDPhnyMMdhKc+Zx19l6SgyeW76BEONY9exVQMzKExGKwwPsCzza7KGSSWRWEQhyEaDXp6ZHEr416ygbiKYOd7TEWvvcQIeusHYMJGhTwF9y7sGnSwaWyFAiyoxzqW0PM/RjghPxF2pWReAowTEXnDh0xgcLs8l2YQmOrj3N7ByiqEoH0cARs4u78WgAVkoEDIDoOi3AkcLOHU60RIg5wC4ZuTC7FaHKQm8Hq1fQuSOBvX/sodmNJSB5geaF5CPIkUeecdMxieoRO5jz9bheL6/tXjrwCyX/UYBUcjCaWHljx1xiX6z9xEjkYAzbGVnB8pvLmyXm9ep+W8CmsSHQQY77Zx1zboxAV0w7ybMhQmfqdmmw3nEp1I0Z+FGO6M8LZdoyZnuzzBdjISicKRnpxzI9fPb+0oYXsNdyi+d3h9bm9MWYHFtPeIZfLwzmFDKy1ai3p+PDls1Llz4yyFpferxjnyjJDSEy9CaCx5m2cJPerq6Xm34eTrZt3PqxYO1XOwDYZrFlH1fWnpU38Y9HRze3lj0vOujZcXKuuXm3jP+s3KbZVra7y2EAAAAAASUVORK5CYII=" alt="Poetry">
|
||||
</a>
|
||||
<a href="https://github.com/astral-sh/ruff">
|
||||
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff">
|
||||
</a>
|
||||
<a href="https://github.com/pre-commit/pre-commit">
|
||||
<img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square" alt="pre-commit">
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://pypi.org/project/aiohappyeyeballs/">
|
||||
<img src="https://img.shields.io/pypi/v/aiohappyeyeballs.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
|
||||
</a>
|
||||
<img src="https://img.shields.io/pypi/pyversions/aiohappyeyeballs.svg?style=flat-square&logo=python&logoColor=fff" alt="Supported Python versions">
|
||||
<img src="https://img.shields.io/pypi/l/aiohappyeyeballs.svg?style=flat-square" alt="License">
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
**Documentation**: <a href="https://aiohappyeyeballs.readthedocs.io" target="_blank">https://aiohappyeyeballs.readthedocs.io </a>
|
||||
|
||||
**Source Code**: <a href="https://github.com/aio-libs/aiohappyeyeballs" target="_blank">https://github.com/aio-libs/aiohappyeyeballs </a>
|
||||
|
||||
---
|
||||
|
||||
[Happy Eyeballs](https://en.wikipedia.org/wiki/Happy_Eyeballs)
|
||||
([RFC 8305](https://www.rfc-editor.org/rfc/rfc8305.html))
|
||||
|
||||
## Use case
|
||||
|
||||
This library exists to allow connecting with
|
||||
[Happy Eyeballs](https://en.wikipedia.org/wiki/Happy_Eyeballs)
|
||||
([RFC 8305](https://www.rfc-editor.org/rfc/rfc8305.html))
|
||||
when you
|
||||
already have a list of addrinfo and not a DNS name.
|
||||
|
||||
The stdlib version of `loop.create_connection()`
|
||||
will only work when you pass in an unresolved name which
|
||||
is not a good fit when using DNS caching or resolving
|
||||
names via another method such as `zeroconf`.
|
||||
|
||||
## Installation
|
||||
|
||||
Install this via pip (or your favourite package manager):
|
||||
|
||||
`pip install aiohappyeyeballs`
|
||||
|
||||
## License
|
||||
|
||||
[aiohappyeyeballs is licensed under the same terms as cpython itself.](https://github.com/python/cpython/blob/main/LICENSE)
|
||||
|
||||
## Example usage
|
||||
|
||||
```python
|
||||
|
||||
addr_infos = await loop.getaddrinfo("example.org", 80)
|
||||
|
||||
socket = await start_connection(addr_infos)
|
||||
socket = await start_connection(addr_infos, local_addr_infos=local_addr_infos, happy_eyeballs_delay=0.2)
|
||||
|
||||
transport, protocol = await loop.create_connection(
|
||||
MyProtocol, sock=socket, ...)
|
||||
|
||||
# Remove the first address for each family from addr_info
|
||||
pop_addr_infos_interleave(addr_info, 1)
|
||||
|
||||
# Remove all matching address from addr_info
|
||||
remove_addr_infos(addr_info, "dead::beef::")
|
||||
|
||||
# Convert a local_addr to local_addr_infos
|
||||
local_addr_infos = addr_to_addr_infos(("127.0.0.1",0))
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
This package contains code from cpython and is licensed under the same terms as cpython itself.
|
||||
|
||||
This package was created with
|
||||
[Copier](https://copier.readthedocs.io/) and the
|
||||
[browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)
|
||||
project template.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
aiohappyeyeballs-2.6.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
aiohappyeyeballs-2.6.1.dist-info/LICENSE,sha256=Oy-B_iHRgcSZxZolbI4ZaEVdZonSaaqFNzv7avQdo78,13936
|
||||
aiohappyeyeballs-2.6.1.dist-info/METADATA,sha256=NSXlhJwAfi380eEjAo7BQ4P_TVal9xi0qkyZWibMsVM,5915
|
||||
aiohappyeyeballs-2.6.1.dist-info/RECORD,,
|
||||
aiohappyeyeballs-2.6.1.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
||||
aiohappyeyeballs/__init__.py,sha256=x7kktHEtaD9quBcWDJPuLeKyjuVAI-Jj14S9B_5hcTs,361
|
||||
aiohappyeyeballs/__pycache__/__init__.cpython-312.pyc,,
|
||||
aiohappyeyeballs/__pycache__/_staggered.cpython-312.pyc,,
|
||||
aiohappyeyeballs/__pycache__/impl.cpython-312.pyc,,
|
||||
aiohappyeyeballs/__pycache__/types.cpython-312.pyc,,
|
||||
aiohappyeyeballs/__pycache__/utils.cpython-312.pyc,,
|
||||
aiohappyeyeballs/_staggered.py,sha256=edfVowFx-P-ywJjIEF3MdPtEMVODujV6CeMYr65otac,6900
|
||||
aiohappyeyeballs/impl.py,sha256=Dlcm2mTJ28ucrGnxkb_fo9CZzLAkOOBizOt7dreBbXE,9681
|
||||
aiohappyeyeballs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
aiohappyeyeballs/types.py,sha256=YZJIAnyoV4Dz0WFtlaf_OyE4EW7Xus1z7aIfNI6tDDQ,425
|
||||
aiohappyeyeballs/utils.py,sha256=on9GxIR0LhEfZu8P6Twi9hepX9zDanuZM20MWsb3xlQ,3028
|
||||
@@ -0,0 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: poetry-core 2.1.1
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
@@ -0,0 +1,14 @@
|
||||
__version__ = "2.6.1"
|
||||
|
||||
from .impl import start_connection
|
||||
from .types import AddrInfoType, SocketFactoryType
|
||||
from .utils import addr_to_addr_infos, pop_addr_infos_interleave, remove_addr_infos
|
||||
|
||||
__all__ = (
|
||||
"AddrInfoType",
|
||||
"SocketFactoryType",
|
||||
"addr_to_addr_infos",
|
||||
"pop_addr_infos_interleave",
|
||||
"remove_addr_infos",
|
||||
"start_connection",
|
||||
)
|
||||
207
venv/lib/python3.12/site-packages/aiohappyeyeballs/_staggered.py
Normal file
207
venv/lib/python3.12/site-packages/aiohappyeyeballs/_staggered.py
Normal file
@@ -0,0 +1,207 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
|
||||
# PY3.9: Import Callable from typing until we drop Python 3.9 support
|
||||
# https://github.com/python/cpython/issues/87131
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
RE_RAISE_EXCEPTIONS = (SystemExit, KeyboardInterrupt)
|
||||
|
||||
|
||||
def _set_result(wait_next: "asyncio.Future[None]") -> None:
|
||||
"""Set the result of a future if it is not already done."""
|
||||
if not wait_next.done():
|
||||
wait_next.set_result(None)
|
||||
|
||||
|
||||
async def _wait_one(
|
||||
futures: "Iterable[asyncio.Future[Any]]",
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
) -> _T:
|
||||
"""Wait for the first future to complete."""
|
||||
wait_next = loop.create_future()
|
||||
|
||||
def _on_completion(fut: "asyncio.Future[Any]") -> None:
|
||||
if not wait_next.done():
|
||||
wait_next.set_result(fut)
|
||||
|
||||
for f in futures:
|
||||
f.add_done_callback(_on_completion)
|
||||
|
||||
try:
|
||||
return await wait_next
|
||||
finally:
|
||||
for f in futures:
|
||||
f.remove_done_callback(_on_completion)
|
||||
|
||||
|
||||
async def staggered_race(
|
||||
coro_fns: Iterable[Callable[[], Awaitable[_T]]],
|
||||
delay: Optional[float],
|
||||
*,
|
||||
loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||
) -> Tuple[Optional[_T], Optional[int], List[Optional[BaseException]]]:
|
||||
"""
|
||||
Run coroutines with staggered start times and take the first to finish.
|
||||
|
||||
This method takes an iterable of coroutine functions. The first one is
|
||||
started immediately. From then on, whenever the immediately preceding one
|
||||
fails (raises an exception), or when *delay* seconds has passed, the next
|
||||
coroutine is started. This continues until one of the coroutines complete
|
||||
successfully, in which case all others are cancelled, or until all
|
||||
coroutines fail.
|
||||
|
||||
The coroutines provided should be well-behaved in the following way:
|
||||
|
||||
* They should only ``return`` if completed successfully.
|
||||
|
||||
* They should always raise an exception if they did not complete
|
||||
successfully. In particular, if they handle cancellation, they should
|
||||
probably reraise, like this::
|
||||
|
||||
try:
|
||||
# do work
|
||||
except asyncio.CancelledError:
|
||||
# undo partially completed work
|
||||
raise
|
||||
|
||||
Args:
|
||||
----
|
||||
coro_fns: an iterable of coroutine functions, i.e. callables that
|
||||
return a coroutine object when called. Use ``functools.partial`` or
|
||||
lambdas to pass arguments.
|
||||
|
||||
delay: amount of time, in seconds, between starting coroutines. If
|
||||
``None``, the coroutines will run sequentially.
|
||||
|
||||
loop: the event loop to use. If ``None``, the running loop is used.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
tuple *(winner_result, winner_index, exceptions)* where
|
||||
|
||||
- *winner_result*: the result of the winning coroutine, or ``None``
|
||||
if no coroutines won.
|
||||
|
||||
- *winner_index*: the index of the winning coroutine in
|
||||
``coro_fns``, or ``None`` if no coroutines won. If the winning
|
||||
coroutine may return None on success, *winner_index* can be used
|
||||
to definitively determine whether any coroutine won.
|
||||
|
||||
- *exceptions*: list of exceptions returned by the coroutines.
|
||||
``len(exceptions)`` is equal to the number of coroutines actually
|
||||
started, and the order is the same as in ``coro_fns``. The winning
|
||||
coroutine's entry is ``None``.
|
||||
|
||||
"""
|
||||
loop = loop or asyncio.get_running_loop()
|
||||
exceptions: List[Optional[BaseException]] = []
|
||||
tasks: Set[asyncio.Task[Optional[Tuple[_T, int]]]] = set()
|
||||
|
||||
async def run_one_coro(
|
||||
coro_fn: Callable[[], Awaitable[_T]],
|
||||
this_index: int,
|
||||
start_next: "asyncio.Future[None]",
|
||||
) -> Optional[Tuple[_T, int]]:
|
||||
"""
|
||||
Run a single coroutine.
|
||||
|
||||
If the coroutine fails, set the exception in the exceptions list and
|
||||
start the next coroutine by setting the result of the start_next.
|
||||
|
||||
If the coroutine succeeds, return the result and the index of the
|
||||
coroutine in the coro_fns list.
|
||||
|
||||
If SystemExit or KeyboardInterrupt is raised, re-raise it.
|
||||
"""
|
||||
try:
|
||||
result = await coro_fn()
|
||||
except RE_RAISE_EXCEPTIONS:
|
||||
raise
|
||||
except BaseException as e:
|
||||
exceptions[this_index] = e
|
||||
_set_result(start_next) # Kickstart the next coroutine
|
||||
return None
|
||||
|
||||
return result, this_index
|
||||
|
||||
start_next_timer: Optional[asyncio.TimerHandle] = None
|
||||
start_next: Optional[asyncio.Future[None]]
|
||||
task: asyncio.Task[Optional[Tuple[_T, int]]]
|
||||
done: Union[asyncio.Future[None], asyncio.Task[Optional[Tuple[_T, int]]]]
|
||||
coro_iter = iter(coro_fns)
|
||||
this_index = -1
|
||||
try:
|
||||
while True:
|
||||
if coro_fn := next(coro_iter, None):
|
||||
this_index += 1
|
||||
exceptions.append(None)
|
||||
start_next = loop.create_future()
|
||||
task = loop.create_task(run_one_coro(coro_fn, this_index, start_next))
|
||||
tasks.add(task)
|
||||
start_next_timer = (
|
||||
loop.call_later(delay, _set_result, start_next) if delay else None
|
||||
)
|
||||
elif not tasks:
|
||||
# We exhausted the coro_fns list and no tasks are running
|
||||
# so we have no winner and all coroutines failed.
|
||||
break
|
||||
|
||||
while tasks or start_next:
|
||||
done = await _wait_one(
|
||||
(*tasks, start_next) if start_next else tasks, loop
|
||||
)
|
||||
if done is start_next:
|
||||
# The current task has failed or the timer has expired
|
||||
# so we need to start the next task.
|
||||
start_next = None
|
||||
if start_next_timer:
|
||||
start_next_timer.cancel()
|
||||
start_next_timer = None
|
||||
|
||||
# Break out of the task waiting loop to start the next
|
||||
# task.
|
||||
break
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(done, asyncio.Task)
|
||||
|
||||
tasks.remove(done)
|
||||
if winner := done.result():
|
||||
return *winner, exceptions
|
||||
finally:
|
||||
# We either have:
|
||||
# - a winner
|
||||
# - all tasks failed
|
||||
# - a KeyboardInterrupt or SystemExit.
|
||||
|
||||
#
|
||||
# If the timer is still running, cancel it.
|
||||
#
|
||||
if start_next_timer:
|
||||
start_next_timer.cancel()
|
||||
|
||||
#
|
||||
# If there are any tasks left, cancel them and than
|
||||
# wait them so they fill the exceptions list.
|
||||
#
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
return None, None, exceptions
|
||||
259
venv/lib/python3.12/site-packages/aiohappyeyeballs/impl.py
Normal file
259
venv/lib/python3.12/site-packages/aiohappyeyeballs/impl.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""Base implementation."""
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
import contextlib
|
||||
import functools
|
||||
import itertools
|
||||
import socket
|
||||
from typing import List, Optional, Sequence, Set, Union
|
||||
|
||||
from . import _staggered
|
||||
from .types import AddrInfoType, SocketFactoryType
|
||||
|
||||
|
||||
async def start_connection(
|
||||
addr_infos: Sequence[AddrInfoType],
|
||||
*,
|
||||
local_addr_infos: Optional[Sequence[AddrInfoType]] = None,
|
||||
happy_eyeballs_delay: Optional[float] = None,
|
||||
interleave: Optional[int] = None,
|
||||
loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||
socket_factory: Optional[SocketFactoryType] = None,
|
||||
) -> socket.socket:
|
||||
"""
|
||||
Connect to a TCP server.
|
||||
|
||||
Create a socket connection to a specified destination. The
|
||||
destination is specified as a list of AddrInfoType tuples as
|
||||
returned from getaddrinfo().
|
||||
|
||||
The arguments are, in order:
|
||||
|
||||
* ``family``: the address family, e.g. ``socket.AF_INET`` or
|
||||
``socket.AF_INET6``.
|
||||
* ``type``: the socket type, e.g. ``socket.SOCK_STREAM`` or
|
||||
``socket.SOCK_DGRAM``.
|
||||
* ``proto``: the protocol, e.g. ``socket.IPPROTO_TCP`` or
|
||||
``socket.IPPROTO_UDP``.
|
||||
* ``canonname``: the canonical name of the address, e.g.
|
||||
``"www.python.org"``.
|
||||
* ``sockaddr``: the socket address
|
||||
|
||||
This method is a coroutine which will try to establish the connection
|
||||
in the background. When successful, the coroutine returns a
|
||||
socket.
|
||||
|
||||
The expected use case is to use this method in conjunction with
|
||||
loop.create_connection() to establish a connection to a server::
|
||||
|
||||
socket = await start_connection(addr_infos)
|
||||
transport, protocol = await loop.create_connection(
|
||||
MyProtocol, sock=socket, ...)
|
||||
"""
|
||||
if not (current_loop := loop):
|
||||
current_loop = asyncio.get_running_loop()
|
||||
|
||||
single_addr_info = len(addr_infos) == 1
|
||||
|
||||
if happy_eyeballs_delay is not None and interleave is None:
|
||||
# If using happy eyeballs, default to interleave addresses by family
|
||||
interleave = 1
|
||||
|
||||
if interleave and not single_addr_info:
|
||||
addr_infos = _interleave_addrinfos(addr_infos, interleave)
|
||||
|
||||
sock: Optional[socket.socket] = None
|
||||
# uvloop can raise RuntimeError instead of OSError
|
||||
exceptions: List[List[Union[OSError, RuntimeError]]] = []
|
||||
if happy_eyeballs_delay is None or single_addr_info:
|
||||
# not using happy eyeballs
|
||||
for addrinfo in addr_infos:
|
||||
try:
|
||||
sock = await _connect_sock(
|
||||
current_loop,
|
||||
exceptions,
|
||||
addrinfo,
|
||||
local_addr_infos,
|
||||
None,
|
||||
socket_factory,
|
||||
)
|
||||
break
|
||||
except (RuntimeError, OSError):
|
||||
continue
|
||||
else: # using happy eyeballs
|
||||
open_sockets: Set[socket.socket] = set()
|
||||
try:
|
||||
sock, _, _ = await _staggered.staggered_race(
|
||||
(
|
||||
functools.partial(
|
||||
_connect_sock,
|
||||
current_loop,
|
||||
exceptions,
|
||||
addrinfo,
|
||||
local_addr_infos,
|
||||
open_sockets,
|
||||
socket_factory,
|
||||
)
|
||||
for addrinfo in addr_infos
|
||||
),
|
||||
happy_eyeballs_delay,
|
||||
)
|
||||
finally:
|
||||
# If we have a winner, staggered_race will
|
||||
# cancel the other tasks, however there is a
|
||||
# small race window where any of the other tasks
|
||||
# can be done before they are cancelled which
|
||||
# will leave the socket open. To avoid this problem
|
||||
# we pass a set to _connect_sock to keep track of
|
||||
# the open sockets and close them here if there
|
||||
# are any "runner up" sockets.
|
||||
for s in open_sockets:
|
||||
if s is not sock:
|
||||
with contextlib.suppress(OSError):
|
||||
s.close()
|
||||
open_sockets = None # type: ignore[assignment]
|
||||
|
||||
if sock is None:
|
||||
all_exceptions = [exc for sub in exceptions for exc in sub]
|
||||
try:
|
||||
first_exception = all_exceptions[0]
|
||||
if len(all_exceptions) == 1:
|
||||
raise first_exception
|
||||
else:
|
||||
# If they all have the same str(), raise one.
|
||||
model = str(first_exception)
|
||||
if all(str(exc) == model for exc in all_exceptions):
|
||||
raise first_exception
|
||||
# Raise a combined exception so the user can see all
|
||||
# the various error messages.
|
||||
msg = "Multiple exceptions: {}".format(
|
||||
", ".join(str(exc) for exc in all_exceptions)
|
||||
)
|
||||
# If the errno is the same for all exceptions, raise
|
||||
# an OSError with that errno.
|
||||
if isinstance(first_exception, OSError):
|
||||
first_errno = first_exception.errno
|
||||
if all(
|
||||
isinstance(exc, OSError) and exc.errno == first_errno
|
||||
for exc in all_exceptions
|
||||
):
|
||||
raise OSError(first_errno, msg)
|
||||
elif isinstance(first_exception, RuntimeError) and all(
|
||||
isinstance(exc, RuntimeError) for exc in all_exceptions
|
||||
):
|
||||
raise RuntimeError(msg)
|
||||
# We have a mix of OSError and RuntimeError
|
||||
# so we have to pick which one to raise.
|
||||
# and we raise OSError for compatibility
|
||||
raise OSError(msg)
|
||||
finally:
|
||||
all_exceptions = None # type: ignore[assignment]
|
||||
exceptions = None # type: ignore[assignment]
|
||||
|
||||
return sock
|
||||
|
||||
|
||||
async def _connect_sock(
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
exceptions: List[List[Union[OSError, RuntimeError]]],
|
||||
addr_info: AddrInfoType,
|
||||
local_addr_infos: Optional[Sequence[AddrInfoType]] = None,
|
||||
open_sockets: Optional[Set[socket.socket]] = None,
|
||||
socket_factory: Optional[SocketFactoryType] = None,
|
||||
) -> socket.socket:
|
||||
"""
|
||||
Create, bind and connect one socket.
|
||||
|
||||
If open_sockets is passed, add the socket to the set of open sockets.
|
||||
Any failure caught here will remove the socket from the set and close it.
|
||||
|
||||
Callers can use this set to close any sockets that are not the winner
|
||||
of all staggered tasks in the result there are runner up sockets aka
|
||||
multiple winners.
|
||||
"""
|
||||
my_exceptions: List[Union[OSError, RuntimeError]] = []
|
||||
exceptions.append(my_exceptions)
|
||||
family, type_, proto, _, address = addr_info
|
||||
sock = None
|
||||
try:
|
||||
if socket_factory is not None:
|
||||
sock = socket_factory(addr_info)
|
||||
else:
|
||||
sock = socket.socket(family=family, type=type_, proto=proto)
|
||||
if open_sockets is not None:
|
||||
open_sockets.add(sock)
|
||||
sock.setblocking(False)
|
||||
if local_addr_infos is not None:
|
||||
for lfamily, _, _, _, laddr in local_addr_infos:
|
||||
# skip local addresses of different family
|
||||
if lfamily != family:
|
||||
continue
|
||||
try:
|
||||
sock.bind(laddr)
|
||||
break
|
||||
except OSError as exc:
|
||||
msg = (
|
||||
f"error while attempting to bind on "
|
||||
f"address {laddr!r}: "
|
||||
f"{(exc.strerror or '').lower()}"
|
||||
)
|
||||
exc = OSError(exc.errno, msg)
|
||||
my_exceptions.append(exc)
|
||||
else: # all bind attempts failed
|
||||
if my_exceptions:
|
||||
raise my_exceptions.pop()
|
||||
else:
|
||||
raise OSError(f"no matching local address with {family=} found")
|
||||
await loop.sock_connect(sock, address)
|
||||
return sock
|
||||
except (RuntimeError, OSError) as exc:
|
||||
my_exceptions.append(exc)
|
||||
if sock is not None:
|
||||
if open_sockets is not None:
|
||||
open_sockets.remove(sock)
|
||||
try:
|
||||
sock.close()
|
||||
except OSError as e:
|
||||
my_exceptions.append(e)
|
||||
raise
|
||||
raise
|
||||
except:
|
||||
if sock is not None:
|
||||
if open_sockets is not None:
|
||||
open_sockets.remove(sock)
|
||||
try:
|
||||
sock.close()
|
||||
except OSError as e:
|
||||
my_exceptions.append(e)
|
||||
raise
|
||||
raise
|
||||
finally:
|
||||
exceptions = my_exceptions = None # type: ignore[assignment]
|
||||
|
||||
|
||||
def _interleave_addrinfos(
|
||||
addrinfos: Sequence[AddrInfoType], first_address_family_count: int = 1
|
||||
) -> List[AddrInfoType]:
|
||||
"""Interleave list of addrinfo tuples by family."""
|
||||
# Group addresses by family
|
||||
addrinfos_by_family: collections.OrderedDict[int, List[AddrInfoType]] = (
|
||||
collections.OrderedDict()
|
||||
)
|
||||
for addr in addrinfos:
|
||||
family = addr[0]
|
||||
if family not in addrinfos_by_family:
|
||||
addrinfos_by_family[family] = []
|
||||
addrinfos_by_family[family].append(addr)
|
||||
addrinfos_lists = list(addrinfos_by_family.values())
|
||||
|
||||
reordered: List[AddrInfoType] = []
|
||||
if first_address_family_count > 1:
|
||||
reordered.extend(addrinfos_lists[0][: first_address_family_count - 1])
|
||||
del addrinfos_lists[0][: first_address_family_count - 1]
|
||||
reordered.extend(
|
||||
a
|
||||
for a in itertools.chain.from_iterable(itertools.zip_longest(*addrinfos_lists))
|
||||
if a is not None
|
||||
)
|
||||
return reordered
|
||||
17
venv/lib/python3.12/site-packages/aiohappyeyeballs/types.py
Normal file
17
venv/lib/python3.12/site-packages/aiohappyeyeballs/types.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Types for aiohappyeyeballs."""
|
||||
|
||||
import socket
|
||||
|
||||
# PY3.9: Import Callable from typing until we drop Python 3.9 support
|
||||
# https://github.com/python/cpython/issues/87131
|
||||
from typing import Callable, Tuple, Union
|
||||
|
||||
AddrInfoType = Tuple[
|
||||
Union[int, socket.AddressFamily],
|
||||
Union[int, socket.SocketKind],
|
||||
int,
|
||||
str,
|
||||
Tuple, # type: ignore[type-arg]
|
||||
]
|
||||
|
||||
SocketFactoryType = Callable[[AddrInfoType], socket.socket]
|
||||
97
venv/lib/python3.12/site-packages/aiohappyeyeballs/utils.py
Normal file
97
venv/lib/python3.12/site-packages/aiohappyeyeballs/utils.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""Utility functions for aiohappyeyeballs."""
|
||||
|
||||
import ipaddress
|
||||
import socket
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
from .types import AddrInfoType
|
||||
|
||||
|
||||
def addr_to_addr_infos(
|
||||
addr: Optional[
|
||||
Union[Tuple[str, int, int, int], Tuple[str, int, int], Tuple[str, int]]
|
||||
],
|
||||
) -> Optional[List[AddrInfoType]]:
|
||||
"""Convert an address tuple to a list of addr_info tuples."""
|
||||
if addr is None:
|
||||
return None
|
||||
host = addr[0]
|
||||
port = addr[1]
|
||||
is_ipv6 = ":" in host
|
||||
if is_ipv6:
|
||||
flowinfo = 0
|
||||
scopeid = 0
|
||||
addr_len = len(addr)
|
||||
if addr_len >= 4:
|
||||
scopeid = addr[3] # type: ignore[misc]
|
||||
if addr_len >= 3:
|
||||
flowinfo = addr[2] # type: ignore[misc]
|
||||
addr = (host, port, flowinfo, scopeid)
|
||||
family = socket.AF_INET6
|
||||
else:
|
||||
addr = (host, port)
|
||||
family = socket.AF_INET
|
||||
return [(family, socket.SOCK_STREAM, socket.IPPROTO_TCP, "", addr)]
|
||||
|
||||
|
||||
def pop_addr_infos_interleave(
|
||||
addr_infos: List[AddrInfoType], interleave: Optional[int] = None
|
||||
) -> None:
|
||||
"""
|
||||
Pop addr_info from the list of addr_infos by family up to interleave times.
|
||||
|
||||
The interleave parameter is used to know how many addr_infos for
|
||||
each family should be popped of the top of the list.
|
||||
"""
|
||||
seen: Dict[int, int] = {}
|
||||
if interleave is None:
|
||||
interleave = 1
|
||||
to_remove: List[AddrInfoType] = []
|
||||
for addr_info in addr_infos:
|
||||
family = addr_info[0]
|
||||
if family not in seen:
|
||||
seen[family] = 0
|
||||
if seen[family] < interleave:
|
||||
to_remove.append(addr_info)
|
||||
seen[family] += 1
|
||||
for addr_info in to_remove:
|
||||
addr_infos.remove(addr_info)
|
||||
|
||||
|
||||
def _addr_tuple_to_ip_address(
|
||||
addr: Union[Tuple[str, int], Tuple[str, int, int, int]],
|
||||
) -> Union[
|
||||
Tuple[ipaddress.IPv4Address, int], Tuple[ipaddress.IPv6Address, int, int, int]
|
||||
]:
|
||||
"""Convert an address tuple to an IPv4Address."""
|
||||
return (ipaddress.ip_address(addr[0]), *addr[1:])
|
||||
|
||||
|
||||
def remove_addr_infos(
|
||||
addr_infos: List[AddrInfoType],
|
||||
addr: Union[Tuple[str, int], Tuple[str, int, int, int]],
|
||||
) -> None:
|
||||
"""
|
||||
Remove an address from the list of addr_infos.
|
||||
|
||||
The addr value is typically the return value of
|
||||
sock.getpeername().
|
||||
"""
|
||||
bad_addrs_infos: List[AddrInfoType] = []
|
||||
for addr_info in addr_infos:
|
||||
if addr_info[-1] == addr:
|
||||
bad_addrs_infos.append(addr_info)
|
||||
if bad_addrs_infos:
|
||||
for bad_addr_info in bad_addrs_infos:
|
||||
addr_infos.remove(bad_addr_info)
|
||||
return
|
||||
# Slow path in case addr is formatted differently
|
||||
match_addr = _addr_tuple_to_ip_address(addr)
|
||||
for addr_info in addr_infos:
|
||||
if match_addr == _addr_tuple_to_ip_address(addr_info[-1]):
|
||||
bad_addrs_infos.append(addr_info)
|
||||
if bad_addrs_infos:
|
||||
for bad_addr_info in bad_addrs_infos:
|
||||
addr_infos.remove(bad_addr_info)
|
||||
return
|
||||
raise ValueError(f"Address {addr} not found in addr_infos")
|
||||
@@ -0,0 +1,262 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: aiohttp
|
||||
Version: 3.13.5
|
||||
Summary: Async http client/server framework (asyncio)
|
||||
Maintainer-email: aiohttp team <team@aiohttp.org>
|
||||
License: Apache-2.0 AND MIT
|
||||
Project-URL: Homepage, https://github.com/aio-libs/aiohttp
|
||||
Project-URL: Chat: Matrix, https://matrix.to/#/#aio-libs:matrix.org
|
||||
Project-URL: Chat: Matrix Space, https://matrix.to/#/#aio-libs-space:matrix.org
|
||||
Project-URL: CI: GitHub Actions, https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI
|
||||
Project-URL: Coverage: codecov, https://codecov.io/github/aio-libs/aiohttp
|
||||
Project-URL: Docs: Changelog, https://docs.aiohttp.org/en/stable/changes.html
|
||||
Project-URL: Docs: RTD, https://docs.aiohttp.org
|
||||
Project-URL: GitHub: issues, https://github.com/aio-libs/aiohttp/issues
|
||||
Project-URL: GitHub: repo, https://github.com/aio-libs/aiohttp
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Framework :: AsyncIO
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Operating System :: POSIX
|
||||
Classifier: Operating System :: MacOS :: MacOS X
|
||||
Classifier: Operating System :: Microsoft :: Windows
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Programming Language :: Python :: 3.13
|
||||
Classifier: Programming Language :: Python :: 3.14
|
||||
Classifier: Topic :: Internet :: WWW/HTTP
|
||||
Requires-Python: >=3.9
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE.txt
|
||||
License-File: vendor/llhttp/LICENSE
|
||||
Requires-Dist: aiohappyeyeballs>=2.5.0
|
||||
Requires-Dist: aiosignal>=1.4.0
|
||||
Requires-Dist: async-timeout<6.0,>=4.0; python_version < "3.11"
|
||||
Requires-Dist: attrs>=17.3.0
|
||||
Requires-Dist: frozenlist>=1.1.1
|
||||
Requires-Dist: multidict<7.0,>=4.5
|
||||
Requires-Dist: propcache>=0.2.0
|
||||
Requires-Dist: yarl<2.0,>=1.17.0
|
||||
Provides-Extra: speedups
|
||||
Requires-Dist: aiodns>=3.3.0; extra == "speedups"
|
||||
Requires-Dist: Brotli>=1.2; platform_python_implementation == "CPython" and extra == "speedups"
|
||||
Requires-Dist: brotlicffi>=1.2; platform_python_implementation != "CPython" and extra == "speedups"
|
||||
Requires-Dist: backports.zstd; (platform_python_implementation == "CPython" and python_version < "3.14") and extra == "speedups"
|
||||
Dynamic: license-file
|
||||
|
||||
==================================
|
||||
Async http client/server framework
|
||||
==================================
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/aio-libs/aiohttp/master/docs/aiohttp-plain.svg
|
||||
:height: 64px
|
||||
:width: 64px
|
||||
:alt: aiohttp logo
|
||||
|
||||
|
|
||||
|
||||
.. image:: https://github.com/aio-libs/aiohttp/workflows/CI/badge.svg
|
||||
:target: https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI
|
||||
:alt: GitHub Actions status for master branch
|
||||
|
||||
.. image:: https://codecov.io/gh/aio-libs/aiohttp/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/aio-libs/aiohttp
|
||||
:alt: codecov.io status for master branch
|
||||
|
||||
.. image:: https://badge.fury.io/py/aiohttp.svg
|
||||
:target: https://pypi.org/project/aiohttp
|
||||
:alt: Latest PyPI package version
|
||||
|
||||
.. image:: https://img.shields.io/pypi/dm/aiohttp
|
||||
:target: https://pypistats.org/packages/aiohttp
|
||||
:alt: Downloads count
|
||||
|
||||
.. image:: https://readthedocs.org/projects/aiohttp/badge/?version=latest
|
||||
:target: https://docs.aiohttp.org/
|
||||
:alt: Latest Read The Docs
|
||||
|
||||
.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json
|
||||
:target: https://codspeed.io/aio-libs/aiohttp
|
||||
:alt: Codspeed.io status for aiohttp
|
||||
|
||||
|
||||
Key Features
|
||||
============
|
||||
|
||||
- Supports both client and server side of HTTP protocol.
|
||||
- Supports both client and server Web-Sockets out-of-the-box and avoids
|
||||
Callback Hell.
|
||||
- Provides Web-server with middleware and pluggable routing.
|
||||
|
||||
|
||||
Getting started
|
||||
===============
|
||||
|
||||
Client
|
||||
------
|
||||
|
||||
To get something from the web:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import aiohttp
|
||||
import asyncio
|
||||
|
||||
async def main():
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get('http://python.org') as response:
|
||||
|
||||
print("Status:", response.status)
|
||||
print("Content-type:", response.headers['content-type'])
|
||||
|
||||
html = await response.text()
|
||||
print("Body:", html[:15], "...")
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
This prints:
|
||||
|
||||
.. code-block::
|
||||
|
||||
Status: 200
|
||||
Content-type: text/html; charset=utf-8
|
||||
Body: <!doctype html> ...
|
||||
|
||||
Coming from `requests <https://requests.readthedocs.io/>`_ ? Read `why we need so many lines <https://aiohttp.readthedocs.io/en/latest/http_request_lifecycle.html>`_.
|
||||
|
||||
Server
|
||||
------
|
||||
|
||||
An example using a simple server:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# examples/server_simple.py
|
||||
from aiohttp import web
|
||||
|
||||
async def handle(request):
|
||||
name = request.match_info.get('name', "Anonymous")
|
||||
text = "Hello, " + name
|
||||
return web.Response(text=text)
|
||||
|
||||
async def wshandle(request):
|
||||
ws = web.WebSocketResponse()
|
||||
await ws.prepare(request)
|
||||
|
||||
async for msg in ws:
|
||||
if msg.type == web.WSMsgType.text:
|
||||
await ws.send_str("Hello, {}".format(msg.data))
|
||||
elif msg.type == web.WSMsgType.binary:
|
||||
await ws.send_bytes(msg.data)
|
||||
elif msg.type == web.WSMsgType.close:
|
||||
break
|
||||
|
||||
return ws
|
||||
|
||||
|
||||
app = web.Application()
|
||||
app.add_routes([web.get('/', handle),
|
||||
web.get('/echo', wshandle),
|
||||
web.get('/{name}', handle)])
|
||||
|
||||
if __name__ == '__main__':
|
||||
web.run_app(app)
|
||||
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
https://aiohttp.readthedocs.io/
|
||||
|
||||
|
||||
Demos
|
||||
=====
|
||||
|
||||
https://github.com/aio-libs/aiohttp-demos
|
||||
|
||||
|
||||
External links
|
||||
==============
|
||||
|
||||
* `Third party libraries
|
||||
<http://aiohttp.readthedocs.io/en/latest/third_party.html>`_
|
||||
* `Built with aiohttp
|
||||
<http://aiohttp.readthedocs.io/en/latest/built_with.html>`_
|
||||
* `Powered by aiohttp
|
||||
<http://aiohttp.readthedocs.io/en/latest/powered_by.html>`_
|
||||
|
||||
Feel free to make a Pull Request for adding your link to these pages!
|
||||
|
||||
|
||||
Communication channels
|
||||
======================
|
||||
|
||||
*aio-libs Discussions*: https://github.com/aio-libs/aiohttp/discussions
|
||||
|
||||
*Matrix*: `#aio-libs:matrix.org <https://matrix.to/#/#aio-libs:matrix.org>`_
|
||||
|
||||
We support `Stack Overflow
|
||||
<https://stackoverflow.com/questions/tagged/aiohttp>`_.
|
||||
Please add *aiohttp* tag to your question there.
|
||||
|
||||
Requirements
|
||||
============
|
||||
|
||||
- attrs_
|
||||
- multidict_
|
||||
- yarl_
|
||||
- frozenlist_
|
||||
|
||||
Optionally you may install the aiodns_ library (highly recommended for sake of speed).
|
||||
|
||||
.. _aiodns: https://pypi.python.org/pypi/aiodns
|
||||
.. _attrs: https://github.com/python-attrs/attrs
|
||||
.. _multidict: https://pypi.python.org/pypi/multidict
|
||||
.. _frozenlist: https://pypi.org/project/frozenlist/
|
||||
.. _yarl: https://pypi.python.org/pypi/yarl
|
||||
.. _async-timeout: https://pypi.python.org/pypi/async_timeout
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
``aiohttp`` is offered under the Apache 2 license.
|
||||
|
||||
|
||||
Keepsafe
|
||||
========
|
||||
|
||||
The aiohttp community would like to thank Keepsafe
|
||||
(https://www.getkeepsafe.com) for its support in the early days of
|
||||
the project.
|
||||
|
||||
|
||||
Source code
|
||||
===========
|
||||
|
||||
The latest developer version is available in a GitHub repository:
|
||||
https://github.com/aio-libs/aiohttp
|
||||
|
||||
Benchmarks
|
||||
==========
|
||||
|
||||
If you are interested in efficiency, the AsyncIO community maintains a
|
||||
list of benchmarks on the official wiki:
|
||||
https://github.com/python/asyncio/wiki/Benchmarks
|
||||
|
||||
--------
|
||||
|
||||
.. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
|
||||
:target: https://matrix.to/#/%23aio-libs:matrix.org
|
||||
:alt: Matrix Room — #aio-libs:matrix.org
|
||||
|
||||
.. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
|
||||
:target: https://matrix.to/#/%23aio-libs-space:matrix.org
|
||||
:alt: Matrix Space — #aio-libs-space:matrix.org
|
||||
|
||||
.. image:: https://insights.linuxfoundation.org/api/badge/health-score?project=aiohttp
|
||||
:target: https://insights.linuxfoundation.org/project/aiohttp
|
||||
:alt: LFX Health Score
|
||||
@@ -0,0 +1,139 @@
|
||||
aiohttp-3.13.5.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
aiohttp-3.13.5.dist-info/METADATA,sha256=yVHd0XDIuuar5cgTh5EZI2mDXCREeUCWCZ-VA6gWvUQ,8145
|
||||
aiohttp-3.13.5.dist-info/RECORD,,
|
||||
aiohttp-3.13.5.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
aiohttp-3.13.5.dist-info/WHEEL,sha256=Tc3fF66yn9Kh-hkUUsdKQyuB9Lw0CDoeANnEbSVc3f4,190
|
||||
aiohttp-3.13.5.dist-info/licenses/LICENSE.txt,sha256=n4DQ2311WpQdtFchcsJw7L2PCCuiFd3QlZhZQu2Uqes,588
|
||||
aiohttp-3.13.5.dist-info/licenses/vendor/llhttp/LICENSE,sha256=68qFTgE0zSVtZzYnwgSZ9CV363S6zwi58ltianPJEnc,1105
|
||||
aiohttp-3.13.5.dist-info/top_level.txt,sha256=iv-JIaacmTl-hSho3QmphcKnbRRYx1st47yjz_178Ro,8
|
||||
aiohttp/.hash/_cparser.pxd.hash,sha256=pjs-sEXNw_eijXGAedwG-BHnlFp8B7sOCgUagIWaU2A,121
|
||||
aiohttp/.hash/_find_header.pxd.hash,sha256=_mbpD6vM-CVCKq3ulUvsOAz5Wdo88wrDzfpOsMQaMNA,125
|
||||
aiohttp/.hash/_http_parser.pyx.hash,sha256=T47Ily1qaTY3xOHgBgJZE5A_bKruDtCG_droC3m7BD8,125
|
||||
aiohttp/.hash/_http_writer.pyx.hash,sha256=DO9V6lTsDngY8e4y5O0ghHRc-RNGq-pVt_dLNHJy7xA,125
|
||||
aiohttp/.hash/hdrs.py.hash,sha256=v6IaKbsxjsdQxBzhb5AjP0x_9G3rUe84D7avf7AI4cs,116
|
||||
aiohttp/__init__.py,sha256=r1a3wmBz41XCNY0dqSnCcgfqrDYSDYD-_vKBN3Bfy3Y,8302
|
||||
aiohttp/__pycache__/__init__.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/_cookie_helpers.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/abc.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/base_protocol.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/client.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/client_exceptions.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/client_middleware_digest_auth.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/client_middlewares.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/client_proto.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/client_reqrep.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/client_ws.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/compression_utils.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/connector.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/cookiejar.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/formdata.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/hdrs.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/helpers.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/http.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/http_exceptions.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/http_parser.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/http_websocket.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/http_writer.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/log.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/multipart.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/payload.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/payload_streamer.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/pytest_plugin.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/resolver.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/streams.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/tcp_helpers.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/test_utils.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/tracing.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/typedefs.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_app.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_exceptions.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_fileresponse.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_log.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_middlewares.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_protocol.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_request.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_response.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_routedef.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_runner.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_server.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_urldispatcher.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/web_ws.cpython-312.pyc,,
|
||||
aiohttp/__pycache__/worker.cpython-312.pyc,,
|
||||
aiohttp/_cookie_helpers.py,sha256=_p7y-B8OCAk7FLjByiuwFIpDLGuNoJn3_vixzymAFnE,13659
|
||||
aiohttp/_cparser.pxd,sha256=UnbUYCHg4NdXfgyRVYAMv2KTLWClB4P-xCrvtj_r7ew,4295
|
||||
aiohttp/_find_header.pxd,sha256=0GfwFCPN2zxEKTO1_MA5sYq2UfzsG8kcV3aTqvwlz3g,68
|
||||
aiohttp/_headers.pxi,sha256=n701k28dVPjwRnx5j6LpJhLTfj7dqu2vJt7f0O60Oyg,2007
|
||||
aiohttp/_http_parser.cpython-312-x86_64-linux-gnu.so,sha256=8_yExteNcPiEn2dGe3fYFBx_b_qsQwVwhrN_0j6dMQQ,2947720
|
||||
aiohttp/_http_parser.pyx,sha256=veuwNKb6YzBJoO3nbvrkOCaTzbcROS5MVksUJhDHNgg,30036
|
||||
aiohttp/_http_writer.cpython-312-x86_64-linux-gnu.so,sha256=2dzfCN0-Sz-bY-MqfHits-FvPnvcxBYgqQrAQSxeIvo,472496
|
||||
aiohttp/_http_writer.pyx,sha256=TXNOC90txk4wnPakxex_ygSbr3xq1UxaCi2T4gv2teY,4690
|
||||
aiohttp/_websocket/.hash/mask.pxd.hash,sha256=Y0zBddk_ck3pi9-BFzMcpkcvCKvwvZ4GTtZFb9u1nxQ,128
|
||||
aiohttp/_websocket/.hash/mask.pyx.hash,sha256=90owpXYM8_kIma4KUcOxhWSk-Uv4NVMBoCYeFM1B3d0,128
|
||||
aiohttp/_websocket/.hash/reader_c.pxd.hash,sha256=5xf3oobk6vx4xbJm-xtZ1_QufB8fYFtLQV2MNdqUc1w,132
|
||||
aiohttp/_websocket/__init__.py,sha256=Mar3R9_vBN_Ea4lsW7iTAVXD7OKswKPGqF5xgSyt77k,44
|
||||
aiohttp/_websocket/__pycache__/__init__.cpython-312.pyc,,
|
||||
aiohttp/_websocket/__pycache__/helpers.cpython-312.pyc,,
|
||||
aiohttp/_websocket/__pycache__/models.cpython-312.pyc,,
|
||||
aiohttp/_websocket/__pycache__/reader.cpython-312.pyc,,
|
||||
aiohttp/_websocket/__pycache__/reader_c.cpython-312.pyc,,
|
||||
aiohttp/_websocket/__pycache__/reader_py.cpython-312.pyc,,
|
||||
aiohttp/_websocket/__pycache__/writer.cpython-312.pyc,,
|
||||
aiohttp/_websocket/helpers.py,sha256=P-XLv8IUaihKzDenVUqfKU5DJbWE5HvG8uhvUZK8Ic4,5038
|
||||
aiohttp/_websocket/mask.cpython-312-x86_64-linux-gnu.so,sha256=EpRwPJm1K1yavMCd9llAWdT4AsqKx_QEN0rb0eJH_Kc,263512
|
||||
aiohttp/_websocket/mask.pxd,sha256=sBmZ1Amym9kW4Ge8lj1fLZ7mPPya4LzLdpkQExQXv5M,112
|
||||
aiohttp/_websocket/mask.pyx,sha256=BHjOtV0O0w7xp9p0LNADRJvGmgfPn9sGeJvSs0fL__4,1397
|
||||
aiohttp/_websocket/models.py,sha256=XAzjs_8JYszWXIgZ6R3ZRrF-tX9Q_6LiD49WRYojopM,2121
|
||||
aiohttp/_websocket/reader.py,sha256=eC4qS0c5sOeQ2ebAHLaBpIaTVFaSKX79pY2xvh3Pqyw,1030
|
||||
aiohttp/_websocket/reader_c.cpython-312-x86_64-linux-gnu.so,sha256=GYd-y-IkGkOIp3vmma5BmZgmG9Py9fLTybcmMWyHNf0,1822128
|
||||
aiohttp/_websocket/reader_c.pxd,sha256=nl_njtDrzlQU0rjgGGjZDB-swguE0tX_bCPobkShVa4,2625
|
||||
aiohttp/_websocket/reader_c.py,sha256=V5YtZ2gj2BjE2Q-W9sR_MdAl1VAm1pB7ZjozVJcOpbg,18868
|
||||
aiohttp/_websocket/reader_py.py,sha256=V5YtZ2gj2BjE2Q-W9sR_MdAl1VAm1pB7ZjozVJcOpbg,18868
|
||||
aiohttp/_websocket/writer.py,sha256=2OvSktPmNh_g20h1cXJt2Xu8u6IvswnPjdur7OwBbJk,11261
|
||||
aiohttp/abc.py,sha256=M66F4S6m00bIEn7y4ha_XLTMDmVQ9dPihfOVB0pGfOo,7149
|
||||
aiohttp/base_protocol.py,sha256=Tp8cxUPQvv9kUPk3w6lAzk6d2MAzV3scwI_3Go3C47c,3025
|
||||
aiohttp/client.py,sha256=JkHGrrJq1pSI7QEJw0jZKx5TLLzM87of8Yx-62m4x6g,58795
|
||||
aiohttp/client_exceptions.py,sha256=t6vDg0ZhHDvij6E89Nv473zLpNu3CyMPAp0LqLHOc9I,11744
|
||||
aiohttp/client_middleware_digest_auth.py,sha256=rInzAPEmFsaF42oVORmYSx56og1B7jZUSmmIdFD0MQ8,17641
|
||||
aiohttp/client_middlewares.py,sha256=kP5N9CMzQPMGPIEydeVUiLUTLsw8Vl8Gr4qAWYdu3vM,1918
|
||||
aiohttp/client_proto.py,sha256=eimrsOVNMt_y62gmXZ4JoBI5VZ0kGaW6WnqMkB1cnNQ,12179
|
||||
aiohttp/client_reqrep.py,sha256=eEREDrZ0M8ZFTt1wjHduR-P8_sm40K65gNz-iMGYask,53391
|
||||
aiohttp/client_ws.py,sha256=1CIjIXwyzOMIYw6AjUES4-qUwbyVHW1seJKQfg_Rta8,15109
|
||||
aiohttp/compression_utils.py,sha256=hJ2LXhN2OWukFHm5b78TJFGKcAiL2kthi9Sf5PRYO-U,11738
|
||||
aiohttp/connector.py,sha256=0AFVMAx7NwkIbovQ0qecQ3JO2bpEZpxMG_OnGZ3tP3E,69451
|
||||
aiohttp/cookiejar.py,sha256=e28ZMQwJ5P0vbPX1OX4Se7-k3zeGvocFEqzGhwpG53k,18922
|
||||
aiohttp/formdata.py,sha256=0cxe-naBSFp_NHB1l-TUSbhesU4dpNvwJMzhsLYoSns,6625
|
||||
aiohttp/hdrs.py,sha256=2rj5MyA-6yRdYPhW5UKkW4iNWhEAlGIOSBH5D4FmKNE,5111
|
||||
aiohttp/helpers.py,sha256=Q1307PCEnWz4RP8crUw8dk58c0YF2Ei3JywkKfRxz5E,30629
|
||||
aiohttp/http.py,sha256=8o8j8xH70OWjnfTWA9V44NR785QPxEPrUtzMXiAVpwc,1842
|
||||
aiohttp/http_exceptions.py,sha256=Z4IiKYxyfn1Pe947OfqW6NjXoVd12bDI4eKBZKC3Htw,3085
|
||||
aiohttp/http_parser.py,sha256=TIvopmjIH3TSTrVTbbqxpBWBpkEuOaNjkzES0-fm4-4,40472
|
||||
aiohttp/http_websocket.py,sha256=8VXFKw6KQUEmPg48GtRMB37v0gTK7A0inoxXuDxMZEc,842
|
||||
aiohttp/http_writer.py,sha256=4sbiFD_RkE0x_xlAPXwGrL0iLS53L72Qw13hBPaQLls,12508
|
||||
aiohttp/log.py,sha256=BbNKx9e3VMIm0xYjZI0IcBBoS7wjdeIeSaiJE7-qK2g,325
|
||||
aiohttp/multipart.py,sha256=qMDEHLf64qQcPQLZv4RdwKUDB2L1CC_vN-5Z-P8wDzw,42805
|
||||
aiohttp/payload.py,sha256=O6nsYNULL7AeM2cyJ6TYX73ncVnL5xJwt5AegxwMKqw,40874
|
||||
aiohttp/payload_streamer.py,sha256=ZzEYyfzcjGWkVkK3XR2pBthSCSIykYvY3Wr5cGQ2eTc,2211
|
||||
aiohttp/py.typed,sha256=sow9soTwP9T_gEAQSVh7Gb8855h04Nwmhs2We-JRgZM,7
|
||||
aiohttp/pytest_plugin.py,sha256=z4XwqmsKdyJCKxbGiA5kFf90zcedvomqk4RqjZbhKNk,12901
|
||||
aiohttp/resolver.py,sha256=gsrfUpFf8iHlcHfJvY-1fiBHW3PRvRVNb5lNZBg3zlY,10031
|
||||
aiohttp/streams.py,sha256=i32xpXEjGL2c6ifepnBxBto5y9wXobzzYAIsi8kAkKI,23920
|
||||
aiohttp/tcp_helpers.py,sha256=BSadqVWaBpMFDRWnhaaR941N9MiDZ7bdTrxgCb0CW-M,961
|
||||
aiohttp/test_utils.py,sha256=7hMcJnOUSkZ54Axgdvveeth5y6R_B2T-0H2xX7mKWYc,23130
|
||||
aiohttp/tracing.py,sha256=-6aaW6l0J9uJD45LzR4cijYH0j62pt0U_nn_aVzFku4,14558
|
||||
aiohttp/typedefs.py,sha256=wUlqwe9Mw9W8jT3HsYJcYk00qP3EMPz3nTkYXmeNN48,1657
|
||||
aiohttp/web.py,sha256=JzSNmejg5G6YeFAnkIgZfytqbU86sNu844yYKmoUpqs,17852
|
||||
aiohttp/web_app.py,sha256=lGU_aAMN-h3wy-LTTHi6SeKH8ydt1G51BXcCspgD5ZA,19452
|
||||
aiohttp/web_exceptions.py,sha256=aXW8MStpRS7JOFxYDPgPlWojMJP9RhxyAqvq2g0jOCE,10495
|
||||
aiohttp/web_fileresponse.py,sha256=arimioSXf0rZQ46H9ejCip4hVN6ri_faFWfuP_XL_us,16374
|
||||
aiohttp/web_log.py,sha256=1hhC05PJizS2POVfNxncorNM_fjYHMx2TbcpP6isxkQ,8624
|
||||
aiohttp/web_middlewares.py,sha256=sFI0AgeNjdyAjuz92QtMIpngmJSOxrqe2Jfbs4BNUu0,4165
|
||||
aiohttp/web_protocol.py,sha256=ZEwjomG6Y2olq2b967UXDezwYnuLJJHDNl48SgHpKJs,27210
|
||||
aiohttp/web_request.py,sha256=xNCrsUQSqTB64G1_9QiC9xfQmdgkJwYkGfPBwnfIYQo,30406
|
||||
aiohttp/web_response.py,sha256=DofOPOU4be_upiqouWjxfhMLyubtRFvOGnJZbIamH6s,29366
|
||||
aiohttp/web_routedef.py,sha256=VT1GAx6BrawoDh5RwBwBu5wSABSqgWwAe74AUCyZAEo,6110
|
||||
aiohttp/web_runner.py,sha256=v1G1nKiOOQgFnTSR4IMc6I9ReEFDMaHtMLvO_roDM-A,11786
|
||||
aiohttp/web_server.py,sha256=-9WDKUAiR9ll-rSdwXSqG6YjaoW79d1R4y0BGSqgUMA,2888
|
||||
aiohttp/web_urldispatcher.py,sha256=9pJ6b3LuujtZuAURNYh0tdhgCJn2ZFrakeEySNwNqhU,44512
|
||||
aiohttp/web_ws.py,sha256=lItgmyatkXh0M6EY7JoZnSZkUl6R0wv8B88X4ILqQbU,22739
|
||||
aiohttp/worker.py,sha256=o6BL8dbCGYysB76i_yGGvYagc9gyy2PyW1UisiOznjA,8345
|
||||
@@ -0,0 +1,7 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (82.0.1)
|
||||
Root-Is-Purelib: false
|
||||
Tag: cp312-cp312-manylinux_2_17_x86_64
|
||||
Tag: cp312-cp312-manylinux2014_x86_64
|
||||
Tag: cp312-cp312-manylinux_2_28_x86_64
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
Copyright aio-libs contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
22
venv/lib/python3.12/site-packages/aiohttp-3.13.5.dist-info/licenses/vendor/llhttp/LICENSE
vendored
Normal file
22
venv/lib/python3.12/site-packages/aiohttp-3.13.5.dist-info/licenses/vendor/llhttp/LICENSE
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
This software is licensed under the MIT License.
|
||||
|
||||
Copyright Fedor Indutny, 2018.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1 @@
|
||||
aiohttp
|
||||
@@ -0,0 +1 @@
|
||||
5276d46021e0e0d7577e0c9155800cbf62932d60a50783fec42aefb63febedec /home/runner/work/aiohttp/aiohttp/aiohttp/_cparser.pxd
|
||||
@@ -0,0 +1 @@
|
||||
d067f01423cddb3c442933b5fcc039b18ab651fcec1bc91c577693aafc25cf78 /home/runner/work/aiohttp/aiohttp/aiohttp/_find_header.pxd
|
||||
@@ -0,0 +1 @@
|
||||
bdebb034a6fa633049a0ede76efae4382693cdb711392e4c564b142610c73608 /home/runner/work/aiohttp/aiohttp/aiohttp/_http_parser.pyx
|
||||
@@ -0,0 +1 @@
|
||||
4d734e0bdd2dc64e309cf6a4c5ec7fca049baf7c6ad54c5a0a2d93e20bf6b5e6 /home/runner/work/aiohttp/aiohttp/aiohttp/_http_writer.pyx
|
||||
@@ -0,0 +1 @@
|
||||
dab8f933203eeb245d60f856e542a45b888d5a110094620e4811f90f816628d1 /home/runner/work/aiohttp/aiohttp/aiohttp/hdrs.py
|
||||
278
venv/lib/python3.12/site-packages/aiohttp/__init__.py
Normal file
278
venv/lib/python3.12/site-packages/aiohttp/__init__.py
Normal file
@@ -0,0 +1,278 @@
|
||||
__version__ = "3.13.5"
|
||||
|
||||
from typing import TYPE_CHECKING, Tuple
|
||||
|
||||
from . import hdrs as hdrs
|
||||
from .client import (
|
||||
BaseConnector,
|
||||
ClientConnectionError,
|
||||
ClientConnectionResetError,
|
||||
ClientConnectorCertificateError,
|
||||
ClientConnectorDNSError,
|
||||
ClientConnectorError,
|
||||
ClientConnectorSSLError,
|
||||
ClientError,
|
||||
ClientHttpProxyError,
|
||||
ClientOSError,
|
||||
ClientPayloadError,
|
||||
ClientProxyConnectionError,
|
||||
ClientRequest,
|
||||
ClientResponse,
|
||||
ClientResponseError,
|
||||
ClientSession,
|
||||
ClientSSLError,
|
||||
ClientTimeout,
|
||||
ClientWebSocketResponse,
|
||||
ClientWSTimeout,
|
||||
ConnectionTimeoutError,
|
||||
ContentTypeError,
|
||||
Fingerprint,
|
||||
InvalidURL,
|
||||
InvalidUrlClientError,
|
||||
InvalidUrlRedirectClientError,
|
||||
NamedPipeConnector,
|
||||
NonHttpUrlClientError,
|
||||
NonHttpUrlRedirectClientError,
|
||||
RedirectClientError,
|
||||
RequestInfo,
|
||||
ServerConnectionError,
|
||||
ServerDisconnectedError,
|
||||
ServerFingerprintMismatch,
|
||||
ServerTimeoutError,
|
||||
SocketTimeoutError,
|
||||
TCPConnector,
|
||||
TooManyRedirects,
|
||||
UnixConnector,
|
||||
WSMessageTypeError,
|
||||
WSServerHandshakeError,
|
||||
request,
|
||||
)
|
||||
from .client_middleware_digest_auth import DigestAuthMiddleware
|
||||
from .client_middlewares import ClientHandlerType, ClientMiddlewareType
|
||||
from .compression_utils import set_zlib_backend
|
||||
from .connector import (
|
||||
AddrInfoType as AddrInfoType,
|
||||
SocketFactoryType as SocketFactoryType,
|
||||
)
|
||||
from .cookiejar import CookieJar as CookieJar, DummyCookieJar as DummyCookieJar
|
||||
from .formdata import FormData as FormData
|
||||
from .helpers import BasicAuth, ChainMapProxy, ETag
|
||||
from .http import (
|
||||
HttpVersion as HttpVersion,
|
||||
HttpVersion10 as HttpVersion10,
|
||||
HttpVersion11 as HttpVersion11,
|
||||
WebSocketError as WebSocketError,
|
||||
WSCloseCode as WSCloseCode,
|
||||
WSMessage as WSMessage,
|
||||
WSMsgType as WSMsgType,
|
||||
)
|
||||
from .multipart import (
|
||||
BadContentDispositionHeader as BadContentDispositionHeader,
|
||||
BadContentDispositionParam as BadContentDispositionParam,
|
||||
BodyPartReader as BodyPartReader,
|
||||
MultipartReader as MultipartReader,
|
||||
MultipartWriter as MultipartWriter,
|
||||
content_disposition_filename as content_disposition_filename,
|
||||
parse_content_disposition as parse_content_disposition,
|
||||
)
|
||||
from .payload import (
|
||||
PAYLOAD_REGISTRY as PAYLOAD_REGISTRY,
|
||||
AsyncIterablePayload as AsyncIterablePayload,
|
||||
BufferedReaderPayload as BufferedReaderPayload,
|
||||
BytesIOPayload as BytesIOPayload,
|
||||
BytesPayload as BytesPayload,
|
||||
IOBasePayload as IOBasePayload,
|
||||
JsonPayload as JsonPayload,
|
||||
Payload as Payload,
|
||||
StringIOPayload as StringIOPayload,
|
||||
StringPayload as StringPayload,
|
||||
TextIOPayload as TextIOPayload,
|
||||
get_payload as get_payload,
|
||||
payload_type as payload_type,
|
||||
)
|
||||
from .payload_streamer import streamer as streamer
|
||||
from .resolver import (
|
||||
AsyncResolver as AsyncResolver,
|
||||
DefaultResolver as DefaultResolver,
|
||||
ThreadedResolver as ThreadedResolver,
|
||||
)
|
||||
from .streams import (
|
||||
EMPTY_PAYLOAD as EMPTY_PAYLOAD,
|
||||
DataQueue as DataQueue,
|
||||
EofStream as EofStream,
|
||||
FlowControlDataQueue as FlowControlDataQueue,
|
||||
StreamReader as StreamReader,
|
||||
)
|
||||
from .tracing import (
|
||||
TraceConfig as TraceConfig,
|
||||
TraceConnectionCreateEndParams as TraceConnectionCreateEndParams,
|
||||
TraceConnectionCreateStartParams as TraceConnectionCreateStartParams,
|
||||
TraceConnectionQueuedEndParams as TraceConnectionQueuedEndParams,
|
||||
TraceConnectionQueuedStartParams as TraceConnectionQueuedStartParams,
|
||||
TraceConnectionReuseconnParams as TraceConnectionReuseconnParams,
|
||||
TraceDnsCacheHitParams as TraceDnsCacheHitParams,
|
||||
TraceDnsCacheMissParams as TraceDnsCacheMissParams,
|
||||
TraceDnsResolveHostEndParams as TraceDnsResolveHostEndParams,
|
||||
TraceDnsResolveHostStartParams as TraceDnsResolveHostStartParams,
|
||||
TraceRequestChunkSentParams as TraceRequestChunkSentParams,
|
||||
TraceRequestEndParams as TraceRequestEndParams,
|
||||
TraceRequestExceptionParams as TraceRequestExceptionParams,
|
||||
TraceRequestHeadersSentParams as TraceRequestHeadersSentParams,
|
||||
TraceRequestRedirectParams as TraceRequestRedirectParams,
|
||||
TraceRequestStartParams as TraceRequestStartParams,
|
||||
TraceResponseChunkReceivedParams as TraceResponseChunkReceivedParams,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# At runtime these are lazy-loaded at the bottom of the file.
|
||||
from .worker import (
|
||||
GunicornUVLoopWebWorker as GunicornUVLoopWebWorker,
|
||||
GunicornWebWorker as GunicornWebWorker,
|
||||
)
|
||||
|
||||
__all__: Tuple[str, ...] = (
|
||||
"hdrs",
|
||||
# client
|
||||
"AddrInfoType",
|
||||
"BaseConnector",
|
||||
"ClientConnectionError",
|
||||
"ClientConnectionResetError",
|
||||
"ClientConnectorCertificateError",
|
||||
"ClientConnectorDNSError",
|
||||
"ClientConnectorError",
|
||||
"ClientConnectorSSLError",
|
||||
"ClientError",
|
||||
"ClientHttpProxyError",
|
||||
"ClientOSError",
|
||||
"ClientPayloadError",
|
||||
"ClientProxyConnectionError",
|
||||
"ClientResponse",
|
||||
"ClientRequest",
|
||||
"ClientResponseError",
|
||||
"ClientSSLError",
|
||||
"ClientSession",
|
||||
"ClientTimeout",
|
||||
"ClientWebSocketResponse",
|
||||
"ClientWSTimeout",
|
||||
"ConnectionTimeoutError",
|
||||
"ContentTypeError",
|
||||
"Fingerprint",
|
||||
"FlowControlDataQueue",
|
||||
"InvalidURL",
|
||||
"InvalidUrlClientError",
|
||||
"InvalidUrlRedirectClientError",
|
||||
"NonHttpUrlClientError",
|
||||
"NonHttpUrlRedirectClientError",
|
||||
"RedirectClientError",
|
||||
"RequestInfo",
|
||||
"ServerConnectionError",
|
||||
"ServerDisconnectedError",
|
||||
"ServerFingerprintMismatch",
|
||||
"ServerTimeoutError",
|
||||
"SocketFactoryType",
|
||||
"SocketTimeoutError",
|
||||
"TCPConnector",
|
||||
"TooManyRedirects",
|
||||
"UnixConnector",
|
||||
"NamedPipeConnector",
|
||||
"WSServerHandshakeError",
|
||||
"request",
|
||||
# client_middleware
|
||||
"ClientMiddlewareType",
|
||||
"ClientHandlerType",
|
||||
# cookiejar
|
||||
"CookieJar",
|
||||
"DummyCookieJar",
|
||||
# formdata
|
||||
"FormData",
|
||||
# helpers
|
||||
"BasicAuth",
|
||||
"ChainMapProxy",
|
||||
"DigestAuthMiddleware",
|
||||
"ETag",
|
||||
"set_zlib_backend",
|
||||
# http
|
||||
"HttpVersion",
|
||||
"HttpVersion10",
|
||||
"HttpVersion11",
|
||||
"WSMsgType",
|
||||
"WSCloseCode",
|
||||
"WSMessage",
|
||||
"WebSocketError",
|
||||
# multipart
|
||||
"BadContentDispositionHeader",
|
||||
"BadContentDispositionParam",
|
||||
"BodyPartReader",
|
||||
"MultipartReader",
|
||||
"MultipartWriter",
|
||||
"content_disposition_filename",
|
||||
"parse_content_disposition",
|
||||
# payload
|
||||
"AsyncIterablePayload",
|
||||
"BufferedReaderPayload",
|
||||
"BytesIOPayload",
|
||||
"BytesPayload",
|
||||
"IOBasePayload",
|
||||
"JsonPayload",
|
||||
"PAYLOAD_REGISTRY",
|
||||
"Payload",
|
||||
"StringIOPayload",
|
||||
"StringPayload",
|
||||
"TextIOPayload",
|
||||
"get_payload",
|
||||
"payload_type",
|
||||
# payload_streamer
|
||||
"streamer",
|
||||
# resolver
|
||||
"AsyncResolver",
|
||||
"DefaultResolver",
|
||||
"ThreadedResolver",
|
||||
# streams
|
||||
"DataQueue",
|
||||
"EMPTY_PAYLOAD",
|
||||
"EofStream",
|
||||
"StreamReader",
|
||||
# tracing
|
||||
"TraceConfig",
|
||||
"TraceConnectionCreateEndParams",
|
||||
"TraceConnectionCreateStartParams",
|
||||
"TraceConnectionQueuedEndParams",
|
||||
"TraceConnectionQueuedStartParams",
|
||||
"TraceConnectionReuseconnParams",
|
||||
"TraceDnsCacheHitParams",
|
||||
"TraceDnsCacheMissParams",
|
||||
"TraceDnsResolveHostEndParams",
|
||||
"TraceDnsResolveHostStartParams",
|
||||
"TraceRequestChunkSentParams",
|
||||
"TraceRequestEndParams",
|
||||
"TraceRequestExceptionParams",
|
||||
"TraceRequestHeadersSentParams",
|
||||
"TraceRequestRedirectParams",
|
||||
"TraceRequestStartParams",
|
||||
"TraceResponseChunkReceivedParams",
|
||||
# workers (imported lazily with __getattr__)
|
||||
"GunicornUVLoopWebWorker",
|
||||
"GunicornWebWorker",
|
||||
"WSMessageTypeError",
|
||||
)
|
||||
|
||||
|
||||
def __dir__() -> Tuple[str, ...]:
|
||||
return __all__ + ("__doc__",)
|
||||
|
||||
|
||||
def __getattr__(name: str) -> object:
|
||||
global GunicornUVLoopWebWorker, GunicornWebWorker
|
||||
|
||||
# Importing gunicorn takes a long time (>100ms), so only import if actually needed.
|
||||
if name in ("GunicornUVLoopWebWorker", "GunicornWebWorker"):
|
||||
try:
|
||||
from .worker import GunicornUVLoopWebWorker as guv, GunicornWebWorker as gw
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
GunicornUVLoopWebWorker = guv # type: ignore[misc]
|
||||
GunicornWebWorker = gw # type: ignore[misc]
|
||||
return guv if name == "GunicornUVLoopWebWorker" else gw
|
||||
|
||||
raise AttributeError(f"module {__name__} has no attribute {name}")
|
||||
338
venv/lib/python3.12/site-packages/aiohttp/_cookie_helpers.py
Normal file
338
venv/lib/python3.12/site-packages/aiohttp/_cookie_helpers.py
Normal file
@@ -0,0 +1,338 @@
|
||||
"""
|
||||
Internal cookie handling helpers.
|
||||
|
||||
This module contains internal utilities for cookie parsing and manipulation.
|
||||
These are not part of the public API and may change without notice.
|
||||
"""
|
||||
|
||||
import re
|
||||
from http.cookies import Morsel
|
||||
from typing import List, Optional, Sequence, Tuple, cast
|
||||
|
||||
from .log import internal_logger
|
||||
|
||||
__all__ = (
|
||||
"parse_set_cookie_headers",
|
||||
"parse_cookie_header",
|
||||
"preserve_morsel_with_coded_value",
|
||||
)
|
||||
|
||||
# Cookie parsing constants
|
||||
# Allow more characters in cookie names to handle real-world cookies
|
||||
# that don't strictly follow RFC standards (fixes #2683)
|
||||
# RFC 6265 defines cookie-name token as per RFC 2616 Section 2.2,
|
||||
# but many servers send cookies with characters like {} [] () etc.
|
||||
# This makes the cookie parser more tolerant of real-world cookies
|
||||
# while still providing some validation to catch obviously malformed names.
|
||||
_COOKIE_NAME_RE = re.compile(r"^[!#$%&\'()*+\-./0-9:<=>?@A-Z\[\]^_`a-z{|}~]+$")
|
||||
_COOKIE_KNOWN_ATTRS = frozenset( # AKA Morsel._reserved
|
||||
(
|
||||
"path",
|
||||
"domain",
|
||||
"max-age",
|
||||
"expires",
|
||||
"secure",
|
||||
"httponly",
|
||||
"samesite",
|
||||
"partitioned",
|
||||
"version",
|
||||
"comment",
|
||||
)
|
||||
)
|
||||
_COOKIE_BOOL_ATTRS = frozenset( # AKA Morsel._flags
|
||||
("secure", "httponly", "partitioned")
|
||||
)
|
||||
|
||||
# SimpleCookie's pattern for parsing cookies with relaxed validation
|
||||
# Based on http.cookies pattern but extended to allow more characters in cookie names
|
||||
# to handle real-world cookies (fixes #2683)
|
||||
_COOKIE_PATTERN = re.compile(
|
||||
r"""
|
||||
\s* # Optional whitespace at start of cookie
|
||||
(?P<key> # Start of group 'key'
|
||||
# aiohttp has extended to include [] for compatibility with real-world cookies
|
||||
[\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\[\]]+ # Any word of at least one letter
|
||||
) # End of group 'key'
|
||||
( # Optional group: there may not be a value.
|
||||
\s*=\s* # Equal Sign
|
||||
(?P<val> # Start of group 'val'
|
||||
"(?:[^\\"]|\\.)*" # Any double-quoted string (properly closed)
|
||||
| # or
|
||||
"[^";]* # Unmatched opening quote (differs from SimpleCookie - issue #7993)
|
||||
| # or
|
||||
# Special case for "expires" attr - RFC 822, RFC 850, RFC 1036, RFC 1123
|
||||
(\w{3,6}day|\w{3}),\s # Day of the week or abbreviated day (with comma)
|
||||
[\w\d\s-]{9,11}\s[\d:]{8}\s # Date and time in specific format
|
||||
(GMT|[+-]\d{4}) # Timezone: GMT or RFC 2822 offset like -0000, +0100
|
||||
# NOTE: RFC 2822 timezone support is an aiohttp extension
|
||||
# for issue #4493 - SimpleCookie does NOT support this
|
||||
| # or
|
||||
# ANSI C asctime() format: "Wed Jun 9 10:18:14 2021"
|
||||
# NOTE: This is an aiohttp extension for issue #4327 - SimpleCookie does NOT support this format
|
||||
\w{3}\s+\w{3}\s+[\s\d]\d\s+\d{2}:\d{2}:\d{2}\s+\d{4}
|
||||
| # or
|
||||
[\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=\[\]]* # Any word or empty string
|
||||
) # End of group 'val'
|
||||
)? # End of optional value group
|
||||
\s* # Any number of spaces.
|
||||
(\s+|;|$) # Ending either at space, semicolon, or EOS.
|
||||
""",
|
||||
re.VERBOSE | re.ASCII,
|
||||
)
|
||||
|
||||
|
||||
def preserve_morsel_with_coded_value(cookie: Morsel[str]) -> Morsel[str]:
|
||||
"""
|
||||
Preserve a Morsel's coded_value exactly as received from the server.
|
||||
|
||||
This function ensures that cookie encoding is preserved exactly as sent by
|
||||
the server, which is critical for compatibility with old servers that have
|
||||
strict requirements about cookie formats.
|
||||
|
||||
This addresses the issue described in https://github.com/aio-libs/aiohttp/pull/1453
|
||||
where Python's SimpleCookie would re-encode cookies, breaking authentication
|
||||
with certain servers.
|
||||
|
||||
Args:
|
||||
cookie: A Morsel object from SimpleCookie
|
||||
|
||||
Returns:
|
||||
A Morsel object with preserved coded_value
|
||||
|
||||
"""
|
||||
mrsl_val = cast("Morsel[str]", cookie.get(cookie.key, Morsel()))
|
||||
# We use __setstate__ instead of the public set() API because it allows us to
|
||||
# bypass validation and set already validated state. This is more stable than
|
||||
# setting protected attributes directly and unlikely to change since it would
|
||||
# break pickling.
|
||||
mrsl_val.__setstate__( # type: ignore[attr-defined]
|
||||
{"key": cookie.key, "value": cookie.value, "coded_value": cookie.coded_value}
|
||||
)
|
||||
return mrsl_val
|
||||
|
||||
|
||||
_unquote_sub = re.compile(r"\\(?:([0-3][0-7][0-7])|(.))").sub
|
||||
|
||||
|
||||
def _unquote_replace(m: re.Match[str]) -> str:
|
||||
"""
|
||||
Replace function for _unquote_sub regex substitution.
|
||||
|
||||
Handles escaped characters in cookie values:
|
||||
- Octal sequences are converted to their character representation
|
||||
- Other escaped characters are unescaped by removing the backslash
|
||||
"""
|
||||
if m[1]:
|
||||
return chr(int(m[1], 8))
|
||||
return m[2]
|
||||
|
||||
|
||||
def _unquote(value: str) -> str:
|
||||
"""
|
||||
Unquote a cookie value.
|
||||
|
||||
Vendored from http.cookies._unquote to ensure compatibility.
|
||||
|
||||
Note: The original implementation checked for None, but we've removed
|
||||
that check since all callers already ensure the value is not None.
|
||||
"""
|
||||
# If there aren't any doublequotes,
|
||||
# then there can't be any special characters. See RFC 2109.
|
||||
if len(value) < 2:
|
||||
return value
|
||||
if value[0] != '"' or value[-1] != '"':
|
||||
return value
|
||||
|
||||
# We have to assume that we must decode this string.
|
||||
# Down to work.
|
||||
|
||||
# Remove the "s
|
||||
value = value[1:-1]
|
||||
|
||||
# Check for special sequences. Examples:
|
||||
# \012 --> \n
|
||||
# \" --> "
|
||||
#
|
||||
return _unquote_sub(_unquote_replace, value)
|
||||
|
||||
|
||||
def parse_cookie_header(header: str) -> List[Tuple[str, Morsel[str]]]:
|
||||
"""
|
||||
Parse a Cookie header according to RFC 6265 Section 5.4.
|
||||
|
||||
Cookie headers contain only name-value pairs separated by semicolons.
|
||||
There are no attributes in Cookie headers - even names that match
|
||||
attribute names (like 'path' or 'secure') should be treated as cookies.
|
||||
|
||||
This parser uses the same regex-based approach as parse_set_cookie_headers
|
||||
to properly handle quoted values that may contain semicolons. When the
|
||||
regex fails to match a malformed cookie, it falls back to simple parsing
|
||||
to ensure subsequent cookies are not lost
|
||||
https://github.com/aio-libs/aiohttp/issues/11632
|
||||
|
||||
Args:
|
||||
header: The Cookie header value to parse
|
||||
|
||||
Returns:
|
||||
List of (name, Morsel) tuples for compatibility with SimpleCookie.update()
|
||||
"""
|
||||
if not header:
|
||||
return []
|
||||
|
||||
cookies: List[Tuple[str, Morsel[str]]] = []
|
||||
morsel: Morsel[str]
|
||||
i = 0
|
||||
n = len(header)
|
||||
|
||||
invalid_names = []
|
||||
while i < n:
|
||||
# Use the same pattern as parse_set_cookie_headers to find cookies
|
||||
match = _COOKIE_PATTERN.match(header, i)
|
||||
if not match:
|
||||
# Fallback for malformed cookies https://github.com/aio-libs/aiohttp/issues/11632
|
||||
# Find next semicolon to skip or attempt simple key=value parsing
|
||||
next_semi = header.find(";", i)
|
||||
eq_pos = header.find("=", i)
|
||||
|
||||
# Try to extract key=value if '=' comes before ';'
|
||||
if eq_pos != -1 and (next_semi == -1 or eq_pos < next_semi):
|
||||
end_pos = next_semi if next_semi != -1 else n
|
||||
key = header[i:eq_pos].strip()
|
||||
value = header[eq_pos + 1 : end_pos].strip()
|
||||
|
||||
# Validate the name (same as regex path)
|
||||
if not _COOKIE_NAME_RE.match(key):
|
||||
invalid_names.append(key)
|
||||
else:
|
||||
morsel = Morsel()
|
||||
morsel.__setstate__( # type: ignore[attr-defined]
|
||||
{"key": key, "value": _unquote(value), "coded_value": value}
|
||||
)
|
||||
cookies.append((key, morsel))
|
||||
|
||||
# Move to next cookie or end
|
||||
i = next_semi + 1 if next_semi != -1 else n
|
||||
continue
|
||||
|
||||
key = match.group("key")
|
||||
value = match.group("val") or ""
|
||||
i = match.end(0)
|
||||
|
||||
# Validate the name
|
||||
if not key or not _COOKIE_NAME_RE.match(key):
|
||||
invalid_names.append(key)
|
||||
continue
|
||||
|
||||
# Create new morsel
|
||||
morsel = Morsel()
|
||||
# Preserve the original value as coded_value (with quotes if present)
|
||||
# We use __setstate__ instead of the public set() API because it allows us to
|
||||
# bypass validation and set already validated state. This is more stable than
|
||||
# setting protected attributes directly and unlikely to change since it would
|
||||
# break pickling.
|
||||
morsel.__setstate__( # type: ignore[attr-defined]
|
||||
{"key": key, "value": _unquote(value), "coded_value": value}
|
||||
)
|
||||
|
||||
cookies.append((key, morsel))
|
||||
|
||||
if invalid_names:
|
||||
internal_logger.debug(
|
||||
"Cannot load cookie. Illegal cookie names: %r", invalid_names
|
||||
)
|
||||
|
||||
return cookies
|
||||
|
||||
|
||||
def parse_set_cookie_headers(headers: Sequence[str]) -> List[Tuple[str, Morsel[str]]]:
|
||||
"""
|
||||
Parse cookie headers using a vendored version of SimpleCookie parsing.
|
||||
|
||||
This implementation is based on SimpleCookie.__parse_string to ensure
|
||||
compatibility with how SimpleCookie parses cookies, including handling
|
||||
of malformed cookies with missing semicolons.
|
||||
|
||||
This function is used for both Cookie and Set-Cookie headers in order to be
|
||||
forgiving. Ideally we would have followed RFC 6265 Section 5.2 (for Cookie
|
||||
headers) and RFC 6265 Section 4.2.1 (for Set-Cookie headers), but the
|
||||
real world data makes it impossible since we need to be a bit more forgiving.
|
||||
|
||||
NOTE: This implementation differs from SimpleCookie in handling unmatched quotes.
|
||||
SimpleCookie will stop parsing when it encounters a cookie value with an unmatched
|
||||
quote (e.g., 'cookie="value'), causing subsequent cookies to be silently dropped.
|
||||
This implementation handles unmatched quotes more gracefully to prevent cookie loss.
|
||||
See https://github.com/aio-libs/aiohttp/issues/7993
|
||||
"""
|
||||
parsed_cookies: List[Tuple[str, Morsel[str]]] = []
|
||||
|
||||
for header in headers:
|
||||
if not header:
|
||||
continue
|
||||
|
||||
# Parse cookie string using SimpleCookie's algorithm
|
||||
i = 0
|
||||
n = len(header)
|
||||
current_morsel: Optional[Morsel[str]] = None
|
||||
morsel_seen = False
|
||||
|
||||
while 0 <= i < n:
|
||||
# Start looking for a cookie
|
||||
match = _COOKIE_PATTERN.match(header, i)
|
||||
if not match:
|
||||
# No more cookies
|
||||
break
|
||||
|
||||
key, value = match.group("key"), match.group("val")
|
||||
i = match.end(0)
|
||||
lower_key = key.lower()
|
||||
|
||||
if key[0] == "$":
|
||||
if not morsel_seen:
|
||||
# We ignore attributes which pertain to the cookie
|
||||
# mechanism as a whole, such as "$Version".
|
||||
continue
|
||||
# Process as attribute
|
||||
if current_morsel is not None:
|
||||
attr_lower_key = lower_key[1:]
|
||||
if attr_lower_key in _COOKIE_KNOWN_ATTRS:
|
||||
current_morsel[attr_lower_key] = value or ""
|
||||
elif lower_key in _COOKIE_KNOWN_ATTRS:
|
||||
if not morsel_seen:
|
||||
# Invalid cookie string - attribute before cookie
|
||||
break
|
||||
if lower_key in _COOKIE_BOOL_ATTRS:
|
||||
# Boolean attribute with any value should be True
|
||||
if current_morsel is not None and current_morsel.isReservedKey(key):
|
||||
current_morsel[lower_key] = True
|
||||
elif value is None:
|
||||
# Invalid cookie string - non-boolean attribute without value
|
||||
break
|
||||
elif current_morsel is not None:
|
||||
# Regular attribute with value
|
||||
current_morsel[lower_key] = _unquote(value)
|
||||
elif value is not None:
|
||||
# This is a cookie name=value pair
|
||||
# Validate the name
|
||||
if key in _COOKIE_KNOWN_ATTRS or not _COOKIE_NAME_RE.match(key):
|
||||
internal_logger.warning(
|
||||
"Can not load cookies: Illegal cookie name %r", key
|
||||
)
|
||||
current_morsel = None
|
||||
else:
|
||||
# Create new morsel
|
||||
current_morsel = Morsel()
|
||||
# Preserve the original value as coded_value (with quotes if present)
|
||||
# We use __setstate__ instead of the public set() API because it allows us to
|
||||
# bypass validation and set already validated state. This is more stable than
|
||||
# setting protected attributes directly and unlikely to change since it would
|
||||
# break pickling.
|
||||
current_morsel.__setstate__( # type: ignore[attr-defined]
|
||||
{"key": key, "value": _unquote(value), "coded_value": value}
|
||||
)
|
||||
parsed_cookies.append((key, current_morsel))
|
||||
morsel_seen = True
|
||||
else:
|
||||
# Invalid cookie string - no value for non-attribute
|
||||
break
|
||||
|
||||
return parsed_cookies
|
||||
158
venv/lib/python3.12/site-packages/aiohttp/_cparser.pxd
Normal file
158
venv/lib/python3.12/site-packages/aiohttp/_cparser.pxd
Normal file
@@ -0,0 +1,158 @@
|
||||
from libc.stdint cimport int32_t, uint8_t, uint16_t, uint64_t
|
||||
|
||||
|
||||
cdef extern from "llhttp.h":
|
||||
|
||||
struct llhttp__internal_s:
|
||||
int32_t _index
|
||||
void* _span_pos0
|
||||
void* _span_cb0
|
||||
int32_t error
|
||||
const char* reason
|
||||
const char* error_pos
|
||||
void* data
|
||||
void* _current
|
||||
uint64_t content_length
|
||||
uint8_t type
|
||||
uint8_t method
|
||||
uint8_t http_major
|
||||
uint8_t http_minor
|
||||
uint8_t header_state
|
||||
uint8_t lenient_flags
|
||||
uint8_t upgrade
|
||||
uint8_t finish
|
||||
uint16_t flags
|
||||
uint16_t status_code
|
||||
void* settings
|
||||
|
||||
ctypedef llhttp__internal_s llhttp__internal_t
|
||||
ctypedef llhttp__internal_t llhttp_t
|
||||
|
||||
ctypedef int (*llhttp_data_cb)(llhttp_t*, const char *at, size_t length) except -1
|
||||
ctypedef int (*llhttp_cb)(llhttp_t*) except -1
|
||||
|
||||
struct llhttp_settings_s:
|
||||
llhttp_cb on_message_begin
|
||||
llhttp_data_cb on_url
|
||||
llhttp_data_cb on_status
|
||||
llhttp_data_cb on_header_field
|
||||
llhttp_data_cb on_header_value
|
||||
llhttp_cb on_headers_complete
|
||||
llhttp_data_cb on_body
|
||||
llhttp_cb on_message_complete
|
||||
llhttp_cb on_chunk_header
|
||||
llhttp_cb on_chunk_complete
|
||||
|
||||
llhttp_cb on_url_complete
|
||||
llhttp_cb on_status_complete
|
||||
llhttp_cb on_header_field_complete
|
||||
llhttp_cb on_header_value_complete
|
||||
|
||||
ctypedef llhttp_settings_s llhttp_settings_t
|
||||
|
||||
enum llhttp_errno:
|
||||
HPE_OK,
|
||||
HPE_INTERNAL,
|
||||
HPE_STRICT,
|
||||
HPE_LF_EXPECTED,
|
||||
HPE_UNEXPECTED_CONTENT_LENGTH,
|
||||
HPE_CLOSED_CONNECTION,
|
||||
HPE_INVALID_METHOD,
|
||||
HPE_INVALID_URL,
|
||||
HPE_INVALID_CONSTANT,
|
||||
HPE_INVALID_VERSION,
|
||||
HPE_INVALID_HEADER_TOKEN,
|
||||
HPE_INVALID_CONTENT_LENGTH,
|
||||
HPE_INVALID_CHUNK_SIZE,
|
||||
HPE_INVALID_STATUS,
|
||||
HPE_INVALID_EOF_STATE,
|
||||
HPE_INVALID_TRANSFER_ENCODING,
|
||||
HPE_CB_MESSAGE_BEGIN,
|
||||
HPE_CB_HEADERS_COMPLETE,
|
||||
HPE_CB_MESSAGE_COMPLETE,
|
||||
HPE_CB_CHUNK_HEADER,
|
||||
HPE_CB_CHUNK_COMPLETE,
|
||||
HPE_PAUSED,
|
||||
HPE_PAUSED_UPGRADE,
|
||||
HPE_USER
|
||||
|
||||
ctypedef llhttp_errno llhttp_errno_t
|
||||
|
||||
enum llhttp_flags:
|
||||
F_CHUNKED,
|
||||
F_CONTENT_LENGTH
|
||||
|
||||
enum llhttp_type:
|
||||
HTTP_REQUEST,
|
||||
HTTP_RESPONSE,
|
||||
HTTP_BOTH
|
||||
|
||||
enum llhttp_method:
|
||||
HTTP_DELETE,
|
||||
HTTP_GET,
|
||||
HTTP_HEAD,
|
||||
HTTP_POST,
|
||||
HTTP_PUT,
|
||||
HTTP_CONNECT,
|
||||
HTTP_OPTIONS,
|
||||
HTTP_TRACE,
|
||||
HTTP_COPY,
|
||||
HTTP_LOCK,
|
||||
HTTP_MKCOL,
|
||||
HTTP_MOVE,
|
||||
HTTP_PROPFIND,
|
||||
HTTP_PROPPATCH,
|
||||
HTTP_SEARCH,
|
||||
HTTP_UNLOCK,
|
||||
HTTP_BIND,
|
||||
HTTP_REBIND,
|
||||
HTTP_UNBIND,
|
||||
HTTP_ACL,
|
||||
HTTP_REPORT,
|
||||
HTTP_MKACTIVITY,
|
||||
HTTP_CHECKOUT,
|
||||
HTTP_MERGE,
|
||||
HTTP_MSEARCH,
|
||||
HTTP_NOTIFY,
|
||||
HTTP_SUBSCRIBE,
|
||||
HTTP_UNSUBSCRIBE,
|
||||
HTTP_PATCH,
|
||||
HTTP_PURGE,
|
||||
HTTP_MKCALENDAR,
|
||||
HTTP_LINK,
|
||||
HTTP_UNLINK,
|
||||
HTTP_SOURCE,
|
||||
HTTP_PRI,
|
||||
HTTP_DESCRIBE,
|
||||
HTTP_ANNOUNCE,
|
||||
HTTP_SETUP,
|
||||
HTTP_PLAY,
|
||||
HTTP_PAUSE,
|
||||
HTTP_TEARDOWN,
|
||||
HTTP_GET_PARAMETER,
|
||||
HTTP_SET_PARAMETER,
|
||||
HTTP_REDIRECT,
|
||||
HTTP_RECORD,
|
||||
HTTP_FLUSH
|
||||
|
||||
ctypedef llhttp_method llhttp_method_t;
|
||||
|
||||
void llhttp_settings_init(llhttp_settings_t* settings)
|
||||
void llhttp_init(llhttp_t* parser, llhttp_type type,
|
||||
const llhttp_settings_t* settings)
|
||||
|
||||
llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len)
|
||||
|
||||
int llhttp_should_keep_alive(const llhttp_t* parser)
|
||||
|
||||
void llhttp_resume_after_upgrade(llhttp_t* parser)
|
||||
|
||||
llhttp_errno_t llhttp_get_errno(const llhttp_t* parser)
|
||||
const char* llhttp_get_error_reason(const llhttp_t* parser)
|
||||
const char* llhttp_get_error_pos(const llhttp_t* parser)
|
||||
|
||||
const char* llhttp_method_name(llhttp_method_t method)
|
||||
|
||||
void llhttp_set_lenient_headers(llhttp_t* parser, int enabled)
|
||||
void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled)
|
||||
void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled)
|
||||
@@ -0,0 +1,2 @@
|
||||
cdef extern from "_find_header.h":
|
||||
int find_header(char *, int)
|
||||
83
venv/lib/python3.12/site-packages/aiohttp/_headers.pxi
Normal file
83
venv/lib/python3.12/site-packages/aiohttp/_headers.pxi
Normal file
@@ -0,0 +1,83 @@
|
||||
# The file is autogenerated from aiohttp/hdrs.py
|
||||
# Run ./tools/gen.py to update it after the origin changing.
|
||||
|
||||
from . import hdrs
|
||||
cdef tuple headers = (
|
||||
hdrs.ACCEPT,
|
||||
hdrs.ACCEPT_CHARSET,
|
||||
hdrs.ACCEPT_ENCODING,
|
||||
hdrs.ACCEPT_LANGUAGE,
|
||||
hdrs.ACCEPT_RANGES,
|
||||
hdrs.ACCESS_CONTROL_ALLOW_CREDENTIALS,
|
||||
hdrs.ACCESS_CONTROL_ALLOW_HEADERS,
|
||||
hdrs.ACCESS_CONTROL_ALLOW_METHODS,
|
||||
hdrs.ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||
hdrs.ACCESS_CONTROL_EXPOSE_HEADERS,
|
||||
hdrs.ACCESS_CONTROL_MAX_AGE,
|
||||
hdrs.ACCESS_CONTROL_REQUEST_HEADERS,
|
||||
hdrs.ACCESS_CONTROL_REQUEST_METHOD,
|
||||
hdrs.AGE,
|
||||
hdrs.ALLOW,
|
||||
hdrs.AUTHORIZATION,
|
||||
hdrs.CACHE_CONTROL,
|
||||
hdrs.CONNECTION,
|
||||
hdrs.CONTENT_DISPOSITION,
|
||||
hdrs.CONTENT_ENCODING,
|
||||
hdrs.CONTENT_LANGUAGE,
|
||||
hdrs.CONTENT_LENGTH,
|
||||
hdrs.CONTENT_LOCATION,
|
||||
hdrs.CONTENT_MD5,
|
||||
hdrs.CONTENT_RANGE,
|
||||
hdrs.CONTENT_TRANSFER_ENCODING,
|
||||
hdrs.CONTENT_TYPE,
|
||||
hdrs.COOKIE,
|
||||
hdrs.DATE,
|
||||
hdrs.DESTINATION,
|
||||
hdrs.DIGEST,
|
||||
hdrs.ETAG,
|
||||
hdrs.EXPECT,
|
||||
hdrs.EXPIRES,
|
||||
hdrs.FORWARDED,
|
||||
hdrs.FROM,
|
||||
hdrs.HOST,
|
||||
hdrs.IF_MATCH,
|
||||
hdrs.IF_MODIFIED_SINCE,
|
||||
hdrs.IF_NONE_MATCH,
|
||||
hdrs.IF_RANGE,
|
||||
hdrs.IF_UNMODIFIED_SINCE,
|
||||
hdrs.KEEP_ALIVE,
|
||||
hdrs.LAST_EVENT_ID,
|
||||
hdrs.LAST_MODIFIED,
|
||||
hdrs.LINK,
|
||||
hdrs.LOCATION,
|
||||
hdrs.MAX_FORWARDS,
|
||||
hdrs.ORIGIN,
|
||||
hdrs.PRAGMA,
|
||||
hdrs.PROXY_AUTHENTICATE,
|
||||
hdrs.PROXY_AUTHORIZATION,
|
||||
hdrs.RANGE,
|
||||
hdrs.REFERER,
|
||||
hdrs.RETRY_AFTER,
|
||||
hdrs.SEC_WEBSOCKET_ACCEPT,
|
||||
hdrs.SEC_WEBSOCKET_EXTENSIONS,
|
||||
hdrs.SEC_WEBSOCKET_KEY,
|
||||
hdrs.SEC_WEBSOCKET_KEY1,
|
||||
hdrs.SEC_WEBSOCKET_PROTOCOL,
|
||||
hdrs.SEC_WEBSOCKET_VERSION,
|
||||
hdrs.SERVER,
|
||||
hdrs.SET_COOKIE,
|
||||
hdrs.TE,
|
||||
hdrs.TRAILER,
|
||||
hdrs.TRANSFER_ENCODING,
|
||||
hdrs.URI,
|
||||
hdrs.UPGRADE,
|
||||
hdrs.USER_AGENT,
|
||||
hdrs.VARY,
|
||||
hdrs.VIA,
|
||||
hdrs.WWW_AUTHENTICATE,
|
||||
hdrs.WANT_DIGEST,
|
||||
hdrs.WARNING,
|
||||
hdrs.X_FORWARDED_FOR,
|
||||
hdrs.X_FORWARDED_HOST,
|
||||
hdrs.X_FORWARDED_PROTO,
|
||||
)
|
||||
Binary file not shown.
877
venv/lib/python3.12/site-packages/aiohttp/_http_parser.pyx
Normal file
877
venv/lib/python3.12/site-packages/aiohttp/_http_parser.pyx
Normal file
@@ -0,0 +1,877 @@
|
||||
# Based on https://github.com/MagicStack/httptools
|
||||
#
|
||||
|
||||
from cpython cimport (
|
||||
Py_buffer,
|
||||
PyBUF_SIMPLE,
|
||||
PyBuffer_Release,
|
||||
PyBytes_AsString,
|
||||
PyBytes_AsStringAndSize,
|
||||
PyObject_GetBuffer,
|
||||
)
|
||||
from cpython.mem cimport PyMem_Free, PyMem_Malloc
|
||||
from libc.limits cimport ULLONG_MAX
|
||||
from libc.string cimport memcpy
|
||||
|
||||
from multidict import CIMultiDict as _CIMultiDict, CIMultiDictProxy as _CIMultiDictProxy
|
||||
from yarl import URL as _URL
|
||||
|
||||
from aiohttp import hdrs
|
||||
from aiohttp.helpers import DEBUG, set_exception
|
||||
|
||||
from .http_exceptions import (
|
||||
BadHttpMessage,
|
||||
BadHttpMethod,
|
||||
BadStatusLine,
|
||||
ContentLengthError,
|
||||
InvalidHeader,
|
||||
InvalidURLError,
|
||||
LineTooLong,
|
||||
PayloadEncodingError,
|
||||
TransferEncodingError,
|
||||
)
|
||||
from .http_parser import DeflateBuffer as _DeflateBuffer
|
||||
from .http_writer import (
|
||||
HttpVersion as _HttpVersion,
|
||||
HttpVersion10 as _HttpVersion10,
|
||||
HttpVersion11 as _HttpVersion11,
|
||||
)
|
||||
from .streams import EMPTY_PAYLOAD as _EMPTY_PAYLOAD, StreamReader as _StreamReader
|
||||
|
||||
cimport cython
|
||||
|
||||
from aiohttp cimport _cparser as cparser
|
||||
|
||||
include "_headers.pxi"
|
||||
|
||||
from aiohttp cimport _find_header
|
||||
|
||||
ALLOWED_UPGRADES = frozenset({"websocket"})
|
||||
DEF DEFAULT_FREELIST_SIZE = 250
|
||||
|
||||
cdef extern from "Python.h":
|
||||
int PyByteArray_Resize(object, Py_ssize_t) except -1
|
||||
Py_ssize_t PyByteArray_Size(object) except -1
|
||||
char* PyByteArray_AsString(object)
|
||||
|
||||
__all__ = ('HttpRequestParser', 'HttpResponseParser',
|
||||
'RawRequestMessage', 'RawResponseMessage')
|
||||
|
||||
cdef object URL = _URL
|
||||
cdef object URL_build = URL.build
|
||||
cdef object CIMultiDict = _CIMultiDict
|
||||
cdef object CIMultiDictProxy = _CIMultiDictProxy
|
||||
cdef object HttpVersion = _HttpVersion
|
||||
cdef object HttpVersion10 = _HttpVersion10
|
||||
cdef object HttpVersion11 = _HttpVersion11
|
||||
cdef object SEC_WEBSOCKET_KEY1 = hdrs.SEC_WEBSOCKET_KEY1
|
||||
cdef object CONTENT_ENCODING = hdrs.CONTENT_ENCODING
|
||||
cdef object EMPTY_PAYLOAD = _EMPTY_PAYLOAD
|
||||
cdef object StreamReader = _StreamReader
|
||||
cdef object DeflateBuffer = _DeflateBuffer
|
||||
cdef bytes EMPTY_BYTES = b""
|
||||
|
||||
# RFC 9110 singleton headers — duplicates are rejected in strict mode.
|
||||
# In lax mode (response parser default), the check is skipped entirely
|
||||
# since real-world servers (e.g. Google APIs, Werkzeug) commonly send
|
||||
# duplicate headers like Content-Type or Server.
|
||||
cdef frozenset SINGLETON_HEADERS = frozenset({
|
||||
hdrs.CONTENT_LENGTH,
|
||||
hdrs.CONTENT_LOCATION,
|
||||
hdrs.CONTENT_RANGE,
|
||||
hdrs.CONTENT_TYPE,
|
||||
hdrs.ETAG,
|
||||
hdrs.HOST,
|
||||
hdrs.MAX_FORWARDS,
|
||||
hdrs.SERVER,
|
||||
hdrs.TRANSFER_ENCODING,
|
||||
hdrs.USER_AGENT,
|
||||
})
|
||||
|
||||
cdef inline object extend(object buf, const char* at, size_t length):
|
||||
cdef Py_ssize_t s
|
||||
cdef char* ptr
|
||||
s = PyByteArray_Size(buf)
|
||||
PyByteArray_Resize(buf, s + length)
|
||||
ptr = PyByteArray_AsString(buf)
|
||||
memcpy(ptr + s, at, length)
|
||||
|
||||
|
||||
DEF METHODS_COUNT = 46;
|
||||
|
||||
cdef list _http_method = []
|
||||
|
||||
for i in range(METHODS_COUNT):
|
||||
_http_method.append(
|
||||
cparser.llhttp_method_name(<cparser.llhttp_method_t> i).decode('ascii'))
|
||||
|
||||
|
||||
cdef inline str http_method_str(int i):
|
||||
if i < METHODS_COUNT:
|
||||
return <str>_http_method[i]
|
||||
else:
|
||||
return "<unknown>"
|
||||
|
||||
cdef inline object find_header(bytes raw_header):
|
||||
cdef Py_ssize_t size
|
||||
cdef char *buf
|
||||
cdef int idx
|
||||
PyBytes_AsStringAndSize(raw_header, &buf, &size)
|
||||
idx = _find_header.find_header(buf, size)
|
||||
if idx == -1:
|
||||
return raw_header.decode('utf-8', 'surrogateescape')
|
||||
return headers[idx]
|
||||
|
||||
|
||||
@cython.freelist(DEFAULT_FREELIST_SIZE)
|
||||
cdef class RawRequestMessage:
|
||||
cdef readonly str method
|
||||
cdef readonly str path
|
||||
cdef readonly object version # HttpVersion
|
||||
cdef readonly object headers # CIMultiDict
|
||||
cdef readonly object raw_headers # tuple
|
||||
cdef readonly object should_close
|
||||
cdef readonly object compression
|
||||
cdef readonly object upgrade
|
||||
cdef readonly object chunked
|
||||
cdef readonly object url # yarl.URL
|
||||
|
||||
def __init__(self, method, path, version, headers, raw_headers,
|
||||
should_close, compression, upgrade, chunked, url):
|
||||
self.method = method
|
||||
self.path = path
|
||||
self.version = version
|
||||
self.headers = headers
|
||||
self.raw_headers = raw_headers
|
||||
self.should_close = should_close
|
||||
self.compression = compression
|
||||
self.upgrade = upgrade
|
||||
self.chunked = chunked
|
||||
self.url = url
|
||||
|
||||
def __repr__(self):
|
||||
info = []
|
||||
info.append(("method", self.method))
|
||||
info.append(("path", self.path))
|
||||
info.append(("version", self.version))
|
||||
info.append(("headers", self.headers))
|
||||
info.append(("raw_headers", self.raw_headers))
|
||||
info.append(("should_close", self.should_close))
|
||||
info.append(("compression", self.compression))
|
||||
info.append(("upgrade", self.upgrade))
|
||||
info.append(("chunked", self.chunked))
|
||||
info.append(("url", self.url))
|
||||
sinfo = ', '.join(name + '=' + repr(val) for name, val in info)
|
||||
return '<RawRequestMessage(' + sinfo + ')>'
|
||||
|
||||
def _replace(self, **dct):
|
||||
cdef RawRequestMessage ret
|
||||
ret = _new_request_message(self.method,
|
||||
self.path,
|
||||
self.version,
|
||||
self.headers,
|
||||
self.raw_headers,
|
||||
self.should_close,
|
||||
self.compression,
|
||||
self.upgrade,
|
||||
self.chunked,
|
||||
self.url)
|
||||
if "method" in dct:
|
||||
ret.method = dct["method"]
|
||||
if "path" in dct:
|
||||
ret.path = dct["path"]
|
||||
if "version" in dct:
|
||||
ret.version = dct["version"]
|
||||
if "headers" in dct:
|
||||
ret.headers = dct["headers"]
|
||||
if "raw_headers" in dct:
|
||||
ret.raw_headers = dct["raw_headers"]
|
||||
if "should_close" in dct:
|
||||
ret.should_close = dct["should_close"]
|
||||
if "compression" in dct:
|
||||
ret.compression = dct["compression"]
|
||||
if "upgrade" in dct:
|
||||
ret.upgrade = dct["upgrade"]
|
||||
if "chunked" in dct:
|
||||
ret.chunked = dct["chunked"]
|
||||
if "url" in dct:
|
||||
ret.url = dct["url"]
|
||||
return ret
|
||||
|
||||
cdef _new_request_message(str method,
|
||||
str path,
|
||||
object version,
|
||||
object headers,
|
||||
object raw_headers,
|
||||
bint should_close,
|
||||
object compression,
|
||||
bint upgrade,
|
||||
bint chunked,
|
||||
object url):
|
||||
cdef RawRequestMessage ret
|
||||
ret = RawRequestMessage.__new__(RawRequestMessage)
|
||||
ret.method = method
|
||||
ret.path = path
|
||||
ret.version = version
|
||||
ret.headers = headers
|
||||
ret.raw_headers = raw_headers
|
||||
ret.should_close = should_close
|
||||
ret.compression = compression
|
||||
ret.upgrade = upgrade
|
||||
ret.chunked = chunked
|
||||
ret.url = url
|
||||
return ret
|
||||
|
||||
|
||||
@cython.freelist(DEFAULT_FREELIST_SIZE)
|
||||
cdef class RawResponseMessage:
|
||||
cdef readonly object version # HttpVersion
|
||||
cdef readonly int code
|
||||
cdef readonly str reason
|
||||
cdef readonly object headers # CIMultiDict
|
||||
cdef readonly object raw_headers # tuple
|
||||
cdef readonly object should_close
|
||||
cdef readonly object compression
|
||||
cdef readonly object upgrade
|
||||
cdef readonly object chunked
|
||||
|
||||
def __init__(self, version, code, reason, headers, raw_headers,
|
||||
should_close, compression, upgrade, chunked):
|
||||
self.version = version
|
||||
self.code = code
|
||||
self.reason = reason
|
||||
self.headers = headers
|
||||
self.raw_headers = raw_headers
|
||||
self.should_close = should_close
|
||||
self.compression = compression
|
||||
self.upgrade = upgrade
|
||||
self.chunked = chunked
|
||||
|
||||
def __repr__(self):
|
||||
info = []
|
||||
info.append(("version", self.version))
|
||||
info.append(("code", self.code))
|
||||
info.append(("reason", self.reason))
|
||||
info.append(("headers", self.headers))
|
||||
info.append(("raw_headers", self.raw_headers))
|
||||
info.append(("should_close", self.should_close))
|
||||
info.append(("compression", self.compression))
|
||||
info.append(("upgrade", self.upgrade))
|
||||
info.append(("chunked", self.chunked))
|
||||
sinfo = ', '.join(name + '=' + repr(val) for name, val in info)
|
||||
return '<RawResponseMessage(' + sinfo + ')>'
|
||||
|
||||
|
||||
cdef _new_response_message(object version,
|
||||
int code,
|
||||
str reason,
|
||||
object headers,
|
||||
object raw_headers,
|
||||
bint should_close,
|
||||
object compression,
|
||||
bint upgrade,
|
||||
bint chunked):
|
||||
cdef RawResponseMessage ret
|
||||
ret = RawResponseMessage.__new__(RawResponseMessage)
|
||||
ret.version = version
|
||||
ret.code = code
|
||||
ret.reason = reason
|
||||
ret.headers = headers
|
||||
ret.raw_headers = raw_headers
|
||||
ret.should_close = should_close
|
||||
ret.compression = compression
|
||||
ret.upgrade = upgrade
|
||||
ret.chunked = chunked
|
||||
return ret
|
||||
|
||||
|
||||
@cython.internal
|
||||
cdef class HttpParser:
|
||||
|
||||
cdef:
|
||||
cparser.llhttp_t* _cparser
|
||||
cparser.llhttp_settings_t* _csettings
|
||||
|
||||
bytes _raw_name
|
||||
object _name
|
||||
bytes _raw_value
|
||||
bint _has_value
|
||||
int _header_name_size
|
||||
|
||||
object _protocol
|
||||
object _loop
|
||||
object _timer
|
||||
|
||||
size_t _max_line_size
|
||||
size_t _max_field_size
|
||||
size_t _max_headers
|
||||
bint _response_with_body
|
||||
bint _read_until_eof
|
||||
bint _lax
|
||||
|
||||
bint _started
|
||||
object _url
|
||||
bytearray _buf
|
||||
str _path
|
||||
str _reason
|
||||
list _headers
|
||||
set _seen_singletons
|
||||
list _raw_headers
|
||||
bint _upgraded
|
||||
list _messages
|
||||
object _payload
|
||||
bint _payload_error
|
||||
object _payload_exception
|
||||
object _last_error
|
||||
bint _auto_decompress
|
||||
int _limit
|
||||
|
||||
str _content_encoding
|
||||
|
||||
Py_buffer py_buf
|
||||
|
||||
def __cinit__(self):
|
||||
self._cparser = <cparser.llhttp_t*> \
|
||||
PyMem_Malloc(sizeof(cparser.llhttp_t))
|
||||
if self._cparser is NULL:
|
||||
raise MemoryError()
|
||||
|
||||
self._csettings = <cparser.llhttp_settings_t*> \
|
||||
PyMem_Malloc(sizeof(cparser.llhttp_settings_t))
|
||||
if self._csettings is NULL:
|
||||
raise MemoryError()
|
||||
|
||||
def __dealloc__(self):
|
||||
PyMem_Free(self._cparser)
|
||||
PyMem_Free(self._csettings)
|
||||
|
||||
cdef _init(
|
||||
self, cparser.llhttp_type mode,
|
||||
object protocol, object loop, int limit,
|
||||
object timer=None,
|
||||
size_t max_line_size=8190, size_t max_headers=128,
|
||||
size_t max_field_size=8190, payload_exception=None,
|
||||
bint response_with_body=True, bint read_until_eof=False,
|
||||
bint auto_decompress=True,
|
||||
):
|
||||
cparser.llhttp_settings_init(self._csettings)
|
||||
cparser.llhttp_init(self._cparser, mode, self._csettings)
|
||||
self._cparser.data = <void*>self
|
||||
self._cparser.content_length = 0
|
||||
|
||||
self._protocol = protocol
|
||||
self._loop = loop
|
||||
self._timer = timer
|
||||
|
||||
self._buf = bytearray()
|
||||
self._payload = None
|
||||
self._payload_error = 0
|
||||
self._payload_exception = payload_exception
|
||||
self._messages = []
|
||||
|
||||
self._raw_name = EMPTY_BYTES
|
||||
self._raw_value = EMPTY_BYTES
|
||||
self._has_value = False
|
||||
self._header_name_size = 0
|
||||
|
||||
self._max_line_size = max_line_size
|
||||
self._max_headers = max_headers
|
||||
self._max_field_size = max_field_size
|
||||
self._response_with_body = response_with_body
|
||||
self._read_until_eof = read_until_eof
|
||||
self._upgraded = False
|
||||
self._auto_decompress = auto_decompress
|
||||
self._content_encoding = None
|
||||
self._lax = False
|
||||
self._seen_singletons = set()
|
||||
|
||||
self._csettings.on_url = cb_on_url
|
||||
self._csettings.on_status = cb_on_status
|
||||
self._csettings.on_header_field = cb_on_header_field
|
||||
self._csettings.on_header_value = cb_on_header_value
|
||||
self._csettings.on_headers_complete = cb_on_headers_complete
|
||||
self._csettings.on_body = cb_on_body
|
||||
self._csettings.on_message_begin = cb_on_message_begin
|
||||
self._csettings.on_message_complete = cb_on_message_complete
|
||||
self._csettings.on_chunk_header = cb_on_chunk_header
|
||||
self._csettings.on_chunk_complete = cb_on_chunk_complete
|
||||
|
||||
self._last_error = None
|
||||
self._limit = limit
|
||||
|
||||
cdef _process_header(self):
|
||||
cdef str value
|
||||
if self._raw_name is not EMPTY_BYTES:
|
||||
name = find_header(self._raw_name)
|
||||
value = self._raw_value.decode('utf-8', 'surrogateescape')
|
||||
|
||||
# reject null bytes in header values - matches the Python parser
|
||||
# check at http_parser.py. llhttp in lenient mode doesn't reject
|
||||
# these itself, so we need to catch them here.
|
||||
# ref: RFC 9110 section 5.5 (CTL chars forbidden in field values)
|
||||
if "\x00" in value:
|
||||
raise InvalidHeader(self._raw_value)
|
||||
|
||||
if not self._lax and name in SINGLETON_HEADERS:
|
||||
if name in self._seen_singletons:
|
||||
raise BadHttpMessage(f"Duplicate '{name}' header found.")
|
||||
self._seen_singletons.add(name)
|
||||
self._headers.append((name, value))
|
||||
if len(self._headers) > self._max_headers:
|
||||
raise BadHttpMessage("Too many headers received")
|
||||
|
||||
if name is CONTENT_ENCODING:
|
||||
self._content_encoding = value
|
||||
|
||||
self._has_value = False
|
||||
self._header_name_size = 0
|
||||
self._raw_headers.append((self._raw_name, self._raw_value))
|
||||
self._raw_name = EMPTY_BYTES
|
||||
self._raw_value = EMPTY_BYTES
|
||||
|
||||
cdef _on_header_field(self, char* at, size_t length):
|
||||
if self._has_value:
|
||||
self._process_header()
|
||||
|
||||
if self._raw_name is EMPTY_BYTES:
|
||||
self._raw_name = at[:length]
|
||||
else:
|
||||
self._raw_name += at[:length]
|
||||
|
||||
cdef _on_header_value(self, char* at, size_t length):
|
||||
if self._raw_value is EMPTY_BYTES:
|
||||
self._raw_value = at[:length]
|
||||
else:
|
||||
self._raw_value += at[:length]
|
||||
self._has_value = True
|
||||
|
||||
cdef _on_headers_complete(self):
|
||||
self._process_header()
|
||||
|
||||
should_close = not cparser.llhttp_should_keep_alive(self._cparser)
|
||||
upgrade = self._cparser.upgrade
|
||||
chunked = self._cparser.flags & cparser.F_CHUNKED
|
||||
|
||||
raw_headers = tuple(self._raw_headers)
|
||||
headers = CIMultiDictProxy(CIMultiDict(self._headers))
|
||||
|
||||
if self._cparser.type == cparser.HTTP_REQUEST:
|
||||
h_upg = headers.get("upgrade", "")
|
||||
allowed = upgrade and h_upg.isascii() and h_upg.lower() in ALLOWED_UPGRADES
|
||||
if allowed or self._cparser.method == cparser.HTTP_CONNECT:
|
||||
self._upgraded = True
|
||||
else:
|
||||
if upgrade and self._cparser.status_code == 101:
|
||||
self._upgraded = True
|
||||
|
||||
# do not support old websocket spec
|
||||
if SEC_WEBSOCKET_KEY1 in headers:
|
||||
raise InvalidHeader(SEC_WEBSOCKET_KEY1)
|
||||
|
||||
encoding = None
|
||||
enc = self._content_encoding
|
||||
if enc is not None:
|
||||
self._content_encoding = None
|
||||
if enc.isascii() and enc.lower() in {"gzip", "deflate", "br", "zstd"}:
|
||||
encoding = enc
|
||||
|
||||
if self._cparser.type == cparser.HTTP_REQUEST:
|
||||
method = http_method_str(self._cparser.method)
|
||||
msg = _new_request_message(
|
||||
method, self._path,
|
||||
self.http_version(), headers, raw_headers,
|
||||
should_close, encoding, upgrade, chunked, self._url)
|
||||
else:
|
||||
msg = _new_response_message(
|
||||
self.http_version(), self._cparser.status_code, self._reason,
|
||||
headers, raw_headers, should_close, encoding,
|
||||
upgrade, chunked)
|
||||
|
||||
if (
|
||||
ULLONG_MAX > self._cparser.content_length > 0 or chunked or
|
||||
self._cparser.method == cparser.HTTP_CONNECT or
|
||||
(self._cparser.status_code >= 199 and
|
||||
self._cparser.content_length == 0 and
|
||||
self._read_until_eof)
|
||||
):
|
||||
payload = StreamReader(
|
||||
self._protocol, timer=self._timer, loop=self._loop,
|
||||
limit=self._limit)
|
||||
else:
|
||||
payload = EMPTY_PAYLOAD
|
||||
|
||||
self._payload = payload
|
||||
if encoding is not None and self._auto_decompress:
|
||||
self._payload = DeflateBuffer(payload, encoding)
|
||||
|
||||
if not self._response_with_body:
|
||||
payload = EMPTY_PAYLOAD
|
||||
|
||||
self._messages.append((msg, payload))
|
||||
|
||||
cdef _on_message_complete(self):
|
||||
self._payload.feed_eof()
|
||||
self._payload = None
|
||||
|
||||
cdef _on_chunk_header(self):
|
||||
self._payload.begin_http_chunk_receiving()
|
||||
|
||||
cdef _on_chunk_complete(self):
|
||||
self._payload.end_http_chunk_receiving()
|
||||
|
||||
cdef object _on_status_complete(self):
|
||||
pass
|
||||
|
||||
cdef inline http_version(self):
|
||||
cdef cparser.llhttp_t* parser = self._cparser
|
||||
|
||||
if parser.http_major == 1:
|
||||
if parser.http_minor == 0:
|
||||
return HttpVersion10
|
||||
elif parser.http_minor == 1:
|
||||
return HttpVersion11
|
||||
|
||||
return HttpVersion(parser.http_major, parser.http_minor)
|
||||
|
||||
### Public API ###
|
||||
|
||||
def feed_eof(self):
|
||||
cdef bytes desc
|
||||
|
||||
if self._payload is not None:
|
||||
if self._cparser.flags & cparser.F_CHUNKED:
|
||||
raise TransferEncodingError(
|
||||
"Not enough data to satisfy transfer length header.")
|
||||
elif self._cparser.flags & cparser.F_CONTENT_LENGTH:
|
||||
raise ContentLengthError(
|
||||
"Not enough data to satisfy content length header.")
|
||||
elif cparser.llhttp_get_errno(self._cparser) != cparser.HPE_OK:
|
||||
desc = cparser.llhttp_get_error_reason(self._cparser)
|
||||
raise PayloadEncodingError(desc.decode('latin-1'))
|
||||
else:
|
||||
self._payload.feed_eof()
|
||||
elif self._started:
|
||||
self._on_headers_complete()
|
||||
if self._messages:
|
||||
return self._messages[-1][0]
|
||||
|
||||
def feed_data(self, data):
|
||||
cdef:
|
||||
size_t data_len
|
||||
size_t nb
|
||||
char* base
|
||||
cdef cparser.llhttp_errno_t errno
|
||||
|
||||
PyObject_GetBuffer(data, &self.py_buf, PyBUF_SIMPLE)
|
||||
# Cache buffer pointer before PyBuffer_Release to avoid use-after-release.
|
||||
base = <char*>self.py_buf.buf
|
||||
data_len = <size_t>self.py_buf.len
|
||||
|
||||
errno = cparser.llhttp_execute(
|
||||
self._cparser,
|
||||
base,
|
||||
data_len)
|
||||
|
||||
if errno is cparser.HPE_PAUSED_UPGRADE:
|
||||
cparser.llhttp_resume_after_upgrade(self._cparser)
|
||||
|
||||
nb = cparser.llhttp_get_error_pos(self._cparser) - base
|
||||
|
||||
PyBuffer_Release(&self.py_buf)
|
||||
|
||||
if errno not in (cparser.HPE_OK, cparser.HPE_PAUSED_UPGRADE):
|
||||
if self._payload_error == 0:
|
||||
if self._last_error is not None:
|
||||
ex = self._last_error
|
||||
self._last_error = None
|
||||
else:
|
||||
after = cparser.llhttp_get_error_pos(self._cparser)
|
||||
before = data[:after - base]
|
||||
after_b = after.split(b"\r\n", 1)[0]
|
||||
before = before.rsplit(b"\r\n", 1)[-1]
|
||||
data = before + after_b
|
||||
pointer = " " * (len(repr(before))-1) + "^"
|
||||
ex = parser_error_from_errno(self._cparser, data, pointer)
|
||||
self._payload = None
|
||||
raise ex
|
||||
|
||||
if self._messages:
|
||||
messages = self._messages
|
||||
self._messages = []
|
||||
else:
|
||||
messages = ()
|
||||
|
||||
if self._upgraded:
|
||||
return messages, True, data[nb:]
|
||||
else:
|
||||
return messages, False, b""
|
||||
|
||||
def set_upgraded(self, val):
|
||||
self._upgraded = val
|
||||
|
||||
|
||||
cdef class HttpRequestParser(HttpParser):
|
||||
|
||||
def __init__(
|
||||
self, protocol, loop, int limit, timer=None,
|
||||
size_t max_line_size=8190, size_t max_headers=128,
|
||||
size_t max_field_size=8190, payload_exception=None,
|
||||
bint response_with_body=True, bint read_until_eof=False,
|
||||
bint auto_decompress=True,
|
||||
):
|
||||
self._init(cparser.HTTP_REQUEST, protocol, loop, limit, timer,
|
||||
max_line_size, max_headers, max_field_size,
|
||||
payload_exception, response_with_body, read_until_eof,
|
||||
auto_decompress)
|
||||
|
||||
cdef object _on_status_complete(self):
|
||||
cdef int idx1, idx2
|
||||
if not self._buf:
|
||||
return
|
||||
self._path = self._buf.decode('utf-8', 'surrogateescape')
|
||||
try:
|
||||
idx3 = len(self._path)
|
||||
if self._cparser.method == cparser.HTTP_CONNECT:
|
||||
# authority-form,
|
||||
# https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3
|
||||
self._url = URL.build(authority=self._path, encoded=True)
|
||||
elif idx3 > 1 and self._path[0] == '/':
|
||||
# origin-form,
|
||||
# https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1
|
||||
idx1 = self._path.find("?")
|
||||
if idx1 == -1:
|
||||
query = ""
|
||||
idx2 = self._path.find("#")
|
||||
if idx2 == -1:
|
||||
path = self._path
|
||||
fragment = ""
|
||||
else:
|
||||
path = self._path[0: idx2]
|
||||
fragment = self._path[idx2+1:]
|
||||
|
||||
else:
|
||||
path = self._path[0:idx1]
|
||||
idx1 += 1
|
||||
idx2 = self._path.find("#", idx1+1)
|
||||
if idx2 == -1:
|
||||
query = self._path[idx1:]
|
||||
fragment = ""
|
||||
else:
|
||||
query = self._path[idx1: idx2]
|
||||
fragment = self._path[idx2+1:]
|
||||
|
||||
self._url = URL.build(
|
||||
path=path,
|
||||
query_string=query,
|
||||
fragment=fragment,
|
||||
encoded=True,
|
||||
)
|
||||
else:
|
||||
# absolute-form for proxy maybe,
|
||||
# https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2
|
||||
self._url = URL(self._path, encoded=True)
|
||||
finally:
|
||||
PyByteArray_Resize(self._buf, 0)
|
||||
|
||||
|
||||
cdef class HttpResponseParser(HttpParser):
|
||||
|
||||
def __init__(
|
||||
self, protocol, loop, int limit, timer=None,
|
||||
size_t max_line_size=8190, size_t max_headers=128,
|
||||
size_t max_field_size=8190, payload_exception=None,
|
||||
bint response_with_body=True, bint read_until_eof=False,
|
||||
bint auto_decompress=True
|
||||
):
|
||||
self._init(cparser.HTTP_RESPONSE, protocol, loop, limit, timer,
|
||||
max_line_size, max_headers, max_field_size,
|
||||
payload_exception, response_with_body, read_until_eof,
|
||||
auto_decompress)
|
||||
# Use strict parsing on dev mode, so users are warned about broken servers.
|
||||
if not DEBUG:
|
||||
cparser.llhttp_set_lenient_headers(self._cparser, 1)
|
||||
cparser.llhttp_set_lenient_optional_cr_before_lf(self._cparser, 1)
|
||||
cparser.llhttp_set_lenient_spaces_after_chunk_size(self._cparser, 1)
|
||||
self._lax = True
|
||||
|
||||
cdef object _on_status_complete(self):
|
||||
if self._buf:
|
||||
self._reason = self._buf.decode('utf-8', 'surrogateescape')
|
||||
PyByteArray_Resize(self._buf, 0)
|
||||
else:
|
||||
self._reason = self._reason or ''
|
||||
|
||||
cdef int cb_on_message_begin(cparser.llhttp_t* parser) except -1:
|
||||
cdef HttpParser pyparser = <HttpParser>parser.data
|
||||
|
||||
pyparser._started = True
|
||||
pyparser._headers = []
|
||||
pyparser._seen_singletons = set()
|
||||
pyparser._raw_headers = []
|
||||
PyByteArray_Resize(pyparser._buf, 0)
|
||||
pyparser._path = None
|
||||
pyparser._reason = None
|
||||
return 0
|
||||
|
||||
|
||||
cdef int cb_on_url(cparser.llhttp_t* parser,
|
||||
const char *at, size_t length) except -1:
|
||||
cdef HttpParser pyparser = <HttpParser>parser.data
|
||||
try:
|
||||
if length > pyparser._max_line_size:
|
||||
status = pyparser._buf + at[:length]
|
||||
raise LineTooLong(status[:100] + b"...", pyparser._max_line_size)
|
||||
extend(pyparser._buf, at, length)
|
||||
except BaseException as ex:
|
||||
pyparser._last_error = ex
|
||||
return -1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
cdef int cb_on_status(cparser.llhttp_t* parser,
|
||||
const char *at, size_t length) except -1:
|
||||
cdef HttpParser pyparser = <HttpParser>parser.data
|
||||
try:
|
||||
if length > pyparser._max_line_size:
|
||||
reason = pyparser._buf + at[:length]
|
||||
raise LineTooLong(reason[:100] + b"...", pyparser._max_line_size)
|
||||
extend(pyparser._buf, at, length)
|
||||
except BaseException as ex:
|
||||
pyparser._last_error = ex
|
||||
return -1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
cdef int cb_on_header_field(cparser.llhttp_t* parser,
|
||||
const char *at, size_t length) except -1:
|
||||
cdef HttpParser pyparser = <HttpParser>parser.data
|
||||
cdef Py_ssize_t size
|
||||
try:
|
||||
pyparser._on_status_complete()
|
||||
size = len(pyparser._raw_name) + length
|
||||
if size > pyparser._max_field_size:
|
||||
name = pyparser._raw_name + at[:length]
|
||||
raise LineTooLong(name[:100] + b"...", pyparser._max_field_size)
|
||||
pyparser._header_name_size = size
|
||||
pyparser._on_header_field(at, length)
|
||||
except BaseException as ex:
|
||||
pyparser._last_error = ex
|
||||
return -1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
cdef int cb_on_header_value(cparser.llhttp_t* parser,
|
||||
const char *at, size_t length) except -1:
|
||||
cdef HttpParser pyparser = <HttpParser>parser.data
|
||||
cdef Py_ssize_t size
|
||||
try:
|
||||
size = len(pyparser._raw_value) + length
|
||||
if pyparser._header_name_size + size > pyparser._max_field_size:
|
||||
value = pyparser._raw_value + at[:length]
|
||||
raise LineTooLong(value[:100] + b"...", pyparser._max_field_size)
|
||||
pyparser._on_header_value(at, length)
|
||||
except BaseException as ex:
|
||||
pyparser._last_error = ex
|
||||
return -1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
cdef int cb_on_headers_complete(cparser.llhttp_t* parser) except -1:
|
||||
cdef HttpParser pyparser = <HttpParser>parser.data
|
||||
try:
|
||||
pyparser._on_status_complete()
|
||||
pyparser._on_headers_complete()
|
||||
except BaseException as exc:
|
||||
pyparser._last_error = exc
|
||||
return -1
|
||||
else:
|
||||
if pyparser._upgraded or pyparser._cparser.method == cparser.HTTP_CONNECT:
|
||||
return 2
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
cdef int cb_on_body(cparser.llhttp_t* parser,
|
||||
const char *at, size_t length) except -1:
|
||||
cdef HttpParser pyparser = <HttpParser>parser.data
|
||||
cdef bytes body = at[:length]
|
||||
try:
|
||||
pyparser._payload.feed_data(body, length)
|
||||
except BaseException as underlying_exc:
|
||||
reraised_exc = underlying_exc
|
||||
if pyparser._payload_exception is not None:
|
||||
reraised_exc = pyparser._payload_exception(str(underlying_exc))
|
||||
|
||||
set_exception(pyparser._payload, reraised_exc, underlying_exc)
|
||||
|
||||
pyparser._payload_error = 1
|
||||
return -1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
cdef int cb_on_message_complete(cparser.llhttp_t* parser) except -1:
|
||||
cdef HttpParser pyparser = <HttpParser>parser.data
|
||||
try:
|
||||
pyparser._started = False
|
||||
pyparser._on_message_complete()
|
||||
except BaseException as exc:
|
||||
pyparser._last_error = exc
|
||||
return -1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
cdef int cb_on_chunk_header(cparser.llhttp_t* parser) except -1:
|
||||
cdef HttpParser pyparser = <HttpParser>parser.data
|
||||
try:
|
||||
pyparser._on_chunk_header()
|
||||
except BaseException as exc:
|
||||
pyparser._last_error = exc
|
||||
return -1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
cdef int cb_on_chunk_complete(cparser.llhttp_t* parser) except -1:
|
||||
cdef HttpParser pyparser = <HttpParser>parser.data
|
||||
try:
|
||||
pyparser._on_chunk_complete()
|
||||
except BaseException as exc:
|
||||
pyparser._last_error = exc
|
||||
return -1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
cdef parser_error_from_errno(cparser.llhttp_t* parser, data, pointer):
|
||||
cdef cparser.llhttp_errno_t errno = cparser.llhttp_get_errno(parser)
|
||||
cdef bytes desc = cparser.llhttp_get_error_reason(parser)
|
||||
|
||||
err_msg = "{}:\n\n {!r}\n {}".format(desc.decode("latin-1"), data, pointer)
|
||||
|
||||
if errno in {cparser.HPE_CB_MESSAGE_BEGIN,
|
||||
cparser.HPE_CB_HEADERS_COMPLETE,
|
||||
cparser.HPE_CB_MESSAGE_COMPLETE,
|
||||
cparser.HPE_CB_CHUNK_HEADER,
|
||||
cparser.HPE_CB_CHUNK_COMPLETE,
|
||||
cparser.HPE_INVALID_CONSTANT,
|
||||
cparser.HPE_INVALID_HEADER_TOKEN,
|
||||
cparser.HPE_INVALID_CONTENT_LENGTH,
|
||||
cparser.HPE_INVALID_CHUNK_SIZE,
|
||||
cparser.HPE_INVALID_EOF_STATE,
|
||||
cparser.HPE_INVALID_TRANSFER_ENCODING}:
|
||||
return BadHttpMessage(err_msg)
|
||||
elif errno == cparser.HPE_INVALID_METHOD:
|
||||
return BadHttpMethod(error=err_msg)
|
||||
elif errno in {cparser.HPE_INVALID_STATUS,
|
||||
cparser.HPE_INVALID_VERSION}:
|
||||
return BadStatusLine(error=err_msg)
|
||||
elif errno == cparser.HPE_INVALID_URL:
|
||||
return InvalidURLError(err_msg)
|
||||
|
||||
return BadHttpMessage(err_msg)
|
||||
Binary file not shown.
162
venv/lib/python3.12/site-packages/aiohttp/_http_writer.pyx
Normal file
162
venv/lib/python3.12/site-packages/aiohttp/_http_writer.pyx
Normal file
@@ -0,0 +1,162 @@
|
||||
from cpython.bytes cimport PyBytes_FromStringAndSize
|
||||
from cpython.exc cimport PyErr_NoMemory
|
||||
from cpython.mem cimport PyMem_Free, PyMem_Malloc, PyMem_Realloc
|
||||
from cpython.object cimport PyObject_Str
|
||||
from libc.stdint cimport uint8_t, uint64_t
|
||||
from libc.string cimport memcpy
|
||||
|
||||
from multidict import istr
|
||||
|
||||
DEF BUF_SIZE = 16 * 1024 # 16KiB
|
||||
|
||||
cdef object _istr = istr
|
||||
|
||||
|
||||
# ----------------- writer ---------------------------
|
||||
|
||||
cdef struct Writer:
|
||||
char *buf
|
||||
Py_ssize_t size
|
||||
Py_ssize_t pos
|
||||
bint heap_allocated
|
||||
|
||||
cdef inline void _init_writer(Writer* writer, char *buf):
|
||||
writer.buf = buf
|
||||
writer.size = BUF_SIZE
|
||||
writer.pos = 0
|
||||
writer.heap_allocated = 0
|
||||
|
||||
|
||||
cdef inline void _release_writer(Writer* writer):
|
||||
if writer.heap_allocated:
|
||||
PyMem_Free(writer.buf)
|
||||
|
||||
|
||||
cdef inline int _write_byte(Writer* writer, uint8_t ch):
|
||||
cdef char * buf
|
||||
cdef Py_ssize_t size
|
||||
|
||||
if writer.pos == writer.size:
|
||||
# reallocate
|
||||
size = writer.size + BUF_SIZE
|
||||
if not writer.heap_allocated:
|
||||
buf = <char*>PyMem_Malloc(size)
|
||||
if buf == NULL:
|
||||
PyErr_NoMemory()
|
||||
return -1
|
||||
memcpy(buf, writer.buf, writer.size)
|
||||
else:
|
||||
buf = <char*>PyMem_Realloc(writer.buf, size)
|
||||
if buf == NULL:
|
||||
PyErr_NoMemory()
|
||||
return -1
|
||||
writer.buf = buf
|
||||
writer.size = size
|
||||
writer.heap_allocated = 1
|
||||
writer.buf[writer.pos] = <char>ch
|
||||
writer.pos += 1
|
||||
return 0
|
||||
|
||||
|
||||
cdef inline int _write_utf8(Writer* writer, Py_UCS4 symbol):
|
||||
cdef uint64_t utf = <uint64_t> symbol
|
||||
|
||||
if utf < 0x80:
|
||||
return _write_byte(writer, <uint8_t>utf)
|
||||
elif utf < 0x800:
|
||||
if _write_byte(writer, <uint8_t>(0xc0 | (utf >> 6))) < 0:
|
||||
return -1
|
||||
return _write_byte(writer, <uint8_t>(0x80 | (utf & 0x3f)))
|
||||
elif 0xD800 <= utf <= 0xDFFF:
|
||||
# surogate pair, ignored
|
||||
return 0
|
||||
elif utf < 0x10000:
|
||||
if _write_byte(writer, <uint8_t>(0xe0 | (utf >> 12))) < 0:
|
||||
return -1
|
||||
if _write_byte(writer, <uint8_t>(0x80 | ((utf >> 6) & 0x3f))) < 0:
|
||||
return -1
|
||||
return _write_byte(writer, <uint8_t>(0x80 | (utf & 0x3f)))
|
||||
elif utf > 0x10FFFF:
|
||||
# symbol is too large
|
||||
return 0
|
||||
else:
|
||||
if _write_byte(writer, <uint8_t>(0xf0 | (utf >> 18))) < 0:
|
||||
return -1
|
||||
if _write_byte(writer,
|
||||
<uint8_t>(0x80 | ((utf >> 12) & 0x3f))) < 0:
|
||||
return -1
|
||||
if _write_byte(writer,
|
||||
<uint8_t>(0x80 | ((utf >> 6) & 0x3f))) < 0:
|
||||
return -1
|
||||
return _write_byte(writer, <uint8_t>(0x80 | (utf & 0x3f)))
|
||||
|
||||
|
||||
cdef inline int _write_str(Writer* writer, str s):
|
||||
cdef Py_UCS4 ch
|
||||
for ch in s:
|
||||
if _write_utf8(writer, ch) < 0:
|
||||
return -1
|
||||
|
||||
|
||||
cdef inline int _write_str_raise_on_nlcr(Writer* writer, object s):
|
||||
cdef Py_UCS4 ch
|
||||
cdef str out_str
|
||||
if type(s) is str:
|
||||
out_str = <str>s
|
||||
elif type(s) is _istr:
|
||||
out_str = PyObject_Str(s)
|
||||
elif not isinstance(s, str):
|
||||
raise TypeError("Cannot serialize non-str key {!r}".format(s))
|
||||
else:
|
||||
out_str = str(s)
|
||||
|
||||
for ch in out_str:
|
||||
if ch in {0x0D, 0x0A, 0x00}:
|
||||
raise ValueError(
|
||||
"Newline, carriage return, or null byte detected in headers. "
|
||||
"Potential header injection attack."
|
||||
)
|
||||
if _write_utf8(writer, ch) < 0:
|
||||
return -1
|
||||
|
||||
|
||||
# --------------- _serialize_headers ----------------------
|
||||
|
||||
def _serialize_headers(str status_line, headers):
|
||||
cdef Writer writer
|
||||
cdef object key
|
||||
cdef object val
|
||||
cdef char buf[BUF_SIZE]
|
||||
|
||||
_init_writer(&writer, buf)
|
||||
|
||||
try:
|
||||
if _write_str_raise_on_nlcr(&writer, status_line) < 0:
|
||||
raise
|
||||
if _write_byte(&writer, b'\r') < 0:
|
||||
raise
|
||||
if _write_byte(&writer, b'\n') < 0:
|
||||
raise
|
||||
|
||||
for key, val in headers.items():
|
||||
if _write_str_raise_on_nlcr(&writer, key) < 0:
|
||||
raise
|
||||
if _write_byte(&writer, b':') < 0:
|
||||
raise
|
||||
if _write_byte(&writer, b' ') < 0:
|
||||
raise
|
||||
if _write_str_raise_on_nlcr(&writer, val) < 0:
|
||||
raise
|
||||
if _write_byte(&writer, b'\r') < 0:
|
||||
raise
|
||||
if _write_byte(&writer, b'\n') < 0:
|
||||
raise
|
||||
|
||||
if _write_byte(&writer, b'\r') < 0:
|
||||
raise
|
||||
if _write_byte(&writer, b'\n') < 0:
|
||||
raise
|
||||
|
||||
return PyBytes_FromStringAndSize(writer.buf, writer.pos)
|
||||
finally:
|
||||
_release_writer(&writer)
|
||||
@@ -0,0 +1 @@
|
||||
b01999d409b29bd916e067bc963d5f2d9ee63cfc9ae0bccb769910131417bf93 /home/runner/work/aiohttp/aiohttp/aiohttp/_websocket/mask.pxd
|
||||
@@ -0,0 +1 @@
|
||||
0478ceb55d0ed30ef1a7da742cd003449bc69a07cf9fdb06789bd2b347cbfffe /home/runner/work/aiohttp/aiohttp/aiohttp/_websocket/mask.pyx
|
||||
@@ -0,0 +1 @@
|
||||
9e5fe78ed0ebce5414d2b8e01868d90c1facc20b84d2d5ff6c23e86e44a155ae /home/runner/work/aiohttp/aiohttp/aiohttp/_websocket/reader_c.pxd
|
||||
@@ -0,0 +1 @@
|
||||
"""WebSocket protocol versions 13 and 8."""
|
||||
147
venv/lib/python3.12/site-packages/aiohttp/_websocket/helpers.py
Normal file
147
venv/lib/python3.12/site-packages/aiohttp/_websocket/helpers.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""Helpers for WebSocket protocol versions 13 and 8."""
|
||||
|
||||
import functools
|
||||
import re
|
||||
from struct import Struct
|
||||
from typing import TYPE_CHECKING, Final, List, Optional, Pattern, Tuple
|
||||
|
||||
from ..helpers import NO_EXTENSIONS
|
||||
from .models import WSHandshakeError
|
||||
|
||||
UNPACK_LEN3 = Struct("!Q").unpack_from
|
||||
UNPACK_CLOSE_CODE = Struct("!H").unpack
|
||||
PACK_LEN1 = Struct("!BB").pack
|
||||
PACK_LEN2 = Struct("!BBH").pack
|
||||
PACK_LEN3 = Struct("!BBQ").pack
|
||||
PACK_CLOSE_CODE = Struct("!H").pack
|
||||
PACK_RANDBITS = Struct("!L").pack
|
||||
MSG_SIZE: Final[int] = 2**14
|
||||
MASK_LEN: Final[int] = 4
|
||||
|
||||
WS_KEY: Final[bytes] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
|
||||
|
||||
# Used by _websocket_mask_python
|
||||
@functools.lru_cache
|
||||
def _xor_table() -> List[bytes]:
|
||||
return [bytes(a ^ b for a in range(256)) for b in range(256)]
|
||||
|
||||
|
||||
def _websocket_mask_python(mask: bytes, data: bytearray) -> None:
|
||||
"""Websocket masking function.
|
||||
|
||||
`mask` is a `bytes` object of length 4; `data` is a `bytearray`
|
||||
object of any length. The contents of `data` are masked with `mask`,
|
||||
as specified in section 5.3 of RFC 6455.
|
||||
|
||||
Note that this function mutates the `data` argument.
|
||||
|
||||
This pure-python implementation may be replaced by an optimized
|
||||
version when available.
|
||||
|
||||
"""
|
||||
assert isinstance(data, bytearray), data
|
||||
assert len(mask) == 4, mask
|
||||
|
||||
if data:
|
||||
_XOR_TABLE = _xor_table()
|
||||
a, b, c, d = (_XOR_TABLE[n] for n in mask)
|
||||
data[::4] = data[::4].translate(a)
|
||||
data[1::4] = data[1::4].translate(b)
|
||||
data[2::4] = data[2::4].translate(c)
|
||||
data[3::4] = data[3::4].translate(d)
|
||||
|
||||
|
||||
if TYPE_CHECKING or NO_EXTENSIONS: # pragma: no cover
|
||||
websocket_mask = _websocket_mask_python
|
||||
else:
|
||||
try:
|
||||
from .mask import _websocket_mask_cython # type: ignore[import-not-found]
|
||||
|
||||
websocket_mask = _websocket_mask_cython
|
||||
except ImportError: # pragma: no cover
|
||||
websocket_mask = _websocket_mask_python
|
||||
|
||||
|
||||
_WS_EXT_RE: Final[Pattern[str]] = re.compile(
|
||||
r"^(?:;\s*(?:"
|
||||
r"(server_no_context_takeover)|"
|
||||
r"(client_no_context_takeover)|"
|
||||
r"(server_max_window_bits(?:=(\d+))?)|"
|
||||
r"(client_max_window_bits(?:=(\d+))?)))*$"
|
||||
)
|
||||
|
||||
_WS_EXT_RE_SPLIT: Final[Pattern[str]] = re.compile(r"permessage-deflate([^,]+)?")
|
||||
|
||||
|
||||
def ws_ext_parse(extstr: Optional[str], isserver: bool = False) -> Tuple[int, bool]:
|
||||
if not extstr:
|
||||
return 0, False
|
||||
|
||||
compress = 0
|
||||
notakeover = False
|
||||
for ext in _WS_EXT_RE_SPLIT.finditer(extstr):
|
||||
defext = ext.group(1)
|
||||
# Return compress = 15 when get `permessage-deflate`
|
||||
if not defext:
|
||||
compress = 15
|
||||
break
|
||||
match = _WS_EXT_RE.match(defext)
|
||||
if match:
|
||||
compress = 15
|
||||
if isserver:
|
||||
# Server never fail to detect compress handshake.
|
||||
# Server does not need to send max wbit to client
|
||||
if match.group(4):
|
||||
compress = int(match.group(4))
|
||||
# Group3 must match if group4 matches
|
||||
# Compress wbit 8 does not support in zlib
|
||||
# If compress level not support,
|
||||
# CONTINUE to next extension
|
||||
if compress > 15 or compress < 9:
|
||||
compress = 0
|
||||
continue
|
||||
if match.group(1):
|
||||
notakeover = True
|
||||
# Ignore regex group 5 & 6 for client_max_window_bits
|
||||
break
|
||||
else:
|
||||
if match.group(6):
|
||||
compress = int(match.group(6))
|
||||
# Group5 must match if group6 matches
|
||||
# Compress wbit 8 does not support in zlib
|
||||
# If compress level not support,
|
||||
# FAIL the parse progress
|
||||
if compress > 15 or compress < 9:
|
||||
raise WSHandshakeError("Invalid window size")
|
||||
if match.group(2):
|
||||
notakeover = True
|
||||
# Ignore regex group 5 & 6 for client_max_window_bits
|
||||
break
|
||||
# Return Fail if client side and not match
|
||||
elif not isserver:
|
||||
raise WSHandshakeError("Extension for deflate not supported" + ext.group(1))
|
||||
|
||||
return compress, notakeover
|
||||
|
||||
|
||||
def ws_ext_gen(
|
||||
compress: int = 15, isserver: bool = False, server_notakeover: bool = False
|
||||
) -> str:
|
||||
# client_notakeover=False not used for server
|
||||
# compress wbit 8 does not support in zlib
|
||||
if compress < 9 or compress > 15:
|
||||
raise ValueError(
|
||||
"Compress wbits must between 9 and 15, zlib does not support wbits=8"
|
||||
)
|
||||
enabledext = ["permessage-deflate"]
|
||||
if not isserver:
|
||||
enabledext.append("client_max_window_bits")
|
||||
|
||||
if compress < 15:
|
||||
enabledext.append("server_max_window_bits=" + str(compress))
|
||||
if server_notakeover:
|
||||
enabledext.append("server_no_context_takeover")
|
||||
# if client_notakeover:
|
||||
# enabledext.append('client_no_context_takeover')
|
||||
return "; ".join(enabledext)
|
||||
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
"""Cython declarations for websocket masking."""
|
||||
|
||||
cpdef void _websocket_mask_cython(bytes mask, bytearray data)
|
||||
@@ -0,0 +1,48 @@
|
||||
from cpython cimport PyBytes_AsString
|
||||
|
||||
|
||||
#from cpython cimport PyByteArray_AsString # cython still not exports that
|
||||
cdef extern from "Python.h":
|
||||
char* PyByteArray_AsString(bytearray ba) except NULL
|
||||
|
||||
from libc.stdint cimport uint32_t, uint64_t, uintmax_t
|
||||
|
||||
|
||||
cpdef void _websocket_mask_cython(bytes mask, bytearray data):
|
||||
"""Note, this function mutates its `data` argument
|
||||
"""
|
||||
cdef:
|
||||
Py_ssize_t data_len, i
|
||||
# bit operations on signed integers are implementation-specific
|
||||
unsigned char * in_buf
|
||||
const unsigned char * mask_buf
|
||||
uint32_t uint32_msk
|
||||
uint64_t uint64_msk
|
||||
|
||||
assert len(mask) == 4
|
||||
|
||||
data_len = len(data)
|
||||
in_buf = <unsigned char*>PyByteArray_AsString(data)
|
||||
mask_buf = <const unsigned char*>PyBytes_AsString(mask)
|
||||
uint32_msk = (<uint32_t*>mask_buf)[0]
|
||||
|
||||
# TODO: align in_data ptr to achieve even faster speeds
|
||||
# does it need in python ?! malloc() always aligns to sizeof(long) bytes
|
||||
|
||||
if sizeof(size_t) >= 8:
|
||||
uint64_msk = uint32_msk
|
||||
uint64_msk = (uint64_msk << 32) | uint32_msk
|
||||
|
||||
while data_len >= 8:
|
||||
(<uint64_t*>in_buf)[0] ^= uint64_msk
|
||||
in_buf += 8
|
||||
data_len -= 8
|
||||
|
||||
|
||||
while data_len >= 4:
|
||||
(<uint32_t*>in_buf)[0] ^= uint32_msk
|
||||
in_buf += 4
|
||||
data_len -= 4
|
||||
|
||||
for i in range(0, data_len):
|
||||
in_buf[i] ^= mask_buf[i]
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Models for WebSocket protocol versions 13 and 8."""
|
||||
|
||||
import json
|
||||
from enum import IntEnum
|
||||
from typing import Any, Callable, Final, NamedTuple, Optional, cast
|
||||
|
||||
WS_DEFLATE_TRAILING: Final[bytes] = bytes([0x00, 0x00, 0xFF, 0xFF])
|
||||
|
||||
|
||||
class WSCloseCode(IntEnum):
|
||||
OK = 1000
|
||||
GOING_AWAY = 1001
|
||||
PROTOCOL_ERROR = 1002
|
||||
UNSUPPORTED_DATA = 1003
|
||||
ABNORMAL_CLOSURE = 1006
|
||||
INVALID_TEXT = 1007
|
||||
POLICY_VIOLATION = 1008
|
||||
MESSAGE_TOO_BIG = 1009
|
||||
MANDATORY_EXTENSION = 1010
|
||||
INTERNAL_ERROR = 1011
|
||||
SERVICE_RESTART = 1012
|
||||
TRY_AGAIN_LATER = 1013
|
||||
BAD_GATEWAY = 1014
|
||||
|
||||
|
||||
class WSMsgType(IntEnum):
|
||||
# websocket spec types
|
||||
CONTINUATION = 0x0
|
||||
TEXT = 0x1
|
||||
BINARY = 0x2
|
||||
PING = 0x9
|
||||
PONG = 0xA
|
||||
CLOSE = 0x8
|
||||
|
||||
# aiohttp specific types
|
||||
CLOSING = 0x100
|
||||
CLOSED = 0x101
|
||||
ERROR = 0x102
|
||||
|
||||
text = TEXT
|
||||
binary = BINARY
|
||||
ping = PING
|
||||
pong = PONG
|
||||
close = CLOSE
|
||||
closing = CLOSING
|
||||
closed = CLOSED
|
||||
error = ERROR
|
||||
|
||||
|
||||
class WSMessage(NamedTuple):
|
||||
type: WSMsgType
|
||||
# To type correctly, this would need some kind of tagged union for each type.
|
||||
data: Any
|
||||
extra: Optional[str]
|
||||
|
||||
def json(self, *, loads: Callable[[Any], Any] = json.loads) -> Any:
|
||||
"""Return parsed JSON data.
|
||||
|
||||
.. versionadded:: 0.22
|
||||
"""
|
||||
return loads(self.data)
|
||||
|
||||
|
||||
# Constructing the tuple directly to avoid the overhead of
|
||||
# the lambda and arg processing since NamedTuples are constructed
|
||||
# with a run time built lambda
|
||||
# https://github.com/python/cpython/blob/d83fcf8371f2f33c7797bc8f5423a8bca8c46e5c/Lib/collections/__init__.py#L441
|
||||
WS_CLOSED_MESSAGE = tuple.__new__(WSMessage, (WSMsgType.CLOSED, None, None))
|
||||
WS_CLOSING_MESSAGE = tuple.__new__(WSMessage, (WSMsgType.CLOSING, None, None))
|
||||
|
||||
|
||||
class WebSocketError(Exception):
|
||||
"""WebSocket protocol parser error."""
|
||||
|
||||
def __init__(self, code: int, message: str) -> None:
|
||||
self.code = code
|
||||
super().__init__(code, message)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return cast(str, self.args[1])
|
||||
|
||||
|
||||
class WSHandshakeError(Exception):
|
||||
"""WebSocket protocol handshake error."""
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Reader for WebSocket protocol versions 13 and 8."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..helpers import NO_EXTENSIONS
|
||||
|
||||
if TYPE_CHECKING or NO_EXTENSIONS: # pragma: no cover
|
||||
from .reader_py import (
|
||||
WebSocketDataQueue as WebSocketDataQueuePython,
|
||||
WebSocketReader as WebSocketReaderPython,
|
||||
)
|
||||
|
||||
WebSocketReader = WebSocketReaderPython
|
||||
WebSocketDataQueue = WebSocketDataQueuePython
|
||||
else:
|
||||
try:
|
||||
from .reader_c import ( # type: ignore[import-not-found]
|
||||
WebSocketDataQueue as WebSocketDataQueueCython,
|
||||
WebSocketReader as WebSocketReaderCython,
|
||||
)
|
||||
|
||||
WebSocketReader = WebSocketReaderCython
|
||||
WebSocketDataQueue = WebSocketDataQueueCython
|
||||
except ImportError: # pragma: no cover
|
||||
from .reader_py import (
|
||||
WebSocketDataQueue as WebSocketDataQueuePython,
|
||||
WebSocketReader as WebSocketReaderPython,
|
||||
)
|
||||
|
||||
WebSocketReader = WebSocketReaderPython
|
||||
WebSocketDataQueue = WebSocketDataQueuePython
|
||||
Binary file not shown.
@@ -0,0 +1,110 @@
|
||||
import cython
|
||||
|
||||
from .mask cimport _websocket_mask_cython as websocket_mask
|
||||
|
||||
|
||||
cdef unsigned int READ_HEADER
|
||||
cdef unsigned int READ_PAYLOAD_LENGTH
|
||||
cdef unsigned int READ_PAYLOAD_MASK
|
||||
cdef unsigned int READ_PAYLOAD
|
||||
|
||||
cdef int OP_CODE_NOT_SET
|
||||
cdef int OP_CODE_CONTINUATION
|
||||
cdef int OP_CODE_TEXT
|
||||
cdef int OP_CODE_BINARY
|
||||
cdef int OP_CODE_CLOSE
|
||||
cdef int OP_CODE_PING
|
||||
cdef int OP_CODE_PONG
|
||||
|
||||
cdef int COMPRESSED_NOT_SET
|
||||
cdef int COMPRESSED_FALSE
|
||||
cdef int COMPRESSED_TRUE
|
||||
|
||||
cdef object UNPACK_LEN3
|
||||
cdef object UNPACK_CLOSE_CODE
|
||||
cdef object TUPLE_NEW
|
||||
|
||||
cdef object WSMsgType
|
||||
cdef object WSMessage
|
||||
|
||||
cdef object WS_MSG_TYPE_TEXT
|
||||
cdef object WS_MSG_TYPE_BINARY
|
||||
|
||||
cdef set ALLOWED_CLOSE_CODES
|
||||
cdef set MESSAGE_TYPES_WITH_CONTENT
|
||||
|
||||
cdef tuple EMPTY_FRAME
|
||||
cdef tuple EMPTY_FRAME_ERROR
|
||||
|
||||
cdef class WebSocketDataQueue:
|
||||
|
||||
cdef unsigned int _size
|
||||
cdef public object _protocol
|
||||
cdef unsigned int _limit
|
||||
cdef object _loop
|
||||
cdef bint _eof
|
||||
cdef object _waiter
|
||||
cdef object _exception
|
||||
cdef public object _buffer
|
||||
cdef object _get_buffer
|
||||
cdef object _put_buffer
|
||||
|
||||
cdef void _release_waiter(self)
|
||||
|
||||
cpdef void feed_data(self, object data, unsigned int size)
|
||||
|
||||
@cython.locals(size="unsigned int")
|
||||
cdef _read_from_buffer(self)
|
||||
|
||||
cdef class WebSocketReader:
|
||||
|
||||
cdef WebSocketDataQueue queue
|
||||
cdef unsigned int _max_msg_size
|
||||
|
||||
cdef Exception _exc
|
||||
cdef bytearray _partial
|
||||
cdef unsigned int _state
|
||||
|
||||
cdef int _opcode
|
||||
cdef bint _frame_fin
|
||||
cdef int _frame_opcode
|
||||
cdef list _payload_fragments
|
||||
cdef Py_ssize_t _frame_payload_len
|
||||
|
||||
cdef bytes _tail
|
||||
cdef bint _has_mask
|
||||
cdef bytes _frame_mask
|
||||
cdef Py_ssize_t _payload_bytes_to_read
|
||||
cdef unsigned int _payload_len_flag
|
||||
cdef int _compressed
|
||||
cdef object _decompressobj
|
||||
cdef bint _compress
|
||||
|
||||
cpdef tuple feed_data(self, object data)
|
||||
|
||||
@cython.locals(
|
||||
is_continuation=bint,
|
||||
fin=bint,
|
||||
has_partial=bint,
|
||||
payload_merged=bytes,
|
||||
)
|
||||
cpdef void _handle_frame(self, bint fin, int opcode, object payload, int compressed) except *
|
||||
|
||||
@cython.locals(
|
||||
start_pos=Py_ssize_t,
|
||||
data_len=Py_ssize_t,
|
||||
length=Py_ssize_t,
|
||||
chunk_size=Py_ssize_t,
|
||||
chunk_len=Py_ssize_t,
|
||||
data_len=Py_ssize_t,
|
||||
data_cstr="const unsigned char *",
|
||||
first_byte="unsigned char",
|
||||
second_byte="unsigned char",
|
||||
f_start_pos=Py_ssize_t,
|
||||
f_end_pos=Py_ssize_t,
|
||||
has_mask=bint,
|
||||
fin=bint,
|
||||
had_fragments=Py_ssize_t,
|
||||
payload_bytearray=bytearray,
|
||||
)
|
||||
cpdef void _feed_data(self, bytes data) except *
|
||||
478
venv/lib/python3.12/site-packages/aiohttp/_websocket/reader_c.py
Normal file
478
venv/lib/python3.12/site-packages/aiohttp/_websocket/reader_c.py
Normal file
@@ -0,0 +1,478 @@
|
||||
"""Reader for WebSocket protocol versions 13 and 8."""
|
||||
|
||||
import asyncio
|
||||
import builtins
|
||||
from collections import deque
|
||||
from typing import Deque, Final, Optional, Set, Tuple, Union
|
||||
|
||||
from ..base_protocol import BaseProtocol
|
||||
from ..compression_utils import ZLibDecompressor
|
||||
from ..helpers import _EXC_SENTINEL, set_exception
|
||||
from ..streams import EofStream
|
||||
from .helpers import UNPACK_CLOSE_CODE, UNPACK_LEN3, websocket_mask
|
||||
from .models import (
|
||||
WS_DEFLATE_TRAILING,
|
||||
WebSocketError,
|
||||
WSCloseCode,
|
||||
WSMessage,
|
||||
WSMsgType,
|
||||
)
|
||||
|
||||
ALLOWED_CLOSE_CODES: Final[Set[int]] = {int(i) for i in WSCloseCode}
|
||||
|
||||
# States for the reader, used to parse the WebSocket frame
|
||||
# integer values are used so they can be cythonized
|
||||
READ_HEADER = 1
|
||||
READ_PAYLOAD_LENGTH = 2
|
||||
READ_PAYLOAD_MASK = 3
|
||||
READ_PAYLOAD = 4
|
||||
|
||||
WS_MSG_TYPE_BINARY = WSMsgType.BINARY
|
||||
WS_MSG_TYPE_TEXT = WSMsgType.TEXT
|
||||
|
||||
# WSMsgType values unpacked so they can by cythonized to ints
|
||||
OP_CODE_NOT_SET = -1
|
||||
OP_CODE_CONTINUATION = WSMsgType.CONTINUATION.value
|
||||
OP_CODE_TEXT = WSMsgType.TEXT.value
|
||||
OP_CODE_BINARY = WSMsgType.BINARY.value
|
||||
OP_CODE_CLOSE = WSMsgType.CLOSE.value
|
||||
OP_CODE_PING = WSMsgType.PING.value
|
||||
OP_CODE_PONG = WSMsgType.PONG.value
|
||||
|
||||
EMPTY_FRAME_ERROR = (True, b"")
|
||||
EMPTY_FRAME = (False, b"")
|
||||
|
||||
COMPRESSED_NOT_SET = -1
|
||||
COMPRESSED_FALSE = 0
|
||||
COMPRESSED_TRUE = 1
|
||||
|
||||
TUPLE_NEW = tuple.__new__
|
||||
|
||||
cython_int = int # Typed to int in Python, but cython with use a signed int in the pxd
|
||||
|
||||
|
||||
class WebSocketDataQueue:
|
||||
"""WebSocketDataQueue resumes and pauses an underlying stream.
|
||||
|
||||
It is a destination for WebSocket data.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, protocol: BaseProtocol, limit: int, *, loop: asyncio.AbstractEventLoop
|
||||
) -> None:
|
||||
self._size = 0
|
||||
self._protocol = protocol
|
||||
self._limit = limit * 2
|
||||
self._loop = loop
|
||||
self._eof = False
|
||||
self._waiter: Optional[asyncio.Future[None]] = None
|
||||
self._exception: Union[BaseException, None] = None
|
||||
self._buffer: Deque[Tuple[WSMessage, int]] = deque()
|
||||
self._get_buffer = self._buffer.popleft
|
||||
self._put_buffer = self._buffer.append
|
||||
|
||||
def is_eof(self) -> bool:
|
||||
return self._eof
|
||||
|
||||
def exception(self) -> Optional[BaseException]:
|
||||
return self._exception
|
||||
|
||||
def set_exception(
|
||||
self,
|
||||
exc: BaseException,
|
||||
exc_cause: builtins.BaseException = _EXC_SENTINEL,
|
||||
) -> None:
|
||||
self._eof = True
|
||||
self._exception = exc
|
||||
if (waiter := self._waiter) is not None:
|
||||
self._waiter = None
|
||||
set_exception(waiter, exc, exc_cause)
|
||||
|
||||
def _release_waiter(self) -> None:
|
||||
if (waiter := self._waiter) is None:
|
||||
return
|
||||
self._waiter = None
|
||||
if not waiter.done():
|
||||
waiter.set_result(None)
|
||||
|
||||
def feed_eof(self) -> None:
|
||||
self._eof = True
|
||||
self._release_waiter()
|
||||
self._exception = None # Break cyclic references
|
||||
|
||||
def feed_data(self, data: "WSMessage", size: "cython_int") -> None:
|
||||
self._size += size
|
||||
self._put_buffer((data, size))
|
||||
self._release_waiter()
|
||||
if self._size > self._limit and not self._protocol._reading_paused:
|
||||
self._protocol.pause_reading()
|
||||
|
||||
async def read(self) -> WSMessage:
|
||||
if not self._buffer and not self._eof:
|
||||
assert not self._waiter
|
||||
self._waiter = self._loop.create_future()
|
||||
try:
|
||||
await self._waiter
|
||||
except (asyncio.CancelledError, asyncio.TimeoutError):
|
||||
self._waiter = None
|
||||
raise
|
||||
return self._read_from_buffer()
|
||||
|
||||
def _read_from_buffer(self) -> WSMessage:
|
||||
if self._buffer:
|
||||
data, size = self._get_buffer()
|
||||
self._size -= size
|
||||
if self._size < self._limit and self._protocol._reading_paused:
|
||||
self._protocol.resume_reading()
|
||||
return data
|
||||
if self._exception is not None:
|
||||
raise self._exception
|
||||
raise EofStream
|
||||
|
||||
|
||||
class WebSocketReader:
|
||||
def __init__(
|
||||
self, queue: WebSocketDataQueue, max_msg_size: int, compress: bool = True
|
||||
) -> None:
|
||||
self.queue = queue
|
||||
self._max_msg_size = max_msg_size
|
||||
|
||||
self._exc: Optional[Exception] = None
|
||||
self._partial = bytearray()
|
||||
self._state = READ_HEADER
|
||||
|
||||
self._opcode: int = OP_CODE_NOT_SET
|
||||
self._frame_fin = False
|
||||
self._frame_opcode: int = OP_CODE_NOT_SET
|
||||
self._payload_fragments: list[bytes] = []
|
||||
self._frame_payload_len = 0
|
||||
|
||||
self._tail: bytes = b""
|
||||
self._has_mask = False
|
||||
self._frame_mask: Optional[bytes] = None
|
||||
self._payload_bytes_to_read = 0
|
||||
self._payload_len_flag = 0
|
||||
self._compressed: int = COMPRESSED_NOT_SET
|
||||
self._decompressobj: Optional[ZLibDecompressor] = None
|
||||
self._compress = compress
|
||||
|
||||
def feed_eof(self) -> None:
|
||||
self.queue.feed_eof()
|
||||
|
||||
# data can be bytearray on Windows because proactor event loop uses bytearray
|
||||
# and asyncio types this to Union[bytes, bytearray, memoryview] so we need
|
||||
# coerce data to bytes if it is not
|
||||
def feed_data(
|
||||
self, data: Union[bytes, bytearray, memoryview]
|
||||
) -> Tuple[bool, bytes]:
|
||||
if type(data) is not bytes:
|
||||
data = bytes(data)
|
||||
|
||||
if self._exc is not None:
|
||||
return True, data
|
||||
|
||||
try:
|
||||
self._feed_data(data)
|
||||
except Exception as exc:
|
||||
self._exc = exc
|
||||
set_exception(self.queue, exc)
|
||||
return EMPTY_FRAME_ERROR
|
||||
|
||||
return EMPTY_FRAME
|
||||
|
||||
def _handle_frame(
|
||||
self,
|
||||
fin: bool,
|
||||
opcode: Union[int, cython_int], # Union intended: Cython pxd uses C int
|
||||
payload: Union[bytes, bytearray],
|
||||
compressed: Union[int, cython_int], # Union intended: Cython pxd uses C int
|
||||
) -> None:
|
||||
msg: WSMessage
|
||||
if opcode in {OP_CODE_TEXT, OP_CODE_BINARY, OP_CODE_CONTINUATION}:
|
||||
# Validate continuation frames before processing
|
||||
if opcode == OP_CODE_CONTINUATION and self._opcode == OP_CODE_NOT_SET:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.PROTOCOL_ERROR,
|
||||
"Continuation frame for non started message",
|
||||
)
|
||||
|
||||
# load text/binary
|
||||
if not fin:
|
||||
# got partial frame payload
|
||||
if opcode != OP_CODE_CONTINUATION:
|
||||
self._opcode = opcode
|
||||
self._partial += payload
|
||||
if self._max_msg_size and len(self._partial) >= self._max_msg_size:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.MESSAGE_TOO_BIG,
|
||||
f"Message size {len(self._partial)} "
|
||||
f"exceeds limit {self._max_msg_size}",
|
||||
)
|
||||
return
|
||||
|
||||
has_partial = bool(self._partial)
|
||||
if opcode == OP_CODE_CONTINUATION:
|
||||
opcode = self._opcode
|
||||
self._opcode = OP_CODE_NOT_SET
|
||||
# previous frame was non finished
|
||||
# we should get continuation opcode
|
||||
elif has_partial:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.PROTOCOL_ERROR,
|
||||
"The opcode in non-fin frame is expected "
|
||||
f"to be zero, got {opcode!r}",
|
||||
)
|
||||
|
||||
assembled_payload: Union[bytes, bytearray]
|
||||
if has_partial:
|
||||
assembled_payload = self._partial + payload
|
||||
self._partial.clear()
|
||||
else:
|
||||
assembled_payload = payload
|
||||
|
||||
if self._max_msg_size and len(assembled_payload) >= self._max_msg_size:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.MESSAGE_TOO_BIG,
|
||||
f"Message size {len(assembled_payload)} "
|
||||
f"exceeds limit {self._max_msg_size}",
|
||||
)
|
||||
|
||||
# Decompress process must to be done after all packets
|
||||
# received.
|
||||
if compressed:
|
||||
if not self._decompressobj:
|
||||
self._decompressobj = ZLibDecompressor(suppress_deflate_header=True)
|
||||
# XXX: It's possible that the zlib backend (isal is known to
|
||||
# do this, maybe others too?) will return max_length bytes,
|
||||
# but internally buffer more data such that the payload is
|
||||
# >max_length, so we return one extra byte and if we're able
|
||||
# to do that, then the message is too big.
|
||||
payload_merged = self._decompressobj.decompress_sync(
|
||||
assembled_payload + WS_DEFLATE_TRAILING,
|
||||
(
|
||||
self._max_msg_size + 1
|
||||
if self._max_msg_size
|
||||
else self._max_msg_size
|
||||
),
|
||||
)
|
||||
if self._max_msg_size and len(payload_merged) > self._max_msg_size:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.MESSAGE_TOO_BIG,
|
||||
f"Decompressed message exceeds size limit {self._max_msg_size}",
|
||||
)
|
||||
elif type(assembled_payload) is bytes:
|
||||
payload_merged = assembled_payload
|
||||
else:
|
||||
payload_merged = bytes(assembled_payload)
|
||||
|
||||
if opcode == OP_CODE_TEXT:
|
||||
try:
|
||||
text = payload_merged.decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.INVALID_TEXT, "Invalid UTF-8 text message"
|
||||
) from exc
|
||||
|
||||
# XXX: The Text and Binary messages here can be a performance
|
||||
# bottleneck, so we use tuple.__new__ to improve performance.
|
||||
# This is not type safe, but many tests should fail in
|
||||
# test_client_ws_functional.py if this is wrong.
|
||||
self.queue.feed_data(
|
||||
TUPLE_NEW(WSMessage, (WS_MSG_TYPE_TEXT, text, "")),
|
||||
len(payload_merged),
|
||||
)
|
||||
else:
|
||||
self.queue.feed_data(
|
||||
TUPLE_NEW(WSMessage, (WS_MSG_TYPE_BINARY, payload_merged, "")),
|
||||
len(payload_merged),
|
||||
)
|
||||
elif opcode == OP_CODE_CLOSE:
|
||||
if len(payload) >= 2:
|
||||
close_code = UNPACK_CLOSE_CODE(payload[:2])[0]
|
||||
if close_code < 3000 and close_code not in ALLOWED_CLOSE_CODES:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.PROTOCOL_ERROR,
|
||||
f"Invalid close code: {close_code}",
|
||||
)
|
||||
try:
|
||||
close_message = payload[2:].decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.INVALID_TEXT, "Invalid UTF-8 text message"
|
||||
) from exc
|
||||
msg = TUPLE_NEW(WSMessage, (WSMsgType.CLOSE, close_code, close_message))
|
||||
elif payload:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.PROTOCOL_ERROR,
|
||||
f"Invalid close frame: {fin} {opcode} {payload!r}",
|
||||
)
|
||||
else:
|
||||
msg = TUPLE_NEW(WSMessage, (WSMsgType.CLOSE, 0, ""))
|
||||
|
||||
self.queue.feed_data(msg, 0)
|
||||
elif opcode == OP_CODE_PING:
|
||||
msg = TUPLE_NEW(WSMessage, (WSMsgType.PING, payload, ""))
|
||||
self.queue.feed_data(msg, len(payload))
|
||||
elif opcode == OP_CODE_PONG:
|
||||
msg = TUPLE_NEW(WSMessage, (WSMsgType.PONG, payload, ""))
|
||||
self.queue.feed_data(msg, len(payload))
|
||||
else:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.PROTOCOL_ERROR, f"Unexpected opcode={opcode!r}"
|
||||
)
|
||||
|
||||
def _feed_data(self, data: bytes) -> None:
|
||||
"""Return the next frame from the socket."""
|
||||
if self._tail:
|
||||
data, self._tail = self._tail + data, b""
|
||||
|
||||
start_pos: int = 0
|
||||
data_len = len(data)
|
||||
data_cstr = data
|
||||
|
||||
while True:
|
||||
# read header
|
||||
if self._state == READ_HEADER:
|
||||
if data_len - start_pos < 2:
|
||||
break
|
||||
first_byte = data_cstr[start_pos]
|
||||
second_byte = data_cstr[start_pos + 1]
|
||||
start_pos += 2
|
||||
|
||||
fin = (first_byte >> 7) & 1
|
||||
rsv1 = (first_byte >> 6) & 1
|
||||
rsv2 = (first_byte >> 5) & 1
|
||||
rsv3 = (first_byte >> 4) & 1
|
||||
opcode = first_byte & 0xF
|
||||
|
||||
# frame-fin = %x0 ; more frames of this message follow
|
||||
# / %x1 ; final frame of this message
|
||||
# frame-rsv1 = %x0 ;
|
||||
# 1 bit, MUST be 0 unless negotiated otherwise
|
||||
# frame-rsv2 = %x0 ;
|
||||
# 1 bit, MUST be 0 unless negotiated otherwise
|
||||
# frame-rsv3 = %x0 ;
|
||||
# 1 bit, MUST be 0 unless negotiated otherwise
|
||||
#
|
||||
# Remove rsv1 from this test for deflate development
|
||||
if rsv2 or rsv3 or (rsv1 and not self._compress):
|
||||
raise WebSocketError(
|
||||
WSCloseCode.PROTOCOL_ERROR,
|
||||
"Received frame with non-zero reserved bits",
|
||||
)
|
||||
|
||||
if opcode > 0x7 and fin == 0:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.PROTOCOL_ERROR,
|
||||
"Received fragmented control frame",
|
||||
)
|
||||
|
||||
has_mask = (second_byte >> 7) & 1
|
||||
length = second_byte & 0x7F
|
||||
|
||||
# Control frames MUST have a payload
|
||||
# length of 125 bytes or less
|
||||
if opcode > 0x7 and length > 125:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.PROTOCOL_ERROR,
|
||||
"Control frame payload cannot be larger than 125 bytes",
|
||||
)
|
||||
|
||||
# Set compress status if last package is FIN
|
||||
# OR set compress status if this is first fragment
|
||||
# Raise error if not first fragment with rsv1 = 0x1
|
||||
if self._frame_fin or self._compressed == COMPRESSED_NOT_SET:
|
||||
self._compressed = COMPRESSED_TRUE if rsv1 else COMPRESSED_FALSE
|
||||
elif rsv1:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.PROTOCOL_ERROR,
|
||||
"Received frame with non-zero reserved bits",
|
||||
)
|
||||
|
||||
self._frame_fin = bool(fin)
|
||||
self._frame_opcode = opcode
|
||||
self._has_mask = bool(has_mask)
|
||||
self._payload_len_flag = length
|
||||
self._state = READ_PAYLOAD_LENGTH
|
||||
|
||||
# read payload length
|
||||
if self._state == READ_PAYLOAD_LENGTH:
|
||||
len_flag = self._payload_len_flag
|
||||
if len_flag == 126:
|
||||
if data_len - start_pos < 2:
|
||||
break
|
||||
first_byte = data_cstr[start_pos]
|
||||
second_byte = data_cstr[start_pos + 1]
|
||||
start_pos += 2
|
||||
self._payload_bytes_to_read = first_byte << 8 | second_byte
|
||||
elif len_flag > 126:
|
||||
if data_len - start_pos < 8:
|
||||
break
|
||||
self._payload_bytes_to_read = UNPACK_LEN3(data, start_pos)[0]
|
||||
start_pos += 8
|
||||
else:
|
||||
self._payload_bytes_to_read = len_flag
|
||||
|
||||
self._state = READ_PAYLOAD_MASK if self._has_mask else READ_PAYLOAD
|
||||
|
||||
# read payload mask
|
||||
if self._state == READ_PAYLOAD_MASK:
|
||||
if data_len - start_pos < 4:
|
||||
break
|
||||
self._frame_mask = data_cstr[start_pos : start_pos + 4]
|
||||
start_pos += 4
|
||||
self._state = READ_PAYLOAD
|
||||
|
||||
if self._state == READ_PAYLOAD:
|
||||
chunk_len = data_len - start_pos
|
||||
if self._payload_bytes_to_read >= chunk_len:
|
||||
f_end_pos = data_len
|
||||
self._payload_bytes_to_read -= chunk_len
|
||||
else:
|
||||
f_end_pos = start_pos + self._payload_bytes_to_read
|
||||
self._payload_bytes_to_read = 0
|
||||
|
||||
had_fragments = self._frame_payload_len
|
||||
self._frame_payload_len += f_end_pos - start_pos
|
||||
f_start_pos = start_pos
|
||||
start_pos = f_end_pos
|
||||
|
||||
if self._payload_bytes_to_read != 0:
|
||||
# If we don't have a complete frame, we need to save the
|
||||
# data for the next call to feed_data.
|
||||
self._payload_fragments.append(data_cstr[f_start_pos:f_end_pos])
|
||||
break
|
||||
|
||||
payload: Union[bytes, bytearray]
|
||||
if had_fragments:
|
||||
# We have to join the payload fragments get the payload
|
||||
self._payload_fragments.append(data_cstr[f_start_pos:f_end_pos])
|
||||
if self._has_mask:
|
||||
assert self._frame_mask is not None
|
||||
payload_bytearray = bytearray(b"".join(self._payload_fragments))
|
||||
websocket_mask(self._frame_mask, payload_bytearray)
|
||||
payload = payload_bytearray
|
||||
else:
|
||||
payload = b"".join(self._payload_fragments)
|
||||
self._payload_fragments.clear()
|
||||
elif self._has_mask:
|
||||
assert self._frame_mask is not None
|
||||
payload_bytearray = data_cstr[f_start_pos:f_end_pos] # type: ignore[assignment]
|
||||
if type(payload_bytearray) is not bytearray: # pragma: no branch
|
||||
# Cython will do the conversion for us
|
||||
# but we need to do it for Python and we
|
||||
# will always get here in Python
|
||||
payload_bytearray = bytearray(payload_bytearray)
|
||||
websocket_mask(self._frame_mask, payload_bytearray)
|
||||
payload = payload_bytearray
|
||||
else:
|
||||
payload = data_cstr[f_start_pos:f_end_pos]
|
||||
|
||||
self._handle_frame(
|
||||
self._frame_fin, self._frame_opcode, payload, self._compressed
|
||||
)
|
||||
self._frame_payload_len = 0
|
||||
self._state = READ_HEADER
|
||||
|
||||
# XXX: Cython needs slices to be bounded, so we can't omit the slice end here.
|
||||
self._tail = data_cstr[start_pos:data_len] if start_pos < data_len else b""
|
||||
@@ -0,0 +1,478 @@
|
||||
"""Reader for WebSocket protocol versions 13 and 8."""
|
||||
|
||||
import asyncio
|
||||
import builtins
|
||||
from collections import deque
|
||||
from typing import Deque, Final, Optional, Set, Tuple, Union
|
||||
|
||||
from ..base_protocol import BaseProtocol
|
||||
from ..compression_utils import ZLibDecompressor
|
||||
from ..helpers import _EXC_SENTINEL, set_exception
|
||||
from ..streams import EofStream
|
||||
from .helpers import UNPACK_CLOSE_CODE, UNPACK_LEN3, websocket_mask
|
||||
from .models import (
|
||||
WS_DEFLATE_TRAILING,
|
||||
WebSocketError,
|
||||
WSCloseCode,
|
||||
WSMessage,
|
||||
WSMsgType,
|
||||
)
|
||||
|
||||
ALLOWED_CLOSE_CODES: Final[Set[int]] = {int(i) for i in WSCloseCode}
|
||||
|
||||
# States for the reader, used to parse the WebSocket frame
|
||||
# integer values are used so they can be cythonized
|
||||
READ_HEADER = 1
|
||||
READ_PAYLOAD_LENGTH = 2
|
||||
READ_PAYLOAD_MASK = 3
|
||||
READ_PAYLOAD = 4
|
||||
|
||||
WS_MSG_TYPE_BINARY = WSMsgType.BINARY
|
||||
WS_MSG_TYPE_TEXT = WSMsgType.TEXT
|
||||
|
||||
# WSMsgType values unpacked so they can by cythonized to ints
|
||||
OP_CODE_NOT_SET = -1
|
||||
OP_CODE_CONTINUATION = WSMsgType.CONTINUATION.value
|
||||
OP_CODE_TEXT = WSMsgType.TEXT.value
|
||||
OP_CODE_BINARY = WSMsgType.BINARY.value
|
||||
OP_CODE_CLOSE = WSMsgType.CLOSE.value
|
||||
OP_CODE_PING = WSMsgType.PING.value
|
||||
OP_CODE_PONG = WSMsgType.PONG.value
|
||||
|
||||
EMPTY_FRAME_ERROR = (True, b"")
|
||||
EMPTY_FRAME = (False, b"")
|
||||
|
||||
COMPRESSED_NOT_SET = -1
|
||||
COMPRESSED_FALSE = 0
|
||||
COMPRESSED_TRUE = 1
|
||||
|
||||
TUPLE_NEW = tuple.__new__
|
||||
|
||||
cython_int = int # Typed to int in Python, but cython with use a signed int in the pxd
|
||||
|
||||
|
||||
class WebSocketDataQueue:
|
||||
"""WebSocketDataQueue resumes and pauses an underlying stream.
|
||||
|
||||
It is a destination for WebSocket data.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, protocol: BaseProtocol, limit: int, *, loop: asyncio.AbstractEventLoop
|
||||
) -> None:
|
||||
self._size = 0
|
||||
self._protocol = protocol
|
||||
self._limit = limit * 2
|
||||
self._loop = loop
|
||||
self._eof = False
|
||||
self._waiter: Optional[asyncio.Future[None]] = None
|
||||
self._exception: Union[BaseException, None] = None
|
||||
self._buffer: Deque[Tuple[WSMessage, int]] = deque()
|
||||
self._get_buffer = self._buffer.popleft
|
||||
self._put_buffer = self._buffer.append
|
||||
|
||||
def is_eof(self) -> bool:
|
||||
return self._eof
|
||||
|
||||
def exception(self) -> Optional[BaseException]:
|
||||
return self._exception
|
||||
|
||||
def set_exception(
|
||||
self,
|
||||
exc: BaseException,
|
||||
exc_cause: builtins.BaseException = _EXC_SENTINEL,
|
||||
) -> None:
|
||||
self._eof = True
|
||||
self._exception = exc
|
||||
if (waiter := self._waiter) is not None:
|
||||
self._waiter = None
|
||||
set_exception(waiter, exc, exc_cause)
|
||||
|
||||
def _release_waiter(self) -> None:
|
||||
if (waiter := self._waiter) is None:
|
||||
return
|
||||
self._waiter = None
|
||||
if not waiter.done():
|
||||
waiter.set_result(None)
|
||||
|
||||
def feed_eof(self) -> None:
|
||||
self._eof = True
|
||||
self._release_waiter()
|
||||
self._exception = None # Break cyclic references
|
||||
|
||||
def feed_data(self, data: "WSMessage", size: "cython_int") -> None:
|
||||
self._size += size
|
||||
self._put_buffer((data, size))
|
||||
self._release_waiter()
|
||||
if self._size > self._limit and not self._protocol._reading_paused:
|
||||
self._protocol.pause_reading()
|
||||
|
||||
async def read(self) -> WSMessage:
|
||||
if not self._buffer and not self._eof:
|
||||
assert not self._waiter
|
||||
self._waiter = self._loop.create_future()
|
||||
try:
|
||||
await self._waiter
|
||||
except (asyncio.CancelledError, asyncio.TimeoutError):
|
||||
self._waiter = None
|
||||
raise
|
||||
return self._read_from_buffer()
|
||||
|
||||
def _read_from_buffer(self) -> WSMessage:
|
||||
if self._buffer:
|
||||
data, size = self._get_buffer()
|
||||
self._size -= size
|
||||
if self._size < self._limit and self._protocol._reading_paused:
|
||||
self._protocol.resume_reading()
|
||||
return data
|
||||
if self._exception is not None:
|
||||
raise self._exception
|
||||
raise EofStream
|
||||
|
||||
|
||||
class WebSocketReader:
|
||||
def __init__(
|
||||
self, queue: WebSocketDataQueue, max_msg_size: int, compress: bool = True
|
||||
) -> None:
|
||||
self.queue = queue
|
||||
self._max_msg_size = max_msg_size
|
||||
|
||||
self._exc: Optional[Exception] = None
|
||||
self._partial = bytearray()
|
||||
self._state = READ_HEADER
|
||||
|
||||
self._opcode: int = OP_CODE_NOT_SET
|
||||
self._frame_fin = False
|
||||
self._frame_opcode: int = OP_CODE_NOT_SET
|
||||
self._payload_fragments: list[bytes] = []
|
||||
self._frame_payload_len = 0
|
||||
|
||||
self._tail: bytes = b""
|
||||
self._has_mask = False
|
||||
self._frame_mask: Optional[bytes] = None
|
||||
self._payload_bytes_to_read = 0
|
||||
self._payload_len_flag = 0
|
||||
self._compressed: int = COMPRESSED_NOT_SET
|
||||
self._decompressobj: Optional[ZLibDecompressor] = None
|
||||
self._compress = compress
|
||||
|
||||
def feed_eof(self) -> None:
|
||||
self.queue.feed_eof()
|
||||
|
||||
# data can be bytearray on Windows because proactor event loop uses bytearray
|
||||
# and asyncio types this to Union[bytes, bytearray, memoryview] so we need
|
||||
# coerce data to bytes if it is not
|
||||
def feed_data(
|
||||
self, data: Union[bytes, bytearray, memoryview]
|
||||
) -> Tuple[bool, bytes]:
|
||||
if type(data) is not bytes:
|
||||
data = bytes(data)
|
||||
|
||||
if self._exc is not None:
|
||||
return True, data
|
||||
|
||||
try:
|
||||
self._feed_data(data)
|
||||
except Exception as exc:
|
||||
self._exc = exc
|
||||
set_exception(self.queue, exc)
|
||||
return EMPTY_FRAME_ERROR
|
||||
|
||||
return EMPTY_FRAME
|
||||
|
||||
def _handle_frame(
|
||||
self,
|
||||
fin: bool,
|
||||
opcode: Union[int, cython_int], # Union intended: Cython pxd uses C int
|
||||
payload: Union[bytes, bytearray],
|
||||
compressed: Union[int, cython_int], # Union intended: Cython pxd uses C int
|
||||
) -> None:
|
||||
msg: WSMessage
|
||||
if opcode in {OP_CODE_TEXT, OP_CODE_BINARY, OP_CODE_CONTINUATION}:
|
||||
# Validate continuation frames before processing
|
||||
if opcode == OP_CODE_CONTINUATION and self._opcode == OP_CODE_NOT_SET:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.PROTOCOL_ERROR,
|
||||
"Continuation frame for non started message",
|
||||
)
|
||||
|
||||
# load text/binary
|
||||
if not fin:
|
||||
# got partial frame payload
|
||||
if opcode != OP_CODE_CONTINUATION:
|
||||
self._opcode = opcode
|
||||
self._partial += payload
|
||||
if self._max_msg_size and len(self._partial) >= self._max_msg_size:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.MESSAGE_TOO_BIG,
|
||||
f"Message size {len(self._partial)} "
|
||||
f"exceeds limit {self._max_msg_size}",
|
||||
)
|
||||
return
|
||||
|
||||
has_partial = bool(self._partial)
|
||||
if opcode == OP_CODE_CONTINUATION:
|
||||
opcode = self._opcode
|
||||
self._opcode = OP_CODE_NOT_SET
|
||||
# previous frame was non finished
|
||||
# we should get continuation opcode
|
||||
elif has_partial:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.PROTOCOL_ERROR,
|
||||
"The opcode in non-fin frame is expected "
|
||||
f"to be zero, got {opcode!r}",
|
||||
)
|
||||
|
||||
assembled_payload: Union[bytes, bytearray]
|
||||
if has_partial:
|
||||
assembled_payload = self._partial + payload
|
||||
self._partial.clear()
|
||||
else:
|
||||
assembled_payload = payload
|
||||
|
||||
if self._max_msg_size and len(assembled_payload) >= self._max_msg_size:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.MESSAGE_TOO_BIG,
|
||||
f"Message size {len(assembled_payload)} "
|
||||
f"exceeds limit {self._max_msg_size}",
|
||||
)
|
||||
|
||||
# Decompress process must to be done after all packets
|
||||
# received.
|
||||
if compressed:
|
||||
if not self._decompressobj:
|
||||
self._decompressobj = ZLibDecompressor(suppress_deflate_header=True)
|
||||
# XXX: It's possible that the zlib backend (isal is known to
|
||||
# do this, maybe others too?) will return max_length bytes,
|
||||
# but internally buffer more data such that the payload is
|
||||
# >max_length, so we return one extra byte and if we're able
|
||||
# to do that, then the message is too big.
|
||||
payload_merged = self._decompressobj.decompress_sync(
|
||||
assembled_payload + WS_DEFLATE_TRAILING,
|
||||
(
|
||||
self._max_msg_size + 1
|
||||
if self._max_msg_size
|
||||
else self._max_msg_size
|
||||
),
|
||||
)
|
||||
if self._max_msg_size and len(payload_merged) > self._max_msg_size:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.MESSAGE_TOO_BIG,
|
||||
f"Decompressed message exceeds size limit {self._max_msg_size}",
|
||||
)
|
||||
elif type(assembled_payload) is bytes:
|
||||
payload_merged = assembled_payload
|
||||
else:
|
||||
payload_merged = bytes(assembled_payload)
|
||||
|
||||
if opcode == OP_CODE_TEXT:
|
||||
try:
|
||||
text = payload_merged.decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.INVALID_TEXT, "Invalid UTF-8 text message"
|
||||
) from exc
|
||||
|
||||
# XXX: The Text and Binary messages here can be a performance
|
||||
# bottleneck, so we use tuple.__new__ to improve performance.
|
||||
# This is not type safe, but many tests should fail in
|
||||
# test_client_ws_functional.py if this is wrong.
|
||||
self.queue.feed_data(
|
||||
TUPLE_NEW(WSMessage, (WS_MSG_TYPE_TEXT, text, "")),
|
||||
len(payload_merged),
|
||||
)
|
||||
else:
|
||||
self.queue.feed_data(
|
||||
TUPLE_NEW(WSMessage, (WS_MSG_TYPE_BINARY, payload_merged, "")),
|
||||
len(payload_merged),
|
||||
)
|
||||
elif opcode == OP_CODE_CLOSE:
|
||||
if len(payload) >= 2:
|
||||
close_code = UNPACK_CLOSE_CODE(payload[:2])[0]
|
||||
if close_code < 3000 and close_code not in ALLOWED_CLOSE_CODES:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.PROTOCOL_ERROR,
|
||||
f"Invalid close code: {close_code}",
|
||||
)
|
||||
try:
|
||||
close_message = payload[2:].decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.INVALID_TEXT, "Invalid UTF-8 text message"
|
||||
) from exc
|
||||
msg = TUPLE_NEW(WSMessage, (WSMsgType.CLOSE, close_code, close_message))
|
||||
elif payload:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.PROTOCOL_ERROR,
|
||||
f"Invalid close frame: {fin} {opcode} {payload!r}",
|
||||
)
|
||||
else:
|
||||
msg = TUPLE_NEW(WSMessage, (WSMsgType.CLOSE, 0, ""))
|
||||
|
||||
self.queue.feed_data(msg, 0)
|
||||
elif opcode == OP_CODE_PING:
|
||||
msg = TUPLE_NEW(WSMessage, (WSMsgType.PING, payload, ""))
|
||||
self.queue.feed_data(msg, len(payload))
|
||||
elif opcode == OP_CODE_PONG:
|
||||
msg = TUPLE_NEW(WSMessage, (WSMsgType.PONG, payload, ""))
|
||||
self.queue.feed_data(msg, len(payload))
|
||||
else:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.PROTOCOL_ERROR, f"Unexpected opcode={opcode!r}"
|
||||
)
|
||||
|
||||
def _feed_data(self, data: bytes) -> None:
|
||||
"""Return the next frame from the socket."""
|
||||
if self._tail:
|
||||
data, self._tail = self._tail + data, b""
|
||||
|
||||
start_pos: int = 0
|
||||
data_len = len(data)
|
||||
data_cstr = data
|
||||
|
||||
while True:
|
||||
# read header
|
||||
if self._state == READ_HEADER:
|
||||
if data_len - start_pos < 2:
|
||||
break
|
||||
first_byte = data_cstr[start_pos]
|
||||
second_byte = data_cstr[start_pos + 1]
|
||||
start_pos += 2
|
||||
|
||||
fin = (first_byte >> 7) & 1
|
||||
rsv1 = (first_byte >> 6) & 1
|
||||
rsv2 = (first_byte >> 5) & 1
|
||||
rsv3 = (first_byte >> 4) & 1
|
||||
opcode = first_byte & 0xF
|
||||
|
||||
# frame-fin = %x0 ; more frames of this message follow
|
||||
# / %x1 ; final frame of this message
|
||||
# frame-rsv1 = %x0 ;
|
||||
# 1 bit, MUST be 0 unless negotiated otherwise
|
||||
# frame-rsv2 = %x0 ;
|
||||
# 1 bit, MUST be 0 unless negotiated otherwise
|
||||
# frame-rsv3 = %x0 ;
|
||||
# 1 bit, MUST be 0 unless negotiated otherwise
|
||||
#
|
||||
# Remove rsv1 from this test for deflate development
|
||||
if rsv2 or rsv3 or (rsv1 and not self._compress):
|
||||
raise WebSocketError(
|
||||
WSCloseCode.PROTOCOL_ERROR,
|
||||
"Received frame with non-zero reserved bits",
|
||||
)
|
||||
|
||||
if opcode > 0x7 and fin == 0:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.PROTOCOL_ERROR,
|
||||
"Received fragmented control frame",
|
||||
)
|
||||
|
||||
has_mask = (second_byte >> 7) & 1
|
||||
length = second_byte & 0x7F
|
||||
|
||||
# Control frames MUST have a payload
|
||||
# length of 125 bytes or less
|
||||
if opcode > 0x7 and length > 125:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.PROTOCOL_ERROR,
|
||||
"Control frame payload cannot be larger than 125 bytes",
|
||||
)
|
||||
|
||||
# Set compress status if last package is FIN
|
||||
# OR set compress status if this is first fragment
|
||||
# Raise error if not first fragment with rsv1 = 0x1
|
||||
if self._frame_fin or self._compressed == COMPRESSED_NOT_SET:
|
||||
self._compressed = COMPRESSED_TRUE if rsv1 else COMPRESSED_FALSE
|
||||
elif rsv1:
|
||||
raise WebSocketError(
|
||||
WSCloseCode.PROTOCOL_ERROR,
|
||||
"Received frame with non-zero reserved bits",
|
||||
)
|
||||
|
||||
self._frame_fin = bool(fin)
|
||||
self._frame_opcode = opcode
|
||||
self._has_mask = bool(has_mask)
|
||||
self._payload_len_flag = length
|
||||
self._state = READ_PAYLOAD_LENGTH
|
||||
|
||||
# read payload length
|
||||
if self._state == READ_PAYLOAD_LENGTH:
|
||||
len_flag = self._payload_len_flag
|
||||
if len_flag == 126:
|
||||
if data_len - start_pos < 2:
|
||||
break
|
||||
first_byte = data_cstr[start_pos]
|
||||
second_byte = data_cstr[start_pos + 1]
|
||||
start_pos += 2
|
||||
self._payload_bytes_to_read = first_byte << 8 | second_byte
|
||||
elif len_flag > 126:
|
||||
if data_len - start_pos < 8:
|
||||
break
|
||||
self._payload_bytes_to_read = UNPACK_LEN3(data, start_pos)[0]
|
||||
start_pos += 8
|
||||
else:
|
||||
self._payload_bytes_to_read = len_flag
|
||||
|
||||
self._state = READ_PAYLOAD_MASK if self._has_mask else READ_PAYLOAD
|
||||
|
||||
# read payload mask
|
||||
if self._state == READ_PAYLOAD_MASK:
|
||||
if data_len - start_pos < 4:
|
||||
break
|
||||
self._frame_mask = data_cstr[start_pos : start_pos + 4]
|
||||
start_pos += 4
|
||||
self._state = READ_PAYLOAD
|
||||
|
||||
if self._state == READ_PAYLOAD:
|
||||
chunk_len = data_len - start_pos
|
||||
if self._payload_bytes_to_read >= chunk_len:
|
||||
f_end_pos = data_len
|
||||
self._payload_bytes_to_read -= chunk_len
|
||||
else:
|
||||
f_end_pos = start_pos + self._payload_bytes_to_read
|
||||
self._payload_bytes_to_read = 0
|
||||
|
||||
had_fragments = self._frame_payload_len
|
||||
self._frame_payload_len += f_end_pos - start_pos
|
||||
f_start_pos = start_pos
|
||||
start_pos = f_end_pos
|
||||
|
||||
if self._payload_bytes_to_read != 0:
|
||||
# If we don't have a complete frame, we need to save the
|
||||
# data for the next call to feed_data.
|
||||
self._payload_fragments.append(data_cstr[f_start_pos:f_end_pos])
|
||||
break
|
||||
|
||||
payload: Union[bytes, bytearray]
|
||||
if had_fragments:
|
||||
# We have to join the payload fragments get the payload
|
||||
self._payload_fragments.append(data_cstr[f_start_pos:f_end_pos])
|
||||
if self._has_mask:
|
||||
assert self._frame_mask is not None
|
||||
payload_bytearray = bytearray(b"".join(self._payload_fragments))
|
||||
websocket_mask(self._frame_mask, payload_bytearray)
|
||||
payload = payload_bytearray
|
||||
else:
|
||||
payload = b"".join(self._payload_fragments)
|
||||
self._payload_fragments.clear()
|
||||
elif self._has_mask:
|
||||
assert self._frame_mask is not None
|
||||
payload_bytearray = data_cstr[f_start_pos:f_end_pos] # type: ignore[assignment]
|
||||
if type(payload_bytearray) is not bytearray: # pragma: no branch
|
||||
# Cython will do the conversion for us
|
||||
# but we need to do it for Python and we
|
||||
# will always get here in Python
|
||||
payload_bytearray = bytearray(payload_bytearray)
|
||||
websocket_mask(self._frame_mask, payload_bytearray)
|
||||
payload = payload_bytearray
|
||||
else:
|
||||
payload = data_cstr[f_start_pos:f_end_pos]
|
||||
|
||||
self._handle_frame(
|
||||
self._frame_fin, self._frame_opcode, payload, self._compressed
|
||||
)
|
||||
self._frame_payload_len = 0
|
||||
self._state = READ_HEADER
|
||||
|
||||
# XXX: Cython needs slices to be bounded, so we can't omit the slice end here.
|
||||
self._tail = data_cstr[start_pos:data_len] if start_pos < data_len else b""
|
||||
262
venv/lib/python3.12/site-packages/aiohttp/_websocket/writer.py
Normal file
262
venv/lib/python3.12/site-packages/aiohttp/_websocket/writer.py
Normal file
@@ -0,0 +1,262 @@
|
||||
"""WebSocket protocol versions 13 and 8."""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import sys
|
||||
from functools import partial
|
||||
from typing import Final, Optional, Set, Union
|
||||
|
||||
from ..base_protocol import BaseProtocol
|
||||
from ..client_exceptions import ClientConnectionResetError
|
||||
from ..compression_utils import ZLibBackend, ZLibCompressor
|
||||
from .helpers import (
|
||||
MASK_LEN,
|
||||
MSG_SIZE,
|
||||
PACK_CLOSE_CODE,
|
||||
PACK_LEN1,
|
||||
PACK_LEN2,
|
||||
PACK_LEN3,
|
||||
PACK_RANDBITS,
|
||||
websocket_mask,
|
||||
)
|
||||
from .models import WS_DEFLATE_TRAILING, WSMsgType
|
||||
|
||||
DEFAULT_LIMIT: Final[int] = 2**16
|
||||
|
||||
# WebSocket opcode boundary: opcodes 0-7 are data frames, 8-15 are control frames
|
||||
# Control frames (ping, pong, close) are never compressed
|
||||
WS_CONTROL_FRAME_OPCODE: Final[int] = 8
|
||||
|
||||
# For websockets, keeping latency low is extremely important as implementations
|
||||
# generally expect to be able to send and receive messages quickly. We use a
|
||||
# larger chunk size to reduce the number of executor calls and avoid task
|
||||
# creation overhead, since both are significant sources of latency when chunks
|
||||
# are small. A size of 16KiB was chosen as a balance between avoiding task
|
||||
# overhead and not blocking the event loop too long with synchronous compression.
|
||||
|
||||
WEBSOCKET_MAX_SYNC_CHUNK_SIZE = 16 * 1024
|
||||
|
||||
|
||||
class WebSocketWriter:
|
||||
"""WebSocket writer.
|
||||
|
||||
The writer is responsible for sending messages to the client. It is
|
||||
created by the protocol when a connection is established. The writer
|
||||
should avoid implementing any application logic and should only be
|
||||
concerned with the low-level details of the WebSocket protocol.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
protocol: BaseProtocol,
|
||||
transport: asyncio.Transport,
|
||||
*,
|
||||
use_mask: bool = False,
|
||||
limit: int = DEFAULT_LIMIT,
|
||||
random: random.Random = random.Random(),
|
||||
compress: int = 0,
|
||||
notakeover: bool = False,
|
||||
) -> None:
|
||||
"""Initialize a WebSocket writer."""
|
||||
self.protocol = protocol
|
||||
self.transport = transport
|
||||
self.use_mask = use_mask
|
||||
self.get_random_bits = partial(random.getrandbits, 32)
|
||||
self.compress = compress
|
||||
self.notakeover = notakeover
|
||||
self._closing = False
|
||||
self._limit = limit
|
||||
self._output_size = 0
|
||||
self._compressobj: Optional[ZLibCompressor] = None
|
||||
self._send_lock = asyncio.Lock()
|
||||
self._background_tasks: Set[asyncio.Task[None]] = set()
|
||||
|
||||
async def send_frame(
|
||||
self, message: bytes, opcode: int, compress: Optional[int] = None
|
||||
) -> None:
|
||||
"""Send a frame over the websocket with message as its payload."""
|
||||
if self._closing and not (opcode & WSMsgType.CLOSE):
|
||||
raise ClientConnectionResetError("Cannot write to closing transport")
|
||||
|
||||
if not (compress or self.compress) or opcode >= WS_CONTROL_FRAME_OPCODE:
|
||||
# Non-compressed frames don't need lock or shield
|
||||
self._write_websocket_frame(message, opcode, 0)
|
||||
elif len(message) <= WEBSOCKET_MAX_SYNC_CHUNK_SIZE:
|
||||
# Small compressed payloads - compress synchronously in event loop
|
||||
# We need the lock even though sync compression has no await points.
|
||||
# This prevents small frames from interleaving with large frames that
|
||||
# compress in the executor, avoiding compressor state corruption.
|
||||
async with self._send_lock:
|
||||
self._send_compressed_frame_sync(message, opcode, compress)
|
||||
else:
|
||||
# Large compressed frames need shield to prevent corruption
|
||||
# For large compressed frames, the entire compress+send
|
||||
# operation must be atomic. If cancelled after compression but
|
||||
# before send, the compressor state would be advanced but data
|
||||
# not sent, corrupting subsequent frames.
|
||||
# Create a task to shield from cancellation
|
||||
# The lock is acquired inside the shielded task so the entire
|
||||
# operation (lock + compress + send) completes atomically.
|
||||
# Use eager_start on Python 3.12+ to avoid scheduling overhead
|
||||
loop = asyncio.get_running_loop()
|
||||
coro = self._send_compressed_frame_async_locked(message, opcode, compress)
|
||||
if sys.version_info >= (3, 12):
|
||||
send_task = asyncio.Task(coro, loop=loop, eager_start=True)
|
||||
else:
|
||||
send_task = loop.create_task(coro)
|
||||
# Keep a strong reference to prevent garbage collection
|
||||
self._background_tasks.add(send_task)
|
||||
send_task.add_done_callback(self._background_tasks.discard)
|
||||
await asyncio.shield(send_task)
|
||||
|
||||
# It is safe to return control to the event loop when using compression
|
||||
# after this point as we have already sent or buffered all the data.
|
||||
# Once we have written output_size up to the limit, we call the
|
||||
# drain helper which waits for the transport to be ready to accept
|
||||
# more data. This is a flow control mechanism to prevent the buffer
|
||||
# from growing too large. The drain helper will return right away
|
||||
# if the writer is not paused.
|
||||
if self._output_size > self._limit:
|
||||
self._output_size = 0
|
||||
if self.protocol._paused:
|
||||
await self.protocol._drain_helper()
|
||||
|
||||
def _write_websocket_frame(self, message: bytes, opcode: int, rsv: int) -> None:
|
||||
"""
|
||||
Write a websocket frame to the transport.
|
||||
|
||||
This method handles frame header construction, masking, and writing to transport.
|
||||
It does not handle compression or flow control - those are the responsibility
|
||||
of the caller.
|
||||
"""
|
||||
msg_length = len(message)
|
||||
|
||||
use_mask = self.use_mask
|
||||
mask_bit = 0x80 if use_mask else 0
|
||||
|
||||
# Depending on the message length, the header is assembled differently.
|
||||
# The first byte is reserved for the opcode and the RSV bits.
|
||||
first_byte = 0x80 | rsv | opcode
|
||||
if msg_length < 126:
|
||||
header = PACK_LEN1(first_byte, msg_length | mask_bit)
|
||||
header_len = 2
|
||||
elif msg_length < 65536:
|
||||
header = PACK_LEN2(first_byte, 126 | mask_bit, msg_length)
|
||||
header_len = 4
|
||||
else:
|
||||
header = PACK_LEN3(first_byte, 127 | mask_bit, msg_length)
|
||||
header_len = 10
|
||||
|
||||
if self.transport.is_closing():
|
||||
raise ClientConnectionResetError("Cannot write to closing transport")
|
||||
|
||||
# https://datatracker.ietf.org/doc/html/rfc6455#section-5.3
|
||||
# If we are using a mask, we need to generate it randomly
|
||||
# and apply it to the message before sending it. A mask is
|
||||
# a 32-bit value that is applied to the message using a
|
||||
# bitwise XOR operation. It is used to prevent certain types
|
||||
# of attacks on the websocket protocol. The mask is only used
|
||||
# when aiohttp is acting as a client. Servers do not use a mask.
|
||||
if use_mask:
|
||||
mask = PACK_RANDBITS(self.get_random_bits())
|
||||
message = bytearray(message)
|
||||
websocket_mask(mask, message)
|
||||
self.transport.write(header + mask + message)
|
||||
self._output_size += MASK_LEN
|
||||
elif msg_length > MSG_SIZE:
|
||||
self.transport.write(header)
|
||||
self.transport.write(message)
|
||||
else:
|
||||
self.transport.write(header + message)
|
||||
|
||||
self._output_size += header_len + msg_length
|
||||
|
||||
def _get_compressor(self, compress: Optional[int]) -> ZLibCompressor:
|
||||
"""Get or create a compressor object for the given compression level."""
|
||||
if compress:
|
||||
# Do not set self._compress if compressing is for this frame
|
||||
return ZLibCompressor(
|
||||
level=ZLibBackend.Z_BEST_SPEED,
|
||||
wbits=-compress,
|
||||
max_sync_chunk_size=WEBSOCKET_MAX_SYNC_CHUNK_SIZE,
|
||||
)
|
||||
if not self._compressobj:
|
||||
self._compressobj = ZLibCompressor(
|
||||
level=ZLibBackend.Z_BEST_SPEED,
|
||||
wbits=-self.compress,
|
||||
max_sync_chunk_size=WEBSOCKET_MAX_SYNC_CHUNK_SIZE,
|
||||
)
|
||||
return self._compressobj
|
||||
|
||||
def _send_compressed_frame_sync(
|
||||
self, message: bytes, opcode: int, compress: Optional[int]
|
||||
) -> None:
|
||||
"""
|
||||
Synchronous send for small compressed frames.
|
||||
|
||||
This is used for small compressed payloads that compress synchronously in the event loop.
|
||||
Since there are no await points, this is inherently cancellation-safe.
|
||||
"""
|
||||
# RSV are the reserved bits in the frame header. They are used to
|
||||
# indicate that the frame is using an extension.
|
||||
# https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
|
||||
compressobj = self._get_compressor(compress)
|
||||
# (0x40) RSV1 is set for compressed frames
|
||||
# https://datatracker.ietf.org/doc/html/rfc7692#section-7.2.3.1
|
||||
self._write_websocket_frame(
|
||||
(
|
||||
compressobj.compress_sync(message)
|
||||
+ compressobj.flush(
|
||||
ZLibBackend.Z_FULL_FLUSH
|
||||
if self.notakeover
|
||||
else ZLibBackend.Z_SYNC_FLUSH
|
||||
)
|
||||
).removesuffix(WS_DEFLATE_TRAILING),
|
||||
opcode,
|
||||
0x40,
|
||||
)
|
||||
|
||||
async def _send_compressed_frame_async_locked(
|
||||
self, message: bytes, opcode: int, compress: Optional[int]
|
||||
) -> None:
|
||||
"""
|
||||
Async send for large compressed frames with lock.
|
||||
|
||||
Acquires the lock and compresses large payloads asynchronously in
|
||||
the executor. The lock is held for the entire operation to ensure
|
||||
the compressor state is not corrupted by concurrent sends.
|
||||
|
||||
MUST be run shielded from cancellation. If cancelled after
|
||||
compression but before sending, the compressor state would be
|
||||
advanced but data not sent, corrupting subsequent frames.
|
||||
"""
|
||||
async with self._send_lock:
|
||||
# RSV are the reserved bits in the frame header. They are used to
|
||||
# indicate that the frame is using an extension.
|
||||
# https://datatracker.ietf.org/doc/html/rfc6455#section-5.2
|
||||
compressobj = self._get_compressor(compress)
|
||||
# (0x40) RSV1 is set for compressed frames
|
||||
# https://datatracker.ietf.org/doc/html/rfc7692#section-7.2.3.1
|
||||
self._write_websocket_frame(
|
||||
(
|
||||
await compressobj.compress(message)
|
||||
+ compressobj.flush(
|
||||
ZLibBackend.Z_FULL_FLUSH
|
||||
if self.notakeover
|
||||
else ZLibBackend.Z_SYNC_FLUSH
|
||||
)
|
||||
).removesuffix(WS_DEFLATE_TRAILING),
|
||||
opcode,
|
||||
0x40,
|
||||
)
|
||||
|
||||
async def close(self, code: int = 1000, message: Union[bytes, str] = b"") -> None:
|
||||
"""Close the websocket, sending the specified code and message."""
|
||||
if isinstance(message, str):
|
||||
message = message.encode("utf-8")
|
||||
try:
|
||||
await self.send_frame(
|
||||
PACK_CLOSE_CODE(code) + message, opcode=WSMsgType.CLOSE
|
||||
)
|
||||
finally:
|
||||
self._closing = True
|
||||
268
venv/lib/python3.12/site-packages/aiohttp/abc.py
Normal file
268
venv/lib/python3.12/site-packages/aiohttp/abc.py
Normal file
@@ -0,0 +1,268 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import socket
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Sized
|
||||
from http.cookies import BaseCookie, Morsel
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Dict,
|
||||
Generator,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
TypedDict,
|
||||
Union,
|
||||
)
|
||||
|
||||
from multidict import CIMultiDict
|
||||
from yarl import URL
|
||||
|
||||
from ._cookie_helpers import parse_set_cookie_headers
|
||||
from .typedefs import LooseCookies
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .web_app import Application
|
||||
from .web_exceptions import HTTPException
|
||||
from .web_request import BaseRequest, Request
|
||||
from .web_response import StreamResponse
|
||||
else:
|
||||
BaseRequest = Request = Application = StreamResponse = None
|
||||
HTTPException = None
|
||||
|
||||
|
||||
class AbstractRouter(ABC):
|
||||
def __init__(self) -> None:
|
||||
self._frozen = False
|
||||
|
||||
def post_init(self, app: Application) -> None:
|
||||
"""Post init stage.
|
||||
|
||||
Not an abstract method for sake of backward compatibility,
|
||||
but if the router wants to be aware of the application
|
||||
it can override this.
|
||||
"""
|
||||
|
||||
@property
|
||||
def frozen(self) -> bool:
|
||||
return self._frozen
|
||||
|
||||
def freeze(self) -> None:
|
||||
"""Freeze router."""
|
||||
self._frozen = True
|
||||
|
||||
@abstractmethod
|
||||
async def resolve(self, request: Request) -> "AbstractMatchInfo":
|
||||
"""Return MATCH_INFO for given request"""
|
||||
|
||||
|
||||
class AbstractMatchInfo(ABC):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@property # pragma: no branch
|
||||
@abstractmethod
|
||||
def handler(self) -> Callable[[Request], Awaitable[StreamResponse]]:
|
||||
"""Execute matched request handler"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def expect_handler(
|
||||
self,
|
||||
) -> Callable[[Request], Awaitable[Optional[StreamResponse]]]:
|
||||
"""Expect handler for 100-continue processing"""
|
||||
|
||||
@property # pragma: no branch
|
||||
@abstractmethod
|
||||
def http_exception(self) -> Optional[HTTPException]:
|
||||
"""HTTPException instance raised on router's resolving, or None"""
|
||||
|
||||
@abstractmethod # pragma: no branch
|
||||
def get_info(self) -> Dict[str, Any]:
|
||||
"""Return a dict with additional info useful for introspection"""
|
||||
|
||||
@property # pragma: no branch
|
||||
@abstractmethod
|
||||
def apps(self) -> Tuple[Application, ...]:
|
||||
"""Stack of nested applications.
|
||||
|
||||
Top level application is left-most element.
|
||||
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def add_app(self, app: Application) -> None:
|
||||
"""Add application to the nested apps stack."""
|
||||
|
||||
@abstractmethod
|
||||
def freeze(self) -> None:
|
||||
"""Freeze the match info.
|
||||
|
||||
The method is called after route resolution.
|
||||
|
||||
After the call .add_app() is forbidden.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class AbstractView(ABC):
|
||||
"""Abstract class based view."""
|
||||
|
||||
def __init__(self, request: Request) -> None:
|
||||
self._request = request
|
||||
|
||||
@property
|
||||
def request(self) -> Request:
|
||||
"""Request instance."""
|
||||
return self._request
|
||||
|
||||
@abstractmethod
|
||||
def __await__(self) -> Generator[None, None, StreamResponse]:
|
||||
"""Execute the view handler."""
|
||||
|
||||
|
||||
class ResolveResult(TypedDict):
|
||||
"""Resolve result.
|
||||
|
||||
This is the result returned from an AbstractResolver's
|
||||
resolve method.
|
||||
|
||||
:param hostname: The hostname that was provided.
|
||||
:param host: The IP address that was resolved.
|
||||
:param port: The port that was resolved.
|
||||
:param family: The address family that was resolved.
|
||||
:param proto: The protocol that was resolved.
|
||||
:param flags: The flags that were resolved.
|
||||
"""
|
||||
|
||||
hostname: str
|
||||
host: str
|
||||
port: int
|
||||
family: int
|
||||
proto: int
|
||||
flags: int
|
||||
|
||||
|
||||
class AbstractResolver(ABC):
|
||||
"""Abstract DNS resolver."""
|
||||
|
||||
@abstractmethod
|
||||
async def resolve(
|
||||
self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET
|
||||
) -> List[ResolveResult]:
|
||||
"""Return IP address for given hostname"""
|
||||
|
||||
@abstractmethod
|
||||
async def close(self) -> None:
|
||||
"""Release resolver"""
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
IterableBase = Iterable[Morsel[str]]
|
||||
else:
|
||||
IterableBase = Iterable
|
||||
|
||||
|
||||
ClearCookiePredicate = Callable[["Morsel[str]"], bool]
|
||||
|
||||
|
||||
class AbstractCookieJar(Sized, IterableBase):
|
||||
"""Abstract Cookie Jar."""
|
||||
|
||||
def __init__(self, *, loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
|
||||
self._loop = loop or asyncio.get_running_loop()
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def quote_cookie(self) -> bool:
|
||||
"""Return True if cookies should be quoted."""
|
||||
|
||||
@abstractmethod
|
||||
def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None:
|
||||
"""Clear all cookies if no predicate is passed."""
|
||||
|
||||
@abstractmethod
|
||||
def clear_domain(self, domain: str) -> None:
|
||||
"""Clear all cookies for domain and all subdomains."""
|
||||
|
||||
@abstractmethod
|
||||
def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> None:
|
||||
"""Update cookies."""
|
||||
|
||||
def update_cookies_from_headers(
|
||||
self, headers: Sequence[str], response_url: URL
|
||||
) -> None:
|
||||
"""Update cookies from raw Set-Cookie headers."""
|
||||
if headers and (cookies_to_update := parse_set_cookie_headers(headers)):
|
||||
self.update_cookies(cookies_to_update, response_url)
|
||||
|
||||
@abstractmethod
|
||||
def filter_cookies(self, request_url: URL) -> "BaseCookie[str]":
|
||||
"""Return the jar's cookies filtered by their attributes."""
|
||||
|
||||
|
||||
class AbstractStreamWriter(ABC):
|
||||
"""Abstract stream writer."""
|
||||
|
||||
buffer_size: int = 0
|
||||
output_size: int = 0
|
||||
length: Optional[int] = 0
|
||||
|
||||
@abstractmethod
|
||||
async def write(self, chunk: Union[bytes, bytearray, memoryview]) -> None:
|
||||
"""Write chunk into stream."""
|
||||
|
||||
@abstractmethod
|
||||
async def write_eof(self, chunk: bytes = b"") -> None:
|
||||
"""Write last chunk."""
|
||||
|
||||
@abstractmethod
|
||||
async def drain(self) -> None:
|
||||
"""Flush the write buffer."""
|
||||
|
||||
@abstractmethod
|
||||
def enable_compression(
|
||||
self, encoding: str = "deflate", strategy: Optional[int] = None
|
||||
) -> None:
|
||||
"""Enable HTTP body compression"""
|
||||
|
||||
@abstractmethod
|
||||
def enable_chunking(self) -> None:
|
||||
"""Enable HTTP chunked mode"""
|
||||
|
||||
@abstractmethod
|
||||
async def write_headers(
|
||||
self, status_line: str, headers: "CIMultiDict[str]"
|
||||
) -> None:
|
||||
"""Write HTTP headers"""
|
||||
|
||||
def send_headers(self) -> None:
|
||||
"""Force sending buffered headers if not already sent.
|
||||
|
||||
Required only if write_headers() buffers headers instead of sending immediately.
|
||||
For backwards compatibility, this method does nothing by default.
|
||||
"""
|
||||
|
||||
|
||||
class AbstractAccessLogger(ABC):
|
||||
"""Abstract writer to access log."""
|
||||
|
||||
__slots__ = ("logger", "log_format")
|
||||
|
||||
def __init__(self, logger: logging.Logger, log_format: str) -> None:
|
||||
self.logger = logger
|
||||
self.log_format = log_format
|
||||
|
||||
@abstractmethod
|
||||
def log(self, request: BaseRequest, response: StreamResponse, time: float) -> None:
|
||||
"""Emit log to logger."""
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
"""Check if logger is enabled."""
|
||||
return True
|
||||
100
venv/lib/python3.12/site-packages/aiohttp/base_protocol.py
Normal file
100
venv/lib/python3.12/site-packages/aiohttp/base_protocol.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import asyncio
|
||||
from typing import Optional, cast
|
||||
|
||||
from .client_exceptions import ClientConnectionResetError
|
||||
from .helpers import set_exception
|
||||
from .tcp_helpers import tcp_nodelay
|
||||
|
||||
|
||||
class BaseProtocol(asyncio.Protocol):
|
||||
__slots__ = (
|
||||
"_loop",
|
||||
"_paused",
|
||||
"_drain_waiter",
|
||||
"_connection_lost",
|
||||
"_reading_paused",
|
||||
"transport",
|
||||
)
|
||||
|
||||
def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
|
||||
self._loop: asyncio.AbstractEventLoop = loop
|
||||
self._paused = False
|
||||
self._drain_waiter: Optional[asyncio.Future[None]] = None
|
||||
self._reading_paused = False
|
||||
|
||||
self.transport: Optional[asyncio.Transport] = None
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
"""Return True if the connection is open."""
|
||||
return self.transport is not None
|
||||
|
||||
@property
|
||||
def writing_paused(self) -> bool:
|
||||
return self._paused
|
||||
|
||||
def pause_writing(self) -> None:
|
||||
assert not self._paused
|
||||
self._paused = True
|
||||
|
||||
def resume_writing(self) -> None:
|
||||
assert self._paused
|
||||
self._paused = False
|
||||
|
||||
waiter = self._drain_waiter
|
||||
if waiter is not None:
|
||||
self._drain_waiter = None
|
||||
if not waiter.done():
|
||||
waiter.set_result(None)
|
||||
|
||||
def pause_reading(self) -> None:
|
||||
if not self._reading_paused and self.transport is not None:
|
||||
try:
|
||||
self.transport.pause_reading()
|
||||
except (AttributeError, NotImplementedError, RuntimeError):
|
||||
pass
|
||||
self._reading_paused = True
|
||||
|
||||
def resume_reading(self) -> None:
|
||||
if self._reading_paused and self.transport is not None:
|
||||
try:
|
||||
self.transport.resume_reading()
|
||||
except (AttributeError, NotImplementedError, RuntimeError):
|
||||
pass
|
||||
self._reading_paused = False
|
||||
|
||||
def connection_made(self, transport: asyncio.BaseTransport) -> None:
|
||||
tr = cast(asyncio.Transport, transport)
|
||||
tcp_nodelay(tr, True)
|
||||
self.transport = tr
|
||||
|
||||
def connection_lost(self, exc: Optional[BaseException]) -> None:
|
||||
# Wake up the writer if currently paused.
|
||||
self.transport = None
|
||||
if not self._paused:
|
||||
return
|
||||
waiter = self._drain_waiter
|
||||
if waiter is None:
|
||||
return
|
||||
self._drain_waiter = None
|
||||
if waiter.done():
|
||||
return
|
||||
if exc is None:
|
||||
waiter.set_result(None)
|
||||
else:
|
||||
set_exception(
|
||||
waiter,
|
||||
ConnectionError("Connection lost"),
|
||||
exc,
|
||||
)
|
||||
|
||||
async def _drain_helper(self) -> None:
|
||||
if self.transport is None:
|
||||
raise ClientConnectionResetError("Connection lost")
|
||||
if not self._paused:
|
||||
return
|
||||
waiter = self._drain_waiter
|
||||
if waiter is None:
|
||||
waiter = self._loop.create_future()
|
||||
self._drain_waiter = waiter
|
||||
await asyncio.shield(waiter)
|
||||
1646
venv/lib/python3.12/site-packages/aiohttp/client.py
Normal file
1646
venv/lib/python3.12/site-packages/aiohttp/client.py
Normal file
File diff suppressed because it is too large
Load Diff
432
venv/lib/python3.12/site-packages/aiohttp/client_exceptions.py
Normal file
432
venv/lib/python3.12/site-packages/aiohttp/client_exceptions.py
Normal file
@@ -0,0 +1,432 @@
|
||||
"""HTTP related errors."""
|
||||
|
||||
import asyncio
|
||||
import warnings
|
||||
from typing import TYPE_CHECKING, Optional, Tuple, Union
|
||||
|
||||
from multidict import MultiMapping
|
||||
|
||||
from .typedefs import StrOrURL
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ssl
|
||||
|
||||
SSLContext = ssl.SSLContext
|
||||
else:
|
||||
try:
|
||||
import ssl
|
||||
|
||||
SSLContext = ssl.SSLContext
|
||||
except ImportError: # pragma: no cover
|
||||
ssl = SSLContext = None # type: ignore[assignment]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .client_reqrep import ClientResponse, ConnectionKey, Fingerprint, RequestInfo
|
||||
from .http_parser import RawResponseMessage
|
||||
else:
|
||||
RequestInfo = ClientResponse = ConnectionKey = RawResponseMessage = None
|
||||
|
||||
__all__ = (
|
||||
"ClientError",
|
||||
"ClientConnectionError",
|
||||
"ClientConnectionResetError",
|
||||
"ClientOSError",
|
||||
"ClientConnectorError",
|
||||
"ClientProxyConnectionError",
|
||||
"ClientSSLError",
|
||||
"ClientConnectorDNSError",
|
||||
"ClientConnectorSSLError",
|
||||
"ClientConnectorCertificateError",
|
||||
"ConnectionTimeoutError",
|
||||
"SocketTimeoutError",
|
||||
"ServerConnectionError",
|
||||
"ServerTimeoutError",
|
||||
"ServerDisconnectedError",
|
||||
"ServerFingerprintMismatch",
|
||||
"ClientResponseError",
|
||||
"ClientHttpProxyError",
|
||||
"WSServerHandshakeError",
|
||||
"ContentTypeError",
|
||||
"ClientPayloadError",
|
||||
"InvalidURL",
|
||||
"InvalidUrlClientError",
|
||||
"RedirectClientError",
|
||||
"NonHttpUrlClientError",
|
||||
"InvalidUrlRedirectClientError",
|
||||
"NonHttpUrlRedirectClientError",
|
||||
"WSMessageTypeError",
|
||||
)
|
||||
|
||||
|
||||
class ClientError(Exception):
|
||||
"""Base class for client connection errors."""
|
||||
|
||||
|
||||
class ClientResponseError(ClientError):
|
||||
"""Base class for exceptions that occur after getting a response.
|
||||
|
||||
request_info: An instance of RequestInfo.
|
||||
history: A sequence of responses, if redirects occurred.
|
||||
status: HTTP status code.
|
||||
message: Error message.
|
||||
headers: Response headers.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
request_info: RequestInfo,
|
||||
history: Tuple[ClientResponse, ...],
|
||||
*,
|
||||
code: Optional[int] = None,
|
||||
status: Optional[int] = None,
|
||||
message: str = "",
|
||||
headers: Optional[MultiMapping[str]] = None,
|
||||
) -> None:
|
||||
self.request_info = request_info
|
||||
if code is not None:
|
||||
if status is not None:
|
||||
raise ValueError(
|
||||
"Both code and status arguments are provided; "
|
||||
"code is deprecated, use status instead"
|
||||
)
|
||||
warnings.warn(
|
||||
"code argument is deprecated, use status instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if status is not None:
|
||||
self.status = status
|
||||
elif code is not None:
|
||||
self.status = code
|
||||
else:
|
||||
self.status = 0
|
||||
self.message = message
|
||||
self.headers = headers
|
||||
self.history = history
|
||||
self.args = (request_info, history)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "{}, message={!r}, url={!r}".format(
|
||||
self.status,
|
||||
self.message,
|
||||
str(self.request_info.real_url),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
args = f"{self.request_info!r}, {self.history!r}"
|
||||
if self.status != 0:
|
||||
args += f", status={self.status!r}"
|
||||
if self.message != "":
|
||||
args += f", message={self.message!r}"
|
||||
if self.headers is not None:
|
||||
args += f", headers={self.headers!r}"
|
||||
return f"{type(self).__name__}({args})"
|
||||
|
||||
@property
|
||||
def code(self) -> int:
|
||||
warnings.warn(
|
||||
"code property is deprecated, use status instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.status
|
||||
|
||||
@code.setter
|
||||
def code(self, value: int) -> None:
|
||||
warnings.warn(
|
||||
"code property is deprecated, use status instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.status = value
|
||||
|
||||
|
||||
class ContentTypeError(ClientResponseError):
|
||||
"""ContentType found is not valid."""
|
||||
|
||||
|
||||
class WSServerHandshakeError(ClientResponseError):
|
||||
"""websocket server handshake error."""
|
||||
|
||||
|
||||
class ClientHttpProxyError(ClientResponseError):
|
||||
"""HTTP proxy error.
|
||||
|
||||
Raised in :class:`aiohttp.connector.TCPConnector` if
|
||||
proxy responds with status other than ``200 OK``
|
||||
on ``CONNECT`` request.
|
||||
"""
|
||||
|
||||
|
||||
class TooManyRedirects(ClientResponseError):
|
||||
"""Client was redirected too many times."""
|
||||
|
||||
|
||||
class ClientConnectionError(ClientError):
|
||||
"""Base class for client socket errors."""
|
||||
|
||||
|
||||
class ClientConnectionResetError(ClientConnectionError, ConnectionResetError):
|
||||
"""ConnectionResetError"""
|
||||
|
||||
|
||||
class ClientOSError(ClientConnectionError, OSError):
|
||||
"""OSError error."""
|
||||
|
||||
|
||||
class ClientConnectorError(ClientOSError):
|
||||
"""Client connector error.
|
||||
|
||||
Raised in :class:`aiohttp.connector.TCPConnector` if
|
||||
a connection can not be established.
|
||||
"""
|
||||
|
||||
def __init__(self, connection_key: ConnectionKey, os_error: OSError) -> None:
|
||||
self._conn_key = connection_key
|
||||
self._os_error = os_error
|
||||
super().__init__(os_error.errno, os_error.strerror)
|
||||
self.args = (connection_key, os_error)
|
||||
|
||||
@property
|
||||
def os_error(self) -> OSError:
|
||||
return self._os_error
|
||||
|
||||
@property
|
||||
def host(self) -> str:
|
||||
return self._conn_key.host
|
||||
|
||||
@property
|
||||
def port(self) -> Optional[int]:
|
||||
return self._conn_key.port
|
||||
|
||||
@property
|
||||
def ssl(self) -> Union[SSLContext, bool, "Fingerprint"]:
|
||||
return self._conn_key.ssl
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "Cannot connect to host {0.host}:{0.port} ssl:{1} [{2}]".format(
|
||||
self, "default" if self.ssl is True else self.ssl, self.strerror
|
||||
)
|
||||
|
||||
# OSError.__reduce__ does too much black magick
|
||||
__reduce__ = BaseException.__reduce__
|
||||
|
||||
|
||||
class ClientConnectorDNSError(ClientConnectorError):
|
||||
"""DNS resolution failed during client connection.
|
||||
|
||||
Raised in :class:`aiohttp.connector.TCPConnector` if
|
||||
DNS resolution fails.
|
||||
"""
|
||||
|
||||
|
||||
class ClientProxyConnectionError(ClientConnectorError):
|
||||
"""Proxy connection error.
|
||||
|
||||
Raised in :class:`aiohttp.connector.TCPConnector` if
|
||||
connection to proxy can not be established.
|
||||
"""
|
||||
|
||||
|
||||
class UnixClientConnectorError(ClientConnectorError):
|
||||
"""Unix connector error.
|
||||
|
||||
Raised in :py:class:`aiohttp.connector.UnixConnector`
|
||||
if connection to unix socket can not be established.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, path: str, connection_key: ConnectionKey, os_error: OSError
|
||||
) -> None:
|
||||
self._path = path
|
||||
super().__init__(connection_key, os_error)
|
||||
|
||||
@property
|
||||
def path(self) -> str:
|
||||
return self._path
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "Cannot connect to unix socket {0.path} ssl:{1} [{2}]".format(
|
||||
self, "default" if self.ssl is True else self.ssl, self.strerror
|
||||
)
|
||||
|
||||
|
||||
class ServerConnectionError(ClientConnectionError):
|
||||
"""Server connection errors."""
|
||||
|
||||
|
||||
class ServerDisconnectedError(ServerConnectionError):
|
||||
"""Server disconnected."""
|
||||
|
||||
def __init__(self, message: Union[RawResponseMessage, str, None] = None) -> None:
|
||||
if message is None:
|
||||
message = "Server disconnected"
|
||||
|
||||
self.args = (message,)
|
||||
self.message = message
|
||||
|
||||
|
||||
class ServerTimeoutError(ServerConnectionError, asyncio.TimeoutError):
|
||||
"""Server timeout error."""
|
||||
|
||||
|
||||
class ConnectionTimeoutError(ServerTimeoutError):
|
||||
"""Connection timeout error."""
|
||||
|
||||
|
||||
class SocketTimeoutError(ServerTimeoutError):
|
||||
"""Socket timeout error."""
|
||||
|
||||
|
||||
class ServerFingerprintMismatch(ServerConnectionError):
|
||||
"""SSL certificate does not match expected fingerprint."""
|
||||
|
||||
def __init__(self, expected: bytes, got: bytes, host: str, port: int) -> None:
|
||||
self.expected = expected
|
||||
self.got = got
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.args = (expected, got, host, port)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<{} expected={!r} got={!r} host={!r} port={!r}>".format(
|
||||
self.__class__.__name__, self.expected, self.got, self.host, self.port
|
||||
)
|
||||
|
||||
|
||||
class ClientPayloadError(ClientError):
|
||||
"""Response payload error."""
|
||||
|
||||
|
||||
class InvalidURL(ClientError, ValueError):
|
||||
"""Invalid URL.
|
||||
|
||||
URL used for fetching is malformed, e.g. it doesn't contains host
|
||||
part.
|
||||
"""
|
||||
|
||||
# Derive from ValueError for backward compatibility
|
||||
|
||||
def __init__(self, url: StrOrURL, description: Union[str, None] = None) -> None:
|
||||
# The type of url is not yarl.URL because the exception can be raised
|
||||
# on URL(url) call
|
||||
self._url = url
|
||||
self._description = description
|
||||
|
||||
if description:
|
||||
super().__init__(url, description)
|
||||
else:
|
||||
super().__init__(url)
|
||||
|
||||
@property
|
||||
def url(self) -> StrOrURL:
|
||||
return self._url
|
||||
|
||||
@property
|
||||
def description(self) -> "str | None":
|
||||
return self._description
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__} {self}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self._description:
|
||||
return f"{self._url} - {self._description}"
|
||||
return str(self._url)
|
||||
|
||||
|
||||
class InvalidUrlClientError(InvalidURL):
|
||||
"""Invalid URL client error."""
|
||||
|
||||
|
||||
class RedirectClientError(ClientError):
|
||||
"""Client redirect error."""
|
||||
|
||||
|
||||
class NonHttpUrlClientError(ClientError):
|
||||
"""Non http URL client error."""
|
||||
|
||||
|
||||
class InvalidUrlRedirectClientError(InvalidUrlClientError, RedirectClientError):
|
||||
"""Invalid URL redirect client error."""
|
||||
|
||||
|
||||
class NonHttpUrlRedirectClientError(NonHttpUrlClientError, RedirectClientError):
|
||||
"""Non http URL redirect client error."""
|
||||
|
||||
|
||||
class ClientSSLError(ClientConnectorError):
|
||||
"""Base error for ssl.*Errors."""
|
||||
|
||||
|
||||
if ssl is not None:
|
||||
cert_errors = (ssl.CertificateError,)
|
||||
cert_errors_bases = (
|
||||
ClientSSLError,
|
||||
ssl.CertificateError,
|
||||
)
|
||||
|
||||
ssl_errors = (ssl.SSLError,)
|
||||
ssl_error_bases = (ClientSSLError, ssl.SSLError)
|
||||
else: # pragma: no cover
|
||||
cert_errors = tuple()
|
||||
cert_errors_bases = (
|
||||
ClientSSLError,
|
||||
ValueError,
|
||||
)
|
||||
|
||||
ssl_errors = tuple()
|
||||
ssl_error_bases = (ClientSSLError,)
|
||||
|
||||
|
||||
class ClientConnectorSSLError(*ssl_error_bases): # type: ignore[misc]
|
||||
"""Response ssl error."""
|
||||
|
||||
|
||||
class ClientConnectorCertificateError(*cert_errors_bases): # type: ignore[misc]
|
||||
"""Response certificate error."""
|
||||
|
||||
_conn_key: ConnectionKey
|
||||
|
||||
def __init__(
|
||||
# TODO: If we require ssl in future, this can become ssl.CertificateError
|
||||
self,
|
||||
connection_key: ConnectionKey,
|
||||
certificate_error: Exception,
|
||||
) -> None:
|
||||
if isinstance(certificate_error, cert_errors + (OSError,)):
|
||||
# ssl.CertificateError has errno and strerror, so we should be fine
|
||||
os_error = certificate_error
|
||||
else:
|
||||
os_error = OSError()
|
||||
|
||||
super().__init__(connection_key, os_error)
|
||||
self._certificate_error = certificate_error
|
||||
self.args = (connection_key, certificate_error)
|
||||
|
||||
@property
|
||||
def certificate_error(self) -> Exception:
|
||||
return self._certificate_error
|
||||
|
||||
@property
|
||||
def host(self) -> str:
|
||||
return self._conn_key.host
|
||||
|
||||
@property
|
||||
def port(self) -> Optional[int]:
|
||||
return self._conn_key.port
|
||||
|
||||
@property
|
||||
def ssl(self) -> bool:
|
||||
return self._conn_key.is_ssl
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
"Cannot connect to host {0.host}:{0.port} ssl:{0.ssl} "
|
||||
"[{0.certificate_error.__class__.__name__}: "
|
||||
"{0.certificate_error.args}]".format(self)
|
||||
)
|
||||
|
||||
|
||||
class WSMessageTypeError(TypeError):
|
||||
"""WebSocket message type is not valid."""
|
||||
@@ -0,0 +1,480 @@
|
||||
"""
|
||||
Digest authentication middleware for aiohttp client.
|
||||
|
||||
This middleware implements HTTP Digest Authentication according to RFC 7616,
|
||||
providing a more secure alternative to Basic Authentication. It supports all
|
||||
standard hash algorithms including MD5, SHA, SHA-256, SHA-512 and their session
|
||||
variants, as well as both 'auth' and 'auth-int' quality of protection (qop) options.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from typing import (
|
||||
Callable,
|
||||
Dict,
|
||||
Final,
|
||||
FrozenSet,
|
||||
List,
|
||||
Literal,
|
||||
Tuple,
|
||||
TypedDict,
|
||||
Union,
|
||||
)
|
||||
|
||||
from yarl import URL
|
||||
|
||||
from . import hdrs
|
||||
from .client_exceptions import ClientError
|
||||
from .client_middlewares import ClientHandlerType
|
||||
from .client_reqrep import ClientRequest, ClientResponse
|
||||
from .payload import Payload
|
||||
|
||||
|
||||
class DigestAuthChallenge(TypedDict, total=False):
|
||||
realm: str
|
||||
nonce: str
|
||||
qop: str
|
||||
algorithm: str
|
||||
opaque: str
|
||||
domain: str
|
||||
stale: str
|
||||
|
||||
|
||||
DigestFunctions: Dict[str, Callable[[bytes], "hashlib._Hash"]] = {
|
||||
"MD5": hashlib.md5,
|
||||
"MD5-SESS": hashlib.md5,
|
||||
"SHA": hashlib.sha1,
|
||||
"SHA-SESS": hashlib.sha1,
|
||||
"SHA256": hashlib.sha256,
|
||||
"SHA256-SESS": hashlib.sha256,
|
||||
"SHA-256": hashlib.sha256,
|
||||
"SHA-256-SESS": hashlib.sha256,
|
||||
"SHA512": hashlib.sha512,
|
||||
"SHA512-SESS": hashlib.sha512,
|
||||
"SHA-512": hashlib.sha512,
|
||||
"SHA-512-SESS": hashlib.sha512,
|
||||
}
|
||||
|
||||
|
||||
# Compile the regex pattern once at module level for performance
|
||||
_HEADER_PAIRS_PATTERN = re.compile(
|
||||
r'(?:^|\s|,\s*)(\w+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))'
|
||||
if sys.version_info < (3, 11)
|
||||
else r'(?:^|\s|,\s*)((?>\w+))\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))'
|
||||
# +------------|--------|--|-|-|--|----|------|----|--||-----|-> Match valid start/sep
|
||||
# +--------|--|-|-|--|----|------|----|--||-----|-> alphanumeric key (atomic
|
||||
# | | | | | | | | || | group reduces backtracking)
|
||||
# +--|-|-|--|----|------|----|--||-----|-> maybe whitespace
|
||||
# | | | | | | | || |
|
||||
# +-|-|--|----|------|----|--||-----|-> = (delimiter)
|
||||
# +-|--|----|------|----|--||-----|-> maybe whitespace
|
||||
# | | | | | || |
|
||||
# +--|----|------|----|--||-----|-> group quoted or unquoted
|
||||
# | | | | || |
|
||||
# +----|------|----|--||-----|-> if quoted...
|
||||
# +------|----|--||-----|-> anything but " or \
|
||||
# +----|--||-----|-> escaped characters allowed
|
||||
# +--||-----|-> or can be empty string
|
||||
# || |
|
||||
# +|-----|-> if unquoted...
|
||||
# +-----|-> anything but , or <space>
|
||||
# +-> at least one char req'd
|
||||
)
|
||||
|
||||
|
||||
# RFC 7616: Challenge parameters to extract
|
||||
CHALLENGE_FIELDS: Final[
|
||||
Tuple[
|
||||
Literal["realm", "nonce", "qop", "algorithm", "opaque", "domain", "stale"], ...
|
||||
]
|
||||
] = (
|
||||
"realm",
|
||||
"nonce",
|
||||
"qop",
|
||||
"algorithm",
|
||||
"opaque",
|
||||
"domain",
|
||||
"stale",
|
||||
)
|
||||
|
||||
# Supported digest authentication algorithms
|
||||
# Use a tuple of sorted keys for predictable documentation and error messages
|
||||
SUPPORTED_ALGORITHMS: Final[Tuple[str, ...]] = tuple(sorted(DigestFunctions.keys()))
|
||||
|
||||
# RFC 7616: Fields that require quoting in the Digest auth header
|
||||
# These fields must be enclosed in double quotes in the Authorization header.
|
||||
# Algorithm, qop, and nc are never quoted per RFC specifications.
|
||||
# This frozen set is used by the template-based header construction to
|
||||
# automatically determine which fields need quotes.
|
||||
QUOTED_AUTH_FIELDS: Final[FrozenSet[str]] = frozenset(
|
||||
{"username", "realm", "nonce", "uri", "response", "opaque", "cnonce"}
|
||||
)
|
||||
|
||||
|
||||
def escape_quotes(value: str) -> str:
|
||||
"""Escape double quotes for HTTP header values."""
|
||||
return value.replace('"', '\\"')
|
||||
|
||||
|
||||
def unescape_quotes(value: str) -> str:
|
||||
"""Unescape double quotes in HTTP header values."""
|
||||
return value.replace('\\"', '"')
|
||||
|
||||
|
||||
def parse_header_pairs(header: str) -> Dict[str, str]:
|
||||
"""
|
||||
Parse key-value pairs from WWW-Authenticate or similar HTTP headers.
|
||||
|
||||
This function handles the complex format of WWW-Authenticate header values,
|
||||
supporting both quoted and unquoted values, proper handling of commas in
|
||||
quoted values, and whitespace variations per RFC 7616.
|
||||
|
||||
Examples of supported formats:
|
||||
- key1="value1", key2=value2
|
||||
- key1 = "value1" , key2="value, with, commas"
|
||||
- key1=value1,key2="value2"
|
||||
- realm="example.com", nonce="12345", qop="auth"
|
||||
|
||||
Args:
|
||||
header: The header value string to parse
|
||||
|
||||
Returns:
|
||||
Dictionary mapping parameter names to their values
|
||||
"""
|
||||
return {
|
||||
stripped_key: unescape_quotes(quoted_val) if quoted_val else unquoted_val
|
||||
for key, quoted_val, unquoted_val in _HEADER_PAIRS_PATTERN.findall(header)
|
||||
if (stripped_key := key.strip())
|
||||
}
|
||||
|
||||
|
||||
class DigestAuthMiddleware:
|
||||
"""
|
||||
HTTP digest authentication middleware for aiohttp client.
|
||||
|
||||
This middleware intercepts 401 Unauthorized responses containing a Digest
|
||||
authentication challenge, calculates the appropriate digest credentials,
|
||||
and automatically retries the request with the proper Authorization header.
|
||||
|
||||
Features:
|
||||
- Handles all aspects of Digest authentication handshake automatically
|
||||
- Supports all standard hash algorithms:
|
||||
- MD5, MD5-SESS
|
||||
- SHA, SHA-SESS
|
||||
- SHA256, SHA256-SESS, SHA-256, SHA-256-SESS
|
||||
- SHA512, SHA512-SESS, SHA-512, SHA-512-SESS
|
||||
- Supports 'auth' and 'auth-int' quality of protection modes
|
||||
- Properly handles quoted strings and parameter parsing
|
||||
- Includes replay attack protection with client nonce count tracking
|
||||
- Supports preemptive authentication per RFC 7616 Section 3.6
|
||||
|
||||
Standards compliance:
|
||||
- RFC 7616: HTTP Digest Access Authentication (primary reference)
|
||||
- RFC 2617: HTTP Authentication (deprecated by RFC 7616)
|
||||
- RFC 1945: Section 11.1 (username restrictions)
|
||||
|
||||
Implementation notes:
|
||||
The core digest calculation is inspired by the implementation in
|
||||
https://github.com/requests/requests/blob/v2.18.4/requests/auth.py
|
||||
with added support for modern digest auth features and error handling.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
login: str,
|
||||
password: str,
|
||||
preemptive: bool = True,
|
||||
) -> None:
|
||||
if login is None:
|
||||
raise ValueError("None is not allowed as login value")
|
||||
|
||||
if password is None:
|
||||
raise ValueError("None is not allowed as password value")
|
||||
|
||||
if ":" in login:
|
||||
raise ValueError('A ":" is not allowed in username (RFC 1945#section-11.1)')
|
||||
|
||||
self._login_str: Final[str] = login
|
||||
self._login_bytes: Final[bytes] = login.encode("utf-8")
|
||||
self._password_bytes: Final[bytes] = password.encode("utf-8")
|
||||
|
||||
self._last_nonce_bytes = b""
|
||||
self._nonce_count = 0
|
||||
self._challenge: DigestAuthChallenge = {}
|
||||
self._preemptive: bool = preemptive
|
||||
# Set of URLs defining the protection space
|
||||
self._protection_space: List[str] = []
|
||||
|
||||
async def _encode(
|
||||
self, method: str, url: URL, body: Union[Payload, Literal[b""]]
|
||||
) -> str:
|
||||
"""
|
||||
Build digest authorization header for the current challenge.
|
||||
|
||||
Args:
|
||||
method: The HTTP method (GET, POST, etc.)
|
||||
url: The request URL
|
||||
body: The request body (used for qop=auth-int)
|
||||
|
||||
Returns:
|
||||
A fully formatted Digest authorization header string
|
||||
|
||||
Raises:
|
||||
ClientError: If the challenge is missing required parameters or
|
||||
contains unsupported values
|
||||
|
||||
"""
|
||||
challenge = self._challenge
|
||||
if "realm" not in challenge:
|
||||
raise ClientError(
|
||||
"Malformed Digest auth challenge: Missing 'realm' parameter"
|
||||
)
|
||||
|
||||
if "nonce" not in challenge:
|
||||
raise ClientError(
|
||||
"Malformed Digest auth challenge: Missing 'nonce' parameter"
|
||||
)
|
||||
|
||||
# Empty realm values are allowed per RFC 7616 (SHOULD, not MUST, contain host name)
|
||||
realm = challenge["realm"]
|
||||
nonce = challenge["nonce"]
|
||||
|
||||
# Empty nonce values are not allowed as they are security-critical for replay protection
|
||||
if not nonce:
|
||||
raise ClientError(
|
||||
"Security issue: Digest auth challenge contains empty 'nonce' value"
|
||||
)
|
||||
|
||||
qop_raw = challenge.get("qop", "")
|
||||
# Preserve original algorithm case for response while using uppercase for processing
|
||||
algorithm_original = challenge.get("algorithm", "MD5")
|
||||
algorithm = algorithm_original.upper()
|
||||
opaque = challenge.get("opaque", "")
|
||||
|
||||
# Convert string values to bytes once
|
||||
nonce_bytes = nonce.encode("utf-8")
|
||||
realm_bytes = realm.encode("utf-8")
|
||||
path = URL(url).path_qs
|
||||
|
||||
# Process QoP
|
||||
qop = ""
|
||||
qop_bytes = b""
|
||||
if qop_raw:
|
||||
valid_qops = {"auth", "auth-int"}.intersection(
|
||||
{q.strip() for q in qop_raw.split(",") if q.strip()}
|
||||
)
|
||||
if not valid_qops:
|
||||
raise ClientError(
|
||||
f"Digest auth error: Unsupported Quality of Protection (qop) value(s): {qop_raw}"
|
||||
)
|
||||
|
||||
qop = "auth-int" if "auth-int" in valid_qops else "auth"
|
||||
qop_bytes = qop.encode("utf-8")
|
||||
|
||||
if algorithm not in DigestFunctions:
|
||||
raise ClientError(
|
||||
f"Digest auth error: Unsupported hash algorithm: {algorithm}. "
|
||||
f"Supported algorithms: {', '.join(SUPPORTED_ALGORITHMS)}"
|
||||
)
|
||||
hash_fn: Final = DigestFunctions[algorithm]
|
||||
|
||||
def H(x: bytes) -> bytes:
|
||||
"""RFC 7616 Section 3: Hash function H(data) = hex(hash(data))."""
|
||||
return hash_fn(x).hexdigest().encode()
|
||||
|
||||
def KD(s: bytes, d: bytes) -> bytes:
|
||||
"""RFC 7616 Section 3: KD(secret, data) = H(concat(secret, ":", data))."""
|
||||
return H(b":".join((s, d)))
|
||||
|
||||
# Calculate A1 and A2
|
||||
A1 = b":".join((self._login_bytes, realm_bytes, self._password_bytes))
|
||||
A2 = f"{method.upper()}:{path}".encode()
|
||||
if qop == "auth-int":
|
||||
if isinstance(body, Payload): # will always be empty bytes unless Payload
|
||||
entity_bytes = await body.as_bytes() # Get bytes from Payload
|
||||
else:
|
||||
entity_bytes = body
|
||||
entity_hash = H(entity_bytes)
|
||||
A2 = b":".join((A2, entity_hash))
|
||||
|
||||
HA1 = H(A1)
|
||||
HA2 = H(A2)
|
||||
|
||||
# Nonce count handling
|
||||
if nonce_bytes == self._last_nonce_bytes:
|
||||
self._nonce_count += 1
|
||||
else:
|
||||
self._nonce_count = 1
|
||||
|
||||
self._last_nonce_bytes = nonce_bytes
|
||||
ncvalue = f"{self._nonce_count:08x}"
|
||||
ncvalue_bytes = ncvalue.encode("utf-8")
|
||||
|
||||
# Generate client nonce
|
||||
cnonce = hashlib.sha1(
|
||||
b"".join(
|
||||
[
|
||||
str(self._nonce_count).encode("utf-8"),
|
||||
nonce_bytes,
|
||||
time.ctime().encode("utf-8"),
|
||||
os.urandom(8),
|
||||
]
|
||||
)
|
||||
).hexdigest()[:16]
|
||||
cnonce_bytes = cnonce.encode("utf-8")
|
||||
|
||||
# Special handling for session-based algorithms
|
||||
if algorithm.upper().endswith("-SESS"):
|
||||
HA1 = H(b":".join((HA1, nonce_bytes, cnonce_bytes)))
|
||||
|
||||
# Calculate the response digest
|
||||
if qop:
|
||||
noncebit = b":".join(
|
||||
(nonce_bytes, ncvalue_bytes, cnonce_bytes, qop_bytes, HA2)
|
||||
)
|
||||
response_digest = KD(HA1, noncebit)
|
||||
else:
|
||||
response_digest = KD(HA1, b":".join((nonce_bytes, HA2)))
|
||||
|
||||
# Define a dict mapping of header fields to their values
|
||||
# Group fields into always-present, optional, and qop-dependent
|
||||
header_fields = {
|
||||
# Always present fields
|
||||
"username": escape_quotes(self._login_str),
|
||||
"realm": escape_quotes(realm),
|
||||
"nonce": escape_quotes(nonce),
|
||||
"uri": path,
|
||||
"response": response_digest.decode(),
|
||||
"algorithm": algorithm_original,
|
||||
}
|
||||
|
||||
# Optional fields
|
||||
if opaque:
|
||||
header_fields["opaque"] = escape_quotes(opaque)
|
||||
|
||||
# QoP-dependent fields
|
||||
if qop:
|
||||
header_fields["qop"] = qop
|
||||
header_fields["nc"] = ncvalue
|
||||
header_fields["cnonce"] = cnonce
|
||||
|
||||
# Build header using templates for each field type
|
||||
pairs: List[str] = []
|
||||
for field, value in header_fields.items():
|
||||
if field in QUOTED_AUTH_FIELDS:
|
||||
pairs.append(f'{field}="{value}"')
|
||||
else:
|
||||
pairs.append(f"{field}={value}")
|
||||
|
||||
return f"Digest {', '.join(pairs)}"
|
||||
|
||||
def _in_protection_space(self, url: URL) -> bool:
|
||||
"""
|
||||
Check if the given URL is within the current protection space.
|
||||
|
||||
According to RFC 7616, a URI is in the protection space if any URI
|
||||
in the protection space is a prefix of it (after both have been made absolute).
|
||||
"""
|
||||
request_str = str(url)
|
||||
for space_str in self._protection_space:
|
||||
# Check if request starts with space URL
|
||||
if not request_str.startswith(space_str):
|
||||
continue
|
||||
# Exact match or space ends with / (proper directory prefix)
|
||||
if len(request_str) == len(space_str) or space_str[-1] == "/":
|
||||
return True
|
||||
# Check next char is / to ensure proper path boundary
|
||||
if request_str[len(space_str)] == "/":
|
||||
return True
|
||||
return False
|
||||
|
||||
def _authenticate(self, response: ClientResponse) -> bool:
|
||||
"""
|
||||
Takes the given response and tries digest-auth, if needed.
|
||||
|
||||
Returns true if the original request must be resent.
|
||||
"""
|
||||
if response.status != 401:
|
||||
return False
|
||||
|
||||
auth_header = response.headers.get("www-authenticate", "")
|
||||
if not auth_header:
|
||||
return False # No authentication header present
|
||||
|
||||
method, sep, headers = auth_header.partition(" ")
|
||||
if not sep:
|
||||
# No space found in www-authenticate header
|
||||
return False # Malformed auth header, missing scheme separator
|
||||
|
||||
if method.lower() != "digest":
|
||||
# Not a digest auth challenge (could be Basic, Bearer, etc.)
|
||||
return False
|
||||
|
||||
if not headers:
|
||||
# We have a digest scheme but no parameters
|
||||
return False # Malformed digest header, missing parameters
|
||||
|
||||
# We have a digest auth header with content
|
||||
if not (header_pairs := parse_header_pairs(headers)):
|
||||
# Failed to parse any key-value pairs
|
||||
return False # Malformed digest header, no valid parameters
|
||||
|
||||
# Extract challenge parameters
|
||||
self._challenge = {}
|
||||
for field in CHALLENGE_FIELDS:
|
||||
if (value := header_pairs.get(field)) is not None:
|
||||
self._challenge[field] = value
|
||||
|
||||
# Update protection space based on domain parameter or default to origin
|
||||
origin = response.url.origin()
|
||||
|
||||
if domain := self._challenge.get("domain"):
|
||||
# Parse space-separated list of URIs
|
||||
self._protection_space = []
|
||||
for uri in domain.split():
|
||||
# Remove quotes if present
|
||||
uri = uri.strip('"')
|
||||
if uri.startswith("/"):
|
||||
# Path-absolute, relative to origin
|
||||
self._protection_space.append(str(origin.join(URL(uri))))
|
||||
else:
|
||||
# Absolute URI
|
||||
self._protection_space.append(str(URL(uri)))
|
||||
else:
|
||||
# No domain specified, protection space is entire origin
|
||||
self._protection_space = [str(origin)]
|
||||
|
||||
# Return True only if we found at least one challenge parameter
|
||||
return bool(self._challenge)
|
||||
|
||||
async def __call__(
|
||||
self, request: ClientRequest, handler: ClientHandlerType
|
||||
) -> ClientResponse:
|
||||
"""Run the digest auth middleware."""
|
||||
response = None
|
||||
for retry_count in range(2):
|
||||
# Apply authorization header if:
|
||||
# 1. This is a retry after 401 (retry_count > 0), OR
|
||||
# 2. Preemptive auth is enabled AND we have a challenge AND the URL is in protection space
|
||||
if retry_count > 0 or (
|
||||
self._preemptive
|
||||
and self._challenge
|
||||
and self._in_protection_space(request.url)
|
||||
):
|
||||
request.headers[hdrs.AUTHORIZATION] = await self._encode(
|
||||
request.method, request.url, request.body
|
||||
)
|
||||
|
||||
# Send the request
|
||||
response = await handler(request)
|
||||
|
||||
# Check if we need to authenticate
|
||||
if not self._authenticate(response):
|
||||
break
|
||||
|
||||
# At this point, response is guaranteed to be defined
|
||||
assert response is not None
|
||||
return response
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Client middleware support."""
|
||||
|
||||
from collections.abc import Awaitable, Callable, Sequence
|
||||
|
||||
from .client_reqrep import ClientRequest, ClientResponse
|
||||
|
||||
__all__ = ("ClientMiddlewareType", "ClientHandlerType", "build_client_middlewares")
|
||||
|
||||
# Type alias for client request handlers - functions that process requests and return responses
|
||||
ClientHandlerType = Callable[[ClientRequest], Awaitable[ClientResponse]]
|
||||
|
||||
# Type for client middleware - similar to server but uses ClientRequest/ClientResponse
|
||||
ClientMiddlewareType = Callable[
|
||||
[ClientRequest, ClientHandlerType], Awaitable[ClientResponse]
|
||||
]
|
||||
|
||||
|
||||
def build_client_middlewares(
|
||||
handler: ClientHandlerType,
|
||||
middlewares: Sequence[ClientMiddlewareType],
|
||||
) -> ClientHandlerType:
|
||||
"""
|
||||
Apply middlewares to request handler.
|
||||
|
||||
The middlewares are applied in reverse order, so the first middleware
|
||||
in the list wraps all subsequent middlewares and the handler.
|
||||
|
||||
This implementation avoids using partial/update_wrapper to minimize overhead
|
||||
and doesn't cache to avoid holding references to stateful middleware.
|
||||
"""
|
||||
# Optimize for single middleware case
|
||||
if len(middlewares) == 1:
|
||||
middleware = middlewares[0]
|
||||
|
||||
async def single_middleware_handler(req: ClientRequest) -> ClientResponse:
|
||||
return await middleware(req, handler)
|
||||
|
||||
return single_middleware_handler
|
||||
|
||||
# Build the chain for multiple middlewares
|
||||
current_handler = handler
|
||||
|
||||
for middleware in reversed(middlewares):
|
||||
# Create a new closure that captures the current state
|
||||
def make_wrapper(
|
||||
mw: ClientMiddlewareType, next_h: ClientHandlerType
|
||||
) -> ClientHandlerType:
|
||||
async def wrapped(req: ClientRequest) -> ClientResponse:
|
||||
return await mw(req, next_h)
|
||||
|
||||
return wrapped
|
||||
|
||||
current_handler = make_wrapper(middleware, current_handler)
|
||||
|
||||
return current_handler
|
||||
361
venv/lib/python3.12/site-packages/aiohttp/client_proto.py
Normal file
361
venv/lib/python3.12/site-packages/aiohttp/client_proto.py
Normal file
@@ -0,0 +1,361 @@
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
from typing import Any, Optional, Tuple, Union
|
||||
|
||||
from .base_protocol import BaseProtocol
|
||||
from .client_exceptions import (
|
||||
ClientConnectionError,
|
||||
ClientOSError,
|
||||
ClientPayloadError,
|
||||
ServerDisconnectedError,
|
||||
SocketTimeoutError,
|
||||
)
|
||||
from .helpers import (
|
||||
_EXC_SENTINEL,
|
||||
EMPTY_BODY_STATUS_CODES,
|
||||
BaseTimerContext,
|
||||
set_exception,
|
||||
set_result,
|
||||
)
|
||||
from .http import HttpResponseParser, RawResponseMessage
|
||||
from .http_exceptions import HttpProcessingError
|
||||
from .streams import EMPTY_PAYLOAD, DataQueue, StreamReader
|
||||
|
||||
|
||||
class ResponseHandler(BaseProtocol, DataQueue[Tuple[RawResponseMessage, StreamReader]]):
|
||||
"""Helper class to adapt between Protocol and StreamReader."""
|
||||
|
||||
def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
|
||||
BaseProtocol.__init__(self, loop=loop)
|
||||
DataQueue.__init__(self, loop)
|
||||
|
||||
self._should_close = False
|
||||
|
||||
self._payload: Optional[StreamReader] = None
|
||||
self._skip_payload = False
|
||||
self._payload_parser = None
|
||||
|
||||
self._timer = None
|
||||
|
||||
self._tail = b""
|
||||
self._upgraded = False
|
||||
self._parser: Optional[HttpResponseParser] = None
|
||||
|
||||
self._read_timeout: Optional[float] = None
|
||||
self._read_timeout_handle: Optional[asyncio.TimerHandle] = None
|
||||
|
||||
self._timeout_ceil_threshold: Optional[float] = 5
|
||||
|
||||
self._closed: Union[None, asyncio.Future[None]] = None
|
||||
self._connection_lost_called = False
|
||||
|
||||
@property
|
||||
def closed(self) -> Union[None, asyncio.Future[None]]:
|
||||
"""Future that is set when the connection is closed.
|
||||
|
||||
This property returns a Future that will be completed when the connection
|
||||
is closed. The Future is created lazily on first access to avoid creating
|
||||
futures that will never be awaited.
|
||||
|
||||
Returns:
|
||||
- A Future[None] if the connection is still open or was closed after
|
||||
this property was accessed
|
||||
- None if connection_lost() was already called before this property
|
||||
was ever accessed (indicating no one is waiting for the closure)
|
||||
"""
|
||||
if self._closed is None and not self._connection_lost_called:
|
||||
self._closed = self._loop.create_future()
|
||||
return self._closed
|
||||
|
||||
@property
|
||||
def upgraded(self) -> bool:
|
||||
return self._upgraded
|
||||
|
||||
@property
|
||||
def should_close(self) -> bool:
|
||||
return bool(
|
||||
self._should_close
|
||||
or (self._payload is not None and not self._payload.is_eof())
|
||||
or self._upgraded
|
||||
or self._exception is not None
|
||||
or self._payload_parser is not None
|
||||
or self._buffer
|
||||
or self._tail
|
||||
)
|
||||
|
||||
def force_close(self) -> None:
|
||||
self._should_close = True
|
||||
|
||||
def close(self) -> None:
|
||||
self._exception = None # Break cyclic references
|
||||
transport = self.transport
|
||||
if transport is not None:
|
||||
transport.close()
|
||||
self.transport = None
|
||||
self._payload = None
|
||||
self._drop_timeout()
|
||||
|
||||
def abort(self) -> None:
|
||||
self._exception = None # Break cyclic references
|
||||
transport = self.transport
|
||||
if transport is not None:
|
||||
transport.abort()
|
||||
self.transport = None
|
||||
self._payload = None
|
||||
self._drop_timeout()
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
return self.transport is not None and not self.transport.is_closing()
|
||||
|
||||
def connection_lost(self, exc: Optional[BaseException]) -> None:
|
||||
self._connection_lost_called = True
|
||||
self._drop_timeout()
|
||||
|
||||
original_connection_error = exc
|
||||
reraised_exc = original_connection_error
|
||||
|
||||
connection_closed_cleanly = original_connection_error is None
|
||||
|
||||
if self._closed is not None:
|
||||
# If someone is waiting for the closed future,
|
||||
# we should set it to None or an exception. If
|
||||
# self._closed is None, it means that
|
||||
# connection_lost() was called already
|
||||
# or nobody is waiting for it.
|
||||
if connection_closed_cleanly:
|
||||
set_result(self._closed, None)
|
||||
else:
|
||||
assert original_connection_error is not None
|
||||
set_exception(
|
||||
self._closed,
|
||||
ClientConnectionError(
|
||||
f"Connection lost: {original_connection_error !s}",
|
||||
),
|
||||
original_connection_error,
|
||||
)
|
||||
|
||||
if self._payload_parser is not None:
|
||||
with suppress(Exception): # FIXME: log this somehow?
|
||||
self._payload_parser.feed_eof()
|
||||
|
||||
uncompleted = None
|
||||
if self._parser is not None:
|
||||
try:
|
||||
uncompleted = self._parser.feed_eof()
|
||||
except Exception as underlying_exc:
|
||||
if self._payload is not None:
|
||||
client_payload_exc_msg = (
|
||||
f"Response payload is not completed: {underlying_exc !r}"
|
||||
)
|
||||
if not connection_closed_cleanly:
|
||||
client_payload_exc_msg = (
|
||||
f"{client_payload_exc_msg !s}. "
|
||||
f"{original_connection_error !r}"
|
||||
)
|
||||
set_exception(
|
||||
self._payload,
|
||||
ClientPayloadError(client_payload_exc_msg),
|
||||
underlying_exc,
|
||||
)
|
||||
|
||||
if not self.is_eof():
|
||||
if isinstance(original_connection_error, OSError):
|
||||
reraised_exc = ClientOSError(*original_connection_error.args)
|
||||
if connection_closed_cleanly:
|
||||
reraised_exc = ServerDisconnectedError(uncompleted)
|
||||
# assigns self._should_close to True as side effect,
|
||||
# we do it anyway below
|
||||
underlying_non_eof_exc = (
|
||||
_EXC_SENTINEL
|
||||
if connection_closed_cleanly
|
||||
else original_connection_error
|
||||
)
|
||||
assert underlying_non_eof_exc is not None
|
||||
assert reraised_exc is not None
|
||||
self.set_exception(reraised_exc, underlying_non_eof_exc)
|
||||
|
||||
self._should_close = True
|
||||
self._parser = None
|
||||
self._payload = None
|
||||
self._payload_parser = None
|
||||
self._reading_paused = False
|
||||
|
||||
super().connection_lost(reraised_exc)
|
||||
|
||||
def eof_received(self) -> None:
|
||||
# should call parser.feed_eof() most likely
|
||||
self._drop_timeout()
|
||||
|
||||
def pause_reading(self) -> None:
|
||||
super().pause_reading()
|
||||
self._drop_timeout()
|
||||
|
||||
def resume_reading(self) -> None:
|
||||
super().resume_reading()
|
||||
self._reschedule_timeout()
|
||||
|
||||
def set_exception(
|
||||
self,
|
||||
exc: BaseException,
|
||||
exc_cause: BaseException = _EXC_SENTINEL,
|
||||
) -> None:
|
||||
self._should_close = True
|
||||
self._drop_timeout()
|
||||
super().set_exception(exc, exc_cause)
|
||||
|
||||
def set_parser(self, parser: Any, payload: Any) -> None:
|
||||
# TODO: actual types are:
|
||||
# parser: WebSocketReader
|
||||
# payload: WebSocketDataQueue
|
||||
# but they are not generi enough
|
||||
# Need an ABC for both types
|
||||
self._payload = payload
|
||||
self._payload_parser = parser
|
||||
|
||||
self._drop_timeout()
|
||||
|
||||
if self._tail:
|
||||
data, self._tail = self._tail, b""
|
||||
self.data_received(data)
|
||||
|
||||
def set_response_params(
|
||||
self,
|
||||
*,
|
||||
timer: Optional[BaseTimerContext] = None,
|
||||
skip_payload: bool = False,
|
||||
read_until_eof: bool = False,
|
||||
auto_decompress: bool = True,
|
||||
read_timeout: Optional[float] = None,
|
||||
read_bufsize: int = 2**16,
|
||||
timeout_ceil_threshold: float = 5,
|
||||
max_line_size: int = 8190,
|
||||
max_field_size: int = 8190,
|
||||
max_headers: int = 128,
|
||||
) -> None:
|
||||
self._skip_payload = skip_payload
|
||||
|
||||
self._read_timeout = read_timeout
|
||||
|
||||
self._timeout_ceil_threshold = timeout_ceil_threshold
|
||||
|
||||
self._parser = HttpResponseParser(
|
||||
self,
|
||||
self._loop,
|
||||
read_bufsize,
|
||||
timer=timer,
|
||||
payload_exception=ClientPayloadError,
|
||||
response_with_body=not skip_payload,
|
||||
read_until_eof=read_until_eof,
|
||||
auto_decompress=auto_decompress,
|
||||
max_line_size=max_line_size,
|
||||
max_field_size=max_field_size,
|
||||
max_headers=max_headers,
|
||||
)
|
||||
|
||||
if self._tail:
|
||||
data, self._tail = self._tail, b""
|
||||
self.data_received(data)
|
||||
|
||||
def _drop_timeout(self) -> None:
|
||||
if self._read_timeout_handle is not None:
|
||||
self._read_timeout_handle.cancel()
|
||||
self._read_timeout_handle = None
|
||||
|
||||
def _reschedule_timeout(self) -> None:
|
||||
timeout = self._read_timeout
|
||||
if self._read_timeout_handle is not None:
|
||||
self._read_timeout_handle.cancel()
|
||||
|
||||
if timeout:
|
||||
self._read_timeout_handle = self._loop.call_later(
|
||||
timeout, self._on_read_timeout
|
||||
)
|
||||
else:
|
||||
self._read_timeout_handle = None
|
||||
|
||||
def start_timeout(self) -> None:
|
||||
self._reschedule_timeout()
|
||||
|
||||
@property
|
||||
def read_timeout(self) -> Optional[float]:
|
||||
return self._read_timeout
|
||||
|
||||
@read_timeout.setter
|
||||
def read_timeout(self, read_timeout: Optional[float]) -> None:
|
||||
self._read_timeout = read_timeout
|
||||
|
||||
def _on_read_timeout(self) -> None:
|
||||
exc = SocketTimeoutError("Timeout on reading data from socket")
|
||||
self.set_exception(exc)
|
||||
if self._payload is not None:
|
||||
set_exception(self._payload, exc)
|
||||
|
||||
def data_received(self, data: bytes) -> None:
|
||||
self._reschedule_timeout()
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
# custom payload parser - currently always WebSocketReader
|
||||
if self._payload_parser is not None:
|
||||
eof, tail = self._payload_parser.feed_data(data)
|
||||
if eof:
|
||||
self._payload = None
|
||||
self._payload_parser = None
|
||||
|
||||
if tail:
|
||||
self.data_received(tail)
|
||||
return
|
||||
|
||||
if self._upgraded or self._parser is None:
|
||||
# i.e. websocket connection, websocket parser is not set yet
|
||||
self._tail += data
|
||||
return
|
||||
|
||||
# parse http messages
|
||||
try:
|
||||
messages, upgraded, tail = self._parser.feed_data(data)
|
||||
except BaseException as underlying_exc:
|
||||
if self.transport is not None:
|
||||
# connection.release() could be called BEFORE
|
||||
# data_received(), the transport is already
|
||||
# closed in this case
|
||||
self.transport.close()
|
||||
# should_close is True after the call
|
||||
if isinstance(underlying_exc, HttpProcessingError):
|
||||
exc = HttpProcessingError(
|
||||
code=underlying_exc.code,
|
||||
message=underlying_exc.message,
|
||||
headers=underlying_exc.headers,
|
||||
)
|
||||
else:
|
||||
exc = HttpProcessingError()
|
||||
self.set_exception(exc, underlying_exc)
|
||||
return
|
||||
|
||||
self._upgraded = upgraded
|
||||
|
||||
payload: Optional[StreamReader] = None
|
||||
for message, payload in messages:
|
||||
if message.should_close:
|
||||
self._should_close = True
|
||||
|
||||
self._payload = payload
|
||||
|
||||
if self._skip_payload or message.code in EMPTY_BODY_STATUS_CODES:
|
||||
self.feed_data((message, EMPTY_PAYLOAD), 0)
|
||||
else:
|
||||
self.feed_data((message, payload), 0)
|
||||
|
||||
if payload is not None:
|
||||
# new message(s) was processed
|
||||
# register timeout handler unsubscribing
|
||||
# either on end-of-stream or immediately for
|
||||
# EMPTY_PAYLOAD
|
||||
if payload is not EMPTY_PAYLOAD:
|
||||
payload.on_eof(self._drop_timeout)
|
||||
else:
|
||||
self._drop_timeout()
|
||||
|
||||
if upgraded and tail:
|
||||
self.data_received(tail)
|
||||
1536
venv/lib/python3.12/site-packages/aiohttp/client_reqrep.py
Normal file
1536
venv/lib/python3.12/site-packages/aiohttp/client_reqrep.py
Normal file
File diff suppressed because it is too large
Load Diff
428
venv/lib/python3.12/site-packages/aiohttp/client_ws.py
Normal file
428
venv/lib/python3.12/site-packages/aiohttp/client_ws.py
Normal file
@@ -0,0 +1,428 @@
|
||||
"""WebSocket client for asyncio."""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from types import TracebackType
|
||||
from typing import Any, Optional, Type, cast
|
||||
|
||||
import attr
|
||||
|
||||
from ._websocket.reader import WebSocketDataQueue
|
||||
from .client_exceptions import ClientError, ServerTimeoutError, WSMessageTypeError
|
||||
from .client_reqrep import ClientResponse
|
||||
from .helpers import calculate_timeout_when, set_result
|
||||
from .http import (
|
||||
WS_CLOSED_MESSAGE,
|
||||
WS_CLOSING_MESSAGE,
|
||||
WebSocketError,
|
||||
WSCloseCode,
|
||||
WSMessage,
|
||||
WSMsgType,
|
||||
)
|
||||
from .http_websocket import _INTERNAL_RECEIVE_TYPES, WebSocketWriter
|
||||
from .streams import EofStream
|
||||
from .typedefs import (
|
||||
DEFAULT_JSON_DECODER,
|
||||
DEFAULT_JSON_ENCODER,
|
||||
JSONDecoder,
|
||||
JSONEncoder,
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
import asyncio as async_timeout
|
||||
else:
|
||||
import async_timeout
|
||||
|
||||
|
||||
@attr.s(frozen=True, slots=True)
|
||||
class ClientWSTimeout:
|
||||
ws_receive = attr.ib(type=Optional[float], default=None)
|
||||
ws_close = attr.ib(type=Optional[float], default=None)
|
||||
|
||||
|
||||
DEFAULT_WS_CLIENT_TIMEOUT = ClientWSTimeout(ws_receive=None, ws_close=10.0)
|
||||
|
||||
|
||||
class ClientWebSocketResponse:
|
||||
def __init__(
|
||||
self,
|
||||
reader: WebSocketDataQueue,
|
||||
writer: WebSocketWriter,
|
||||
protocol: Optional[str],
|
||||
response: ClientResponse,
|
||||
timeout: ClientWSTimeout,
|
||||
autoclose: bool,
|
||||
autoping: bool,
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
*,
|
||||
heartbeat: Optional[float] = None,
|
||||
compress: int = 0,
|
||||
client_notakeover: bool = False,
|
||||
) -> None:
|
||||
self._response = response
|
||||
self._conn = response.connection
|
||||
|
||||
self._writer = writer
|
||||
self._reader = reader
|
||||
self._protocol = protocol
|
||||
self._closed = False
|
||||
self._closing = False
|
||||
self._close_code: Optional[int] = None
|
||||
self._timeout = timeout
|
||||
self._autoclose = autoclose
|
||||
self._autoping = autoping
|
||||
self._heartbeat = heartbeat
|
||||
self._heartbeat_cb: Optional[asyncio.TimerHandle] = None
|
||||
self._heartbeat_when: float = 0.0
|
||||
if heartbeat is not None:
|
||||
self._pong_heartbeat = heartbeat / 2.0
|
||||
self._pong_response_cb: Optional[asyncio.TimerHandle] = None
|
||||
self._loop = loop
|
||||
self._waiting: bool = False
|
||||
self._close_wait: Optional[asyncio.Future[None]] = None
|
||||
self._exception: Optional[BaseException] = None
|
||||
self._compress = compress
|
||||
self._client_notakeover = client_notakeover
|
||||
self._ping_task: Optional[asyncio.Task[None]] = None
|
||||
|
||||
self._reset_heartbeat()
|
||||
|
||||
def _cancel_heartbeat(self) -> None:
|
||||
self._cancel_pong_response_cb()
|
||||
if self._heartbeat_cb is not None:
|
||||
self._heartbeat_cb.cancel()
|
||||
self._heartbeat_cb = None
|
||||
if self._ping_task is not None:
|
||||
self._ping_task.cancel()
|
||||
self._ping_task = None
|
||||
|
||||
def _cancel_pong_response_cb(self) -> None:
|
||||
if self._pong_response_cb is not None:
|
||||
self._pong_response_cb.cancel()
|
||||
self._pong_response_cb = None
|
||||
|
||||
def _reset_heartbeat(self) -> None:
|
||||
if self._heartbeat is None:
|
||||
return
|
||||
self._cancel_pong_response_cb()
|
||||
loop = self._loop
|
||||
assert loop is not None
|
||||
conn = self._conn
|
||||
timeout_ceil_threshold = (
|
||||
conn._connector._timeout_ceil_threshold if conn is not None else 5
|
||||
)
|
||||
now = loop.time()
|
||||
when = calculate_timeout_when(now, self._heartbeat, timeout_ceil_threshold)
|
||||
self._heartbeat_when = when
|
||||
if self._heartbeat_cb is None:
|
||||
# We do not cancel the previous heartbeat_cb here because
|
||||
# it generates a significant amount of TimerHandle churn
|
||||
# which causes asyncio to rebuild the heap frequently.
|
||||
# Instead _send_heartbeat() will reschedule the next
|
||||
# heartbeat if it fires too early.
|
||||
self._heartbeat_cb = loop.call_at(when, self._send_heartbeat)
|
||||
|
||||
def _send_heartbeat(self) -> None:
|
||||
self._heartbeat_cb = None
|
||||
loop = self._loop
|
||||
now = loop.time()
|
||||
if now < self._heartbeat_when:
|
||||
# Heartbeat fired too early, reschedule
|
||||
self._heartbeat_cb = loop.call_at(
|
||||
self._heartbeat_when, self._send_heartbeat
|
||||
)
|
||||
return
|
||||
|
||||
conn = self._conn
|
||||
timeout_ceil_threshold = (
|
||||
conn._connector._timeout_ceil_threshold if conn is not None else 5
|
||||
)
|
||||
when = calculate_timeout_when(now, self._pong_heartbeat, timeout_ceil_threshold)
|
||||
self._cancel_pong_response_cb()
|
||||
self._pong_response_cb = loop.call_at(when, self._pong_not_received)
|
||||
|
||||
coro = self._writer.send_frame(b"", WSMsgType.PING)
|
||||
if sys.version_info >= (3, 12):
|
||||
# Optimization for Python 3.12, try to send the ping
|
||||
# immediately to avoid having to schedule
|
||||
# the task on the event loop.
|
||||
ping_task = asyncio.Task(coro, loop=loop, eager_start=True)
|
||||
else:
|
||||
ping_task = loop.create_task(coro)
|
||||
|
||||
if not ping_task.done():
|
||||
self._ping_task = ping_task
|
||||
ping_task.add_done_callback(self._ping_task_done)
|
||||
else:
|
||||
self._ping_task_done(ping_task)
|
||||
|
||||
def _ping_task_done(self, task: "asyncio.Task[None]") -> None:
|
||||
"""Callback for when the ping task completes."""
|
||||
if not task.cancelled() and (exc := task.exception()):
|
||||
self._handle_ping_pong_exception(exc)
|
||||
self._ping_task = None
|
||||
|
||||
def _pong_not_received(self) -> None:
|
||||
self._handle_ping_pong_exception(
|
||||
ServerTimeoutError(f"No PONG received after {self._pong_heartbeat} seconds")
|
||||
)
|
||||
|
||||
def _handle_ping_pong_exception(self, exc: BaseException) -> None:
|
||||
"""Handle exceptions raised during ping/pong processing."""
|
||||
if self._closed:
|
||||
return
|
||||
self._set_closed()
|
||||
self._close_code = WSCloseCode.ABNORMAL_CLOSURE
|
||||
self._exception = exc
|
||||
self._response.close()
|
||||
if self._waiting and not self._closing:
|
||||
self._reader.feed_data(WSMessage(WSMsgType.ERROR, exc, None), 0)
|
||||
|
||||
def _set_closed(self) -> None:
|
||||
"""Set the connection to closed.
|
||||
|
||||
Cancel any heartbeat timers and set the closed flag.
|
||||
"""
|
||||
self._closed = True
|
||||
self._cancel_heartbeat()
|
||||
|
||||
def _set_closing(self) -> None:
|
||||
"""Set the connection to closing.
|
||||
|
||||
Cancel any heartbeat timers and set the closing flag.
|
||||
"""
|
||||
self._closing = True
|
||||
self._cancel_heartbeat()
|
||||
|
||||
@property
|
||||
def closed(self) -> bool:
|
||||
return self._closed
|
||||
|
||||
@property
|
||||
def close_code(self) -> Optional[int]:
|
||||
return self._close_code
|
||||
|
||||
@property
|
||||
def protocol(self) -> Optional[str]:
|
||||
return self._protocol
|
||||
|
||||
@property
|
||||
def compress(self) -> int:
|
||||
return self._compress
|
||||
|
||||
@property
|
||||
def client_notakeover(self) -> bool:
|
||||
return self._client_notakeover
|
||||
|
||||
def get_extra_info(self, name: str, default: Any = None) -> Any:
|
||||
"""extra info from connection transport"""
|
||||
conn = self._response.connection
|
||||
if conn is None:
|
||||
return default
|
||||
transport = conn.transport
|
||||
if transport is None:
|
||||
return default
|
||||
return transport.get_extra_info(name, default)
|
||||
|
||||
def exception(self) -> Optional[BaseException]:
|
||||
return self._exception
|
||||
|
||||
async def ping(self, message: bytes = b"") -> None:
|
||||
await self._writer.send_frame(message, WSMsgType.PING)
|
||||
|
||||
async def pong(self, message: bytes = b"") -> None:
|
||||
await self._writer.send_frame(message, WSMsgType.PONG)
|
||||
|
||||
async def send_frame(
|
||||
self, message: bytes, opcode: WSMsgType, compress: Optional[int] = None
|
||||
) -> None:
|
||||
"""Send a frame over the websocket."""
|
||||
await self._writer.send_frame(message, opcode, compress)
|
||||
|
||||
async def send_str(self, data: str, compress: Optional[int] = None) -> None:
|
||||
if not isinstance(data, str):
|
||||
raise TypeError("data argument must be str (%r)" % type(data))
|
||||
await self._writer.send_frame(
|
||||
data.encode("utf-8"), WSMsgType.TEXT, compress=compress
|
||||
)
|
||||
|
||||
async def send_bytes(self, data: bytes, compress: Optional[int] = None) -> None:
|
||||
if not isinstance(data, (bytes, bytearray, memoryview)):
|
||||
raise TypeError("data argument must be byte-ish (%r)" % type(data))
|
||||
await self._writer.send_frame(data, WSMsgType.BINARY, compress=compress)
|
||||
|
||||
async def send_json(
|
||||
self,
|
||||
data: Any,
|
||||
compress: Optional[int] = None,
|
||||
*,
|
||||
dumps: JSONEncoder = DEFAULT_JSON_ENCODER,
|
||||
) -> None:
|
||||
await self.send_str(dumps(data), compress=compress)
|
||||
|
||||
async def close(self, *, code: int = WSCloseCode.OK, message: bytes = b"") -> bool:
|
||||
# we need to break `receive()` cycle first,
|
||||
# `close()` may be called from different task
|
||||
if self._waiting and not self._closing:
|
||||
assert self._loop is not None
|
||||
self._close_wait = self._loop.create_future()
|
||||
self._set_closing()
|
||||
self._reader.feed_data(WS_CLOSING_MESSAGE, 0)
|
||||
await self._close_wait
|
||||
|
||||
if self._closed:
|
||||
return False
|
||||
|
||||
self._set_closed()
|
||||
try:
|
||||
await self._writer.close(code, message)
|
||||
except asyncio.CancelledError:
|
||||
self._close_code = WSCloseCode.ABNORMAL_CLOSURE
|
||||
self._response.close()
|
||||
raise
|
||||
except Exception as exc:
|
||||
self._close_code = WSCloseCode.ABNORMAL_CLOSURE
|
||||
self._exception = exc
|
||||
self._response.close()
|
||||
return True
|
||||
|
||||
if self._close_code:
|
||||
self._response.close()
|
||||
return True
|
||||
|
||||
while True:
|
||||
try:
|
||||
async with async_timeout.timeout(self._timeout.ws_close):
|
||||
msg = await self._reader.read()
|
||||
except asyncio.CancelledError:
|
||||
self._close_code = WSCloseCode.ABNORMAL_CLOSURE
|
||||
self._response.close()
|
||||
raise
|
||||
except Exception as exc:
|
||||
self._close_code = WSCloseCode.ABNORMAL_CLOSURE
|
||||
self._exception = exc
|
||||
self._response.close()
|
||||
return True
|
||||
|
||||
if msg.type is WSMsgType.CLOSE:
|
||||
self._close_code = msg.data
|
||||
self._response.close()
|
||||
return True
|
||||
|
||||
async def receive(self, timeout: Optional[float] = None) -> WSMessage:
|
||||
receive_timeout = timeout or self._timeout.ws_receive
|
||||
|
||||
while True:
|
||||
if self._waiting:
|
||||
raise RuntimeError("Concurrent call to receive() is not allowed")
|
||||
|
||||
if self._closed:
|
||||
return WS_CLOSED_MESSAGE
|
||||
elif self._closing:
|
||||
await self.close()
|
||||
return WS_CLOSED_MESSAGE
|
||||
|
||||
try:
|
||||
self._waiting = True
|
||||
try:
|
||||
if receive_timeout:
|
||||
# Entering the context manager and creating
|
||||
# Timeout() object can take almost 50% of the
|
||||
# run time in this loop so we avoid it if
|
||||
# there is no read timeout.
|
||||
async with async_timeout.timeout(receive_timeout):
|
||||
msg = await self._reader.read()
|
||||
else:
|
||||
msg = await self._reader.read()
|
||||
self._reset_heartbeat()
|
||||
finally:
|
||||
self._waiting = False
|
||||
if self._close_wait:
|
||||
set_result(self._close_wait, None)
|
||||
except (asyncio.CancelledError, asyncio.TimeoutError):
|
||||
self._close_code = WSCloseCode.ABNORMAL_CLOSURE
|
||||
raise
|
||||
except EofStream:
|
||||
self._close_code = WSCloseCode.OK
|
||||
await self.close()
|
||||
return WSMessage(WSMsgType.CLOSED, None, None)
|
||||
except ClientError:
|
||||
# Likely ServerDisconnectedError when connection is lost
|
||||
self._set_closed()
|
||||
self._close_code = WSCloseCode.ABNORMAL_CLOSURE
|
||||
return WS_CLOSED_MESSAGE
|
||||
except WebSocketError as exc:
|
||||
self._close_code = exc.code
|
||||
await self.close(code=exc.code)
|
||||
return WSMessage(WSMsgType.ERROR, exc, None)
|
||||
except Exception as exc:
|
||||
self._exception = exc
|
||||
self._set_closing()
|
||||
self._close_code = WSCloseCode.ABNORMAL_CLOSURE
|
||||
await self.close()
|
||||
return WSMessage(WSMsgType.ERROR, exc, None)
|
||||
|
||||
if msg.type not in _INTERNAL_RECEIVE_TYPES:
|
||||
# If its not a close/closing/ping/pong message
|
||||
# we can return it immediately
|
||||
return msg
|
||||
|
||||
if msg.type is WSMsgType.CLOSE:
|
||||
self._set_closing()
|
||||
self._close_code = msg.data
|
||||
if not self._closed and self._autoclose:
|
||||
await self.close()
|
||||
elif msg.type is WSMsgType.CLOSING:
|
||||
self._set_closing()
|
||||
elif msg.type is WSMsgType.PING and self._autoping:
|
||||
await self.pong(msg.data)
|
||||
continue
|
||||
elif msg.type is WSMsgType.PONG and self._autoping:
|
||||
continue
|
||||
|
||||
return msg
|
||||
|
||||
async def receive_str(self, *, timeout: Optional[float] = None) -> str:
|
||||
msg = await self.receive(timeout)
|
||||
if msg.type is not WSMsgType.TEXT:
|
||||
raise WSMessageTypeError(
|
||||
f"Received message {msg.type}:{msg.data!r} is not WSMsgType.TEXT"
|
||||
)
|
||||
return cast(str, msg.data)
|
||||
|
||||
async def receive_bytes(self, *, timeout: Optional[float] = None) -> bytes:
|
||||
msg = await self.receive(timeout)
|
||||
if msg.type is not WSMsgType.BINARY:
|
||||
raise WSMessageTypeError(
|
||||
f"Received message {msg.type}:{msg.data!r} is not WSMsgType.BINARY"
|
||||
)
|
||||
return cast(bytes, msg.data)
|
||||
|
||||
async def receive_json(
|
||||
self,
|
||||
*,
|
||||
loads: JSONDecoder = DEFAULT_JSON_DECODER,
|
||||
timeout: Optional[float] = None,
|
||||
) -> Any:
|
||||
data = await self.receive_str(timeout=timeout)
|
||||
return loads(data)
|
||||
|
||||
def __aiter__(self) -> "ClientWebSocketResponse":
|
||||
return self
|
||||
|
||||
async def __anext__(self) -> WSMessage:
|
||||
msg = await self.receive()
|
||||
if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING, WSMsgType.CLOSED):
|
||||
raise StopAsyncIteration
|
||||
return msg
|
||||
|
||||
async def __aenter__(self) -> "ClientWebSocketResponse":
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional[TracebackType],
|
||||
) -> None:
|
||||
await self.close()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user