I am working on writing some of my remaining node-red flows into openHAB rules. One of the flows reads out the status of a Hikvision doorbell camera to see if the doorbell button is pressed. This can be done with a HTTP GET request at:
http://192.168.1.XX/ISAPI/VideoIntercom/callStatus
The camera uses digest authentication. This works great from node-red or curl e.g. with the following command:
curl -v --digest -u user:password http://192.168.1.XX/ISAPI/VideoIntercom/callStatus
Using the openHAB HTTP binding with the following configuration:
UID: http:url:doorbellpoll
label: Doorbell Poll
thingTypeUID: http:url
configuration:
authMode: DIGEST
ignoreSSLErrors: false
baseURL: http://192.168.1.XX/ISAPI/VideoIntercom/callStatus
password: password
delay: 0
stateMethod: GET
refresh: 30
commandMethod: GET
timeout: 3000
username: user
bufferSize: 2048
the call only works once. A next refresh results in a failed request. Binding TRACE log of the first call:
[TRACE] [.HttpDynamicStateDescriptionProvider] - removing state description for thing http:url:doorbellpoll
[INFO ] [nding.http.internal.HttpThingHandler] - Using the secure client for thing 'http:url:doorbellpoll'.
[DEBUG] [nding.http.internal.HttpThingHandler] - Digest Authentication configured for thing 'http:url:doorbellpoll'
[TRACE] [ttp.internal.http.RefreshingUrlCache] - Started refresh task for URL 'http://192.168.1.XX/ISAPI/VideoIntercom/callStatus' with interval 30s
[TRACE] [.HttpDynamicStateDescriptionProvider] - adding state description for channel http:url:doorbellpoll:doorbellpollstatus
[TRACE] [ttp.internal.http.RefreshingUrlCache] - Stopped refresh task for URL 'http://192.168.1.XX/ISAPI/VideoIntercom/callStatus'
[TRACE] [.HttpDynamicStateDescriptionProvider] - removing state description for thing http:url:doorbellpoll
[INFO ] [nding.http.internal.HttpThingHandler] - Using the secure client for thing 'http:url:doorbellpoll'.
[DEBUG] [nding.http.internal.HttpThingHandler] - Digest Authentication configured for thing 'http:url:doorbellpoll'
[TRACE] [ttp.internal.http.RefreshingUrlCache] - Started refresh task for URL 'http://192.168.1.XX/ISAPI/VideoIntercom/callStatus' with interval 30s
[TRACE] [.HttpDynamicStateDescriptionProvider] - adding state description for channel http:url:doorbellpoll:doorbellpollstatus
[TRACE] [ttp.internal.http.RefreshingUrlCache] - Requesting refresh (retry=false) from 'http://192.168.1.XX/ISAPI/VideoIntercom/callStatus' with timeout 3000ms
[TRACE] [ttp.internal.http.RefreshingUrlCache] - Sending to 'http://192.168.1.XX/ISAPI/VideoIntercom/callStatus': Method = {GET}, Headers = {Accept-Encoding: gzip, User-Agent: Jetty/9.4.52.v20230823}, Content = {null}
[TRACE] [p.internal.http.HttpResponseListener] - Received from 'http://192.168.1.XX/ISAPI/VideoIntercom/callStatus': Code = {200}, Headers = {Date: Sun, 24 Dec 2023 10:49:13 GMT, Server: webs, Content-Length: 42, Connection: close, X-Frame-Options: SAMEORIGIN, Cache-Control: no-store, Pragma: no-cache, Content-Type: application/xml}, Content = {{
"CallStatus": {
"status": "idle"
}
}}
and the TRACE log for the subsequent calls:
[TRACE] [ttp.internal.http.RefreshingUrlCache] - Requesting refresh (retry=false) from 'http://192.168.1.XX/ISAPI/VideoIntercom/callStatus' with timeout 3000ms
[TRACE] [ttp.internal.http.RefreshingUrlCache] - Sending to 'http://192.168.1.XX/ISAPI/VideoIntercom/callStatus': Method = {GET}, Headers = {Accept-Encoding: gzip, User-Agent: Jetty/9.4.52.v20230823}, Content = {null}
[TRACE] [p.internal.http.HttpResponseListener] - Received from 'http://192.168.1.XX/ISAPI/VideoIntercom/callStatus': Code = {401}, Headers = {Date: Sun, 24 Dec 2023 10:49:42 GMT, Server: webs, Content-Length: 387, Connection: close, X-Frame-Options: SAMEORIGIN, Cache-Control: no-store, Pragma: no-cache, Content-Type: application/xml}, Content = {<?xml version="1.0" encoding="UTF-8"?>
<ResponseStatus version="1.0" xmlns="http://www.std-cgi.com/ver10/XMLSchema">
<requestURL>/ISAPI/VideoIntercom/callStatus</requestURL>
<statusCode>4</statusCode>
<statusString>Invalid Operation</statusString>
<subStatusCode>invalidOperation</subStatusCode>
<errorCode>1073741830</errorCode>
<errorMsg>invalid operation</errorMsg>
</ResponseStatus>
}
[WARN ] [p.internal.http.HttpResponseListener] - Requesting 'http://192.168.1.XX/ISAPI/VideoIntercom/callStatus' (method='GET', content='null') failed: org.eclipse.jetty.client.HttpResponseException: HTTP protocol violation: Authentication challenge without WWW-Authenticate header
To understand why this failed I did some packet inspections on the communication with the camera. Following is a “human readable” packet trace from the above curl command. Each time a request is made a new digest authorization handshake is performed:
Reguest:
GET /ISAPI/VideoIntercom/callStatus HTTP/1.1
Host: 192.168.1.XX
User-Agent: curl/7.74.0
Accept: */*
Response:
HTTP/1.1 401 Unauthorized
Date: Sun, 24 Dec 2023 14:08:54 GMT
Server: webs
Content-Length: 235
Connection: close
X-Frame-Options: SAMEORIGIN
Cache-Control: no-store
Pragma: no-cache
WWW-Authenticate: Digest qop="auth", realm="DS-1762ACD3", nonce="NDI5NThhYzAxNTM0ZmY0ZmIzZjE2OGFmMTJlYTNhYzM=", stale="false", opaque="", domain="::"
Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8"?>
<userCheck version="2.0" xmlns="http://www.isapi.org/ver20/XMLSchema">
<statusValue>401</statusValue>
<statusString>Unauthorized</statusString>
<isActivated>false</isActivated>
</userCheck>
Second request:
GET /ISAPI/VideoIntercom/callStatus HTTP/1.1
Host: 192.168.1.XX
Authorization: Digest username="user", realm="DS-1762ACD3", nonce="NDI5NThhYzAxNTM0ZmY0ZmIzZjE2OGFmMTJlYTNhYzM=", uri="/ISAPI/VideoIntercom/callStatus", cnonce="ZTE0OGQ4MmJmZTQ0YzE5ODA1NDQyODA5NTUwZTU0ZTg=", nc=00000001, qop=auth, response="a89cd52425f42477d77ee33954fafb39", opaque=""
User-Agent: curl/7.74.0
Accept: */*
Response with status:
HTTP/1.1 200 OK
Date: Sun, 24 Dec 2023 14:08:54 GMT
Server: webs
Content-Length: 42
Connection: close
X-Frame-Options: SAMEORIGIN
Cache-Control: no-store
Pragma: no-cache
Content-Type: application/xml
{
"CallStatus": {
"status": "idle"
}
}
The HTTP binding however performs the digest handshake only once and increases the NC=0000X parameter for each subsequent call which is denied by the camera:
* First attempt works *
GET /ISAPI/VideoIntercom/callStatus HTTP/1.1
Accept-Encoding: gzip
User-Agent: Jetty/9.4.52.v20230823
Authorization: Digest username="user", realm="DS-1762ACD3", nonce="NTU2MDI3MTlmNGIwZmNkODZmMzg4OTdiZWI2ZGZhYzk=", opaque="", algorithm="MD5", uri="/ISAPI/VideoIntercom/callStatus", qop="auth", nc="00000001", cnonce="4a67b856d0345194", response="99f08809ce0811b236f793eccecc9bdb"
Host: 192.168.1.XX
* This response *
HTTP/1.1 200 OK
Date: Sun, 24 Dec 2023 13:09:07 GMT
Server: webs
Content-Length: 42
Connection: close
X-Frame-Options: SAMEORIGIN
Cache-Control: no-store
Pragma: no-cache
Content-Type: application/xml
{
"CallStatus": {
"status": "idle"
}
}
* Next attempt fails *
GET /ISAPI/VideoIntercom/callStatus HTTP/1.1
Accept-Encoding: gzip
User-Agent: Jetty/9.4.52.v20230823
Host: 192.168.1.XX
Authorization: Digest username="user", realm="DS-1762ACD3", nonce="NTU2MDI3MTlmNGIwZmNkODZmMzg4OTdiZWI2ZGZhYzk=", opaque="", algorithm="MD5", uri="/ISAPI/VideoIntercom/callStatus", qop="auth", nc="00000002", cnonce="81a72588115fc84c", response="18adfcd5dc6d0427ce31da7210a82327"
HTTP/1.1 401 Unauthorized
Date: Sun, 24 Dec 2023 13:09:19 GMT
Server: webs
Content-Length: 387
Connection: close
X-Frame-Options: SAMEORIGIN
Cache-Control: no-store
Pragma: no-cache
Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8"?>
<ResponseStatus version="1.0" xmlns="http://www.std-cgi.com/ver10/XMLSchema">
<requestURL>/ISAPI/VideoIntercom/callStatus</requestURL>
<statusCode>4</statusCode>
<statusString>Invalid Operation</statusString>
<subStatusCode>invalidOperation</subStatusCode>
<errorCode>1073741830</errorCode>
<errorMsg>invalid operation</errorMsg>
</ResponseStatus>
Not sure who is violating the HTTP Digest Access Authentication, Hikvision or the HTTP binding / Jetty but I notice that all other methods I tried (curl, node-red, etc) re-negotiate the authorization for each call. I think it is problematic to rely on re-using the authorization as multiple servers (like my VMS) are accessing the camera with the same credentials.
I think the binding should re-negotiate authorization when the server (camera in this case) responds “unauthorized”. Or at least there should be some setting to re-negotiate authorization for each call instead of relying on a single previously successful authentication.
Question is whether there is a way to make this work or that a bug report needs to be filed against the binding?
Thanks!