Files
Apollo/src/nvhttp.cpp
2025-04-10 05:46:41 +08:00

1806 lines
62 KiB
C++

/**
* @file src/nvhttp.cpp
* @brief Definitions for the nvhttp (GameStream) server.
*/
// macros
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
// standard includes
#include <filesystem>
#include <string>
#include <utility>
#include <string>
// lib includes
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/context_base.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <Simple-Web-Server/server_http.hpp>
// local includes
#include "config.h"
#include "display_device.h"
#include "file_handler.h"
#include "globals.h"
#include "httpcommon.h"
#include "logging.h"
#include "network.h"
#include "nvhttp.h"
#include "platform/common.h"
#include "process.h"
#include "rtsp.h"
#include "stream.h"
#include "system_tray.h"
#include "utility.h"
#include "uuid.h"
#include "video.h"
#ifdef _WIN32
#include "platform/windows/virtual_display.h"
#endif
using namespace std::literals;
namespace nvhttp {
namespace fs = std::filesystem;
namespace pt = boost::property_tree;
using p_named_cert_t = crypto::p_named_cert_t;
using PERM = crypto::PERM;
struct client_t {
std::vector<p_named_cert_t> named_devices;
};
struct pair_session_t;
crypto::cert_chain_t cert_chain;
static std::string one_time_pin;
static std::string otp_passphrase;
static std::string otp_device_name;
static std::chrono::time_point<std::chrono::steady_clock> otp_creation_time;
class SunshineHTTPSServer: public SimpleWeb::ServerBase<SunshineHTTPS> {
public:
SunshineHTTPSServer(const std::string &certification_file, const std::string &private_key_file):
ServerBase<SunshineHTTPS>::ServerBase(443),
context(boost::asio::ssl::context::tls_server) {
// Disabling TLS 1.0 and 1.1 (see RFC 8996)
context.set_options(boost::asio::ssl::context::no_tlsv1);
context.set_options(boost::asio::ssl::context::no_tlsv1_1);
context.use_certificate_chain_file(certification_file);
context.use_private_key_file(private_key_file, boost::asio::ssl::context::pem);
}
std::function<bool(std::shared_ptr<Request>, SSL*)> verify;
std::function<void(std::shared_ptr<Response>, std::shared_ptr<Request>)> on_verify_failed;
protected:
boost::asio::ssl::context context;
void after_bind() override {
if (verify) {
context.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once);
context.set_verify_callback([](int verified, boost::asio::ssl::verify_context &ctx) {
// To respond with an error message, a connection must be established
return 1;
});
}
}
// This is Server<HTTPS>::accept() with SSL validation support added
void accept() override {
auto connection = create_connection(*io_service, context);
acceptor->async_accept(connection->socket->lowest_layer(), [this, connection](const SimpleWeb::error_code &ec) {
auto lock = connection->handler_runner->continue_lock();
if (!lock) {
return;
}
if (ec != SimpleWeb::error::operation_aborted) {
this->accept();
}
auto session = std::make_shared<Session>(config.max_request_streambuf_size, connection);
if (!ec) {
boost::asio::ip::tcp::no_delay option(true);
SimpleWeb::error_code ec;
session->connection->socket->lowest_layer().set_option(option, ec);
session->connection->set_timeout(config.timeout_request);
session->connection->socket->async_handshake(boost::asio::ssl::stream_base::server, [this, session](const SimpleWeb::error_code &ec) {
session->connection->cancel_timeout();
auto lock = session->connection->handler_runner->continue_lock();
if (!lock) {
return;
}
if (!ec) {
if (verify && !verify(session->request, session->connection->socket->native_handle())) {
this->write(session, on_verify_failed);
} else {
this->read(session);
}
} else if (this->on_error) {
this->on_error(session->request, ec);
}
});
} else if (this->on_error) {
this->on_error(session->request, ec);
}
});
}
};
using https_server_t = SunshineHTTPSServer;
using http_server_t = SimpleWeb::Server<SimpleWeb::HTTP>;
struct conf_intern_t {
std::string servercert;
std::string pkey;
} conf_intern;
// uniqueID, session
std::unordered_map<std::string, pair_session_t> map_id_sess;
client_t client_root;
std::atomic<uint32_t> session_id_counter;
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Response>;
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Request>;
using resp_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>;
using req_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Request>;
enum class op_e {
ADD, ///< Add certificate
REMOVE ///< Remove certificate
};
std::string get_arg(const args_t &args, const char *name, const char *default_value) {
auto it = args.find(name);
if (it == std::end(args)) {
if (default_value != NULL) {
return std::string(default_value);
}
throw std::out_of_range(name);
}
return it->second;
}
// Helper function to extract command entries from a JSON object.
cmd_list_t extract_command_entries(const nlohmann::json& j, const std::string& key) {
cmd_list_t commands;
// Check if the key exists in the JSON.
if (j.contains(key)) {
// Ensure that the value for the key is an array.
try {
for (const auto& item : j.at(key)) {
try {
// Extract "cmd" and "elevated" fields from the JSON object.
std::string cmd = item.at("cmd").get<std::string>();
bool elevated = util::get_non_string_json_value<bool>(item, "elevated", false);
// Add the command entry to the list.
commands.push_back({cmd, elevated});
} catch (const std::exception& e) {
BOOST_LOG(warning) << "Error parsing command entry: " << e.what();
}
}
} catch (const std::exception &e) {
BOOST_LOG(warning) << "Error retrieving key \"" << key << "\": " << e.what();
}
} else {
BOOST_LOG(debug) << "Key \"" << key << "\" not found in the JSON.";
}
return commands;
}
void save_state() {
nlohmann::json root = nlohmann::json::object();
// If the state file exists, try to read it.
if (fs::exists(config::nvhttp.file_state)) {
try {
std::ifstream in(config::nvhttp.file_state);
in >> root;
} catch (std::exception &e) {
BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what();
return;
}
}
// Erase any previous "root" key.
root.erase("root");
// Create a new "root" object and set the unique id.
root["root"] = nlohmann::json::object();
root["root"]["uniqueid"] = http::unique_id;
client_t &client = client_root;
nlohmann::json named_cert_nodes = nlohmann::json::array();
std::unordered_set<std::string> unique_certs;
std::unordered_map<std::string, int> name_counts;
for (auto &named_cert_p : client.named_devices) {
// Only add each unique certificate once.
if (unique_certs.insert(named_cert_p->cert).second) {
nlohmann::json named_cert_node = nlohmann::json::object();
std::string base_name = named_cert_p->name;
// Remove any pending id suffix (e.g., " (2)") if present.
size_t pos = base_name.find(" (");
if (pos != std::string::npos) {
base_name = base_name.substr(0, pos);
}
int count = name_counts[base_name]++;
std::string final_name = base_name;
if (count > 0) {
final_name += " (" + std::to_string(count + 1) + ")";
}
named_cert_node["name"] = final_name;
named_cert_node["cert"] = named_cert_p->cert;
named_cert_node["uuid"] = named_cert_p->uuid;
named_cert_node["display_mode"] = named_cert_p->display_mode;
named_cert_node["perm"] = static_cast<uint32_t>(named_cert_p->perm);
// Add "do" commands if available.
if (!named_cert_p->do_cmds.empty()) {
nlohmann::json do_cmds_node = nlohmann::json::array();
for (const auto &cmd : named_cert_p->do_cmds) {
do_cmds_node.push_back(crypto::command_entry_t::serialize(cmd));
}
named_cert_node["do"] = do_cmds_node;
}
// Add "undo" commands if available.
if (!named_cert_p->undo_cmds.empty()) {
nlohmann::json undo_cmds_node = nlohmann::json::array();
for (const auto &cmd : named_cert_p->undo_cmds) {
undo_cmds_node.push_back(crypto::command_entry_t::serialize(cmd));
}
named_cert_node["undo"] = undo_cmds_node;
}
named_cert_nodes.push_back(named_cert_node);
}
}
root["root"]["named_devices"] = named_cert_nodes;
try {
std::ofstream out(config::nvhttp.file_state);
out << root.dump(4); // Pretty-print with an indent of 4 spaces.
} catch (std::exception &e) {
BOOST_LOG(error) << "Couldn't write "sv << config::nvhttp.file_state << ": "sv << e.what();
return;
}
}
void load_state() {
if (!fs::exists(config::nvhttp.file_state)) {
BOOST_LOG(info) << "File "sv << config::nvhttp.file_state << " doesn't exist"sv;
http::unique_id = uuid_util::uuid_t::generate().string();
return;
}
nlohmann::json tree;
try {
std::ifstream in(config::nvhttp.file_state);
in >> tree;
} catch (std::exception &e) {
BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what();
return;
}
// Check that the file contains a "root.uniqueid" value.
if (!tree.contains("root") || !tree["root"].contains("uniqueid")) {
http::uuid = uuid_util::uuid_t::generate();
http::unique_id = http::uuid.string();
return;
}
std::string uid = tree["root"]["uniqueid"];
http::uuid = uuid_util::uuid_t::parse(uid);
http::unique_id = uid;
nlohmann::json root = tree["root"];
client_t client; // Local client to load into
// Import from the old format if available.
if (root.contains("devices")) {
for (auto &device_node : root["devices"]) {
// For each device, if there is a "certs" array, add a named certificate.
if (device_node.contains("certs")) {
for (auto &el : device_node["certs"]) {
auto named_cert_p = std::make_shared<crypto::named_cert_t>();
named_cert_p->name = "";
named_cert_p->cert = el.get<std::string>();
named_cert_p->uuid = uuid_util::uuid_t::generate().string();
named_cert_p->display_mode = "";
named_cert_p->perm = PERM::_all;
client.named_devices.emplace_back(named_cert_p);
}
}
}
}
// Import from the new format.
if (root.contains("named_devices")) {
for (auto &el : root["named_devices"]) {
auto named_cert_p = std::make_shared<crypto::named_cert_t>();
named_cert_p->name = el.value("name", "");
named_cert_p->cert = el.value("cert", "");
named_cert_p->uuid = el.value("uuid", "");
named_cert_p->display_mode = el.value("display_mode", "");
named_cert_p->perm = (PERM)(util::get_non_string_json_value<uint32_t>(el, "perm", (uint32_t)PERM::_all)) & PERM::_all;
// Load command entries for "do" and "undo" keys.
named_cert_p->do_cmds = extract_command_entries(el, "do");
named_cert_p->undo_cmds = extract_command_entries(el, "undo");
client.named_devices.emplace_back(named_cert_p);
}
}
// Clear any existing certificate chain and add the imported certificates.
cert_chain.clear();
for (auto &named_cert : client.named_devices) {
cert_chain.add(named_cert);
}
client_root = client;
}
void add_authorized_client(const p_named_cert_t& named_cert_p) {
client_t &client = client_root;
client.named_devices.push_back(named_cert_p);
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
system_tray::update_tray_paired(named_cert_p->name);
#endif
if (!config::sunshine.flags[config::flag::FRESH_STATE]) {
save_state();
load_state();
}
}
std::shared_ptr<rtsp_stream::launch_session_t> make_launch_session(bool host_audio, bool input_only, int appid, const args_t &args, const crypto::named_cert_t* named_cert_p) {
auto launch_session = std::make_shared<rtsp_stream::launch_session_t>();
launch_session->id = ++session_id_counter;
// If launched from client
if (named_cert_p->uuid != http::unique_id) {
auto rikey = util::from_hex_vec(get_arg(args, "rikey"), true);
std::copy(rikey.cbegin(), rikey.cend(), std::back_inserter(launch_session->gcm_key));
launch_session->host_audio = host_audio;
// Encrypted RTSP is enabled with client reported corever >= 1
auto corever = util::from_view(get_arg(args, "corever", "0"));
if (corever >= 1) {
launch_session->rtsp_cipher = crypto::cipher::gcm_t {
launch_session->gcm_key, false
};
launch_session->rtsp_iv_counter = 0;
}
launch_session->rtsp_url_scheme = launch_session->rtsp_cipher ? "rtspenc://"s : "rtsp://"s;
// Generate the unique identifiers for this connection that we will send later during RTSP handshake
unsigned char raw_payload[8];
RAND_bytes(raw_payload, sizeof(raw_payload));
launch_session->av_ping_payload = util::hex_vec(raw_payload);
RAND_bytes((unsigned char *) &launch_session->control_connect_data, sizeof(launch_session->control_connect_data));
launch_session->iv.resize(16);
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(get_arg(args, "rikeyid")));
auto prepend_iv_p = (uint8_t *) &prepend_iv;
std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session->iv));
}
std::stringstream mode;
if (named_cert_p->display_mode.empty()) {
auto mode_str = get_arg(args, "mode", config::video.fallback_mode.c_str());
mode = std::stringstream(mode_str);
BOOST_LOG(info) << "Display mode for client ["sv << named_cert_p->name <<"] requested to ["sv << mode_str << ']';
} else {
mode = std::stringstream(named_cert_p->display_mode);
BOOST_LOG(info) << "Display mode for client ["sv << named_cert_p->name <<"] overriden to ["sv << named_cert_p->display_mode << ']';
}
// Split mode by the char "x", to populate width/height/fps
int x = 0;
std::string segment;
while (std::getline(mode, segment, 'x')) {
if (x == 0) {
launch_session->width = atoi(segment.c_str());
}
if (x == 1) {
launch_session->height = atoi(segment.c_str());
}
if (x == 2) {
auto fps = atof(segment.c_str());
if (fps < 1000) {
fps *= 1000;
};
launch_session->fps = (int)fps;
break;
}
x++;
}
// Parsing have failed or missing components
if (x != 2) {
launch_session->width = 1920;
launch_session->height = 1080;
launch_session->fps = 60000; // 60fps * 1000 denominator
}
launch_session->device_name = named_cert_p->name.empty() ? "ApolloDisplay"s : named_cert_p->name;
launch_session->unique_id = named_cert_p->uuid;
launch_session->perm = named_cert_p->perm;
launch_session->appid = appid;
launch_session->enable_sops = util::from_view(get_arg(args, "sops", "0"));
launch_session->surround_info = util::from_view(get_arg(args, "surroundAudioInfo", "196610"));
launch_session->surround_params = (get_arg(args, "surroundParams", ""));
launch_session->gcmap = util::from_view(get_arg(args, "gcmap", "0"));
launch_session->enable_hdr = util::from_view(get_arg(args, "hdrMode", "0"));
launch_session->virtual_display = util::from_view(get_arg(args, "virtualDisplay", "0"));
launch_session->scale_factor = util::from_view(get_arg(args, "scaleFactor", "100"));
launch_session->client_do_cmds = named_cert_p->do_cmds;
launch_session->client_undo_cmds = named_cert_p->undo_cmds;
launch_session->input_only = input_only;
return launch_session;
}
void remove_session(const pair_session_t &sess) {
map_id_sess.erase(sess.client.uniqueID);
}
void fail_pair(pair_session_t &sess, pt::ptree &tree, const std::string status_msg) {
tree.put("root.paired", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", status_msg);
remove_session(sess); // Security measure, delete the session when something went wrong and force a re-pair
BOOST_LOG(warning) << "Pair attempt failed due to " << status_msg;
}
void getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin) {
if (sess.last_phase != PAIR_PHASE::NONE) {
fail_pair(sess, tree, "Out of order call to getservercert");
return;
}
sess.last_phase = PAIR_PHASE::GETSERVERCERT;
if (sess.async_insert_pin.salt.size() < 32) {
fail_pair(sess, tree, "Salt too short");
return;
}
std::string_view salt_view {sess.async_insert_pin.salt.data(), 32};
auto salt = util::from_hex<std::array<uint8_t, 16>>(salt_view, true);
auto key = crypto::gen_aes_key(salt, pin);
sess.cipher_key = std::make_unique<crypto::aes_t>(key);
tree.put("root.paired", 1);
tree.put("root.plaincert", util::hex_vec(conf_intern.servercert, true));
tree.put("root.<xmlattr>.status_code", 200);
}
void clientchallenge(pair_session_t &sess, pt::ptree &tree, const std::string &challenge) {
if (sess.last_phase != PAIR_PHASE::GETSERVERCERT) {
fail_pair(sess, tree, "Out of order call to clientchallenge");
return;
}
sess.last_phase = PAIR_PHASE::CLIENTCHALLENGE;
if (!sess.cipher_key) {
fail_pair(sess, tree, "Cipher key not set");
return;
}
crypto::cipher::ecb_t cipher(*sess.cipher_key, false);
std::vector<uint8_t> decrypted;
cipher.decrypt(challenge, decrypted);
auto x509 = crypto::x509(conf_intern.servercert);
auto sign = crypto::signature(x509);
auto serversecret = crypto::rand(16);
decrypted.insert(std::end(decrypted), std::begin(sign), std::end(sign));
decrypted.insert(std::end(decrypted), std::begin(serversecret), std::end(serversecret));
auto hash = crypto::hash({(char *) decrypted.data(), decrypted.size()});
auto serverchallenge = crypto::rand(16);
std::string plaintext;
plaintext.reserve(hash.size() + serverchallenge.size());
plaintext.insert(std::end(plaintext), std::begin(hash), std::end(hash));
plaintext.insert(std::end(plaintext), std::begin(serverchallenge), std::end(serverchallenge));
std::vector<uint8_t> encrypted;
cipher.encrypt(plaintext, encrypted);
sess.serversecret = std::move(serversecret);
sess.serverchallenge = std::move(serverchallenge);
tree.put("root.paired", 1);
tree.put("root.challengeresponse", util::hex_vec(encrypted, true));
tree.put("root.<xmlattr>.status_code", 200);
}
void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const std::string &encrypted_response) {
if (sess.last_phase != PAIR_PHASE::CLIENTCHALLENGE) {
fail_pair(sess, tree, "Out of order call to serverchallengeresp");
return;
}
sess.last_phase = PAIR_PHASE::SERVERCHALLENGERESP;
if (!sess.cipher_key || sess.serversecret.empty()) {
fail_pair(sess, tree, "Cipher key or serversecret not set");
return;
}
std::vector<uint8_t> decrypted;
crypto::cipher::ecb_t cipher(*sess.cipher_key, false);
cipher.decrypt(encrypted_response, decrypted);
sess.clienthash = std::move(decrypted);
auto serversecret = sess.serversecret;
auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret);
serversecret.insert(std::end(serversecret), std::begin(sign), std::end(sign));
tree.put("root.pairingsecret", util::hex_vec(serversecret, true));
tree.put("root.paired", 1);
tree.put("root.<xmlattr>.status_code", 200);
}
void clientpairingsecret(pair_session_t &sess, pt::ptree &tree, const std::string &client_pairing_secret) {
if (sess.last_phase != PAIR_PHASE::SERVERCHALLENGERESP) {
fail_pair(sess, tree, "Out of order call to clientpairingsecret");
return;
}
sess.last_phase = PAIR_PHASE::CLIENTPAIRINGSECRET;
auto &client = sess.client;
if (client_pairing_secret.size() <= 16) {
fail_pair(sess, tree, "Client pairing secret too short");
return;
}
std::string_view secret {client_pairing_secret.data(), 16};
std::string_view sign {client_pairing_secret.data() + secret.size(), client_pairing_secret.size() - secret.size()};
auto x509 = crypto::x509(client.cert);
if (!x509) {
fail_pair(sess, tree, "Invalid client certificate");
return;
}
auto x509_sign = crypto::signature(x509);
std::string data;
data.reserve(sess.serverchallenge.size() + x509_sign.size() + secret.size());
data.insert(std::end(data), std::begin(sess.serverchallenge), std::end(sess.serverchallenge));
data.insert(std::end(data), std::begin(x509_sign), std::end(x509_sign));
data.insert(std::end(data), std::begin(secret), std::end(secret));
auto hash = crypto::hash(data);
// if hash not correct, probably MITM
bool same_hash = hash.size() == sess.clienthash.size() && std::equal(hash.begin(), hash.end(), sess.clienthash.begin());
auto verify = crypto::verify256(crypto::x509(client.cert), secret, sign);
if (same_hash && verify) {
tree.put("root.paired", 1);
auto named_cert_p = std::make_shared<crypto::named_cert_t>();
named_cert_p->name = client.name;
for (char& c : named_cert_p->name) {
if (c == '(') c = '[';
else if (c == ')') c = ']';
}
named_cert_p->cert = std::move(client.cert);
named_cert_p->uuid = uuid_util::uuid_t::generate().string();
// If the device is the first one paired with the server, assign full permission.
if (client_root.named_devices.empty()) {
named_cert_p->perm = PERM::_all;
} else {
named_cert_p->perm = PERM::_default;
}
auto it = map_id_sess.find(client.uniqueID);
map_id_sess.erase(it);
add_authorized_client(named_cert_p);
} else {
tree.put("root.paired", 0);
BOOST_LOG(warning) << "Pair attempt failed due to same_hash: " << same_hash << ", verify: " << verify;
}
remove_session(sess);
tree.put("root.<xmlattr>.status_code", 200);
}
template<class T>
struct tunnel;
template<>
struct tunnel<SunshineHTTPS> {
static auto constexpr to_string = "HTTPS"sv;
};
template<>
struct tunnel<SimpleWeb::HTTP> {
static auto constexpr to_string = "NONE"sv;
};
inline crypto::named_cert_t* get_verified_cert(req_https_t request) {
return (crypto::named_cert_t*)request->userp.get();
}
template <class T>
void print_req(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
BOOST_LOG(debug) << "TUNNEL :: "sv << tunnel<T>::to_string;
BOOST_LOG(debug) << "METHOD :: "sv << request->method;
BOOST_LOG(debug) << "DESTINATION :: "sv << request->path;
for (auto &[name, val] : request->header) {
BOOST_LOG(debug) << name << " -- " << val;
}
BOOST_LOG(debug) << " [--] "sv;
for (auto &[name, val] : request->parse_query_string()) {
BOOST_LOG(debug) << name << " -- " << val;
}
BOOST_LOG(debug) << " [--] "sv;
}
template<class T>
void not_found(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
print_req<T>(request);
pt::ptree tree;
tree.put("root.<xmlattr>.status_code", 404);
std::ostringstream data;
pt::write_xml(data, tree);
response->write(SimpleWeb::StatusCode::client_error_not_found, data.str());
response->close_connection_after_response = true;
}
template <class T>
void pair(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
print_req<T>(request);
pt::ptree tree;
auto fg = util::fail_guard([&]() {
std::ostringstream data;
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
});
if (!config::sunshine.enable_pairing) {
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", "Pairing is disabled for this instance");
return;
}
auto args = request->parse_query_string();
if (args.find("uniqueid"s) == std::end(args)) {
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Missing uniqueid parameter");
return;
}
auto uniqID {get_arg(args, "uniqueid")};
args_t::const_iterator it;
if (it = args.find("phrase"); it != std::end(args)) {
if (it->second == "getservercert"sv) {
pair_session_t sess;
auto deviceName { get_arg(args, "devicename") };
if (deviceName == "roth"sv) {
deviceName = "Legacy Moonlight Client";
}
sess.client.uniqueID = std::move(uniqID);
sess.client.name = std::move(deviceName);
sess.client.cert = util::from_hex_vec(get_arg(args, "clientcert"), true);
BOOST_LOG(debug) << sess.client.cert;
auto ptr = map_id_sess.emplace(sess.client.uniqueID, std::move(sess)).first;
ptr->second.async_insert_pin.salt = std::move(get_arg(args, "salt"));
auto it = args.find("otpauth");
if (it != std::end(args)) {
if (one_time_pin.empty() || (std::chrono::steady_clock::now() - otp_creation_time > OTP_EXPIRE_DURATION)) {
one_time_pin.clear();
otp_passphrase.clear();
otp_device_name.clear();
tree.put("root.<xmlattr>.status_code", 503);
tree.put("root.<xmlattr>.status_message", "OTP auth not available.");
} else {
auto hash = util::hex(crypto::hash(one_time_pin + ptr->second.async_insert_pin.salt + otp_passphrase), true);
if (hash.to_string_view() == it->second) {
if (!otp_device_name.empty()) {
ptr->second.client.name = std::move(otp_device_name);
}
getservercert(ptr->second, tree, one_time_pin);
one_time_pin.clear();
otp_passphrase.clear();
otp_device_name.clear();
return;
}
}
// Always return positive, attackers will fail in the next steps.
getservercert(ptr->second, tree, crypto::rand(16));
return;
}
if (config::sunshine.flags[config::flag::PIN_STDIN]) {
std::string pin;
std::cout << "Please insert pin: "sv;
std::getline(std::cin, pin);
getservercert(ptr->second, tree, pin);
} else {
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
system_tray::update_tray_require_pin();
#endif
ptr->second.async_insert_pin.response = std::move(response);
fg.disable();
return;
}
} else if (it->second == "pairchallenge"sv) {
tree.put("root.paired", 1);
tree.put("root.<xmlattr>.status_code", 200);
return;
}
}
auto sess_it = map_id_sess.find(uniqID);
if (sess_it == std::end(map_id_sess)) {
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Invalid uniqueid");
return;
}
if (it = args.find("clientchallenge"); it != std::end(args)) {
auto challenge = util::from_hex_vec(it->second, true);
clientchallenge(sess_it->second, tree, challenge);
} else if (it = args.find("serverchallengeresp"); it != std::end(args)) {
auto encrypted_response = util::from_hex_vec(it->second, true);
serverchallengeresp(sess_it->second, tree, encrypted_response);
} else if (it = args.find("clientpairingsecret"); it != std::end(args)) {
auto pairingsecret = util::from_hex_vec(it->second, true);
clientpairingsecret(sess_it->second, tree, pairingsecret);
} else {
tree.put("root.<xmlattr>.status_code", 404);
tree.put("root.<xmlattr>.status_message", "Invalid pairing request");
}
}
bool pin(std::string pin, std::string name) {
pt::ptree tree;
if (map_id_sess.empty()) {
return false;
}
// ensure pin is 4 digits
if (pin.size() != 4) {
tree.put("root.paired", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put(
"root.<xmlattr>.status_message",
"Pin must be 4 digits, " + std::to_string(pin.size()) + " provided"
);
return false;
}
// ensure all pin characters are numeric
if (!std::all_of(pin.begin(), pin.end(), ::isdigit)) {
tree.put("root.paired", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Pin must be numeric");
return false;
}
auto &sess = std::begin(map_id_sess)->second;
getservercert(sess, tree, pin);
if (!name.empty()) {
sess.client.name = name;
}
// response to the request for pin
std::ostringstream data;
pt::write_xml(data, tree);
auto &async_response = sess.async_insert_pin.response;
if (async_response.has_left() && async_response.left()) {
async_response.left()->write(data.str());
} else if (async_response.has_right() && async_response.right()) {
async_response.right()->write(data.str());
} else {
return false;
}
// reset async_response
async_response = std::decay_t<decltype(async_response.left())>();
// response to the current request
return true;
}
template<class T>
void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
print_req<T>(request);
int pair_status = 0;
if constexpr (std::is_same_v<SunshineHTTPS, T>) {
auto args = request->parse_query_string();
auto clientID = args.find("uniqueid"s);
if (clientID != std::end(args)) {
pair_status = 1;
}
}
auto local_endpoint = request->local_endpoint();
pt::ptree tree;
tree.put("root.<xmlattr>.status_code", 200);
tree.put("root.hostname", config::nvhttp.sunshine_name);
tree.put("root.appversion", VERSION);
tree.put("root.GfeVersion", GFE_VERSION);
tree.put("root.uniqueid", http::unique_id);
tree.put("root.HttpsPort", net::map_port(PORT_HTTPS));
tree.put("root.ExternalPort", net::map_port(PORT_HTTP));
tree.put("root.MaxLumaPixelsHEVC", video::active_hevc_mode > 1 ? "1869449984" : "0");
// Only include the MAC address for requests sent from paired clients over HTTPS.
// For HTTP requests, use a placeholder MAC address that Moonlight knows to ignore.
if constexpr (std::is_same_v<SunshineHTTPS, T>) {
tree.put("root.mac", platf::get_mac_address(net::addr_to_normalized_string(local_endpoint.address())));
auto named_cert_p = get_verified_cert(request);
if (!!(named_cert_p->perm & PERM::server_cmd)) {
pt::ptree& root_node = tree.get_child("root");
if (config::sunshine.server_cmds.size() > 0) {
// Broadcast server_cmds
for (const auto& cmd : config::sunshine.server_cmds) {
pt::ptree cmd_node;
cmd_node.put_value(cmd.cmd_name);
root_node.push_back(std::make_pair("ServerCommand", cmd_node));
}
}
} else {
BOOST_LOG(debug) << "Permission Get ServerCommand denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")";
}
tree.put("root.Permission", std::to_string((uint32_t)named_cert_p->perm));
#ifdef _WIN32
tree.put("root.VirtualDisplayCapable", true);
if (!!(named_cert_p->perm & PERM::_all_actions)) {
tree.put("root.VirtualDisplayDriverReady", proc::vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK);
} else {
tree.put("root.VirtualDisplayDriverReady", true);
}
#endif
} else {
tree.put("root.mac", "00:00:00:00:00:00");
tree.put("root.Permission", "0");
}
// Moonlight clients track LAN IPv6 addresses separately from LocalIP which is expected to
// always be an IPv4 address. If we return that same IPv6 address here, it will clobber the
// stored LAN IPv4 address. To avoid this, we need to return an IPv4 address in this field
// when we get a request over IPv6.
//
// HACK: We should return the IPv4 address of local interface here, but we don't currently
// have that implemented. For now, we will emulate the behavior of GFE+GS-IPv6-Forwarder,
// which returns 127.0.0.1 as LocalIP for IPv6 connections. Moonlight clients with IPv6
// support know to ignore this bogus address.
if (local_endpoint.address().is_v6() && !local_endpoint.address().to_v6().is_v4_mapped()) {
tree.put("root.LocalIP", "127.0.0.1");
} else {
tree.put("root.LocalIP", net::addr_to_normalized_string(local_endpoint.address()));
}
uint32_t codec_mode_flags = SCM_H264;
if (video::last_encoder_probe_supported_yuv444_for_codec[0]) {
codec_mode_flags |= SCM_H264_HIGH8_444;
}
if (video::active_hevc_mode >= 2) {
codec_mode_flags |= SCM_HEVC;
if (video::last_encoder_probe_supported_yuv444_for_codec[1]) {
codec_mode_flags |= SCM_HEVC_REXT8_444;
}
}
if (video::active_hevc_mode >= 3) {
codec_mode_flags |= SCM_HEVC_MAIN10;
if (video::last_encoder_probe_supported_yuv444_for_codec[1]) {
codec_mode_flags |= SCM_HEVC_REXT10_444;
}
}
if (video::active_av1_mode >= 2) {
codec_mode_flags |= SCM_AV1_MAIN8;
if (video::last_encoder_probe_supported_yuv444_for_codec[2]) {
codec_mode_flags |= SCM_AV1_HIGH8_444;
}
}
if (video::active_av1_mode >= 3) {
codec_mode_flags |= SCM_AV1_MAIN10;
if (video::last_encoder_probe_supported_yuv444_for_codec[2]) {
codec_mode_flags |= SCM_AV1_HIGH10_444;
}
}
tree.put("root.ServerCodecModeSupport", codec_mode_flags);
tree.put("root.PairStatus", pair_status);
if constexpr (std::is_same_v<SunshineHTTPS, T>) {
int current_appid = proc::proc.running();
// When input only mode is enabled, the only resume method should be launching the same app again.
if (config::input.enable_input_only_mode && current_appid != proc::input_only_app_id) {
current_appid = 0;
}
tree.put("root.currentgame", current_appid);
tree.put("root.state", current_appid > 0 ? "SUNSHINE_SERVER_BUSY" : "SUNSHINE_SERVER_FREE");
} else {
tree.put("root.currentgame", 0);
tree.put("root.state", "SUNSHINE_SERVER_FREE");
}
std::ostringstream data;
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
}
nlohmann::json get_all_clients() {
nlohmann::json named_cert_nodes = nlohmann::json::array();
client_t &client = client_root;
std::list<std::string> connected_uuids = rtsp_stream::get_all_session_uuids();
for (auto &named_cert : client.named_devices) {
nlohmann::json named_cert_node;
named_cert_node["name"] = named_cert->name;
named_cert_node["uuid"] = named_cert->uuid;
named_cert_node["display_mode"] = named_cert->display_mode;
named_cert_node["perm"] = static_cast<uint32_t>(named_cert->perm);
// Add "do" commands if available
if (!named_cert->do_cmds.empty()) {
nlohmann::json do_cmds_node = nlohmann::json::array();
for (const auto &cmd : named_cert->do_cmds) {
do_cmds_node.push_back(crypto::command_entry_t::serialize(cmd));
}
named_cert_node["do"] = do_cmds_node;
}
// Add "undo" commands if available
if (!named_cert->undo_cmds.empty()) {
nlohmann::json undo_cmds_node = nlohmann::json::array();
for (const auto &cmd : named_cert->undo_cmds) {
undo_cmds_node.push_back(crypto::command_entry_t::serialize(cmd));
}
named_cert_node["undo"] = undo_cmds_node;
}
// Determine connection status
bool connected = false;
if (connected_uuids.empty()) {
connected = false;
} else {
for (auto it = connected_uuids.begin(); it != connected_uuids.end(); ++it) {
if (*it == named_cert->uuid) {
connected = true;
connected_uuids.erase(it);
break;
}
}
}
named_cert_node["connected"] = connected;
named_cert_nodes.push_back(named_cert_node);
}
return named_cert_nodes;
}
void applist(resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
pt::ptree tree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
});
auto &apps = tree.add_child("root", pt::ptree {});
apps.put("<xmlattr>.status_code", 200);
auto named_cert_p = get_verified_cert(request);
if (!!(named_cert_p->perm & PERM::_all_actions)) {
auto current_appid = proc::proc.running();
auto should_hide_inactive_apps = config::input.enable_input_only_mode && current_appid > 0 && current_appid != proc::input_only_app_id;
for (auto &app : proc::proc.get_apps()) {
auto appid = util::from_view(app.id);
if (should_hide_inactive_apps) {
if (
appid != current_appid
&& appid != proc::input_only_app_id
&& appid != proc::terminate_app_id
) {
continue;
}
} else {
if (appid == proc::terminate_app_id) {
continue;
}
}
pt::ptree app_node;
app_node.put("IsHdrSupported"s, video::active_hevc_mode == 3 ? 1 : 0);
app_node.put("AppTitle"s, app.name);
app_node.put("UUID", app.uuid);
app_node.put("ID", app.id);
apps.push_back(std::make_pair("App", std::move(app_node)));
}
} else {
BOOST_LOG(debug) << "Permission ListApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")";
pt::ptree app_node;
app_node.put("IsHdrSupported"s, 0);
app_node.put("AppTitle"s, "Permission Denied");
app_node.put("UUID", "");
app_node.put("ID", "114514");
apps.push_back(std::make_pair("App", std::move(app_node)));
return;
}
}
void launch(bool &host_audio, resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
pt::ptree tree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
});
auto args = request->parse_query_string();
auto appid_str = get_arg(args, "appid");
auto appid = util::from_view(appid_str);
auto current_appid = proc::proc.running();
bool is_input_only = config::input.enable_input_only_mode && appid == proc::input_only_app_id;
auto named_cert_p = get_verified_cert(request);
auto perm = PERM::launch;
// If we have already launched an app, we should allow clients with view permission to join the input only or current app's session.
if (current_appid > 0 && appid != proc::terminate_app_id && (is_input_only || appid == current_appid)) {
perm = PERM::_allow_view;
}
if (!(named_cert_p->perm & perm)) {
BOOST_LOG(debug) << "Permission LaunchApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")";
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", "Permission denied");
return;
}
if (
args.find("rikey"s) == std::end(args) ||
args.find("rikeyid"s) == std::end(args) ||
args.find("localAudioPlayMode"s) == std::end(args) ||
args.find("appid"s) == std::end(args)
) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Missing a required launch parameter");
return;
}
if (!is_input_only) {
// Special handling for the "terminate" app
if (config::input.enable_input_only_mode && appid == proc::terminate_app_id) {
proc::proc.terminate();
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 410);
tree.put("root.<xmlattr>.status_message", "App terminated.");
return;
}
if (current_appid > 0 && current_appid != proc::input_only_app_id && appid != current_appid) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "An app is already running on this host");
return;
}
}
host_audio = util::from_view(get_arg(args, "localAudioPlayMode"));
auto launch_session = make_launch_session(host_audio, is_input_only, appid, args, named_cert_p);
auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address());
if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) {
BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv;
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", "Encryption is mandatory for this host but unsupported by the client");
tree.put("root.gamesession", 0);
return;
}
bool no_active_sessions = rtsp_stream::session_count() == 0;
if (is_input_only) {
BOOST_LOG(info) << "Launching input only session..."sv;
launch_session->client_do_cmds.clear();
launch_session->client_undo_cmds.clear();
// Still probe encoders once, if input only session is launched first
// But we're ignoring if it's successful or not
if (no_active_sessions && !proc::proc.virtual_display) {
video::probe_encoders();
if (current_appid == 0) {
proc::proc.launch_input_only();
}
}
} else if (appid > 0) {
if (appid == current_appid) {
// We're basically resuming the same app
if (!proc::proc.allow_client_commands) {
launch_session->client_do_cmds.clear();
launch_session->client_undo_cmds.clear();
}
if (current_appid == proc::input_only_app_id) {
launch_session->input_only = true;
}
if (no_active_sessions && !proc::proc.virtual_display) {
display_device::configure_display(config::video, *launch_session);
if (video::probe_encoders()) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 503);
tree.put("root.<xmlattr>.status_message", "Failed to initialize video capture/encoding. Is a display connected and turned on?");
return;
}
}
} else {
const auto& apps = proc::proc.get_apps();
auto app_iter = std::find_if(apps.begin(), apps.end(), [&appid_str](const auto _app) {
return _app.id == appid_str;
});
if (app_iter == apps.end()) {
BOOST_LOG(error) << "Couldn't find app with ID ["sv << appid_str << ']';
tree.put("root.<xmlattr>.status_code", 404);
tree.put("root.<xmlattr>.status_message", "Cannot find requested application");
tree.put("root.gamesession", 0);
return;
}
if (!app_iter->allow_client_commands) {
launch_session->client_do_cmds.clear();
launch_session->client_undo_cmds.clear();
}
auto err = proc::proc.execute(appid, *app_iter, launch_session);
if (err) {
tree.put("root.<xmlattr>.status_code", err);
tree.put(
"root.<xmlattr>.status_message",
err == 503
? "Failed to initialize video capture/encoding. Is a display connected and turned on?"
: "Failed to start the specified application");
tree.put("root.gamesession", 0);
return;
}
}
} else {
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", "How did you get here?");
tree.put("root.gamesession", 0);
}
tree.put("root.<xmlattr>.status_code", 200);
tree.put("root.sessionUrl0", launch_session->rtsp_url_scheme + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + std::to_string(net::map_port(rtsp_stream::RTSP_SETUP_PORT)));
tree.put("root.gamesession", 1);
rtsp_stream::launch_session_raise(launch_session);
}
void resume(bool &host_audio, resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
pt::ptree tree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
});
auto named_cert_p = get_verified_cert(request);
if (!(named_cert_p->perm & PERM::_allow_view)) {
BOOST_LOG(debug) << "Permission ViewApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")";
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", "Permission denied");
return;
}
auto current_appid = proc::proc.running();
if (current_appid == 0) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 503);
tree.put("root.<xmlattr>.status_message", "No running app to resume");
return;
}
auto args = request->parse_query_string();
if (
args.find("rikey"s) == std::end(args) ||
args.find("rikeyid"s) == std::end(args)
) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Missing a required resume parameter");
return;
}
// Newer Moonlight clients send localAudioPlayMode on /resume too,
// so we should use it if it's present in the args and there are
// no active sessions we could be interfering with.
const bool no_active_sessions {rtsp_stream::session_count() == 0};
if (no_active_sessions && args.find("localAudioPlayMode"s) != std::end(args)) {
host_audio = util::from_view(get_arg(args, "localAudioPlayMode"));
}
auto launch_session = make_launch_session(host_audio, false, 0, args, named_cert_p);
if (!proc::proc.allow_client_commands) {
launch_session->client_do_cmds.clear();
launch_session->client_undo_cmds.clear();
}
if (config::input.enable_input_only_mode && current_appid == proc::input_only_app_id) {
launch_session->input_only = true;
}
if (no_active_sessions && !proc::proc.virtual_display) {
// We want to prepare display only if there are no active sessions
// and the current session isn't virtual display at the moment.
// This should be done before probing encoders as it could change the active displays.
display_device::configure_display(config::video, *launch_session);
// Probe encoders again before streaming to ensure our chosen
// encoder matches the active GPU (which could have changed
// due to hotplugging, driver crash, primary monitor change,
// or any number of other factors).
if (video::probe_encoders()) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 503);
tree.put("root.<xmlattr>.status_message", "Failed to initialize video capture/encoding. Is a display connected and turned on?");
return;
}
}
auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address());
if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) {
BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv;
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", "Encryption is mandatory for this host but unsupported by the client");
tree.put("root.gamesession", 0);
return;
}
tree.put("root.<xmlattr>.status_code", 200);
tree.put("root.sessionUrl0", launch_session->rtsp_url_scheme + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + std::to_string(net::map_port(rtsp_stream::RTSP_SETUP_PORT)));
tree.put("root.resume", 1);
rtsp_stream::launch_session_raise(launch_session);
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
system_tray::update_tray_client_connected(named_cert_p->name);
#endif
}
void cancel(resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
pt::ptree tree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
});
auto named_cert_p = get_verified_cert(request);
if (!(named_cert_p->perm & PERM::launch)) {
BOOST_LOG(debug) << "Permission CancelApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")";
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", "Permission denied");
return;
}
tree.put("root.cancel", 1);
tree.put("root.<xmlattr>.status_code", 200);
rtsp_stream::terminate_sessions();
if (proc::proc.running() > 0) {
proc::proc.terminate();
}
// The config needs to be reverted regardless of whether "proc::proc.terminate()" was called or not.
display_device::revert_configuration();
}
void appasset(resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
auto fg = util::fail_guard([&]() {
response->write(SimpleWeb::StatusCode::server_error_internal_server_error);
response->close_connection_after_response = true;
});
auto named_cert_p = get_verified_cert(request);
if (!(named_cert_p->perm & PERM::_all_actions)) {
BOOST_LOG(debug) << "Permission Get AppAsset denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")";
fg.disable();
response->write(SimpleWeb::StatusCode::client_error_unauthorized);
response->close_connection_after_response = true;
return;
}
auto args = request->parse_query_string();
auto app_image = proc::proc.get_app_image(util::from_view(get_arg(args, "appid")));
fg.disable();
std::ifstream in(app_image, std::ios::binary);
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "image/png");
response->write(SimpleWeb::StatusCode::success_ok, in, headers);
response->close_connection_after_response = true;
}
void getClipboard(resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
auto named_cert_p = get_verified_cert(request);
if (
!(named_cert_p->perm & PERM::_allow_view)
|| !(named_cert_p->perm & PERM::clipboard_read)
) {
BOOST_LOG(debug) << "Permission Read Clipboard denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")";
response->write(SimpleWeb::StatusCode::client_error_unauthorized);
response->close_connection_after_response = true;
return;
}
auto args = request->parse_query_string();
auto clipboard_type = get_arg(args, "type");
if (clipboard_type != "text"sv) {
BOOST_LOG(debug) << "Clipboard type [" << clipboard_type << "] is not supported!";
response->write(SimpleWeb::StatusCode::client_error_bad_request);
response->close_connection_after_response = true;
return;
}
std::list<std::string> connected_uuids = rtsp_stream::get_all_session_uuids();
bool found = !connected_uuids.empty();
if (found) {
found = (std::find(connected_uuids.begin(), connected_uuids.end(), named_cert_p->uuid) != connected_uuids.end());
}
if (!found) {
BOOST_LOG(debug) << "Client ["<< named_cert_p->name << "] trying to get clipboard is not connected to a stream";
response->write(SimpleWeb::StatusCode::client_error_forbidden);
response->close_connection_after_response = true;
return;
}
std::string content = platf::get_clipboard();
response->write(content);
return;
}
void
setClipboard(resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);
auto named_cert_p = get_verified_cert(request);
if (
!(named_cert_p->perm & PERM::_allow_view)
|| !(named_cert_p->perm & PERM::clipboard_set)
) {
BOOST_LOG(debug) << "Permission Write Clipboard denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")";
response->write(SimpleWeb::StatusCode::client_error_unauthorized);
response->close_connection_after_response = true;
return;
}
auto args = request->parse_query_string();
auto clipboard_type = get_arg(args, "type");
if (clipboard_type != "text"sv) {
BOOST_LOG(debug) << "Clipboard type [" << clipboard_type << "] is not supported!";
response->write(SimpleWeb::StatusCode::client_error_bad_request);
response->close_connection_after_response = true;
return;
}
std::list<std::string> connected_uuids = rtsp_stream::get_all_session_uuids();
bool found = !connected_uuids.empty();
if (found) {
found = (std::find(connected_uuids.begin(), connected_uuids.end(), named_cert_p->uuid) != connected_uuids.end());
}
if (!found) {
BOOST_LOG(debug) << "Client ["<< named_cert_p->name << "] trying to set clipboard is not connected to a stream";
response->write(SimpleWeb::StatusCode::client_error_forbidden);
response->close_connection_after_response = true;
return;
}
std::string content = request->content.string();
bool success = platf::set_clipboard(content);
if (!success) {
BOOST_LOG(debug) << "Setting clipboard failed!";
response->write(SimpleWeb::StatusCode::server_error_internal_server_error);
response->close_connection_after_response = true;
}
response->write();
return;
}
void setup(const std::string &pkey, const std::string &cert) {
conf_intern.pkey = pkey;
conf_intern.servercert = cert;
}
void start() {
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
auto port_http = net::map_port(PORT_HTTP);
auto port_https = net::map_port(PORT_HTTPS);
auto address_family = net::af_from_enum_string(config::sunshine.address_family);
bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE];
if (!clean_slate) {
load_state();
}
auto pkey = file_handler::read_file(config::nvhttp.pkey.c_str());
auto cert = file_handler::read_file(config::nvhttp.cert.c_str());
setup(pkey, cert);
// resume doesn't always get the parameter "localAudioPlayMode"
// launch will store it in host_audio
bool host_audio {};
https_server_t https_server {config::nvhttp.cert, config::nvhttp.pkey};
http_server_t http_server;
// Verify certificates after establishing connection
https_server.verify = [](req_https_t req, SSL *ssl) {
crypto::x509_t x509 {
#if OPENSSL_VERSION_MAJOR >= 3
SSL_get1_peer_certificate(ssl)
#else
SSL_get_peer_certificate(ssl)
#endif
};
if (!x509) {
BOOST_LOG(info) << "unknown -- denied"sv;
return false;
}
bool verified = false;
p_named_cert_t named_cert_p;
auto fg = util::fail_guard([&]() {
char subject_name[256];
X509_NAME_oneline(X509_get_subject_name(x509.get()), subject_name, sizeof(subject_name));
if (verified) {
BOOST_LOG(debug) << subject_name << " -- "sv << "verified, device name: "sv << named_cert_p->name;
} else {
BOOST_LOG(debug) << subject_name << " -- "sv << "denied"sv;
}
});
auto err_str = cert_chain.verify(x509.get(), named_cert_p);
if (err_str) {
BOOST_LOG(warning) << "SSL Verification error :: "sv << err_str;
return verified;
}
verified = true;
req->userp = named_cert_p;
return true;
};
https_server.on_verify_failed = [](resp_https_t resp, req_https_t req) {
pt::ptree tree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_xml(data, tree);
resp->write(data.str());
resp->close_connection_after_response = true;
});
tree.put("root.<xmlattr>.status_code"s, 401);
tree.put("root.<xmlattr>.query"s, req->path);
tree.put("root.<xmlattr>.status_message"s, "The client is not authorized. Certificate verification failed."s);
};
https_server.default_resource["GET"] = not_found<SunshineHTTPS>;
https_server.resource["^/serverinfo$"]["GET"] = serverinfo<SunshineHTTPS>;
https_server.resource["^/pair$"]["GET"] = pair<SunshineHTTPS>;
https_server.resource["^/applist$"]["GET"] = applist;
https_server.resource["^/appasset$"]["GET"] = appasset;
https_server.resource["^/launch$"]["GET"] = [&host_audio](auto resp, auto req) {
launch(host_audio, resp, req);
};
https_server.resource["^/resume$"]["GET"] = [&host_audio](auto resp, auto req) {
resume(host_audio, resp, req);
};
https_server.resource["^/cancel$"]["GET"] = cancel;
https_server.resource["^/actions/clipboard$"]["GET"] = getClipboard;
https_server.resource["^/actions/clipboard$"]["POST"] = setClipboard;
https_server.config.reuse_address = true;
https_server.config.address = net::af_to_any_address_string(address_family);
https_server.config.port = port_https;
http_server.default_resource["GET"] = not_found<SimpleWeb::HTTP>;
http_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTP>;
http_server.resource["^/pair$"]["GET"] = pair<SimpleWeb::HTTP>;
http_server.config.reuse_address = true;
http_server.config.address = net::af_to_any_address_string(address_family);
http_server.config.port = port_http;
auto accept_and_run = [&](auto *http_server) {
try {
http_server->start();
} catch (boost::system::system_error &err) {
// It's possible the exception gets thrown after calling http_server->stop() from a different thread
if (shutdown_event->peek()) {
return;
}
BOOST_LOG(fatal) << "Couldn't start http server on ports ["sv << port_https << ", "sv << port_https << "]: "sv << err.what();
shutdown_event->raise(true);
return;
}
};
std::thread ssl {accept_and_run, &https_server};
std::thread tcp {accept_and_run, &http_server};
// Wait for any event
shutdown_event->view();
map_id_sess.clear();
https_server.stop();
http_server.stop();
ssl.join();
tcp.join();
}
std::string request_otp(const std::string& passphrase, const std::string& deviceName) {
if (passphrase.size() < 4) {
return "";
}
one_time_pin = crypto::rand_alphabet(4, "0123456789"sv);
otp_passphrase = passphrase;
otp_device_name = deviceName;
otp_creation_time = std::chrono::steady_clock::now();
return one_time_pin;
}
void
erase_all_clients() {
client_t client;
client_root = client;
cert_chain.clear();
save_state();
load_state();
}
void stop_session(stream::session_t& session, bool graceful) {
if (graceful) {
stream::session::graceful_stop(session);
} else {
stream::session::stop(session);
}
}
bool find_and_stop_session(const std::string& uuid, bool graceful) {
auto session = rtsp_stream::find_session(uuid);
if (session) {
stop_session(*session, graceful);
return true;
}
return false;
}
void update_session_info(stream::session_t& session, const std::string& name, const crypto::PERM newPerm) {
stream::session::update_device_info(session, name, newPerm);
}
bool find_and_udpate_session_info(const std::string& uuid, const std::string& name, const crypto::PERM newPerm) {
auto session = rtsp_stream::find_session(uuid);
if (session) {
update_session_info(*session, name, newPerm);
return true;
}
return false;
}
bool update_device_info(
const std::string& uuid,
const std::string& name,
const std::string& display_mode,
const cmd_list_t& do_cmds,
const cmd_list_t& undo_cmds,
const crypto::PERM newPerm
) {
find_and_udpate_session_info(uuid, name, newPerm);
client_t &client = client_root;
auto it = client.named_devices.begin();
for (; it != client.named_devices.end(); ++it) {
auto named_cert_p = *it;
if (named_cert_p->uuid == uuid) {
named_cert_p->name = name;
named_cert_p->display_mode = display_mode;
named_cert_p->perm = newPerm;
named_cert_p->do_cmds = do_cmds;
named_cert_p->undo_cmds = undo_cmds;
save_state();
return true;
}
}
return false;
}
bool unpair_client(const std::string_view uuid) {
bool removed = false;
client_t &client = client_root;
for (auto it = client.named_devices.begin(); it != client.named_devices.end();) {
if ((*it)->uuid == uuid) {
it = client.named_devices.erase(it);
removed = true;
} else {
++it;
}
}
save_state();
load_state();
if (removed) {
auto session = rtsp_stream::find_session(uuid);
if (session) {
stop_session(*session, true);
}
if (client.named_devices.empty()) {
proc::proc.terminate();
}
}
return removed;
}
} // namespace nvhttp