feat: add auth_mode option to ONVIF config for Hikvision HTTP Digest support

Some Hikvision PTZ cameras (e.g. DS-2SE4C425MWG-E) reject WSSE wsUsername
tokens and return 401 on /onvif/Media and /onvif/PTZ endpoints. They require
HTTP Digest authentication at the transport level before processing SOAP.

Add `auth_mode` field to OnvifConfig (auto/digest/wsse, default: auto).
When auth_mode=digest, inject aiohttp.DigestAuthMiddleware into the
ONVIFCamera session so Digest challenge-response completes before SOAP.

Usage:
  cameras:
    my_hikvision_ptz:
      onvif:
        host: 192.168.31.86
        port: 80
        user: admin
        password: yourpassword
        auth_mode: digest

Fixes #22622
This commit is contained in:
Arturo Naredo 2026-03-25 05:18:29 +01:00
parent 4b42039568
commit 27c80b4944
2 changed files with 22 additions and 1 deletions

View File

@ -1,5 +1,5 @@
from enum import Enum from enum import Enum
from typing import Optional, Union from typing import Literal, Optional, Union
from pydantic import Field, field_validator from pydantic import Field, field_validator
@ -117,6 +117,11 @@ class OnvifConfig(FrigateBaseModel):
title="Disable TLS verify", title="Disable TLS verify",
description="Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only).", description="Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only).",
) )
auth_mode: Literal["auto", "digest", "wsse"] = Field(
default="auto",
title="ONVIF authentication mode",
description="Authentication mode for ONVIF connections. 'auto' tries WSSE first (default behavior). 'digest' forces HTTP Digest auth at the transport level — required for some Hikvision cameras that reject WSSE wsUsername tokens and return 401. 'wsse' forces WSSE UsernameToken only.",
)
autotracking: PtzAutotrackConfig = Field( autotracking: PtzAutotrackConfig = Field(
default_factory=PtzAutotrackConfig, default_factory=PtzAutotrackConfig,
title="Autotracking", title="Autotracking",

View File

@ -9,6 +9,7 @@ from importlib.util import find_spec
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
import aiohttp
import numpy import numpy
from onvif import ONVIFCamera, ONVIFError, ONVIFService from onvif import ONVIFCamera, ONVIFError, ONVIFService
from zeep.exceptions import Fault, TransportError from zeep.exceptions import Fault, TransportError
@ -104,6 +105,20 @@ class OnvifController:
if password is not None and isinstance(password, bytes): if password is not None and isinstance(password, bytes):
password = password.decode("utf-8") password = password.decode("utf-8")
# Build extra kwargs for digest auth mode (Hikvision and similar cameras
# that reject WSSE wsUsername tokens and require HTTP Digest at transport level).
onvif_extra: dict[str, Any] = {}
if cam.onvif.auth_mode == "digest" and user and password:
try:
onvif_extra["middlewares"] = [
aiohttp.DigestAuthMiddleware(user, password)
]
except AttributeError:
logger.warning(
f"DigestAuthMiddleware not available in installed aiohttp version "
f"for camera {cam_name}; falling back to default auth."
)
self.cams[cam_name] = { self.cams[cam_name] = {
"onvif": ONVIFCamera( "onvif": ONVIFCamera(
cam.onvif.host, cam.onvif.host,
@ -113,6 +128,7 @@ class OnvifController:
wsdl_dir=str(Path(find_spec("onvif").origin).parent / "wsdl"), wsdl_dir=str(Path(find_spec("onvif").origin).parent / "wsdl"),
adjust_time=cam.onvif.ignore_time_mismatch, adjust_time=cam.onvif.ignore_time_mismatch,
encrypt=not cam.onvif.tls_insecure, encrypt=not cam.onvif.tls_insecure,
**onvif_extra,
), ),
"init": False, "init": False,
"active": False, "active": False,