mirror of
https://gitee.com/xia-chu/ZLMediaKit.git
synced 2026-05-06 10:57:50 +08:00
新增cookie登录鉴权模式,避免secret硬编码鉴权安全缺陷
Some checks failed
Android / build (push) Has been cancelled
CodeQL / Analyze (cpp) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Docker / build (push) Has been cancelled
Linux / build (push) Has been cancelled
Linux_Python / build (push) Has been cancelled
macOS / build (push) Has been cancelled
macOS_Python / build (push) Has been cancelled
Windows / build (push) Has been cancelled
Windows_Python / build (push) Has been cancelled
Some checks failed
Android / build (push) Has been cancelled
CodeQL / Analyze (cpp) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Docker / build (push) Has been cancelled
Linux / build (push) Has been cancelled
Linux_Python / build (push) Has been cancelled
macOS / build (push) Has been cancelled
macOS_Python / build (push) Has been cancelled
Windows / build (push) Has been cancelled
Windows_Python / build (push) Has been cancelled
This commit is contained in:
parent
22dede5a18
commit
3a35144243
@ -17,6 +17,9 @@ snapRoot=./www/snap/
|
||||
defaultSnap=./www/logo.png
|
||||
#downloadFile http接口可访问文件的根目录,支持多个目录,不同目录通过分号(;)分隔
|
||||
downloadRoot=./www
|
||||
#是否采用传统secret硬编码鉴权模式,默认开启,开启后每次http接口请求都需要传递secret
|
||||
#关闭传统鉴权模式后,需要先调用/index/api/login接口登录,成功后将设置cookie,在cookie有效期内访问所有接口都将放行。
|
||||
legacyAuth=1
|
||||
|
||||
[ffmpeg]
|
||||
#FFmpeg可执行程序路径,支持相对路径/绝对路径
|
||||
|
||||
@ -2950,7 +2950,52 @@
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "登录(login)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{ZLMediaKit_URL}}/index/api/login?digest=d00414822dfd8eabed87c5e24ffcdca7",
|
||||
"host": [
|
||||
"{{ZLMediaKit_URL}}"
|
||||
],
|
||||
"path": [
|
||||
"index",
|
||||
"api",
|
||||
"login"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "digest",
|
||||
"value": "",
|
||||
"description": "MD5(\"zlmediakit:\"+${secret}+\":\" +${cookie})"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "登出(logout)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{ZLMediaKit_URL}}/index/api/logout",
|
||||
"host": [
|
||||
"{{ZLMediaKit_URL}}"
|
||||
],
|
||||
"path": [
|
||||
"index",
|
||||
"api",
|
||||
"logout"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
],
|
||||
"event": [
|
||||
{
|
||||
|
||||
@ -86,6 +86,7 @@ const string kSecret = API_FIELD"secret";
|
||||
const string kSnapRoot = API_FIELD"snapRoot";
|
||||
const string kDefaultSnap = API_FIELD"defaultSnap";
|
||||
const string kDownloadRoot = API_FIELD"downloadRoot";
|
||||
const string kLegacyAuth = API_FIELD"legacyAuth";
|
||||
|
||||
static onceToken token([]() {
|
||||
mINI::Instance()[kApiDebug] = "1";
|
||||
@ -93,6 +94,7 @@ static onceToken token([]() {
|
||||
mINI::Instance()[kSnapRoot] = "./www/snap/";
|
||||
mINI::Instance()[kDefaultSnap] = "./www/logo.png";
|
||||
mINI::Instance()[kDownloadRoot] = "./www";
|
||||
mINI::Instance()[kLegacyAuth] = 1;
|
||||
});
|
||||
}//namespace API
|
||||
|
||||
@ -101,18 +103,20 @@ using HttpApi = function<void(const Parser &parser, const HttpSession::HttpRespo
|
||||
// http api list
|
||||
static map<string, HttpApi, StrCaseCompare> s_map_api;
|
||||
|
||||
static void responseApi(const Json::Value &res, const HttpSession::HttpResponseInvoker &invoker){
|
||||
GET_CONFIG(string, charSet, Http::kCharSet);
|
||||
HttpSession::KeyValue headerOut;
|
||||
headerOut["Content-Type"] = string("application/json; charset=") + charSet;
|
||||
invoker(200, headerOut, res.toStyledString());
|
||||
};
|
||||
|
||||
static void responseApi(int code, const string &msg, const HttpSession::HttpResponseInvoker &invoker){
|
||||
static void responseApi(int code, const string &msg, const HttpSession::HttpResponseInvoker &invoker, ApiRetException *ex = nullptr){
|
||||
Json::Value res;
|
||||
HttpSession::KeyValue headerOut;
|
||||
if (ex) {
|
||||
res = ex->getBody();
|
||||
headerOut = ex->getHeaders();
|
||||
}
|
||||
res["code"] = code;
|
||||
res["msg"] = msg;
|
||||
responseApi(res, invoker);
|
||||
|
||||
GET_CONFIG(string, charSet, Http::kCharSet);
|
||||
headerOut["Content-Type"] = string("application/json; charset=") + charSet;
|
||||
|
||||
invoker(200, headerOut, res.toStyledString());
|
||||
}
|
||||
|
||||
static HttpApi toApi(const function<void(API_ARGS_MAP_ASYNC)> &cb) {
|
||||
@ -304,12 +308,12 @@ static inline void addHttpListener(){
|
||||
try {
|
||||
it->second(parser, invoker, *helper);
|
||||
} catch (ApiRetException &ex) {
|
||||
responseApi(ex.code(), ex.what(), invoker);
|
||||
responseApi(ex.code(), ex.what(), invoker, &ex);
|
||||
helper->getPoller()->async([helper, ex]() { helper->shutdown(SockException(Err_shutdown, ex.what())); }, false);
|
||||
}
|
||||
#ifdef ENABLE_MYSQL
|
||||
catch (SqlException &ex) {
|
||||
responseApi(API::SqlFailed, StrPrinter << "操作数据库失败:" << ex.what() << ":" << ex.getSql(), invoker);
|
||||
responseApi(API::SqlFailed, StrPrinter << "操作数据库失败:" << ex.what() << ":" << ex.getSql(), invoker, &ex);
|
||||
}
|
||||
#endif // ENABLE_MYSQL
|
||||
catch (std::exception &ex) {
|
||||
@ -363,7 +367,7 @@ Value ToJson(const PusherProxy::Ptr& p) {
|
||||
item["url"] = p->getUrl();
|
||||
item["status"] = p->getStatus();
|
||||
item["liveSecs"] = p->getLiveSecs();
|
||||
item["rePublishCount"] = p->getRePublishCount();
|
||||
item["rePublishCount"] = p->getRePublishCount();
|
||||
item["bytesSpeed"] = (Json::UInt64) p->getSendSpeed();
|
||||
item["totalBytes"] =(Json::UInt64) p->getSendTotalBytes();
|
||||
|
||||
@ -708,6 +712,38 @@ void getThreadsLoad(TaskExecutorGetterImp &getter, API_ARGS_MAP_ASYNC) {
|
||||
});
|
||||
}
|
||||
|
||||
static constexpr char kLoginCookiePath[] = "/";
|
||||
static constexpr char kUnLoginCookieName[] = "ZLM_UNLOGIN";
|
||||
static constexpr char kLoginedCookieName[] = "ZLM_LOGINED";
|
||||
static constexpr size_t kUnLoginCookieLifeSeconds = 60;
|
||||
static constexpr size_t kLoginedCookieLifeSeconds = 24 * 3600;
|
||||
|
||||
void check_secret(toolkit::SockInfo &sender, mediakit::HttpSession::KeyValue &headerOut, const ArgsMap &allArgs, Json::Value &val) {
|
||||
GET_CONFIG(bool, legacy_auth , API::kLegacyAuth);
|
||||
GET_CONFIG(std::string, api_secret, API::kSecret);
|
||||
|
||||
auto ip = sender.get_peer_ip();
|
||||
if (!HttpFileManager::isIPAllowed(ip)) {
|
||||
throw AuthException("Your ip is not allowed to access the service.");
|
||||
}
|
||||
if (legacy_auth) {
|
||||
CHECK_ARGS("secret");
|
||||
if (api_secret != allArgs["secret"]) {
|
||||
throw AuthException("Incorrect secret");
|
||||
}
|
||||
} else {
|
||||
auto logined_cookie = HttpCookieManager::Instance().getCookie(kLoginedCookieName, allArgs.getParser().getHeader());
|
||||
if (!logined_cookie) {
|
||||
auto unlogin_cookie = HttpCookieManager::Instance().getCookie(kUnLoginCookieName, allArgs.getParser().getHeader());
|
||||
if (!unlogin_cookie) {
|
||||
unlogin_cookie = HttpCookieManager::Instance().addCookie(kUnLoginCookieName, "", kUnLoginCookieLifeSeconds);
|
||||
headerOut["Set-Cookie"] = unlogin_cookie->getCookie(kLoginCookiePath);
|
||||
}
|
||||
val["cookie"] = unlogin_cookie->getCookie();
|
||||
throw AuthException("Please login first", headerOut, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 安装api接口
|
||||
* 所有api都支持GET和POST两种方式
|
||||
@ -720,7 +756,6 @@ void getThreadsLoad(TaskExecutorGetterImp &getter, API_ARGS_MAP_ASYNC) {
|
||||
*/
|
||||
void installWebApi() {
|
||||
addHttpListener();
|
||||
GET_CONFIG(string,api_secret,API::kSecret);
|
||||
|
||||
// 获取线程负载 [AUTO-TRANSLATED:3b0ece5c]
|
||||
// Get thread load
|
||||
@ -2346,10 +2381,6 @@ void installWebApi() {
|
||||
|
||||
string subnet_prefix = allArgs["subnet_prefix"];
|
||||
|
||||
// if (subnet_prefix.empty()) {
|
||||
// subnet_prefix = "192.168.1"; //default ip prefix
|
||||
// }
|
||||
|
||||
auto result = std::make_shared<Value>(std::move(val));
|
||||
auto complete_token = std::make_shared<onceToken>(nullptr, [result, headerOut, invoker]() { invoker(200, headerOut, result->toStyledString()); });
|
||||
auto lam_search = [complete_token, result](const std::map<string, string> &device_info, const std::string &onvif_url) {
|
||||
@ -2362,7 +2393,7 @@ void installWebApi() {
|
||||
//继续等待扫描
|
||||
return true;
|
||||
};
|
||||
OnvifSearcher::Instance().sendSearchBroadcast(move(subnet_prefix), std::move(lam_search), allArgs["timeout_ms"]);
|
||||
OnvifSearcher::Instance().sendSearchBroadcast(std::move(subnet_prefix), std::move(lam_search), allArgs["timeout_ms"]);
|
||||
});
|
||||
|
||||
api_regist("/index/api/getStreamUrl", [](API_ARGS_MAP_ASYNC) {
|
||||
@ -2389,6 +2420,58 @@ void installWebApi() {
|
||||
});
|
||||
});
|
||||
|
||||
api_regist("/index/api/login", [](API_ARGS_MAP) {
|
||||
auto logined_cookie = HttpCookieManager::Instance().getCookie(kLoginedCookieName, allArgs.getParser().getHeader());
|
||||
if (logined_cookie) {
|
||||
// 已经登录成功
|
||||
val["code"] = API::Success;
|
||||
val["msg"] = "You are already logined";
|
||||
return;
|
||||
}
|
||||
CHECK_ARGS("digest");
|
||||
GET_CONFIG(std::string, api_secret, API::kSecret);
|
||||
|
||||
auto unlogin_cookie = HttpCookieManager::Instance().getCookie(kUnLoginCookieName, allArgs.getParser().getHeader());
|
||||
// MD5("zlmediakit:"+${secret}+":" +${cookie})
|
||||
auto digest_ok = unlogin_cookie ? MD5("zlmediakit:" + api_secret + ":" + unlogin_cookie->getCookie()).hexdigest() : "";
|
||||
if (!unlogin_cookie || digest_ok != allArgs["digest"]) {
|
||||
if (!unlogin_cookie) {
|
||||
unlogin_cookie = HttpCookieManager::Instance().addCookie(kUnLoginCookieName, "", kUnLoginCookieLifeSeconds);
|
||||
headerOut["Set-Cookie"] = unlogin_cookie->getCookie(kLoginCookiePath);
|
||||
}
|
||||
val["cookie"] = unlogin_cookie->getCookie();
|
||||
throw AuthException("Digest does not match, incorrect secret?", headerOut, val);
|
||||
}
|
||||
// 登录成功, cookie保持24小时
|
||||
logined_cookie = HttpCookieManager::Instance().addCookie(kLoginedCookieName, "", kLoginedCookieLifeSeconds);
|
||||
headerOut["Set-Cookie"] = logined_cookie->getCookie(kLoginCookiePath);
|
||||
|
||||
// 删除未登录状态的cookie
|
||||
unlogin_cookie->setExpired();
|
||||
HttpCookieManager::Instance().delCookie(unlogin_cookie);
|
||||
headerOut.emplace_force("Set-Cookie", unlogin_cookie->getCookie(kLoginCookiePath));
|
||||
|
||||
val["code"] = API::Success;
|
||||
});
|
||||
|
||||
api_regist("/index/api/logout", [](API_ARGS_MAP) {
|
||||
auto logined_cookie = HttpCookieManager::Instance().getCookie(kLoginedCookieName, allArgs.getParser().getHeader());
|
||||
if (logined_cookie) {
|
||||
// 已经登录成功, 删除cookie
|
||||
logined_cookie->setExpired();
|
||||
HttpCookieManager::Instance().delCookie(logined_cookie);
|
||||
headerOut["Set-Cookie"] = logined_cookie->getCookie(kLoginCookiePath);
|
||||
} else {
|
||||
val["msg"] = "You are not logined";
|
||||
}
|
||||
auto unlogin_cookie = HttpCookieManager::Instance().getCookie(kUnLoginCookieName, allArgs.getParser().getHeader());
|
||||
if (!unlogin_cookie) {
|
||||
unlogin_cookie = HttpCookieManager::Instance().addCookie(kUnLoginCookieName, "", kUnLoginCookieLifeSeconds);
|
||||
headerOut["Set-Cookie"] = unlogin_cookie->getCookie(kLoginCookiePath);
|
||||
}
|
||||
val["cookie"] = unlogin_cookie->getCookie();
|
||||
});
|
||||
|
||||
#if defined(ENABLE_VIDEOSTACK) && defined(ENABLE_X264) && defined(ENABLE_FFMPEG)
|
||||
VideoStackManager::Instance().loadBgImg("novideo.yuv");
|
||||
NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastStreamNoneReader, [](BroadcastStreamNoneReaderArgs) {
|
||||
|
||||
@ -23,6 +23,8 @@
|
||||
#include "webrtc/WebRtcTransport.h"
|
||||
#endif
|
||||
|
||||
#include "Http/HttpCookieManager.h"
|
||||
|
||||
// 配置文件路径 [AUTO-TRANSLATED:8a373c2f]
|
||||
// Configuration file path
|
||||
extern std::string g_ini_file;
|
||||
@ -53,31 +55,45 @@ typedef enum {
|
||||
} ApiErr;
|
||||
|
||||
extern const std::string kSecret;
|
||||
}//namespace API
|
||||
extern const std::string kLegacyAuth;
|
||||
} // namespace API
|
||||
|
||||
class ApiRetException: public std::runtime_error {
|
||||
class ApiRetException : public std::runtime_error {
|
||||
public:
|
||||
ApiRetException(const char *str = "success" ,int code = API::Success):runtime_error(str){
|
||||
ApiRetException(const char *str = "success", int code = API::Success, mediakit::StrCaseMap headers = {}, Json::Value body = {})
|
||||
: runtime_error(str) {
|
||||
_code = code;
|
||||
_headers = std::move(headers);
|
||||
_body = std::move(body);
|
||||
}
|
||||
int code(){ return _code; }
|
||||
int code() { return _code; }
|
||||
|
||||
mediakit::StrCaseMap &getHeaders() { return _headers; }
|
||||
|
||||
Json::Value &getBody() { return _body; }
|
||||
|
||||
private:
|
||||
int _code;
|
||||
mediakit::StrCaseMap _headers;
|
||||
Json::Value _body;
|
||||
};
|
||||
|
||||
class AuthException : public ApiRetException {
|
||||
public:
|
||||
AuthException(const char *str):ApiRetException(str,API::AuthFailed){}
|
||||
AuthException(const char *str, mediakit::StrCaseMap headers = {}, Json::Value body = {})
|
||||
: ApiRetException(str, API::AuthFailed, std::move(headers), std::move(body)) {}
|
||||
};
|
||||
|
||||
class InvalidArgsException: public ApiRetException {
|
||||
class InvalidArgsException : public ApiRetException {
|
||||
public:
|
||||
InvalidArgsException(const char *str):ApiRetException(str,API::InvalidArgs){}
|
||||
InvalidArgsException(const char *str, mediakit::StrCaseMap headers = {}, Json::Value body = {})
|
||||
: ApiRetException(str, API::InvalidArgs, std::move(headers), std::move(body)) {}
|
||||
};
|
||||
|
||||
class SuccessException: public ApiRetException {
|
||||
class SuccessException : public ApiRetException {
|
||||
public:
|
||||
SuccessException():ApiRetException("success",API::Success){}
|
||||
SuccessException(mediakit::StrCaseMap headers = {}, Json::Value body = {})
|
||||
: ApiRetException("success", API::Success, std::move(headers), std::move(body)) {}
|
||||
};
|
||||
|
||||
using ApiArgsType = std::map<std::string, std::string, mediakit::StrCaseCompare>;
|
||||
@ -218,17 +234,8 @@ bool checkArgs(Args &args, const Key &key, const KeyTypes &...keys) {
|
||||
// Check whether the http parameters contain the secret key, the ip of 127.0.0.1 does not check the key
|
||||
// 同时检测是否在ip白名单内 [AUTO-TRANSLATED:d12f963d]
|
||||
// Check whether it is in the ip whitelist at the same time
|
||||
#define CHECK_SECRET() \
|
||||
do { \
|
||||
auto ip = sender.get_peer_ip(); \
|
||||
if (!HttpFileManager::isIPAllowed(ip)) { \
|
||||
throw AuthException("Your ip is not allowed to access the service."); \
|
||||
} \
|
||||
CHECK_ARGS("secret"); \
|
||||
if (api_secret != allArgs["secret"]) { \
|
||||
throw AuthException("Incorrect secret"); \
|
||||
} \
|
||||
} while(false);
|
||||
void check_secret(toolkit::SockInfo &sender, mediakit::HttpSession::KeyValue &headerOut, const ArgsMap &allArgs, Json::Value &val);
|
||||
#define CHECK_SECRET() check_secret(sender, headerOut, allArgs, val)
|
||||
|
||||
void installWebApi();
|
||||
void unInstallWebApi();
|
||||
|
||||
@ -123,18 +123,25 @@ void handle_http_request(const py::object &check_route, const py::object &submit
|
||||
}
|
||||
consumed = true;
|
||||
|
||||
Json::Value val;
|
||||
HttpSession::KeyValue headerOut;
|
||||
// http api被python拦截了,再api统一鉴权
|
||||
try {
|
||||
auto args = getAllArgs(parser);
|
||||
auto allArgs = ArgsMap(parser, args);
|
||||
GET_CONFIG(std::string, api_secret, API::kSecret);
|
||||
// TODO python http api暂不开启secret鉴权
|
||||
// CHECK_SECRET(); // 检测secret
|
||||
GET_CONFIG(bool, legacy_auth , API::kLegacyAuth);
|
||||
if (!legacy_auth) {
|
||||
// 非传统secret鉴权模式,Python接口强制要求登录鉴权
|
||||
CHECK_SECRET();
|
||||
}
|
||||
} catch (std::exception &ex) {
|
||||
Json::Value val;
|
||||
val["code"] = API::Exception;
|
||||
auto ex1 = dynamic_cast<ApiRetException *>(&ex);
|
||||
if (ex1) {
|
||||
val["code"] = ex1->code();
|
||||
} else {
|
||||
val["code"] = API::Exception;
|
||||
}
|
||||
val["msg"] = ex.what();
|
||||
HttpSession::KeyValue headerOut;
|
||||
headerOut["Content-Type"] = "application/json";
|
||||
invoker(200, headerOut, val.toStyledString());
|
||||
return;
|
||||
|
||||
@ -61,6 +61,11 @@ bool HttpServerCookie::isExpired() {
|
||||
return _ticker.elapsedTime() > _max_elapsed * 1000;
|
||||
}
|
||||
|
||||
void HttpServerCookie::setExpired() {
|
||||
_ticker.resetTime();
|
||||
_max_elapsed = 0;
|
||||
}
|
||||
|
||||
void HttpServerCookie::setAttach(toolkit::Any attach) {
|
||||
_attach = std::move(attach);
|
||||
}
|
||||
|
||||
@ -118,6 +118,11 @@ public:
|
||||
*/
|
||||
bool isExpired();
|
||||
|
||||
/**
|
||||
* 使cookie过期作废
|
||||
*/
|
||||
void setExpired();
|
||||
|
||||
/**
|
||||
* 设置附加数据
|
||||
* Set additional data
|
||||
|
||||
@ -31,7 +31,7 @@ namespace mediakit {
|
||||
// If the player does not access the cookie within 60 seconds, the hls playback authentication will be triggered again.
|
||||
static size_t kHlsCookieSecond = 60;
|
||||
static size_t kFindSrcIntervalSecond = 3;
|
||||
static const string kCookieName = "ZL_COOKIE";
|
||||
static const string kCookieName = "ZLM_HTTP_COOKIE";
|
||||
static const string kHlsSuffix = "/hls.m3u8";
|
||||
static const string kHlsFMP4Suffix = "/hls.fmp4.m3u8";
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user