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

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

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

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

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

1 摘要认证介绍 
ISAPI 协议基于 HTTP REST 架构,协议交互需要安全认证,Digest 摘要认证比 Basic 基础认证的安全级别
更高: 
1)通过传递用户名、密码等计算出来的摘要来解决明文方式在网络上发送密码的问题。 
2)通过服务产生随机数 nonce 的方式可以防止恶意用户捕获并重放认证的握手过程。 
1.1 认证握手过程 
1. 客户端发出一个没有认证证书的请求。 
GET /ISAPI/Security/userCheck HTTP/1.1 
Accept: text/html, application/xhtml+xml, */* 
Accept-Language: zh-CN 
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko 
Accept-Encoding: gzip, deflate 
Host: 10.18.37.12 
Connection: Keep-Alive 
注:此处示例为用户名密码校验的ISAPI协议命令(GET方法),每次下发新的命令都需要重新认证。 
 
  
2. 服务器产生一个随机数nonce,并且将该随机数放在WWW-Authenticate响应头,与服务器支持的认
证算法列表,认证的域realm一起发送给客户端。 
HTTP/1.1 401 Unauthorized 
Date: Wed, 30 May 2018 19:16:52 GMT 
Server: App-webs/ 
Content-Length: 178 
Content-Type: text/html 
Connection: keep-alive 
Keep-Alive: timeout=10, max=99 
WWW-Authenticate: Digest qop="auth", realm="IP Camera(12345)", 
nonce="4e5749344e7a4d794e544936596a4933596a51784e44553d", stale="FALSE" 
注:401 Unauthorized表示认证失败、未授权。返回的WWW-Authenticate表示设备支持的认证方式,此
处设备只支持Digest摘要认证方式。 
 
### ===> http reply
HTTP/1.1 401 Unauthorized 
Date: Wed, 30 May 2018 19:23:32 GMT 
Server: App-webs/ 
Content-Length: 178 
Content-Type: text/html 
 
Connection: keep-alive 
Keep-Alive: timeout=10, max=99 
WWW-Authenticate: Digest qop="auth", realm="IP Camera(12345)", 
nonce="4f5455784e4452684f544136596a49344d54566a4f57553d", stale="FALSE" 
WWW-Authenticate: Basic realm="IP Camera(12345)" 
注:401 Unauthorized表示认证失败、未授权。返回的WWW-Authenticate表示设备支持的认证方式,此
处设备同时支持Digest摘要认证和Basic认证两种方式。stale表示nonce值是否过期,如果过期会生成新的随
机数。 
 
3. 客户端接收到401响应表示需要进行认证,选择一个算法(目前只支持MD5)生成一个消息摘要
(message digest,该摘要包含用户名、密码、给定的nonce值、HTTP方法以及所请求的URL),将摘要放到
Authorization的请求头中重新发送命令给服务器。如果客户端要对服务器也进行认证,可以同时发送客户端
随机数cnonce,客户端是否需要认证,通过报文里面的qop值进行判断,详见1.2章节介绍。 
GET /ISAPI/Security/userCheck HTTP/1.1 
Accept: text/html, application/xhtml+xml, */* 
Accept-Language: zh-CN 
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko 
Accept-Encoding: gzip, deflate 
Host: 10.18.37.12 
Connection: Keep-Alive 
Authorization: Digest username="admin",realm="IP 
Camera(12345)",nonce="595463314d5755354d7a4936596a49344f475a6a5a44453d",uri="/ISAPI/Security/userCheck",cnonc
e="011e08f6c9d5b3e13acfa810ede73ecc",nc=00000001,response="82091ef5aaf9b54118b4887f8720ae06",qop="auth" 
  
4. 服务接收到摘要,选择算法以及掌握的数据,重新计算新的摘要跟客户端传输的摘要进行比较,验
证是否匹配,若客户端反过来用客户端随机数对服务器进行质询,就会创建客户端摘要,服务可以预先
将下一个随机数计算出来,提前传递给客户端,通过 Authentication-Info 发送下一个随机数。该步骤选
择实现。 
HTTP/1.1 200 OK 
Date: Wed, 30 May 2018 19:32:49 GMT 
Server: App-webs/ 
Content-Length: 132 
Connection: keep-alive 
Keep-Alive: timeout=10, max=98 
Content-Type: text/xml 
 
<?xml version="1.0" encoding="UTF-8"?> 
<userCheck> 
<statusValue>200</statusValue> 
<statusString>OK</statusString> 
</userCheck> 
注:响应200 OK表示认证成功。 
 

详细说明请参考RFC 2617规范文档。 
1.2 摘要计算过程 
在说明如何计算摘要之前,先说明参加摘要计算的信息块。信息块主要有两种: 
1. 表示与安全相关的数据的A1 
A1中的数据时密码和受保护信息的产物,它包括用户名、密码、保护域和随机数等内容,A1只涉及安
全信息,与底层报文自身无关。 
若算法是:MD5 
则 A1=<user>:<realm>:<password> 
若算法是:MD5-sess 
则 A1=MD5(<user>:<realm>:<password>):<nonce>:<cnonce> 
 
2. 表示与报文相关的数据的A2 
A2表示是与报文自身相关的信息,比如URL,请求反复和报文实体的主体部分,A2加入摘要计算主要目
的是有助于防止反复,资源或者报文被篡改。 
若 qop 未定义或者 auth: 
A2=<request-method>:<uri-directive-value> 
若 qop 为 auth:-int 
A2=<request-method>:<uri-directive-value>:MD5(<request-entity-body>) 
注:<uri-directive-value>为完整的协议命令 URI,比如“/ISAPI/Security/userCheck”。 
 
下面定义摘要的计算规则: 
若 qop 没有定义: 
摘要 response=MD5(MD5(A1):<nonce>:MD5(A2)) 
  
若 qop 为 auth: 
摘要 response=MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2)) 
 
若 qop 为 auth-int: 
摘要 response= MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2)) 

1.3 随机数的生成 
RFC2617建议采用这个假想的随机数公式: 
nonce = BASE64(time-stamp MD5(time-stamp ":" ETag ":" private-key)) 
其中: 
time-stamp是服务器产生的时间戳或者其他不会重复的序列号,ETag是与所请求实体有关的HTTP ETag
首部的值,priviate-key是只有服务器知道的数据。 
 
这样,服务器就可以收到客户端的认证首部之后重新计算散列部分,如果结果与那个首部的随机数不
符,或者是时间戳的值不够新,就可以拒绝请求,服务器可以通过这种方式来限制随机数的有效持续时间。 
 
包括了ETag可以防止对已经更新资源版本的重放请求。注意:在随机数中包含客户端IP,服务器好像就
可以限制原来获取此随机数的客户端重用这个随机数了,但这会破坏代理集群的工作,使用代理集群时候,
来自单个用户的多条请求通常会经过不同的代理进行传输,而且IP地址欺骗实现起来也不复杂。 
 

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

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

class QHttpAuth {
public:
    QHttpAuth() {}
    QHttpAuth(QString host, QString uri, QString method, QString user, QString passwd);

    void printArgs();
    void parseResponse(const QString &_data);
    void setPostData(const QString &_type, const QString &_data);

    QString host();
    QString method();
    QString contentType();
    QByteArray content();

    QString toMd5(const QByteArray &_content) const;
    QString getReponse() const;
    QByteArray getHttpRequest(bool isAuth = false) const;

    void setHttpRequest(QNetworkRequest *_request, bool isAuth = false);

private:
    QString _host;
    QString _uri;
    QString _method;
    QString _user;
    QString _passwd;

    QString _realm;
    QString _qop;
    QString _nonce;
    QString _nc;
    QString _cnonce;
    QString _reponse;

    QString _contentType;
    QString _postData;
};

QHttpAuth.cpp,方法实现

//
QHttpAuth::QHttpAuth(QString host, QString uri, QString method, QString user, QString passwd) :
    _host(host),_uri(uri),_method(method),_user(user),_passwd(passwd)
{
    _realm = "IP Camera(G9813)";
    _nonce = "4f4746684f44686a4e4755364d6a526b4e475a6c5a57493d";
    _nc = "00000001";
    _qop = "auth";
    _cnonce = "Yj2jyQll";
}

void QHttpAuth::printArgs()
{
    qDebug() << "Host" << _host;
    qDebug() << "Method" << _method;
    qDebug() << "Url" << _uri;
    qDebug() << "User" << _user;
    qDebug() << "Passwd" << _passwd;
}

void QHttpAuth::parseResponse(const QString &_data)
{
    if (_data.isEmpty()) return;
    QString strAuth = _data;

    QRegExp regExp("qop=\"(\\S+)\"");
    if (-1 != regExp.indexIn(strAuth)) {
        _qop = regExp.cap(1);
    }

    regExp = QRegExp("nonce=\"(\\S+)\"");
    if (-1 != regExp.indexIn(strAuth)) {
        _nonce = regExp.cap(1);
    }

    int index = strAuth.indexOf("realm=\"") + 7;
    int count = strAuth.indexOf(", nonce") - index -1;
    _realm = strAuth.mid(index, count);
}

void QHttpAuth::setPostData(const QString &_type, const QString &_data)
{
    _contentType = _type;
    _postData = _data;
}

QString QHttpAuth::host()
{
    return _host;
}

QString QHttpAuth::method()
{
    return _method;
}

QString QHttpAuth::contentType()
{
    return _contentType;
}

QByteArray QHttpAuth::content()
{
    return _postData.toUtf8();
}

QString QHttpAuth::toMd5(const QByteArray &_content) const
{
    QByteArray md5 = QCryptographicHash::hash(_content, QCryptographicHash::Md5);
    return md5.toHex();
}

QString QHttpAuth::getReponse() const
{
#define PRINT_TEST 0
    // md5(<username>:<realm>:<password>)  --- sha1
    QString strA1 = _user + ":" + _realm + ":" + _passwd;
    QString md5A1 = toMd5(strA1.toUtf8());
#if PRINT_TEST
    qDebug() << "a1 -> str:" << strA1;
    qDebug() << "a1 -> md5:" << md5A1;
#endif

    // md5(<method>:<url>) -- sha2
    QString strA2 = _method + ":" + _uri;
    QString md5A2 = toMd5(strA2.toUtf8());
#if PRINT_TEST
    qDebug() << "a2 -> str:" << strA2;
    qDebug() << "a2 -> md5:" << md5A2;
#endif

    // md5(<sha1>:<nonce>:<nc>:<cnonce>:<qop>:<sha2>)
    QString strResponse = md5A1 + ":" + _nonce + ":" + _nc + ":" + _cnonce + ":" + _qop + ":" + md5A2;
    QString md5Response = toMd5(strResponse.toUtf8());
#if PRINT_TEST
    qDebug() << "response -> str:" << strResponse;
    qDebug() << "response -> md5:" << md5Response;
#endif

    return md5Response;
}

QByteArray QHttpAuth::getHttpRequest(bool isAuth) const
{
    QString strRequest = QString("%1 %2 HTTP/1.1\r\n").arg(_method).arg(_uri);
    strRequest += QString("Host: %1\r\n").arg(_host);
    strRequest += QString("Connection: keep-alive\r\n");
    strRequest += QString("User-Agent: \"xiaomu_niyh\"\r\n");
    strRequest += QString("Accept: \"*/*\"\r\n");
    strRequest += QString("Accept-Encoding: gzip, deflate, br\r\n");
    strRequest += QString("Accept-Language: zh-CN,zh;q=0.8\r\n");
    if (isAuth)
    {
        strRequest += QString("Authorization: Digest username=\"%1\", ").arg(_user);
        strRequest += QString("realm=\"%1\", ").arg(_realm);
        strRequest += QString("nonce=\"%1\", ").arg(_nonce);
        strRequest += QString("uri=\"%1\", ").arg(_uri);
        strRequest += QString("algorithm=\"MD5\", ");
        strRequest += QString("qop=%1, ").arg(_qop);
        strRequest += QString("nc=%1, ").arg(_nc);
        strRequest += QString("cnonce=\"%1\", ").arg(_cnonce);
        strRequest += QString("response=\"%1\"\r\n").arg(getReponse());
    }

    if (!QString::compare("POST", _method) && !_postData.isEmpty())
    {
        strRequest += QString("Content-Type: %1\r\n").arg(_contentType);
        strRequest += QString("Content-Length: %1\r\n\r\n").arg(_postData.length());
        strRequest += _postData;
    }
    else {
        strRequest += "\r\n";
    }

    return strRequest.toUtf8();
}

/*此方法可以用于QNetworkAccessManager */
void QHttpAuth::setHttpRequest(QNetworkRequest *_request, bool isAuth)
{
    _request->setUrl(QString("http://%1:80%2").arg(_host).arg(_uri));
    _request->setRawHeader("Host", _host.toUtf8());
    _request->setRawHeader("Connection", "keep-alive");
    _request->setRawHeader("User-Agent", "xiaomu_niyh");
    _request->setRawHeader("Accept", "*/*");
    _request->setRawHeader("Accept-Encoding", "gzip, deflate, br");
    _request->setRawHeader("Accept-Language", "zh-CN,zh;q=0.8");

    if (isAuth){
        QString strDigest = QString("Digest username=\"%1\", ").arg(_user);
        strDigest += QString("realm=\"%1\", ").arg(_realm);
        strDigest += QString("nonce=\"%1\", ").arg(_nonce);
        strDigest += QString("uri=\"%1\", ").arg(_uri);
        strDigest += QString("algorithm=\"MD5\", ");
        strDigest += QString("qop=%1, ").arg(_qop);
        strDigest += QString("nc=%1, ").arg(_nc);
        strDigest += QString("cnonce=\"%1\", ").arg(_cnonce);
        strDigest += QString("response=\"%1\"\r\n").arg(getReponse());
        _request->setRawHeader("Authorization", strDigest.toUtf8());
    }

    if ("POST" == _method)
    {
        _request->setHeader(QNetworkRequest::ContentTypeHeader, QVariant(_contentType));
        _request->setHeader(QNetworkRequest::ContentLengthHeader, _postData.length());
    }
}

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

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

头文件:

class QIsapiThread : public QThread
{
    Q_OBJECT
public:
    explicit QIsapiThread(QObject *parent = nullptr);
    ~QIsapiThread();

    void setHttpAuth(QHttpAuth _auth);

signals:
    void showResult(const QString &_text);
    void signalContent(const QString &_type, const QByteArray &_byContent);

private:
    void httpRequest();

private:
    QHttpAuth m_auth;

protected:
    void run() override;
    bool m_isRun;
};

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

void QIsapiThread::run()
{
    QNetworkAccessManager http;
    QNetworkReply *_reply;
    QNetworkRequest request;
    int check = 0;
    do {
        m_auth.setHttpRequest(&request, 0 != check);
        if ("POST" == m_auth.method())
        {
            _reply = http.post(request, m_auth.content());
            qDebug() << "====> POST" << m_auth.content();
        }
        else {
            _reply = http.get(request);
        }

        QEventLoop eventLoop;
        connect(_reply, SIGNAL(finished()), &eventLoop, SLOT(quit()));
        eventLoop.exec();

        int stateCode = _reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
        qDebug() << "stateCode" << stateCode ;

        if (_reply->hasRawHeader("WWW-Authenticate") && stateCode == 401) {
            qDebug() << _reply->rawHeader("WWW-Authenticate");
            QString strAuth = _reply->rawHeader("WWW-Authenticate");
            m_auth.parseResponse(strAuth);
        }
        else if (200 == stateCode) {
            QString strType = _reply->rawHeader("Content-Type");
            int len = _reply->rawHeader("Content-Length").toInt();
            QByteArray _byData = _reply->readAll();
            qDebug() << strType << len << _byData.length();
            emit signalContent(strType, _byData);
        }
        else {
            qDebug() << _reply->readAll();
        }
    } while (check++ < 1);
}

方法二: QTcpSocket数据重组方式

void QIsapiThread::run()
{
    QTcpSocket _tcpsocket;
    int check = 0;
    do {
        _tcpsocket.connectToHost(m_auth.host(), 80);
        _tcpsocket.waitForConnected(3000);
        check++;
    } while (check < 3 && QTcpSocket::ConnectedState != _tcpsocket.state());

    if (QTcpSocket::ConnectedState != _tcpsocket.state())
    {
        qDebug() << "Connect server timeout";
        return;
    }

    check = 0;
    QByteArray _cmd;
    do {
        _cmd = m_auth.getHttpRequest(0 != check);
        _tcpsocket.write(_cmd);
        QByteArray byContent;
        int bytes = 0;
        do {
            _tcpsocket.waitForReadyRead(1000);
            QByteArray readBuff;
            readBuff.resize(2048);
            bytes = _tcpsocket.read(readBuff.data(), readBuff.length());
            if (bytes > 0) {
                byContent += readBuff.left(bytes);
            }
        } while (bytes > 0);

        if (0 == check){
            m_auth.parseResponse(byContent);
        } else {
            int statusCode = 0;
            QRegExp regExp("HTTP/1.1 (\\d+) ");
            if (-1 != regExp.indexIn(byContent.left(20)))
            {
                statusCode = regExp.cap(1).toInt();
            }
            if (200 == statusCode)
            {
                int index = byContent.lastIndexOf("\r\n\r\n");
                QByteArray httpHeader = byContent.left(index + 4);
                int start = httpHeader.lastIndexOf("Content-Length");
                int len = httpHeader.mid(start + 15, index - start - 15).toInt();
                QByteArray _byData = byContent.mid(index + 4, len);
                emit signalContent("image/jpeg", _byData);
            }
            else {
                qDebug() << "Error" << statusCode << byContent;
            }
        }
        check ++;
    } while (check < 2);

    _tcpsocket.close();
}

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

3.1抓拍可见光

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

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

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

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

POST参数:

{
    "JpegPicWithAppendDataParam": {
        "captureMode": "standard"
    }
}

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

 

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

4、扩展

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

5、致谢

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

  • 6
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 28
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 28
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值