1、GB/T 28181—2016协议基础介绍:
近年来,国内视频监控应用发展迅猛,系统接入规模不断扩大,涌现了大量平台提供商,平台提供商的接入协议各不相同,终端制造商需要给每款终端维护提供各种不同平台的软件版本,造成了极大的资源浪费。各地视频大规模建设后,省级、国家级集中调阅,对重特大事件通过视频掌握现场并进行指挥调度的需求逐步涌现,然而不同平台间缺乏统一的互通协议。在这样的产业背景下,基于终端标准化、平台互联互通的需求,GB/T28181应运而生。 GB28181标准规定了公共安全视频监控联网系统(以下简称联网系统) 的互联结构, 传输、 交换、 控制的基本要求和安全性要求, 以及控制、 传输流程和协议接口等技术要求。
GB28181项目的基本设计思路流程如下图:
接下来我们只了解一下注册流程, 根据GB/T 28181 —2016》第9章关于注册的描述,GB28181的注册需要满足一下要求:
- 注册时应进行认证,认证方式应支持数字摘要认证方式。
- SIP代理在注册时间超时之前,应向注册服务器进行刷新注册,系统、设备注册超时时间可配置 ,缺省值为86400s,应在注册超时时间到来之前刷新注册消息,为SIP服务器预留适当刷新注册处理时间,注册过期时间不应短于3600s。 SIP代理注册成功则认为
- SIP服务器为在线状态,注册失败则认为SIP服务器为离线状态。SIP服务器在SIP代理注册成功后置为在线状态,SIP代理注册超时则认为其为离线状态。
2、注册流程及描
- SIP代理主动向SIP服务器发送注册请求。
- SIP服务器收到请求后向SIP代理发送401响应,并在响应的头消息www_Authenticate字段中给出适合SIP代理的认证体制和参数。
- SIP代理收到SIP服务器发送的401响应后,重新向SIP服务器发送注册请求,请求的Authorization字段中给出信任书,包括认证信息。【这个时候如果SIP服务器有鉴权认证,SIP代理会携带鉴权信息】
- SIP服务器对请求进行验证,若检验出SIP代理身份合法,则向SIP代理发送成功响应200 OK,若身份不合法会发出拒绝应答。
3、基于数字认证书的双向认证注册说明
SIP 代理和SIP 服务器进行双向认证时,IETF RFC 3261 中定义的方法 Register 进行如下头域扩展:
- Authorization的值增加Capability项来描述编码器的安全能力,当Authorization的值为Capability时,只携带一个参数algorithrn,参数algorithm 的值分为三部分, 中间以逗号分割,第一部分为非对称算法描述, 取值为 RSA; 第二部分为摘要算法描述, 取值为 MD5/SHA-1/SHA-256 中的一个或者多个; 第三部分为对称算法的描述, 取值为 DES/3DES/SM1 中的一个或者多个。
- www-Authenticate 的 值 增 加 Asymmetric 项 用 来 携 带 验 证 SIP 服 务 器 身 份 的 数 据。 当www-Authenticate 的值为 Asymmetric 时, 只携带参数 nonce 和algorithm, 在algorithm 的值取安全能力中指明的算法。
- Authorization 的值增加 Asymmetric 项用来携带验证编码器的数据。 当 Authorization 的值为 Asymmetric 时, 携带nonce、response、algorithm 三个参数。
4、基于SIP库的注册
在这里我们都是基于libosip和libexsip库进行开发的,至于库文件的如何加入工程请根据不同开发语音选择合适的调用方法,下面是设备注册的步骤:
- 初始化
初始化SIP,主要包括初始化eXosip_init和eXosip_listen_addr开启端口监听,具体代码如下:
bool CSIPServer::Start()
{
initialize();
for (int i = 0; i < 1; i++) {
x_eCtx[i].reset(eXosip_malloc(), [](struct eXosip_t* ptr) {
if (ptr) {
eXosip_quit(ptr);
osip_free(ptr);
}
});
if (!CheckParam()
|| x_eCtx[i] == nullptr
|| eXosip_init(x_eCtx[i].get()) != OSIP_SUCCESS)
{
Stop();
return false;
}
int _ret = eXosip_listen_addr(x_eCtx[i].get(), i == 0 ? IPPROTO_UDP : IPPROTO_TCP, NULL, m_SIPServerPort, AF_INET, 0);
if (_ret != OSIP_SUCCESS)
{
Stop();
return false;
}
eXosip_set_user_agent(x_eCtx[i].get(), "SIPServer/0.0.1");
int opt_keepalive = 0;
eXosip_set_option(x_eCtx[i].get(), EXOSIP_OPT_UDP_KEEP_ALIVE, &opt_keepalive);
int opt_use_rport = 1;
eXosip_set_option(x_eCtx[i].get(), EXOSIP_OPT_USE_RPORT, &opt_use_rport);
}
x_isRunning = true;
if (IsSuspended())
Resume();
return true;
}
- 注册
void CSIPServer::HANDLE_REGISTER(eXosip_t* eCtx, eXosip_event_t* event)
{
osip_header_t* header = NULL;
osip_message_header_get_byname(event->request, "expires", 0, &header);
if (header == nullptr || header->hvalue == NULL)
{
Reply(eCtx, event, 400);
return;
}
int _expires = atoi(header->hvalue);
if (_expires)
{
//提取出各个字段值,进行 MD5 计算
osip_authorization_t* authorization = NULL;
osip_message_get_authorization(event->request, 0, &authorization);
if (authorization == NULL)
{
bool _success = false;
do
{
osip_www_authenticate_t* header = NULL;
osip_www_authenticate_init(&header);
if (header == NULL) break;
std::shared_ptr<osip_www_authenticate_t> auth_object(header, [](osip_www_authenticate_t* ptr){ if (ptr) { osip_free(ptr); } });
osip_www_authenticate_set_auth_type(auth_object.get(), osip_strdup("Digest"));
osip_www_authenticate_set_realm(auth_object.get(), osip_enquote(m_SIPDomain.c_str()));
osip_www_authenticate_set_nonce(auth_object.get(), osip_enquote(GB_NONCE));
char* body = NULL;
osip_www_authenticate_to_str(auth_object.get(), &body);
if (body == NULL) break;
std::shared_ptr<char> auth_str(body, osip_free_defult);
osip_message_t* answer = NULL;
{
CSIPAutoLock _autoLock(eCtx);
eXosip_message_build_answer(eCtx, event->tid, 401, &answer);
}
if (answer != nullptr)
{
osip_message_set_content_type(answer, "Application/MANSCDP+xml");
osip_message_set_www_authenticate(answer, auth_str.get());/*设置认证消息*/
Reply(eCtx, event, 401, answer);
_success = true;
}
} while (0);
if (!_success) {
Reply(eCtx, event, 500);
}
return;
}
std::shared_ptr<char> algorithm(authorization->algorithm ? osip_strdup_without_quote(authorization->algorithm) : nullptr, osip_free_defult);
std::shared_ptr<char> username(authorization->username ? osip_strdup_without_quote(authorization->username) : nullptr, osip_free_defult);
std::shared_ptr<char> realm(authorization->realm ? osip_strdup_without_quote(authorization->realm) : nullptr, osip_free_defult);
std::shared_ptr<char> nonce(authorization->nonce ? osip_strdup_without_quote(authorization->nonce) : nullptr, osip_free_defult);
std::shared_ptr<char> nonce_count(authorization->nonce_count ? osip_strdup_without_quote(authorization->nonce_count) : nullptr, osip_free_defult);
std::shared_ptr<char> uri(authorization->uri ? osip_strdup_without_quote(authorization->uri) : nullptr, osip_free_defult);
std::shared_ptr<char> response(authorization->response ? osip_strdup_without_quote(authorization->response) : nullptr, osip_free_defult);
if (username == nullptr
|| realm == nullptr
|| nonce == nullptr
|| response == nullptr)
{
Reply(eCtx, event, 400);
return;
}
///1.计算出response
///2.与客户端请求过来的response做比较,如果一样,则通过注册,回复200.否则回复403失败
HASHHEX HA1 = { 0 };
DigestCalcHA1(algorithm.get(), username.get(), realm.get(), m_SIPServerPwd.c_str(), nonce.get(), nonce_count.get(), HA1);
HASHHEX Response = { 0 };
HASHHEX HA2 = { 0 };
//在下面这个函数里面,已经计算了 H(A2),所以不需要自己计算 H(A2)
DigestCalcResponse(HA1, nonce.get(), nonce_count.get(), authorization->cnonce, authorization->message_qop, 0, event->request->sip_method, uri.get(), HA2, Response);
if (memcmp(response.get(), Response, HASHHEXLEN) != 0)
{
Reply(eCtx, event, 403);
return;
}
osip_via_t* via = NULL;
osip_message_get_via(event->request, 0, &via);
char* szSIPClientIP = osip_via_get_host(via);
char* szSIPClientPort = osip_via_get_port(via);
osip_generic_param_t* br = nullptr;
osip_via_param_get_byname(via, "received", &br);
if (br != NULL && br->gvalue != NULL)
szSIPClientIP = br->gvalue;
osip_via_param_get_byname(via, "rport", &br);
if (br != NULL && br->gvalue != NULL)
szSIPClientPort = br->gvalue;
string _req_host(event->request->req_uri->host);
if (!StringUtil::IsValidIPAddr(_req_host)) {
if (!GetAddress(szSIPClientIP, _req_host)) {
Reply(eCtx, event, 400);
return;
}
}
if (!x_pEventCallBack(NET_GB28181_EVENT_E::EVENT_AUTOREGISTER, (eCtx == x_eCtx[0].get()) ? 0 : 1, event->request->from->url->username, szSIPClientIP, atoi(szSIPClientPort), _req_host.c_str(), NULL, 0, x_dwUser))
{
Reply(eCtx, event, 404);
return;
}
osip_message_t* answer = NULL;
{
CSIPAutoLock _autoLock(eCtx);
eXosip_message_build_answer(eCtx, event->tid, 200, &answer);
}
if (answer != nullptr)
{
string _szDateTime = DateUtil::Now().GetDateTimeAsString();
StringUtil::ReplaceStr(_szDateTime, " ", "T");
osip_message_set_date(answer, _szDateTime.c_str());
Reply(eCtx, event, 200, answer);
}
}
else
{
osip_via_t* via = NULL;
osip_message_get_via(event->request, 0, &via);
char* szSIPClientIP = osip_via_get_host(via);
char* szSIPClientPort = osip_via_get_port(via);
osip_generic_param_t* br = nullptr;
osip_via_param_get_byname(via, "received", &br);
if (br != NULL && br->gvalue != NULL)
szSIPClientIP = br->gvalue;
osip_via_param_get_byname(via, "rport", &br);
if (br != NULL && br->gvalue != NULL)
szSIPClientPort = br->gvalue;
Reply(eCtx, event, 200);
x_pEventCallBack(NET_GB28181_EVENT_E::EVENT_UNREGISTER, (eCtx == x_eCtx[0].get()) ? 0 : 1, event->request->from->url->username, szSIPClientIP, atoi(szSIPClientPort), event->request->req_uri->host, NULL, 0, x_dwUser);
}
}
以上接口可以实现GB28181的初始化操作。
如有需要可登录http://www.meichang.cc进行验证测试。