Pass complete Settings struct around

This commit is contained in:
badaix 2024-07-01 21:57:44 +02:00
parent 964801896a
commit c112058998
18 changed files with 584 additions and 140 deletions

View file

@ -28,61 +28,227 @@
// 3rd party headers
// standard headers
#include <algorithm>
#include <chrono>
#include <fstream>
#include <string>
#include <string_view>
#include <system_error>
using namespace std;
static constexpr auto LOG_TAG = "AuthInfo";
AuthInfo::AuthInfo(std::string authheader)
namespace snapcast::error::auth
{
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)
namespace detail
{
/// Error category for auth errors
struct category : public std::error_category
{
public:
/// @return category name
const char* name() const noexcept override;
/// @return error message for @p value
std::string message(int value) const override;
};
const char* category::name() const noexcept
{
return "auth";
}
std::string category::message(int value) const
{
switch (static_cast<AuthErrc>(value))
{
token = token.erase(0, pos + bearer.length());
utils::string::trim(token);
std::ifstream ifs("certs/snapserver.crt");
std::string certificate((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
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";
case AuthErrc::auth_scheme_not_supported:
return "Authentication scheme not supported";
case AuthErrc::failed_to_create_token:
return "Failed to create token";
case AuthErrc::unknown_user:
return "Unknown user";
case AuthErrc::wrong_password:
return "Wrong password";
case AuthErrc::expired:
return "Expired";
case AuthErrc::token_validation_failed:
return "Token validation failed";
default:
return "Unknown";
}
}
} // namespace detail
bool AuthInfo::valid() const
const std::error_category& category()
{
// The category singleton
static detail::category instance;
return instance;
}
} // namespace snapcast::error::auth
std::error_code make_error_code(AuthErrc errc)
{
return std::error_code(static_cast<int>(errc), snapcast::error::auth::category());
}
AuthInfo::AuthInfo(const ServerSettings& settings) : has_auth_info_(false), settings_(settings)
{
}
ErrorCode AuthInfo::validateUser(const std::string& username, const std::optional<std::string>& password) const
{
auto iter = std::find_if(settings_.users.begin(), settings_.users.end(), [&](const ServerSettings::User& user) { return user.name == username; });
if (iter == settings_.users.end())
return ErrorCode{AuthErrc::unknown_user};
if (password.has_value() && (iter->password != password.value()))
return ErrorCode{AuthErrc::wrong_password};
return {};
}
ErrorCode AuthInfo::authenticate(const std::string& scheme, const std::string& param)
{
std::string scheme_normed = utils::string::trim_copy(utils::string::tolower_copy(scheme));
std::string param_normed = utils::string::trim_copy(param);
if (scheme_normed == "bearer")
return authenticateBearer(param_normed);
else if (scheme_normed == "basic")
return authenticateBasic(param_normed);
return {AuthErrc::auth_scheme_not_supported, "Scheme must be 'Basic' or 'Bearer'"};
}
ErrorCode AuthInfo::authenticate(const std::string& auth)
{
LOG(INFO, LOG_TAG) << "authenticate: " << auth << "\n";
std::string param;
std::string scheme = utils::string::split_left(utils::string::trim_copy(auth), ' ', param);
return authenticate(scheme, param);
}
ErrorCode AuthInfo::authenticateBasic(const std::string& credentials)
{
has_auth_info_ = false;
std::string username = base64_decode(credentials);
std::string password;
username_ = utils::string::split_left(username, ':', password);
auto ec = validateUser(username_, password);
LOG(INFO, LOG_TAG) << "Authorization basic: " << credentials << ", user: " << username_ << ", password: " << password << "\n";
has_auth_info_ = (ec.value() == 0);
return ec;
}
ErrorCode AuthInfo::authenticateBearer(const std::string& token)
{
has_auth_info_ = false;
std::ifstream ifs(settings_.ssl.certificate);
std::string certificate((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
Jwt jwt;
if (!jwt.parse(token, certificate))
return {AuthErrc::token_validation_failed};
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";
if (auto ec = validateUser(username_); ec)
return ec;
if (isExpired())
return {AuthErrc::expired};
has_auth_info_ = true;
return {};
}
ErrorOr<std::string> AuthInfo::getToken(const std::string& username, const std::string& password) const
{
ErrorCode ec = validateUser(username, password);
if (ec)
return ec;
Jwt jwt;
auto now = std::chrono::system_clock::now();
jwt.setIat(now);
jwt.setExp(now + 10h);
jwt.setSub(username);
std::ifstream ifs(settings_.ssl.private_key);
std::string private_key((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
if (!ifs.good())
return ErrorCode{std::make_error_code(std::errc::io_error), "Failed to read private key file"};
// TODO tls: eroor handling
std::optional<std::string> token = jwt.getToken(private_key);
if (!token.has_value())
return ErrorCode{AuthErrc::failed_to_create_token};
return token.value();
}
bool AuthInfo::isExpired() const
{
if (expires_.has_value())
{
LOG(INFO, LOG_TAG) << "Expires in " << std::chrono::duration_cast<std::chrono::seconds>(expires_.value() - std::chrono::system_clock::now()).count()
<< " sec\n";
return expires_ > std::chrono::system_clock::now();
if (std::chrono::system_clock::now() > expires_.value())
return true;
}
return true;
return false;
}
bool AuthInfo::hasAuthInfo() const
{
return has_auth_info_;
}
// ErrorCode AuthInfo::isValid(const std::string& command) const
// {
// std::ignore = command;
// if (isExpired())
// return {AuthErrc::expired};
// return {};
// }
const std::string& AuthInfo::username() const
{
return username_;
}
bool AuthInfo::hasPermission(const std::string& resource) const
{
if (!hasAuthInfo())
return false;
auto iter = std::find_if(settings_.users.begin(), settings_.users.end(), [&](const ServerSettings::User& user) { return user.name == username_; });
if (iter == settings_.users.end())
return false;
auto perm_iter = std::find_if(iter->permissions.begin(), iter->permissions.end(),
[&](const std::string& permission) { return utils::string::wildcardMatch(permission, resource); });
if (perm_iter != iter->permissions.end())
{
LOG(DEBUG, LOG_TAG) << "Found permission for ressource '" << resource << "': '" << *perm_iter << "'\n";
return true;
}
return false;
}

View file

@ -19,7 +19,8 @@
#pragma once
// local headers
#include "common/jwt.hpp"
#include "common/error_code.hpp"
#include "server_settings.hpp"
// 3rd party headers
@ -27,19 +28,82 @@
#include <chrono>
#include <optional>
#include <string>
#include <system_error>
/// Authentication error codes
enum class AuthErrc
{
auth_scheme_not_supported = 1,
failed_to_create_token = 2,
unknown_user = 3,
wrong_password = 4,
expired = 5,
token_validation_failed = 6,
};
namespace snapcast::error::auth
{
const std::error_category& category();
}
namespace std
{
template <>
struct is_error_code_enum<AuthErrc> : public std::true_type
{
};
} // namespace std
std::error_code make_error_code(AuthErrc);
using snapcast::ErrorCode;
using snapcast::ErrorOr;
/// Authentication Info class
class AuthInfo
{
public:
AuthInfo(std::string authheader);
/// c'tor
explicit AuthInfo(const ServerSettings& settings);
// explicit AuthInfo(std::string authheader);
/// d'tor
virtual ~AuthInfo() = default;
bool valid() const;
/// @return if authentication info is available
bool hasAuthInfo() const;
// ErrorCode isValid(const std::string& command) const;
/// @return the username
const std::string& username() const;
/// Authenticate with basic scheme
ErrorCode authenticateBasic(const std::string& credentials);
/// Authenticate with bearer scheme
ErrorCode authenticateBearer(const std::string& token);
/// Authenticate with basic or bearer scheme with an auth header
ErrorCode authenticate(const std::string& auth);
/// Authenticate with scheme ("basic" or "bearer") and auth param
ErrorCode authenticate(const std::string& scheme, const std::string& param);
/// @return JWS token for @p username and @p password
ErrorOr<std::string> getToken(const std::string& username, const std::string& password) const;
/// @return if the authenticated user has permission to access @p ressource
bool hasPermission(const std::string& resource) const;
private:
/// has auth info
bool has_auth_info_;
/// auth user name
std::string username_;
/// optional token expiration
std::optional<std::chrono::system_clock::time_point> expires_;
/// server configuration
ServerSettings settings_;
/// Validate @p username and @p password
/// @return true if username and password are correct
ErrorCode validateUser(const std::string& username, const std::optional<std::string>& password = std::nullopt) const;
/// @return if the authentication is expired
bool isExpired() const;
};

View file

@ -39,11 +39,10 @@ static constexpr auto LOG_TAG = "ControlServer";
ControlServer::ControlServer(boost::asio::io_context& io_context, const ServerSettings& settings, ControlMessageReceiver* controlMessageReceiver)
: io_context_(io_context), ssl_context_(boost::asio::ssl::context::sslv23), tcp_settings_(settings.tcp), http_settings_(settings.http),
controlMessageReceiver_(controlMessageReceiver)
: io_context_(io_context), ssl_context_(boost::asio::ssl::context::sslv23), settings_(settings), controlMessageReceiver_(controlMessageReceiver)
{
const ServerSettings::Ssl& ssl = settings.ssl;
if (http_settings_.ssl_enabled)
if (settings_.http.ssl_enabled)
{
ssl_context_.set_options(boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_sslv2 |
boost::asio::ssl::context::single_dh_use);
@ -99,7 +98,7 @@ void ControlServer::send(const std::string& message, const ControlSession* exclu
}
void ControlServer::onMessageReceived(std::shared_ptr<ControlSession> session, const std::string& message, const ResponseHander& response_handler)
void ControlServer::onMessageReceived(std::shared_ptr<ControlSession> session, const std::string& message, const ResponseHandler& response_handler)
{
// LOG(DEBUG, LOG_TAG) << "received: \"" << message << "\"\n";
if (controlMessageReceiver_ != nullptr)
@ -138,19 +137,19 @@ void ControlServer::startAccept()
auto port = socket.local_endpoint().port();
LOG(NOTICE, LOG_TAG) << "New connection from: " << socket.remote_endpoint().address().to_string() << ", port: " << port << endl;
if (port == http_settings_.ssl_port)
if (port == settings_.http.ssl_port)
{
auto session = make_shared<ControlSessionHttp>(this, ssl_socket(std::move(socket), ssl_context_), http_settings_);
auto session = make_shared<ControlSessionHttp>(this, ssl_socket(std::move(socket), ssl_context_), settings_);
onNewSession(std::move(session));
}
else if (port == http_settings_.port)
else if (port == settings_.http.port)
{
auto session = make_shared<ControlSessionHttp>(this, std::move(socket), http_settings_);
auto session = make_shared<ControlSessionHttp>(this, std::move(socket), settings_);
onNewSession(std::move(session));
}
else if (port == tcp_settings_.port)
else if (port == settings_.tcp.port)
{
auto session = make_shared<ControlSessionTcp>(this, std::move(socket));
auto session = make_shared<ControlSessionTcp>(this, std::move(socket), settings_);
onNewSession(std::move(session));
}
else
@ -171,15 +170,15 @@ void ControlServer::startAccept()
void ControlServer::start()
{
if (tcp_settings_.enabled)
if (settings_.tcp.enabled)
{
for (const auto& address : tcp_settings_.bind_to_address)
for (const auto& address : settings_.tcp.bind_to_address)
{
try
{
LOG(INFO, LOG_TAG) << "Creating TCP acceptor for address: " << address << ", port: " << tcp_settings_.port << "\n";
LOG(INFO, LOG_TAG) << "Creating TCP acceptor for address: " << address << ", port: " << settings_.tcp.port << "\n";
acceptor_.emplace_back(make_unique<tcp::acceptor>(boost::asio::make_strand(io_context_.get_executor()),
tcp::endpoint(boost::asio::ip::address::from_string(address), tcp_settings_.port)));
tcp::endpoint(boost::asio::ip::address::from_string(address), settings_.tcp.port)));
}
catch (const boost::system::system_error& e)
{
@ -187,17 +186,17 @@ void ControlServer::start()
}
}
}
if (http_settings_.enabled || http_settings_.ssl_enabled)
if (settings_.http.enabled || settings_.http.ssl_enabled)
{
if (http_settings_.enabled)
if (settings_.http.enabled)
{
for (const auto& address : http_settings_.bind_to_address)
for (const auto& address : settings_.http.bind_to_address)
{
try
{
LOG(INFO, LOG_TAG) << "Creating HTTP acceptor for address: " << address << ", port: " << http_settings_.port << "\n";
LOG(INFO, LOG_TAG) << "Creating HTTP acceptor for address: " << address << ", port: " << settings_.http.port << "\n";
acceptor_.emplace_back(make_unique<tcp::acceptor>(boost::asio::make_strand(io_context_.get_executor()),
tcp::endpoint(boost::asio::ip::address::from_string(address), http_settings_.port)));
tcp::endpoint(boost::asio::ip::address::from_string(address), settings_.http.port)));
}
catch (const boost::system::system_error& e)
{
@ -206,15 +205,15 @@ void ControlServer::start()
}
}
if (http_settings_.ssl_enabled)
if (settings_.http.ssl_enabled)
{
for (const auto& address : http_settings_.ssl_bind_to_address)
for (const auto& address : settings_.http.ssl_bind_to_address)
{
try
{
LOG(INFO, LOG_TAG) << "Creating HTTPS acceptor for address: " << address << ", port: " << http_settings_.ssl_port << "\n";
LOG(INFO, LOG_TAG) << "Creating HTTPS acceptor for address: " << address << ", port: " << settings_.http.ssl_port << "\n";
acceptor_.emplace_back(make_unique<tcp::acceptor>(boost::asio::make_strand(io_context_.get_executor()),
tcp::endpoint(boost::asio::ip::address::from_string(address), http_settings_.ssl_port)));
tcp::endpoint(boost::asio::ip::address::from_string(address), settings_.http.ssl_port)));
}
catch (const boost::system::system_error& e)
{

View file

@ -43,10 +43,14 @@ using acceptor_ptr = std::unique_ptr<tcp::acceptor>;
class ControlServer : public ControlMessageReceiver
{
public:
/// c'tor
ControlServer(boost::asio::io_context& io_context, const ServerSettings& settings, ControlMessageReceiver* controlMessageReceiver = nullptr);
/// d'tor
virtual ~ControlServer();
/// Start accepting control connections
void start();
/// Stop accepting connections and stop all running sessions
void stop();
/// Send a message to all connected clients
@ -58,7 +62,7 @@ private:
void cleanup();
/// Implementation of ControlMessageReceiver
void onMessageReceived(std::shared_ptr<ControlSession> session, const std::string& message, const ResponseHander& response_handler) override;
void onMessageReceived(std::shared_ptr<ControlSession> session, const std::string& message, const ResponseHandler& response_handler) override;
void onNewSession(std::shared_ptr<ControlSession> session) override;
void onNewSession(std::shared_ptr<StreamSession> session) override;
@ -69,7 +73,6 @@ private:
boost::asio::io_context& io_context_;
boost::asio::ssl::context ssl_context_;
ServerSettings::Tcp tcp_settings_;
ServerSettings::Http http_settings_;
ServerSettings settings_;
ControlMessageReceiver* controlMessageReceiver_;
};

View file

@ -20,6 +20,7 @@
// local headers
#include "authinfo.hpp"
#include "server_settings.hpp"
// 3rd party headers
@ -37,10 +38,14 @@ class StreamSession;
class ControlMessageReceiver
{
public:
using ResponseHander = std::function<void(const std::string& response)>;
/// Response callback function for requests
using ResponseHandler = std::function<void(const std::string& response)>;
// TODO: rename, error handling
virtual void onMessageReceived(std::shared_ptr<ControlSession> session, const std::string& message, const ResponseHander& response_handler) = 0;
/// Called when a comtrol message @p message is received by @p session, response is written to @p response_handler
virtual void onMessageReceived(std::shared_ptr<ControlSession> session, const std::string& message, const ResponseHandler& response_handler) = 0;
/// Called when a comtrol session is created
virtual void onNewSession(std::shared_ptr<ControlSession> session) = 0;
/// Called when a stream session is created
virtual void onNewSession(std::shared_ptr<StreamSession> session) = 0;
};
@ -55,18 +60,22 @@ class ControlSession : public std::enable_shared_from_this<ControlSession>
{
public:
/// ctor. Received message from the client are passed to ControlMessageReceiver
ControlSession(ControlMessageReceiver* receiver) : message_receiver_(receiver)
ControlSession(ControlMessageReceiver* receiver, const ServerSettings& settings) : authinfo(settings), message_receiver_(receiver)
{
}
virtual ~ControlSession() = default;
/// Start the control session
virtual void start() = 0;
/// Stop the control session
virtual void stop() = 0;
/// Sends a message to the client (asynchronous)
virtual void sendAsync(const std::string& message) = 0;
std::optional<AuthInfo> authinfo;
/// Authentication info attached to this session
AuthInfo authinfo;
protected:
/// The control message receiver
ControlMessageReceiver* message_receiver_;
};

View file

@ -149,14 +149,14 @@ std::string path_cat(boost::beast::string_view base, boost::beast::string_view p
}
} // namespace
ControlSessionHttp::ControlSessionHttp(ControlMessageReceiver* receiver, ssl_socket&& socket, const ServerSettings::Http& settings)
: ControlSession(receiver), ssl_socket_(std::move(socket)), settings_(settings), is_ssl_(true)
ControlSessionHttp::ControlSessionHttp(ControlMessageReceiver* receiver, ssl_socket&& socket, const ServerSettings& settings)
: ControlSession(receiver, settings), ssl_socket_(std::move(socket)), settings_(settings), is_ssl_(true)
{
LOG(DEBUG, LOG_TAG) << "ControlSessionHttp, mode: ssl, Local IP: " << ssl_socket_->next_layer().local_endpoint().address().to_string() << "\n";
}
ControlSessionHttp::ControlSessionHttp(ControlMessageReceiver* receiver, tcp_socket&& socket, const ServerSettings::Http& settings)
: ControlSession(receiver), tcp_socket_(std::move(socket)), settings_(settings), is_ssl_(false)
ControlSessionHttp::ControlSessionHttp(ControlMessageReceiver* receiver, tcp_socket&& socket, const ServerSettings& settings)
: ControlSession(receiver, settings), tcp_socket_(std::move(socket)), settings_(settings), is_ssl_(false)
{
LOG(DEBUG, LOG_TAG) << "ControlSessionHttp, mode: tcp, Local IP: " << tcp_socket_->local_endpoint().address().to_string() << "\n";
}
@ -288,7 +288,7 @@ void ControlSessionHttp::handle_request(http::request<Body, http::basic_fields<A
{
pos += image_cache_target.size();
target = target.substr(pos);
auto image = settings_.image_cache.getImage(std::string(target));
auto image = settings_.http.image_cache.getImage(std::string(target));
LOG(DEBUG, LOG_TAG) << "image cache: " << target << ", found: " << image.has_value() << "\n";
if (image.has_value())
{
@ -307,11 +307,11 @@ void ControlSessionHttp::handle_request(http::request<Body, http::basic_fields<A
}
// Build the path to the requested file
std::string path = path_cat(settings_.doc_root, target);
std::string path = path_cat(settings_.http.doc_root, target);
if (req.target().back() == '/')
path.append("index.html");
if (settings_.doc_root.empty())
if (settings_.http.doc_root.empty())
{
static constexpr auto default_page = "/usr/share/snapserver/index.html";
if (utils::file::exists(default_page))
@ -406,7 +406,7 @@ void ControlSessionHttp::on_read(beast::error_code ec, std::size_t bytes_transfe
{
if (req_.target() == "/jsonrpc")
{
auto ws_session = make_shared<ControlSessionWebsocket>(message_receiver_, std::move(*ws));
auto ws_session = make_shared<ControlSessionWebsocket>(message_receiver_, std::move(*ws), settings_);
message_receiver_->onNewSession(std::move(ws_session));
}
else // if (req_.target() == "/stream")
@ -433,7 +433,7 @@ void ControlSessionHttp::on_read(beast::error_code ec, std::size_t bytes_transfe
{
if (req_.target() == "/jsonrpc")
{
auto ws_session = make_shared<ControlSessionWebsocket>(message_receiver_, std::move(*ws));
auto ws_session = make_shared<ControlSessionWebsocket>(message_receiver_, std::move(*ws), settings_);
message_receiver_->onNewSession(std::move(ws_session));
}
else // if (req_.target() == "/stream")
@ -452,7 +452,11 @@ void ControlSessionHttp::on_read(beast::error_code ec, std::size_t bytes_transfe
std::string_view authheader = req_[beast::http::field::authorization];
if (!authheader.empty())
{
authinfo = AuthInfo(std::string(authheader));
auto ec = authinfo.authenticate(std::string(authheader));
if (ec)
{
LOG(ERROR, LOG_TAG) << "Authentication failed: " << ec.detailed_message() << "\n";
}
}
// Send the response

View file

@ -55,9 +55,10 @@ using ssl_socket = boost::asio::ssl::stream<tcp_socket>;
class ControlSessionHttp : public ControlSession
{
public:
/// ctor. Received message from the client are passed to ControlMessageReceiver
ControlSessionHttp(ControlMessageReceiver* receiver, ssl_socket&& socket, const ServerSettings::Http& settings);
ControlSessionHttp(ControlMessageReceiver* receiver, tcp_socket&& socket, const ServerSettings::Http& settings);
/// c'tor for ssl sockets. Received message from the client are passed to ControlMessageReceiver
ControlSessionHttp(ControlMessageReceiver* receiver, ssl_socket&& socket, const ServerSettings& settings);
/// c'tor for tcp sockets
ControlSessionHttp(ControlMessageReceiver* receiver, tcp_socket&& socket, const ServerSettings& settings);
~ControlSessionHttp() override;
void start() override;
void stop() override;
@ -65,21 +66,21 @@ public:
/// Sends a message to the client (asynchronous)
void sendAsync(const std::string& message) override;
protected:
// HTTP methods
private:
/// HTTP on read callback
void on_read(beast::error_code ec, std::size_t bytes_transferred);
/// HTTP on write callback
void on_write(beast::error_code ec, std::size_t bytes, bool close);
/// Handle an incoming HTTP request
template <class Body, class Allocator, class Send>
void handle_request(http::request<Body, http::basic_fields<Allocator>>&& req, Send&& send);
http::request<http::string_body> req_;
protected:
std::optional<tcp_socket> tcp_socket_;
std::optional<ssl_socket> ssl_socket_;
beast::flat_buffer buffer_;
ServerSettings::Http settings_;
ServerSettings settings_;
std::deque<std::string> messages_;
bool is_ssl_;
};

View file

@ -26,6 +26,7 @@
// local headers
#include "common/aixlog.hpp"
#include "server_settings.hpp"
using namespace std;
@ -35,8 +36,8 @@ static constexpr auto LOG_TAG = "ControlSessionTCP";
// https://stackoverflow.com/questions/7754695/boost-asio-async-write-how-to-not-interleaving-async-write-calls/7756894
ControlSessionTcp::ControlSessionTcp(ControlMessageReceiver* receiver, tcp::socket&& socket)
: ControlSession(receiver), socket_(std::move(socket)), strand_(boost::asio::make_strand(socket_.get_executor()))
ControlSessionTcp::ControlSessionTcp(ControlMessageReceiver* receiver, tcp::socket&& socket, const ServerSettings& settings)
: ControlSession(receiver, settings), socket_(std::move(socket)), strand_(boost::asio::make_strand(socket_.get_executor()))
{
}

View file

@ -43,7 +43,7 @@ class ControlSessionTcp : public ControlSession
{
public:
/// ctor. Received message from the client are passed to ControlMessageReceiver
ControlSessionTcp(ControlMessageReceiver* receiver, tcp::socket&& socket);
ControlSessionTcp(ControlMessageReceiver* receiver, tcp::socket&& socket, const ServerSettings& settings);
~ControlSessionTcp() override;
void start() override;
void stop() override;
@ -51,7 +51,7 @@ public:
/// Sends a message to the client (asynchronous)
void sendAsync(const std::string& message) override;
protected:
private:
void do_read();
void send_next();

View file

@ -32,14 +32,14 @@ using namespace std;
static constexpr auto LOG_TAG = "ControlSessionWS";
ControlSessionWebsocket::ControlSessionWebsocket(ControlMessageReceiver* receiver, ssl_websocket&& ssl_ws)
: ControlSession(receiver), ssl_ws_(std::move(ssl_ws)), strand_(boost::asio::make_strand(ssl_ws_->get_executor())), is_ssl_(true)
ControlSessionWebsocket::ControlSessionWebsocket(ControlMessageReceiver* receiver, ssl_websocket&& ssl_ws, const ServerSettings& settings)
: ControlSession(receiver, settings), ssl_ws_(std::move(ssl_ws)), strand_(boost::asio::make_strand(ssl_ws_->get_executor())), is_ssl_(true)
{
LOG(DEBUG, LOG_TAG) << "ControlSessionWebsocket, mode: ssl\n";
}
ControlSessionWebsocket::ControlSessionWebsocket(ControlMessageReceiver* receiver, tcp_websocket&& tcp_ws)
: ControlSession(receiver), tcp_ws_(std::move(tcp_ws)), strand_(boost::asio::make_strand(tcp_ws_->get_executor())), is_ssl_(false)
ControlSessionWebsocket::ControlSessionWebsocket(ControlMessageReceiver* receiver, tcp_websocket&& tcp_ws, const ServerSettings& settings)
: ControlSession(receiver, settings), tcp_ws_(std::move(tcp_ws)), strand_(boost::asio::make_strand(tcp_ws_->get_executor())), is_ssl_(false)
{
LOG(DEBUG, LOG_TAG) << "ControlSessionWebsocket, mode: tcp\n";
}

View file

@ -65,9 +65,10 @@ using ssl_websocket = websocket::stream<ssl_socket>;
class ControlSessionWebsocket : public ControlSession
{
public:
/// ctor. Received message from the client are passed to ControlMessageReceiver
ControlSessionWebsocket(ControlMessageReceiver* receiver, ssl_websocket&& ssl_ws);
ControlSessionWebsocket(ControlMessageReceiver* receiver, tcp_websocket&& tcp_ws);
/// c'tor for ssl websockets. Received message from the client are passed to ControlMessageReceiver
ControlSessionWebsocket(ControlMessageReceiver* receiver, ssl_websocket&& ssl_ws, const ServerSettings& settings);
/// c'tor for TCP websockets. Received message from the client are passed to ControlMessageReceiver
ControlSessionWebsocket(ControlMessageReceiver* receiver, tcp_websocket&& tcp_ws, const ServerSettings& settings);
~ControlSessionWebsocket() override;
void start() override;
void stop() override;
@ -75,7 +76,7 @@ public:
/// Sends a message to the client (asynchronous)
void sendAsync(const std::string& message) override;
protected:
private:
// Websocket methods
void on_read_ws(beast::error_code ec, std::size_t bytes_transferred);
void do_read_ws();
@ -84,7 +85,6 @@ protected:
std::optional<ssl_websocket> ssl_ws_;
std::optional<tcp_websocket> tcp_ws_;
protected:
beast::flat_buffer buffer_;
boost::asio::strand<boost::asio::any_io_executor> strand_;
std::deque<std::string> messages_;

View file

@ -21,12 +21,15 @@
// local headers
#include "common/aixlog.hpp"
#include "common/base64.h"
#include "common/jwt.hpp"
#include "common/message/client_info.hpp"
#include "common/message/hello.hpp"
#include "common/message/server_settings.hpp"
#include "common/message/time.hpp"
#include "common/utils/string_utils.hpp"
#include "config.hpp"
#include "jsonrpcpp.hpp"
// 3rd party headers
@ -131,9 +134,9 @@ void Server::onDisconnect(StreamSession* streamSession)
}
void Server::processRequest(const jsonrpcpp::request_ptr request, const OnResponse& on_response) const
void Server::processRequest(const jsonrpcpp::request_ptr request, AuthInfo& authinfo, const OnResponse& on_response) const
{
jsonrpcpp::entity_ptr response;
jsonrpcpp::entity_ptr response = nullptr;
jsonrpcpp::notification_ptr notification;
try
{
@ -407,30 +410,48 @@ void Server::processRequest(const jsonrpcpp::request_ptr request, const OnRespon
else if (request->method() == "Server.Authenticate")
{
// clang-format off
// Request: {"id":8,"jsonrpc":"2.0","method":"Server.Authenticate","params":{"user":"badaix","password":"secret"}}
// Request: {"id":8,"jsonrpc":"2.0","method":"Server.Authenticate","params":{"scheme":"Basic","param":"YmFkYWl4OnNlY3JldA=="}}
// Response: {"id":8,"jsonrpc":"2.0","result":"ok"}
// Request: {"id":8,"jsonrpc":"2.0","method":"Server.Authenticate","params":{"scheme":"Bearer","param":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTg1NjQ1MTYsImlhdCI6MTcxODUyODUxNiwic3ViIjoiQmFkYWl4In0.gHrMVp7jTAg8aCSg3cttcfIxswqmOPuqVNOb5p79Cn0NmAqRmLXtDLX4QjOoOqqb66ezBBeikpNjPi_aO18YPoNmX9fPxSwcObTHBupnm5eugEpneMPDASFUSE2hg8rrD_OEoAVxx6hCLln7Z3ILyWDmR6jcmy7z0bp0BiAqOywUrFoVIsnlDZRs3wOaap5oS9J2oaA_gNi_7OuvAhrydn26LDhm0KiIqEcyIholkpRHrDYODkz98h2PkZdZ2U429tTvVhzDBJ1cBq2Zq3cvuMZT6qhwaUc8eYA8fUJ7g65iP4o2OZtUzlfEUqX1TKyuWuSK6CUlsZooNE-MSCT7_w"}}
// Response: {"id":8,"jsonrpc":"2.0","result":"ok"}
// clang-format on
if (!request->params().has("scheme"))
throw jsonrpcpp::InvalidParamsException("Parameter 'scheme' is missing", request->id());
if (!request->params().has("param"))
throw jsonrpcpp::InvalidParamsException("Parameter 'param' is missing", request->id());
auto scheme = request->params().get<std::string>("scheme");
auto param = request->params().get<std::string>("param");
LOG(INFO, LOG_TAG) << "Authorization scheme: " << scheme << ", param: " << param << "\n";
auto ec = authinfo.authenticate(scheme, param);
if (ec)
response = make_shared<jsonrpcpp::Response>(request->id(), jsonrpcpp::Error(ec.detailed_message(), ec.value()));
else
response = make_shared<jsonrpcpp::Response>(request->id(), "ok");
// LOG(DEBUG, LOG_TAG) << response->to_json().dump() << "\n";
}
else if (request->method() == "Server.GetToken")
{
// clang-format off
// Request: {"id":8,"jsonrpc":"2.0","method":"Server.GetToken","params":{"username":"Badaix","password":"secret"}}
// Response: {"id":8,"jsonrpc":"2.0","result":{"token":"<token>"}}
// clang-format on
if (request->params().has("token"))
{
auto token = request->params().get<std::string>("token");
LOG(INFO, LOG_TAG) << "Server.Authenticate, token: " << token << "\n";
result["token"] = token;
}
else if (request->params().has("user"))
{
auto user = request->params().get<std::string>("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<char>(ifs)), std::istreambuf_iterator<char>());
std::optional<std::string> token = jwt.getToken(private_key);
result["token"] = token.value();
LOG(INFO, LOG_TAG) << "Server.Authenticate, user: " << user << ", password: " << request->params().get<std::string>("password")
<< ", jwt claims: " << jwt.claims.dump() << ", token: '" << token.value_or("") << "'\n";
}
if (!request->params().has("username"))
throw jsonrpcpp::InvalidParamsException("Parameter 'username' is missing", request->id());
if (!request->params().has("password"))
throw jsonrpcpp::InvalidParamsException("Parameter 'password' is missing", request->id());
auto username = request->params().get<std::string>("username");
auto password = request->params().get<std::string>("password");
LOG(INFO, LOG_TAG) << "GetToken username: " << username << ", password: " << password << "\n";
auto token = authinfo.getToken(username, password);
if (token.hasError())
response = make_shared<jsonrpcpp::Response>(request->id(), jsonrpcpp::Error(token.getError().detailed_message(), token.getError().value()));
else
result["token"] = token.takeValue();
// LOG(DEBUG, LOG_TAG) << response->to_json().dump() << "\n";
}
else
throw jsonrpcpp::MethodNotFoundException(request->id());
@ -473,7 +494,7 @@ void Server::processRequest(const jsonrpcpp::request_ptr request, const OnRespon
<< ", params: " << (request->params().has("params") ? request->params().get("params") : "") << "\n";
// Find stream
string streamId = request->params().get<std::string>("id");
auto streamId = request->params().get<std::string>("id");
PcmStreamPtr stream = streamManager_->getStream(streamId);
if (stream == nullptr)
throw jsonrpcpp::InternalErrorException("Stream not found", request->id());
@ -565,9 +586,9 @@ void Server::processRequest(const jsonrpcpp::request_ptr request, const OnRespon
auto value = request->params().get("value");
LOG(INFO, LOG_TAG) << "Stream '" << streamId << "' set property: " << name << " = " << value << "\n";
auto handle_response = [request, on_response](const snapcast::ErrorCode& ec)
auto handle_response = [request, on_response](const std::string& command, const snapcast::ErrorCode& ec)
{
LOG(ERROR, LOG_TAG) << "SetShuffle: " << ec << ", message: " << ec.detailed_message() << ", msg: " << ec.message()
LOG(ERROR, LOG_TAG) << "Result for '" << command << "': " << ec << ", message: " << ec.detailed_message() << ", msg: " << ec.message()
<< ", category: " << ec.category().name() << "\n";
std::shared_ptr<jsonrpcpp::Response> response;
if (ec)
@ -583,31 +604,31 @@ void Server::processRequest(const jsonrpcpp::request_ptr request, const OnRespon
LoopStatus loop_status = loop_status_from_string(val);
if (loop_status == LoopStatus::kUnknown)
throw jsonrpcpp::InvalidParamsException("Value for loopStatus must be one of 'none', 'track', 'playlist'", request->id());
stream->setLoopStatus(loop_status, [handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); });
stream->setLoopStatus(loop_status, [handle_response, name](const snapcast::ErrorCode& ec) { handle_response(name, ec); });
}
else if (name == "shuffle")
{
if (!value.is_boolean())
throw jsonrpcpp::InvalidParamsException("Value for shuffle must be bool", request->id());
stream->setShuffle(value.get<bool>(), [handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); });
stream->setShuffle(value.get<bool>(), [handle_response, name](const snapcast::ErrorCode& ec) { handle_response(name, ec); });
}
else if (name == "volume")
{
if (!value.is_number_integer())
throw jsonrpcpp::InvalidParamsException("Value for volume must be an int", request->id());
stream->setVolume(value.get<int16_t>(), [handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); });
stream->setVolume(value.get<int16_t>(), [handle_response, name](const snapcast::ErrorCode& ec) { handle_response(name, ec); });
}
else if (name == "mute")
{
if (!value.is_boolean())
throw jsonrpcpp::InvalidParamsException("Value for mute must be bool", request->id());
stream->setMute(value.get<bool>(), [handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); });
stream->setMute(value.get<bool>(), [handle_response, name](const snapcast::ErrorCode& ec) { handle_response(name, ec); });
}
else if (name == "rate")
{
if (!value.is_number_float())
throw jsonrpcpp::InvalidParamsException("Value for rate must be float", request->id());
stream->setRate(value.get<float>(), [handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); });
stream->setRate(value.get<float>(), [handle_response, name](const snapcast::ErrorCode& ec) { handle_response(name, ec); });
}
else
throw jsonrpcpp::InvalidParamsException("Property '" + name + "' not supported", request->id());
@ -655,7 +676,8 @@ void Server::processRequest(const jsonrpcpp::request_ptr request, const OnRespon
else
throw jsonrpcpp::MethodNotFoundException(request->id());
response = std::make_shared<jsonrpcpp::Response>(*request, result);
if (!response)
response = std::make_shared<jsonrpcpp::Response>(*request, result);
}
catch (const jsonrpcpp::RequestException& e)
{
@ -671,7 +693,7 @@ void Server::processRequest(const jsonrpcpp::request_ptr request, const OnRespon
}
void Server::onMessageReceived(std::shared_ptr<ControlSession> controlSession, const std::string& message, const ResponseHander& response_handler)
void Server::onMessageReceived(std::shared_ptr<ControlSession> controlSession, const std::string& message, const ResponseHandler& response_handler)
{
// LOG(DEBUG, LOG_TAG) << "onMessageReceived: " << message << "\n";
std::lock_guard<std::mutex> lock(Config::instance().getMutex());
@ -696,14 +718,14 @@ void Server::onMessageReceived(std::shared_ptr<ControlSession> controlSession, c
if (entity->is_request())
{
jsonrpcpp::request_ptr request = dynamic_pointer_cast<jsonrpcpp::Request>(entity);
processRequest(request,
processRequest(request, controlSession->authinfo,
[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";
}
// if (controlSession->authinfo.hasAuthInfo())
// {
// 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)
@ -733,7 +755,7 @@ void Server::onMessageReceived(std::shared_ptr<ControlSession> controlSession, c
if (batch_entity->is_request())
{
jsonrpcpp::request_ptr request = dynamic_pointer_cast<jsonrpcpp::Request>(batch_entity);
processRequest(request,
processRequest(request, controlSession->authinfo,
[controlSession, response_handler, &responseBatch, &notificationBatch](jsonrpcpp::entity_ptr response,
jsonrpcpp::notification_ptr notification)
{

View file

@ -20,6 +20,7 @@
// local headers
#include "authinfo.hpp"
#include "common/message/message.hpp"
#include "common/queue.hpp"
#include "control_server.hpp"
@ -52,12 +53,16 @@ class Server : public StreamMessageReceiver, public ControlMessageReceiver, publ
{
public:
// TODO: revise handler names
/// Response handler for json control requests, returning a @p response and/or a @p notification broadcast
using OnResponse = std::function<void(jsonrpcpp::entity_ptr response, jsonrpcpp::notification_ptr notification)>;
/// c'tor
Server(boost::asio::io_context& io_context, const ServerSettings& serverSettings);
virtual ~Server();
/// Start the server (control server, stream server and stream manager)
void start();
/// Stop the server (control server, stream server and stream manager)
void stop();
private:
@ -66,7 +71,7 @@ private:
void onDisconnect(StreamSession* streamSession) override;
/// Implementation of ControllMessageReceiver
void onMessageReceived(std::shared_ptr<ControlSession> controlSession, const std::string& message, const ResponseHander& response_handler) override;
void onMessageReceived(std::shared_ptr<ControlSession> controlSession, const std::string& message, const ResponseHandler& response_handler) override;
void onNewSession(std::shared_ptr<ControlSession> session) override
{
std::ignore = session;
@ -81,7 +86,7 @@ private:
void onResync(const PcmStream* pcmStream, double ms) override;
private:
void processRequest(const jsonrpcpp::request_ptr request, const OnResponse& on_response) const;
void processRequest(const jsonrpcpp::request_ptr request, AuthInfo& authinfo, const OnResponse& on_response) const;
/// Save the server state deferred to prevent blocking and lower disk io
/// @param deferred the delay after the last call to saveConfig
void saveConfig(const std::chrono::milliseconds& deferred = std::chrono::seconds(2));

View file

@ -20,6 +20,7 @@
// local headers
#include "common/utils/string_utils.hpp"
#include "image_cache.hpp"
// standard headers
@ -45,6 +46,23 @@ struct ServerSettings
std::string key_password{""};
};
struct User
{
User(const std::string& user_permissions_password)
{
std::string perm;
name = utils::string::split_left(user_permissions_password, ':', perm);
perm = utils::string::split_left(perm, ':', password);
permissions = utils::string::split(perm, ',');
}
std::string name;
std::vector<std::string> permissions;
std::string password;
};
std::vector<User> users;
struct Http
{
bool enabled{true};

View file

@ -85,6 +85,9 @@ int main(int argc, char* argv[])
conf.add<Value<string>>("", "ssl.private_key", "private key file (PEM format)", settings.ssl.private_key, &settings.ssl.private_key);
conf.add<Value<string>>("", "ssl.key_password", "key password (for encrypted private key)", settings.ssl.key_password, &settings.ssl.key_password);
// Users setting
auto users_value = conf.add<Value<string>>("", "users.user", "<User nane>:<permissions>:<password>");
// HTTP RPC settings
conf.add<Value<bool>>("", "http.enabled", "enable HTTP Json RPC (HTTP POST and websockets)", settings.http.enabled, &settings.http.enabled);
conf.add<Value<size_t>>("", "http.port", "which port the server should listen on", settings.http.port, &settings.http.port);
@ -265,6 +268,15 @@ int main(int argc, char* argv[])
settings.stream.sources.push_back(sourceValue->value(n));
}
for (size_t n = 0; n < users_value->count(); ++n)
{
settings.users.emplace_back(users_value->value(n));
LOG(DEBUG, LOG_TAG) << "User: " << settings.users.back().name
<< ", permissions: " << utils::string::container_to_string(settings.users.back().permissions)
<< ", pw: " << settings.users.back().password << "\n";
}
#ifdef HAS_DAEMON
std::unique_ptr<Daemon> daemon;
if (daemonOption->is_set())

View file

@ -87,7 +87,5 @@ const std::error_category& category()
std::error_code make_error_code(ControlErrc errc)
{
// Create an error_code with the original mpg123 error value
// and the mpg123 error category.
return std::error_code(static_cast<int>(errc), snapcast::error::control::category());
}

View file

@ -13,11 +13,15 @@ set(TEST_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/test_main.cpp
${CMAKE_SOURCE_DIR}/common/jwt.cpp
${CMAKE_SOURCE_DIR}/common/base64.cpp
${CMAKE_SOURCE_DIR}/common/utils/string_utils.cpp
${CMAKE_SOURCE_DIR}/server/authinfo.cpp
${CMAKE_SOURCE_DIR}/server/streamreader/control_error.cpp
${CMAKE_SOURCE_DIR}/server/streamreader/properties.cpp
${CMAKE_SOURCE_DIR}/server/streamreader/metadata.cpp
${CMAKE_SOURCE_DIR}/server/streamreader/stream_uri.cpp)
include_directories(${Boost_INCLUDE_DIR})
add_executable(snapcast_test ${TEST_SOURCES})
if(ANDROID)

View file

@ -20,8 +20,12 @@
// local headers
#include "common/aixlog.hpp"
#include "common/base64.h"
#include "common/error_code.hpp"
#include "common/jwt.hpp"
#include "common/utils/string_utils.hpp"
#include "server/authinfo.hpp"
#include "server/server_settings.hpp"
#include "server/streamreader/control_error.hpp"
#include "server/streamreader/properties.hpp"
#include "server/streamreader/stream_uri.hpp"
@ -32,6 +36,8 @@
// standard headers
#include <chrono>
#include <regex>
#include <system_error>
#include <vector>
using namespace std;
@ -41,6 +47,32 @@ TEST_CASE("String utils")
{
using namespace utils::string;
REQUIRE(ltrim_copy(" test") == "test");
auto strings = split("1*2", '*');
REQUIRE(strings.size() == 2);
REQUIRE(strings[0] == "1");
REQUIRE(strings[1] == "2");
strings = split("1**2", '*');
REQUIRE(strings.size() == 3);
REQUIRE(strings[0] == "1");
REQUIRE(strings[1] == "");
REQUIRE(strings[2] == "2");
strings = split("*1*2", '*');
REQUIRE(strings.size() == 3);
REQUIRE(strings[0] == "");
REQUIRE(strings[1] == "1");
REQUIRE(strings[2] == "2");
strings = split("*1*2*", '*');
REQUIRE(strings.size() == 3);
REQUIRE(strings[0] == "");
REQUIRE(strings[1] == "1");
REQUIRE(strings[2] == "2");
std::vector<std::string> vec{"1", "2", "3"};
REQUIRE(container_to_string(vec) == "1, 2, 3");
}
@ -574,4 +606,110 @@ TEST_CASE("Error")
ec = make_error_code(ControlErrc::can_not_control);
REQUIRE(ec.category() == snapcast::error::control::category());
std::cout << "Category: " << ec.category().name() << ", " << ec.message() << std::endl;
snapcast::ErrorCode error_code{};
REQUIRE(!error_code);
}
TEST_CASE("ErrorOr")
{
{
snapcast::ErrorOr<std::string> error_or("test");
REQUIRE(error_or.hasValue());
REQUIRE(!error_or.hasError());
// Get value by reference
REQUIRE(error_or.getValue() == "test");
// Move value out
REQUIRE(error_or.takeValue() == "test");
// Value has been moved out, get will return an empty string
REQUIRE(error_or.getValue() == "");
}
{
snapcast::ErrorOr<std::string> error_or(make_error_code(ControlErrc::can_not_control));
REQUIRE(error_or.hasError());
REQUIRE(!error_or.hasValue());
// Get error by reference
REQUIRE(error_or.getError() == make_error_code(ControlErrc::can_not_control));
// Get error by reference
REQUIRE(error_or.getError() == ControlErrc::can_not_control);
// Get error by reference
REQUIRE(error_or.getError() != ControlErrc::parse_error);
// Get error by reference
REQUIRE(error_or.getError() == snapcast::ErrorCode(ControlErrc::can_not_control));
// Move error out
REQUIRE(error_or.takeError() == snapcast::ErrorCode(ControlErrc::can_not_control));
// Error is moved out, will return something else
// REQUIRE(error_or.getError() != snapcast::ErrorCode(ControlErrc::can_not_control));
}
}
TEST_CASE("WildcardMatch")
{
using namespace utils::string;
REQUIRE(wildcardMatch("*", "Server.getToken"));
REQUIRE(wildcardMatch("Server.*", "Server.getToken"));
REQUIRE(wildcardMatch("Server.getToken", "Server.getToken"));
REQUIRE(wildcardMatch("*.getToken", "Server.getToken"));
REQUIRE(wildcardMatch("*.get*", "Server.getToken"));
REQUIRE(wildcardMatch("**.get*", "Server.getToken"));
REQUIRE(wildcardMatch("*.get**", "Server.getToken"));
REQUIRE(wildcardMatch("*.ge**t*", "Server.getToken"));
REQUIRE(!wildcardMatch("*.set*", "Server.getToken"));
REQUIRE(!wildcardMatch(".*", "Server.getToken"));
REQUIRE(!wildcardMatch("*.get", "Server.getToken"));
REQUIRE(wildcardMatch("*erver*get*", "Server.getToken"));
REQUIRE(!wildcardMatch("*get*erver*", "Server.getToken"));
}
TEST_CASE("Auth")
{
{
ServerSettings settings;
ServerSettings::User user("badaix:*:secret");
REQUIRE(user.permissions.size() == 1);
REQUIRE(user.permissions[0] == "*");
settings.users.push_back(user);
AuthInfo auth(settings);
auto ec = auth.authenticateBasic(base64_encode("badaix:secret"));
REQUIRE(!ec);
REQUIRE(auth.hasAuthInfo());
REQUIRE(auth.hasPermission("stream"));
}
{
ServerSettings settings;
ServerSettings::User user("badaix::secret");
REQUIRE(user.permissions.empty());
settings.users.push_back(user);
AuthInfo auth(settings);
auto ec = auth.authenticateBasic(base64_encode("badaix:secret"));
REQUIRE(!ec);
REQUIRE(auth.hasAuthInfo());
REQUIRE(!auth.hasPermission("stream"));
}
{
ServerSettings settings;
ServerSettings::User user("badaix:*:secret");
settings.users.push_back(user);
AuthInfo auth(settings);
auto ec = auth.authenticateBasic(base64_encode("badaix:wrong_password"));
REQUIRE(ec == AuthErrc::wrong_password);
REQUIRE(!auth.hasAuthInfo());
REQUIRE(!auth.hasPermission("stream"));
ec = auth.authenticateBasic(base64_encode("unknown_user:secret"));
REQUIRE(ec == AuthErrc::unknown_user);
REQUIRE(!auth.hasAuthInfo());
REQUIRE(!auth.hasPermission("stream"));
}
}