Merge branch 'permission-system'

This commit is contained in:
Yukino Song
2024-09-16 23:14:15 +08:00
23 changed files with 1075 additions and 245 deletions
+1 -1
View File
@@ -32,7 +32,7 @@
branch = sdk branch = sdk
[submodule "third-party/Simple-Web-Server"] [submodule "third-party/Simple-Web-Server"]
path = third-party/Simple-Web-Server path = third-party/Simple-Web-Server
url = https://gitlab.com/eidheim/Simple-Web-Server.git url = https://github.com/ClassicOldSong/Simple-Web-Server
branch = master branch = master
[submodule "third-party/TPCircularBuffer"] [submodule "third-party/TPCircularBuffer"]
path = third-party/TPCircularBuffer path = third-party/TPCircularBuffer
+63
View File
@@ -866,6 +866,38 @@ namespace confighttp {
outputTree.put("status", true); outputTree.put("status", true);
} }
void
updateClient(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return;
print_req(request);
std::stringstream ss;
ss << request->content.rdbuf();
pt::ptree inputTree, outputTree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_json(data, outputTree);
response->write(data.str());
});
try {
pt::read_json(ss, inputTree);
std::string uuid = inputTree.get<std::string>("uuid");
std::string name = inputTree.get<std::string>("name");
auto perm = (crypto::PERM)inputTree.get<uint32_t>("perm") & crypto::PERM::_all;
outputTree.put("status", nvhttp::update_device_info(uuid, name, perm));
}
catch (std::exception &e) {
BOOST_LOG(warning) << "Update Client: "sv << e.what();
outputTree.put("status", false);
outputTree.put("error", e.what());
return;
}
}
void void
unpair(resp_https_t response, req_https_t request) { unpair(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return; if (!authenticate(response, request)) return;
@@ -897,6 +929,35 @@ namespace confighttp {
} }
} }
void
disconnect(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return;
print_req(request);
std::stringstream ss;
ss << request->content.rdbuf();
pt::ptree inputTree, outputTree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_json(data, outputTree);
response->write(data.str());
});
try {
pt::read_json(ss, inputTree);
std::string uuid = inputTree.get<std::string>("uuid");
outputTree.put("status", nvhttp::find_and_stop_session(uuid, true));
}
catch (std::exception &e) {
BOOST_LOG(warning) << "Disconnect: "sv << e.what();
outputTree.put("status", false);
outputTree.put("error", e.what());
}
}
void void
listClients(resp_https_t response, req_https_t request) { listClients(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return; if (!authenticate(response, request)) return;
@@ -969,7 +1030,9 @@ namespace confighttp {
server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp; server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp;
server.resource["^/api/clients/unpair-all$"]["POST"] = unpairAll; server.resource["^/api/clients/unpair-all$"]["POST"] = unpairAll;
server.resource["^/api/clients/list$"]["GET"] = listClients; server.resource["^/api/clients/list$"]["GET"] = listClients;
server.resource["^/api/clients/update$"]["POST"] = updateClient;
server.resource["^/api/clients/unpair$"]["POST"] = unpair; server.resource["^/api/clients/unpair$"]["POST"] = unpair;
server.resource["^/api/clients/disconnect$"]["POST"] = disconnect;
server.resource["^/api/apps/close$"]["POST"] = closeApp; server.resource["^/api/apps/close$"]["POST"] = closeApp;
server.resource["^/api/covers/upload$"]["POST"] = uploadCover; server.resource["^/api/covers/upload$"]["POST"] = uploadCover;
server.resource["^/images/apollo.ico$"]["GET"] = getFaviconImage; server.resource["^/images/apollo.ico$"]["GET"] = getFaviconImage;
+6 -5
View File
@@ -11,11 +11,11 @@ namespace crypto {
cert_chain_t::cert_chain_t(): cert_chain_t::cert_chain_t():
_certs {}, _cert_ctx { X509_STORE_CTX_new() } {} _certs {}, _cert_ctx { X509_STORE_CTX_new() } {}
void void
cert_chain_t::add(x509_t &&cert) { cert_chain_t::add(p_named_cert_t& named_cert_p) {
x509_store_t x509_store { X509_STORE_new() }; x509_store_t x509_store { X509_STORE_new() };
X509_STORE_add_cert(x509_store.get(), cert.get()); X509_STORE_add_cert(x509_store.get(), x509(named_cert_p->cert).get());
_certs.emplace_back(std::make_pair(std::move(cert), std::move(x509_store))); _certs.emplace_back(std::make_pair(named_cert_p, std::move(x509_store)));
} }
void void
cert_chain_t::clear() { cert_chain_t::clear() {
@@ -52,9 +52,9 @@ namespace crypto {
* @return nullptr if the certificate is valid, otherwise an error string. * @return nullptr if the certificate is valid, otherwise an error string.
*/ */
const char * const char *
cert_chain_t::verify(x509_t::element_type *cert) { cert_chain_t::verify(x509_t::element_type *cert, p_named_cert_t& named_cert_out) {
int err_code = 0; int err_code = 0;
for (auto &[_, x509_store] : _certs) { for (auto &[named_cert_p, x509_store] : _certs) {
auto fg = util::fail_guard([this]() { auto fg = util::fail_guard([this]() {
X509_STORE_CTX_cleanup(_cert_ctx.get()); X509_STORE_CTX_cleanup(_cert_ctx.get());
}); });
@@ -70,6 +70,7 @@ namespace crypto {
auto err = X509_verify_cert(_cert_ctx.get()); auto err = X509_verify_cert(_cert_ctx.get());
if (err == 1) { if (err == 1) {
named_cert_out = named_cert_p;
return nullptr; return nullptr;
} }
+55 -3
View File
@@ -34,6 +34,58 @@ namespace crypto {
using pkey_ctx_t = util::safe_ptr<EVP_PKEY_CTX, EVP_PKEY_CTX_free>; using pkey_ctx_t = util::safe_ptr<EVP_PKEY_CTX, EVP_PKEY_CTX_free>;
using bignum_t = util::safe_ptr<BIGNUM, BN_free>; using bignum_t = util::safe_ptr<BIGNUM, BN_free>;
/**
* @brief The permissions of a client.
*/
enum class PERM: uint32_t {
_reserved = 1,
_input = _reserved << 8, // Input permission group
input_controller = _input << 0, // Allow controller input
input_touch = _input << 1, // Allow touch input
input_pen = _input << 2, // Allow pen input
input_kbdm = _input << 3, // Allow kbd/mouse input
_all_inputs = input_controller | input_touch | input_pen | input_kbdm,
_operation = _input << 8, // Operation permission group
clipboard_set = _operation << 0, // Allow set clipboard from client
clipboard_read = _operation << 1, // Allow read clipboard from host
file_upload = _operation << 2, // Allow upload files to host
file_dwnload = _operation << 3, // Allow download files from host
server_cmd = _operation << 4, // Allow execute server cmd
_all_opeiations = clipboard_set | clipboard_read | file_upload | file_dwnload | server_cmd,
_action = _operation << 8, // Action permission group
list = _action << 0, // Allow list apps
view = _action << 1, // Allow view streams
launch = _action << 2, // Allow launch apps
_allow_view = view | launch, // If no view permission is granted, disconnect the device upon permission update
_all_actions = list | view | launch,
_default = view | list, // Default permissions for new clients
_no = 0, // No permissions are granted
_all = _all_inputs | _all_opeiations | _all_actions, // All current permissions
};
inline constexpr PERM
operator&(PERM x, PERM y) {
return static_cast<PERM>(static_cast<uint32_t>(x) & static_cast<uint32_t>(y));
}
inline constexpr bool
operator!(PERM p) {
return static_cast<uint32_t>(p) == 0;
}
struct named_cert_t {
std::string name;
std::string uuid;
std::string cert;
PERM perm;
};
using p_named_cert_t = std::shared_ptr<named_cert_t>;
/** /**
* @brief Hashes the given plaintext using SHA-256. * @brief Hashes the given plaintext using SHA-256.
* @param plaintext * @param plaintext
@@ -76,16 +128,16 @@ namespace crypto {
KITTY_DECL_CONSTR(cert_chain_t) KITTY_DECL_CONSTR(cert_chain_t)
void void
add(x509_t &&cert); add(p_named_cert_t& named_cert_p);
void void
clear(); clear();
const char * const char *
verify(x509_t::element_type *cert); verify(x509_t::element_type *cert, p_named_cert_t& named_cert_out);
private: private:
std::vector<std::pair<x509_t, x509_store_t>> _certs; std::vector<std::pair<p_named_cert_t, x509_store_t>> _certs;
x509_store_ctx_t _cert_ctx; x509_store_ctx_t _cert_ctx;
}; };
+55 -1
View File
@@ -1628,7 +1628,61 @@ namespace input {
* @param input_data The input message. * @param input_data The input message.
*/ */
void void
passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data) { passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data, const crypto::PERM& permission) {
// No input permissions at all
if (!(permission & crypto::PERM::_all_inputs)) {
return;
}
// Have some input permission
// Otherwise have all input permission
if ((permission & crypto::PERM::_all_inputs) != crypto::PERM::_all_inputs) {
PNV_INPUT_HEADER payload = (PNV_INPUT_HEADER)input_data.data();
// Check permission
switch (util::endian::little(payload->magic)) {
case MULTI_CONTROLLER_MAGIC_GEN5:
case SS_CONTROLLER_ARRIVAL_MAGIC:
case SS_CONTROLLER_TOUCH_MAGIC:
case SS_CONTROLLER_MOTION_MAGIC:
case SS_CONTROLLER_BATTERY_MAGIC:
if (!(permission & crypto::PERM::input_controller)) {
return;
} else {
break;
}
case MOUSE_MOVE_REL_MAGIC_GEN5:
case MOUSE_MOVE_ABS_MAGIC:
case MOUSE_BUTTON_DOWN_EVENT_MAGIC_GEN5:
case MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5:
case SCROLL_MAGIC_GEN5:
case SS_HSCROLL_MAGIC:
case KEY_DOWN_EVENT_MAGIC:
case KEY_UP_EVENT_MAGIC:
case UTF8_TEXT_EVENT_MAGIC:
if (!(permission & crypto::PERM::input_kbdm)) {
return;
} else {
break;
}
case SS_TOUCH_MAGIC:
if (!(permission & crypto::PERM::input_touch)) {
return;
} else {
break;
}
case SS_PEN_MAGIC:
if (!(permission & crypto::PERM::input_pen)) {
return;
} else {
break;
}
default:
// Unknown input event
return;
}
}
{ {
std::lock_guard<std::mutex> lg(input->input_queue_lock); std::lock_guard<std::mutex> lg(input->input_queue_lock);
input->input_queue.push_back(std::move(input_data)); input->input_queue.push_back(std::move(input_data));
+2 -1
View File
@@ -8,6 +8,7 @@
#include "platform/common.h" #include "platform/common.h"
#include "thread_safe.h" #include "thread_safe.h"
#include "crypto.h"
namespace input { namespace input {
struct input_t; struct input_t;
@@ -17,7 +18,7 @@ namespace input {
void void
reset(std::shared_ptr<input_t> &input); reset(std::shared_ptr<input_t> &input);
void void
passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data); passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data, const crypto::PERM& permission);
[[nodiscard]] std::unique_ptr<platf::deinit_t> [[nodiscard]] std::unique_ptr<platf::deinit_t>
init(); init();
+274 -74
View File
@@ -31,6 +31,7 @@
#include "platform/common.h" #include "platform/common.h"
#include "process.h" #include "process.h"
#include "rtsp.h" #include "rtsp.h"
#include "stream.h"
#include "system_tray.h" #include "system_tray.h"
#include "utility.h" #include "utility.h"
#include "uuid.h" #include "uuid.h"
@@ -46,6 +47,15 @@ namespace nvhttp {
namespace fs = std::filesystem; namespace fs = std::filesystem;
namespace pt = boost::property_tree; 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; crypto::cert_chain_t cert_chain;
static std::string one_time_pin; static std::string one_time_pin;
static std::string otp_passphrase; static std::string otp_passphrase;
@@ -76,7 +86,7 @@ namespace nvhttp {
context.use_private_key_file(private_key_file, boost::asio::ssl::context::pem); context.use_private_key_file(private_key_file, boost::asio::ssl::context::pem);
} }
std::function<int(SSL *)> verify; std::function<bool(std::shared_ptr<Request>, SSL*)> verify;
std::function<void(std::shared_ptr<Response>, std::shared_ptr<Request>)> on_verify_failed; std::function<void(std::shared_ptr<Response>, std::shared_ptr<Request>)> on_verify_failed;
protected: protected:
@@ -120,7 +130,7 @@ namespace nvhttp {
if (!lock) if (!lock)
return; return;
if (!ec) { if (!ec) {
if (verify && !verify(session->connection->socket->native_handle())) if (verify && !verify(session->request, session->connection->socket->native_handle()))
this->write(session, on_verify_failed); this->write(session, on_verify_failed);
else else
this->read(session); this->read(session);
@@ -143,16 +153,6 @@ namespace nvhttp {
std::string pkey; std::string pkey;
} conf_intern; } conf_intern;
struct named_cert_t {
std::string name;
std::string uuid;
std::string cert;
};
struct client_t {
std::vector<named_cert_t> named_devices;
};
struct pair_session_t { struct pair_session_t {
struct { struct {
std::string uniqueID; std::string uniqueID;
@@ -225,13 +225,29 @@ namespace nvhttp {
pt::ptree node; pt::ptree node;
pt::ptree named_cert_nodes; pt::ptree named_cert_nodes;
for (auto &named_cert : client.named_devices) { std::unordered_set<std::string> unique_certs;
std::unordered_map<std::string, int> name_counts;
for (auto &named_cert_p : client.named_devices) {
if (unique_certs.insert(named_cert_p->cert).second) {
pt::ptree named_cert_node; pt::ptree named_cert_node;
named_cert_node.put("name"s, named_cert.name); std::string base_name = named_cert_p->name;
named_cert_node.put("cert"s, named_cert.cert); // Remove existing pending id if present
named_cert_node.put("uuid"s, named_cert.uuid); 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.put("name"s, final_name);
named_cert_node.put("cert"s, named_cert_p->cert);
named_cert_node.put("uuid"s, named_cert_p->uuid);
named_cert_node.put("perm"s, (uint32_t)named_cert_p->perm);
named_cert_nodes.push_back(std::make_pair(""s, named_cert_node)); named_cert_nodes.push_back(std::make_pair(""s, named_cert_node));
} }
}
root.add_child("root.named_devices"s, named_cert_nodes); root.add_child("root.named_devices"s, named_cert_nodes);
try { try {
@@ -282,11 +298,12 @@ namespace nvhttp {
if (device_node.count("certs")) { if (device_node.count("certs")) {
for (auto &[_, el] : device_node.get_child("certs")) { for (auto &[_, el] : device_node.get_child("certs")) {
named_cert_t named_cert; auto named_cert_p = std::make_shared<crypto::named_cert_t>();
named_cert.name = ""s; named_cert_p->name = ""s;
named_cert.cert = el.get_value<std::string>(); named_cert_p->cert = el.get_value<std::string>();
named_cert.uuid = uuid_util::uuid_t::generate().string(); named_cert_p->uuid = uuid_util::uuid_t::generate().string();
client.named_devices.emplace_back(named_cert); named_cert_p->perm = PERM::_all;
client.named_devices.emplace_back(named_cert_p);
} }
} }
} }
@@ -294,39 +311,42 @@ namespace nvhttp {
if (root.count("named_devices")) { if (root.count("named_devices")) {
for (auto &[_, el] : root.get_child("named_devices")) { for (auto &[_, el] : root.get_child("named_devices")) {
named_cert_t named_cert; auto named_cert_p = std::make_shared<crypto::named_cert_t>();
named_cert.name = el.get_child("name").get_value<std::string>(); named_cert_p->name = el.get<std::string>("name");
named_cert.cert = el.get_child("cert").get_value<std::string>(); named_cert_p->cert = el.get<std::string>("cert");
named_cert.uuid = el.get_child("uuid").get_value<std::string>(); named_cert_p->uuid = el.get<std::string>("uuid");
client.named_devices.emplace_back(named_cert); named_cert_p->perm = (PERM)el.get("perm", (uint32_t)PERM::_all) & PERM::_all;
client.named_devices.emplace_back(named_cert_p);
} }
} }
// Empty certificate chain and import certs from file // Empty certificate chain and import certs from file
cert_chain.clear(); cert_chain.clear();
for (auto &named_cert : client.named_devices) { for (auto &named_cert : client.named_devices) {
cert_chain.add(crypto::x509(named_cert.cert)); cert_chain.add(named_cert);
} }
client_root = client; client_root = client;
} }
void void
add_authorized_client(const std::string &name, std::string &&cert) { add_authorized_client(const p_named_cert_t& named_cert_p) {
client_t &client = client_root; client_t &client = client_root;
named_cert_t named_cert; client.named_devices.push_back(named_cert_p);
named_cert.name = name;
named_cert.cert = std::move(cert);
named_cert.uuid = uuid_util::uuid_t::generate().string(); #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
client.named_devices.emplace_back(named_cert); system_tray::update_tray_paired(named_cert_p->name);
#endif
if (!config::sunshine.flags[config::flag::FRESH_STATE]) { if (!config::sunshine.flags[config::flag::FRESH_STATE]) {
save_state(); save_state();
load_state();
} }
} }
std::shared_ptr<rtsp_stream::launch_session_t> std::shared_ptr<rtsp_stream::launch_session_t>
make_launch_session(bool host_audio, const args_t &args) { make_launch_session(bool host_audio, const args_t &args, const crypto::named_cert_t* named_cert_p) {
auto launch_session = std::make_shared<rtsp_stream::launch_session_t>(); auto launch_session = std::make_shared<rtsp_stream::launch_session_t>();
launch_session->id = ++session_id_counter; launch_session->id = ++session_id_counter;
@@ -345,8 +365,10 @@ namespace nvhttp {
if (x == 2) launch_session->fps = atoi(segment.c_str()); if (x == 2) launch_session->fps = atoi(segment.c_str());
x++; x++;
} }
launch_session->device_name = (get_arg(args, "devicename", "unknown"));
launch_session->unique_id = (get_arg(args, "uniqueid", "unknown")); 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 = util::from_view(get_arg(args, "appid", "unknown")); launch_session->appid = util::from_view(get_arg(args, "appid", "unknown"));
launch_session->enable_sops = util::from_view(get_arg(args, "sops", "0")); 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_info = util::from_view(get_arg(args, "surroundAudioInfo", "196610"));
@@ -458,7 +480,7 @@ namespace nvhttp {
} }
void void
clientpairingsecret(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, pair_session_t &sess, pt::ptree &tree, const args_t &args) { clientpairingsecret(pair_session_t &sess, pt::ptree &tree, const args_t &args) {
auto &client = sess.client; auto &client = sess.client;
auto pairingsecret = util::from_hex_vec(get_arg(args, "clientpairingsecret"), true); auto pairingsecret = util::from_hex_vec(get_arg(args, "clientpairingsecret"), true);
@@ -487,11 +509,26 @@ namespace nvhttp {
// if hash not correct, probably MITM // if hash not correct, probably MITM
if (!std::memcmp(hash.data(), sess.clienthash.data(), hash.size()) && crypto::verify256(crypto::x509(client.cert), secret, sign)) { if (!std::memcmp(hash.data(), sess.clienthash.data(), hash.size()) && crypto::verify256(crypto::x509(client.cert), secret, sign)) {
tree.put("root.paired", 1); tree.put("root.paired", 1);
add_cert->raise(crypto::x509(client.cert));
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); auto it = map_id_sess.find(client.uniqueID);
add_authorized_client(client.name, std::move(client.cert));
map_id_sess.erase(it); map_id_sess.erase(it);
add_authorized_client(named_cert_p);
} }
else { else {
map_id_sess.erase(client.uniqueID); map_id_sess.erase(client.uniqueID);
@@ -514,6 +551,11 @@ namespace nvhttp {
static auto constexpr to_string = "NONE"sv; 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> template <class T>
void void
print_req(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) { print_req(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
@@ -557,7 +599,7 @@ namespace nvhttp {
template <class T> template <class T>
void void
pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) { pair(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
print_req<T>(request); print_req<T>(request);
pt::ptree tree; pt::ptree tree;
@@ -621,10 +663,6 @@ namespace nvhttp {
if (hash.to_string_view() == it->second) { if (hash.to_string_view() == it->second) {
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
system_tray::update_tray_otp_pair(ptr->second.client.name);
#endif
if (!otp_device_name.empty()) { if (!otp_device_name.empty()) {
ptr->second.client.name = std::move(otp_device_name); ptr->second.client.name = std::move(otp_device_name);
} }
@@ -673,7 +711,7 @@ namespace nvhttp {
serverchallengeresp(sess_it->second, tree, args); serverchallengeresp(sess_it->second, tree, args);
} }
else if (it = args.find("clientpairingsecret"); it != std::end(args)) { else if (it = args.find("clientpairingsecret"); it != std::end(args)) {
clientpairingsecret(add_cert, sess_it->second, tree, args); clientpairingsecret(sess_it->second, tree, args);
} }
else { else {
tree.put("root.<xmlattr>.status_code", 404); tree.put("root.<xmlattr>.status_code", 404);
@@ -767,6 +805,8 @@ namespace nvhttp {
if constexpr (std::is_same_v<SunshineHTTPS, T>) { if constexpr (std::is_same_v<SunshineHTTPS, T>) {
tree.put("root.mac", platf::get_mac_address(net::addr_to_normalized_string(local_endpoint.address()))); 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"); pt::ptree& root_node = tree.get_child("root");
if (config::sunshine.server_cmds.size() > 0) { if (config::sunshine.server_cmds.size() > 0) {
@@ -777,10 +817,19 @@ namespace nvhttp {
root_node.push_back(std::make_pair("ServerCommand", cmd_node)); 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 #ifdef _WIN32
tree.put("root.VirtualDisplayCapable", true); tree.put("root.VirtualDisplayCapable", true);
if (!!(named_cert_p->perm & PERM::_all_actions)) {
tree.put("root.VirtualDisplayDriverReady", proc::vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK); tree.put("root.VirtualDisplayDriverReady", proc::vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK);
} else {
tree.put("root.VirtualDisplayDriverReady", true);
}
#endif #endif
} }
else { else {
@@ -833,10 +882,16 @@ namespace nvhttp {
} }
tree.put("root.ServerCodecModeSupport", codec_mode_flags); tree.put("root.ServerCodecModeSupport", codec_mode_flags);
auto current_appid = proc::proc.running();
tree.put("root.PairStatus", pair_status); tree.put("root.PairStatus", pair_status);
if constexpr (std::is_same_v<SunshineHTTPS, T>) {
auto current_appid = proc::proc.running();
tree.put("root.currentgame", current_appid); tree.put("root.currentgame", current_appid);
tree.put("root.state", current_appid > 0 ? "SUNSHINE_SERVER_BUSY" : "SUNSHINE_SERVER_FREE"); 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; std::ostringstream data;
@@ -849,10 +904,29 @@ namespace nvhttp {
get_all_clients() { get_all_clients() {
pt::ptree named_cert_nodes; pt::ptree named_cert_nodes;
client_t &client = client_root; client_t &client = client_root;
for (auto &named_cert : client.named_devices) {
std::list<std::string> connected_uuids = rtsp_stream::get_all_session_uuids();
for (auto &named_cert_p : client.named_devices) {
pt::ptree named_cert_node; pt::ptree named_cert_node;
named_cert_node.put("name"s, named_cert.name); named_cert_node.put("name"s, named_cert_p->name);
named_cert_node.put("uuid"s, named_cert.uuid); named_cert_node.put("uuid"s, named_cert_p->uuid);
named_cert_node.put("perm", (uint32_t)named_cert_p->perm);
if (connected_uuids.empty()) {
named_cert_node.put("connected"s, false);
} else {
bool connected = false;
for (auto it = connected_uuids.begin(); it != connected_uuids.end(); ++it) {
if (*it == named_cert_p->uuid) {
connected = true;
connected_uuids.erase(it);
break;
}
}
named_cert_node.put("connected"s, connected);
}
named_cert_nodes.push_back(std::make_pair(""s, named_cert_node)); named_cert_nodes.push_back(std::make_pair(""s, named_cert_node));
} }
@@ -877,6 +951,8 @@ namespace nvhttp {
apps.put("<xmlattr>.status_code", 200); apps.put("<xmlattr>.status_code", 200);
auto named_cert_p = get_verified_cert(request);
if (!!(named_cert_p->perm & PERM::_all_actions)) {
for (auto &proc : proc::proc.get_apps()) { for (auto &proc : proc::proc.get_apps()) {
pt::ptree app; pt::ptree app;
@@ -886,6 +962,20 @@ namespace nvhttp {
apps.push_back(std::make_pair("App", std::move(app))); apps.push_back(std::make_pair("App", std::move(app)));
} }
} else {
BOOST_LOG(debug) << "Permission ListApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")";
pt::ptree app;
app.put("IsHdrSupported"s, 0);
app.put("AppTitle"s, "Permission Denied");
app.put("ID", "1145141919810");
apps.push_back(std::make_pair("App", std::move(app)));
return;
}
} }
void void
@@ -901,6 +991,17 @@ namespace nvhttp {
response->close_connection_after_response = true; 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 LaunchApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")";
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 503);
tree.put("root.<xmlattr>.status_message", "Permission denied");
return;
}
if (rtsp_stream::session_count() == config::stream.channels) { if (rtsp_stream::session_count() == config::stream.channels) {
tree.put("root.resume", 0); tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 503); tree.put("root.<xmlattr>.status_code", 503);
@@ -932,7 +1033,7 @@ namespace nvhttp {
} }
host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); host_audio = util::from_view(get_arg(args, "localAudioPlayMode"));
auto launch_session = make_launch_session(host_audio, args); auto launch_session = make_launch_session(host_audio, args, named_cert_p);
auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address());
if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) {
@@ -998,6 +1099,17 @@ namespace nvhttp {
response->close_connection_after_response = true; 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", 503);
tree.put("root.<xmlattr>.status_message", "Permission denied");
return;
}
// It is possible that due a race condition that this if-statement gives a false negative, // It is possible that due a race condition that this if-statement gives a false negative,
// that is automatically resolved in rtsp_server_t // that is automatically resolved in rtsp_server_t
if (rtsp_stream::session_count() == config::stream.channels) { if (rtsp_stream::session_count() == config::stream.channels) {
@@ -1049,7 +1161,7 @@ namespace nvhttp {
} }
} }
auto launch_session = make_launch_session(host_audio, args); auto launch_session = make_launch_session(host_audio, args, named_cert_p);
auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address());
if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) {
@@ -1069,6 +1181,10 @@ namespace nvhttp {
tree.put("root.resume", 1); tree.put("root.resume", 1);
rtsp_stream::launch_session_raise(launch_session); 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 void
@@ -1084,6 +1200,17 @@ namespace nvhttp {
response->close_connection_after_response = true; 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", 503);
tree.put("root.<xmlattr>.status_message", "Permission denied");
return;
}
// It is possible that due a race condition that this if-statement gives a false positive, // It is possible that due a race condition that this if-statement gives a false positive,
// the client should try again // the client should try again
if (rtsp_stream::session_count() != 0) { if (rtsp_stream::session_count() != 0) {
@@ -1106,9 +1233,27 @@ namespace nvhttp {
appasset(resp_https_t response, req_https_t request) { appasset(resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(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 args = request->parse_query_string();
auto app_image = proc::proc.get_app_image(util::from_view(get_arg(args, "appid"))); 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); std::ifstream in(app_image, std::ios::binary);
SimpleWeb::CaseInsensitiveMultimap headers; SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "image/png"); headers.emplace("Content-Type", "image/png");
@@ -1133,8 +1278,6 @@ namespace nvhttp {
conf_intern.pkey = file_handler::read_file(config::nvhttp.pkey.c_str()); conf_intern.pkey = file_handler::read_file(config::nvhttp.pkey.c_str());
conf_intern.servercert = file_handler::read_file(config::nvhttp.cert.c_str()); conf_intern.servercert = file_handler::read_file(config::nvhttp.cert.c_str());
auto add_cert = std::make_shared<safe::queue_t<crypto::x509_t>>(30);
// resume doesn't always get the parameter "localAudioPlayMode" // resume doesn't always get the parameter "localAudioPlayMode"
// launch will store it in host_audio // launch will store it in host_audio
bool host_audio {}; bool host_audio {};
@@ -1143,43 +1286,39 @@ namespace nvhttp {
http_server_t http_server; http_server_t http_server;
// Verify certificates after establishing connection // Verify certificates after establishing connection
https_server.verify = [add_cert](SSL *ssl) { https_server.verify = [](req_https_t req, SSL *ssl) {
crypto::x509_t x509 { SSL_get_peer_certificate(ssl) }; crypto::x509_t x509 { SSL_get_peer_certificate(ssl) };
if (!x509) { if (!x509) {
BOOST_LOG(info) << "unknown -- denied"sv; BOOST_LOG(info) << "unknown -- denied"sv;
return 0; return false;
} }
int verified = 0; bool verified = false;
p_named_cert_t named_cert_p;
auto fg = util::fail_guard([&]() { auto fg = util::fail_guard([&]() {
char subject_name[256]; char subject_name[256];
X509_NAME_oneline(X509_get_subject_name(x509.get()), subject_name, sizeof(subject_name)); X509_NAME_oneline(X509_get_subject_name(x509.get()), subject_name, sizeof(subject_name));
BOOST_LOG(debug) << subject_name << " -- "sv << (verified ? "verified"sv : "denied"sv); 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;
}
}); });
while (add_cert->peek()) { auto err_str = cert_chain.verify(x509.get(), named_cert_p);
char subject_name[256];
auto cert = add_cert->pop();
X509_NAME_oneline(X509_get_subject_name(cert.get()), subject_name, sizeof(subject_name));
BOOST_LOG(debug) << "Added cert ["sv << subject_name << ']';
cert_chain.add(std::move(cert));
}
auto err_str = cert_chain.verify(x509.get());
if (err_str) { if (err_str) {
BOOST_LOG(warning) << "SSL Verification error :: "sv << err_str; BOOST_LOG(warning) << "SSL Verification error :: "sv << err_str;
return verified; return verified;
} }
verified = 1; verified = true;
req->userp = named_cert_p;
return verified; return true;
}; };
https_server.on_verify_failed = [](resp_https_t resp, req_https_t req) { https_server.on_verify_failed = [](resp_https_t resp, req_https_t req) {
@@ -1199,7 +1338,7 @@ namespace nvhttp {
https_server.default_resource["GET"] = not_found<SunshineHTTPS>; https_server.default_resource["GET"] = not_found<SunshineHTTPS>;
https_server.resource["^/serverinfo$"]["GET"] = serverinfo<SunshineHTTPS>; https_server.resource["^/serverinfo$"]["GET"] = serverinfo<SunshineHTTPS>;
https_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair<SunshineHTTPS>(add_cert, resp, req); }; https_server.resource["^/pair$"]["GET"] = pair<SunshineHTTPS>;
https_server.resource["^/applist$"]["GET"] = applist; https_server.resource["^/applist$"]["GET"] = applist;
https_server.resource["^/appasset$"]["GET"] = appasset; 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["^/launch$"]["GET"] = [&host_audio](auto resp, auto req) { launch(host_audio, resp, req); };
@@ -1212,7 +1351,7 @@ namespace nvhttp {
http_server.default_resource["GET"] = not_found<SimpleWeb::HTTP>; http_server.default_resource["GET"] = not_found<SimpleWeb::HTTP>;
http_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTP>; http_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTP>;
http_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair<SimpleWeb::HTTP>(add_cert, resp, req); }; http_server.resource["^/pair$"]["GET"] = pair<SimpleWeb::HTTP>;
http_server.config.reuse_address = true; http_server.config.reuse_address = true;
http_server.config.address = net::af_to_any_address_string(address_family); http_server.config.address = net::af_to_any_address_string(address_family);
@@ -1265,6 +1404,55 @@ namespace nvhttp {
client_root = client; client_root = client;
cert_chain.clear(); cert_chain.clear();
save_state(); 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 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->perm = newPerm;
save_state();
return true;
}
}
return false;
} }
int int
@@ -1272,7 +1460,7 @@ namespace nvhttp {
int removed = 0; int removed = 0;
client_t &client = client_root; client_t &client = client_root;
for (auto it = client.named_devices.begin(); it != client.named_devices.end();) { for (auto it = client.named_devices.begin(); it != client.named_devices.end();) {
if ((*it).uuid == uuid) { if ((*it)->uuid == uuid) {
it = client.named_devices.erase(it); it = client.named_devices.erase(it);
removed++; removed++;
} }
@@ -1283,6 +1471,18 @@ namespace nvhttp {
save_state(); save_state();
load_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; return removed;
} }
} // namespace nvhttp } // namespace nvhttp
+45
View File
@@ -13,6 +13,8 @@
#include <boost/property_tree/ptree.hpp> #include <boost/property_tree/ptree.hpp>
// local includes // local includes
#include "crypto.h"
#include "rtsp.h"
#include "thread_safe.h" #include "thread_safe.h"
using namespace std::chrono_literals; using namespace std::chrono_literals;
@@ -96,4 +98,47 @@ namespace nvhttp {
*/ */
void void
erase_all_clients(); erase_all_clients();
/**
* @brief Stops a session.
*
* @param session The session
* @param[in] graceful Whether to stop gracefully
*/
void stop_session(stream::session_t& session, bool graceful);
/**
* @brief Finds and stop session.
*
* @param[in] uuid The uuid string
* @param[in] graceful Whether to stop gracefully
*/
bool find_and_stop_session(const std::string& uuid, bool graceful);
/**
* @brief Update device info associated to the session
*
* @param session The session
* @param[in] name New name
* @param[in] newPerm New permission
*/
void update_session_info(stream::session_t& session, const std::string& name, const crypto::PERM newPerm);
/**
* @brief Finds and udpate session information.
*
* @param[in] uuid The uuid string
* @param[in] name New name
* @param[in] newPerm New permission
*/
bool find_and_udpate_session_info(const std::string& uuid, const std::string& name, const crypto::PERM newPerm);
/**
* @brief Update device info
*
* @param[in] uuid The uuid string
* @param[in] name New name
* @param[in] newPerm New permission
*/
bool update_device_info(const std::string& uuid, const std::string& name, const crypto::PERM newPerm);
} // namespace nvhttp } // namespace nvhttp
+1 -20
View File
@@ -290,34 +290,15 @@ bool setRenderAdapterByName(const std::wstring& adapterName) {
std::wstring createVirtualDisplay( std::wstring createVirtualDisplay(
const char* s_client_uid, const char* s_client_uid,
const char* s_client_name, const char* s_client_name,
const char* s_app_name,
uint32_t width, uint32_t width,
uint32_t height, uint32_t height,
uint32_t fps, uint32_t fps,
GUID& guid const GUID& guid
) { ) {
if (SUDOVDA_DRIVER_HANDLE == INVALID_HANDLE_VALUE) { if (SUDOVDA_DRIVER_HANDLE == INVALID_HANDLE_VALUE) {
return std::wstring(); return std::wstring();
} }
if (!s_app_name || !strlen(s_app_name) || !strcmp(s_app_name, "unknown")) {
s_app_name = "ApolloVDisp";
}
if (!s_client_name || !strlen(s_client_name) || !strcmp(s_client_name, "unknown")) {
s_client_name = s_app_name;
}
if (s_client_uid && strcmp(s_client_uid, "unknown")) {
size_t len = strlen(s_client_uid);
if (len > sizeof(GUID)) {
len = sizeof(GUID);
}
memcpy((void*)&guid, (void*)s_client_uid, len);
} else {
s_client_uid = "unknown";
}
VIRTUAL_DISPLAY_ADD_OUT output; VIRTUAL_DISPLAY_ADD_OUT output;
if (!AddVirtualDisplay(SUDOVDA_DRIVER_HANDLE, width, height, fps, guid, s_client_name, s_client_uid, output)) { if (!AddVirtualDisplay(SUDOVDA_DRIVER_HANDLE, width, height, fps, guid, s_client_name, s_client_uid, output)) {
printf("[SUDOVDA] Failed to add virtual display.\n"); printf("[SUDOVDA] Failed to add virtual display.\n");
+1 -2
View File
@@ -35,11 +35,10 @@ namespace VDISPLAY {
std::wstring createVirtualDisplay( std::wstring createVirtualDisplay(
const char* s_client_uid, const char* s_client_uid,
const char* s_client_name, const char* s_client_name,
const char* s_app_name,
uint32_t width, uint32_t width,
uint32_t height, uint32_t height,
uint32_t fps, uint32_t fps,
GUID& guid const GUID& guid
); );
bool removeVirtualDisplay(const GUID& guid); bool removeVirtualDisplay(const GUID& guid);
} }
+4 -2
View File
@@ -29,6 +29,7 @@
#include "system_tray.h" #include "system_tray.h"
#include "utility.h" #include "utility.h"
#include "video.h" #include "video.h"
#include "uuid.h"
#ifdef _WIN32 #ifdef _WIN32
// from_utf8() string conversion function // from_utf8() string conversion function
@@ -205,12 +206,13 @@ namespace proc {
VDISPLAY::setRenderAdapterByName(platf::from_utf8(config::video.adapter_name)); VDISPLAY::setRenderAdapterByName(platf::from_utf8(config::video.adapter_name));
} }
memcpy(&launch_session->display_guid, &http::uuid, sizeof(GUID)); auto device_uuid = uuid_util::uuid_t::parse(launch_session->unique_id);
memcpy(&launch_session->display_guid, &device_uuid, sizeof(GUID));
std::wstring vdisplayName = VDISPLAY::createVirtualDisplay( std::wstring vdisplayName = VDISPLAY::createVirtualDisplay(
launch_session->unique_id.c_str(), launch_session->unique_id.c_str(),
launch_session->device_name.c_str(), launch_session->device_name.c_str(),
_app.name.c_str(),
render_width, render_width,
render_height, render_height,
launch_session->fps, launch_session->fps,
+35
View File
@@ -618,6 +618,31 @@ namespace rtsp_stream {
return nullptr; return nullptr;
} }
std::shared_ptr<stream::session_t>
find_session(const std::string& uuid) {
auto lg = _session_slots.lock();
for (auto &slot : *_session_slots) {
if (slot && stream::session::uuid_match(*slot, uuid)) {
return slot;
}
}
return nullptr;
}
std::list<std::string>
get_all_session_uuids() {
std::list<std::string> uuids;
auto lg = _session_slots.lock();
for (auto &slot : *_session_slots) {
if (slot) {
uuids.push_back(stream::session::uuid(*slot));
}
}
return uuids;
}
private: private:
std::unordered_map<std::string_view, cmd_func_t> _map_cmd_cb; std::unordered_map<std::string_view, cmd_func_t> _map_cmd_cb;
@@ -652,6 +677,16 @@ namespace rtsp_stream {
return server.session_count(); return server.session_count();
} }
std::shared_ptr<stream::session_t>
find_session(const std::string& uuid) {
return server.find_session(uuid);
}
std::list<std::string>
get_all_session_uuids() {
return server.get_all_session_uuids();
}
int int
send(tcp::socket &sock, const std::string_view &sv) { send(tcp::socket &sock, const std::string_view &sv) {
std::size_t bytes_send = 0; std::size_t bytes_send = 0;
+15 -1
View File
@@ -5,10 +5,16 @@
#pragma once #pragma once
#include <atomic> #include <atomic>
#include <memory>
#include "crypto.h" #include "crypto.h"
#include "thread_safe.h" #include "thread_safe.h"
// Resolve circular dependencies
namespace stream {
struct session_t;
}
namespace rtsp_stream { namespace rtsp_stream {
constexpr auto RTSP_SETUP_PORT = 21; constexpr auto RTSP_SETUP_PORT = 21;
@@ -21,9 +27,11 @@ namespace rtsp_stream {
std::string av_ping_payload; std::string av_ping_payload;
uint32_t control_connect_data; uint32_t control_connect_data;
bool host_audio;
std::string device_name; std::string device_name;
std::string unique_id; std::string unique_id;
crypto::PERM perm;
bool host_audio;
int width; int width;
int height; int height;
int fps; int fps;
@@ -58,6 +66,12 @@ namespace rtsp_stream {
int int
session_count(); session_count();
std::shared_ptr<stream::session_t>
find_session(const std::string& uuid);
std::list<std::string>
get_all_session_uuids();
void void
rtpThread(); rtpThread();
+93 -2
View File
@@ -20,6 +20,7 @@ extern "C" {
} }
#include "config.h" #include "config.h"
#include "crypto.h"
#include "globals.h" #include "globals.h"
#include "input.h" #include "input.h"
#include "logging.h" #include "logging.h"
@@ -403,6 +404,9 @@ namespace stream {
} control; } control;
std::uint32_t launch_session_id; std::uint32_t launch_session_id;
std::string device_name;
std::string device_uuid;
crypto::PERM permission;
safe::mail_raw_t::event_t<bool> shutdown_event; safe::mail_raw_t::event_t<bool> shutdown_event;
safe::signal_t controlEnd; safe::signal_t controlEnd;
@@ -992,11 +996,17 @@ namespace stream {
std::copy(payload.end() - 16, payload.end(), std::begin(iv)); std::copy(payload.end() - 16, payload.end(), std::begin(iv));
} }
input::passthrough(session->input, std::move(plaintext)); input::passthrough(session->input, std::move(plaintext), session->permission);
}); });
server->map(packetTypes[IDX_EXEC_SERVER_CMD], [server](session_t *session, const std::string_view &payload) { server->map(packetTypes[IDX_EXEC_SERVER_CMD], [server](session_t *session, const std::string_view &payload) {
BOOST_LOG(debug) << "type [IDX_EXEC_SERVER_CMD]"sv; BOOST_LOG(debug) << "type [IDX_EXEC_SERVER_CMD]"sv;
if (!(session->permission & crypto::PERM::server_cmd)) {
BOOST_LOG(debug) << "Permission Exec Server Cmd deined for [" << session->device_name << "]";
return;
}
uint8_t cmdIndex = *(uint8_t*)payload.data(); uint8_t cmdIndex = *(uint8_t*)payload.data();
if (cmdIndex < config::sunshine.server_cmds.size()) { if (cmdIndex < config::sunshine.server_cmds.size()) {
@@ -1024,10 +1034,20 @@ namespace stream {
server->map(packetTypes[IDX_SET_CLIPBOARD], [server](session_t *session, const std::string_view &payload) { server->map(packetTypes[IDX_SET_CLIPBOARD], [server](session_t *session, const std::string_view &payload) {
BOOST_LOG(info) << "type [IDX_SET_CLIPBOARD]: "sv << payload << " size: " << payload.size(); BOOST_LOG(info) << "type [IDX_SET_CLIPBOARD]: "sv << payload << " size: " << payload.size();
if (!(session->permission & crypto::PERM::clipboard_set)) {
BOOST_LOG(debug) << "Permission Clipboard Set deined for [" << session->device_name << "]";
return;
}
}); });
server->map(packetTypes[IDX_FILE_TRANSFER_NONCE_REQUEST], [server](session_t *session, const std::string_view &payload) { server->map(packetTypes[IDX_FILE_TRANSFER_NONCE_REQUEST], [server](session_t *session, const std::string_view &payload) {
BOOST_LOG(info) << "type [IDX_FILE_TRANSFER_NONCE_REQUEST]: "sv << payload << " size: " << payload.size(); BOOST_LOG(info) << "type [IDX_FILE_TRANSFER_NONCE_REQUEST]: "sv << payload << " size: " << payload.size();
if (!(session->permission & crypto::PERM::file_upload)) {
BOOST_LOG(debug) << "Permission File Upload deined for [" << session->device_name << "]";
return;
}
}); });
server->map(packetTypes[IDX_ENCRYPTED], [server](session_t *session, const std::string_view &payload) { server->map(packetTypes[IDX_ENCRYPTED], [server](session_t *session, const std::string_view &payload) {
@@ -1091,7 +1111,7 @@ namespace stream {
// IDX_INPUT_DATA callback will attempt to decrypt unencrypted data, therefore we need pass it directly // IDX_INPUT_DATA callback will attempt to decrypt unencrypted data, therefore we need pass it directly
if (type == packetTypes[IDX_INPUT_DATA]) { if (type == packetTypes[IDX_INPUT_DATA]) {
plaintext.erase(std::begin(plaintext), std::begin(plaintext) + 4); plaintext.erase(std::begin(plaintext), std::begin(plaintext) + 4);
input::passthrough(session->input, std::move(plaintext)); input::passthrough(session->input, std::move(plaintext), session->permission);
} }
else { else {
server->call(type, session, next_payload, true); server->call(type, session, next_payload, true);
@@ -1949,6 +1969,40 @@ namespace stream {
return session.state.load(std::memory_order_relaxed); return session.state.load(std::memory_order_relaxed);
} }
inline bool
send(session_t& session, const std::string_view &payload) {
return session.broadcast_ref->control_server.send(payload, session.control.peer);
}
std::string
uuid(const session_t& session) {
return session.device_uuid;
}
bool
uuid_match(const session_t &session, const std::string& uuid) {
return session.device_uuid == uuid;
}
bool
update_device_info(session_t& session, const std::string& name, const crypto::PERM& newPerm) {
session.permission = newPerm;
if (!(newPerm & crypto::PERM::_allow_view)) {
BOOST_LOG(debug) << "Session: View permission revoked for [" << session.device_name << "], disconnecting...";
graceful_stop(session);
return true;
}
BOOST_LOG(debug) << "Session: Permission updated for [" << session.device_name << "]";
if (session.device_name != name) {
BOOST_LOG(debug) << "Session: Device name changed from [" << session.device_name << "] to [" << name << "]";
session.device_name = name;
}
return false;
}
void void
stop(session_t &session) { stop(session_t &session) {
while_starting_do_nothing(session.state); while_starting_do_nothing(session.state);
@@ -1961,6 +2015,40 @@ namespace stream {
session.shutdown_event->raise(true); session.shutdown_event->raise(true);
} }
void
graceful_stop(session_t& session) {
while_starting_do_nothing(session.state);
auto expected = state_e::RUNNING;
auto already_stopping = !session.state.compare_exchange_strong(expected, state_e::STOPPING);
if (already_stopping) {
return;
}
// reason: graceful termination
std::uint32_t reason = 0x80030023;
control_terminate_t plaintext;
plaintext.header.type = packetTypes[IDX_TERMINATION];
plaintext.header.payloadLength = sizeof(plaintext.ec);
plaintext.ec = util::endian::big<uint32_t>(reason);
// We may not have gotten far enough to have an ENet connection yet
if (session.control.peer) {
std::array<std::uint8_t,
sizeof(control_encrypted_t) + crypto::cipher::round_to_pkcs7_padded(sizeof(plaintext)) + crypto::cipher::tag_size>
encrypted_payload;
auto payload = stream::encode_control(&session, util::view(plaintext), encrypted_payload);
if (send(session, payload)) {
TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *) &session.control.peer->address.address));
BOOST_LOG(warning) << "Couldn't send termination code to ["sv << addr << ':' << port << ']';
}
}
session.shutdown_event->raise(true);
session.controlEnd.raise(true);
}
void void
join(session_t &session) { join(session_t &session) {
// Current Nvidia drivers have a bug where NVENC can deadlock the encoder thread with hardware-accelerated // Current Nvidia drivers have a bug where NVENC can deadlock the encoder thread with hardware-accelerated
@@ -2051,6 +2139,9 @@ namespace stream {
session->shutdown_event = mail->event<bool>(mail::shutdown); session->shutdown_event = mail->event<bool>(mail::shutdown);
session->launch_session_id = launch_session.id; session->launch_session_id = launch_session.id;
session->device_name = launch_session.device_name;
session->device_uuid = launch_session.unique_id;
session->permission = launch_session.perm;
session->config = config; session->config = config;
+10
View File
@@ -43,13 +43,23 @@ namespace stream {
std::shared_ptr<session_t> std::shared_ptr<session_t>
alloc(config_t &config, rtsp_stream::launch_session_t &launch_session); alloc(config_t &config, rtsp_stream::launch_session_t &launch_session);
std::string
uuid(const session_t& session);
bool
uuid_match(const session_t& session, const std::string& uuid);
bool
update_device_info(session_t& session, const std::string& name, const crypto::PERM& newPerm);
int int
start(session_t &session, const std::string &addr_string); start(session_t &session, const std::string &addr_string);
void void
stop(session_t &session); stop(session_t &session);
void void
graceful_stop(session_t& session);
void
join(session_t &session); join(session_t &session);
state_e state_e
state(session_t &session); state(session_t &session);
inline bool
send(session_t& session);
} // namespace session } // namespace session
} // namespace stream } // namespace stream
+33 -11
View File
@@ -268,8 +268,8 @@ namespace system_tray {
snprintf(msg, std::size(msg), "Streaming started for %s", app_name.c_str()); snprintf(msg, std::size(msg), "Streaming started for %s", app_name.c_str());
snprintf(force_close_msg, std::size(force_close_msg), "Force close [%s]", app_name.c_str()); snprintf(force_close_msg, std::size(force_close_msg), "Force close [%s]", app_name.c_str());
#ifdef _WIN32 #ifdef _WIN32
strcpy(msg, convertUtf8ToCurrentCodepage(msg).c_str()); strncpy(msg, convertUtf8ToCurrentCodepage(msg).c_str(), std::size(msg) - 1);
strcpy(force_close_msg, convertUtf8ToCurrentCodepage(force_close_msg).c_str()); strncpy(force_close_msg, convertUtf8ToCurrentCodepage(force_close_msg).c_str(), std::size(force_close_msg) - 1);
#endif #endif
tray.notification_text = msg; tray.notification_text = msg;
tray.notification_icon = TRAY_ICON_PLAYING; tray.notification_icon = TRAY_ICON_PLAYING;
@@ -293,7 +293,7 @@ namespace system_tray {
char msg[256]; char msg[256];
snprintf(msg, std::size(msg), "Streaming paused for %s", app_name.c_str()); snprintf(msg, std::size(msg), "Streaming paused for %s", app_name.c_str());
#ifdef _WIN32 #ifdef _WIN32
strcpy(msg, convertUtf8ToCurrentCodepage(msg).c_str()); strncpy(msg, convertUtf8ToCurrentCodepage(msg).c_str(), std::size(msg) - 1);
#endif #endif
tray.icon = TRAY_ICON_PAUSING; tray.icon = TRAY_ICON_PAUSING;
tray.notification_title = "Stream Paused"; tray.notification_title = "Stream Paused";
@@ -318,7 +318,7 @@ namespace system_tray {
char msg[256]; char msg[256];
snprintf(msg, std::size(msg), "Streaming stopped for %s", app_name.c_str()); snprintf(msg, std::size(msg), "Streaming stopped for %s", app_name.c_str());
#ifdef _WIN32 #ifdef _WIN32
strcpy(msg, convertUtf8ToCurrentCodepage(msg).c_str()); strncpy(msg, convertUtf8ToCurrentCodepage(msg).c_str(), std::size(msg) - 1);
#endif #endif
tray.icon = TRAY_ICON; tray.icon = TRAY_ICON;
tray.notification_icon = TRAY_ICON; tray.notification_icon = TRAY_ICON;
@@ -344,7 +344,7 @@ namespace system_tray {
char msg[256]; char msg[256];
snprintf(msg, std::size(msg), "Application %s exited too fast with code %d. Click here to terminate the stream.", app_name.c_str(), exit_code); snprintf(msg, std::size(msg), "Application %s exited too fast with code %d. Click here to terminate the stream.", app_name.c_str(), exit_code);
#ifdef _WIN32 #ifdef _WIN32
strcpy(msg, convertUtf8ToCurrentCodepage(msg).c_str()); strncpy(msg, convertUtf8ToCurrentCodepage(msg).c_str(), std::size(msg) - 1);
#endif #endif
tray.icon = TRAY_ICON; tray.icon = TRAY_ICON;
tray.notification_icon = TRAY_ICON; tray.notification_icon = TRAY_ICON;
@@ -382,7 +382,30 @@ namespace system_tray {
} }
void void
update_tray_otp_pair(std::string device_name) { update_tray_paired(std::string device_name) {
if (!tray_initialized) {
return;
}
tray.notification_title = NULL;
tray.notification_text = NULL;
tray.notification_cb = NULL;
tray.notification_icon = NULL;
tray_update(&tray);
char msg[256];
snprintf(msg, std::size(msg), "Device %s paired Succesfully. Please make sure you have access to the device.", device_name.c_str());
#ifdef _WIN32
strncpy(msg, convertUtf8ToCurrentCodepage(msg).c_str(), std::size(msg) - 1 - 1);
#endif
tray.notification_title = "Device Paired Succesfully";
tray.notification_text = msg;
tray.notification_icon = TRAY_ICON;
tray.tooltip = PROJECT_NAME;
tray_update(&tray);
}
void
update_tray_client_connected(std::string client_name) {
if (!tray_initialized) { if (!tray_initialized) {
return; return;
} }
@@ -394,14 +417,13 @@ namespace system_tray {
tray.icon = TRAY_ICON; tray.icon = TRAY_ICON;
tray_update(&tray); tray_update(&tray);
char msg[256]; char msg[256];
snprintf(msg, std::size(msg), "OTP Pairing started for device \"%s\". Please make sure you have access to the device initiating the pairing request.", device_name.c_str()); snprintf(msg, std::size(msg), "%s has connected to the session.", client_name.c_str());
#ifdef _WIN32 #ifdef _WIN32
strcpy(msg, convertUtf8ToCurrentCodepage(msg).c_str()); strncpy(msg, convertUtf8ToCurrentCodepage(msg).c_str(), std::size(msg) - 1);
#endif #endif
tray.icon = TRAY_ICON; tray.notification_title = "Client Connected";
tray.notification_title = "Incoming OTP Pairing Request";
tray.notification_text = msg; tray.notification_text = msg;
tray.notification_icon = TRAY_ICON_LOCKED; tray.notification_icon = TRAY_ICON;
tray.tooltip = PROJECT_NAME; tray.tooltip = PROJECT_NAME;
tray_update(&tray); tray_update(&tray);
} }
+4 -1
View File
@@ -86,5 +86,8 @@ namespace system_tray {
update_tray_require_pin(); update_tray_require_pin();
void void
update_tray_otp_pair(std::string device_name); update_tray_paired(std::string device_name);
void
update_tray_client_connected(std::string client_name);
} // namespace system_tray } // namespace system_tray
+11
View File
@@ -405,6 +405,17 @@
}, 5000); }, 5000);
fetch("/api/restart", { fetch("/api/restart", {
method: "POST" method: "POST"
})
.then((resp) => {
if (resp.status !== 200) {
location.href = './login'
return
}
})
.catch((e) => {
location.href = './login'
console.error(e)
return
}); });
} }
}); });
+299 -2
View File
@@ -57,6 +57,71 @@
<div class="alert alert-warning"> <div class="alert alert-warning">
<b>{{ $t('_common.warning') }}</b> {{ $t('pin.warning_msg') }} <b>{{ $t('_common.warning') }}</b> {{ $t('pin.warning_msg') }}
</div> </div>
<!-- Unpair Clients -->
<div class="card my-4 align-self-stretch">
<div class="card-body">
<div class="p-2">
<div class="d-flex justify-content-end align-items-center">
<h2 id="unpair" class="me-auto">{{ $t('pin.device_management') }}</h2>
<button class="btn btn-danger" :disabled="unpairAllPressed" @click="unpairAll">
{{ $t('pin.unpair_all') }}
</button>
</div>
<br />
<p class="mb-0">{{ $t('pin.device_management_desc') }}</p>
<div id="apply-alert" class="alert alert-success d-flex align-items-center mt-3" :style="{ 'display': (showApplyMessage ? 'flex !important': 'none !important') }">
<div class="me-2"><b>{{ $t('_common.success') }}</b> {{ $t('pin.unpair_single_success') }}</div>
<button class="btn btn-success ms-auto apply" @click="clickedApplyBanner">{{ $t('_common.dismiss') }}</button>
</div>
<div class="alert alert-success mt-3" v-if="unpairAllStatus === true">
{{ $t('pin.unpair_all_success') }}
</div>
<div class="alert alert-danger mt-3" v-if="unpairAllStatus === false">
{{ $t('pin.unpair_all_error') }}
</div>
</div>
</div>
<ul id="client-list" class="list-group list-group-flush list-group-item-light" v-if="clients && clients.length > 0">
<template v-for="client in clients" class="list-group-item d-flex align-items-center">
<div v-if="client.editing" class="list-group-item d-flex align-items-stretch flex-column">
<div class="d-flex align-items-center">
<div class="p-2 flex-grow-1 d-flex align-items-center">
<span class="badge" :class="client.editPerm >= 0x04000000 ? 'bg-danger' : 'bg-primary'">
[ {{permToStr(client.editPerm)}} ]
</span>
&nbsp;
<input v-model="client.editName" @keyup.enter="saveClient(client)" class="form-control flex-grow-1" type="text" :placeholder="$t('pin.device_name')">
</div>
<div class="me-2 btn btn-success" @click="saveClient(client)"><i class="fas fa-check"></i></div>
<div class="me-2 btn btn-secondary" @click="cancelEdit(client)"><i class="fas fa-times"></i></div>
</div>
<div class="align-items-top d-flex flex-row justify-content-center">
<div v-for="group in permissionGroups" class="d-flex flex-column mx-2">
<div class="mx-2">{{ group.name }}:</div>
<button v-for="perm in group.permissions" class="my-1 btn btn-sm" :disabled="isSupressed(client.editPerm, perm.name, perm.supressed_by)" :class="(isSupressed(client.editPerm, perm.name, perm.supressed_by) || checkPermission(client.editPerm, perm.name)) ? 'btn-success' : 'btn-outline-secondary'" @click="togglePermission(client, perm.name)">
{{ $t(`permissions.${perm.name}`) }}
</button>
</div>
</div>
</div>
<div v-else class="list-group-item d-flex align-items-center">
<div class="p-2 flex-grow-1 d-flex align-items-center">
<span class="badge" :class="client.perm >= 0x04000000 ? 'bg-danger' : 'bg-primary'">
[ {{permToStr(client.perm)}} ]
</span>
&nbsp;
<span class="me-2">{{client.name != "" ? client.name : $t('pin.unpair_single_unknown')}}</span>
</div>
<div v-if="client.connected" class="me-2 btn btn-warning" @click="disconnectClient(client.uuid)"><i class="fas fa-link-slash"></i></div>
<div class="me-2 btn btn-primary" @click="editClient(client)"><i class="fas fa-edit"></i></div>
<div class="me-2 btn btn-danger" @click="unpairSingle(client.uuid)"><i class="fas fa-trash"></i></div>
</div>
</template>
</ul>
<ul v-else class="list-group list-group-flush list-group-item-light">
<div class="list-group-item p-3 text-center"><em>{{ $t('pin.unpair_single_no_devices') }}</em></div>
</ul>
</div>
</div> </div>
</body> </body>
@@ -91,6 +156,106 @@
} }
} }
/**
* Permissions:
enum class PERM: uint32_t {
_reserved = 1,
_input = _reserved << 8, // Input permission group
input_controller = _input << 0, // Allow controller input
input_touch = _input << 1, // Allow touch input
input_pen = _input << 2, // Allow pen input
input_kbdm = _input << 3, // Allow kbd/mouse input
_all_inputs = input_controller | input_touch | input_pen | input_kbdm,
_operation = _input << 8, // Operation permission group
clipboard_set = _operation << 0, // Allow set clipboard from client
clipboard_read = _operation << 1, // Allow read clipboard from host
file_upload = _operation << 2, // Allow upload files to host
file_dwnload = _operation << 3, // Allow download files from host
server_cmd = _operation << 4, // Allow execute server cmd
_all_opeiations = clipboard_set | clipboard_read | file_upload | file_dwnload | server_cmd,
_action = _operation << 8, // Action permission group
list = _action << 0, // Allow list apps
view = _action << 1, // Allow view streams
launch = _action << 2, // Allow launch apps
_allow_view = view | launch, // Launch contains view permission
_all_actions = list | view | launch,
_default = view | list, // Default permissions for new clients
_no = 0, // No permissions are granted
_all = _all_inputs | _all_opeiations | _all_actions, // All current permissions
};
*/
const permissionMapping = {
// Input permission group
input_controller: 0x00000100,
input_touch: 0x00000200,
input_pen: 0x00000400,
input_kbdm: 0x00000800,
_all_inputs: 0x00000F00,
// Operation permission group
clipboard_set: 0x00010000,
clipboard_read: 0x00020000,
file_upload: 0x00040000,
file_dwnload: 0x00080000,
server_cmd: 0x00100000,
_all_operations: 0x001F0000,
// Action permission group
list: 0x01000000,
view: 0x02000000,
launch: 0x04000000,
_allow_view: 0x06000000,
_all_actions: 0x07000000,
// Special permissions
_default: 0x03000000,
_no: 0x00000000,
_all: 0x071F0F00
};
const permissionGroups = [
{ name: 'Action', permissions: [
{
name: 'list',
supressed_by: ['view', 'launch']
}, {
name: 'view',
supressed_by: ['launch']
}, {
name: 'launch',
supressed_by: []
}
] },
{ name: 'Operation', permissions: [
{
name: 'server_cmd',
supressed_by: []
}
] },
{ name: 'Input', permissions: [
{
name: 'input_controller',
supressed_by: []
}, {
name: 'input_touch',
supressed_by: []
}, {
name: 'input_pen',
supressed_by: []
}, {
name: 'input_kbdm',
supressed_by: []
}
] },
];
let currentEditingClient = null;
const data = () => { const data = () => {
return { return {
editingHost: false, editingHost: false,
@@ -102,7 +267,12 @@
deviceName: '', deviceName: '',
hostAddr: '', hostAddr: '',
hostPort: '', hostPort: '',
hostName: '' hostName: '',
permissionGroups,
clients: [],
showApplyMessage: false,
unpairAllPressed: false,
unpairAllStatus: null
} }
} }
@@ -120,10 +290,14 @@
return !!(this.hostAddr && this.hostPort); return !!(this.hostAddr && this.hostPort);
} }
}, },
created() {
this.refreshClients();
},
methods: { methods: {
switchTab(currentTab) { switchTab(currentTab) {
location.hash = currentTab; location.hash = currentTab;
Object.assign(this, data()); const clients = this.clients;
Object.assign(this, data(), { clients });
hostInfoCache = null; hostInfoCache = null;
clearTimeout(resetOTPTimeout); clearTimeout(resetOTPTimeout);
}, },
@@ -155,6 +329,8 @@
).innerHTML = `<div class="alert alert-success" role="alert">${this.i18n.t('pin.pair_success')}</div>`; ).innerHTML = `<div class="alert alert-success" role="alert">${this.i18n.t('pin.pair_success')}</div>`;
document.querySelector("#pin-input").value = ""; document.querySelector("#pin-input").value = "";
document.querySelector("#name-input").value = ""; document.querySelector("#name-input").value = "";
setTimeout(() => this.refreshClients(), 1000);
} else { } else {
document.querySelector( document.querySelector(
"#status" "#status"
@@ -217,7 +393,128 @@
} }
} }
}) })
},
clickedApplyBanner() {
this.showApplyMessage = false;
},
editClient(client) {
if (currentEditingClient) {
this.cancelEdit(currentEditingClient);
} }
currentEditingClient = client;
client.editing = true;
client.editPerm = client.perm;
client.editName = client.name;
},
cancelEdit(client) {
client.editing = false;
client.editPerm = client.perm;
client.editName = client.name;
currentEditingClient = null;
},
saveClient(client) {
client.editing = false;
currentEditingClient = null;
const editedClient = {
uuid: client.uuid,
name: client.editName,
perm: client.editPerm & permissionMapping._all
}
fetch("/api/clients/update", {
credentials: 'include',
method: "POST",
body: JSON.stringify(editedClient)
}).finally(() => {
setTimeout(() => {
this.refreshClients();
}, 1000);
});
},
permToStr(perm) {
const permSegments = [];
permSegments.push((perm >> 24) & 0xFF);
permSegments.push((perm >> 16) & 0xFF);
permSegments.push((perm >> 8) & 0xFF);
return permSegments.map(seg => seg.toString(16).toUpperCase().padStart(2, '0')).join(' ');
},
checkPermission(perm, permission) {
return (perm & permissionMapping[permission]) !== 0;
},
isSupressed(perm, permission, supressed_by) {
for (const supressed of supressed_by) {
if (this.checkPermission(perm, supressed)) {
return true;
}
}
return false;
},
togglePermission(client, permission) {
client.editPerm ^= permissionMapping[permission];
},
disconnectClient(uuid) {
fetch("/api/clients/disconnect", {
credentials: 'include',
method: "POST",
body: JSON.stringify({ uuid })
}).finally(() => {
setTimeout(() => {
this.refreshClients();
}, 1000);
});
},
unpairAll() {
this.unpairAllPressed = true;
fetch("/api/clients/unpair-all", {
credentials: 'include',
method: "POST"
})
.then((r) => r.json())
.then((r) => {
this.unpairAllPressed = false;
this.unpairAllStatus = r.status.toString() === "true";
setTimeout(() => {
this.unpairAllStatus = null;
}, 5000);
this.refreshClients();
});
},
unpairSingle(uuid) {
fetch("/api/clients/unpair", {
credentials: 'include',
method: "POST",
body: JSON.stringify({ uuid })
}).then(() => {
this.showApplyMessage = true;
this.refreshClients();
});
},
refreshClients() {
if (currentEditingClient) {
this.cancelEdit(currentEditingClient);
}
fetch("/api/clients/list", { credentials: 'include' })
.then((response) => response.json())
.then((response) => {
const clientList = document.querySelector("#client-list");
if (response.status === 'true' && response.named_certs && response.named_certs.length) {
this.clients = response.named_certs.map(({name, uuid, perm, connected}) => {
const permInt = parseInt(perm, 10);
return {
name,
uuid,
perm: permInt,
connected: connected === 'true',
editing: false,
editPerm: permInt,
editName: name
}
})
currentEditingClient = null;
} else {
this.clients = [];
}
});
},
} }
}); });
@@ -93,7 +93,7 @@
"client_card": { "client_card": {
"clients": "Clients", "clients": "Clients",
"clients_desc": "Clients that are specifically tuned to work the best with Apollo", "clients_desc": "Clients that are specifically tuned to work the best with Apollo",
"generic_moonlight_clients_desc": "Gereric Moonlight clients are still usable with Apollo." "generic_moonlight_clients_desc": "Generic Moonlight clients are still usable with Apollo."
}, },
"config": { "config": {
"adapter_name": "Adapter Name", "adapter_name": "Adapter Name",
@@ -388,6 +388,20 @@
"password_change": "Password Change", "password_change": "Password Change",
"success_msg": "Password has been changed successfully! This page will reload soon, your browser will ask you for the new credentials." "success_msg": "Password has been changed successfully! This page will reload soon, your browser will ask you for the new credentials."
}, },
"permissions": {
"input_controller": "Controller Input",
"input_touch": "Touch Input",
"input_pen": "Pen Input",
"input_kbdm": "Keyboard & Mouse Input",
"clipboard_set": "Clipboard Set",
"clipboard_read": "Clipboard Read",
"file_upload": "File Upload",
"file_dwnload": "File Download",
"server_cmd": "Server Command",
"list": "List Apps",
"view": "View Streams",
"launch": "Launch Apps"
},
"pin": { "pin": {
"device_name": "Optional: Device Name", "device_name": "Optional: Device Name",
"pair_failure": "Pairing Failed: Check if the PIN is typed correctly", "pair_failure": "Pairing Failed: Check if the PIN is typed correctly",
@@ -402,7 +416,15 @@
"otp_expired_msg": "OTP expired. Please request a new one.", "otp_expired_msg": "OTP expired. Please request a new one.",
"otp_success": "PIN request success, the PIN is available within 3 minutes.", "otp_success": "PIN request success, the PIN is available within 3 minutes.",
"otp_msg": "OTP pairing is only available for the latest Artemis clients. Please use legacy pairing method for other clients.", "otp_msg": "OTP pairing is only available for the latest Artemis clients. Please use legacy pairing method for other clients.",
"otp_pair_now": "PIN generated successfully, do you want to pair now?" "otp_pair_now": "PIN generated successfully, do you want to pair now?",
"device_management": "Device Management",
"device_management_desc": "Manage your paired devices.",
"unpair_all": "Unpair All",
"unpair_all_success": "All devices unpaired.",
"unpair_all_error": "Error while unpairing",
"unpair_single_no_devices": "There are no paired devices.",
"unpair_single_success": "However, the device(s) may still be in an active session. Use the 'Force Close' button above to end any open sessions.",
"unpair_single_unknown": "Unknown Client"
}, },
"resource_card": { "resource_card": {
"github_discussions": "GitHub Discussions", "github_discussions": "GitHub Discussions",
@@ -430,15 +452,7 @@
"quit_apollo_success": "Apollo has exited.", "quit_apollo_success": "Apollo has exited.",
"quit_apollo_success_ongoing": "Apollo is quitting...", "quit_apollo_success_ongoing": "Apollo is quitting...",
"quit_apollo_confirm": "Do you really want to quit Apollo? You'll not be able to start Apollo again if you have no other methods to operate your computer.", "quit_apollo_confirm": "Do you really want to quit Apollo? You'll not be able to start Apollo again if you have no other methods to operate your computer.",
"troubleshooting": "Troubleshooting", "troubleshooting": "Troubleshooting"
"unpair_all": "Unpair All",
"unpair_all_error": "Error while unpairing",
"unpair_all_success": "All devices unpaired.",
"unpair_desc": "Remove your paired devices. Individually unpaired devices with an active session will remain connected, but cannot start or resume a session.",
"unpair_single_no_devices": "There are no paired devices.",
"unpair_single_success": "However, the device(s) may still be in an active session. Use the 'Force Close' button above to end any open sessions.",
"unpair_single_unknown": "Unknown Client",
"unpair_title": "Unpair Devices"
}, },
"welcome": { "welcome": {
"confirm_password": "Confirm password", "confirm_password": "Confirm password",
@@ -388,6 +388,20 @@
"password_change": "更改密码", "password_change": "更改密码",
"success_msg": "密码已成功更改!此页面即将重新加载,您的浏览器将要求您输入新的账户信息。" "success_msg": "密码已成功更改!此页面即将重新加载,您的浏览器将要求您输入新的账户信息。"
}, },
"permissions": {
"input_controller": "手柄输入",
"input_touch": "触摸输入",
"input_pen": "笔输入",
"input_kbdm": "键鼠输入",
"clipboard_set": "上传剪贴板",
"clipboard_read": "获取剪贴板",
"file_upload": "上传文件",
"file_dwnload": "下载文件",
"server_cmd": "服务端命令",
"list": "列出APP",
"view": "查看串流",
"launch": "启动APP"
},
"pin": { "pin": {
"device_name": "设备名称", "device_name": "设备名称",
"pair_failure": "配对失败:请检查 PIN 码是否正确输入", "pair_failure": "配对失败:请检查 PIN 码是否正确输入",
@@ -402,7 +416,15 @@
"otp_expired_msg": "口令已过期,请重新请求。", "otp_expired_msg": "口令已过期,请重新请求。",
"otp_success": "一次性 PIN 请求成功,3分钟内有效。", "otp_success": "一次性 PIN 请求成功,3分钟内有效。",
"otp_msg": "一次性口令目前仅支持最新的 Artemis 客户端使用。其他客户端请使用传统配对方式。", "otp_msg": "一次性口令目前仅支持最新的 Artemis 客户端使用。其他客户端请使用传统配对方式。",
"otp_pair_now": "PIN 请求成功,是否一键配对?" "otp_pair_now": "PIN 请求成功,是否一键配对?",
"device_management": "设备管理",
"device_management_desc": "管理已配对的设备。",
"unpair_all": "全部取消配对",
"unpair_all_success": "全部取消配对成功!",
"unpair_all_error": "取消配对时出错",
"unpair_single_no_devices": "没有配对的设备。",
"unpair_single_success": "然而,设备可能仍然处于活动会话中,使用上面的“强制关闭”按钮结束任何打开的会话。",
"unpair_single_unknown": "未知客户端"
}, },
"resource_card": { "resource_card": {
"github_discussions": "Github 讨论区", "github_discussions": "Github 讨论区",
@@ -430,15 +452,7 @@
"quit_apollo_success": "Apollo 已成功退出。", "quit_apollo_success": "Apollo 已成功退出。",
"quit_apollo_success_ongoing": "Apollo 正在退出...", "quit_apollo_success_ongoing": "Apollo 正在退出...",
"quit_apollo_confirm": "确定要退出 Apollo 吗?如果没有其他操作方式,你将无法再次启动 Apollo。", "quit_apollo_confirm": "确定要退出 Apollo 吗?如果没有其他操作方式,你将无法再次启动 Apollo。",
"troubleshooting": "故障排除", "troubleshooting": "故障排除"
"unpair_all": "全部取消配对",
"unpair_all_error": "取消配对时出错",
"unpair_all_success": "取消配对成功!",
"unpair_desc": "删除您已配对的设备。未配对的单独设备与活动会话将保持连接,但不能启动或继续会话。",
"unpair_single_no_devices": "没有配对的设备。",
"unpair_single_success": "然而,设备可能仍然处于活动会话中,使用上面的“强制关闭”按钮结束任何打开的会话。",
"unpair_single_unknown": "未知客户端",
"unpair_title": "取消配对设备"
}, },
"welcome": { "welcome": {
"confirm_password": "确认密码", "confirm_password": "确认密码",
@@ -94,40 +94,6 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Unpair Clients -->
<div class="card my-4">
<div class="card-body">
<div class="p-2">
<div class="d-flex justify-content-end align-items-center">
<h2 id="unpair" class="text-center me-auto">{{ $t('troubleshooting.unpair_title') }}</h2>
<button class="btn btn-danger" :disabled="unpairAllPressed" @click="unpairAll">
{{ $t('troubleshooting.unpair_all') }}
</button>
</div>
<br />
<p class="mb-0">{{ $t('troubleshooting.unpair_desc') }}</p>
<div id="apply-alert" class="alert alert-success d-flex align-items-center mt-3" :style="{ 'display': (showApplyMessage ? 'flex !important': 'none !important') }">
<div class="me-2"><b>{{ $t('_common.success') }}</b> {{ $t('troubleshooting.unpair_single_success') }}</div>
<button class="btn btn-success ms-auto apply" @click="clickedApplyBanner">{{ $t('_common.dismiss') }}</button>
</div>
<div class="alert alert-success mt-3" v-if="unpairAllStatus === true">
{{ $t('troubleshooting.unpair_all_success') }}
</div>
<div class="alert alert-danger mt-3" v-if="unpairAllStatus === false">
{{ $t('troubleshooting.unpair_all_error') }}
</div>
</div>
</div>
<ul id="client-list" class="list-group list-group-flush list-group-item-light" v-if="clients && clients.length > 0">
<div v-for="client in clients" class="list-group-item d-flex">
<div class="p-2 flex-grow-1">{{client.name != "" ? client.name : $t('troubleshooting.unpair_single_unknown')}}</div><div class="me-2 ms-auto btn btn-danger" @click="unpairSingle(client.uuid)"><i class="fas fa-trash"></i></div>
</div>
</ul>
<ul v-else class="list-group list-group-flush list-group-item-light">
<div class="list-group-item p-3 text-center"><em>{{ $t('troubleshooting.unpair_single_no_devices') }}</em></div>
</ul>
</div>
<!-- Logs --> <!-- Logs -->
<div class="card p-2 my-4"> <div class="card p-2 my-4">
<div class="card-body"> <div class="card-body">
@@ -166,10 +132,7 @@
logInterval: null, logInterval: null,
serverRestarting: false, serverRestarting: false,
serverQuitting: false, serverQuitting: false,
serverQuit: false, serverQuit: false
showApplyMessage: false,
unpairAllPressed: false,
unpairAllStatus: null,
}; };
}, },
computed: { computed: {
@@ -213,48 +176,6 @@
}, 5000); }, 5000);
}); });
}, },
unpairAll() {
this.unpairAllPressed = true;
fetch("/api/clients/unpair-all", {
credentials: 'include',
method: "POST"
})
.then((r) => r.json())
.then((r) => {
this.unpairAllPressed = false;
this.unpairAllStatus = r.status.toString() === "true";
setTimeout(() => {
this.unpairAllStatus = null;
}, 5000);
this.refreshClients();
});
},
unpairSingle(uuid) {
fetch("/api/clients/unpair", { credentials: 'include',
method: "POST",
body: JSON.stringify({ uuid })
}).then(() => {
this.showApplyMessage = true;
this.refreshClients();
});
},
refreshClients() {
fetch("/api/clients/list", { credentials: 'include' })
.then((response) => response.json())
.then((response) => {
const clientList = document.querySelector("#client-list");
if (response.status === 'true' && response.named_certs && response.named_certs.length) {
this.clients = response.named_certs.sort((a, b) => {
return (a.name.toLowerCase() > b.name.toLowerCase() || a.name == "" ? 1 : -1)
});
} else {
this.clients = [];
}
});
},
clickedApplyBanner() {
this.showApplyMessage = false;
},
copyLogs() { copyLogs() {
navigator.clipboard.writeText(this.actualLogs); navigator.clipboard.writeText(this.actualLogs);
}, },