ZLMediaKit/server/WebHook.cpp

480 lines
19 KiB
C++
Raw Normal View History

2019-08-08 19:01:45 +08:00
/*
2019-06-11 09:25:54 +08:00
* MIT License
*
* Copyright (c) 2016-2019 xiongziliang <771730766@qq.com>
*
* This file is part of ZLMediaKit(https://github.com/xiongziliang/ZLMediaKit).
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
2019-05-20 11:22:59 +08:00
#include <sstream>
#include <unordered_map>
#include <mutex>
#include "jsoncpp/json.h"
#include "Util/logger.h"
#include "Util/util.h"
#include "Util/onceToken.h"
#include "Util/NoticeCenter.h"
#include "Common/config.h"
#include "Common/MediaSource.h"
#include "Http/HttpRequester.h"
#include "Network/TcpSession.h"
2019-05-20 16:26:04 +08:00
#include "Rtsp/RtspSession.h"
2019-06-12 17:53:48 +08:00
#include "Http/HttpSession.h"
#include "WebHook.h"
2019-12-04 10:45:38 +08:00
#include "Record/MP4Recorder.h"
2019-05-20 11:22:59 +08:00
using namespace Json;
using namespace toolkit;
using namespace mediakit;
2019-05-20 16:26:04 +08:00
//支持json或urlencoded方式传输参数
#define JSON_ARGS
#ifdef JSON_ARGS
typedef Value ArgsType;
#else
typedef HttpArgs ArgsType;
#endif
2019-05-20 11:22:59 +08:00
namespace Hook {
#define HOOK_FIELD "hook."
const string kEnable = HOOK_FIELD"enable";
const string kTimeoutSec = HOOK_FIELD"timeoutSec";
const string kOnPublish = HOOK_FIELD"on_publish";
const string kOnPlay = HOOK_FIELD"on_play";
const string kOnFlowReport = HOOK_FIELD"on_flow_report";
const string kOnRtspRealm = HOOK_FIELD"on_rtsp_realm";
const string kOnRtspAuth = HOOK_FIELD"on_rtsp_auth";
const string kOnStreamChanged = HOOK_FIELD"on_stream_changed";
const string kOnStreamNotFound = HOOK_FIELD"on_stream_not_found";
const string kOnRecordMp4 = HOOK_FIELD"on_record_mp4";
const string kOnShellLogin = HOOK_FIELD"on_shell_login";
const string kOnStreamNoneReader = HOOK_FIELD"on_stream_none_reader";
const string kOnHttpAccess = HOOK_FIELD"on_http_access";
2019-11-29 10:12:20 +08:00
const string kOnServerStarted = HOOK_FIELD"on_server_started";
const string kAdminParams = HOOK_FIELD"admin_params";
2019-05-20 11:22:59 +08:00
onceToken token([](){
2019-08-16 15:34:34 +08:00
mINI::Instance()[kEnable] = false;
2019-05-20 11:22:59 +08:00
mINI::Instance()[kTimeoutSec] = 10;
mINI::Instance()[kOnPublish] = "https://127.0.0.1/index/hook/on_publish";
mINI::Instance()[kOnPlay] = "https://127.0.0.1/index/hook/on_play";
mINI::Instance()[kOnFlowReport] = "https://127.0.0.1/index/hook/on_flow_report";
mINI::Instance()[kOnRtspRealm] = "https://127.0.0.1/index/hook/on_rtsp_realm";
mINI::Instance()[kOnRtspAuth] = "https://127.0.0.1/index/hook/on_rtsp_auth";
mINI::Instance()[kOnStreamChanged] = "https://127.0.0.1/index/hook/on_stream_changed";
mINI::Instance()[kOnStreamNotFound] = "https://127.0.0.1/index/hook/on_stream_not_found";
mINI::Instance()[kOnRecordMp4] = "https://127.0.0.1/index/hook/on_record_mp4";
mINI::Instance()[kOnShellLogin] = "https://127.0.0.1/index/hook/on_shell_login";
2019-05-27 18:39:43 +08:00
mINI::Instance()[kOnStreamNoneReader] = "https://127.0.0.1/index/hook/on_stream_none_reader";
2019-06-12 17:53:48 +08:00
mINI::Instance()[kOnHttpAccess] = "https://127.0.0.1/index/hook/on_http_access";
2019-11-29 10:12:20 +08:00
mINI::Instance()[kOnServerStarted] = "https://127.0.0.1/index/hook/on_server_started";
2019-05-20 17:12:00 +08:00
mINI::Instance()[kAdminParams] = "secret=035c73f7-bb6b-4889-a715-d9eb2d1925cc";
2019-05-20 11:22:59 +08:00
},nullptr);
}//namespace Hook
static void parse_http_response(const SockException &ex,
const string &status,
const HttpClient::HttpHeader &header,
const string &strRecvBody,
const function<void(const Value &,const string &)> &fun){
if(ex){
auto errStr = StrPrinter << "[network err]:" << ex.what() << endl;
fun(Json::nullValue,errStr);
return;
}
if(status != "200"){
auto errStr = StrPrinter << "[bad http status code]:" << status << endl;
fun(Json::nullValue,errStr);
return;
}
try {
stringstream ss(strRecvBody);
Value result;
ss >> result;
if(result["code"].asInt() != 0) {
auto errStr = StrPrinter << "[json code]:" << "code=" << result["code"] << ",msg=" << result["msg"] << endl;
fun(Json::nullValue,errStr);
return;
}
fun(result,"");
}catch (std::exception &ex){
auto errStr = StrPrinter << "[parse json failed]:" << ex.what() << endl;
fun(Json::nullValue,errStr);
}
}
2019-05-20 16:26:04 +08:00
string to_string(const Value &value){
return value.toStyledString();
}
string to_string(const HttpArgs &value){
return value.make();
}
const char *getContentType(const Value &value){
return "application/json";
}
const char *getContentType(const HttpArgs &value){
return "application/x-www-form-urlencoded";
}
static void do_http_hook(const string &url,const ArgsType &body,const function<void(const Value &,const string &)> &fun){
2019-05-28 17:14:36 +08:00
GET_CONFIG(float,hook_timeoutSec,Hook::kTimeoutSec);
2019-05-20 11:22:59 +08:00
HttpRequester::Ptr requester(new HttpRequester);
requester->setMethod("POST");
2019-05-20 16:26:04 +08:00
auto bodyStr = to_string(body);
requester->setBody(bodyStr);
requester->addHeader("Content-Type",getContentType(body));
2019-05-20 11:22:59 +08:00
std::shared_ptr<Ticker> pTicker(new Ticker);
2019-05-20 16:26:04 +08:00
requester->startRequester(url,[url,fun,bodyStr,requester,pTicker](const SockException &ex,
2019-05-20 11:22:59 +08:00
const string &status,
const HttpClient::HttpHeader &header,
const string &strRecvBody){
onceToken token(nullptr,[&](){
const_cast<HttpRequester::Ptr &>(requester).reset();
});
parse_http_response(ex,status,header,strRecvBody,[&](const Value &obj,const string &err){
if(fun){
fun(obj,err);
}
if(!err.empty()) {
2019-05-20 16:26:04 +08:00
WarnL << "hook " << url << " " <<pTicker->elapsedTime() << "ms,failed" << err << ":" << bodyStr;
2019-05-20 11:22:59 +08:00
}else if(pTicker->elapsedTime() > 500){
2019-05-20 16:26:04 +08:00
DebugL << "hook " << url << " " <<pTicker->elapsedTime() << "ms,success:" << bodyStr;
2019-05-20 11:22:59 +08:00
}
});
},hook_timeoutSec);
}
2019-05-20 16:26:04 +08:00
static ArgsType make_json(const MediaInfo &args){
ArgsType body;
2019-05-20 11:22:59 +08:00
body["schema"] = args._schema;
body["vhost"] = args._vhost;
body["app"] = args._app;
body["stream"] = args._streamid;
body["params"] = args._param_strs;
2019-05-20 16:26:04 +08:00
return std::move(body);
2019-05-20 11:22:59 +08:00
}
2019-11-29 10:12:20 +08:00
static void reportServerStarted(){
GET_CONFIG(bool,hook_enable,Hook::kEnable);
GET_CONFIG(string,hook_server_started,Hook::kOnServerStarted);
if(!hook_enable || hook_server_started.empty()){
return;
}
ArgsType body;
for (auto &pr : mINI::Instance()) {
body[pr.first] = (string &) pr.second;
}
//执行hook
do_http_hook(hook_server_started,body, nullptr);
}
2019-05-20 11:22:59 +08:00
void installWebHook(){
2019-05-28 17:14:36 +08:00
GET_CONFIG(bool,hook_enable,Hook::kEnable);
GET_CONFIG(string,hook_publish,Hook::kOnPublish);
GET_CONFIG(string,hook_play,Hook::kOnPlay);
GET_CONFIG(string,hook_flowreport,Hook::kOnFlowReport);
GET_CONFIG(string,hook_adminparams,Hook::kAdminParams);
GET_CONFIG(string,hook_rtsp_realm,Hook::kOnRtspRealm);
GET_CONFIG(string,hook_rtsp_auth,Hook::kOnRtspAuth);
GET_CONFIG(string,hook_stream_chaned,Hook::kOnStreamChanged);
GET_CONFIG(string,hook_stream_not_found,Hook::kOnStreamNotFound);
GET_CONFIG(string,hook_record_mp4,Hook::kOnRecordMp4);
GET_CONFIG(string,hook_shell_login,Hook::kOnShellLogin);
GET_CONFIG(string,hook_stream_none_reader,Hook::kOnStreamNoneReader);
2019-06-12 17:53:48 +08:00
GET_CONFIG(string,hook_http_access,Hook::kOnHttpAccess);
2019-05-20 11:22:59 +08:00
2019-05-20 16:26:04 +08:00
NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastMediaPublish,[](BroadcastMediaPublishArgs){
GET_CONFIG(bool,toRtxp,General::kPublishToRtxp);
GET_CONFIG(bool,toHls,General::kPublishToHls);
GET_CONFIG(bool,toMP4,General::kPublishToMP4);
2019-06-06 15:33:11 +08:00
if(!hook_enable || args._param_strs == hook_adminparams || hook_publish.empty() || sender.get_peer_ip() == "127.0.0.1"){
invoker("",toRtxp,toHls,toMP4);
2019-05-20 11:22:59 +08:00
return;
}
//异步执行该hook api防止阻塞NoticeCenter
2019-05-20 16:26:04 +08:00
auto body = make_json(args);
2019-05-20 11:22:59 +08:00
body["ip"] = sender.get_peer_ip();
body["port"] = sender.get_peer_port();
body["id"] = sender.getIdentifier();
2019-05-20 17:34:39 +08:00
//执行hook
do_http_hook(hook_publish,body,[invoker](const Value &obj,const string &err){
if(err.empty()){
//推流鉴权成功
//兼容用户不传递enableRtxp、enableHls、enableMP4参数
if(obj.isMember("enableRtxp")){
toRtxp = obj["enableRtxp"].asBool();
}
if(obj.isMember("enableHls")){
toHls = obj["enableHls"].asBool();
}
if(obj.isMember("enableMP4")){
toMP4 = obj["enableMP4"].asBool();
}
invoker(err,toRtxp,toHls,toMP4);
}else{
//推流鉴权失败
invoker(err,false, false, false);
}
2019-05-20 11:22:59 +08:00
});
});
NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastMediaPlayed,[](BroadcastMediaPlayedArgs){
2019-06-06 15:33:11 +08:00
if(!hook_enable || args._param_strs == hook_adminparams || hook_play.empty() || sender.get_peer_ip() == "127.0.0.1"){
2019-05-20 11:22:59 +08:00
invoker("");
return;
}
2019-05-20 16:26:04 +08:00
auto body = make_json(args);
2019-05-20 11:22:59 +08:00
body["ip"] = sender.get_peer_ip();
body["port"] = sender.get_peer_port();
body["id"] = sender.getIdentifier();
2019-05-20 17:34:39 +08:00
//执行hook
do_http_hook(hook_play,body,[invoker](const Value &obj,const string &err){
invoker(err);
2019-05-20 11:22:59 +08:00
});
});
NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastFlowReport,[](BroadcastFlowReportArgs){
2019-12-29 15:38:29 +08:00
if(!hook_enable || args._param_strs == hook_adminparams || hook_flowreport.empty()){
2019-05-20 11:22:59 +08:00
return;
}
2019-05-20 16:26:04 +08:00
auto body = make_json(args);
2019-05-20 11:22:59 +08:00
body["totalBytes"] = (Json::UInt64)totalBytes;
body["duration"] = (Json::UInt64)totalDuration;
2019-05-20 17:46:06 +08:00
body["player"] = isPlayer;
2019-05-20 17:34:39 +08:00
//执行hook
do_http_hook(hook_flowreport,body, nullptr);
2019-05-20 11:22:59 +08:00
});
2019-05-20 16:26:04 +08:00
static const string unAuthedRealm = "unAuthedRealm";
//监听kBroadcastOnGetRtspRealm事件决定rtsp链接是否需要鉴权(传统的rtsp鉴权方案)才能访问
NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastOnGetRtspRealm,[](BroadcastOnGetRtspRealmArgs){
2019-06-06 15:33:11 +08:00
if(!hook_enable || args._param_strs == hook_adminparams || hook_rtsp_realm.empty() || sender.get_peer_ip() == "127.0.0.1"){
2019-05-20 16:26:04 +08:00
//无需认证
invoker("");
return;
}
auto body = make_json(args);
body["ip"] = sender.get_peer_ip();
body["port"] = sender.get_peer_port();
body["id"] = sender.getIdentifier();
2019-05-20 17:34:39 +08:00
//执行hook
do_http_hook(hook_rtsp_realm,body, [invoker](const Value &obj,const string &err){
if(!err.empty()){
//如果接口访问失败那么该rtsp流认证失败
invoker(unAuthedRealm);
return;
}
invoker(obj["realm"].asString());
2019-05-20 16:26:04 +08:00
});
});
//监听kBroadcastOnRtspAuth事件返回正确的rtsp鉴权用户密码
NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastOnRtspAuth,[](BroadcastOnRtspAuthArgs){
if(unAuthedRealm == realm || !hook_enable || hook_rtsp_auth.empty()){
//认证失败
invoker(false,makeRandStr(12));
return;
}
auto body = make_json(args);
body["ip"] = sender.get_peer_ip();
body["port"] = sender.get_peer_port();
body["id"] = sender.getIdentifier();
body["user_name"] = user_name;
body["must_no_encrypt"] = must_no_encrypt;
body["realm"] = realm;
2019-05-20 17:34:39 +08:00
//执行hook
do_http_hook(hook_rtsp_auth,body, [invoker](const Value &obj,const string &err){
if(!err.empty()){
//认证失败
invoker(false,makeRandStr(12));
return;
}
invoker(obj["encrypted"].asBool(),obj["passwd"].asString());
2019-05-20 16:26:04 +08:00
});
});
//监听rtsp、rtmp源注册或注销事件
NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastMediaChanged,[](BroadcastMediaChangedArgs){
if(!hook_enable || hook_stream_chaned.empty()){
return;
}
ArgsType body;
body["regist"] = bRegist;
2019-12-24 14:08:16 +08:00
body["schema"] = sender.getSchema();
body["vhost"] = sender.getVhost();
body["app"] = sender.getApp();
body["stream"] = sender.getId();
2019-05-20 17:34:39 +08:00
//执行hook
do_http_hook(hook_stream_chaned,body, nullptr);
2019-05-20 16:26:04 +08:00
});
//监听播放失败(未找到特定的流)事件
NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastNotFoundStream,[](BroadcastNotFoundStreamArgs){
if(!hook_enable || hook_stream_not_found.empty()){
return;
}
auto body = make_json(args);
body["ip"] = sender.get_peer_ip();
body["port"] = sender.get_peer_port();
body["id"] = sender.getIdentifier();
2019-05-20 17:34:39 +08:00
//执行hook
do_http_hook(hook_stream_not_found,body, nullptr);
2019-05-20 16:26:04 +08:00
});
2019-08-22 17:46:47 +08:00
#ifdef ENABLE_MP4RECORD
2019-05-20 16:26:04 +08:00
//录制mp4文件成功后广播
NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastRecordMP4,[](BroadcastRecordMP4Args){
if(!hook_enable || hook_record_mp4.empty()){
return;
}
ArgsType body;
2019-05-23 09:23:24 +08:00
body["start_time"] = (Json::UInt64)info.ui64StartedTime;
body["time_len"] = (Json::UInt64)info.ui64TimeLen;
body["file_size"] = (Json::UInt64)info.ui64FileSize;
2019-05-20 16:26:04 +08:00
body["file_path"] = info.strFilePath;
body["file_name"] = info.strFileName;
body["folder"] = info.strFolder;
body["url"] = info.strUrl;
body["app"] = info.strAppName;
body["stream"] = info.strStreamId;
body["vhost"] = info.strVhost;
2019-05-20 17:34:39 +08:00
//执行hook
do_http_hook(hook_record_mp4,body, nullptr);
2019-05-20 16:26:04 +08:00
});
2019-08-22 17:46:47 +08:00
#endif //ENABLE_MP4RECORD
2019-05-20 16:26:04 +08:00
NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastShellLogin,[](BroadcastShellLoginArgs){
2019-06-06 15:33:11 +08:00
if(!hook_enable || hook_shell_login.empty() || sender.get_peer_ip() == "127.0.0.1"){
invoker("");
return;
}
ArgsType body;
body["ip"] = sender.get_peer_ip();
body["port"] = sender.get_peer_port();
body["id"] = sender.getIdentifier();
body["user_name"] = user_name;
body["passwd"] = passwd;
//执行hook
do_http_hook(hook_shell_login,body, [invoker](const Value &,const string &err){
invoker(err);
});
});
2019-05-27 18:39:43 +08:00
NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastStreamNoneReader,[](BroadcastStreamNoneReaderArgs){
if(!hook_enable || hook_stream_none_reader.empty()){
return;
}
ArgsType body;
body["schema"] = sender.getSchema();
body["vhost"] = sender.getVhost();
body["app"] = sender.getApp();
body["stream"] = sender.getId();
weak_ptr<MediaSource> weakSrc = sender.shared_from_this();
//执行hook
do_http_hook(hook_stream_none_reader,body, [weakSrc](const Value &obj,const string &err){
bool flag = obj["close"].asBool();
auto strongSrc = weakSrc.lock();
if(!flag || !err.empty() || !strongSrc){
return;
}
strongSrc->close(false);
});
});
/**
* kBroadcastHttpAccess事件触发机制
* 1http请求头查找cookie3
2019-11-30 11:44:05 +08:00
* 2http url参数查找cookiecookie则进入步骤5
* 3cookie标记是否有权限访问文件
* 4cookie中记录的url参数是否跟本次url参数一致
* 5kBroadcastHttpAccess事件
*/
//开发者应该通过该事件判定http客户端是否有权限访问http服务器上的特定文件
//ZLMediaKit会记录本次鉴权的结果至cookie
//如果鉴权成功在cookie有效期内那么下次客户端再访问授权目录时ZLMediaKit会直接返回文件
//如果鉴权失败在cookie有效期内如果http url参数不变(否则会立即再次触发鉴权事件)ZLMediaKit会直接返回错误码
//如果用户客户端不支持cookie那么ZLMediaKit会根据url参数查找cookie并追踪用户
//如果没有url参数客户端又不支持cookie那么会根据ip和端口追踪用户
//追踪用户的目的是为了缓存上次鉴权结果,减少鉴权次数,提高性能
2019-06-12 17:53:48 +08:00
NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastHttpAccess,[](BroadcastHttpAccessArgs){
2019-12-29 11:52:02 +08:00
if(sender.get_peer_ip() == "127.0.0.1" && parser.Params() == hook_adminparams){
//如果是本机或超级管理员访问那么不做访问鉴权权限有效期1个小时
invoker("","",60 * 60);
return;
}
if(!hook_enable || hook_http_access.empty()){
//未开启http文件访问鉴权那么允许访问但是每次访问都要鉴权
//因为后续随时都可能开启鉴权(重载配置文件后可能重新开启鉴权)
invoker("","",0);
2019-06-12 17:53:48 +08:00
return;
}
ArgsType body;
body["ip"] = sender.get_peer_ip();
body["port"] = sender.get_peer_port();
body["id"] = sender.getIdentifier();
body["path"] = path;
body["is_dir"] = is_dir;
body["params"] = parser.Params();
for(auto &pr : parser.getValues()){
body[string("header.") + pr.first] = pr.second;
2019-06-12 17:53:48 +08:00
}
//执行hook
do_http_hook(hook_http_access,body, [invoker](const Value &obj,const string &err){
if(!err.empty()){
//如果接口访问失败那么仅限本次没有访问http服务器的权限
invoker(err,"",0);
2019-06-12 17:53:48 +08:00
return;
}
//err参数代表不能访问的原因空则代表可以访问
//path参数是该客户端能访问或被禁止的顶端目录如果path为空字符串则表述为当前目录
//second参数规定该cookie超时时间如果second为0本次鉴权结果不缓存
invoker(obj["err"].asString(),obj["path"].asString(),obj["second"].asInt());
2019-06-12 17:53:48 +08:00
});
});
2019-11-29 10:12:20 +08:00
//汇报服务器重新启动
reportServerStarted();
}
void unInstallWebHook(){
2019-05-20 16:26:04 +08:00
}