server/src/http_auth.cpp
Rama main · réplica de lectura del repositorio original.
#include "http_auth.h"
#include "http_auth_request_policy.h"
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
#include "config.h"
#include "confighttp.h"
#include "crypto.h"
#include "httpcommon.h"
#include "logging.h"
#include "network.h"
#include "nvhttp.h"
#include "state_storage.h"
#include "utility.h"
#endif
#include <algorithm>
#include <boost/algorithm/string.hpp>
#include <boost/function.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <chrono>
#include <ctime>
#include <exception>
#include <filesystem>
#include <format>
#include <iostream>
#include <map>
#include <nlohmann/json.hpp>
#include <optional>
#include <ranges>
#include <regex>
#include <set>
#include <Simple-Web-Server/crypto.hpp>
#include <Simple-Web-Server/server_https.hpp>
#include <string>
#include <utility>
#include <vector>
#ifdef SUNSHINE_HTTP_AUTH_POLICY_ONLY
#include <functional>
#include <random>
#endif
using namespace std::literals;
namespace pt = boost::property_tree;
namespace fs = std::filesystem;
namespace confighttp {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
// Global instances for authentication
ApiTokenManager api_token_manager;
SessionTokenManager session_token_manager(SessionTokenManager::make_default_dependencies());
SessionTokenAPI session_token_api(session_token_manager);
#endif
namespace {
constexpr std::chrono::seconds default_remember_me_token_ttl {std::chrono::hours(24 * 7)};
constexpr std::chrono::seconds default_refresh_token_ttl {std::chrono::hours(24)};
std::chrono::seconds remember_me_token_ttl() {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
const auto configured = config::sunshine.remember_me_refresh_token_ttl;
if (configured <= std::chrono::seconds::zero()) {
return default_remember_me_token_ttl;
}
return configured;
#else
return default_remember_me_token_ttl;
#endif
}
std::chrono::seconds configured_session_token_ttl() {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
return config::sunshine.session_token_ttl;
#else
return std::chrono::hours(1);
#endif
}
std::string authentication_state_path() {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
statefile::migrate_recent_state_keys();
return statefile::vibeshine_state_path();
#else
return "sunshine_state.json";
#endif
}
std::string detect_os(const std::string &ua_lower) {
using boost::algorithm::icontains;
if (icontains(ua_lower, "windows nt 10")) {
return "Windows 10/11";
}
if (icontains(ua_lower, "windows nt 6.3")) {
return "Windows 8.1";
}
if (icontains(ua_lower, "windows nt 6.2")) {
return "Windows 8";
}
if (icontains(ua_lower, "windows nt 6.1")) {
return "Windows 7";
}
if (icontains(ua_lower, "mac os x")) {
return "macOS";
}
if (icontains(ua_lower, "iphone") || icontains(ua_lower, "ipad")) {
return "iOS";
}
if (icontains(ua_lower, "android")) {
return "Android";
}
if (icontains(ua_lower, "linux")) {
return "Linux";
}
return {};
}
std::string detect_browser(const std::string &ua_lower) {
using boost::algorithm::icontains;
if (icontains(ua_lower, "edg/")) {
return "Microsoft Edge";
}
if (icontains(ua_lower, "opr/")) {
return "Opera";
}
if (icontains(ua_lower, "chrome/") && !icontains(ua_lower, "edg/") && !icontains(ua_lower, "opr/")) {
return "Google Chrome";
}
if (icontains(ua_lower, "firefox")) {
return "Mozilla Firefox";
}
if (icontains(ua_lower, "safari") && !icontains(ua_lower, "chrome")) {
return "Safari";
}
if (icontains(ua_lower, "brave")) {
return "Brave";
}
return {};
}
std::string truncate_label(const std::string &value) {
constexpr std::size_t kMaxLabelLength = 80;
if (value.size() <= kMaxLabelLength) {
return value;
}
return value.substr(0, kMaxLabelLength - 1) + "…";
}
std::string derive_device_label(const std::string &user_agent, const std::string &remote_address) {
if (user_agent.empty()) {
if (!remote_address.empty()) {
return remote_address;
}
return "Unknown device";
}
std::string ua_lower = boost::algorithm::to_lower_copy(user_agent);
std::string os = detect_os(ua_lower);
std::string browser = detect_browser(ua_lower);
std::string label;
if (!browser.empty() && !os.empty()) {
label = std::format("{} on {}", browser, os);
} else if (!browser.empty()) {
label = browser;
} else if (!os.empty()) {
label = os;
}
if (label.empty()) {
label = truncate_label(user_agent);
}
return label;
}
std::string format_cookie_expires(std::chrono::system_clock::time_point tp) {
auto tt = std::chrono::system_clock::to_time_t(tp);
std::tm tm {};
#if defined(_WIN32)
gmtime_s(&tm, &tt);
#else
gmtime_r(&tt, &tm);
#endif
char buffer[64] {};
if (std::strftime(buffer, sizeof(buffer), "%a, %d %b %Y %H:%M:%S GMT", &tm) == 0) {
return {};
}
return buffer;
}
} // namespace
InvalidScopeException::InvalidScopeException(const std::string &msg):
_message(msg) {}
const char *InvalidScopeException::what() const noexcept {
return _message.c_str();
}
ApiTokenManager::ApiTokenManager(const ApiTokenManagerDependencies &dependencies):
_dependencies(dependencies) {}
bool ApiTokenManager::authenticate_token(const std::string &token, const std::string &path, const std::string &method) {
std::scoped_lock lock(_mutex);
std::string token_hash = _dependencies.hash(token);
auto it = _api_tokens.find(token_hash);
if (it == _api_tokens.end()) {
return false;
}
std::string req_method = boost::to_upper_copy(method);
auto is_method_allowed = [&](const std::set<std::string, std::less<>> &methods) {
return std::ranges::any_of(methods, [&](const std::string &m) {
return boost::iequals(m, req_method);
});
};
auto regex_path_match = [](const std::string &scope_path, const std::string &request_path) {
std::string pattern = scope_path;
if (pattern.empty() || pattern[0] != '^') {
pattern = "^" + pattern;
}
if (pattern.empty() || pattern.back() != '$') {
pattern += "$";
}
std::regex re(pattern);
return std::regex_match(request_path, re);
};
return std::ranges::any_of(it->second.path_methods, [&](const auto &pair) {
const auto &scope_path = pair.first;
const auto &methods = pair.second;
return regex_path_match(scope_path, path) && is_method_allowed(methods);
});
}
bool ApiTokenManager::authenticate_bearer(std::string_view raw_auth, const std::string &path, const std::string &method) {
if (raw_auth.length() <= 7 || !raw_auth.starts_with("Bearer ")) {
return false;
}
auto token = std::string(raw_auth.substr(7));
return authenticate_token(token, path, method);
}
std::optional<std::string> ApiTokenManager::create_api_token(const nlohmann::json &scopes_json, const std::string &username) {
auto path_methods = parse_json_scopes(scopes_json);
if (!path_methods) {
return std::nullopt;
}
std::string token = _dependencies.rand_alphabet(32);
std::string token_hash = _dependencies.hash(token);
ApiTokenInfo info {token_hash, *path_methods, username, _dependencies.now()};
{
std::scoped_lock lock(_mutex);
_api_tokens[token_hash] = info;
}
save_api_tokens();
return token;
}
std::optional<std::string> ApiTokenManager::generate_api_token(const std::string &request_body, const std::string &username) {
nlohmann::json input;
try {
input = nlohmann::json::parse(request_body);
} catch (const nlohmann::json::exception &) {
return std::nullopt;
}
if (!input.contains("scopes") || !input["scopes"].is_array()) {
return std::nullopt;
}
return create_api_token(input["scopes"], username);
}
std::optional<std::map<std::string, std::set<std::string, std::less<>>, std::less<>>>
ApiTokenManager::parse_json_scopes(const nlohmann::json &scopes_json) const {
std::map<std::string, std::set<std::string, std::less<>>, std::less<>> path_methods;
try {
for (const auto &s : scopes_json) {
if (!s.contains("path") || !s.contains("methods") || !s["methods"].is_array()) {
throw InvalidScopeException("Invalid scopes configured on API Token, missing 'path' or 'methods' array");
}
std::string path = s["path"].get<std::string>();
std::set<std::string, std::less<>> methods;
for (const auto &m : s["methods"]) {
methods.insert(boost::to_upper_copy(m.get<std::string>()));
}
path_methods[path] = methods;
}
} catch (const InvalidScopeException &) {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
BOOST_LOG(warning) << "Invalid scope detected in API token, please delete and recreate the token to resolve.";
#endif
return std::nullopt;
}
return path_methods;
}
nlohmann::json ApiTokenManager::get_api_tokens_list() const {
nlohmann::json arr = nlohmann::json::array();
std::scoped_lock lock(_mutex);
for (const auto &[hash, info] : _api_tokens) {
nlohmann::json obj;
obj["hash"] = hash;
obj["username"] = info.username;
obj["created_at"] = std::chrono::duration_cast<std::chrono::seconds>(info.created_at.time_since_epoch()).count();
obj["scopes"] = nlohmann::json::array();
for (const auto &[path, methods] : info.path_methods) {
obj["scopes"].push_back({{"path", path}, {"methods", methods}});
}
arr.push_back(obj);
}
return arr;
}
std::string ApiTokenManager::list_api_tokens_json() const {
return get_api_tokens_list().dump();
}
bool ApiTokenManager::revoke_api_token_by_hash(const std::string &hash) {
if (hash.empty()) {
return false;
}
bool erased = false;
{
std::scoped_lock lock(_mutex);
erased = _api_tokens.erase(hash) > 0;
}
if (erased) {
save_api_tokens();
}
return erased;
}
void ApiTokenManager::save_api_tokens() const {
const auto state_path = authentication_state_path();
nlohmann::json j;
{
std::scoped_lock lock(_mutex);
for (const auto &[hash, info] : _api_tokens) {
nlohmann::json obj;
obj["hash"] = hash;
obj["username"] = info.username;
obj["created_at"] = std::chrono::duration_cast<std::chrono::seconds>(info.created_at.time_since_epoch()).count();
obj["scopes"] = nlohmann::json::array();
for (const auto &[path, methods] : info.path_methods) {
obj["scopes"].push_back({{"path", path}, {"methods", methods}});
}
j.push_back(obj);
}
}
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
std::lock_guard<std::mutex> state_lock(statefile::state_mutex());
#endif
pt::ptree root;
if (_dependencies.file_exists(state_path)) {
try {
_dependencies.read_json(state_path, root);
} catch (const std::exception &e) {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
BOOST_LOG(error) << "ApiTokenManager: refusing to write state after failed read: " << e.what();
#endif
return;
} catch (...) {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
BOOST_LOG(error) << "ApiTokenManager: refusing to write state after unknown read failure";
#endif
return;
}
}
pt::ptree tokens_pt;
for (const auto &tok : j) {
pt::ptree t;
t.put("hash", tok["hash"].get<std::string>());
t.put("username", tok["username"].get<std::string>());
t.put("created_at", tok["created_at"].get<std::int64_t>());
pt::ptree scopes_pt;
for (const auto &s : tok["scopes"]) {
pt::ptree spt;
spt.put("path", s["path"].get<std::string>());
pt::ptree mpt;
for (const auto &m : s["methods"]) {
mpt.push_back({"", pt::ptree(m.get<std::string>())});
}
spt.add_child("methods", mpt);
scopes_pt.push_back({"", spt});
}
t.add_child("scopes", scopes_pt);
tokens_pt.push_back({"", t});
}
root.put_child("root.api_tokens", tokens_pt);
_dependencies.write_json(state_path, root);
}
std::optional<std::pair<std::string, std::set<std::string, std::less<>>>> ApiTokenManager::parse_scope(const pt::ptree &scope_tree) const {
const std::string path = scope_tree.get<std::string>("path", "");
if (path.empty()) {
return std::nullopt;
}
std::set<std::string, std::less<>> methods;
if (auto m_child = scope_tree.get_child_optional("methods")) {
for (const auto &[_, method_node] : *m_child) {
methods.insert(method_node.data());
}
}
if (methods.empty()) {
return std::nullopt;
}
return std::make_pair(path, std::move(methods));
}
std::map<std::string, std::set<std::string, std::less<>>, std::less<>> ApiTokenManager::build_scope_map(const pt::ptree &scopes_node) const {
std::map<std::string, std::set<std::string, std::less<>>, std::less<>> out;
for (const auto &[_, scope_tree] : scopes_node) {
if (auto parsed = parse_scope(scope_tree)) {
auto [path, methods] = std::move(*parsed);
out.try_emplace(std::move(path), std::move(methods));
}
}
return out;
}
void ApiTokenManager::load_api_tokens() {
const auto state_path = authentication_state_path();
pt::ptree root;
bool have_root = false;
{
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
std::lock_guard<std::mutex> state_lock(statefile::state_mutex());
#endif
if (_dependencies.file_exists(state_path)) {
try {
_dependencies.read_json(state_path, root);
have_root = true;
} catch (...) {
}
}
}
std::scoped_lock lock(_mutex);
_api_tokens.clear();
if (!have_root) {
return;
}
if (auto tokens_node = root.get_child_optional("root.api_tokens")) {
for (const auto &[_, token_tree] : *tokens_node) {
ApiTokenInfo info;
info.hash = token_tree.get<std::string>("hash", "");
if (info.hash.empty()) {
continue;
}
info.username = token_tree.get<std::string>("username", "");
std::int64_t created_secs = token_tree.get<std::int64_t>("created_at", 0);
info.created_at = std::chrono::system_clock::time_point(std::chrono::seconds(created_secs));
if (auto scopes_node = token_tree.get_child_optional("scopes")) {
info.path_methods = build_scope_map(*scopes_node);
}
_api_tokens.try_emplace(info.hash, std::move(info));
}
}
}
ApiTokenManagerDependencies ApiTokenManager::make_default_dependencies() {
ApiTokenManagerDependencies dependencies;
dependencies.file_exists = [](const std::string &path) {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
return fs::exists(path) || fs::exists(path + ".bak");
#else
return fs::exists(path);
#endif
};
dependencies.read_json = [](const std::string &path, pt::ptree &tree) {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
if (statefile::load_json(path, tree) != statefile::json_load_result_e::loaded) {
throw std::runtime_error("authentication state is unavailable");
}
#else
boost::property_tree::json_parser::read_json(path, tree);
#endif
};
dependencies.write_json = [](const std::string &path, const pt::ptree &tree) {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
statefile::write_json_atomic(path, tree);
#else
boost::property_tree::json_parser::write_json(path, tree);
#endif
};
dependencies.now = []() {
return std::chrono::system_clock::now();
};
dependencies.rand_alphabet = [](std::size_t length) {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
return crypto::rand_alphabet(length);
#else
static constexpr char alphabet[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
std::string value;
value.reserve(length);
std::random_device random;
for (std::size_t i = 0; i < length; ++i) {
value.push_back(alphabet[random() % (sizeof(alphabet) - 1)]);
}
return value;
#endif
};
dependencies.hash = [](const std::string &input) {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
return util::hex(crypto::hash(input)).to_string();
#else
return std::to_string(std::hash<std::string> {}(input));
#endif
};
return dependencies;
}
const std::map<std::string, ApiTokenInfo, std::less<>> &ApiTokenManager::retrieve_loaded_api_tokens() const {
return _api_tokens;
}
SessionTokenManager::SessionTokenManager(const SessionTokenManagerDependencies &dependencies):
_dependencies(dependencies) {}
SessionTokenManagerDependencies SessionTokenManager::make_default_dependencies() {
SessionTokenManagerDependencies deps;
deps.now = []() {
return std::chrono::system_clock::now();
};
deps.rand_alphabet = [](std::size_t length) {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
return crypto::rand_alphabet(length);
#else
static constexpr char alphabet[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
std::string value;
value.reserve(length);
std::random_device random;
for (std::size_t i = 0; i < length; ++i) {
value.push_back(alphabet[random() % (sizeof(alphabet) - 1)]);
}
return value;
#endif
};
deps.hash = [](const std::string &input) {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
auto hash_result = crypto::hash(input);
return util::hex(hash_result).to_string();
#else
return std::to_string(std::hash<std::string> {}(input));
#endif
};
deps.file_exists = [](const std::string &path) {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
return fs::exists(path) || fs::exists(path + ".bak");
#else
return fs::exists(path);
#endif
};
deps.read_json = [](const std::string &path, pt::ptree &tree) {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
if (statefile::load_json(path, tree) != statefile::json_load_result_e::loaded) {
throw std::runtime_error("authentication state is unavailable");
}
#else
boost::property_tree::json_parser::read_json(path, tree);
#endif
};
deps.write_json = [](const std::string &path, const pt::ptree &tree) {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
statefile::write_json_atomic(path, tree);
#else
boost::property_tree::json_parser::write_json(path, tree);
#endif
};
return deps;
}
std::string SessionTokenManager::generate_session_token(const std::string &username, std::chrono::seconds lifetime, const std::string &user_agent, const std::string &remote_address, bool remember_me) {
return issue_session_tokens(username, lifetime, std::chrono::seconds::zero(), user_agent, remote_address, remember_me).session_token;
}
SessionTokenBundle SessionTokenManager::issue_session_tokens(const std::string &username, std::chrono::seconds session_ttl, std::chrono::seconds refresh_ttl, const std::string &user_agent, const std::string &remote_address, bool remember_me) {
if (session_ttl <= std::chrono::seconds::zero()) {
session_ttl = configured_session_token_ttl();
}
if (refresh_ttl <= std::chrono::seconds::zero()) {
refresh_ttl = remember_me ? remember_me_token_ttl() : default_refresh_token_ttl;
}
if (refresh_ttl < session_ttl) {
refresh_ttl = session_ttl;
}
auto now = _dependencies.now();
auto refresh_expires_at = now + refresh_ttl;
auto session_expires_at = std::min(now + session_ttl, refresh_expires_at);
std::string session_token = _dependencies.rand_alphabet(64);
std::string refresh_token = _dependencies.rand_alphabet(64);
std::string session_hash = _dependencies.hash(session_token);
std::string refresh_hash = _dependencies.hash(refresh_token);
std::string device_label = derive_device_label(user_agent, remote_address);
std::string rotation_id = _dependencies.rand_alphabet(24);
{
std::scoped_lock lock(_mutex);
SessionToken token {
username,
now,
session_expires_at,
refresh_expires_at,
user_agent,
remote_address,
now,
remember_me,
device_label,
refresh_hash,
rotation_id,
};
_session_tokens.erase(session_hash);
_session_tokens.emplace(session_hash, std::move(token));
_refresh_index[refresh_hash] = session_hash;
_dirty = true;
}
cleanup_expired_session_tokens();
save_session_tokens();
SessionTokenBundle bundle;
bundle.session_token = std::move(session_token);
bundle.refresh_token = std::move(refresh_token);
bundle.session_ttl = std::chrono::duration_cast<std::chrono::seconds>(session_expires_at - now);
bundle.refresh_ttl = std::chrono::duration_cast<std::chrono::seconds>(refresh_expires_at - now);
bundle.remember_me = remember_me;
return bundle;
}
std::optional<SessionTokenBundle> SessionTokenManager::refresh_session_tokens(const std::string &refresh_token, const std::string &user_agent, const std::string &remote_address) {
if (refresh_token.empty()) {
return std::nullopt;
}
auto now = _dependencies.now();
std::string refresh_hash = _dependencies.hash(refresh_token);
std::optional<SessionTokenBundle> result;
bool needs_persist = false;
bool invalid = false;
{
std::scoped_lock lock(_mutex);
auto ref_it = _refresh_index.find(refresh_hash);
if (ref_it == _refresh_index.end()) {
return std::nullopt;
}
auto sess_it = _session_tokens.find(ref_it->second);
if (sess_it == _session_tokens.end()) {
_refresh_index.erase(ref_it);
_dirty = true;
needs_persist = true;
invalid = true;
}
if (!invalid) {
auto &entry = sess_it->second;
if (now > entry.refresh_expires_at) {
_refresh_index.erase(ref_it);
_session_tokens.erase(sess_it);
_dirty = true;
needs_persist = true;
invalid = true;
} else {
const bool remember_me = entry.remember_me;
auto session_ttl = configured_session_token_ttl();
auto refresh_expires_at = entry.refresh_expires_at; // keep absolute lifetime; no sliding extension
auto session_expires_at = std::min(now + session_ttl, refresh_expires_at);
std::string new_session_token = _dependencies.rand_alphabet(64);
std::string new_refresh_token = _dependencies.rand_alphabet(64);
std::string new_session_hash = _dependencies.hash(new_session_token);
std::string new_refresh_hash = _dependencies.hash(new_refresh_token);
std::string device_label = derive_device_label(user_agent, remote_address);
std::string rotation_id = _dependencies.rand_alphabet(24);
SessionToken updated {
entry.username,
now,
session_expires_at,
refresh_expires_at,
user_agent,
remote_address,
now,
remember_me,
device_label,
new_refresh_hash,
rotation_id,
};
_session_tokens.erase(sess_it);
_refresh_index.erase(ref_it);
_session_tokens.emplace(new_session_hash, std::move(updated));
_refresh_index[new_refresh_hash] = new_session_hash;
_dirty = true;
needs_persist = true;
result = SessionTokenBundle {
std::move(new_session_token),
std::move(new_refresh_token),
std::chrono::duration_cast<std::chrono::seconds>(session_expires_at - now),
std::chrono::duration_cast<std::chrono::seconds>(refresh_expires_at - now),
remember_me,
};
}
}
}
if (needs_persist) {
save_session_tokens();
}
if (invalid) {
return std::nullopt;
}
return result;
}
bool SessionTokenManager::validate_session_token(const std::string &token) {
bool should_persist = false;
bool valid = false;
{
std::scoped_lock lock(_mutex);
std::string token_hash = _dependencies.hash(token);
auto it = _session_tokens.find(token_hash);
if (it == _session_tokens.end()) {
return false;
}
auto now = _dependencies.now();
if (now > it->second.refresh_expires_at) {
if (!it->second.refresh_token_hash.empty()) {
_refresh_index.erase(it->second.refresh_token_hash);
}
_session_tokens.erase(it);
_dirty = true;
should_persist = true;
return false;
}
if (now > it->second.expires_at) {
return false; // access token expired but refresh token may still be valid
}
valid = true;
if (now - it->second.last_seen >= std::chrono::minutes(5)) {
it->second.last_seen = now;
_dirty = true;
should_persist = true;
}
}
if (should_persist) {
save_session_tokens();
}
return valid;
}
void SessionTokenManager::revoke_session_token(const std::string &token) {
std::string token_hash = _dependencies.hash(token);
revoke_session_by_hash(token_hash);
}
bool SessionTokenManager::revoke_session_by_hash(const std::string &token_hash) {
bool removed = false;
{
std::scoped_lock lock(_mutex);
if (auto it = _session_tokens.find(token_hash); it != _session_tokens.end()) {
if (!it->second.refresh_token_hash.empty()) {
_refresh_index.erase(it->second.refresh_token_hash);
}
_session_tokens.erase(it);
removed = true;
_dirty = true;
}
}
if (removed) {
save_session_tokens();
}
return removed;
}
bool SessionTokenManager::revoke_refresh_token(const std::string &refresh_token) {
if (refresh_token.empty()) {
return false;
}
std::string refresh_hash = _dependencies.hash(refresh_token);
std::optional<std::string> session_hash;
{
std::scoped_lock lock(_mutex);
auto it = _refresh_index.find(refresh_hash);
if (it == _refresh_index.end()) {
return false;
}
session_hash = it->second;
}
if (session_hash) {
return revoke_session_by_hash(*session_hash);
}
return false;
}
bool SessionTokenManager::cleanup_expired_session_tokens() {
bool removed = false;
{
std::scoped_lock lock(_mutex);
auto now = _dependencies.now();
auto erased = std::erase_if(_session_tokens, [&](const auto &pair) {
if (now > pair.second.refresh_expires_at) {
if (!pair.second.refresh_token_hash.empty()) {
_refresh_index.erase(pair.second.refresh_token_hash);
}
return true;
}
return false;
});
if (erased > 0) {
removed = true;
_dirty = true;
}
}
return removed;
}
std::optional<std::string> SessionTokenManager::get_username_for_token(const std::string &token) {
std::optional<std::string> username;
bool should_persist = false;
{
std::scoped_lock lock(_mutex);
std::string token_hash = _dependencies.hash(token);
auto it = _session_tokens.find(token_hash);
if (it == _session_tokens.end()) {
return std::nullopt;
}
auto now = _dependencies.now();
if (now > it->second.refresh_expires_at) {
if (!it->second.refresh_token_hash.empty()) {
_refresh_index.erase(it->second.refresh_token_hash);
}
_session_tokens.erase(it);
_dirty = true;
should_persist = true;
return std::nullopt;
}
if (now > it->second.expires_at) {
return std::nullopt;
}
username = it->second.username;
if (now - it->second.last_seen >= std::chrono::minutes(5)) {
it->second.last_seen = now;
_dirty = true;
should_persist = true;
}
}
if (should_persist) {
save_session_tokens();
}
return username;
}
size_t SessionTokenManager::session_count() const {
std::scoped_lock lock(_mutex);
return _session_tokens.size();
}
std::optional<std::string> SessionTokenManager::get_hash_for_token(const std::string &token) const {
std::scoped_lock lock(_mutex);
std::string token_hash = _dependencies.hash(token);
auto it = _session_tokens.find(token_hash);
if (it == _session_tokens.end()) {
return std::nullopt;
}
auto now = _dependencies.now();
if (now > it->second.refresh_expires_at || now > it->second.expires_at) {
return std::nullopt;
}
return token_hash;
}
void SessionTokenManager::save_session_tokens() const {
const auto state_path = authentication_state_path();
std::vector<std::pair<std::string, SessionToken>> snapshot;
bool had_dirty = false;
{
std::scoped_lock lock(_mutex);
if (!_dirty) {
return;
}
snapshot.reserve(_session_tokens.size());
for (const auto &entry : _session_tokens) {
snapshot.push_back(entry);
}
_dirty = false;
had_dirty = true;
}
bool write_failed = false;
{
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
std::lock_guard<std::mutex> state_lock(statefile::state_mutex());
#endif
pt::ptree root;
if (_dependencies.file_exists(state_path)) {
try {
_dependencies.read_json(state_path, root);
} catch (const std::exception &e) {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
BOOST_LOG(error) << "SessionTokenManager: refusing to write state after failed read: " << e.what();
#endif
write_failed = true;
} catch (...) {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
BOOST_LOG(error) << "SessionTokenManager: refusing to write state after unknown read failure";
#endif
write_failed = true;
}
}
if (!write_failed) {
pt::ptree sessions_pt;
for (const auto &entry : snapshot) {
const auto &hash = entry.first;
const auto &token = entry.second;
pt::ptree node;
node.put("hash", hash);
node.put("username", token.username);
node.put("created_at", std::chrono::duration_cast<std::chrono::seconds>(token.created_at.time_since_epoch()).count());
node.put("expires_at", std::chrono::duration_cast<std::chrono::seconds>(token.expires_at.time_since_epoch()).count());
node.put("refresh_expires_at", std::chrono::duration_cast<std::chrono::seconds>(token.refresh_expires_at.time_since_epoch()).count());
node.put("last_seen", std::chrono::duration_cast<std::chrono::seconds>(token.last_seen.time_since_epoch()).count());
node.put("remember_me", token.remember_me);
if (!token.refresh_token_hash.empty()) {
node.put("refresh_token_hash", token.refresh_token_hash);
}
if (!token.rotation_id.empty()) {
node.put("rotation_id", token.rotation_id);
}
if (!token.user_agent.empty()) {
node.put("user_agent", token.user_agent);
}
if (!token.remote_address.empty()) {
node.put("remote_address", token.remote_address);
}
if (!token.device_label.empty()) {
node.put("device_label", token.device_label);
}
sessions_pt.push_back({"", node});
}
root.put_child("root.session_tokens", sessions_pt);
try {
_dependencies.write_json(state_path, root);
} catch (const std::exception &e) {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
BOOST_LOG(error) << "SessionTokenManager: failed writing state file: " << e.what();
#endif
write_failed = true;
}
}
}
if (had_dirty) {
std::scoped_lock lock(_mutex);
if (write_failed) {
_dirty = true;
} else if (!_dirty) {
_last_persist = _dependencies.now();
}
}
}
void SessionTokenManager::load_session_tokens() {
const auto state_path = authentication_state_path();
pt::ptree root;
bool have_root = false;
bool load_failed = false;
{
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
std::lock_guard<std::mutex> state_lock(statefile::state_mutex());
#endif
if (_dependencies.file_exists(state_path)) {
try {
_dependencies.read_json(state_path, root);
have_root = true;
} catch (const std::exception &e) {
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
BOOST_LOG(warning) << "SessionTokenManager: failed loading state file: " << e.what();
#endif
load_failed = true;
}
}
}
bool needs_resave = false;
auto now = _dependencies.now();
{
std::scoped_lock lock(_mutex);
_session_tokens.clear();
_refresh_index.clear();
if (!have_root || load_failed) {
_dirty = false;
_last_persist = now;
return;
}
if (auto sessions_node = root.get_child_optional("root.session_tokens")) {
for (const auto &[_, node] : *sessions_node) {
auto hash = node.get<std::string>("hash", "");
if (hash.empty()) {
continue;
}
SessionToken token;
token.username = node.get<std::string>("username", "");
auto created_secs = node.get<std::int64_t>("created_at", 0);
token.created_at = std::chrono::system_clock::time_point(std::chrono::seconds(created_secs));
auto expires_secs = node.get<std::int64_t>("expires_at", 0);
token.expires_at = std::chrono::system_clock::time_point(std::chrono::seconds(expires_secs));
auto refresh_expires_secs = node.get<std::int64_t>("refresh_expires_at", expires_secs);
token.refresh_expires_at = std::chrono::system_clock::time_point(std::chrono::seconds(refresh_expires_secs));
auto last_seen_secs = node.get<std::int64_t>("last_seen", created_secs);
token.last_seen = std::chrono::system_clock::time_point(std::chrono::seconds(last_seen_secs));
token.remember_me = node.get<bool>("remember_me", false);
token.refresh_token_hash = node.get<std::string>("refresh_token_hash", "");
token.rotation_id = node.get<std::string>("rotation_id", "");
token.user_agent = node.get<std::string>("user_agent", "");
token.remote_address = node.get<std::string>("remote_address", "");
token.device_label = node.get<std::string>("device_label", "");
if (token.device_label.empty()) {
token.device_label = derive_device_label(token.user_agent, token.remote_address);
needs_resave = true;
}
if (token.refresh_token_hash.empty()) {
// Legacy sessions without refresh support expire with the access token
token.refresh_token_hash.clear();
token.refresh_expires_at = token.expires_at;
}
if (token.expires_at > token.refresh_expires_at) {
token.expires_at = token.refresh_expires_at;
needs_resave = true;
}
if (now > token.refresh_expires_at) {
continue;
}
auto inserted = _session_tokens.emplace(hash, std::move(token));
if (inserted.second && !inserted.first->second.refresh_token_hash.empty()) {
_refresh_index[inserted.first->second.refresh_token_hash] = inserted.first->first;
}
}
}
_dirty = needs_resave;
_last_persist = now;
}
if (needs_resave) {
save_session_tokens();
}
}
std::vector<SessionTokenView> SessionTokenManager::list_sessions(const std::string &username_filter) const {
std::vector<SessionTokenView> out;
std::scoped_lock lock(_mutex);
auto now = _dependencies.now();
out.reserve(_session_tokens.size());
for (const auto &[hash, token] : _session_tokens) {
if (now > token.refresh_expires_at) {
continue;
}
if (!username_filter.empty() && !boost::iequals(token.username, username_filter)) {
continue;
}
out.push_back(SessionTokenView {
hash,
token.username,
token.created_at,
token.expires_at,
token.refresh_expires_at,
token.last_seen,
token.remember_me,
token.user_agent,
token.remote_address,
token.device_label,
});
}
return out;
}
#ifndef SUNSHINE_HTTP_AUTH_POLICY_ONLY
SessionTokenAPI::SessionTokenAPI(SessionTokenManager &session_manager):
_session_manager(session_manager) {}
APIResponse SessionTokenAPI::login(const std::string &username, const std::string &password, const std::string &redirect_url, bool remember_me, const std::string &user_agent, const std::string &remote_address) {
if (!validate_credentials(username, password)) {
BOOST_LOG(info) << "Configuration API: Login failed for user: " << username;
return create_error_response("Invalid credentials", StatusCode::client_error_unauthorized);
}
auto issued = _session_manager.issue_session_tokens(
username,
config::sunshine.session_token_ttl,
std::chrono::seconds::zero(),
user_agent,
remote_address,
remember_me
);
nlohmann::json response_data;
response_data["token"] = issued.session_token;
response_data["expires_in"] = issued.session_ttl.count();
response_data["refresh_expires_in"] = issued.refresh_ttl.count();
response_data["remember_me"] = remember_me;
// Hardened secure redirect handling
std::string safe_redirect = "/";
if (!redirect_url.empty() && redirect_url[0] == '/') {
std::string lower = redirect_url;
boost::algorithm::to_lower(lower);
// Disallow dangerous patterns
if (!boost::algorithm::contains(lower, "://") &&
!boost::algorithm::contains(lower, "%2f") &&
!boost::algorithm::contains(lower, "\\") &&
!boost::algorithm::contains(lower, "/..") &&
!(redirect_url.size() > 1 && redirect_url[1] == '/')) { // reject double slash
// Unicode normalization: reject if normalized path differs
std::string norm = redirect_url;
std::ranges::replace(norm, '\\', '/');
if (norm == redirect_url) {
safe_redirect = redirect_url;
}
}
}
response_data["redirect"] = safe_redirect;
APIResponse response = create_success_response(response_data);
// Set session cookie with Secure if HTTPS or localhost
// Percent-encode token for safe cookie storage
auto now = std::chrono::system_clock::now();
auto append_expiry = [&](std::string &cookie, std::chrono::seconds ttl) {
cookie += std::format("; Max-Age={}", ttl.count());
auto expires_at = now + ttl;
if (auto expires_str = format_cookie_expires(expires_at); !expires_str.empty()) {
cookie += "; Expires=" + expires_str;
}
};
std::string encoded_session = http::cookie_escape(issued.session_token);
std::string session_cookie = std::string(session_cookie_name) + "=" + encoded_session + "; Path=/; HttpOnly; SameSite=Strict; Secure; Priority=High";
append_expiry(session_cookie, issued.session_ttl);
response.headers.emplace("Set-Cookie", session_cookie);
std::string encoded_refresh = http::cookie_escape(issued.refresh_token);
std::string refresh_cookie = std::string(refresh_cookie_name) + "=" + encoded_refresh + "; Path=/; HttpOnly; SameSite=Strict; Secure; Priority=High";
append_expiry(refresh_cookie, issued.refresh_ttl);
response.headers.emplace("Set-Cookie", refresh_cookie);
// Set CORS header for localhost only (no wildcard), dynamically set port
std::uint16_t https_port = net::map_port(nvhttp::PORT_HTTPS);
std::string cors_origin = std::format("https://localhost:{}", https_port);
response.headers.emplace("Access-Control-Allow-Origin", cors_origin);
return response;
}
APIResponse SessionTokenAPI::logout(const std::string &session_token, const std::string &refresh_token) {
if (!session_token.empty()) {
_session_manager.revoke_session_token(session_token);
} else if (!refresh_token.empty()) {
_session_manager.revoke_refresh_token(refresh_token);
}
nlohmann::json response_data;
response_data["message"] = "Logged out successfully";
// Create success response and clear the session cookie so the client no longer retains it
APIResponse response = create_success_response(response_data);
// Set-Cookie header to clear the session token on client
std::string clear_cookie = std::string(session_cookie_name) + "=; Path=/; HttpOnly; SameSite=Strict; Secure; Priority=High; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0";
std::string clear_refresh_cookie = std::string(refresh_cookie_name) + "=; Path=/; HttpOnly; SameSite=Strict; Secure; Priority=High; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0";
response.headers.emplace("Set-Cookie", clear_cookie);
response.headers.emplace("Set-Cookie", clear_refresh_cookie);
return response;
}
APIResponse SessionTokenAPI::validate_session(const std::string &session_token) {
if (session_token.empty()) {
return create_error_response("Session token required", SimpleWeb::StatusCode::client_error_unauthorized);
}
if (bool is_valid = _session_manager.validate_session_token(session_token); !is_valid) {
return create_error_response("Invalid or expired session token", SimpleWeb::StatusCode::client_error_unauthorized);
}
return create_success_response();
}
APIResponse SessionTokenAPI::refresh_session(const std::string &refresh_token, const std::string &user_agent, const std::string &remote_address) {
if (refresh_token.empty()) {
return create_error_response("Refresh token required", StatusCode::client_error_unauthorized);
}
auto refreshed = _session_manager.refresh_session_tokens(refresh_token, user_agent, remote_address);
if (!refreshed) {
return create_error_response("Invalid or expired refresh token", StatusCode::client_error_unauthorized);
}
nlohmann::json response_data;
response_data["token"] = refreshed->session_token;
response_data["expires_in"] = refreshed->session_ttl.count();
response_data["refresh_expires_in"] = refreshed->refresh_ttl.count();
response_data["remember_me"] = refreshed->remember_me;
APIResponse response = create_success_response(response_data);
auto now = std::chrono::system_clock::now();
auto append_expiry = [&](std::string &cookie, std::chrono::seconds ttl) {
cookie += std::format("; Max-Age={}", ttl.count());
auto expires_at = now + ttl;
if (auto expires_str = format_cookie_expires(expires_at); !expires_str.empty()) {
cookie += "; Expires=" + expires_str;
}
};
std::string encoded_session = http::cookie_escape(refreshed->session_token);
std::string session_cookie = std::string(session_cookie_name) + "=" + encoded_session + "; Path=/; HttpOnly; SameSite=Strict; Secure; Priority=High";
append_expiry(session_cookie, refreshed->session_ttl);
response.headers.emplace("Set-Cookie", session_cookie);
std::string encoded_refresh = http::cookie_escape(refreshed->refresh_token);
std::string refresh_cookie = std::string(refresh_cookie_name) + "=" + encoded_refresh + "; Path=/; HttpOnly; SameSite=Strict; Secure; Priority=High";
append_expiry(refresh_cookie, refreshed->refresh_ttl);
response.headers.emplace("Set-Cookie", refresh_cookie);
return response;
}
APIResponse SessionTokenAPI::list_sessions(const std::string &username_filter, const std::string &active_session_hash) const {
auto sessions = _session_manager.list_sessions(username_filter);
nlohmann::json response;
response["sessions"] = nlohmann::json::array();
auto to_seconds = [](const std::chrono::system_clock::time_point &tp) {
return std::chrono::duration_cast<std::chrono::seconds>(tp.time_since_epoch()).count();
};
for (const auto &session : sessions) {
nlohmann::json entry;
entry["id"] = session.hash;
entry["username"] = session.username;
entry["created_at"] = to_seconds(session.created_at);
entry["expires_at"] = to_seconds(session.expires_at);
entry["refresh_expires_at"] = to_seconds(session.refresh_expires_at);
entry["last_seen"] = to_seconds(session.last_seen);
entry["remember_me"] = session.remember_me;
entry["current"] = (!active_session_hash.empty() && active_session_hash == session.hash);
if (!session.user_agent.empty()) {
entry["user_agent"] = session.user_agent;
}
if (!session.remote_address.empty()) {
entry["remote_address"] = session.remote_address;
}
if (!session.device_label.empty()) {
entry["device_label"] = session.device_label;
}
response["sessions"].push_back(std::move(entry));
}
return create_success_response(response);
}
APIResponse SessionTokenAPI::revoke_session_by_hash(const std::string &session_hash) {
if (session_hash.empty()) {
return create_error_response("Session identifier required", StatusCode::client_error_bad_request);
}
if (!_session_manager.revoke_session_by_hash(session_hash)) {
return create_error_response("Session not found", StatusCode::client_error_not_found);
}
nlohmann::json data;
data["message"] = "Session revoked";
return create_success_response(data);
}
bool SessionTokenAPI::validate_credentials(const std::string &username, const std::string &password) const {
if (auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
!boost::iequals(username, config::sunshine.username) || hash != config::sunshine.password) {
return false;
}
return true;
}
namespace {
policy::RequestAuthPolicy make_request_auth_policy() {
policy::RequestAuthDependencies dependencies;
dependencies.remote_allowed = [](const std::string &address) { return net::from_address(address) <= http::origin_web_ui_allowed; };
dependencies.credentials_configured = [] { return !config::sunshine.username.empty(); };
dependencies.credentials_valid = [](const std::string &username, const std::string &password) {
return boost::iequals(username, config::sunshine.username) && util::hex(crypto::hash(password + config::sunshine.salt)).to_string() == config::sunshine.password;
};
dependencies.bearer_valid = [](const std::string &raw, const std::string &path, const std::string &method) { return api_token_manager.authenticate_bearer(raw, path, method); };
dependencies.session_valid = [](const std::string &token) { return session_token_api.validate_session(token).status_code == StatusCode::success_ok; };
dependencies.decode_base64 = [](const std::string &encoded) { return SimpleWeb::Crypto::Base64::decode(encoded); };
dependencies.cookie_unescape = [](const std::string &value) { return http::cookie_unescape(value); };
dependencies.https_port = [] { return net::map_port(confighttp::PORT_HTTPS); };
return policy::RequestAuthPolicy(std::move(dependencies));
}
std::string get_cors_origin() {
std::uint16_t https_port = net::map_port(confighttp::PORT_HTTPS);
return std::format("https://localhost:{}", https_port);
}
} // namespace
APIResponse SessionTokenAPI::create_success_response(const nlohmann::json &data) const {
nlohmann::json response_body;
response_body["status"] = true;
for (auto &[key, value] : data.items()) {
response_body[key] = value;
}
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "application/json");
headers.emplace("Access-Control-Allow-Origin", get_cors_origin());
return APIResponse(StatusCode::success_ok, response_body.dump(), headers);
}
APIResponse SessionTokenAPI::create_error_response(const std::string &error_message, StatusCode status_code) const {
nlohmann::json response_body;
response_body["status"] = false;
response_body["error"] = error_message;
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "application/json");
headers.emplace("Access-Control-Allow-Origin", get_cors_origin());
return APIResponse(status_code, response_body.dump(), headers);
}
AuthResult make_auth_error(StatusCode code, const std::string &error) {
return make_request_auth_policy().make_error(code, error);
#if 0
AuthResult result {false, code, {}, {}};
// Always add CORS origin header for auth errors (and redirects) to satisfy UI expectations
{
std::uint16_t https_port = net::map_port(confighttp::PORT_HTTPS);
std::string cors_origin = std::format("https://localhost:{}", https_port);
result.headers.emplace("Access-Control-Allow-Origin", cors_origin);
}
if (!error.empty()) {
nlohmann::json j;
j["status"] = false;
j["error"] = error;
result.body = j.dump();
result.headers.emplace("Content-Type", "application/json");
}
return result;
#endif
}
AuthResult make_basic_auth_error(const std::string &error_message = "Unauthorized") {
AuthResult result = make_auth_error(StatusCode::client_error_unauthorized, error_message);
result.headers.emplace("WWW-Authenticate", "Basic realm=\"Sunshine\"");
return result;
}
namespace {
std::optional<std::pair<std::string, std::string>> parse_basic_credentials(const std::string &raw_auth) {
auto space_pos = raw_auth.find(' ');
if (space_pos == std::string::npos) {
return std::nullopt;
}
std::string scheme = raw_auth.substr(0, space_pos);
if (!boost::iequals(scheme, "Basic")) {
return std::nullopt;
}
std::string encoded = raw_auth.substr(space_pos + 1);
if (encoded.empty()) {
return std::nullopt;
}
try {
std::string decoded = SimpleWeb::Crypto::Base64::decode(encoded);
auto colon_pos = decoded.find(':');
if (colon_pos == std::string::npos) {
return std::nullopt;
}
return std::make_pair(decoded.substr(0, colon_pos), decoded.substr(colon_pos + 1));
} catch (...) {
return std::nullopt;
}
}
bool validate_basic_credentials(const std::string &username, const std::string &password) {
if (config::sunshine.username.empty()) {
return false;
}
auto hashed = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
return boost::iequals(username, config::sunshine.username) && hashed == config::sunshine.password;
}
} // namespace
AuthResult check_basic_auth(const std::string &raw_auth) {
if (auto credentials = parse_basic_credentials(raw_auth);
credentials && validate_basic_credentials(credentials->first, credentials->second)) {
return {true, StatusCode::success_ok, {}, {}};
}
return make_basic_auth_error("Unauthorized");
}
AuthResult check_bearer_auth(const std::string &raw_auth, const std::string &path, const std::string &method) {
return make_request_auth_policy().check_bearer(raw_auth, path, method);
#if 0
if (!api_token_manager.authenticate_bearer(raw_auth, path, method)) {
return make_auth_error(StatusCode::client_error_forbidden, "Forbidden: Token does not have permission for this path/method.");
}
return {true, StatusCode::success_ok, {}, {}};
#endif
}
AuthResult check_session_auth(const std::string &raw_auth) {
return make_request_auth_policy().check_session(raw_auth);
#if 0
if (raw_auth.rfind("Session ", 0) != 0) {
return make_auth_error(StatusCode::client_error_unauthorized, "Invalid session token format");
}
std::string token = raw_auth.substr(8);
if (APIResponse api_response = session_token_api.validate_session(token); api_response.status_code == StatusCode::success_ok) {
return {true, StatusCode::success_ok, {}, {}};
}
return make_auth_error(StatusCode::client_error_unauthorized, "Invalid or expired session token");
#endif
}
bool is_html_request(const std::string &path) {
return make_request_auth_policy().is_html_request(path);
#if 0
// API requests start with /api/
if (path.rfind("/api/", 0) == 0) {
return false;
}
// Asset requests in known directories
if (path.rfind("/assets/", 0) == 0 || path.rfind("/images/", 0) == 0) {
return false;
}
// Static file extensions should not be treated as HTML
{
std::string ext = std::filesystem::path(path).extension().string();
boost::algorithm::to_lower(ext);
static const std::vector<std::string> non_html_ext = {".js", ".css", ".map", ".json", ".woff", ".woff2", ".ttf", ".eot", ".ico", ".png", ".jpg", ".jpeg", ".svg"};
if (std::ranges::find(non_html_ext, ext) != non_html_ext.end()) {
return false;
}
}
// Everything else is likely an HTML page request
return true;
#endif
}
AuthResult check_auth(const std::string &remote_address, const std::string &auth_header, const std::string &path, const std::string &method) {
return make_request_auth_policy().check(remote_address, auth_header, path, method);
#if 0
// Strip query string from path for matching
auto base_path = path;
if (auto qpos = base_path.find('?'); qpos != std::string::npos) {
base_path.resize(qpos);
}
// Allow welcome page without authentication
if (base_path == "/welcome" || base_path == "/welcome/") {
return {true, StatusCode::success_ok, {}, {}};
}
if (auto ip_type = net::from_address(remote_address); ip_type > http::origin_web_ui_allowed) {
BOOST_LOG(info) << "Configuration API: ["sv << remote_address << "] -- denied"sv;
return make_auth_error(StatusCode::client_error_forbidden, "Forbidden");
}
// If no username configured yet, allow unauthenticated access so SPA can drive setup (except protected APIs later)
bool credentials_configured = !config::sunshine.username.empty();
// Only protect /api/ endpoints (except auth endpoints) for SPA model; all other paths (HTML shell, assets) are always allowed
bool is_api = base_path.rfind("/api/", 0) == 0;
bool is_auth_api = (base_path == "/api/auth/login" || base_path == "/api/auth/logout" || base_path == "/api/csrf-token");
if (!is_api) {
return {true, StatusCode::success_ok, {}, {}}; // public content served; SPA handles routing and will trigger API calls
}
if (is_auth_api) {
return {true, StatusCode::success_ok, {}, {}}; // login/logout endpoints public (logout will no-op if no token)
}
// From here on: /api/ (non-auth) endpoints must have credentials configured and valid session or bearer token
if (!credentials_configured) {
return make_auth_error(StatusCode::client_error_unauthorized, "Credentials not configured");
}
if (auth_header.empty()) {
return make_auth_error(StatusCode::client_error_unauthorized, "Unauthorized");
}
if (auth_header.rfind("Bearer ", 0) == 0) {
return check_bearer_auth(auth_header, path, method);
}
if (auth_header.rfind("Session ", 0) == 0) {
{
auto session_res = check_session_auth(auth_header);
if (!session_res.ok) {
return make_auth_error(StatusCode::client_error_unauthorized, "Invalid or expired session token");
}
return session_res;
}
}
if (auto scheme_end = auth_header.find(' ');
scheme_end != std::string::npos && boost::iequals(auth_header.substr(0, scheme_end), "Basic")) {
return check_basic_auth(auth_header);
}
// Default: unauthorized
return make_auth_error(StatusCode::client_error_unauthorized, "Unauthorized");
#endif
}
namespace {
[[maybe_unused]] std::string extract_cookie_value(const SimpleWeb::CaseInsensitiveMultimap &headers, std::string_view name) {
if (auto cookie_it = headers.find("Cookie"); cookie_it != headers.end()) {
const std::string &cookies = cookie_it->second;
const std::string prefix = std::string(name) + "=";
auto pos = cookies.find(prefix);
if (pos != std::string::npos) {
pos += prefix.size();
auto end = cookies.find(';', pos);
auto raw = cookies.substr(pos, end == std::string::npos ? std::string::npos : end - pos);
return http::cookie_unescape(raw);
}
}
return {};
}
} // namespace
std::string extract_session_token_from_cookie(const SimpleWeb::CaseInsensitiveMultimap &headers) {
return make_request_auth_policy().extract_cookie(headers, session_cookie_name);
}
std::string extract_refresh_token_from_cookie(const SimpleWeb::CaseInsensitiveMultimap &headers) {
return make_request_auth_policy().extract_cookie(headers, refresh_cookie_name);
}
#endif
} // namespace confighttp