"""Isolated ONVIF fixture for Sense acceptance tests.
Credentials are required through process environment and are never printed or
persisted. The fixture validates RFC 7616 MD5 qop=auth so the test exercises
Sense's real Digest retry instead of accepting an arbitrary header.
"""
from __future__ import annotations
import hashlib
import json
import os
import re
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
HOST = "127.0.0.1"
PORT = int(os.environ["SENSE_E2E_ONVIF_PORT"])
RTSP_PORT = int(os.environ["SENSE_E2E_RTSP_PORT"])
USERNAME = os.environ["SENSE_E2E_CAMERA_USERNAME"]
PASSWORD = os.environ["SENSE_E2E_CAMERA_PASSWORD"]
STATE_PATH = Path(os.environ["SENSE_E2E_PROFILE_STATE_FILE"])
STATUS_PATH = Path(os.environ["SENSE_E2E_FIXTURE_STATUS_FILE"])
REALM = "sense-e2e-camera"
NONCE = hashlib.sha256(os.urandom(32)).hexdigest()
def _md5(value: str) -> str:
return hashlib.md5(value.encode("utf-8"), usedforsecurity=False).hexdigest()
def _parameters(value: str) -> dict[str, str]:
result: dict[str, str] = {}
for match in re.finditer(r'(\w+)=(?:"([^"]*)"|([^,\s]+))', value):
result[match.group(1).lower()] = match.group(2) or match.group(3)
return result
def _authorized(header: str, method: str, uri: str) -> bool:
if not header.startswith("Digest "):
return False
values = _parameters(header[7:])
if values.get("username") != USERNAME or values.get("realm") != REALM:
return False
if values.get("nonce") != NONCE or values.get("uri") != uri:
return False
ha1 = _md5(f"{USERNAME}:{REALM}:{PASSWORD}")
ha2 = _md5(f"{method}:{uri}")
if values.get("qop") == "auth":
expected = _md5(
f"{ha1}:{NONCE}:{values.get('nc', '')}:{values.get('cnonce', '')}:auth:{ha2}"
)
else:
expected = _md5(f"{ha1}:{NONCE}:{ha2}")
return values.get("response") == expected
def _write_status(digest_requests: int) -> None:
STATUS_PATH.write_text(
json.dumps({"ready": True, "digestRequests": digest_requests}),
encoding="utf-8",
)
class Handler(BaseHTTPRequestHandler):
digest_requests = 0
def log_message(self, _format: str, *_args: object) -> None:
return
def do_POST(self) -> None: # noqa: N802 - BaseHTTPRequestHandler API
length = min(int(self.headers.get("Content-Length", "0")), 2 << 20)
body = self.rfile.read(length).decode("utf-8", errors="replace")
authorization = self.headers.get("Authorization", "")
if not _authorized(authorization, "POST", self.path):
self.send_response(401)
self.send_header(
"WWW-Authenticate",
f'Digest realm="{REALM}", nonce="{NONCE}", algorithm=MD5, qop="auth"',
)
self.end_headers()
return
type(self).digest_requests += 1
_write_status(type(self).digest_requests)
state = STATE_PATH.read_text(encoding="utf-8").strip()
width, height = ((1280, 720) if state == "changed" else (1920, 1080))
if "GetCapabilities" in body:
payload = (
""
f"http://{HOST}:{PORT}/onvif/media"
""
)
elif "GetProfiles" in body:
payload = (
''
"主码流H264"
f"{width}{height}"
""
)
elif "GetStreamUri" in body:
payload = (
""
f"rtsp://{HOST}:{RTSP_PORT}/fixture"
""
)
else:
self.send_error(400)
return
encoded = payload.encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/soap+xml; charset=utf-8")
self.send_header("Content-Length", str(len(encoded)))
self.end_headers()
self.wfile.write(encoded)
if __name__ == "__main__":
_write_status(0)
ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()