WebHook.cpp 18.7 KB
Newer Older
xiongziliang committed
1
/*
xiongziliang committed
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
 * 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.
 */

27 28 29 30 31 32 33 34 35 36 37 38
#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
39
#include "Rtsp/RtspSession.h"
40
#include "Http/HttpSession.h"
41
#include "WebHook.h"
42 43 44 45 46

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

xiongziliang committed
47 48 49 50 51 52 53 54 55 56 57

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

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


58 59 60
namespace Hook {
#define HOOK_FIELD "hook."

61 62 63 64 65 66 67 68 69 70 71 72 73 74
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";
const string kAdminParams = HOOK_FIELD"admin_params";
75 76

onceToken token([](){
xiongziliang committed
77
    mINI::Instance()[kEnable] = false;
78
    mINI::Instance()[kTimeoutSec] = 10;
79 80 81 82 83 84 85 86 87
    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";
88
    mINI::Instance()[kOnStreamNoneReader] = "https://127.0.0.1/index/hook/on_stream_none_reader";
89
    mINI::Instance()[kOnHttpAccess] = "https://127.0.0.1/index/hook/on_http_access";
90
    mINI::Instance()[kAdminParams] = "secret=035c73f7-bb6b-4889-a715-d9eb2d1925cc";
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
},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
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
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){
143
    GET_CONFIG(float,hook_timeoutSec,Hook::kTimeoutSec);
144 145
    HttpRequester::Ptr requester(new HttpRequester);
    requester->setMethod("POST");
xiongziliang committed
146 147 148
    auto bodyStr = to_string(body);
    requester->setBody(bodyStr);
    requester->addHeader("Content-Type",getContentType(body));
149
    std::shared_ptr<Ticker> pTicker(new Ticker);
xiongziliang committed
150
    requester->startRequester(url,[url,fun,bodyStr,requester,pTicker](const SockException &ex,
151 152 153 154 155 156 157 158 159 160 161
                                                    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
162
                WarnL << "hook " << url << " " <<pTicker->elapsedTime() << "ms,failed" << err << ":" << bodyStr;
163
            }else if(pTicker->elapsedTime() > 500){
xiongziliang committed
164
                DebugL << "hook " << url << " " <<pTicker->elapsedTime() << "ms,success:" << bodyStr;
165 166 167 168 169
            }
        });
    },hook_timeoutSec);
}

xiongziliang committed
170 171
static ArgsType make_json(const MediaInfo &args){
    ArgsType body;
172 173 174 175 176
    body["schema"] = args._schema;
    body["vhost"] = args._vhost;
    body["app"] = args._app;
    body["stream"] = args._streamid;
    body["params"] = args._param_strs;
xiongziliang committed
177
    return std::move(body);
178 179 180 181
}


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

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

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

                if(obj.isMember("enableHls")){
                    enableHls = obj["enableHls"].asBool();
                }

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

                invoker(err,enableRtxp,enableHls,enableMP4);
            }else{
                //推流鉴权失败
                invoker(err,false, false, false);
            }

233 234 235 236
        });
    });

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

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


    static const string unAuthedRealm = "unAuthedRealm";

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

    //监听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;
305 306 307 308 309 310 311 312
        //执行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
313 314 315 316 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;
        body["schema"] = schema;
        body["vhost"] = vhost;
        body["app"] = app;
        body["stream"] = stream;
328 329
        //执行hook
        do_http_hook(hook_stream_chaned,body, nullptr);
xiongziliang committed
330 331 332 333 334 335 336 337 338 339 340
    });

    //监听播放失败(未找到特定的流)事件
    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();
341 342
        //执行hook
        do_http_hook(hook_stream_not_found,body, nullptr);
xiongziliang committed
343 344
    });

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

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

385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
    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);
        });

    });

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

void unInstallWebHook(){
xiongziliang committed
462

463
}