diff --git a/common/jwt.cpp b/common/jwt.cpp index f9bf6baa..4bec892b 100644 --- a/common/jwt.cpp +++ b/common/jwt.cpp @@ -37,6 +37,7 @@ // standard headers #include #include +#include #include #include #include @@ -196,32 +197,32 @@ Jwt::Jwt() : claims({}) { } -std::optional Jwt::getIat() const +std::optional Jwt::getIat() const { if (!claims.contains("iat")) return std::nullopt; - return std::chrono::seconds(claims.at("iat").get()); + return std::chrono::system_clock::from_time_t(claims.at("iat").get()); } -void Jwt::setIat(const std::optional& iat) +void Jwt::setIat(const std::optional& iat) { if (iat.has_value()) - claims["iat"] = iat->count(); + claims["iat"] = std::chrono::system_clock::to_time_t(iat.value()); else if (claims.contains("iat")) claims.erase("iat"); } -std::optional Jwt::getExp() const +std::optional Jwt::getExp() const { if (!claims.contains("exp")) return std::nullopt; - return std::chrono::seconds(claims.at("exp").get()); + return std::chrono::system_clock::from_time_t(claims.at("exp").get()); } -void Jwt::setExp(const std::optional& exp) +void Jwt::setExp(const std::optional& exp) { if (exp.has_value()) - claims["exp"] = exp->count(); + claims["exp"] = std::chrono::system_clock::to_time_t(exp.value()); else if (claims.contains("exp")) claims.erase("exp"); } diff --git a/common/jwt.hpp b/common/jwt.hpp index 51e1166b..bdcfc2a6 100644 --- a/common/jwt.hpp +++ b/common/jwt.hpp @@ -96,15 +96,15 @@ public: /// Get the iat "Issued at time" claim /// @return the claim or nullopt, if not present - std::optional getIat() const; + std::optional getIat() const; /// Set the iat "Issued at time" claim, use nullopt to delete the iat - void setIat(const std::optional& iat); + void setIat(const std::optional& iat); /// Get the exp "Expiration time" claim /// @return the claim or nullopt, if not present - std::optional getExp() const; + std::optional getExp() const; /// Set the exp "Expiration time" claim, use nullopt to delete the exp - void setExp(const std::optional& exp); + void setExp(const std::optional& exp); /// Get the sub "Subject" claim /// @return the claim or nullopt, if not present diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 06fbb49a..539805fd 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,4 +1,5 @@ set(SERVER_SOURCES + authinfo.cpp config.cpp control_server.cpp control_session_tcp.cpp diff --git a/server/authinfo.cpp b/server/authinfo.cpp new file mode 100644 index 00000000..27ae9d75 --- /dev/null +++ b/server/authinfo.cpp @@ -0,0 +1,88 @@ +/*** + This file is part of snapcast + Copyright (C) 2014-2024 Johannes Pohl + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +***/ + +// prototype/interface header file +#include "authinfo.hpp" + +// local headers +#include "common/aixlog.hpp" +#include "common/base64.h" +#include "common/jwt.hpp" +#include "common/utils/string_utils.hpp" + +// 3rd party headers + +// standard headers +#include +#include +#include +#include + + +using namespace std; + +static constexpr auto LOG_TAG = "AuthInfo"; + +AuthInfo::AuthInfo(std::string authheader) +{ + LOG(INFO, LOG_TAG) << "Authorization: " << authheader << "\n"; + std::string token(std::move(authheader)); + static constexpr auto bearer = "bearer"sv; + auto pos = utils::string::tolower_copy(token).find(bearer); + if (pos != string::npos) + { + token = token.erase(0, pos + bearer.length()); + utils::string::trim(token); + std::ifstream ifs("certs/snapserver.crt"); + std::string certificate((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + Jwt jwt; + jwt.parse(token, certificate); + if (jwt.getExp().has_value()) + expires_ = jwt.getExp().value(); + username_ = jwt.getSub().value_or(""); + LOG(INFO, LOG_TAG) << "Authorization token: " << token << ", user: " << username_ << ", claims: " << jwt.claims.dump() << "\n"; + } + static constexpr auto basic = "basic"sv; + pos = utils::string::tolower_copy(token).find(basic); + if (pos != string::npos) + { + token = token.erase(0, pos + basic.length()); + utils::string::trim(token); + username_ = base64_decode(token); + std::string password; + username_ = utils::string::split_left(username_, ':', password); + LOG(INFO, LOG_TAG) << "Authorization basic: " << token << ", user: " << username_ << ", password: " << password << "\n"; + } +} + + +bool AuthInfo::valid() const +{ + if (expires_.has_value()) + { + LOG(INFO, LOG_TAG) << "Expires in " << std::chrono::duration_cast(expires_.value() - std::chrono::system_clock::now()).count() + << " sec\n"; + return expires_ > std::chrono::system_clock::now(); + } + return true; +} + +const std::string& AuthInfo::username() const +{ + return username_; +} diff --git a/server/authinfo.hpp b/server/authinfo.hpp new file mode 100644 index 00000000..7cc4f5f3 --- /dev/null +++ b/server/authinfo.hpp @@ -0,0 +1,45 @@ +/*** + This file is part of snapcast + Copyright (C) 2014-2024 Johannes Pohl + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +***/ + +#pragma once + +// local headers +#include "common/jwt.hpp" + +// 3rd party headers + +// standard headers +#include +#include +#include + + + +class AuthInfo +{ +public: + AuthInfo(std::string authheader); + virtual ~AuthInfo() = default; + + bool valid() const; + const std::string& username() const; + +private: + std::string username_; + std::optional expires_; +}; diff --git a/server/control_session.hpp b/server/control_session.hpp index 3fde9b68..f5a52004 100644 --- a/server/control_session.hpp +++ b/server/control_session.hpp @@ -19,12 +19,14 @@ #pragma once // local headers +#include "authinfo.hpp" // 3rd party headers // standard headers #include #include +#include #include @@ -63,6 +65,8 @@ public: /// Sends a message to the client (asynchronous) virtual void sendAsync(const std::string& message) = 0; + std::optional authinfo; + protected: ControlMessageReceiver* message_receiver_; }; diff --git a/server/control_session_http.cpp b/server/control_session_http.cpp index 5e2e944b..d5b4c3e5 100644 --- a/server/control_session_http.cpp +++ b/server/control_session_http.cpp @@ -19,20 +19,22 @@ // prototype/interface header file #include "control_session_http.hpp" -// standard headers -#include -#include +// local headers +#include "authinfo.hpp" +#include "common/aixlog.hpp" +#include "common/utils/file_utils.hpp" +#include "control_session_ws.hpp" +#include "stream_session_ws.hpp" // 3rd party headers #include #include #include -// local headers -#include "common/aixlog.hpp" -#include "common/utils/file_utils.hpp" -#include "control_session_ws.hpp" -#include "stream_session_ws.hpp" +// standard headers +#include +#include + using namespace std; namespace websocket = beast::websocket; // from @@ -358,14 +360,16 @@ void ControlSessionHttp::handle_request(http::requestshutdown(res); - else - res = tcp_socket_->shutdown(tcp_socket::shutdown_send, ec); - if (res.failed()) + ssl_socket_->async_shutdown( + [](const boost::system::error_code& error) + { + if (error.failed()) + LOG(ERROR, LOG_TAG) << "Failed to shudown ssl socket: " << error << "\n"; + }); + else if (boost::system::error_code res = tcp_socket_->shutdown(tcp_socket::shutdown_send, ec); res.failed()) LOG(ERROR, LOG_TAG) << "Failed to shudown socket: " << res << "\n"; return; } @@ -373,7 +377,7 @@ void ControlSessionHttp::on_read(beast::error_code ec, std::size_t bytes_transfe // Handle the error, if any if (ec) { - LOG(ERROR, LOG_TAG) << "ControlSessionHttp::on_read error: " << ec.message() << "\n"; + LOG(ERROR, LOG_TAG) << "ControlSessionHttp::on_read error: " << ec.message() << ", code: " << ec.value() << "\n"; return; } @@ -444,6 +448,13 @@ void ControlSessionHttp::on_read(beast::error_code ec, std::size_t bytes_transfe return; } + + std::string_view authheader = req_[beast::http::field::authorization]; + if (!authheader.empty()) + { + authinfo = AuthInfo(std::string(authheader)); + } + // Send the response handle_request(std::move(req_), [this](auto&& response) diff --git a/server/server.cpp b/server/server.cpp index cdfd8533..3a88aafe 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -21,6 +21,7 @@ // local headers #include "common/aixlog.hpp" +#include "common/jwt.hpp" #include "common/message/client_info.hpp" #include "common/message/hello.hpp" #include "common/message/server_settings.hpp" @@ -30,6 +31,7 @@ // 3rd party headers // standard headers +#include #include @@ -402,6 +404,34 @@ void Server::processRequest(const jsonrpcpp::request_ptr request, const OnRespon /// Notify others notification = std::make_shared("Server.OnUpdate", jsonrpcpp::Parameter("server", server)); } + else if (request->method() == "Server.Authenticate") + { + // clang-format off + // Request: {"id":8,"jsonrpc":"2.0","method":"Server.Authenticate","params":{"user":"badaix","password":"secret"}} + // Response: {"id":8,"jsonrpc":"2.0","result":{"token":""}} + // clang-format on + if (request->params().has("token")) + { + auto token = request->params().get("token"); + LOG(INFO, LOG_TAG) << "Server.Authenticate, token: " << token << "\n"; + result["token"] = token; + } + else if (request->params().has("user")) + { + auto user = request->params().get("user"); + Jwt jwt; + auto now = std::chrono::system_clock::now(); + jwt.setIat(now); + jwt.setExp(now + 10h); + jwt.setSub(user); + std::ifstream ifs(settings_.ssl.private_key.c_str()); + std::string private_key((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + std::optional token = jwt.getToken(private_key); + result["token"] = token.value(); + LOG(INFO, LOG_TAG) << "Server.Authenticate, user: " << user << ", password: " << request->params().get("password") + << ", jwt claims: " << jwt.claims.dump() << ", token: '" << token.value_or("") << "'\n"; + } + } else throw jsonrpcpp::MethodNotFoundException(request->id()); } @@ -669,6 +699,11 @@ void Server::onMessageReceived(std::shared_ptr controlSession, c processRequest(request, [this, controlSession, response_handler](jsonrpcpp::entity_ptr response, jsonrpcpp::notification_ptr notification) { + if (controlSession->authinfo.has_value()) + { + LOG(INFO, LOG_TAG) << "Request auth info - username: " << controlSession->authinfo->username() + << ", valid: " << controlSession->authinfo->valid() << "\n"; + } saveConfig(); ////cout << "Request: " << request->to_json().dump() << "\n"; if (notification) diff --git a/test/test_main.cpp b/test/test_main.cpp index 960860d8..9f5adebc 100644 --- a/test/test_main.cpp +++ b/test/test_main.cpp @@ -30,6 +30,7 @@ #include // standard headers +#include #include @@ -154,7 +155,7 @@ TEST_CASE("JWT") "-----END CERTIFICATE-----\n"; Jwt jwt; - jwt.setIat(std::chrono::seconds(1516239022)); + jwt.setIat(std::chrono::system_clock::from_time_t(1516239022)); jwt.setSub("Badaix"); std::optional token = jwt.getToken(key); REQUIRE(token.has_value()); @@ -168,7 +169,7 @@ TEST_CASE("JWT") REQUIRE(jwt.getSub().has_value()); REQUIRE(jwt.getSub().value() == "Badaix"); REQUIRE(jwt.getIat().has_value()); - REQUIRE(jwt.getIat().value() == std::chrono::seconds(1516239022)); + REQUIRE(jwt.getIat().value() == std::chrono::system_clock::from_time_t(1516239022)); REQUIRE(!jwt.getExp().has_value()); } }