工具

自从国标28181推出以来,国家安防行业一直在主推这个标准协议。刚开始确实有不少阻力,比如很多厂家还采用私有协议或是ONVIF协议作为主要的对接协议。这样很大的阻碍了安防行业的互联互通,虽然GB28181的推出刚出来是有些不足(比如缺少很多的控制信令标准),这个在2014补充版及2016正式版(替代2011版)已经基本完善了。

GB28181最大的优势在于采用了SIP协议作为通信信令,RTP协议作为流媒体传送协议。这样基本上与国际标准协议接轨,极大的促进了安防行业的资源整合。比如以前的摄像头都是零散的,每个公安局只能看到自己小范围的监控录像,发生了重大案件,还需要把硬盘监控资料交到上级部门,上级部门再组织大量人员人工回放查看,这样太影响效率了!不能更快的把案情处理,那天网工程投入这么多银子不打水漂了。公安部门强制在招标文件中规定要求支持28181,所以后面越来越多的设备,平台都把GB28181协议作为主要的互联协议。促进了GB28181的发展以及整个安防行业的发展。

现在越来越多的大企业加入了安防,阿里、华为、腾讯这种超大公司也都有自己的28181平台,阿里云还在做支持国标28181的设备协议接入。下面关于是如何在终端实现GB28181的设计思路:

开发身份:要分设备端(IPC、NVR、平台服务器)

自动化测试工具

  • https://pan.baidu.com/s/1pslazYuasUemp3i_vnwRSA,提取码: 3d3v ,截图

怎么用:

需要同时打开GB28181-2016自动化测试工具\GB28181Test.exeGB28181-2016模拟设备\GB28181Device.exe

  • GB28181-2016模拟设备:模拟了一个IPC客户端
  • GB28181-2016自动化测试工具:是GB28181服务端。
    注意GB28181Test.exe界面上配置的,和config.xml文件中填写的必须一致

域名配置:

  • sipID:44010200492000000001
  • sip域:4401020049
  • sipID:34020000002000000001
  • sip域:3402000000

实现注册功能

理论

流程如下:

下级平台,向上级平台(SIP服务器)发起注册

  • Step.1: 注册设备对方中心服务器发送 Register 注册消息
  • Step.2: 中心服务器检查注册设备带来信令中的 Authorization 字段(鉴权字段),发现 Register 信令中未带鉴权字段。回复注册设备: 401 Unauthorized(注册未带鉴权)。注意,这不是异常报错,这是国标注册中的正常流程。
  • Step.3:注册设备重新向中心服务器发送 Register 注册消息,并带上鉴权字段(Register With Authorized)
  • Step.4:中心服务器检查 Authorization 字段,如果该鉴权通过,则回复 200OK,设备在线。
  1. 下级平台(注册设备)向上级平台(服务器)注册, 下级平台需要知道上级平台 sipid、ip、端口号,这样就可以向上级平台注册了。
  2. 上级平台收到下级平台注册消息后, 需要构造sipto, sipfrom, sip contact 这些header, 这些值怎么取?, 其实这些值就在客户请求过的 eXosip_event_t 对象里面

抓包

注意,下面不是同一个抓包流程,仅仅分析怎么填写

第零步:启动服务端

启动CG28181 服务端,也即 SIP Server,这正是我们要实现的

第一步:【设备>>服务端】请求注册

摄像机端发送Register消息,如果服务端不应答,摄像机端会一直发送直到收到服务端应答为止。如果服务器端重新运行,需要手动再次开启设备。

REGISTER sip:440102004921111111111@4401020049 SIP/2.0
Via: SIP/2.0/TCP 192.168.0.213:51762;rport;branch=z9hG4bK2144142982
From: <sip:44010200492222222221@4401020049>;tag=1054224337
To: <sip:44010200492222222221@4401020049>
Call-ID: 1507740566
CSeq: 1 REGISTER
Contact: <sip:44010200492222222221@192.168.0.213:33727>
Max-Forwards: 70
User-Agent: IP Camera
Expires: 3600
Content-Length: 0

看下起始行:

REGISTER sip:440102004921111111111@4401020049 SIP/2.0
  • 表示设备向域名为4401020049的服务器发送注册,SIP版本号为2.0

    • 440102004921111111111为SIP服务器ID
    • 4401020049 为SIP服务器域,这里也可以填写SIP服务器的地址,类似92.168.10.177:5060
Via: SIP/2.0/TCP 192.168.0.213:51762;rport;branch=z9hG4bK2144142982
  • 192.168.0.213:51762:为摄像头IP和端口
From: <sip:44010200492222222221@4401020049>;tag=1054224337
  • 表示这个请求是哪个设备发起的。意思是该设备是由域名为4401020049的服务器控制的,它将向4401020049发送一个注册请求。

    • 44010200492222222221:设备ID,也叫做 SIP用户名,即设备的ID44010200492222222221,用来标识当前设备的省份
    • 4401020049:SIP服务器域名,也可以写成IP格式,比如92.168.10.177:5060
To: <sip:44010200492222222221@4401020049>

请求起始行:表示UAC向IP地址为192.168.10.177的服务器发起注册,SIP版本号为2.0

From字段:指明该REGISTER请求消息由UAS(IP地址:192.168.10.177)控制的UAC发起的。

To字段:指明REGISTER请求接收方的地址。此时REGISTER请求的接收方为IP地址为192.168.10.177的UAS。(这个值和To头域的值相同,除非这个请求是第三方发起的注册请求。)
Call-ID字段:UAC发出的给某个注册服务器(registrar)的所有注册请求都应该有相同的Call-ID头域值。如果相同的客户端用了不同的Call-ID值,注册服务器(registrar)就不能检测是否一个REGISTER请求由于延时的关系导致了故障。
Cseq字段:Cseq值保证了REGISTER请求的正确顺序。一个UA为每一个具备相同的Call-ID的REGISTER请求顺序递增这个Cseq字段。
Contact字段:在REGISTER请求中的Contact字段指明用户可达位置。
Expires字段:表示该登记生存期为3600s。

  • Content-Length字段:表明此请求消息消息体的长度为空,即此消息不带会话描述。

第二步:【服务端>>设备】要求鉴权

SIP/2.0 401 Unauthorized
Via: SIP/2.0/UDP 192.168.0.107:55204;branch=z9hG4bKc1b6d
From: <sip:34020000001110000001@4401020049>;tag=4e3e
To: <sip:34020000001110000001@4401020049>;tag=1724123124
Call-ID: 00002F3F00007AEF@192.168.0.107
CSeq: 1 REGISTER
WWW-Authenticate: Digest realm="34020000", nonce="awer23sdfj123123", opaque="c3a02f1ecb122d255c4ae2266129d044", algorithm=MD5
User-Agent: General
Content-Length: 0

第三步:【设备>>服务端】携带鉴权信息

REGISTER sip:sip:44010200492000000001@192.168.0.60:5060 SIP/2.0
Via: SIP/2.0/UDP 192.168.0.107:55204;branch=z9hG4bKc1bda
From: <sip:34020000001110000001@4401020049>;tag=4e3e
To: <sip:34020000001110000001@4401020049>
Contact: <sip:34020000001110000001@192.168.0.107:55204>
Call-ID: 00002F3F00007AEF@192.168.0.107
CSeq: 2 REGISTER
Max-Forwards: 70
Expires: 3600
Allow: INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, NOTIFY, MESSAGE, SUBSCRIBE, INFO
User-Agent: HTSIP AGENT 2.0
Authorization: Digest username="34020000001110000001",realm="34020000",uri="sip:192.168.0.60",nonce="awer23sdfj123123",response="aeb71cf5e03ec6ae60b82e7f013357fa",algorithm=MD5
Content-Length: 0

(2)第四步:【服务端>>设备】,回应鉴权成功

SIP/2.0 200 OK
Via: SIP/2.0/UDP 192.168.0.107:55204;branch=z9hG4bKc1bda
From: <sip:34020000001110000001@4401020049>;tag=4e3e
To: <sip:34020000001110000001@4401020049>;tag=31243r3412
Call-ID: 00002F3F00007AEF@192.168.0.107
CSeq: 2 REGISTER
User-Agent: General
Date: 2022-05-19T21:19:29
Expires: 300
Content-Length: 0

实践

IPC客户端发送注册请求

运行[GB28181-2016模拟设备]。注意填好IP地址(下面之所以填写5060,是因为代码中运行了5060)

我们用wireshark抓包抓到的包如下:

wireshark抓到的包内容:

REGISTER sip:sip:44010200492000000001@192.168.0.12:5060 SIP/2.0
Via: SIP/2.0/UDP 192.168.0.60:60564;branch=z9hG4bK15982d8f
From: <sip:34020000001110000001@4401020049>;tag=4989
To: <sip:34020000001110000001@4401020049>
Contact: <sip:34020000001110000001@192.168.0.60:60564>
Call-ID: 0000575C00003D94@192.168.0.60
CSeq: 1 REGISTER
Max-Forwards: 70
Expires: 3600
Allow: INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, NOTIFY, MESSAGE, SUBSCRIBE, INFO
User-Agent: HTSIP AGENT 2.0
Content-Length: 0

怎么用代码接收上面的内容呢?

接收IPC客户端发来的信息

#include <stdlib.h>
#include <stdio.h>
#include <Common/Log.h>
#include "eXosip2/eXosip.h"
#include "osipparser2/osip_message.h"
#include "eXosip2.h"//初始化 监听端口
int Init(int listenport)
{int iRet = eXosip_init();if (iRet != OSIP_SUCCESS){printf("eXosip2 fail! \n");exit(1);}iRet = eXosip_listen_addr(IPPROTO_UDP, NULL, listenport, AF_INET, 0);if (iRet != OSIP_SUCCESS){eXosip_quit();DBGPrint(M_SipUA, BREAK_LEVEL, "eXosip2 could not initialize transport layer! \n");exit(1);}DBGPrint(M_SipUA, BREAK_LEVEL, "%s: SipSvr Listen Port:%d Sucess!!!!", __FUNCTION__ , listenport);return 0;
}int main(int argc, char *argv[]) {logger::Instance();LOG_START("", DEBUG_LEVEL, 0, "");//初始化exosip协议栈端口Init(5060);//等待接收sip数据while (1) {//---------------- eXosip running process ----------------//eXosip_execute();  //这是必须的eXosip_event_t *pSipEvt = eXosip_event_wait(0, 50);while (pSipEvt != NULL) {printf("-----------------\r\n");eXosip_lock();eXosip_default_action(pSipEvt);eXosip_unlock();eXosip_event_free(pSipEvt);pSipEvt = eXosip_event_wait(0, 50);}usleep(50000);}
}

运行效果

看到了服务端有反应了,

pSipEvt就是刚刚接收到的内容,我们打印一下:


int main(int argc, char *argv[]) {logger::Instance();LOG_START("", DEBUG_LEVEL, 0, "");//初始化exosip协议栈端口Init(5060);//等待接收sip数据while (1) {//---------------- eXosip running process ----------------//eXosip_execute();  //这是必须的eXosip_event_t *pSipEvt = eXosip_event_wait(0, 50);while (pSipEvt != NULL) {printf("type: %d, textinfo: %s, tid: %d, tid:%d, sip_method:  %s\r\n", pSipEvt->type, pSipEvt->textinfo, pSipEvt->tid, pSipEvt->tid, pSipEvt->request->sip_method);eXosip_lock();eXosip_default_action(pSipEvt);eXosip_unlock();eXosip_event_free(pSipEvt);pSipEvt = eXosip_event_wait(0, 50);}usleep(50000);}
}

我们可以看到,

对应

确实是REGISTER请求。

但是IPC设备会发来不同的信令,因此我们写一个ProceXsipEvt函数来处理接收的数据

int ProceXsipEvt(eXosip_event_t* pSipEvt){if (NULL == pSipEvt){DBGPrint(M_SipUA, ERROR_LEVEL, "%s: pSipEvt is null pointer!", __FUNCTION__);return -1;}switch (pSipEvt->type){/* REGISTER related events */case EXOSIP_REGISTRATION_NEW:   // 宣布新的登记 ---> 注册事件处理DBGPrint(M_SipUA, BREAK_LEVEL, "(%d)注册事件处理\r\n", pSipEvt->type);break;case EXOSIP_REGISTRATION_SUCCESS :DBGPrint(M_SipUA, BREAK_LEVEL, "(%d)用户注册成功\r\n", pSipEvt->type);break;case EXOSIP_REGISTRATION_FAILURE  :DBGPrint(M_SipUA, BREAK_LEVEL, "(%d)用户注册不成功\r\n", pSipEvt->type);break;case EXOSIP_REGISTRATION_REFRESHED  :DBGPrint(M_SipUA, BREAK_LEVEL, "(%d)注册已刷新    \r\n", pSipEvt->type);break;case EXOSIP_REGISTRATION_TERMINATED  :printf("(%d)UA is not registred any more    \r\n", pSipEvt->type);break;/* INVITE related events within calls */case EXOSIP_CALL_INVITE  :printf("(%d)announce a new call    \r\n", pSipEvt->type);break;case EXOSIP_CALL_REINVITE  :printf("(%d)announce a new INVITE within call    \r\n", pSipEvt->type);break;case EXOSIP_CALL_NOANSWER  :printf("(%d)announce no answer within the timeout    \r\n", pSipEvt->type);break;case EXOSIP_CALL_PROCEEDING  :printf("(%d)announce processing by a remote app    \r\n", pSipEvt->type);break;case EXOSIP_CALL_RINGING  :printf("(%d)announce ringback    \r\n", pSipEvt->type);break;case EXOSIP_CALL_ANSWERED  :printf("(%d)announce start of call     \r\n", pSipEvt->type);break;case EXOSIP_CALL_REDIRECTED  :printf("(%d)announce a redirection      \r\n", pSipEvt->type);break;case EXOSIP_CALL_REQUESTFAILURE  :printf("(%d) announce a request failure    \r\n", pSipEvt->type);break;case EXOSIP_CALL_SERVERFAILURE  :printf("(%d) announce a server failure    \r\n", pSipEvt->type);break;case EXOSIP_CALL_GLOBALFAILURE  :printf("(%d)  announce a global failure    \r\n", pSipEvt->type);break;case EXOSIP_CALL_ACK  :printf("(%d) ACK received for 200ok to INVITE    \r\n", pSipEvt->type);break;case EXOSIP_CALL_CANCELLED  :printf("(%d) announce that call has been cancelled   \r\n", pSipEvt->type);break;case EXOSIP_CALL_TIMEOUT  :printf("(%d)  announce that call has failed    \r\n", pSipEvt->type);break;/* request related events within calls (except INVITE) */case EXOSIP_CALL_MESSAGE_NEW  :printf("(%d) announce new incoming request.   \r\n", pSipEvt->type);break;case EXOSIP_CALL_MESSAGE_PROCEEDING  :printf("(%d) announce a 1xx for request.   \r\n", pSipEvt->type);break;case EXOSIP_CALL_MESSAGE_ANSWERED  :printf("(%d) announce a 200ok   \r\n", pSipEvt->type);break;case EXOSIP_CALL_MESSAGE_REDIRECTED  :printf("(%d)  announce a failure.  \r\n", pSipEvt->type);break;case EXOSIP_CALL_MESSAGE_REQUESTFAILURE  :printf("(%d)  announce a failure:  \r\n", pSipEvt->type);break;case EXOSIP_CALL_MESSAGE_SERVERFAILURE  :printf("(%d)  announce a failure:  \r\n", pSipEvt->type);break;case EXOSIP_CALL_MESSAGE_GLOBALFAILURE  :printf("(%d)  announce a failure:  \r\n", pSipEvt->type);break;case EXOSIP_CALL_CLOSED  :printf("(%d)  a BYE was received for this call   \r\n", pSipEvt->type);break;/* for both UAS & UAC events */case EXOSIP_CALL_RELEASED  :printf("(%d) call context is cleared.     \r\n", pSipEvt->type);break;/* response received for request outside calls */case EXOSIP_MESSAGE_NEW  :printf("(%d) announce new incoming request.   \r\n", pSipEvt->type);break;case EXOSIP_MESSAGE_PROCEEDING  :printf("(%d) announce a 1xx for request.  \r\n", pSipEvt->type);break;case EXOSIP_MESSAGE_ANSWERED  :printf("(%d) announce a 200ok \r\n", pSipEvt->type);break;case EXOSIP_MESSAGE_REDIRECTED  :printf("(%d) announce a failure.  \r\n", pSipEvt->type);break;case EXOSIP_MESSAGE_REQUESTFAILURE  :printf("(%d) announce a failure.   \r\n", pSipEvt->type);break;case EXOSIP_MESSAGE_SERVERFAILURE  :printf("(%d) announce a failure.  \r\n", pSipEvt->type);break;case EXOSIP_MESSAGE_GLOBALFAILURE  :printf("(%d) announce a failure.   \r\n", pSipEvt->type);break;/* Presence and Instant Messaging */case EXOSIP_SUBSCRIPTION_UPDATE  :printf("(%d) announce incoming SUBSCRIBE.   \r\n", pSipEvt->type);break;case EXOSIP_SUBSCRIPTION_CLOSED  :printf("(%d) announce end of subscription.   \r\n", pSipEvt->type);break;case EXOSIP_SUBSCRIPTION_NOANSWER  :printf("(%d) announce no answer      \r\n", pSipEvt->type);break;case EXOSIP_SUBSCRIPTION_PROCEEDING  :printf("(%d) announce a 1xx      \r\n", pSipEvt->type);break;case EXOSIP_SUBSCRIPTION_ANSWERED  :printf("(%d)  announce a 200ok        \r\n", pSipEvt->type);break;case EXOSIP_SUBSCRIPTION_REDIRECTED  :printf("(%d)  announce a redirection        \r\n", pSipEvt->type);break;case EXOSIP_SUBSCRIPTION_REQUESTFAILURE  :printf("(%d) announce a request failure      \r\n", pSipEvt->type);break;case EXOSIP_SUBSCRIPTION_SERVERFAILURE  :printf("(%d)  announce a server failure        \r\n", pSipEvt->type);break;case EXOSIP_SUBSCRIPTION_GLOBALFAILURE  :printf("(%d) announce a global failure    \r\n", pSipEvt->type);break;case EXOSIP_SUBSCRIPTION_NOTIFY  :printf("(%d)   announce new NOTIFY request     \r\n", pSipEvt->type);break;case EXOSIP_SUBSCRIPTION_RELEASED  :printf("(%d)  call context is cleared.   \r\n", pSipEvt->type);break;case EXOSIP_IN_SUBSCRIPTION_NEW  :printf("(%d)   announce new incoming SUBSCRIBE \r\n", pSipEvt->type);break;case EXOSIP_IN_SUBSCRIPTION_RELEASED  :printf("(%d)  announce end of subscription.   \r\n", pSipEvt->type);break;case EXOSIP_NOTIFICATION_NOANSWER  :printf("(%d)  announce no answer  \r\n", pSipEvt->type);break;case EXOSIP_NOTIFICATION_PROCEEDING  :printf("(%d)   announce a 1xx      .   \r\n", pSipEvt->type);break;case EXOSIP_NOTIFICATION_ANSWERED  :printf("(%d)  announce a 200ok   .   \r\n", pSipEvt->type);break;case EXOSIP_NOTIFICATION_REDIRECTED  :printf("(%d)  announce a redirection       .   \r\n", pSipEvt->type);break;case EXOSIP_NOTIFICATION_REQUESTFAILURE  :printf("(%d)  announce a request failure   .   \r\n", pSipEvt->type);break;case EXOSIP_NOTIFICATION_SERVERFAILURE  :printf("(%d)  announce a server failure   .   \r\n", pSipEvt->type);break;case EXOSIP_NOTIFICATION_GLOBALFAILURE  :printf("(%d)  announce a global failure  .   \r\n", pSipEvt->type);break;case EXOSIP_EVENT_COUNT  :printf("(%d)  MAX number of events    .   \r\n", pSipEvt->type);break;default:printf( "(%d)  未处理消息 \r\n ", pSipEvt->type);break;}return 0;
}

下面我们要做的就是根据IPC客户端发来的不同类型的信令,然后做出回应。

回应第一个REGISTER请求,要求其鉴权

第一步:检查 Authorization 字段字段

根据上面的流程,我们应该检查 Authorization 字段,看是否有鉴权信息,怎么检测有没有这个字段呢?用osip_message_get_authorization接口,它可以检测header中有没有这个字段,如果没有,就返回-1:

/*** Get one Authorization header.* @param sip The element to work on.* @param pos The index of the element to get.* @param dest A pointer on the header found.*/int osip_message_get_authorization(const osip_message_t * sip, int pos,osip_authorization_t ** dest);

代码如下:

int ProceXsipEvt(eXosip_event_t* pSipEvt){if (NULL == pSipEvt){DBGPrint(M_SipUA, ERROR_LEVEL, "%s: pSipEvt is null pointer!", __FUNCTION__);return -1;}printf("type: %d, textinfo: %s, tid: %d, tid:%d, sip_method:  %s\r\n", pSipEvt->type, pSipEvt->textinfo, pSipEvt->tid, pSipEvt->tid, pSipEvt->request->sip_method);int iRet = -1;osip_authorization_t *pWWWAuEcho = NULL;iRet = osip_message_get_authorization(pSipEvt->request, 0, &pWWWAuEcho);if(pWWWAuEcho == NULL){printf("摄像机第一次发来未鉴权注册信息,返回401  %d\r\n" , iRet);  // 摄像机第一次发来未鉴权注册信息,返回401}else{printf(" 摄像机第二次发来鉴权注册信息,返回200 OK %d\r\n" , iRet);}return 0;
}

打印出了这个。

第二步:向IPC客户端,回复401 Unauthorized无权限,要求进行用户认证,并在消息包头添加如下字段:

 WWW-Authenticate: Digest realm="3402000000", nonce="a321cfdd39ff6233"

其中realm指的是域名(随意根据自己项目的实际情况填写),Nonce[点击查看]为以随机数;

代码实现如下:

char *pcRealm = "640001";
char *pcNonce = "6fe9ba44a76be22a";
static void Register401Unauthorized(eXosip_event_t* pSipEvt)
{int iReturnCode = 0;osip_message_t * pSRegister = NULL;osip_www_authenticate_t * header = NULL;osip_www_authenticate_init(&header);osip_www_authenticate_set_auth_type (header,osip_strdup("Digest"));osip_www_authenticate_set_realm(header,osip_enquote(pcRealm));osip_www_authenticate_set_nonce(header,osip_enquote(pcNonce));char *pDest = NULL;osip_www_authenticate_to_str(header,&pDest);iReturnCode = eXosip_message_build_answer(pSipEvt->tid,SIP_UNAUTHORIZED,&pSRegister);if ( iReturnCode == 0 && pSRegister != NULL ){osip_message_set_www_authenticate(pSRegister,pDest);eXosip_message_send_answer(pSipEvt->tid,SIP_UNAUTHORIZED,pSRegister);}osip_www_authenticate_free(header);osip_free(pDest);
}int ProceXsipEvt(eXosip_event_t* pSipEvt){if (NULL == pSipEvt){DBGPrint(M_SipUA, ERROR_LEVEL, "%s: pSipEvt is null pointer!", __FUNCTION__);return -1;}printf("type: %d, textinfo: %s, tid: %d, tid:%d, sip_method:  %s\r\n", pSipEvt->type, pSipEvt->textinfo, pSipEvt->tid, pSipEvt->tid, pSipEvt->request->sip_method);int iRet = -1;osip_authorization_t *pWWWAuEcho = NULL;iRet = osip_message_get_authorization(pSipEvt->request, 0, &pWWWAuEcho);if(pWWWAuEcho == NULL){printf("摄像机第一次发来未鉴权注册信息,返回401  %d\r\n" , iRet);  // 摄像机第一次发来未鉴权注册信息,返回401Register401Unauthorized(pSipEvt);}else{printf(" 摄像机第二次发来鉴权注册信息,返回200 OK %d\r\n" , iRet);}return 0;
}

效果:

抓包抓到的内容如下:

SIP/2.0 401 Unauthorized
Via: SIP/2.0/UDP 192.168.0.60:62066;branch=z9hG4bK15ab93de
From: <sip:34020000001110000001@4401020049>;tag=5a25
To: <sip:34020000001110000001@4401020049>;tag=501755099
Call-ID: 00003F6C0000474D@192.168.0.60
CSeq: 1 REGISTER
WWW-Authenticate: Digest realm="640001", nonce="6fe9ba44a76be22a"
User-Agent: eXosip/3.3.0
Content-Length: 0

可以看出:

  • 401 Unauthorized(无权限)响应
  • 并且通过WWW-Authenticate字段携带UAS支持的认证方式,产生本次认证的nonce

UAC会重新发送了一个请求:

IPC客户端会重新发送一个请求,抓包如下:

ZREGISTER sip:sip:44010200492000000001@192.168.0.12:5060 SIP/2.0
Via: SIP/2.0/UDP 192.168.0.60:61926;branch=z9hG4bK233cb42b
From: <sip:34020000001110000001@4401020049>;tag=31a1
To: <sip:34020000001110000001@4401020049>
Contact: <sip:34020000001110000001@192.168.0.60:61926>
Call-ID: 00000D4F00003828@192.168.0.60
CSeq: 2 REGISTER
Max-Forwards: 70
Expires: 3600
Allow: INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, NOTIFY, MESSAGE, SUBSCRIBE, INFO
User-Agent: HTSIP AGENT 2.0
Authorization: Digest username="34020000001110000001",realm="640001",uri="sip:192.168.0.12",nonce="6fe9ba44a76be22a",response="a0a200e89e34dc33aaa9dc2347541cf0",algorithm=MD5
Content-Length: 0

可以看到,UAC重新向UAS发起注册请求,携带WWW-Authorization字段:

Authorization: Digest username="34020000001110000001",realm="640001",uri="sip:192.168.0.12",nonce="6fe9ba44a76be22a",response="a0a200e89e34dc33aaa9dc2347541cf0",algorithm=MD5

另外,还需要注意的是由一个Expires,当其值为0时表示注销。

Expires: 3600

回应第二个REGISTER请求

  • 如果鉴权成功, 回复200
  • 如果鉴权密码错误,回复403
  • 如果pRegisterMsg->from->url->username也就是设备ID在数据库中找不到,回应404
    //鉴权, 鉴权成功回复200OKif (MD5AuthValidate(pSipEvt->request->sip_method, pWWWAuEcho, auth_username, auth_passwd)) {//鉴权成功,回复200成功//添加用户信息SendRegisterAnswer(pSipEvt->tid, SIP_OK, true);} else {//鉴权失败,回复403SendRegisterAnswer(pSipEvt->tid, SIP_FORBIDDEN, false);}

回应403

static void RegisterValidate(eXosip_event_t* pSipEvt) {osip_message_t *pRegisterMsg = NULL;pRegisterMsg = pSipEvt->request;printf("device = %s, passwd = %s, sip_method = %s, tid = %d\r\n, ",pRegisterMsg->from->url->username, pRegisterMsg->from->url->password,pRegisterMsg->sip_method, pSipEvt->tid);int tid = pSipEvt->tid, code = 403;osip_message_t * pMsgAnswer = NULL;eXosip_message_build_answer(pSipEvt->tid, code,&pMsgAnswer);eXosip_message_send_answer(tid, code, pMsgAnswer);return;
}

抓包如下:

SIP/2.0 403 Forbidden
Via: SIP/2.0/UDP 192.168.0.60:57108;branch=z9hG4bK2372eeec
From: <sip:34020000001110000001@4401020049>;tag=5ef7
To: <sip:34020000001110000001@4401020049>;tag=1746130768
Call-ID: 0000524000001A44@192.168.0.60
CSeq: 2 REGISTER
User-Agent: eXosip/3.3.0
Content-Length: 0

回复200

疑问:REGISTER认证加密计算?

计算Response过程:

  • 1)下载MD5加解密工具。
  • 2)将username,realm,password依次组合获取1个字符串,并对这个字符串使用算法H来进行加密,获得密文1.
  • 3)将method,uri依次组合获取1个字符串,并对这个字符串使用算法H来进行加密,获得密文2.
  • 4)将密文1,nonce和密文2依次组合获取1个字符串,并对这个字符串使用算法H来进行加密,获得密文3.

这个密文3就是最终的结果Response。

步骤1.

32028104561321000011:32028100002000000000:admin12345
MD5加密后密文 :
c9504793987d578e0640c26357fb1097

步骤2:
REGISTER:sip:32028100002000000000@3202810000
MD5加密后密文 :
3e9f8eefefb80928175d2f3671f9b5e9

步骤3:

密文1+nonce+密文2:
c9504793987d578e0640c26357fb1097:519c58b9519c58b9ac8c58b9a79c58b9d39c58b9009c58b9ac8c58b9198c58:3e9f8eefefb80928175d2f3671f9b5e9
MD5加密后密文:
c990b63bfa21138bd724a467ec27b134
response = c990b63bfa21138bd724a467ec27b134

MD5加密后密文 = response 所以注册校验成功。


bool MD5AuthValidate(char *pMethod, osip_authorization_t *pAuthEcho, char *auth_username, char *auth_passwd){return true;
}int SendMessageAnswer(int Tid, int Code, bool bSetTime, int Expires, TraverseAddr_T *pTvsAddr)
{osip_message_t * pMsgAnswer = NULL;int ret = eXosip_message_build_answer(Tid, Code,&pMsgAnswer);if(ret == OSIP_SUCCESS && pMsgAnswer != NULL ){#define CLIP_BUFFER_SIZE               256if (bSetTime == true){char TmpBuf[CLIP_BUFFER_SIZE+1] = {0};snprintf(TmpBuf, CLIP_BUFFER_SIZE, "%d", (int)Expires);osip_message_set_expires(pMsgAnswer, TmpBuf);osip_header_t*  pHeader = NULL;//char            TmpBuf[CLIP_BUFFER_SIZE+1];//initialize Date headerint iRet = osip_header_init(&pHeader);if (OSIP_SUCCESS == iRet){time_t now;struct timeval tv;//Get current timegettimeofday(&tv,NULL);now = tv.tv_sec;//          time(&now);//struct tm* pTmVal = localtime(&now);struct tm STm = {0};struct tm* pTmVal = localtime_r(&now, &STm);  //使用线程安全函数if (pTmVal != NULL){//系统启用夏令时,需要减去一个小时。if (1 == pTmVal->tm_isdst){now -= 3600;//pTmVal = localtime(&now);pTmVal = localtime_r(&now, &STm);  //使用线程安全函数if (NULL == pTmVal)return -1;}//2010-07-26 16:05:10snprintf(TmpBuf, CLIP_BUFFER_SIZE, "%4d-%02d-%02dT%02d:%02d:%02d.%03d", pTmVal->tm_year+1900, pTmVal->tm_mon+1, pTmVal->tm_mday, pTmVal->tm_hour, \pTmVal->tm_min, pTmVal->tm_sec,(int)(tv.tv_usec/1000));//set Date fieldosip_header_set_name(pHeader, osip_strdup("Date") );osip_header_set_value(pHeader, osip_strdup(TmpBuf) );osip_list_add(&pMsgAnswer->headers, pHeader, -1);}}
#define USERNAME_MAX_LEN               256
#define DOMAIN_MAX_LEN                 64
#define PASSWD_MAX_LEN                 64
#define SIP_AOR_MAX_SIZE               (USERNAME_MAX_LEN + DOMAIN_MAX_LEN)if (pTvsAddr != NULL){osip_header_t*  pHeader2 = NULL;iRet = osip_header_init(&pHeader2);if (OSIP_SUCCESS == iRet){char Contact[SIP_AOR_MAX_SIZE + 1] = {0};snprintf(Contact, SIP_AOR_MAX_SIZE, "sip:%s@%s:%d", pMsgAnswer->to->url->username, pTvsAddr->OutIpAddr, pTvsAddr->OutPort);osip_header_set_name(pHeader2, osip_strdup("Contact") );osip_header_set_value(pHeader2, osip_strdup(Contact) );osip_list_add(&pMsgAnswer->headers, pHeader2, -1);}}}eXosip_message_send_answer(Tid, Code, pMsgAnswer);}else{//DBGPrint(M_SipUA, ERROR_LEVEL, "%s:Build Message Answer Failed Tid<%d> Code<%d>, ret:%d!", __FUNCTION__, Tid, Code, ret);}return 0;
}int ProceXsipEvt(eXosip_event_t* pSipEvt){if (NULL == pSipEvt){DBGPrint(M_SipUA, ERROR_LEVEL, "%s: pSipEvt is null pointer!", __FUNCTION__);return -1;}printf("type: %d, textinfo: %s, tid: %d, tid:%d, sip_method:  %s\r\n", pSipEvt->type, pSipEvt->textinfo, pSipEvt->tid, pSipEvt->tid, pSipEvt->request->sip_method);int iRet = -1;osip_authorization_t *pWWWAuEcho = NULL;iRet = osip_message_get_authorization(pSipEvt->request, 0, &pWWWAuEcho);if(pWWWAuEcho == NULL){printf("摄像机第一次发来未鉴权注册信息,返回401  %d\r\n" , iRet);  // 摄像机第一次发来未鉴权注册信息,返回401Register401Unauthorized(pSipEvt);}else{printf(" 摄像机第二次发来鉴权注册信息,返回200 OK %d\r\n" , iRet);if (MD5AuthValidate(pSipEvt->request->sip_method, pWWWAuEcho,  "34020000001110000001", "12345678a")){osip_message_t* pRegisterMsg = NULL;pRegisterMsg = pSipEvt->request;osip_header_t*        p_header = NULL;int                expires = -1;osip_message_get_expires(pRegisterMsg, 0, &p_header);if (p_header != NULL){if (p_header->hvalue != NULL){expires = atoi(p_header->hvalue);}}else{printf("fatal");exit(0);}if (expires <= 0){printf("bbbbbbbbbbbbbbbbb");}else{SendMessageAnswer(pSipEvt->tid, SIP_OK, true, expires, &pRegisterMsg->TvsAddr);}}}return 0;
}

抓包如下:

SIP/2.0 200 OK
Via: SIP/2.0/UDP 192.168.0.60:54392;branch=z9hG4bK23bc2f3d
From: <sip:34020000001110000001@4401020049>;tag=1c35
To: <sip:34020000001110000001@4401020049>;tag=1789518916
Call-ID: 00000140000000E0@192.168.0.60
CSeq: 2 REGISTER
User-Agent: eXosip/3.3.0
Expires: 3600
Date: 2022-05-16T11:24:11.309
Contact: sip:34020000001110000001@192.168.0.60:54392
Content-Length: 0

小结

bool MD5AuthValidate(char *pMethod, osip_authorization_t *pAuthEcho, char *auth_username, char *auth_passwd){return true;
}
char *pcRealm = "640001";
char *pcNonce = "6fe9ba44a76be22a";
static void Register401Unauthorized(eXosip_event_t* pSipEvt)
{int iReturnCode = 0;osip_message_t * pSRegister = NULL;osip_www_authenticate_t * header = NULL;osip_www_authenticate_init(&header);osip_www_authenticate_set_auth_type (header,osip_strdup("Digest"));osip_www_authenticate_set_realm(header,osip_enquote(pcRealm));osip_www_authenticate_set_nonce(header,osip_enquote(pcNonce));char *pDest = NULL;osip_www_authenticate_to_str(header,&pDest);iReturnCode = eXosip_message_build_answer(pSipEvt->tid,SIP_UNAUTHORIZED,&pSRegister);if ( iReturnCode == 0 && pSRegister != NULL ){osip_message_set_www_authenticate(pSRegister,pDest);eXosip_message_send_answer(pSipEvt->tid,SIP_UNAUTHORIZED,pSRegister);}osip_www_authenticate_free(header);osip_free(pDest);
}
int SendMessageAnswer(int Tid, int Code, bool bSetTime = false, int Expires = 0, TraverseAddr_T *pTvsAddr = NULL)
{#define CLIP_BUFFER_SIZE               256
#define USERNAME_MAX_LEN               256
#define DOMAIN_MAX_LEN                 64
#define PASSWD_MAX_LEN                 64
#define SIP_AOR_MAX_SIZE               (USERNAME_MAX_LEN + DOMAIN_MAX_LEN)osip_message_t * pMsgAnswer = NULL;int ret = eXosip_message_build_answer(Tid, Code,&pMsgAnswer);if(ret == OSIP_SUCCESS && pMsgAnswer != NULL ){if (bSetTime == true){char TmpBuf[CLIP_BUFFER_SIZE+1] = {0};snprintf(TmpBuf, CLIP_BUFFER_SIZE, "%d", (int)Expires);osip_message_set_expires(pMsgAnswer, TmpBuf);osip_header_t*  pHeader = NULL;int iRet = osip_header_init(&pHeader);if (OSIP_SUCCESS == iRet){time_t now;struct timeval tv;gettimeofday(&tv,NULL);now = tv.tv_sec;struct tm STm = {0};struct tm* pTmVal = localtime_r(&now, &STm);  //使用线程安全函数if (pTmVal != NULL){//系统启用夏令时,需要减去一个小时。if (1 == pTmVal->tm_isdst){now -= 3600;pTmVal = localtime_r(&now, &STm);  //使用线程安全函数if (NULL == pTmVal)return -1;}//2010-07-26 16:05:10snprintf(TmpBuf, CLIP_BUFFER_SIZE, "%4d-%02d-%02dT%02d:%02d:%02d.%03d", pTmVal->tm_year+1900, pTmVal->tm_mon+1, pTmVal->tm_mday, pTmVal->tm_hour, \pTmVal->tm_min, pTmVal->tm_sec,(int)(tv.tv_usec/1000));//set Date fieldosip_header_set_name(pHeader, osip_strdup("Date") );osip_header_set_value(pHeader, osip_strdup(TmpBuf) );osip_list_add(&pMsgAnswer->headers, pHeader, -1);}}if (pTvsAddr != NULL){osip_header_t*  pHeader2 = NULL;iRet = osip_header_init(&pHeader2);if (OSIP_SUCCESS == iRet){char Contact[SIP_AOR_MAX_SIZE + 1] = {0};snprintf(Contact, SIP_AOR_MAX_SIZE, "sip:%s@%s:%d", pMsgAnswer->to->url->username, pTvsAddr->OutIpAddr, pTvsAddr->OutPort);osip_header_set_name(pHeader2, osip_strdup("Contact") );osip_header_set_value(pHeader2, osip_strdup(Contact) );osip_list_add(&pMsgAnswer->headers, pHeader2, -1);}}}eXosip_message_send_answer(Tid, Code, pMsgAnswer);}else{DBGPrint(M_SipUA, ERROR_LEVEL, "%s:Build Message Answer Failed Tid<%d> Code<%d>, ret:%d!", __FUNCTION__, Tid, Code, ret);}return 0;
}int ProceXsipEvt(eXosip_event_t* pSipEvt){if (NULL == pSipEvt){DBGPrint(M_SipUA, ERROR_LEVEL, "%s: pSipEvt is null pointer!", __FUNCTION__);return -1;}printf("type: %d, textinfo: %s, tid: %d, tid:%d, sip_method:  %s\r\n", pSipEvt->type, pSipEvt->textinfo, pSipEvt->tid, pSipEvt->tid, pSipEvt->request->sip_method);int iRet = -1;osip_authorization_t *pWWWAuEcho = NULL;iRet = osip_message_get_authorization(pSipEvt->request, 0, &pWWWAuEcho);if(pWWWAuEcho == NULL){printf("摄像机第一次发来未鉴权注册信息,返回401  %d\r\n" , iRet);  // 摄像机第一次发来未鉴权注册信息,返回401Register401Unauthorized(pSipEvt);}else{printf(" 摄像机第二次发来鉴权注册信息,返回200 OK %d\r\n" , iRet);osip_message_t* pRegisterMsg = NULL;pRegisterMsg = pSipEvt->request;SendMessageAnswer(pSipEvt->tid, SIP_OK, true, 3600, &pRegisterMsg->TvsAddr);}return 0;
}

实现注销功能

理论

  1.  注销的Expires必须为0,表示注销。
    
  2.  SIP服务器回401要求携带Authorization信息。
    
  3.  IPC向SIP服务器发送认证信息,认证信息和注册时候的认证信息相同。
    
  4.   注销成功SIP服务器会返回200OK。
    

整个过程如下(总结)

参考

  • DZ先生怪谈国标28181之国标注册

  • GB28181注册流程讲解

  • sip注册流程

  • GB28181 平台注册 ( 二 )

  • 国标GBT28181协议,注册功能服务端与客户端实现代码

国标28181:接收设备注册相关推荐

  1. 国标28181:IPC信号检索设备目录查询

    待IPC客户端注册了服务端之后,服务端就应该查询设备 设备目录查询 设备目录查询是国标平台对国标设备接入的目录查询,目的是查询该设备带有的监控点和报警设备信息以及语音设备信息. 使用场景: 比如平台国 ...

  2. 视频监控安防平台-国标28181平台(支持国标28181转RTSP/RTMP/HLS/WEBRTC直播)

    视频监控安防平台-国标28181平台(支持国标28181转RTSP/RTMP/HLS/WEBRTC直播) 发现很久都未更新博客了,最近把小平台的功能做了完善,在原来的功能基础上添加了功能,支持国标28 ...

  3. java使用国标方式取流,一种基于JAIN-SIP的国标28181平台分布式集群实现系统的制作方法...

    本发明涉及国标设备接入相关技术领域,尤其是指一种基于jain-sip的国标28181平台分布式集群实现系统. 背景技术: 在传统安防行业,采用较多的是用c++编写的产品,该类产品存在一些缺陷:该类产品 ...

  4. 视频监控安防平台(企业级)-国标28181平台

    很久没有更新博客了,最近在完善平台的改造,下面我把平台信息发布出来,可以对接国标28181平台.国标28181摄像机.NVR,平台接入量目前项目上使用最多是接入50W路摄像机,欢迎大家使用! 客户端登 ...

  5. 教程篇(5.4) 03. FortiManager 设备注册 ❀ Fortinet 网络安全专家 NSE5

    在本课中,我们将学习设备管理器窗格的主要功能,以及如何从FortiManager管理FortiGate. 本课程中,主要包含以下三个部分: 部署设备的通用设置 设备注册的方式 设备发现的故障排除 开始 ...

  6. wvp+zlmediakit实现国标28181对讲

    wvp+zlmediakit实现国标28181对讲 一.前言 ZLMediaKit WVP-GB28181 语音对讲源码地址 首先感谢wvp作者和zlmediakit作者提供这么棒的开源项目,我这个例 ...

  7. java对接移动物联网onenet平台实现接收设备上报数据命令下发

    java对接移动物联网onenet平台实现接收设备上报数据命令下发. 最近由于工作需要,公司设备集成了物联网卡,需要实现数据上报命令下发等(目前集成了电信,移动,联通三个平台).电信和移动都有自己的开 ...

  8. LiveGBS流媒体平台GB/T28181常见问题-海康大华宇视监控摄像机NVR硬件设备注册不上来如何排查?

    LiveGBS中海康大华宇视监控摄像机NVR硬件设备注册不上来如何排查 1.网页打开后, 看不到设备注册上来 2.检查方式 2.1.检查设备注册信息 2.2.检查服务器防火墙 3.尝试配置免密接入 3 ...

  9. 六、linux虚拟平台设备注册

    一.使用到的设备结构体 注册设备使用结构体platform_device,该结构体在头文件"viminclude/linux/platform_device.h"中.头文件中也有注 ...

最新文章

  1. Linux下查找Nginx配置文件位置
  2. 上线前一个小时,dubbo这个问题可把我折腾惨了
  3. 用python实现矩阵乘法
  4. pycharm 中HTML代码的对齐
  5. macos php无法访问,Mac上,Apache启动正常,却无法访问localhost和127.0.0.1
  6. STL 容器简介:C++ 容器:顺序性容器、关联式容器和容器适配器
  7. C语言解析动态html,【c语言】使用gumbo解析HTML
  8. 男性加入防晒大军 购买遮阳伞比例同比增长23.54%
  9. uniapp文件路径转base64格式
  10. 《基于运算放大器和模拟集成电路的电路设计》PDF云盘资源分享
  11. EyouCms前台GetShell漏洞复现
  12. 手机和电脑如何快速传大文件
  13. 计算机ppt里怎么应用背景图,如何快速生成一个PPT图片背景“遮罩”?-ppt背景图片怎么设置...
  14. python华氏温度和摄氏温度相互转换
  15. 计算机一级证书英文 简历,通用于计算机英文简历范文
  16. WebDAV之葫芦儿•派盘+RS文件管理器
  17. PDF怎么编辑修改内容?教你一招轻松搞定
  18. android 微信朋友圈头像,微信进阶玩法,这样设置朋友圈和头像,个性又好看
  19. Linux中,如何解决进程被Kill
  20. 解决物理机为ubuntu与virtualbox客户机为windows10间剪切板不能用的问题

热门文章

  1. erp故障处理流程图_完整ERP流程图
  2. js获取label标签中的value值
  3. 学习mongo系列(四) find().pretty() remove() 查询
  4. ElasticJob笔记
  5. python pandas包,Python的常用包pandas,numpy
  6. 替代Xshell的良心国产SSH工具软件
  7. 贴几张Google Earth的图
  8. 借助 GPU 和容器支持,在 Amazon Robomaker 中运行任何高保真模拟
  9. 乱杀HTML知识点(小白版本)
  10. ViewPager自动轮播,手指按住停止轮播