• Qt 利用海康摄像头的ISAPI协议进行抓图等操作


    特别说明:个人笔记,不喜勿喷,纯属记录!~ 部分转载。

      本人有个项目需要用到海康摄像头的抓图功能,但是程序是运行在嵌入式系统,海康提供的SDK不支持ARM版本,咨询技术得知,这种情况可以用拉流的方式进行抓拍,或者用海康的私有协议ISAPI进行http请求亦可完成图像抓拍功能。

      由于我们摄像头是双光摄像头(一路可见光,一路红外),在抓拍过程中还需要对热图进行抓拍,并且热图必须要支持温度矩阵数据,因此背景下,决定尝试ISAPI协议进行数据交互。

      ISAPI协议测试前需要了解海康摄像头的摘要认证,一下部分为摘录部分。

    1. 1 摘要认证介绍
    2. ISAPI 协议基于 HTTP REST 架构,协议交互需要安全认证,Digest 摘要认证比 Basic 基础认证的安全级别
    3. 更高:
    4. 1)通过传递用户名、密码等计算出来的摘要来解决明文方式在网络上发送密码的问题。
    5. 2)通过服务产生随机数 nonce 的方式可以防止恶意用户捕获并重放认证的握手过程。
    6. 1.1 认证握手过程
    7. 1. 客户端发出一个没有认证证书的请求。
    8. GET /ISAPI/Security/userCheck HTTP/1.1
    9. Accept: text/html, application/xhtml+xml, */*
    10. Accept-Language: zh-CN
    11. User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
    12. Accept-Encoding: gzip, deflate
    13. Host: 10.18.37.12
    14. Connection: Keep-Alive
    15. 注:此处示例为用户名密码校验的ISAPI协议命令(GET方法),每次下发新的命令都需要重新认证。
    16. 2. 服务器产生一个随机数nonce,并且将该随机数放在WWW-Authenticate响应头,与服务器支持的认
    17. 证算法列表,认证的域realm一起发送给客户端。
    18. HTTP/1.1 401 Unauthorized
    19. Date: Wed, 30 May 2018 19:16:52 GMT
    20. Server: App-webs/
    21. Content-Length: 178
    22. Content-Type: text/html
    23. Connection: keep-alive
    24. Keep-Alive: timeout=10, max=99
    25. WWW-Authenticate: Digest qop="auth", realm="IP Camera(12345)",
    26. nonce="4e5749344e7a4d794e544936596a4933596a51784e44553d", stale="FALSE"
    27. 注:401 Unauthorized表示认证失败、未授权。返回的WWW-Authenticate表示设备支持的认证方式,此
    28. 处设备只支持Digest摘要认证方式。
    29. ### ===> http reply
    30. HTTP/1.1 401 Unauthorized
    31. Date: Wed, 30 May 2018 19:23:32 GMT
    32. Server: App-webs/
    33. Content-Length: 178
    34. Content-Type: text/html
    35. Connection: keep-alive
    36. Keep-Alive: timeout=10, max=99
    37. WWW-Authenticate: Digest qop="auth", realm="IP Camera(12345)",
    38. nonce="4f5455784e4452684f544136596a49344d54566a4f57553d", stale="FALSE"
    39. WWW-Authenticate: Basic realm="IP Camera(12345)"
    40. 注:401 Unauthorized表示认证失败、未授权。返回的WWW-Authenticate表示设备支持的认证方式,此
    41. 处设备同时支持Digest摘要认证和Basic认证两种方式。stale表示nonce值是否过期,如果过期会生成新的随
    42. 机数。
    43. 3. 客户端接收到401响应表示需要进行认证,选择一个算法(目前只支持MD5)生成一个消息摘要
    44. (message digest,该摘要包含用户名、密码、给定的nonce值、HTTP方法以及所请求的URL),将摘要放到
    45. Authorization的请求头中重新发送命令给服务器。如果客户端要对服务器也进行认证,可以同时发送客户端
    46. 随机数cnonce,客户端是否需要认证,通过报文里面的qop值进行判断,详见1.2章节介绍。
    47. GET /ISAPI/Security/userCheck HTTP/1.1
    48. Accept: text/html, application/xhtml+xml, */*
    49. Accept-Language: zh-CN
    50. User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
    51. Accept-Encoding: gzip, deflate
    52. Host: 10.18.37.12
    53. Connection: Keep-Alive
    54. Authorization: Digest username="admin",realm="IP
    55. Camera(12345)",nonce="595463314d5755354d7a4936596a49344f475a6a5a44453d",uri="/ISAPI/Security/userCheck",cnonc
    56. e="011e08f6c9d5b3e13acfa810ede73ecc",nc=00000001,response="82091ef5aaf9b54118b4887f8720ae06",qop="auth"
    57. 4. 服务接收到摘要,选择算法以及掌握的数据,重新计算新的摘要跟客户端传输的摘要进行比较,验
    58. 证是否匹配,若客户端反过来用客户端随机数对服务器进行质询,就会创建客户端摘要,服务可以预先
    59. 将下一个随机数计算出来,提前传递给客户端,通过 Authentication-Info 发送下一个随机数。该步骤选
    60. 择实现。
    61. HTTP/1.1 200 OK
    62. Date: Wed, 30 May 2018 19:32:49 GMT
    63. Server: App-webs/
    64. Content-Length: 132
    65. Connection: keep-alive
    66. Keep-Alive: timeout=10, max=98
    67. Content-Type: text/xml
    68. <?xml version="1.0" encoding="UTF-8"?>
    69. <userCheck>
    70. <statusValue>200</statusValue>
    71. <statusString>OK</statusString>
    72. </userCheck>
    73. 注:响应200 OK表示认证成功。
    74. 详细说明请参考RFC 2617规范文档。
    75. 1.2 摘要计算过程
    76. 在说明如何计算摘要之前,先说明参加摘要计算的信息块。信息块主要有两种:
    77. 1. 表示与安全相关的数据的A1
    78. A1中的数据时密码和受保护信息的产物,它包括用户名、密码、保护域和随机数等内容,A1只涉及安
    79. 全信息,与底层报文自身无关。
    80. 若算法是:MD5
    81. 则 A1=<user>:<realm>:<password>
    82. 若算法是:MD5-sess
    83. 则 A1=MD5(<user>:<realm>:<password>):<nonce>:<cnonce>
    84. 2. 表示与报文相关的数据的A2
    85. A2表示是与报文自身相关的信息,比如URL,请求反复和报文实体的主体部分,A2加入摘要计算主要目
    86. 的是有助于防止反复,资源或者报文被篡改。
    87. 若 qop 未定义或者 auth:
    88. A2=<request-method>:<uri-directive-value>
    89. 若 qop 为 auth:-int
    90. A2=<request-method>:<uri-directive-value>:MD5(<request-entity-body>)
    91. 注:<uri-directive-value>为完整的协议命令 URI,比如“/ISAPI/Security/userCheck”。
    92. 下面定义摘要的计算规则:
    93. 若 qop 没有定义:
    94. 摘要 response=MD5(MD5(A1):<nonce>:MD5(A2))
    95. 若 qop 为 auth:
    96. 摘要 response=MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2))
    97. 若 qop 为 auth-int:
    98. 摘要 response= MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2))
    99. 1.3 随机数的生成
    100. RFC2617建议采用这个假想的随机数公式:
    101. nonce = BASE64(time-stamp MD5(time-stamp ":" ETag ":" private-key))
    102. 其中:
    103. time-stamp是服务器产生的时间戳或者其他不会重复的序列号,ETag是与所请求实体有关的HTTP ETag
    104. 首部的值,priviate-key是只有服务器知道的数据。
    105. 这样,服务器就可以收到客户端的认证首部之后重新计算散列部分,如果结果与那个首部的随机数不
    106. 符,或者是时间戳的值不够新,就可以拒绝请求,服务器可以通过这种方式来限制随机数的有效持续时间。
    107. 包括了ETag可以防止对已经更新资源版本的重放请求。注意:在随机数中包含客户端IP,服务器好像就
    108. 可以限制原来获取此随机数的客户端重用这个随机数了,但这会破坏代理集群的工作,使用代理集群时候,
    109. 来自单个用户的多条请求通常会经过不同的代理进行传输,而且IP地址欺骗实现起来也不复杂。

      有了以上知识做铺垫,就能大致明白ISAPI协议的具体工作流程了。那么接下来正题开始。

    1、首先需要创建一个摘要认证类(QHttpAuth)

    1. class QHttpAuth {
    2. public:
    3. QHttpAuth() {}
    4. QHttpAuth(QString host, QString uri, QString method, QString user, QString passwd);
    5. void printArgs();
    6. void parseResponse(const QString &_data);
    7. void setPostData(const QString &_type, const QString &_data);
    8. QString host();
    9. QString method();
    10. QString contentType();
    11. QByteArray content();
    12. QString toMd5(const QByteArray &_content) const;
    13. QString getReponse() const;
    14. QByteArray getHttpRequest(bool isAuth = false) const;
    15. void setHttpRequest(QNetworkRequest *_request, bool isAuth = false);
    16. private:
    17. QString _host;
    18. QString _uri;
    19. QString _method;
    20. QString _user;
    21. QString _passwd;
    22. QString _realm;
    23. QString _qop;
    24. QString _nonce;
    25. QString _nc;
    26. QString _cnonce;
    27. QString _reponse;
    28. QString _contentType;
    29. QString _postData;
    30. };

    QHttpAuth.cpp,方法实现

    1. //
    2. QHttpAuth::QHttpAuth(QString host, QString uri, QString method, QString user, QString passwd) :
    3. _host(host),_uri(uri),_method(method),_user(user),_passwd(passwd)
    4. {
    5. _realm = "IP Camera(G9813)";
    6. _nonce = "4f4746684f44686a4e4755364d6a526b4e475a6c5a57493d";
    7. _nc = "00000001";
    8. _qop = "auth";
    9. _cnonce = "Yj2jyQll";
    10. }
    11. void QHttpAuth::printArgs()
    12. {
    13. qDebug() << "Host" << _host;
    14. qDebug() << "Method" << _method;
    15. qDebug() << "Url" << _uri;
    16. qDebug() << "User" << _user;
    17. qDebug() << "Passwd" << _passwd;
    18. }
    19. void QHttpAuth::parseResponse(const QString &_data)
    20. {
    21. if (_data.isEmpty()) return;
    22. QString strAuth = _data;
    23. QRegExp regExp("qop=\"(\\S+)\"");
    24. if (-1 != regExp.indexIn(strAuth)) {
    25. _qop = regExp.cap(1);
    26. }
    27. regExp = QRegExp("nonce=\"(\\S+)\"");
    28. if (-1 != regExp.indexIn(strAuth)) {
    29. _nonce = regExp.cap(1);
    30. }
    31. int index = strAuth.indexOf("realm=\"") + 7;
    32. int count = strAuth.indexOf(", nonce") - index -1;
    33. _realm = strAuth.mid(index, count);
    34. }
    35. void QHttpAuth::setPostData(const QString &_type, const QString &_data)
    36. {
    37. _contentType = _type;
    38. _postData = _data;
    39. }
    40. QString QHttpAuth::host()
    41. {
    42. return _host;
    43. }
    44. QString QHttpAuth::method()
    45. {
    46. return _method;
    47. }
    48. QString QHttpAuth::contentType()
    49. {
    50. return _contentType;
    51. }
    52. QByteArray QHttpAuth::content()
    53. {
    54. return _postData.toUtf8();
    55. }
    56. QString QHttpAuth::toMd5(const QByteArray &_content) const
    57. {
    58. QByteArray md5 = QCryptographicHash::hash(_content, QCryptographicHash::Md5);
    59. return md5.toHex();
    60. }
    61. QString QHttpAuth::getReponse() const
    62. {
    63. #define PRINT_TEST 0
    64. // md5(<username>:<realm>:<password>) --- sha1
    65. QString strA1 = _user + ":" + _realm + ":" + _passwd;
    66. QString md5A1 = toMd5(strA1.toUtf8());
    67. #if PRINT_TEST
    68. qDebug() << "a1 -> str:" << strA1;
    69. qDebug() << "a1 -> md5:" << md5A1;
    70. #endif
    71. // md5(<method>:<url>) -- sha2
    72. QString strA2 = _method + ":" + _uri;
    73. QString md5A2 = toMd5(strA2.toUtf8());
    74. #if PRINT_TEST
    75. qDebug() << "a2 -> str:" << strA2;
    76. qDebug() << "a2 -> md5:" << md5A2;
    77. #endif
    78. // md5(<sha1>:<nonce>:<nc>:<cnonce>:<qop>:<sha2>)
    79. QString strResponse = md5A1 + ":" + _nonce + ":" + _nc + ":" + _cnonce + ":" + _qop + ":" + md5A2;
    80. QString md5Response = toMd5(strResponse.toUtf8());
    81. #if PRINT_TEST
    82. qDebug() << "response -> str:" << strResponse;
    83. qDebug() << "response -> md5:" << md5Response;
    84. #endif
    85. return md5Response;
    86. }
    87. QByteArray QHttpAuth::getHttpRequest(bool isAuth) const
    88. {
    89. QString strRequest = QString("%1 %2 HTTP/1.1\r\n").arg(_method).arg(_uri);
    90. strRequest += QString("Host: %1\r\n").arg(_host);
    91. strRequest += QString("Connection: keep-alive\r\n");
    92. strRequest += QString("User-Agent: \"xiaomu_niyh\"\r\n");
    93. strRequest += QString("Accept: \"*/*\"\r\n");
    94. strRequest += QString("Accept-Encoding: gzip, deflate, br\r\n");
    95. strRequest += QString("Accept-Language: zh-CN,zh;q=0.8\r\n");
    96. if (isAuth)
    97. {
    98. strRequest += QString("Authorization: Digest username=\"%1\", ").arg(_user);
    99. strRequest += QString("realm=\"%1\", ").arg(_realm);
    100. strRequest += QString("nonce=\"%1\", ").arg(_nonce);
    101. strRequest += QString("uri=\"%1\", ").arg(_uri);
    102. strRequest += QString("algorithm=\"MD5\", ");
    103. strRequest += QString("qop=%1, ").arg(_qop);
    104. strRequest += QString("nc=%1, ").arg(_nc);
    105. strRequest += QString("cnonce=\"%1\", ").arg(_cnonce);
    106. strRequest += QString("response=\"%1\"\r\n").arg(getReponse());
    107. }
    108. if (!QString::compare("POST", _method) && !_postData.isEmpty())
    109. {
    110. strRequest += QString("Content-Type: %1\r\n").arg(_contentType);
    111. strRequest += QString("Content-Length: %1\r\n\r\n").arg(_postData.length());
    112. strRequest += _postData;
    113. }
    114. else {
    115. strRequest += "\r\n";
    116. }
    117. return strRequest.toUtf8();
    118. }
    119. /*此方法可以用于QNetworkAccessManager */
    120. void QHttpAuth::setHttpRequest(QNetworkRequest *_request, bool isAuth)
    121. {
    122. _request->setUrl(QString("http://%1:80%2").arg(_host).arg(_uri));
    123. _request->setRawHeader("Host", _host.toUtf8());
    124. _request->setRawHeader("Connection", "keep-alive");
    125. _request->setRawHeader("User-Agent", "xiaomu_niyh");
    126. _request->setRawHeader("Accept", "*/*");
    127. _request->setRawHeader("Accept-Encoding", "gzip, deflate, br");
    128. _request->setRawHeader("Accept-Language", "zh-CN,zh;q=0.8");
    129. if (isAuth){
    130. QString strDigest = QString("Digest username=\"%1\", ").arg(_user);
    131. strDigest += QString("realm=\"%1\", ").arg(_realm);
    132. strDigest += QString("nonce=\"%1\", ").arg(_nonce);
    133. strDigest += QString("uri=\"%1\", ").arg(_uri);
    134. strDigest += QString("algorithm=\"MD5\", ");
    135. strDigest += QString("qop=%1, ").arg(_qop);
    136. strDigest += QString("nc=%1, ").arg(_nc);
    137. strDigest += QString("cnonce=\"%1\", ").arg(_cnonce);
    138. strDigest += QString("response=\"%1\"\r\n").arg(getReponse());
    139. _request->setRawHeader("Authorization", strDigest.toUtf8());
    140. }
    141. if ("POST" == _method)
    142. {
    143. _request->setHeader(QNetworkRequest::ContentTypeHeader, QVariant(_contentType));
    144. _request->setHeader(QNetworkRequest::ContentLengthHeader, _postData.length());
    145. }
    146. }

    2、接下来就是ISAPI的通信交互流程。此处啰嗦点,本来可以直接用

    QNetworkAccessManager来我完成http的相关请求,但是自己想研究下具体通信流程,于是直接用的QTcpSocket完成的相关协议组装及解析等。 不多说上代码

    头文件:

    1. class QIsapiThread : public QThread
    2. {
    3. Q_OBJECT
    4. public:
    5. explicit QIsapiThread(QObject *parent = nullptr);
    6. ~QIsapiThread();
    7. void setHttpAuth(QHttpAuth _auth);
    8. signals:
    9. void showResult(const QString &_text);
    10. void signalContent(const QString &_type, const QByteArray &_byContent);
    11. private:
    12. void httpRequest();
    13. private:
    14. QHttpAuth m_auth;
    15. protected:
    16. void run() override;
    17. bool m_isRun;
    18. };

    方法一:Qt原生http请求方法

    1. void QIsapiThread::run()
    2. {
    3. QNetworkAccessManager http;
    4. QNetworkReply *_reply;
    5. QNetworkRequest request;
    6. int check = 0;
    7. do {
    8. m_auth.setHttpRequest(&request, 0 != check);
    9. if ("POST" == m_auth.method())
    10. {
    11. _reply = http.post(request, m_auth.content());
    12. qDebug() << "====> POST" << m_auth.content();
    13. }
    14. else {
    15. _reply = http.get(request);
    16. }
    17. QEventLoop eventLoop;
    18. connect(_reply, SIGNAL(finished()), &eventLoop, SLOT(quit()));
    19. eventLoop.exec();
    20. int stateCode = _reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    21. qDebug() << "stateCode" << stateCode ;
    22. if (_reply->hasRawHeader("WWW-Authenticate") && stateCode == 401) {
    23. qDebug() << _reply->rawHeader("WWW-Authenticate");
    24. QString strAuth = _reply->rawHeader("WWW-Authenticate");
    25. m_auth.parseResponse(strAuth);
    26. }
    27. else if (200 == stateCode) {
    28. QString strType = _reply->rawHeader("Content-Type");
    29. int len = _reply->rawHeader("Content-Length").toInt();
    30. QByteArray _byData = _reply->readAll();
    31. qDebug() << strType << len << _byData.length();
    32. emit signalContent(strType, _byData);
    33. }
    34. else {
    35. qDebug() << _reply->readAll();
    36. }
    37. } while (check++ < 1);
    38. }

    方法二: QTcpSocket数据重组方式

    1. void QIsapiThread::run()
    2. {
    3. QTcpSocket _tcpsocket;
    4. int check = 0;
    5. do {
    6. _tcpsocket.connectToHost(m_auth.host(), 80);
    7. _tcpsocket.waitForConnected(3000);
    8. check++;
    9. } while (check < 3 && QTcpSocket::ConnectedState != _tcpsocket.state());
    10. if (QTcpSocket::ConnectedState != _tcpsocket.state())
    11. {
    12. qDebug() << "Connect server timeout";
    13. return;
    14. }
    15. check = 0;
    16. QByteArray _cmd;
    17. do {
    18. _cmd = m_auth.getHttpRequest(0 != check);
    19. _tcpsocket.write(_cmd);
    20. QByteArray byContent;
    21. int bytes = 0;
    22. do {
    23. _tcpsocket.waitForReadyRead(1000);
    24. QByteArray readBuff;
    25. readBuff.resize(2048);
    26. bytes = _tcpsocket.read(readBuff.data(), readBuff.length());
    27. if (bytes > 0) {
    28. byContent += readBuff.left(bytes);
    29. }
    30. } while (bytes > 0);
    31. if (0 == check){
    32. m_auth.parseResponse(byContent);
    33. } else {
    34. int statusCode = 0;
    35. QRegExp regExp("HTTP/1.1 (\\d+) ");
    36. if (-1 != regExp.indexIn(byContent.left(20)))
    37. {
    38. statusCode = regExp.cap(1).toInt();
    39. }
    40. if (200 == statusCode)
    41. {
    42. int index = byContent.lastIndexOf("\r\n\r\n");
    43. QByteArray httpHeader = byContent.left(index + 4);
    44. int start = httpHeader.lastIndexOf("Content-Length");
    45. int len = httpHeader.mid(start + 15, index - start - 15).toInt();
    46. QByteArray _byData = byContent.mid(index + 4, len);
    47. emit signalContent("image/jpeg", _byData);
    48. }
    49. else {
    50. qDebug() << "Error" << statusCode << byContent;
    51. }
    52. }
    53. check ++;
    54. } while (check < 2);
    55. _tcpsocket.close();
    56. }

    3、以上就完成了摄像头的认证工作。上几张效果图

    3.1抓拍可见光

    ISAPI接口:/ISAPI/Streaming/channels/101/picture

     3.2 抓拍红外热图(带点位矩阵)

    这个方法需要用的POST的方法,

    接口:/ISAPI/Thermal/channels/2/thermometry/jpegPicWithAppendData?format=json

    POST参数:

    1. {
    2. "JpegPicWithAppendDataParam": {
    3. "captureMode": "standard"
    4. }
    5. }

     3.3 下面列一个数据组包的log。

     

    至此ISAPI认证完毕。已达到预期。当然这个协议不一定要用Qt来完成,我之所以用QTcpSocket的方式来执行一遍就是方便以后换成c语言或者其他语言,需要理解交互流程。

    4、扩展

    该方法理解过后,可以应对海康摄像头其他任何ISAPI接口。大家可以根据自己的需求进行测试。

    5、致谢

    最后如果需要demo源码的可以评论区留言,看情况发送。感谢大家。

  • 相关阅读:
    【强化学习论文合集 | 2020年合集】四. ICLR-2020 强化学习论文
    嵌入式linux--sysfs文件系统以及操作GPIO
    HashMap详解
    多模态知识图谱构建系统论文笔记
    MySQL 数据库主从复制
    TCP三次握手具体过程
    机器学习-模型评估与选择(第2章)课后习题
    电吉他学习笔记
    如何才能设计出“好的”测试用例?
    Linux(12)进程间通信之管道
  • 原文地址:https://blog.csdn.net/nigoole/article/details/125632600