HTTP Binding Request only works once

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!

Looks like a bug. The authentication cache is not cleared on 401.

Thanks, I filed a bug report:

That is allowed by the HTTP standards, I tried to implement it into the ipcamera binding, however all the cameras I tried did not like doing it. I believe it is simply the cameras do not have a shortcut way implemented in them so they have to do 2 http requests instead of doing 1 per call your wanting to do. More overheads because the NC method does not work.

Thanks for making modifications to the http binding @J-N-K! Even with buggy IP camera’s the http requests now work reliably. Very much appreciated!!