diff --git a/CMakeLists.txt b/CMakeLists.txt index ffa12301..72b08245 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,7 @@ if(WIN32) set(SUNSHINE_PREPARED_BINARIES "${CMAKE_CURRENT_BINARY_DIR}/pre-compiled/windows") endif() + add_compile_definitions(SUNSHINE_PLATFORM="windows") add_subdirectory(tools) #This is temporary, only tools for Windows are needed, for now list(APPEND SUNSHINE_DEFINITIONS APPS_JSON="apps_windows.json") @@ -91,6 +92,7 @@ if(WIN32) set_source_files_properties(ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_DEFINITIONS "UNICODE=1;ERROR_INVALID_DEVICE_OBJECT_PARAMETER=650") set_source_files_properties(ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_FLAGS "-Wno-unknown-pragmas -Wno-misleading-indentation -Wno-class-memaccess") else() + add_compile_definitions(SUNSHINE_PLATFORM="linux") list(APPEND SUNSHINE_DEFINITIONS APPS_JSON="apps_linux.json") find_package(X11 REQUIRED) @@ -144,6 +146,10 @@ set(SUNSHINE_TARGET_FILES sunshine/crypto.h sunshine/nvhttp.cpp sunshine/nvhttp.h + sunshine/httpcommon.cpp + sunshine/httpcommon.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..d1784608 --- /dev/null +++ b/assets/web/apps.html @@ -0,0 +1,160 @@ +
+
+

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
+ +
+ +
+ +
+
{{c}}
+ +
+
+ + +
+
A list of commands to be run and forgotten about
+
+ +
+ + +
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..356e4d45 --- /dev/null +++ b/assets/web/config.html @@ -0,0 +1,492 @@ +
+

Configuration

+
+ + + +
+ +
+ + +
The name displayed by Moonlight. If not specified, the PC's hostname is used +
+
+ +
+ + +
The minimum log level printed to standard out
+
+ +
+ + +
The origin of the remote endpoint address that is not denied for HTTP method /pin +
+
+ +
+ + +
If no external IP address is given, the local IP address is used
+
+ +
+ + +
How long to wait in milliseconds for data from moonlight before shutting down the + stream
+
+ +
+ +
+ +
+
+ {{r}} + × +
+
+ + +
+
+
+
+ +
+
+ {{f}} + × +
+
+ + +
+
+
+
+ The display modes advertised by Sunshine
+ Some versions of Moonlight, such as Moonlight-nx (Switch), + rely on this list to ensure that the requested resolutions and fps + are supported. +
+
+
+ +
+ +
+ + +
The private key must be 2048 bits
+
+ +
+ + +
The certificate must be signed with a 2048 bit key
+
+ + +
+ + +
The file where current state of Sunshine is stored
+
+ +
+ + +
The file where current apps of Sunshine are stored
+
+
+
+ +
+ + +
+ The back/select button on the controller.
+ On the Shield, the home and powerbutton are not passed to Moonlight.
+ If, after the timeout, the back button is still pressed down, Home/Guide button press is + emulated.
+ If back_button_timeout < 0, then the Home/Guide button will not be emulated
+
+
+ +
+ + +
+ Control how fast keys will repeat themselves
+ The initial delay in milliseconds before repeating keys +
+
+ +
+ + +
+ How often keys repeat every second
+ This configurable option supports decimals +
+
+
+ +
+ +
+ + +
+ The name of the audio sink used for Audio Loopback
+ You can find the name of the audio sink using the following command:
+
tools\audio-info.exe
+
+
+ The name of the audio sink used for Audio Loopback
+ If you do not specify this variable, pulseaudio will select the default monitor device.
+
+ You can find the name of the audio sink using the following command:
+
pacmd list-sinks | grep "name:"

+
+
+ +
+ + +
+ The virtual sink, is the audio device that's virtual (Like Steam Streaming Speakers), it allows Sunshine + to stream audio, while muting the speakers. +
+
+ +
+ + +
+ You can select the video card you want to stream:
+ The appropriate values can be found using the following command:
+
tools\dxgi-info.exe
+
+
+ +
+ + +
+ You can select the video card you want to stream:
+ The appropriate values can be found using the following command:
+ tools\dxgi-info.exe
+ !! Linux only !!
+ Set the display number to stream. I have no idea how they are numbered. They start from 0, usually.
+
+
+
+
+ +
+ + +
+ Constant Rate Factor. Between 1 and 52. It allows QP to go up during motion and down with still + image, + resulting in constant perceived quality
+ Higher value means more compression, but less quality
+ If crf == 0, then use QP directly instead +
+
+ +
+ + +
+ Quantitization Parameter
+ Higher value means more compression, but less quality
+ If crf != 0, then this parameter is ignored +
+
+ +
+ + +
+ Minimum number of threads used by ffmpeg to encode the video.
+ Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually
+ worth it to gain the use of more CPU cores for encoding. The ideal value is the lowest
+ value that can reliably encode at your desired streaming settings on your hardware. +
+
+ +
+ + +
+ Allows the client to request HEVC Main or HEVC Main10 video streams.
+ HEVC is more CPU-intensive to encode, so enabling this may reduce performance when using software + encoding. +
+
+ +
+ + +
+ Force a specific encoder, otherwise Sunshine will use the first encoder that is available +
+
+ +
+ + +
+ How much error correcting packets must be send for every video.
+ This is just some random number, don't know the optimal value.
+ The higher fec_percentage, the lower space for the actual data to send per frame there is +
+
+ +
+ + +
+ When multicasting, it could be useful to have different configurations for each connected Client. + For example: +
    +
  • Clients connected through WAN and LAN have different bitrate contstraints.
  • +
  • Decoders may require different settings for color
  • +
+ Unlike simply broadcasting to multiple Client, this will generate distinct video streams.
+ Note, CPU usage increases for each distinct video stream generated +
+
+
+ +
+
+ + +
+
+ + +
+
+ +
+ +
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ + +
+
+ + +
+
+ + +
+
+
+
Success! Restart Sunshine to apply changes
+
+ +
+
+ + + + \ No newline at end of file diff --git a/assets/web/header.html b/assets/web/header.html new file mode 100644 index 00000000..1c9702e6 --- /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..4b02f845 --- /dev/null +++ b/assets/web/index.html @@ -0,0 +1,9 @@ +
+
+
+

Hello, Sunshine!

+

Sunshine is a Gamestream host for Moonlight

+ Official GitHub Repository +
+
+
\ No newline at end of file diff --git a/assets/web/password.html b/assets/web/password.html new file mode 100644 index 00000000..3e3f2140 --- /dev/null +++ b/assets/web/password.html @@ -0,0 +1,97 @@ +
+

Password Change

+
+
+
+

Current Credentials

+
+ + +
 
+
+
+ + +
+
+
+

New Credentials

+
+ + +
If not specified, the username will not change +
+
+
+ + +
+
+ + +
+
+
+
Error: {{error}}
+
Success! This page will reload soon, your browser will ask you for the new credentials
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/assets/web/pin.html b/assets/web/pin.html new file mode 100644 index 00000000..89e5b704 --- /dev/null +++ b/assets/web/pin.html @@ -0,0 +1,31 @@ + +
+

PIN Pairing

+
+
+ + +
+
+ Warning! Make sure you have access to the client you are pairing with.
+ This software can give total control to your computer, so be careful! +
+
+
+
+ + \ No newline at end of file diff --git a/sunshine/config.cpp b/sunshine/config.cpp index e24514b2..f2cf391e 100644 --- a/sunshine/config.cpp +++ b/sunshine/config.cpp @@ -209,8 +209,12 @@ input_t input { }; sunshine_t sunshine { - 2, // min_log_level - 0 // flags + 2, // min_log_level + 0, // flags + "user_credentials.json"s, //User file + ""s, //Username + ""s, //Password + ""s //Password Salt }; bool endline(char ch) { diff --git a/sunshine/config.h b/sunshine/config.h index 37f59066..99f50408 100644 --- a/sunshine/config.h +++ b/sunshine/config.h @@ -5,6 +5,7 @@ #include #include #include +#include #include namespace config { @@ -88,8 +89,11 @@ enum flag_e : std::size_t { struct sunshine_t { int min_log_level; - std::bitset flags; + std::string credentials_file; + std::string username; + std::string password; + std::string salt; }; extern video_t video; @@ -100,6 +104,6 @@ extern input_t input; extern sunshine_t sunshine; int parse(int argc, char *argv[]); +std::unordered_map parse_config(std::string_view file_content); } // namespace config - #endif diff --git a/sunshine/confighttp.cpp b/sunshine/confighttp.cpp new file mode 100644 index 00000000..503a8c2f --- /dev/null +++ b/sunshine/confighttp.cpp @@ -0,0 +1,477 @@ +// +// 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 + +#include "config.h" +#include "confighttp.h" +#include "crypto.h" +#include "httpcommon.h" +#include "main.h" +#include "network.h" +#include "nvhttp.h" +#include "platform/common.h" +#include "rtsp.h" +#include "utility.h" +#include "uuid.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 +}; + +void send_unauthorized(resp_https_t response, req_https_t request) { + auto address = request->remote_endpoint_address(); + BOOST_LOG(info) << '[' << address << "] -- denied"sv; + const SimpleWeb::CaseInsensitiveMultimap headers { + { "WWW-Authenticate", R"(Basic realm="Sunshine Gamestream Host", charset="UTF-8")" } + }; + response->write(SimpleWeb::StatusCode::client_error_unauthorized, headers); +} + +bool authenticate(resp_https_t response, req_https_t request) { + auto address = request->remote_endpoint_address(); + auto ip_type = net::from_address(address); + if(ip_type > http::origin_pin_allowed) { + BOOST_LOG(info) << '[' << address << "] -- denied"sv; + response->write(SimpleWeb::StatusCode::client_error_forbidden); + return false; + } + auto auth = request->header.find("authorization"); + if(auth == request->header.end()) { + send_unauthorized(response, request); + return false; + } + std::string rawAuth = auth->second; + std::string authData = rawAuth.substr("Basic "sv.length()); + authData = SimpleWeb::Crypto::Base64::decode(authData); + int index = authData.find(':'); + std::string username = authData.substr(0, index); + std::string password = authData.substr(index + 1); + std::string hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string(); + if(username == config::sunshine.username && hash == config::sunshine.password) return true; + + send_unauthorized(response, request); + return false; +} + +void not_found(resp_https_t response, req_https_t 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) { + if(!authenticate(response, request)) return; + + std::string header = read_file(WEB_DIR "header.html"); + std::string content = read_file(WEB_DIR "index.html"); + response->write(header + content); +} + +void getPinPage(resp_https_t response, req_https_t request) { + if(!authenticate(response, request)) return; + + std::string header = read_file(WEB_DIR "header.html"); + std::string content = read_file(WEB_DIR "pin.html"); + response->write(header + content); +} + +void getAppsPage(resp_https_t response, req_https_t request) { + if(!authenticate(response, request)) return; + + std::string header = read_file(WEB_DIR "header.html"); + std::string content = read_file(WEB_DIR "apps.html"); + response->write(header + content); +} + +void getClientsPage(resp_https_t response, req_https_t request) { + if(!authenticate(response, request)) return; + + std::string header = read_file(WEB_DIR "header.html"); + std::string content = read_file(WEB_DIR "clients.html"); + response->write(header + content); +} + +void getConfigPage(resp_https_t response, req_https_t request) { + if(!authenticate(response, request)) return; + + std::string header = read_file(WEB_DIR "header.html"); + std::string content = read_file(WEB_DIR "config.html"); + response->write(header + content); +} + +void getPasswordPage(resp_https_t response, req_https_t request) { + if(!authenticate(response, request)) return; + + std::string header = read_file(WEB_DIR "header.html"); + std::string content = read_file(WEB_DIR "password.html"); + response->write(header + content); +} + +void getApps(resp_https_t response, req_https_t request) { + if(!authenticate(response, request)) return; + + std::string content = read_file(SUNSHINE_ASSETS_DIR "/" APPS_JSON); + response->write(content); +} + +void saveApp(resp_https_t response, req_https_t request) { + if(!authenticate(response, request)) return; + + 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"); + if(inputTree.get_child("prep-cmd").empty()) + inputTree.erase("prep-cmd"); + + if(inputTree.get_child("detached").empty()) + inputTree.erase("detached"); + + 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, copy 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) { + if(!authenticate(response, request)) return; + + 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(config::stream.file_apps, 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, copy 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 getConfig(resp_https_t response, req_https_t request) { + if(!authenticate(response, request)) return; + + pt::ptree outputTree; + auto g = util::fail_guard([&]() { + std::ostringstream data; + + pt::write_json(data, outputTree); + response->write(data.str()); + }); + try { + outputTree.put("status", "true"); + outputTree.put("platform", SUNSHINE_PLATFORM); + const char *config_file = SUNSHINE_ASSETS_DIR "/sunshine.conf"; + std::ifstream in { config_file }; + + if(!in.is_open()) { + std::cout << "Error: Couldn't open "sv << config_file << std::endl; + } + + auto vars = config::parse_config(std::string { + // Quick and dirty + std::istreambuf_iterator(in), + std::istreambuf_iterator() }); + + for(auto &[name, value] : vars) { + outputTree.put(std::move(name), std::move(value)); + } + } + catch(std::exception &e) { + BOOST_LOG(warning) << e.what(); + outputTree.put("status", "false"); + outputTree.put("error", "Invalid File JSON"); + return; + } +} + +void saveConfig(resp_https_t response, req_https_t request) { + if(!authenticate(response, request)) return; + + std::stringstream ss; + std::stringstream configStream; + 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; + try { + //TODO: Input Validation + pt::read_json(ss, inputTree); + for(const auto &kv : inputTree) { + std::string value = inputTree.get(kv.first); + if(value.length() == 0 || value.compare("null") == 0) continue; + + configStream << kv.first << " = " << value << std::endl; + } + http::write_file(SUNSHINE_ASSETS_DIR "/sunshine.conf", configStream.str()); + } + catch(std::exception &e) { + BOOST_LOG(warning) << e.what(); + outputTree.put("status", "false"); + outputTree.put("error", e.what()); + return; + } +} + +void savePassword(resp_https_t response, req_https_t request) { + if(!authenticate(response, request)) return; + + std::stringstream ss; + std::stringstream configStream; + ss << request->content.rdbuf(); + + pt::ptree inputTree,outputTree,fileTree; + + auto g = util::fail_guard([&]() { + std::ostringstream data; + pt::write_json(data, outputTree); + response->write(data.str()); + }); + + try { + //TODO: Input Validation + pt::read_json(ss, inputTree); + std::string username = inputTree.get("currentUsername"); + std::string newUsername = inputTree.get("newUsername"); + std::string password = inputTree.get("currentPassword"); + std::string newPassword = inputTree.get("newPassword"); + std::string confirmPassword = inputTree.get("confirmNewPassword"); + if(newUsername.length() == 0) newUsername = username; + + std::string hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string(); + if(username == config::sunshine.username && hash == config::sunshine.password){ + if(newPassword != confirmPassword){ + outputTree.put("status",false); + outputTree.put("error","Password Mismatch"); + } + fileTree.put("username",newUsername); + fileTree.put("password",util::hex(crypto::hash(newPassword + config::sunshine.salt)).to_string()); + fileTree.put("salt",config::sunshine.salt); + pt::write_json(config::sunshine.credentials_file,fileTree); + http::reload_user_creds(config::sunshine.credentials_file); + outputTree.put("status",true); + } else { + outputTree.put("status",false); + outputTree.put("error","Invalid Current Credentials"); + } + } + catch(std::exception &e) { + BOOST_LOG(warning) << e.what(); + outputTree.put("status", false); + outputTree.put("error", e.what()); + return; + } +} + +void savePin(resp_https_t response, req_https_t request){ + if(!authenticate(response, request)) return; + + 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 { + //TODO: Input Validation + pt::read_json(ss, inputTree); + std::string pin = inputTree.get("pin"); + outputTree.put("status",nvhttp::pin(pin)); + } + catch(std::exception &e) { + BOOST_LOG(warning) << e.what(); + outputTree.put("status", false); + outputTree.put("error", e.what()); + 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 server { ctx, 0 }; + server.default_resource = not_found; + server.resource["^/$"]["GET"] = getIndexPage; + server.resource["^/pin$"]["GET"] = getPinPage; + server.resource["^/apps$"]["GET"] = getAppsPage; + server.resource["^/clients$"]["GET"] = getClientsPage; + server.resource["^/config$"]["GET"] = getConfigPage; + server.resource["^/password$"]["GET"] = getPasswordPage; + server.resource["^/api/pin"]["POST"] = savePin; + server.resource["^/api/apps$"]["GET"] = getApps; + server.resource["^/api/apps$"]["POST"] = saveApp; + server.resource["^/api/config$"]["GET"] = getConfig; + server.resource["^/api/config$"]["POST"] = saveConfig; + server.resource["^/api/password$"]["POST"] = savePassword; + server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp; + server.config.reuse_address = true; + server.config.address = "0.0.0.0"s; + server.config.port = PORT_HTTP; + + try { + 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; + } + auto accept_and_run = [&](auto *server) { + try { + server->accept_and_run(); + } + catch(boost::system::system_error &err) { + // It's possible the exception gets thrown after calling server->stop() from a different thread + if(shutdown_event->peek()) { + return; + } + + BOOST_LOG(fatal) << "Couldn't start Configuration HTTP server to ports ["sv << PORT_HTTP << ", "sv << PORT_HTTP << "]: "sv << err.what(); + shutdown_event->raise(true); + return; + } + }; + std::thread tcp { accept_and_run, &server }; + + // Wait for any event + shutdown_event->view(); + + server.stop(); + + tcp.join(); +} +} // namespace confighttp + +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..edd7181e --- /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/crypto.cpp b/sunshine/crypto.cpp index c5069a71..58053a03 100644 --- a/sunshine/crypto.cpp +++ b/sunshine/crypto.cpp @@ -3,9 +3,10 @@ // #include "crypto.h" +#include #include namespace crypto { -using big_num_t = util::safe_ptr; +using big_num_t = util::safe_ptr; //using rsa_t = util::safe_ptr; using asn1_string_t = util::safe_ptr; @@ -338,4 +339,22 @@ bool verify256(const x509_t &x509, const std::string_view &data, const std::stri void md_ctx_destroy(EVP_MD_CTX *ctx) { EVP_MD_CTX_destroy(ctx); } -} // namespace crypto + +std::string rand_string(std::size_t bytes) { + std::string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!%&()=-"; + std::string value = rand(bytes); + for(std::size_t i = 0; i != value.size(); ++i) { + value[i] = alphabet[value[i] % alphabet.length()]; + } + return value; +} + +std::string hash_hexstr(const std::string_view &plaintext) { + sha256_t hashBytes = crypto::hash(plaintext); + std::ostringstream hashStream; + hashStream << std::hex << std::setfill('0'); + std::for_each(hashBytes.cbegin(), hashBytes.cend(), [&](int c) { hashStream << std::setw(2) << c; }); + std::string hashString = hashStream.str(); + return hashString; +} +} // namespace crypto \ No newline at end of file diff --git a/sunshine/crypto.h b/sunshine/crypto.h index 8a1b70c8..369a417c 100644 --- a/sunshine/crypto.h +++ b/sunshine/crypto.h @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -35,6 +36,7 @@ using bio_t = util::safe_ptr; using pkey_t = util::safe_ptr; sha256_t hash(const std::string_view &plaintext); +std::string hash_hexstr(const std::string_view &plaintext); aes_t gen_aes_key(const std::array &salt, const std::string_view &pin); x509_t x509(const std::string_view &x); @@ -50,6 +52,7 @@ creds_t gen_creds(const std::string_view &cn, std::uint32_t key_bits); std::string_view signature(const x509_t &x); std::string rand(std::size_t bytes); +std::string rand_string(std::size_t bytes); class cert_chain_t { public: diff --git a/sunshine/httpcommon.cpp b/sunshine/httpcommon.cpp new file mode 100644 index 00000000..825ae3d6 --- /dev/null +++ b/sunshine/httpcommon.cpp @@ -0,0 +1,183 @@ +#include "process.h" + +#include + +#include +#include +#include + +#include + +#include +#include +#include + +#include "config.h" +#include "utility.h" +#include "rtsp.h" +#include "crypto.h" +#include "nvhttp.h" +#include "platform/common.h" +#include "network.h" +#include "uuid.h" +#include "main.h" +#include "httpcommon.h" + +namespace http { +using namespace std::literals; +namespace fs = std::filesystem; +namespace pt = boost::property_tree; + +int create_creds(const std::string &pkey, const std::string &cert); +int generate_user_creds(const std::string &file); +int reload_user_creds(const std::string &file); +std::string read_file(const char *path); +int write_file(const char *path, const std::string_view &contents); +std::string unique_id; +net::net_e origin_pin_allowed; + +void init(std::shared_ptr shutdown_event) { + bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE]; + origin_pin_allowed = net::from_enum_string(config::nvhttp.origin_pin_allowed); + if(clean_slate) { + unique_id = util::uuid_t::generate().string(); + auto dir = std::filesystem::temp_directory_path() / "Sushine"sv; + config::nvhttp.cert = (dir / ("cert-"s + unique_id)).string(); + config::nvhttp.pkey = (dir / ("pkey-"s + unique_id)).string(); + } + + if(!fs::exists(config::nvhttp.pkey) || !fs::exists(config::nvhttp.cert)) { + if(create_creds(config::nvhttp.pkey, config::nvhttp.cert)) { + shutdown_event->raise(true); + return; + } + } + if(!fs::exists(config::sunshine.credentials_file)) { + if(generate_user_creds(config::sunshine.credentials_file)) { + shutdown_event->raise(true); + return; + } + } + if(reload_user_creds(config::sunshine.credentials_file)) { + shutdown_event->raise(true); + return; + } +} + +int generate_user_creds(const std::string &file) { + pt::ptree outputTree; + try { + std::string username = "sunshine"; + std::string plainPassword = crypto::rand_string(16); + std::string salt = crypto::rand_string(16); + outputTree.put("username", "sunshine"); + outputTree.put("salt", salt); + outputTree.put("password", util::hex(crypto::hash(plainPassword + salt)).to_string()); + BOOST_LOG(info) << "New credentials has been created"; + BOOST_LOG(info) << "Username: " << username; + BOOST_LOG(info) << "Password: " << plainPassword; + pt::write_json(file, outputTree); + } + catch(std::exception &e) { + BOOST_LOG(fatal) << e.what(); + return 1; + } + return 0; +} + +int reload_user_creds(const std::string &file) { + pt::ptree inputTree; + try { + pt::read_json(file, inputTree); + config::sunshine.username = inputTree.get("username"); + config::sunshine.password = inputTree.get("password"); + config::sunshine.salt = inputTree.get("salt"); + } + catch(std::exception &e) { + BOOST_LOG(fatal) << e.what(); + return 1; + } + return 0; +} + +int create_creds(const std::string &pkey, const std::string &cert) { + fs::path pkey_path = pkey; + fs::path cert_path = cert; + + auto creds = crypto::gen_creds("Sunshine Gamestream Host"sv, 2048); + + auto pkey_dir = pkey_path; + auto cert_dir = cert_path; + pkey_dir.remove_filename(); + cert_dir.remove_filename(); + + std::error_code err_code {}; + fs::create_directories(pkey_dir, err_code); + if(err_code) { + BOOST_LOG(fatal) << "Couldn't create directory ["sv << pkey_dir << "] :"sv << err_code.message(); + return -1; + } + + fs::create_directories(cert_dir, err_code); + if(err_code) { + BOOST_LOG(fatal) << "Couldn't create directory ["sv << cert_dir << "] :"sv << err_code.message(); + return -1; + } + + if(write_file(pkey.c_str(), creds.pkey)) { + BOOST_LOG(fatal) << "Couldn't open ["sv << config::nvhttp.pkey << ']'; + return -1; + } + + if(write_file(cert.c_str(), creds.x509)) { + BOOST_LOG(fatal) << "Couldn't open ["sv << config::nvhttp.cert << ']'; + return -1; + } + + fs::permissions(pkey_path, + fs::perms::owner_read | fs::perms::owner_write, + fs::perm_options::replace, err_code); + + if(err_code) { + BOOST_LOG(fatal) << "Couldn't change permissions of ["sv << config::nvhttp.pkey << "] :"sv << err_code.message(); + return -1; + } + + fs::permissions(cert_path, + fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read | fs::perms::owner_write, + fs::perm_options::replace, err_code); + + if(err_code) { + BOOST_LOG(fatal) << "Couldn't change permissions of ["sv << config::nvhttp.cert << "] :"sv << err_code.message(); + return -1; + } + + return 0; +} +int write_file(const char *path, const std::string_view &contents) { + std::ofstream out(path); + + if(!out.is_open()) { + return -1; + } + + out << contents; + + return 0; +} + +std::string read_file(const char *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; +} +} // namespace http \ No newline at end of file diff --git a/sunshine/httpcommon.h b/sunshine/httpcommon.h new file mode 100644 index 00000000..7c441a7c --- /dev/null +++ b/sunshine/httpcommon.h @@ -0,0 +1,10 @@ +#include "network.h" +namespace http { +void init(std::shared_ptr shutdown_event); +int create_creds(const std::string &pkey, const std::string &cert); +std::string read_file(const char *path); +int write_file(const char *path, const std::string_view &contents); +int reload_user_creds(const std::string &file); +extern std::string unique_id; +extern net::net_e origin_pin_allowed; +} // namespace http \ No newline at end of file diff --git a/sunshine/main.cpp b/sunshine/main.cpp index 0da0a6f6..70301297 100644 --- a/sunshine/main.cpp +++ b/sunshine/main.cpp @@ -16,6 +16,8 @@ #include "config.h" #include "nvhttp.h" +#include "httpcommon.h" +#include "confighttp.h" #include "rtsp.h" #include "thread_pool.h" #include "video.h" @@ -123,18 +125,7 @@ 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(); if(!deinit_guard) { @@ -145,12 +136,12 @@ int main(int argc, char *argv[]) { if(video::init()) { return 2; } - + http::init(shutdown_event); task_pool.start(1); std::thread httpThread { nvhttp::start, shutdown_event }; + std::thread configThread { confighttp::start, shutdown_event }; stream::rtpThread(shutdown_event); - httpThread.join(); task_pool.stop(); task_pool.join(); diff --git a/sunshine/nvhttp.cpp b/sunshine/nvhttp.cpp index 255407cc..e438e2f1 100644 --- a/sunshine/nvhttp.cpp +++ b/sunshine/nvhttp.cpp @@ -18,6 +18,7 @@ #include #include + #include "config.h" #include "crypto.h" #include "main.h" @@ -27,7 +28,8 @@ #include "rtsp.h" #include "utility.h" #include "uuid.h" - +#include "main.h" +#include "httpcommon.h" namespace nvhttp { using namespace std::literals; @@ -40,9 +42,6 @@ constexpr auto GFE_VERSION = "3.12.0.1"; namespace fs = std::filesystem; namespace pt = boost::property_tree; -std::string read_file(const char *path); -int write_file(const char *path, const std::string_view &contents); - using https_server_t = SimpleWeb::Server; using http_server_t = SimpleWeb::Server; @@ -80,8 +79,6 @@ struct pair_session_t { // uniqueID, session std::unordered_map map_id_sess; std::unordered_map map_id_client; -std::string unique_id; -net::net_e origin_pin_allowed; using args_t = SimpleWeb::CaseInsensitiveMultimap; using resp_https_t = std::shared_ptr::Response>; @@ -97,7 +94,7 @@ enum class op_e { void save_state() { pt::ptree root; - root.put("root.uniqueid", unique_id); + root.put("root.uniqueid", http::unique_id); auto &nodes = root.add_child("root.devices", pt::ptree {}); for(auto &[_, client] : map_id_client) { pt::ptree node; @@ -119,8 +116,9 @@ void save_state() { } void load_state() { - if(!fs::exists(config::nvhttp.file_state)) { - unique_id = util::uuid_t::generate().string(); + auto file_state = fs::current_path() / config::nvhttp.file_state; + if(!fs::exists(file_state)) { + http::unique_id = util::uuid_t::generate().string(); return; } @@ -134,7 +132,7 @@ void load_state() { return; } - unique_id = root.get("root.uniqueid"); + http::unique_id = root.get("root.uniqueid"); auto device_nodes = root.get_child("root.devices"); for(auto &[_, device_node] : device_nodes) { @@ -422,30 +420,14 @@ void pair(std::shared_ptr> &add_cert, std::shared_ response->write(data.str()); } -template -void pin(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { - print_req(request); - - auto address = request->remote_endpoint_address(); - auto ip_type = net::from_address(address); - if(ip_type > origin_pin_allowed) { - BOOST_LOG(info) << '[' << address << "] -- denied"sv; - - response->write(SimpleWeb::StatusCode::client_error_forbidden); - - return; - } - +bool pin(std::string pin){ pt::ptree tree; - if(map_id_sess.empty()) { - response->write(SimpleWeb::StatusCode::client_error_im_a_teapot); - - return; + return false; } auto &sess = std::begin(map_id_sess)->second; - getservercert(sess, tree, request->path_match[1]); + getservercert(sess, tree, pin); // response to the request for pin std::ostringstream data; @@ -459,15 +441,35 @@ void pin(std::shared_ptr::Response> response, async_response.right()->write(data.str()); } else { - response->write(SimpleWeb::StatusCode::client_error_im_a_teapot); - - return; + return false; } // reset async_response async_response = std::decay_t(); // response to the current request - response->write(SimpleWeb::StatusCode::success_ok); + return true; +} + +template +void pin(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { + print_req(request); + + auto address = request->remote_endpoint_address(); + auto ip_type = net::from_address(address); + if(ip_type > http::origin_pin_allowed) { + BOOST_LOG(info) << '[' << address << "] -- denied"sv; + + response->write(SimpleWeb::StatusCode::client_error_forbidden); + + return; + } + + bool pinResponse = pin(request->path_match[1]); + if(pinResponse){ + response->write(SimpleWeb::StatusCode::success_ok); + } else { + response->write(SimpleWeb::StatusCode::client_error_im_a_teapot); + } } template @@ -494,7 +496,7 @@ void serverinfo(std::shared_ptr::Response> res tree.put("root.appversion", VERSION); tree.put("root.GfeVersion", GFE_VERSION); - tree.put("root.uniqueid", unique_id); + tree.put("root.uniqueid", http::unique_id); tree.put("root.mac", platf::get_mac_address(request->local_endpoint_address())); tree.put("root.MaxLumaPixelsHEVC", config::video.hevc_mode > 1 ? "1869449984" : "0"); tree.put("root.LocalIP", request->local_endpoint_address()); @@ -731,88 +733,16 @@ void appasset(resp_https_t response, req_https_t request) { response->write(SimpleWeb::StatusCode::success_ok, in); } -int create_creds(const std::string &pkey, const std::string &cert) { - fs::path pkey_path = pkey; - fs::path cert_path = cert; - - auto creds = crypto::gen_creds("Sunshine Gamestream Host"sv, 2048); - - auto pkey_dir = pkey_path; - auto cert_dir = cert_path; - pkey_dir.remove_filename(); - cert_dir.remove_filename(); - - std::error_code err_code {}; - fs::create_directories(pkey_dir, err_code); - if(err_code) { - BOOST_LOG(fatal) << "Couldn't create directory ["sv << pkey_dir << "] :"sv << err_code.message(); - return -1; - } - - fs::create_directories(cert_dir, err_code); - if(err_code) { - BOOST_LOG(fatal) << "Couldn't create directory ["sv << cert_dir << "] :"sv << err_code.message(); - return -1; - } - - if(write_file(pkey.c_str(), creds.pkey)) { - BOOST_LOG(fatal) << "Couldn't open ["sv << config::nvhttp.pkey << ']'; - return -1; - } - - if(write_file(cert.c_str(), creds.x509)) { - BOOST_LOG(fatal) << "Couldn't open ["sv << config::nvhttp.cert << ']'; - return -1; - } - - fs::permissions(pkey_path, - fs::perms::owner_read | fs::perms::owner_write, - fs::perm_options::replace, err_code); - - if(err_code) { - BOOST_LOG(fatal) << "Couldn't change permissions of ["sv << config::nvhttp.pkey << "] :"sv << err_code.message(); - return -1; - } - - fs::permissions(cert_path, - fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read | fs::perms::owner_write, - fs::perm_options::replace, err_code); - - if(err_code) { - BOOST_LOG(fatal) << "Couldn't change permissions of ["sv << config::nvhttp.cert << "] :"sv << err_code.message(); - return -1; - } - - return 0; -} - void start(std::shared_ptr shutdown_event) { + bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE]; - if(clean_slate) { - unique_id = util::uuid_t::generate().string(); - - auto dir = std::filesystem::temp_directory_path() / "Sushine"sv; - - config::nvhttp.cert = (dir / ("cert-"s + unique_id)).string(); - config::nvhttp.pkey = (dir / ("pkey-"s + unique_id)).string(); - } - - - if(!fs::exists(config::nvhttp.pkey) || !fs::exists(config::nvhttp.cert)) { - if(create_creds(config::nvhttp.pkey, config::nvhttp.cert)) { - shutdown_event->raise(true); - return; - } - } - - origin_pin_allowed = net::from_enum_string(config::nvhttp.origin_pin_allowed); if(!clean_slate) { load_state(); } - conf_intern.pkey = read_file(config::nvhttp.pkey.c_str()); - conf_intern.servercert = read_file(config::nvhttp.cert.c_str()); + conf_intern.pkey = http::read_file(config::nvhttp.pkey.c_str()); + conf_intern.servercert = http::read_file(config::nvhttp.cert.c_str()); auto ctx = std::make_shared(boost::asio::ssl::context::tls); ctx->use_certificate_chain_file(config::nvhttp.cert); @@ -931,31 +861,4 @@ void start(std::shared_ptr shutdown_event) { ssl.join(); tcp.join(); } - -int write_file(const char *path, const std::string_view &contents) { - std::ofstream out(path); - - if(!out.is_open()) { - return -1; - } - - out << contents; - - return 0; -} - -std::string read_file(const char *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; -} -} // namespace nvhttp +} // namespace nvhttp \ No newline at end of file diff --git a/sunshine/nvhttp.h b/sunshine/nvhttp.h index 8b3d50a4..f6853b60 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); -} +bool pin(std::string pin); +} // namespace nvhttp #endif //SUNSHINE_NVHTTP_H diff --git a/sunshine/process.cpp b/sunshine/process.cpp index 88532aca..697868d8 100644 --- a/sunshine/process.cpp +++ b/sunshine/process.cpp @@ -332,6 +332,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); } }