RTSP向ZLM流媒体服务器的推流和拉流鉴权

文章目录

  • 前言
  • 一、HTTP Hook
  • 二、向ZLM推流鉴权
  • 三、向ZLM拉流
  • 四、参考


前言

本篇博客的测试环境: Windows 10 + Qt 5.12.2 MSVC。
由于项目中使用了RTSP协议,为了防止别人知道我们的流地址随便就能播放观看我们的视频,所以就使用鉴权筛掉一些不合适的请求。
在鉴权之前呢,需要准备一下:

  1. ZLM流媒体服务器,是从ZLMediaKit中server中编译出来的,MediaServer项目非常强大支持推RTSP自动转RTMP、FLV、TS、MP4等,好用。
  2. HTTP HOOK Server:就是一个HTTPServer,用于接收ZLM的HTTP HOOK的通知,控制允不允许推流的一个角色。

一、HTTP Hook

我们向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,分别是:

  • on_publish:推流鉴权事件
  • on_rtsp_realm:rtsp播放是否开启专属鉴权事件,开启rtsp专属鉴权后,将不再触发on_play鉴权事件。
  • on_rtsp_auth:rtsp播放鉴权事件,此事件中比对rtsp的用户名密码

下面将搭建一个简易的测试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推流鉴权

我们先来看一张图,来自ZLM的wiki
RTSP向ZLM流媒体服务器的推流和拉流鉴权_第1张图片
看了看这张图,我有一下疑问:

  1. 我业务服务器怎么知道参数合法有权推流呢?
  2. 我应该怎么传递参数呢?

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就是这个业务服务器给的推流器,怪不知的可以校验。

我将我的业务流程改一下:
RTSP向ZLM流媒体服务器的推流和拉流鉴权_第2张图片

三、向ZLM拉流

老规矩,先来看一张图
RTSP向ZLM流媒体服务器的推流和拉流鉴权_第3张图片

计算鉴权的公式有两种:
(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。
接下来一步步去看怎么鉴权的:

  1. 客户端 - request-> 服务端(OPTIONS)
  2. 服务端 - response-> 客户端(OPTIONS)
  3. 客户端 - request-> 服务端(DESCRIBE - 第1次)触发HOOK API的on_rtsp_realm
  4. 服务端 -requets-> 业务服务器(on_rtsp_realm)
  5. 业务服务器 -response-> 服务端(on_rtsp_realm)

服务端接收到DESCRIBE请求之后,触发HOOK API的on_rtsp_realm,我们直接应答这个API

//应答内容
{
   "code" : 0,						//固定返回0
   "realm" : "zlmediakit_reaml"		//realm由业务服务器指定,给客户端计算鉴权结果,
   									//因为我们这里使用第一种计算方式,所以用不上这个值
}

客户端接收到 realmnonce ,开始计算鉴权结果,使用第一种计算方式。

  1. 服务端 - response-> 客户端(DESCRIBE - 1) --401 Unauthorized 带 realm 和 nonce
  2. 客户端 -request-> 服务端(DESCRIBE - 2)带 username、realm、onnce、responce(鉴权结果) ,触发HOOK API on_rtsp_auth
  3. 服务端 -request-> 业务服务器(on_rtsp_auth)
  4. 业务服务器 -response-> 服务端(on_rtsp_auth)带 md5(password)

服务端接收到第二次DESCRIBE请求,并携带username、realm、onnce、responce(鉴权结果),便触发HOOK API on_rtsp_auth,我们这样应答

//应答内容
{
   "code" : 0,			//0允许播放,其它为错误代码
   "encrypted" : true,	//传入的passwd是否加密
   "passwd" : "e10adc3949ba59abbe56e057f20f883e"	//密码,我这里传入的是使用md5加密后的密码
}

当第九步执行完,将加密后的密码给到ZLM去计算鉴权结果,是否与客户端的鉴权结果一致,不一致说明账号和密码不对。

  1. 服务端 - response-> 客户端(DESCRIBE - 2)鉴权成功返回结果带SDP信息
//推流器和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计算方法)

你可能感兴趣的:(Qt,服务器,qt,ZLM)