WebHook.cpp 18.7 KB
Newer Older
xiongziliang committed
1
/*
xiongziliang committed
2
 * Copyright (c) 2016 The ZLMediaKit project authors. All Rights Reserved.
xiongziliang committed
3 4 5
 *
 * This file is part of ZLMediaKit(https://github.com/xiongziliang/ZLMediaKit).
 *
xiongziliang committed
6 7 8
 * Use of this source code is governed by MIT license that can be found in the
 * LICENSE file in the root of the source tree. All contributing project authors
 * may be found in the AUTHORS file in the root of the source tree.
xiongziliang committed
9 10
 */

11 12 13 14 15 16 17 18 19 20 21 22
#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"
xiongziliang committed
23
#include "Rtsp/RtspSession.h"
24
#include "Http/HttpSession.h"
25
#include "WebHook.h"
xiongziliang committed
26
#include "Record/MP4Recorder.h"
27 28 29 30 31

using namespace Json;
using namespace toolkit;
using namespace mediakit;

xiongziliang committed
32 33 34 35 36 37 38 39 40 41 42

//支持json或urlencoded方式传输参数
#define JSON_ARGS

#ifdef JSON_ARGS
typedef Value ArgsType;
#else
typedef HttpArgs ArgsType;
#endif


43 44 45
namespace Hook {
#define HOOK_FIELD "hook."

46 47 48 49 50 51 52 53 54 55 56 57 58
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";
59
const string kOnServerStarted = HOOK_FIELD"on_server_started";
60
const string kAdminParams = HOOK_FIELD"admin_params";
61 62

onceToken token([](){
xiongziliang committed
63
    mINI::Instance()[kEnable] = false;
64
    mINI::Instance()[kTimeoutSec] = 10;
65 66 67 68 69 70 71 72 73
    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";
74
    mINI::Instance()[kOnStreamNoneReader] = "https://127.0.0.1/index/hook/on_stream_none_reader";
75
    mINI::Instance()[kOnHttpAccess] = "https://127.0.0.1/index/hook/on_http_access";
76
    mINI::Instance()[kOnServerStarted] = "https://127.0.0.1/index/hook/on_server_started";
77
    mINI::Instance()[kAdminParams] = "secret=035c73f7-bb6b-4889-a715-d9eb2d1925cc";
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
},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);
    }
}

xiongziliang committed
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
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){
130
    GET_CONFIG(float,hook_timeoutSec,Hook::kTimeoutSec);
131 132
    HttpRequester::Ptr requester(new HttpRequester);
    requester->setMethod("POST");
xiongziliang committed
133 134 135
    auto bodyStr = to_string(body);
    requester->setBody(bodyStr);
    requester->addHeader("Content-Type",getContentType(body));
136
    std::shared_ptr<Ticker> pTicker(new Ticker);
xiongziliang committed
137
    requester->startRequester(url,[url,fun,bodyStr,requester,pTicker](const SockException &ex,
138 139 140 141 142 143 144 145 146 147 148
                                                    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()) {
xiongziliang committed
149
                WarnL << "hook " << url << " " <<pTicker->elapsedTime() << "ms,failed" << err << ":" << bodyStr;
150
            }else if(pTicker->elapsedTime() > 500){
xiongziliang committed
151
                DebugL << "hook " << url << " " <<pTicker->elapsedTime() << "ms,success:" << bodyStr;
152 153 154 155 156
            }
        });
    },hook_timeoutSec);
}

xiongziliang committed
157 158
static ArgsType make_json(const MediaInfo &args){
    ArgsType body;
159 160 161 162 163
    body["schema"] = args._schema;
    body["vhost"] = args._vhost;
    body["app"] = args._app;
    body["stream"] = args._streamid;
    body["params"] = args._param_strs;
xiongziliang committed
164
    return std::move(body);
165 166
}

167 168 169 170 171 172 173 174 175 176 177 178 179 180
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);
}
181 182

void installWebHook(){
183 184 185 186 187 188 189 190 191 192 193 194
    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);
195
    GET_CONFIG(string,hook_http_access,Hook::kOnHttpAccess);
196

xiongziliang committed
197
    NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastMediaPublish,[](BroadcastMediaPublishArgs){
198 199 200
        GET_CONFIG(bool,toRtxp,General::kPublishToRtxp);
        GET_CONFIG(bool,toHls,General::kPublishToHls);
        GET_CONFIG(bool,toMP4,General::kPublishToMP4);
201
        if(!hook_enable || args._param_strs == hook_adminparams || hook_publish.empty() || sender.get_peer_ip() == "127.0.0.1"){
202
            invoker("",toRtxp,toHls,toMP4);
203 204 205
            return;
        }
        //异步执行该hook api,防止阻塞NoticeCenter
xiongziliang committed
206
        auto body = make_json(args);
207 208 209
        body["ip"] = sender.get_peer_ip();
        body["port"] = sender.get_peer_port();
        body["id"] = sender.getIdentifier();
210 211
        //执行hook
        do_http_hook(hook_publish,body,[invoker](const Value &obj,const string &err){
212 213
            if(err.empty()){
                //推流鉴权成功
214 215 216 217
                bool enableRtxp = toRtxp;
                bool enableHls = toHls;
                bool enableMP4 = toMP4;

218 219
                //兼容用户不传递enableRtxp、enableHls、enableMP4参数
                if(obj.isMember("enableRtxp")){
220
                    enableRtxp = obj["enableRtxp"].asBool();
221 222 223
                }

                if(obj.isMember("enableHls")){
224
                    enableHls = obj["enableHls"].asBool();
225 226 227
                }

                if(obj.isMember("enableMP4")){
228
                    enableMP4 = obj["enableMP4"].asBool();
229
                }
230

231
                invoker(err,enableRtxp,enableHls,enableMP4);
232 233 234 235 236
            }else{
                //推流鉴权失败
                invoker(err,false, false, false);
            }

237 238 239 240
        });
    });

    NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastMediaPlayed,[](BroadcastMediaPlayedArgs){
241
        if(!hook_enable || args._param_strs == hook_adminparams || hook_play.empty() || sender.get_peer_ip() == "127.0.0.1"){
242 243 244
            invoker("");
            return;
        }
xiongziliang committed
245
        auto body = make_json(args);
246 247 248
        body["ip"] = sender.get_peer_ip();
        body["port"] = sender.get_peer_port();
        body["id"] = sender.getIdentifier();
249 250 251
        //执行hook
        do_http_hook(hook_play,body,[invoker](const Value &obj,const string &err){
            invoker(err);
252 253 254 255
        });
    });

    NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastFlowReport,[](BroadcastFlowReportArgs){
256
        if(!hook_enable || args._param_strs == hook_adminparams || hook_flowreport.empty() || sender.get_peer_ip() == "127.0.0.1"){
257 258
            return;
        }
xiongziliang committed
259
        auto body = make_json(args);
260 261
        body["totalBytes"] = (Json::UInt64)totalBytes;
        body["duration"] = (Json::UInt64)totalDuration;
262
        body["player"] = isPlayer;
263 264 265
        body["ip"] = sender.get_peer_ip();
        body["port"] = sender.get_peer_port();
        body["id"] = sender.getIdentifier();
266 267
        //执行hook
        do_http_hook(hook_flowreport,body, nullptr);
268
    });
xiongziliang committed
269 270 271 272 273 274


    static const string unAuthedRealm = "unAuthedRealm";

    //监听kBroadcastOnGetRtspRealm事件决定rtsp链接是否需要鉴权(传统的rtsp鉴权方案)才能访问
    NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastOnGetRtspRealm,[](BroadcastOnGetRtspRealmArgs){
275
        if(!hook_enable || args._param_strs == hook_adminparams || hook_rtsp_realm.empty() || sender.get_peer_ip() == "127.0.0.1"){
xiongziliang committed
276 277 278 279 280 281 282 283
            //无需认证
            invoker("");
            return;
        }
        auto body = make_json(args);
        body["ip"] = sender.get_peer_ip();
        body["port"] = sender.get_peer_port();
        body["id"] = sender.getIdentifier();
284 285 286 287 288 289 290 291
        //执行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());
xiongziliang committed
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
        });
    });

    //监听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;
309 310 311 312 313 314 315 316
        //执行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());
xiongziliang committed
317 318 319 320 321 322 323 324 325 326 327
        });
    });


    //监听rtsp、rtmp源注册或注销事件
    NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastMediaChanged,[](BroadcastMediaChangedArgs){
        if(!hook_enable || hook_stream_chaned.empty()){
            return;
        }
        ArgsType body;
        body["regist"] = bRegist;
xiongziliang committed
328 329 330 331
        body["schema"] = sender.getSchema();
        body["vhost"] = sender.getVhost();
        body["app"] = sender.getApp();
        body["stream"] = sender.getId();
332 333
        //执行hook
        do_http_hook(hook_stream_chaned,body, nullptr);
xiongziliang committed
334 335 336 337 338
    });

    //监听播放失败(未找到特定的流)事件
    NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastNotFoundStream,[](BroadcastNotFoundStreamArgs){
        if(!hook_enable || hook_stream_not_found.empty()){
339
//            closePlayer();
xiongziliang committed
340 341 342 343 344 345
            return;
        }
        auto body = make_json(args);
        body["ip"] = sender.get_peer_ip();
        body["port"] = sender.get_peer_port();
        body["id"] = sender.getIdentifier();
346 347
        //执行hook
        do_http_hook(hook_stream_not_found,body, nullptr);
xiongziliang committed
348 349
    });

xiongziliang committed
350
#ifdef ENABLE_MP4
xiongziliang committed
351 352 353 354 355 356
    //录制mp4文件成功后广播
    NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastRecordMP4,[](BroadcastRecordMP4Args){
        if(!hook_enable || hook_record_mp4.empty()){
            return;
        }
        ArgsType body;
xiongziliang committed
357 358 359
        body["start_time"] = (Json::UInt64)info.ui64StartedTime;
        body["time_len"] = (Json::UInt64)info.ui64TimeLen;
        body["file_size"] = (Json::UInt64)info.ui64FileSize;
xiongziliang committed
360 361 362 363 364 365 366
        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;
367 368
        //执行hook
        do_http_hook(hook_record_mp4,body, nullptr);
xiongziliang committed
369
    });
xiongziliang committed
370
#endif //ENABLE_MP4
xiongziliang committed
371

372
    NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastShellLogin,[](BroadcastShellLoginArgs){
373 374
        if(!hook_enable || hook_shell_login.empty() || sender.get_peer_ip() == "127.0.0.1"){
            invoker("");
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389
            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);
        });
    });

390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412
    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);
        });

    });

413 414 415
    /**
     * kBroadcastHttpAccess事件触发机制
     * 1、根据http请求头查找cookie,找到进入步骤3
xiongziliang committed
416
     * 2、根据http url参数查找cookie,如果还是未找到cookie则进入步骤5
417 418 419 420
     * 3、cookie标记是否有权限访问文件,如果有权限,直接返回文件
     * 4、cookie中记录的url参数是否跟本次url参数一致,如果一致直接返回客户端错误码
     * 5、触发kBroadcastHttpAccess事件
     */
421
    //开发者应该通过该事件判定http客户端是否有权限访问http服务器上的特定文件
422 423 424 425 426 427
    //ZLMediaKit会记录本次鉴权的结果至cookie
    //如果鉴权成功,在cookie有效期内,那么下次客户端再访问授权目录时,ZLMediaKit会直接返回文件
    //如果鉴权失败,在cookie有效期内,如果http url参数不变(否则会立即再次触发鉴权事件),ZLMediaKit会直接返回错误码
    //如果用户客户端不支持cookie,那么ZLMediaKit会根据url参数查找cookie并追踪用户,
    //如果没有url参数,客户端又不支持cookie,那么会根据ip和端口追踪用户
    //追踪用户的目的是为了缓存上次鉴权结果,减少鉴权次数,提高性能
428
    NoticeCenter::Instance().addListener(nullptr,Broadcast::kBroadcastHttpAccess,[](BroadcastHttpAccessArgs){
429
        if(sender.get_peer_ip() == "127.0.0.1" || parser.Params() == hook_adminparams){
430
            //如果是本机或超级管理员访问,那么不做访问鉴权;权限有效期1个小时
431
            invoker("","",60 * 60);
432 433 434 435 436
            return;
        }
        if(!hook_enable || hook_http_access.empty()){
            //未开启http文件访问鉴权,那么允许访问,但是每次访问都要鉴权;
            //因为后续随时都可能开启鉴权(重载配置文件后可能重新开启鉴权)
437
            invoker("","",0);
438 439 440 441 442 443 444 445 446 447
            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();
xiongziliang committed
448
        for(auto &pr : parser.getHeader()){
449
            body[string("header.") + pr.first] = pr.second;
450 451 452 453
        }
        //执行hook
        do_http_hook(hook_http_access,body, [invoker](const Value &obj,const string &err){
            if(!err.empty()){
454
                //如果接口访问失败,那么仅限本次没有访问http服务器的权限
455
                invoker(err,"",0);
456 457
                return;
            }
458 459 460 461
            //err参数代表不能访问的原因,空则代表可以访问
            //path参数是该客户端能访问或被禁止的顶端目录,如果path为空字符串,则表述为当前目录
            //second参数规定该cookie超时时间,如果second为0,本次鉴权结果不缓存
            invoker(obj["err"].asString(),obj["path"].asString(),obj["second"].asInt());
462 463
        });
    });
464 465 466

    //汇报服务器重新启动
    reportServerStarted();
467 468 469
}

void unInstallWebHook(){
xiongziliang committed
470

471
}