本篇博客的测试环境: Windows 10 + Qt 5.12.2 MSVC。
由于项目中使用了RTSP协议,为了防止别人知道我们的流地址随便就能播放观看我们的视频,所以就使用鉴权筛掉一些不合适的请求。
在鉴权之前呢,需要准备一下:
我们向ZLM流媒体服务器推流和拉流,要使用鉴权就必须得开启ZLM服务的HOOK,配置文件中enable
要置为1。
[hook]
enable=1
admin_params=secret=035c73f7-bb6b-4889-a715-d9eb2d1925cc
timeoutSec=10
# 下面的http地址改成你HTTP HOOK的服务地址就好
# 还有一些但是我没有列出来,你们记得将地址全改成你的HOOK Server地址
on_flow_report=https://127.0.0.1/index/hook/on_flow_report
on_http_access=https://127.0.0.1/index/hook/on_http_access
....
接下来我们将会在推流和拉流介绍它三个HTTP HOOK API,分别是:
下面将搭建一个简易的测试HOOK Server,用Qt一个HTTP Server开源库开发。
开源库:JQHttpServer
拉下来后,直接在demos/HttpServerDemo项目中添加下面的类,再修改一下main.cpp
#ifndef JSONPARSER_H
#define JSONPARSER_H
#include
#include
#include
#include
#include
#include
#include
typedef void(*ReplySession)(const QPointer< JQHttpServer::Session > &session);
class JsonParser : public QObject
{
Q_OBJECT
public:
explicit JsonParser(QObject *parent = nullptr);
public:
void parser(const QPointer< JQHttpServer::Session > &session);
private:
static void replyOn_flow_report(const QPointer< JQHttpServer::Session > &session);
static void replyOn_http_access(const QPointer< JQHttpServer::Session > &session);
static void replyOn_play(const QPointer< JQHttpServer::Session > &session);
static void replyOn_publish(const QPointer< JQHttpServer::Session > &session);
static void replyOn_rtsp_realm(const QPointer< JQHttpServer::Session > &session);
static void replyOn_rtsp_auth(const QPointer< JQHttpServer::Session > &session);
private:
QStringList m_urls;
QList< ReplySession > m_funs;
};
#endif // JSONPARSER_H
#include "jsonparser.h"
#include
JsonParser::JsonParser(QObject *parent) : QObject(parent)
{
m_urls.append(QString("/index/hook/on_flow_report ").trimmed());
m_urls.append(QString("/index/hook/on_http_access ").trimmed());
m_urls.append(QString("/index/hook/on_play ").trimmed());
m_urls.append(QString("/index/hook/on_publish ").trimmed());
m_urls.append(QString("/index/hook/on_rtsp_auth ").trimmed());
m_urls.append(QString("/index/hook/on_rtsp_realm ").trimmed());
m_funs.append(replyOn_flow_report);
m_funs.append(replyOn_http_access);
m_funs.append(replyOn_play);
m_funs.append(replyOn_publish);
m_funs.append(replyOn_rtsp_auth);
m_funs.append(replyOn_rtsp_realm);
}
void JsonParser::parser(const QPointer< JQHttpServer::Session > &session)
{
QString url = session->requestUrl();
QByteArray data = session->requestBody();
int index = m_urls.indexOf(url.trimmed());
if(index >= 0) {
qDebug() << "-u-:" << url;
m_funs[index](session);
}
else {
qDebug() << "n:" << url;
QJsonObject jsonObj;
jsonObj.insert("code", 0);
jsonObj.insert("message", "ok");
session->replyJsonObject(jsonObj);
}
}
void JsonParser::replyOn_flow_report(const QPointer<JQHttpServer::Session> &session)
{
QJsonObject jsonObj;
jsonObj.insert("code", 0);
jsonObj.insert("message", "ok");
session->replyJsonObject(jsonObj);
}
void JsonParser::replyOn_http_access(const QPointer<JQHttpServer::Session> &session)
{
QJsonObject jsonObj;
jsonObj.insert("code", 0);
jsonObj.insert("err", "");
jsonObj.insert("path", "");
jsonObj.insert("second", 600);
session->replyJsonObject(jsonObj);
}
void JsonParser::replyOn_play(const QPointer<JQHttpServer::Session> &session)
{
QJsonObject jsonObj;
jsonObj.insert("code", 0);
jsonObj.insert("message", "ok");
session->replyJsonObject(jsonObj);
}
void JsonParser::replyOn_publish(const QPointer<JQHttpServer::Session> &session)
{
qDebug() << session->requestBody();
//在这里将session->requestBody()序列化成JSON对象,去取里面的params校验
//token,是否一致,一致code为0,不一致code为其它值。我这里没做检验了
QJsonObject jsonObj;
jsonObj.insert("code", 0);
jsonObj.insert("message", "ok");
jsonObj.insert("enable_hls", true);
jsonObj.insert("enable_mp4", false);
jsonObj.insert("enable_rtsp", true);
jsonObj.insert("enable_rtmp", true);
jsonObj.insert("enable_ts", false);
jsonObj.insert("enable_audio", true);
jsonObj.insert("add_mute_audio", true);
jsonObj.insert("mp4_as_player", false);
jsonObj.insert("modify_stamp", false);
session->replyJsonObject(jsonObj);
}
void JsonParser::replyOn_rtsp_realm(const QPointer<JQHttpServer::Session> &session)
{
qDebug() << session->requestBody();
QJsonObject jsonObj;
jsonObj.insert("code", 0);
jsonObj.insert("realm", "zlmediakit_reaml_t");
session->replyJsonObject(jsonObj);
}
void JsonParser::replyOn_rtsp_auth(const QPointer<JQHttpServer::Session> &session)
{
qDebug() << endl;
qDebug() << session->requestBody();
QJsonObject jsonObj;
jsonObj.insert("code", 0);
jsonObj.insert("encrypted", true);
//这里passwd使用了md5加密,原密码是123456
jsonObj.insert("passwd", "e10adc3949ba59abbe56e057f20f883e");
session->replyJsonObject(jsonObj);
}
添加一个刚刚创建类的头文件,修改main.cpp中的onHttpAccepted函数
#include "jsonparser.h"
JsonParser g_parser;
void onHttpAccepted(const QPointer< JQHttpServer::Session > &session)
{
g_parser.parser(session);
}
我们先来看一张图,来自ZLM的wiki
看了看这张图,我有一下疑问:
URL传递参数例子:
rtsp://192.168.10.16:554/test/test?token=xxxxxxxxxxxxxxxxx
我们再来看一下它HTTP HOOK的API,on_publish
on_publish:rtsp/rtmp/rtp推流鉴权事件,当我们向ZLM推流时,ZLM会向配置文件中指定的HOOK地址进行POST,以下就是它POST的内容,已经去掉了其它无用信息。
{
"mediaServerId" : "your_server_id", //服务器id,通过配置文件设置
"app" : "live", //流应用名
"id" : "140186529001776", //TCP链接唯一ID
"ip" : "10.0.17.132", //推流器ip
"params" : "token=1677193e-1244-49f2-8868-13b3fcc31b17", //推流url参数
"port" : 65284, //推流器端口号
"schema" : "rtmp", //推流的协议,可能是rtsp、rtmp
"stream" : "obs", //流ID
"vhost" : "__defaultVhost__" //流虚拟主机
}
这里面我们只要关心params
这个参数,这个就是我们传递的参数,通过这个参数的token去跟业务服务器给的token进行校验,通过接流,不通过就不接流。
我们应该这样应答这个API
{
"code" : 0, //为0说明通过校验,接收推流
"add_mute_audio" : true,
"continue_push_ms" : 10000,
"enable_audio" : true,
"enable_fmp4" : true,
"enable_hls" : true,
"enable_mp4" : false,
"enable_rtmp" : true,
"enable_rtsp" : true,
"enable_ts" : true,
"hls_save_path" : "/hls_save_path/",
"modify_stamp" : false,
"mp4_as_player" : false,
"mp4_max_second" : 3600,
"mp4_save_path" : "/mp4_save_path/"
}
也就是说这个token就是这个业务服务器给的推流器,怪不知的可以校验。
计算鉴权的公式有两种:
(1)当password为MD5编码,则
response = md5(password:nonce:md5(public_method:url));
(2)当password为ANSI字符串,则
response= md5(md5(username:realm:password):nonce:md5(public_method:url));
我们这里使用的是第一种计算方式,图中计算鉴权步骤我都省略不画了,主要都是与ZLM的交互的步骤。
我们先来看一下RUL:
rtsp://admin:md5(password)@192.168.10.150:554/test/test
其中密码经过md5加密,推流器会将账号和密码取下来后面用于计算鉴权结果,实际上URL是rtsp://192.168.10.150:554/test/test。
接下来一步步去看怎么鉴权的:
on_rtsp_realm
服务端接收到DESCRIBE请求之后,触发HOOK API的on_rtsp_realm
,我们直接应答这个API
//应答内容
{
"code" : 0, //固定返回0
"realm" : "zlmediakit_reaml" //realm由业务服务器指定,给客户端计算鉴权结果,
//因为我们这里使用第一种计算方式,所以用不上这个值
}
客户端接收到 realm
和 nonce
,开始计算鉴权结果,使用第一种计算方式。
on_rtsp_auth
服务端接收到第二次DESCRIBE请求,并携带username、realm、onnce、responce(鉴权结果),便触发HOOK API on_rtsp_auth
,我们这样应答
//应答内容
{
"code" : 0, //0允许播放,其它为错误代码
"encrypted" : true, //传入的passwd是否加密
"passwd" : "e10adc3949ba59abbe56e057f20f883e" //密码,我这里传入的是使用md5加密后的密码
}
当第九步执行完,将加密后的密码给到ZLM去计算鉴权结果,是否与客户端的鉴权结果一致,不一致说明账号和密码不对。
//推流器和ZLM完整的请求流程,不涉及业务服务器
[RTSP] connected to server 192.168.10.150:554
[RTSP] Sending Request:
OPTIONS rtsp://192.168.10.150:554/test/test RTSP/1.0
CSeq: 1
User-Agent: DXMediaPlayer
[RTSP] Received OPTIONS response:
RTSP/1.0 200 OK
CSeq: 1
Date: Thu, Feb 23 2023 06:59:11 GMT
Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, ANNOUNCE, RECORD, SET_PARAMETER, GET_PARAMETER
Server: ZLMediaKit(git hash:14da5ab2,branch:master,build time:Feb 6 2023 08:30:31)
[RTSP] Sending Request:
DESCRIBE rtsp://192.168.10.150:554/test/test RTSP/1.0
CSeq: 2
Accept: application/sdp
[RTSP] Received DESCRIBE response:
RTSP/1.0 401 Unauthorized
CSeq: 2
Date: Thu, Feb 23 2023 06:59:11 GMT
Server: ZLMediaKit(git hash:14da5ab2,branch:master,build time:Feb 6 2023 08:30:31)
WWW-Authenticate: Digest realm="zlmediakit_reaml_t",nonce="BwOFuMasoVvwYmHDMLe9b2GxIfG6N0OC"
[RTSP] Sending Request:
DESCRIBE rtsp://192.168.10.150:554/test/test RTSP/1.0
CSeq: 3
Accept: application/sdp
Authorization: Digest username="admin", realm="zlmediakit_reaml_t", nonce="BwOFuMasoVvwYmHDMLe9b2GxIfG6N0OC", uri="rtsp:
//192.168.10.150:554/test/test", response="f638db74ed99496721927269def1f249"
[RTSP] Received DESCRIBE response:
RTSP/1.0 200 OK
Content-Base: rtsp://192.168.10.150:554/test/test/
Content-Length: 417
Content-Type: application/sdp
CSeq: 3
Date: Thu, Feb 23 2023 06:59:11 GMT
Server: ZLMediaKit(git hash:14da5ab2,branch:master,build time:Feb 6 2023 08:30:31)
Session: P13JeSDZUsal
x-Accept-Dynamic-Rate: 1
x-Accept-Retransmit: our-retransmit
v=0
o=- 0 0 IN IP4 0.0.0.0
s=Streamed by ZLMediaKit(git hash:14da5ab2,branch:master,build time:Feb 6 2023 08:30:31)
c=IN IP4 0.0.0.0
t=0 0
a=range:npt=now-
a=control:*
m=video 0 RTP/AVP 96
a=rtpmap:96 H264/90000
a=control:track0
m=audio 0 RTP/AVP 97
a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1190
a=rtpmap:97 MPEG4-GENERIC/48000/2
a=control:track1
[RTSP] Sending Request:
SETUP rtsp://192.168.10.150:554/test/test/track0 RTSP/1.0
CSeq: 4
Transport: RTP/AVP/TCP;unicast;interleaved=0-1
Session: P13JeSDZUsal
Authorization: Digest username="admin", realm="zlmediakit_reaml_t", nonce="BwOFuMasoVvwYmHDMLe9b2GxIfG6N0OC", uri="rtsp:
//192.168.10.150:554/test/test/", response="cb998748e94b8a59a1c6a9a5cf80d1fd"
User-Agent: DXMediaPlayer
[RTSP] Received SETUP response:
RTSP/1.0 200 OK
CSeq: 4
Date: Thu, Feb 23 2023 06:59:11 GMT
Server: ZLMediaKit(git hash:14da5ab2,branch:master,build time:Feb 6 2023 08:30:31)
Session: P13JeSDZUsal
Transport: RTP/AVP/TCP;unicast;interleaved=0-1;ssrc=02A77A16
x-Dynamic-Rate: 1
x-Transport-Options: late-tolerance=1.400000
[RTSP] Sending Request:
SETUP rtsp://192.168.10.150:554/test/test/track1 RTSP/1.0
CSeq: 5
Transport: RTP/AVP/TCP;unicast;interleaved=2-3
Session: P13JeSDZUsal
Authorization: Digest username="admin", realm="zlmediakit_reaml_t", nonce="BwOFuMasoVvwYmHDMLe9b2GxIfG6N0OC", uri="rtsp:
//192.168.10.150:554/test/test/", response="cb998748e94b8a59a1c6a9a5cf80d1fd"
User-Agent: DXMediaPlayer
[RTSP] Received SETUP response:
RTSP/1.0 200 OK
CSeq: 5
Date: Thu, Feb 23 2023 06:59:11 GMT
Server: ZLMediaKit(git hash:14da5ab2,branch:master,build time:Feb 6 2023 08:30:31)
Session: P13JeSDZUsal
Transport: RTP/AVP/TCP;unicast;interleaved=2-3;ssrc=00000000
x-Dynamic-Rate: 1
x-Transport-Options: late-tolerance=1.400000
[RTSP] Sending Request:
PLAY rtsp://192.168.10.150:554/test/test/ RTSP/1.0
CSeq: 6
Session: P13JeSDZUsal
Range: npt=0.000-
Authorization: Digest username="admin", realm="zlmediakit_reaml_t", nonce="BwOFuMasoVvwYmHDMLe9b2GxIfG6N0OC", uri="rtsp:
//192.168.10.150:554/test/test/", response="e2eb560a0a3cc9dce66a7c8b5259b660"
User-Agent: DXMediaPlayer
[RTSP] Received PLAY response:
RTSP/1.0 200 OK
CSeq: 6
Date: Thu, Feb 23 2023 06:59:11 GMT
Range: npt=0.000-
RTP-Info: url=rtsp://192.168.10.150:554/test/test/track0;seq=33042;rtptime=2096923770,url=rtsp://192.168.10.150:554/test
/test/track1;seq=0;rtptime=0
Server: ZLMediaKit(git hash:14da5ab2,branch:master,build time:Feb 6 2023 08:30:31)
Session: P13JeSDZUsal
至此结束
ZLM
Qt HTTP Server 开源库
MediaServer支持HTTP HOOK API
rtsp摘要认证协议(Response计算方法)