diff --git a/trunk/3rdparty/srs-bench/blackbox/http_api_test.go b/trunk/3rdparty/srs-bench/blackbox/http_api_test.go new file mode 100644 index 0000000000..3a2f177879 --- /dev/null +++ b/trunk/3rdparty/srs-bench/blackbox/http_api_test.go @@ -0,0 +1,93 @@ +// The MIT License (MIT) +// +// # Copyright (c) 2023 Winlin +// +// 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. +package blackbox + +import ( + "context" + "fmt" + "github.com/ossrs/go-oryx-lib/errors" + "github.com/ossrs/go-oryx-lib/logger" + "net/http" + "sync" + "testing" + "time" +) + +func TestFast_Http_Api_Basic_Auth(t *testing.T) { + // This case is run in parallel. + t.Parallel() + + // Setup the max timeout for this case. + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + defer cancel() + + // Check a set of errors. + var r0, r1, r2, r3, r4, r5, r6 error + defer func(ctx context.Context) { + if err := filterTestError(ctx.Err(), r0, r1, r2, r3, r4, r5, r6); err != nil { + t.Errorf("Fail for err %+v", err) + } else { + logger.Tf(ctx, "test done with err %+v", err) + } + }(ctx) + + var wg sync.WaitGroup + defer wg.Wait() + + // Start SRS server and wait for it to be ready. + svr := NewSRSServer(func(v *srsServer) { + v.envs = []string{ + "SRS_HTTP_API_AUTH_ENABLED=on", + "SRS_HTTP_API_AUTH_USERNAME=admin", + "SRS_HTTP_API_AUTH_PASSWORD=admin", + } + }) + wg.Add(1) + go func() { + defer wg.Done() + r0 = svr.Run(ctx, cancel) + }() + + <-svr.ReadyCtx().Done() + + if true { + defer cancel() + + var res *http.Response + url := fmt.Sprintf("http://admin:admin@localhost:%v/api/v1/versions", svr.APIPort()) + res, r1 = http.Get(url) + if r1 == nil && res.StatusCode != 200 { + r2 = errors.Errorf("get status code=%v, expect=200", res.StatusCode) + } + + url = fmt.Sprintf("http://admin:123456@localhost:%v/api/v1/versions", svr.APIPort()) + res, r3 = http.Get(url) + if r3 == nil && res.StatusCode != 401 { + r4 = errors.Errorf("get status code=%v, expect=401", res.StatusCode) + } + + url = fmt.Sprintf("http://localhost:%v/api/v1/versions", svr.APIPort()) + res, r5 = http.Get(url) + if r5 == nil && res.StatusCode != 401 { + r6 = errors.Errorf("get status code=%v, expect=401", res.StatusCode) + } + } +} diff --git a/trunk/3rdparty/srs-bench/blackbox/util.go b/trunk/3rdparty/srs-bench/blackbox/util.go index 88c175f954..cc6f2a317d 100644 --- a/trunk/3rdparty/srs-bench/blackbox/util.go +++ b/trunk/3rdparty/srs-bench/blackbox/util.go @@ -329,7 +329,7 @@ func (v *backendService) Run(ctx context.Context, cancel context.CancelFunc) err } select { - case <- ctx.Done(): + case <-ctx.Done(): case <-time.After(v.duration): logger.Tf(ctx, "Process killed duration=%v, pid=%v, name=%v, args=%v", v.duration, v.pid, v.name, v.args) cmd.Process.Kill() @@ -418,6 +418,8 @@ type SRSServer interface { RTMPPort() int // HTTPPort is the HTTP stream port. HTTPPort() int + // APIPort is the HTTP API port. + APIPort() int // SRTPort is the SRT UDP port. SRTPort() int } @@ -510,6 +512,10 @@ func (v *srsServer) HTTPPort() int { return v.httpListen } +func (v *srsServer) APIPort() int { + return v.apiListen +} + func (v *srsServer) SRTPort() int { return v.srtListen } @@ -524,7 +530,7 @@ func (v *srsServer) Run(ctx context.Context, cancel context.CancelFunc) error { ) // Create directories. - if err := os.MkdirAll(path.Join(v.workDir, "./objs/nginx/html"), os.FileMode(0755) | os.ModeDir); err != nil { + if err := os.MkdirAll(path.Join(v.workDir, "./objs/nginx/html"), os.FileMode(0755)|os.ModeDir); err != nil { return errors.Wrapf(err, "SRS create directory %v", path.Join(v.workDir, "./objs/nginx/html")) } @@ -657,7 +663,7 @@ type ffmpegClient struct { func NewFFmpeg(opts ...func(v *ffmpegClient)) FFmpegClient { v := &ffmpegClient{ - process: newBackendService(), + process: newBackendService(), cancelCaseWhenQuit: true, } @@ -701,7 +707,7 @@ func (v *ffmpegClient) Run(ctx context.Context, cancel context.CancelFunc) error ffCtx, ffCancel := context.WithCancel(ctx) go func() { select { - case <- ctx.Done(): + case <-ctx.Done(): case <-ffCtx.Done(): if v.cancelCaseWhenQuit { cancel() @@ -1144,17 +1150,17 @@ func (v *HooksEventBase) HookAction() string { type HooksEventOnDvr struct { HooksEventBase - Stream string `json:"stream"` + Stream string `json:"stream"` StreamUrl string `json:"stream_url"` - StreamID string `json:"stream_id"` - CWD string `json:"cwd"` - File string `json:"file"` - TcUrl string `json:"tcUrl"` - App string `json:"app"` - Vhost string `json:"vhost"` - IP string `json:"ip"` - ClientIP string `json:"client_id"` - ServerID string `json:"server_id"` + StreamID string `json:"stream_id"` + CWD string `json:"cwd"` + File string `json:"file"` + TcUrl string `json:"tcUrl"` + App string `json:"app"` + Vhost string `json:"vhost"` + IP string `json:"ip"` + ClientIP string `json:"client_id"` + ServerID string `json:"server_id"` } type HooksService interface { @@ -1171,7 +1177,7 @@ type hooksService struct { httpPort int dispose func() - r0 error + r0 error hooksOnDvr chan HooksEvent } diff --git a/trunk/conf/full.conf b/trunk/conf/full.conf index e3cd0f39c9..352f039203 100644 --- a/trunk/conf/full.conf +++ b/trunk/conf/full.conf @@ -203,6 +203,19 @@ http_api { # Always off by https://github.com/ossrs/srs/issues/2653 #allow_update off; } + # the auth is authentication for http api + auth { + # whether enable the HTTP AUTH. + # Overwrite by env SRS_HTTP_API_AUTH_ENABLED + # default: off + enabled on; + # The username of Basic authentication: + # Overwrite by env SRS_HTTP_API_AUTH_USERNAME + username admin; + # The password of Basic authentication: + # Overwrite by env SRS_HTTP_API_AUTH_PASSWORD + password admin; + } # For https_api or HTTPS API. https { # Whether enable HTTPS API. diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md index 326f84b889..bebc38b065 100644 --- a/trunk/doc/CHANGELOG.md +++ b/trunk/doc/CHANGELOG.md @@ -8,6 +8,7 @@ The changelog for SRS. ## SRS 6.0 Changelog +* v5.0, 2023-04-01, Merge [#3458](https://github.com/ossrs/srs/pull/3450): API: Support HTTP basic authentication for API. v6.0.40 (#3458) * v6.0, 2023-03-27, Merge [#3450](https://github.com/ossrs/srs/pull/3450): WebRTC: Error message carries the SDP when failed. v6.0.39 (#3450) * v6.0, 2023-03-25, Merge [#3477](https://github.com/ossrs/srs/pull/3477): Remove unneccessary NULL check in srs_freep. v6.0.38 (#3477) * v6.0, 2023-03-25, Merge [#3455](https://github.com/ossrs/srs/pull/3455): RTC: Call on_play before create session, for it might be freed for timeout. v6.0.37 (#3455) @@ -53,6 +54,7 @@ The changelog for SRS. ## SRS 5.0 Changelog +* v5.0, 2023-04-01, Merge [#3458](https://github.com/ossrs/srs/pull/3450): API: Support HTTP basic authentication for API. v5.0.152 (#3458) * v5.0, 2023-03-27, Merge [#3450](https://github.com/ossrs/srs/pull/3450): WebRTC: Error message carries the SDP when failed. v5.0.151 (#3450) * v5.0, 2023-03-25, Merge [#3477](https://github.com/ossrs/srs/pull/3477): Remove unneccessary NULL check in srs_freep. v5.0.150 (#3477) * v5.0, 2023-03-25, Merge [#3455](https://github.com/ossrs/srs/pull/3455): RTC: Call on_play before create session, for it might be freed for timeout. v5.0.149 (#3455) diff --git a/trunk/src/app/srs_app_config.cpp b/trunk/src/app/srs_app_config.cpp index 3a0719905c..5fb625667c 100644 --- a/trunk/src/app/srs_app_config.cpp +++ b/trunk/src/app/srs_app_config.cpp @@ -2284,7 +2284,7 @@ srs_error_t SrsConfig::check_normal_config() for (int i = 0; conf && i < (int)conf->directives.size(); i++) { SrsConfDirective* obj = conf->at(i); string n = obj->name; - if (n != "enabled" && n != "listen" && n != "crossdomain" && n != "raw_api" && n != "https") { + if (n != "enabled" && n != "listen" && n != "crossdomain" && n != "raw_api" && n != "auth" && n != "https") { return srs_error_new(ERROR_SYSTEM_CONFIG_INVALID, "illegal http_api.%s", n.c_str()); } @@ -2296,6 +2296,15 @@ srs_error_t SrsConfig::check_normal_config() } } } + + if (n == "auth") { + for (int j = 0; j < (int)obj->directives.size(); j++) { + string m = obj->at(j)->name; + if (m != "enabled" && m != "username" && m != "password") { + return srs_error_new(ERROR_SYSTEM_CONFIG_INVALID, "illegal http_api.auth.%s", m.c_str()); + } + } + } } } if (true) { @@ -7605,6 +7614,78 @@ bool SrsConfig::get_raw_api_allow_update() return false; } +bool SrsConfig::get_http_api_auth_enabled() +{ + SRS_OVERWRITE_BY_ENV_BOOL("srs.http_api.auth.enabled"); // SRS_HTTP_API_AUTH_ENABLED + + static bool DEFAULT = false; + + SrsConfDirective* conf = root->get("http_api"); + if (!conf) { + return DEFAULT; + } + + conf = conf->get("auth"); + if (!conf) { + return DEFAULT; + } + + conf = conf->get("enabled"); + if (!conf || conf->arg0().empty()) { + return DEFAULT; + } + + return SRS_CONF_PERFER_FALSE(conf->arg0()); +} + +std::string SrsConfig::get_http_api_auth_username() +{ + SRS_OVERWRITE_BY_ENV_STRING("srs.http_api.auth.username"); // SRS_HTTP_API_AUTH_USERNAME + + static string DEFAULT = ""; + + SrsConfDirective* conf = root->get("http_api"); + if (!conf) { + return DEFAULT; + } + + conf = conf->get("auth"); + if (!conf) { + return DEFAULT; + } + + conf = conf->get("username"); + if (!conf) { + return DEFAULT; + } + + return conf->arg0(); +} + +std::string SrsConfig::get_http_api_auth_password() +{ + SRS_OVERWRITE_BY_ENV_STRING("srs.http_api.auth.password"); // SRS_HTTP_API_AUTH_PASSWORD + + static string DEFAULT = ""; + + SrsConfDirective* conf = root->get("http_api"); + if (!conf) { + return DEFAULT; + } + + conf = conf->get("auth"); + if (!conf) { + return DEFAULT; + } + + conf = conf->get("password"); + if (!conf) { + return DEFAULT; + } + + return conf->arg0(); +} + SrsConfDirective* SrsConfig::get_https_api() { SrsConfDirective* conf = root->get("http_api"); diff --git a/trunk/src/app/srs_app_config.hpp b/trunk/src/app/srs_app_config.hpp index f0028b357b..87e9b170d6 100644 --- a/trunk/src/app/srs_app_config.hpp +++ b/trunk/src/app/srs_app_config.hpp @@ -1021,6 +1021,12 @@ class SrsConfig virtual bool get_raw_api_allow_query(); // Whether allow rpc update. virtual bool get_raw_api_allow_update(); + // Whether http api auth enabled. + virtual bool get_http_api_auth_enabled(); + // Get the http api auth username. + virtual std::string get_http_api_auth_username(); + // Get the http api auth password. + virtual std::string get_http_api_auth_password(); // https api section private: SrsConfDirective* get_https_api(); diff --git a/trunk/src/app/srs_app_http_conn.cpp b/trunk/src/app/srs_app_http_conn.cpp index 31aeaa3b1d..b040bb79a9 100644 --- a/trunk/src/app/srs_app_http_conn.cpp +++ b/trunk/src/app/srs_app_http_conn.cpp @@ -54,7 +54,9 @@ ISrsHttpConnOwner::~ISrsHttpConnOwner() SrsHttpConn::SrsHttpConn(ISrsHttpConnOwner* handler, ISrsProtocolReadWriter* fd, ISrsHttpServeMux* m, string cip, int cport) { parser = new SrsHttpParser(); - cors = new SrsHttpCorsMux(); + auth = new SrsHttpAuthMux(m); + cors = new SrsHttpCorsMux(auth); + http_mux = m; handler_ = handler; @@ -74,6 +76,7 @@ SrsHttpConn::~SrsHttpConn() srs_freep(parser); srs_freep(cors); + srs_freep(auth); srs_freep(delta_); } @@ -227,10 +230,10 @@ srs_error_t SrsHttpConn::process_request(ISrsHttpResponseWriter* w, ISrsHttpMess srs_trace("HTTP #%d %s:%d %s %s, content-length=%" PRId64 "", rid, ip.c_str(), port, r->method_str().c_str(), r->url().c_str(), r->content_length()); - - // use cors server mux to serve http request, which will proxy to http_remux. + + // proxy to cors-->auth-->http_remux. if ((err = cors->serve_http(w, r)) != srs_success) { - return srs_error_wrap(err, "mux serve"); + return srs_error_wrap(err, "cors serve"); } return err; @@ -256,14 +259,27 @@ srs_error_t SrsHttpConn::set_crossdomain_enabled(bool v) { srs_error_t err = srs_success; - // initialize the cors, which will proxy to mux. - if ((err = cors->initialize(http_mux, v)) != srs_success) { + if ((err = cors->initialize(v)) != srs_success) { return srs_error_wrap(err, "init cors"); } return err; } +srs_error_t SrsHttpConn::set_auth_enabled(bool auth_enabled) +{ + srs_error_t err = srs_success; + + // initialize the auth, which will proxy to mux. + if ((err = auth->initialize(auth_enabled, + _srs_config->get_http_api_auth_username(), + _srs_config->get_http_api_auth_password())) != srs_success) { + return srs_error_wrap(err, "init auth"); + } + + return err; +} + srs_error_t SrsHttpConn::set_jsonp(bool v) { parser->set_jsonp(v); @@ -451,6 +467,11 @@ srs_error_t SrsHttpxConn::start() return srs_error_wrap(err, "set cors=%d", v); } + bool auth_enabled = _srs_config->get_http_api_auth_enabled(); + if ((err = conn->set_auth_enabled(auth_enabled)) != srs_success) { + return srs_error_wrap(err, "set auth"); + } + return conn->start(); } diff --git a/trunk/src/app/srs_app_http_conn.hpp b/trunk/src/app/srs_app_http_conn.hpp index 4c0c20ff20..7c33b1c8b8 100644 --- a/trunk/src/app/srs_app_http_conn.hpp +++ b/trunk/src/app/srs_app_http_conn.hpp @@ -67,6 +67,7 @@ class SrsHttpConn : public ISrsConnection, public ISrsStartable, public ISrsCoro SrsHttpParser* parser; ISrsHttpServeMux* http_mux; SrsHttpCorsMux* cors; + SrsHttpAuthMux* auth; ISrsHttpConnOwner* handler_; protected: ISrsProtocolReadWriter* skt; @@ -111,6 +112,8 @@ class SrsHttpConn : public ISrsConnection, public ISrsStartable, public ISrsCoro virtual srs_error_t pull(); // Whether enable the CORS(cross-domain). virtual srs_error_t set_crossdomain_enabled(bool v); + // Whether enable the Auth. + virtual srs_error_t set_auth_enabled(bool auth_enabled); // Whether enable the JSONP. virtual srs_error_t set_jsonp(bool v); // Interface ISrsConnection. diff --git a/trunk/src/core/srs_core_version5.hpp b/trunk/src/core/srs_core_version5.hpp index 2231275639..97bfe7db55 100644 --- a/trunk/src/core/srs_core_version5.hpp +++ b/trunk/src/core/srs_core_version5.hpp @@ -9,6 +9,6 @@ #define VERSION_MAJOR 5 #define VERSION_MINOR 0 -#define VERSION_REVISION 151 +#define VERSION_REVISION 152 #endif diff --git a/trunk/src/core/srs_core_version6.hpp b/trunk/src/core/srs_core_version6.hpp index 5971215ea6..4082877c12 100644 --- a/trunk/src/core/srs_core_version6.hpp +++ b/trunk/src/core/srs_core_version6.hpp @@ -9,6 +9,6 @@ #define VERSION_MAJOR 6 #define VERSION_MINOR 0 -#define VERSION_REVISION 39 +#define VERSION_REVISION 40 #endif diff --git a/trunk/src/protocol/srs_protocol_http_stack.cpp b/trunk/src/protocol/srs_protocol_http_stack.cpp index c00ba0783a..56e1532d99 100644 --- a/trunk/src/protocol/srs_protocol_http_stack.cpp +++ b/trunk/src/protocol/srs_protocol_http_stack.cpp @@ -24,6 +24,9 @@ using namespace std; // @see ISrsHttpMessage._http_ts_send_buffer #define SRS_HTTP_TS_SEND_BUFFER_SIZE 4096 +#define SRS_HTTP_AUTH_SCHEME_BASIC "Basic" +#define SRS_HTTP_AUTH_PREFIX_BASIC SRS_HTTP_AUTH_SCHEME_BASIC " " + // get the status text of code. string srs_generate_http_status_text(int status) { @@ -861,22 +864,20 @@ bool SrsHttpServeMux::path_match(string pattern, string path) return false; } -SrsHttpCorsMux::SrsHttpCorsMux() +SrsHttpCorsMux::SrsHttpCorsMux(ISrsHttpHandler* h) { - next = NULL; enabled = false; required = false; + next_ = h; } SrsHttpCorsMux::~SrsHttpCorsMux() { } -srs_error_t SrsHttpCorsMux::initialize(ISrsHttpServeMux* worker, bool cros_enabled) +srs_error_t SrsHttpCorsMux::initialize(bool cros_enabled) { - next = worker; enabled = cros_enabled; - return srs_success; } @@ -920,9 +921,89 @@ srs_error_t SrsHttpCorsMux::serve_http(ISrsHttpResponseWriter* w, ISrsHttpMessag } return w->final_request(); } - - srs_assert(next); - return next->serve_http(w, r); + + return next_->serve_http(w, r); +} + +SrsHttpAuthMux::SrsHttpAuthMux(ISrsHttpHandler* h) +{ + next_ = h; + enabled_ = false; +} + +SrsHttpAuthMux::~SrsHttpAuthMux() +{ +} + +srs_error_t SrsHttpAuthMux::initialize(bool enabled, std::string username, std::string password) +{ + enabled_ = enabled; + username_ = username; + password_ = password; + + return srs_success; +} + +srs_error_t SrsHttpAuthMux::serve_http(ISrsHttpResponseWriter* w, ISrsHttpMessage* r) +{ + srs_error_t err; + if ((err = do_auth(w, r)) != srs_success) { + srs_error("do_auth %s", srs_error_desc(err).c_str()); + srs_freep(err); + w->write_header(SRS_CONSTS_HTTP_Unauthorized); + return w->final_request(); + } + + srs_assert(next_); + return next_->serve_http(w, r); +} + +srs_error_t SrsHttpAuthMux::do_auth(ISrsHttpResponseWriter* w, ISrsHttpMessage* r) +{ + srs_error_t err = srs_success; + + if (!enabled_) { + return err; + } + + // We only apply for api starts with /api/ for HTTP API. + // We don't apply for other apis such as /rtc/, for which we use http callback. + if (r->path().find("/api/") == std::string::npos) { + return err; + } + + std::string auth = r->header()->get("Authorization"); + if (auth.empty()) { + w->header()->set("WWW-Authenticate", SRS_HTTP_AUTH_SCHEME_BASIC); + return srs_error_new(SRS_CONSTS_HTTP_Unauthorized, "empty Authorization"); + } + + if (!srs_string_contains(auth, SRS_HTTP_AUTH_PREFIX_BASIC)) { + return srs_error_new(SRS_CONSTS_HTTP_Unauthorized, "invalid auth %s, should start with %s", auth.c_str(), SRS_HTTP_AUTH_PREFIX_BASIC); + } + + std::string token = srs_erase_first_substr(auth, SRS_HTTP_AUTH_PREFIX_BASIC); + if (token.empty()) { + return srs_error_new(SRS_CONSTS_HTTP_Unauthorized, "empty token from auth %s", auth.c_str()); + } + + std::string plaintext; + if ((err = srs_av_base64_decode(token, plaintext)) != srs_success) { + return srs_error_wrap(err, "decode token %s", token.c_str()); + } + + // The token format must be username:password + std::vector user_pwd = srs_string_split(plaintext, ":"); + if (user_pwd.size() != 2) { + return srs_error_new(SRS_CONSTS_HTTP_Unauthorized, "invalid token %s", plaintext.c_str()); + } + + if (username_ != user_pwd[0] || password_ != user_pwd[1]) { + w->header()->set("WWW-Authenticate", SRS_HTTP_AUTH_SCHEME_BASIC); + return srs_error_new(SRS_CONSTS_HTTP_Unauthorized, "invalid token %s:%s", user_pwd[0].c_str(), user_pwd[1].c_str()); + } + + return err; } ISrsHttpMessage::ISrsHttpMessage() diff --git a/trunk/src/protocol/srs_protocol_http_stack.hpp b/trunk/src/protocol/srs_protocol_http_stack.hpp index b0e5154520..4cb6eee7e8 100644 --- a/trunk/src/protocol/srs_protocol_http_stack.hpp +++ b/trunk/src/protocol/srs_protocol_http_stack.hpp @@ -482,22 +482,44 @@ class SrsHttpServeMux : public ISrsHttpServeMux virtual bool path_match(std::string pattern, std::string path); }; -// The filter http mux, directly serve the http CORS requests, -// while proxy to the worker mux for services. +// The filter http mux, directly serve the http CORS requests class SrsHttpCorsMux : public ISrsHttpHandler { private: bool required; bool enabled; - ISrsHttpServeMux* next; + ISrsHttpHandler* next_; public: - SrsHttpCorsMux(); + SrsHttpCorsMux(ISrsHttpHandler* h); virtual ~SrsHttpCorsMux(); public: - virtual srs_error_t initialize(ISrsHttpServeMux* worker, bool cros_enabled); + virtual srs_error_t initialize(bool cros_enabled); +// Interface ISrsHttpServeMux +public: + virtual srs_error_t serve_http(ISrsHttpResponseWriter* w, ISrsHttpMessage* r); +}; + +// The filter http mux, directly serve the http AUTH requests, +// while proxy to the worker mux for services. +// @see https://www.rfc-editor.org/rfc/rfc7617 +// @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate +class SrsHttpAuthMux : public ISrsHttpHandler +{ +private: + bool enabled_; + std::string username_; + std::string password_; + ISrsHttpHandler* next_; +public: + SrsHttpAuthMux(ISrsHttpHandler* h); + virtual ~SrsHttpAuthMux(); +public: + virtual srs_error_t initialize(bool enabled, std::string username, std::string password); // Interface ISrsHttpServeMux public: virtual srs_error_t serve_http(ISrsHttpResponseWriter* w, ISrsHttpMessage* r); +private: + virtual srs_error_t do_auth(ISrsHttpResponseWriter* w, ISrsHttpMessage* r); }; // A Request represents an HTTP request received by a server diff --git a/trunk/src/utest/srs_utest_config.cpp b/trunk/src/utest/srs_utest_config.cpp index 6bf13be5d7..ebc0d4a04a 100644 --- a/trunk/src/utest/srs_utest_config.cpp +++ b/trunk/src/utest/srs_utest_config.cpp @@ -3639,7 +3639,7 @@ VOID TEST(ConfigMainTest, CheckVhostConfig5) if (true) { MockSrsConfig conf; - HELPER_ASSERT_SUCCESS(conf.parse(_MIN_OK_CONF "http_api{enabled on;listen xxx;crossdomain off;raw_api {enabled on;allow_reload on;allow_query on;allow_update on;}}")); + HELPER_ASSERT_SUCCESS(conf.parse(_MIN_OK_CONF "http_api{enabled on;listen xxx;crossdomain off;auth {enabled on;username admin;password 123456;}raw_api {enabled on;allow_reload on;allow_query on;allow_update on;}}")); EXPECT_TRUE(conf.get_http_api_enabled()); EXPECT_STREQ("xxx", conf.get_http_api_listen().c_str()); EXPECT_FALSE(conf.get_http_api_crossdomain()); @@ -3647,6 +3647,9 @@ VOID TEST(ConfigMainTest, CheckVhostConfig5) EXPECT_TRUE(conf.get_raw_api_allow_reload()); EXPECT_FALSE(conf.get_raw_api_allow_query()); // Always disabled EXPECT_FALSE(conf.get_raw_api_allow_update()); // Always disabled + EXPECT_TRUE(conf.get_http_api_auth_enabled()); + EXPECT_STREQ("admin", conf.get_http_api_auth_username().c_str()); + EXPECT_STREQ("123456", conf.get_http_api_auth_password().c_str()); } if (true) { @@ -4112,6 +4115,15 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesHttpApi) SrsSetEnvConfig(http_api_crossdomain, "SRS_HTTP_API_CROSSDOMAIN", "off"); EXPECT_FALSE(conf.get_http_api_crossdomain()); + + SrsSetEnvConfig(http_api_auth_enabled, "SRS_HTTP_API_AUTH_ENABLED", "on"); + EXPECT_TRUE(conf.get_http_api_auth_enabled()); + + SrsSetEnvConfig(http_api_auth_username, "SRS_HTTP_API_AUTH_USERNAME", "admin"); + EXPECT_STREQ("admin", conf.get_http_api_auth_username().c_str()); + + SrsSetEnvConfig(http_api_auth_password, "SRS_HTTP_API_AUTH_PASSWORD", "123456"); + EXPECT_STREQ("123456", conf.get_http_api_auth_password().c_str()); } if (true) { diff --git a/trunk/src/utest/srs_utest_http.cpp b/trunk/src/utest/srs_utest_http.cpp index 7aeb80d150..ba1c344972 100644 --- a/trunk/src/utest/srs_utest_http.cpp +++ b/trunk/src/utest/srs_utest_http.cpp @@ -1059,11 +1059,10 @@ VOID TEST(ProtocolHTTPTest, HTTPServerMuxerCORS) r.set_basic(HTTP_REQUEST, HTTP_POST, (http_status)200, -1); HELPER_ASSERT_SUCCESS(r.set_url("/index.html", false)); - SrsHttpCorsMux cs; - HELPER_ASSERT_SUCCESS(cs.initialize(&s, true)); + SrsHttpCorsMux cs(&s); + HELPER_ASSERT_SUCCESS(cs.initialize(true)); HELPER_ASSERT_SUCCESS(cs.serve_http(&w, &r)); - __MOCK_HTTP_EXPECT_STREQ(200, "Hello, world!", w); } // If CORS enabled, response OPTIONS with ok @@ -1079,8 +1078,8 @@ VOID TEST(ProtocolHTTPTest, HTTPServerMuxerCORS) r.set_basic(HTTP_REQUEST, HTTP_OPTIONS, (http_status)200, -1); HELPER_ASSERT_SUCCESS(r.set_url("/index.html", false)); - SrsHttpCorsMux cs; - HELPER_ASSERT_SUCCESS(cs.initialize(&s, true)); + SrsHttpCorsMux cs(&s); + HELPER_ASSERT_SUCCESS(cs.initialize(true)); HELPER_ASSERT_SUCCESS(cs.serve_http(&w, &r)); __MOCK_HTTP_EXPECT_STREQ(200, "", w); @@ -1099,11 +1098,10 @@ VOID TEST(ProtocolHTTPTest, HTTPServerMuxerCORS) r.set_basic(HTTP_REQUEST, HTTP_POST, (http_status)200, -1); HELPER_ASSERT_SUCCESS(r.set_url("/index.html", false)); - SrsHttpCorsMux cs; - HELPER_ASSERT_SUCCESS(cs.initialize(&s, false)); + SrsHttpCorsMux cs(&s); + HELPER_ASSERT_SUCCESS(cs.initialize(false)); HELPER_ASSERT_SUCCESS(cs.serve_http(&w, &r)); - __MOCK_HTTP_EXPECT_STREQ(200, "Hello, world!", w); } // If CORS not enabled, response error for options. @@ -1119,8 +1117,8 @@ VOID TEST(ProtocolHTTPTest, HTTPServerMuxerCORS) r.set_basic(HTTP_REQUEST, HTTP_OPTIONS, (http_status)200, -1); HELPER_ASSERT_SUCCESS(r.set_url("/index.html", false)); - SrsHttpCorsMux cs; - HELPER_ASSERT_SUCCESS(cs.initialize(&s, false)); + SrsHttpCorsMux cs(&s); + HELPER_ASSERT_SUCCESS(cs.initialize(false)); HELPER_ASSERT_SUCCESS(cs.serve_http(&w, &r)); __MOCK_HTTP_EXPECT_STREQ(405, "", w); @@ -1137,10 +1135,253 @@ VOID TEST(ProtocolHTTPTest, HTTPServerMuxerCORS) SrsHttpMessage r(NULL, NULL); HELPER_ASSERT_SUCCESS(r.set_url("/index.html", false)); - SrsHttpCorsMux cs; - HELPER_ASSERT_SUCCESS(cs.initialize(&s, true)); + SrsHttpCorsMux cs(&s); + HELPER_ASSERT_SUCCESS(cs.initialize(true)); HELPER_ASSERT_SUCCESS(cs.serve_http(&w, &r)); + } +} + +VOID TEST(ProtocolHTTPTest, HTTPServerMuxerAuth) +{ + srs_error_t err; + + if (true) { + SrsHttpServeMux s; + HELPER_ASSERT_SUCCESS(s.initialize()); + + MockHttpHandler* hroot = new MockHttpHandler("Hello, world!"); + HELPER_ASSERT_SUCCESS(s.handle("/", hroot)); + + MockResponseWriter w; + SrsHttpMessage r(NULL, NULL); + r.set_basic(HTTP_REQUEST, HTTP_POST, (http_status)200, -1); + + SrsHttpHeader h ; + h.set("Authorization", "Basic YWRtaW46YWRtaW4="); // admin:admin + r.set_header(&h, false); + + HELPER_ASSERT_SUCCESS(r.set_url("/index.html", false)); + + SrsHttpAuthMux auth(&s); + HELPER_ASSERT_SUCCESS(auth.initialize(true, "admin", "admin")); + + HELPER_ASSERT_SUCCESS(auth.serve_http(&w, &r)); + __MOCK_HTTP_EXPECT_STREQ(200, "Hello, world!", w); + } + + // incorrect token + if (true) { + SrsHttpServeMux s; + HELPER_ASSERT_SUCCESS(s.initialize()); + + MockHttpHandler* hroot = new MockHttpHandler("Hello, world!"); + HELPER_ASSERT_SUCCESS(s.handle("/", hroot)); + + MockResponseWriter w; + SrsHttpMessage r(NULL, NULL); + r.set_basic(HTTP_REQUEST, HTTP_POST, (http_status)200, -1); + + SrsHttpHeader h ; + h.set("Authorization", "Basic YWRtaW46YWRtaW4="); // admin:admin + r.set_header(&h, false); + + HELPER_ASSERT_SUCCESS(r.set_url("/api/v1/clients/", false)); + + SrsHttpAuthMux auth(&s); + HELPER_ASSERT_SUCCESS(auth.initialize(true, "admin", "123456")); + + HELPER_ASSERT_SUCCESS(auth.serve_http(&w, &r)); + EXPECT_EQ(401, w.w->status); + } + + // incorrect token, duplicate Basic + if (true) { + SrsHttpServeMux s; + HELPER_ASSERT_SUCCESS(s.initialize()); + + MockHttpHandler* hroot = new MockHttpHandler("Hello, world!"); + HELPER_ASSERT_SUCCESS(s.handle("/", hroot)); + + MockResponseWriter w; + SrsHttpMessage r(NULL, NULL); + r.set_basic(HTTP_REQUEST, HTTP_POST, (http_status)200, -1); + + SrsHttpHeader h ; + h.set("Authorization", "Basic BasicYWRtaW46YWRtaW4="); // duplicate 'Basic' + r.set_header(&h, false); + + HELPER_ASSERT_SUCCESS(r.set_url("/api/v1/clients/", false)); + + SrsHttpAuthMux auth(&s); + HELPER_ASSERT_SUCCESS(auth.initialize(true, "admin", "admin")); + + HELPER_ASSERT_SUCCESS(auth.serve_http(&w, &r)); + EXPECT_EQ(401, w.w->status); + } + + // Authorization NOT start with 'Basic ' + if (true) { + SrsHttpServeMux s; + HELPER_ASSERT_SUCCESS(s.initialize()); + + MockHttpHandler* hroot = new MockHttpHandler("Hello, world!"); + HELPER_ASSERT_SUCCESS(s.handle("/", hroot)); + + MockResponseWriter w; + SrsHttpMessage r(NULL, NULL); + r.set_basic(HTTP_REQUEST, HTTP_POST, (http_status)200, -1); + + SrsHttpHeader h ; + h.set("Authorization", "YWRtaW46YWRtaW4="); // admin:admin + r.set_header(&h, false); + + HELPER_ASSERT_SUCCESS(r.set_url("/api/v1/clients/", false)); + + SrsHttpAuthMux auth(&s); + HELPER_ASSERT_SUCCESS(auth.initialize(true, "admin", "admin")); + + HELPER_ASSERT_SUCCESS(auth.serve_http(&w, &r)); + EXPECT_EQ(401, w.w->status); + } + + // NOT base64 + if (true) { + SrsHttpServeMux s; + HELPER_ASSERT_SUCCESS(s.initialize()); + + MockHttpHandler* hroot = new MockHttpHandler("Hello, world!"); + HELPER_ASSERT_SUCCESS(s.handle("/", hroot)); + + MockResponseWriter w; + SrsHttpMessage r(NULL, NULL); + r.set_basic(HTTP_REQUEST, HTTP_POST, (http_status)200, -1); + + SrsHttpHeader h ; + h.set("Authorization", "Basic admin:admin"); // admin:admin + r.set_header(&h, false); + + HELPER_ASSERT_SUCCESS(r.set_url("/api/v1/clients/", false)); + + SrsHttpAuthMux auth(&s); + HELPER_ASSERT_SUCCESS(auth.initialize(true, "admin", "admin")); + + HELPER_ASSERT_SUCCESS(auth.serve_http(&w, &r)); + EXPECT_EQ(401, w.w->status); + } + + // empty Authorization + if (true) { + SrsHttpServeMux s; + HELPER_ASSERT_SUCCESS(s.initialize()); + + MockHttpHandler* hroot = new MockHttpHandler("Hello, world!"); + HELPER_ASSERT_SUCCESS(s.handle("/", hroot)); + + MockResponseWriter w; + SrsHttpMessage r(NULL, NULL); + r.set_basic(HTTP_REQUEST, HTTP_POST, (http_status)200, -1); + HELPER_ASSERT_SUCCESS(r.set_url("/api/v1/clients/", false)); + + SrsHttpAuthMux auth(&s); + HELPER_ASSERT_SUCCESS(auth.initialize(true, "admin", "admin")); + + HELPER_ASSERT_SUCCESS(auth.serve_http(&w, &r)); + EXPECT_EQ(401, w.w->status); + } + + // auth disabled, response with 200 ok, even though empty Authorization + if (true) { + SrsHttpServeMux s; + HELPER_ASSERT_SUCCESS(s.initialize()); + + MockHttpHandler* hroot = new MockHttpHandler("Hello, world!"); + HELPER_ASSERT_SUCCESS(s.handle("/", hroot)); + + MockResponseWriter w; + SrsHttpMessage r(NULL, NULL); + r.set_basic(HTTP_REQUEST, HTTP_POST, (http_status)200, -1); + HELPER_ASSERT_SUCCESS(r.set_url("/api/v1/clients/", false)); + + SrsHttpAuthMux auth(&s); + HELPER_ASSERT_SUCCESS(auth.initialize(false, "admin", "admin")); + + HELPER_ASSERT_SUCCESS(auth.serve_http(&w, &r)); + __MOCK_HTTP_EXPECT_STREQ(200, "Hello, world!", w); + } + + // auth disabled, response with 200 ok, even though wrong token + if (true) { + SrsHttpServeMux s; + HELPER_ASSERT_SUCCESS(s.initialize()); + + MockHttpHandler* hroot = new MockHttpHandler("Hello, world!"); + HELPER_ASSERT_SUCCESS(s.handle("/", hroot)); + + MockResponseWriter w; + SrsHttpMessage r(NULL, NULL); + r.set_basic(HTTP_REQUEST, HTTP_POST, (http_status)200, -1); + + SrsHttpHeader h ; + h.set("Authorization", "Basic YWRtaW46YWRtaW4="); // admin:admin + r.set_header(&h, false); + + HELPER_ASSERT_SUCCESS(r.set_url("/api/v1/clients/", false)); + + SrsHttpAuthMux auth(&s); + HELPER_ASSERT_SUCCESS(auth.initialize(false, "admin", "123456")); + + HELPER_ASSERT_SUCCESS(auth.serve_http(&w, &r)); + __MOCK_HTTP_EXPECT_STREQ(200, "Hello, world!", w); + } + + // always response with 200 ok, for /rtc/*/ + if (true) { + SrsHttpServeMux s; + HELPER_ASSERT_SUCCESS(s.initialize()); + + MockHttpHandler* hroot = new MockHttpHandler("Hello, world!"); + HELPER_ASSERT_SUCCESS(s.handle("/", hroot)); + + MockResponseWriter w; + SrsHttpMessage r(NULL, NULL); + r.set_basic(HTTP_REQUEST, HTTP_POST, (http_status)200, -1); + + SrsHttpHeader h ; + h.set("Authorization", "Basic YWRtaW46YWRtaW4="); // admin:admin + r.set_header(&h, false); + + HELPER_ASSERT_SUCCESS(r.set_url("/rtc/play/", false)); + + SrsHttpAuthMux auth(&s); + HELPER_ASSERT_SUCCESS(auth.initialize(false, "admin", "123456")); + + HELPER_ASSERT_SUCCESS(auth.serve_http(&w, &r)); + __MOCK_HTTP_EXPECT_STREQ(200, "Hello, world!", w); + } + + // always response with 200 ok, for /rtc/*/ + if (true) { + SrsHttpServeMux s; + HELPER_ASSERT_SUCCESS(s.initialize()); + + MockHttpHandler* hroot = new MockHttpHandler("Hello, world!"); + HELPER_ASSERT_SUCCESS(s.handle("/", hroot)); + + MockResponseWriter w; + SrsHttpMessage r(NULL, NULL); + r.set_basic(HTTP_REQUEST, HTTP_POST, (http_status)200, -1); + + SrsHttpHeader h ; + h.set("Authorization", "Basic YWRtaW46YWRtaW4="); // admin:admin + r.set_header(&h, false); + + HELPER_ASSERT_SUCCESS(r.set_url("/index.html", false)); + + SrsHttpAuthMux auth(&s); + HELPER_ASSERT_SUCCESS(auth.initialize(false, "admin", "123456")); + + HELPER_ASSERT_SUCCESS(auth.serve_http(&w, &r)); __MOCK_HTTP_EXPECT_STREQ(200, "Hello, world!", w); } } diff --git a/trunk/src/utest/srs_utest_kernel.cpp b/trunk/src/utest/srs_utest_kernel.cpp index 4586583173..e2bbe21fed 100644 --- a/trunk/src/utest/srs_utest_kernel.cpp +++ b/trunk/src/utest/srs_utest_kernel.cpp @@ -6209,4 +6209,33 @@ VOID TEST(KernelUtilityTest, CoverCheckIPAddrValid) ASSERT_FALSE(srs_check_ip_addr_valid("2001:0db8:85a3:0:0:8A2E:0370:7334:")); #endif ASSERT_FALSE(srs_check_ip_addr_valid("1e1.4.5.6")); +} + +VOID TEST(KernelUtilityTest, Base64Decode) +{ + srs_error_t err = srs_success; + + if (true) { + string plaintext; + HELPER_EXPECT_SUCCESS(srs_av_base64_decode("YWRtaW46YWRtaW4=", plaintext)); + EXPECT_STREQ("admin:admin", plaintext.c_str()); + } + + if (true) { + string plaintext; + HELPER_EXPECT_SUCCESS(srs_av_base64_decode("YWRtaW46MTIzNDU2", plaintext)); + EXPECT_STREQ("admin:123456", plaintext.c_str()); + } + + if (true) { + string plaintext; + HELPER_EXPECT_SUCCESS(srs_av_base64_decode("YWRtaW46MTIzNDU2", plaintext)); + EXPECT_STRNE("admin:admin", plaintext.c_str()); + } + + if (true) { + string plaintext; + HELPER_EXPECT_FAILED(srs_av_base64_decode("YWRtaW46YWRtaW", plaintext)); + EXPECT_STRNE("admin:admin", plaintext.c_str()); + } } \ No newline at end of file