diff --git a/CMakeLists.txt b/CMakeLists.txt index 1347b7d7..33e09423 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -167,6 +167,8 @@ set(SUNSHINE_TARGET_FILES sunshine/crypto.h sunshine/nvhttp.cpp sunshine/nvhttp.h + sunshine/confighttp.cpp + sunshine/confighttp.h sunshine/rtsp.cpp sunshine/rtsp.h sunshine/stream.cpp diff --git a/assets/web/apps.html b/assets/web/apps.html new file mode 100644 index 00000000..d63f8260 --- /dev/null +++ b/assets/web/apps.html @@ -0,0 +1,124 @@ +
+

Applications

+
Applications are refreshed only when Client is restarted
+ + + + + + + + + + + + + +
NameActions
{{app.name}} +
+
+
+ + +
Application Name, as shown on Moonlight
+
+
+ + +
The file where the output of the command is stored, if it is not specified, the output is ignored
+
+
+ +
A list of commands to be run before/after the application.
If any of the prep-commands fail, starting the application is aborted
+ + + + + + + + + + + + + +
DoUndo
+ +
+
+ + +
The main application, if it is not specified, a processs is started that sleeps indefinitely
+
+
+ + +
+
+ +
+ + + + \ No newline at end of file diff --git a/assets/web/clients.html b/assets/web/clients.html new file mode 100644 index 00000000..c2d03025 --- /dev/null +++ b/assets/web/clients.html @@ -0,0 +1,3 @@ +
+

Clients

+
\ No newline at end of file diff --git a/assets/web/config.html b/assets/web/config.html new file mode 100644 index 00000000..c63ec39d --- /dev/null +++ b/assets/web/config.html @@ -0,0 +1,4 @@ +
+

Hello, Sunshine!

+

Placeholer for config page

+
\ No newline at end of file diff --git a/assets/web/header.html b/assets/web/header.html new file mode 100644 index 00000000..59c11c2f --- /dev/null +++ b/assets/web/header.html @@ -0,0 +1,45 @@ + + + + + + + + Sunshine + + + + + + + \ No newline at end of file diff --git a/assets/web/index.html b/assets/web/index.html new file mode 100644 index 00000000..376f8613 --- /dev/null +++ b/assets/web/index.html @@ -0,0 +1,4 @@ +
+

Hello, Sunshine!

+

Sunshine is a Gamestream host for Moonlight

+
\ No newline at end of file diff --git a/assets/web/pin.html b/assets/web/pin.html new file mode 100644 index 00000000..811a1796 --- /dev/null +++ b/assets/web/pin.html @@ -0,0 +1,22 @@ +
+
+ + +
+
+
+ + \ No newline at end of file diff --git a/sunshine/confighttp.cpp b/sunshine/confighttp.cpp new file mode 100644 index 00000000..aa923f11 --- /dev/null +++ b/sunshine/confighttp.cpp @@ -0,0 +1,262 @@ +// +// Created by TheElixZammuto on 2021-05-09. +// TODO: Authentication, better handling of routes common to nvhttp, cleanup + +#include "process.h" + +#include + +#include +#include +#include + +#include + +#include +#include + +#include "config.h" +#include "utility.h" +#include "rtsp.h" +#include "crypto.h" +#include "confighttp.h" +#include "platform/common.h" +#include "network.h" +#include "nvhttp.h" +#include "uuid.h" +#include "main.h" + +std::string read_file(std::string path); + +namespace confighttp +{ + using namespace std::literals; + constexpr auto PORT_HTTP = 47990; + + namespace fs = std::filesystem; + namespace pt = boost::property_tree; + + using https_server_t = SimpleWeb::Server; + + using args_t = SimpleWeb::CaseInsensitiveMultimap; + using resp_https_t = std::shared_ptr::Response>; + using req_https_t = std::shared_ptr::Request>; + + enum class op_e + { + ADD, + REMOVE + }; + + template + void not_found(std::shared_ptr::Response> response, std::shared_ptr::Request> request) + { + pt::ptree tree; + tree.put("root..status_code", 404); + + std::ostringstream data; + + pt::write_xml(data, tree); + response->write(data.str()); + + *response << "HTTP/1.1 404 NOT FOUND\r\n" + << data.str(); + } + + void getIndexPage(resp_https_t response, req_https_t request) + { + std::string header = read_file(WEB_DIR "header.html"); + std::string content = read_file(WEB_DIR "index.html"); + response->write(header + content); + } + + template + void getPinPage(std::shared_ptr::Response> response, std::shared_ptr::Request> request) + { + std::string header = read_file(WEB_DIR "header.html"); + std::string content = read_file(WEB_DIR "pin.html"); + response->write(header + content); + } + + template + void getAppsPage(std::shared_ptr::Response> response, std::shared_ptr::Request> request) + { + std::string header = read_file(WEB_DIR "header.html"); + std::string content = read_file(WEB_DIR "apps.html"); + response->write(header + content); + } + + template + void getClientsPage(std::shared_ptr::Response> response, std::shared_ptr::Request> request) + { + std::string header = read_file(WEB_DIR "header.html"); + std::string content = read_file(WEB_DIR "clients.html"); + response->write(header + content); + } + + template + void getConfigPage(std::shared_ptr::Response> response, std::shared_ptr::Request> request) + { + std::string header = read_file(WEB_DIR "header.html"); + std::string content = read_file(WEB_DIR "config.html"); + response->write(header + content); + } + + void getApps(resp_https_t response, req_https_t request) + { + std::string content = read_file(SUNSHINE_ASSETS_DIR "/" APPS_JSON); + response->write(content); + } + + void saveApp(resp_https_t response, req_https_t request) + { + std::stringstream ss; + ss << request->content.rdbuf(); + pt::ptree outputTree; + auto g = util::fail_guard([&]() { + std::ostringstream data; + + pt::write_json(data, outputTree); + response->write(data.str()); + }); + pt::ptree inputTree,fileTree; + try { + //TODO: Input Validation + pt::read_json(ss, inputTree); + pt::read_json(SUNSHINE_ASSETS_DIR "/" APPS_JSON, fileTree); + auto &apps_node = fileTree.get_child("apps"s); + int index = inputTree.get("index"); + BOOST_LOG(info) << inputTree.get_child("prep-cmd").empty(); + if(inputTree.get_child("prep-cmd").empty())inputTree.erase("prep-cmd"); + inputTree.erase("index"); + if(index == -1){ + apps_node.push_back(std::make_pair("",inputTree)); + } else { + //Unfortuantely Boost PT does not allow to directly edit the array, copt should do the trick + pt::ptree newApps; + int i = 0; + for (const auto& kv : apps_node) { + if(i == index){ + newApps.push_back(std::make_pair("",inputTree)); + } else { + newApps.push_back(std::make_pair("",kv.second)); + } + i++; + } + fileTree.erase("apps"); + fileTree.push_back(std::make_pair("apps",newApps)); + } + pt::write_json(SUNSHINE_ASSETS_DIR "/" APPS_JSON, fileTree); + outputTree.put("status","true"); + proc::refresh(SUNSHINE_ASSETS_DIR "/" APPS_JSON); + } catch (std::exception &e) { + BOOST_LOG(warning) << e.what(); + outputTree.put("status","false"); + outputTree.put("error","Invalid Input JSON"); + return; + } + } + + void deleteApp(resp_https_t response, req_https_t request) + { + pt::ptree outputTree; + auto g = util::fail_guard([&]() { + std::ostringstream data; + + pt::write_json(data, outputTree); + response->write(data.str()); + }); + pt::ptree fileTree; + try { + pt::read_json(SUNSHINE_ASSETS_DIR "/" APPS_JSON, fileTree); + auto &apps_node = fileTree.get_child("apps"s); + int index = stoi(request->path_match[1]); + BOOST_LOG(info) << index; + if(index <= 0){ + outputTree.put("status","false"); + outputTree.put("error","Invalid Index"); + return; + } else { + //Unfortuantely Boost PT does not allow to directly edit the array, copt should do the trick + pt::ptree newApps; + int i = 0; + for (const auto& kv : apps_node) { + if(i != index){ + newApps.push_back(std::make_pair("",kv.second)); + } + i++; + } + fileTree.erase("apps"); + fileTree.push_back(std::make_pair("apps",newApps)); + } + pt::write_json(SUNSHINE_ASSETS_DIR "/" APPS_JSON, fileTree); + outputTree.put("status","true"); + proc::refresh(SUNSHINE_ASSETS_DIR "/" APPS_JSON); + } catch (std::exception &e) { + BOOST_LOG(warning) << e.what(); + outputTree.put("status","false"); + outputTree.put("error","Invalid File JSON"); + return; + } + } + + void start(std::shared_ptr shutdown_event) + { + auto ctx = std::make_shared(boost::asio::ssl::context::tls); + ctx->use_certificate_chain_file(config::nvhttp.cert); + ctx->use_private_key_file(config::nvhttp.pkey, boost::asio::ssl::context::pem); + https_server_t http_server { ctx, 0 }; + http_server.default_resource = not_found; + http_server.resource["^/$"]["GET"] = getIndexPage; + http_server.resource["^/pin$"]["GET"] = getPinPage; + http_server.resource["^/apps$"]["GET"] = getAppsPage; + http_server.resource["^/api/apps$"]["GET"] = getApps; + http_server.resource["^/api/apps$"]["POST"] = saveApp; + http_server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp; + http_server.resource["^/clients$"]["GET"] = getClientsPage; + http_server.resource["^/config$"]["GET"] = getConfigPage; + http_server.resource["^/pin/([0-9]+)$"]["GET"] = nvhttp::pin; + http_server.config.reuse_address = true; + http_server.config.address = "0.0.0.0"s; + http_server.config.port = PORT_HTTP; + + try + { + http_server.bind(); + BOOST_LOG(info) << "Configuration UI available at [https://localhost:"sv << PORT_HTTP << "]"; + } + catch (boost::system::system_error &err) + { + BOOST_LOG(fatal) << "Couldn't bind http server to ports ["sv << PORT_HTTP << "]: "sv << err.what(); + + shutdown_event->raise(true); + return; + } + + std::thread tcp{&https_server_t::accept_and_run, &http_server}; + + // Wait for any event + shutdown_event->view(); + + http_server.stop(); + + tcp.join(); + } +} + +std::string read_file(std::string path) +{ + std::ifstream in(path); + + std::string input; + std::string base64_cert; + + //FIXME: Being unable to read file could result in infinite loop + while (!in.eof()) + { + std::getline(in, input); + base64_cert += input + '\n'; + } + + return base64_cert; +} \ No newline at end of file diff --git a/sunshine/confighttp.h b/sunshine/confighttp.h new file mode 100644 index 00000000..d3b9e85e --- /dev/null +++ b/sunshine/confighttp.h @@ -0,0 +1,20 @@ +// +// Created by loki on 6/3/19. +// + +#ifndef SUNSHINE_CONFIGHTTP_H +#define SUNSHINE_CONFIGHTTP_H + +#include +#include + +#include "thread_safe.h" + +#define WEB_DIR SUNSHINE_ASSETS_DIR "/web/" + + +namespace confighttp { + void start(std::shared_ptr shutdown_event); +} + +#endif //SUNSHINE_CONFIGHTTP_H diff --git a/sunshine/main.cpp b/sunshine/main.cpp index c21f81fc..2938e69f 100644 --- a/sunshine/main.cpp +++ b/sunshine/main.cpp @@ -17,6 +17,7 @@ #include "video.h" #include "input.h" #include "nvhttp.h" +#include "confighttp.h" #include "rtsp.h" #include "config.h" #include "thread_pool.h" @@ -124,19 +125,8 @@ int main(int argc, char *argv[]) { shutdown_event->raise(true); }); - auto proc_opt = proc::parse(config::stream.file_apps); - if(!proc_opt) { - return 7; - } - - { - proc::ctx_t ctx; - ctx.name = "Desktop"s; - proc_opt->get_apps().emplace(std::begin(proc_opt->get_apps()), std::move(ctx)); - } - - proc::proc = std::move(*proc_opt); - + proc::refresh(config::stream.file_apps); + auto deinit_guard = platf::init(); input::init(); reed_solomon_init(); @@ -147,6 +137,7 @@ int main(int argc, char *argv[]) { task_pool.start(1); std::thread httpThread { nvhttp::start, shutdown_event }; + std::thread configThread { confighttp::start, shutdown_event }; stream::rtpThread(shutdown_event); httpThread.join(); diff --git a/sunshine/nvhttp.h b/sunshine/nvhttp.h index eb90540a..9958f193 100644 --- a/sunshine/nvhttp.h +++ b/sunshine/nvhttp.h @@ -7,7 +7,8 @@ #include #include - +#include +#include #include "thread_safe.h" #define CA_DIR SUNSHINE_ASSETS_DIR "/demoCA" @@ -16,6 +17,7 @@ namespace nvhttp { void start(std::shared_ptr shutdown_event); +template void pin(std::shared_ptr::Response> response, std::shared_ptr::Request> request); } #endif //SUNSHINE_NVHTTP_H diff --git a/sunshine/process.cpp b/sunshine/process.cpp index ec2aa2fe..c61b2481 100644 --- a/sunshine/process.cpp +++ b/sunshine/process.cpp @@ -302,6 +302,11 @@ void refresh(const std::string &file_name) { auto proc_opt = proc::parse(file_name); if(proc_opt) { + { + proc::ctx_t ctx; + ctx.name = "Desktop"s; + proc_opt->get_apps().emplace(std::begin(proc_opt->get_apps()), std::move(ctx)); + } proc = std::move(*proc_opt); } }