工具
自从国标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.exe
和GB28181-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,设备在线。
- 下级平台(注册设备)向上级平台(服务器)注册, 下级平台需要知道上级平台 sipid、ip、端口号,这样就可以向上级平台注册了。
- 上级平台收到下级平台注册消息后, 需要构造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用户名,即设备的ID是44010200492222222221,用来标识当前设备的省份
- 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); // 摄像机第一次发来未鉴权注册信息,返回401
Register401Unauthorized(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
//鉴权, 鉴权成功回复200OK
if (MD5AuthValidate(pSipEvt->request->sip_method, pWWWAuEcho, auth_username, auth_passwd)) {
//鉴权成功,回复200成功
//添加用户信息
SendRegisterAnswer(pSipEvt->tid, SIP_OK, true);
} else {
//鉴权失败,回复403
SendRegisterAnswer(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 256
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;
//char TmpBuf[CLIP_BUFFER_SIZE+1];
//initialize Date header
int iRet = osip_header_init(&pHeader);
if (OSIP_SUCCESS == iRet)
{
time_t now;
struct timeval tv;
//Get current time
gettimeofday(&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:10
snprintf(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 field
osip_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); // 摄像机第一次发来未鉴权注册信息,返回401
Register401Unauthorized(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:10
snprintf(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 field
osip_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); // 摄像机第一次发来未鉴权注册信息,返回401
Register401Unauthorized(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;
}
实现注销功能
理论
-
注销的Expires必须为0,表示注销。
-
SIP服务器回401要求携带Authorization信息。
-
IPC向SIP服务器发送认证信息,认证信息和注册时候的认证信息相同。
-
注销成功SIP服务器会返回200OK。
整个过程如下(总结)