diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 066bfc27..28fca7e8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -383,6 +383,7 @@ jobs: libboost-log-dev \ libboost-thread-dev \ libcap-dev \ + libcurl4-openssl-dev \ libdrm-dev \ libevdev-dev \ libpulse-dev \ @@ -548,7 +549,7 @@ jobs: - name: Setup Dependencies MacOS run: | # install dependencies using homebrew - brew install boost cmake ffmpeg opus + brew install boost cmake curl ffmpeg opus # fix openssl header not found ln -sf /usr/local/opt/openssl/include/openssl /usr/local/include/openssl @@ -846,6 +847,7 @@ jobs: mingw-w64-x86_64-binutils mingw-w64-x86_64-boost mingw-w64-x86_64-cmake + mingw-w64-x86_64-curl mingw-w64-x86_64-nsis mingw-w64-x86_64-openssl mingw-w64-x86_64-opus diff --git a/CMakeLists.txt b/CMakeLists.txt index 840087b8..bd5ae938 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,9 @@ include_directories(third-party/miniupnp) find_package(Threads REQUIRED) find_package(OpenSSL REQUIRED) +find_package(PkgConfig REQUIRED) +pkg_check_modules (CURL REQUIRED libcurl) + if(NOT APPLE) set(Boost_USE_STATIC_LIBS ON) endif() @@ -88,6 +91,7 @@ if(WIN32) INPUT "${CMAKE_CURRENT_BINARY_DIR}/pre-compiled.zip" DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/pre-compiled) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${CURL_STATIC_LDFLAGS} ${CURL_STATIC_CFLAGS}") if(NOT DEFINED SUNSHINE_PREPARED_BINARIES) set(SUNSHINE_PREPARED_BINARIES "${CMAKE_CURRENT_BINARY_DIR}/pre-compiled/windows") @@ -149,6 +153,7 @@ if(WIN32) d3d11 dxgi D3DCompiler setupapi dwmapi + ${CURL_STATIC_LIBRARIES} ) set_source_files_properties(third-party/ViGEmClient/src/ViGEmClient.cpp PROPERTIES COMPILE_DEFINITIONS "UNICODE=1;ERROR_INVALID_DEVICE_OBJECT_PARAMETER=650") @@ -451,6 +456,7 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${FFMPEG_LIBRARIES} ${Boost_LIBRARIES} ${OPENSSL_LIBRARIES} + ${CURL_LIBRARIES} ${PLATFORM_LIBRARIES}) if(NOT WIN32) @@ -458,6 +464,11 @@ if(NOT WIN32) endif() add_executable(sunshine ${SUNSHINE_TARGET_FILES}) + +if(WIN32) + set_target_properties(sunshine PROPERTIES LINK_SEARCH_START_STATIC 1) +endif() + target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES} ${EXTRA_LIBS}) target_compile_definitions(sunshine PUBLIC ${SUNSHINE_DEFINITIONS}) set_target_properties(sunshine PROPERTIES CXX_STANDARD 17 @@ -647,8 +658,8 @@ elseif(UNIX) # Dependencies set(CPACK_DEB_COMPONENT_INSTALL ON) - set(CPACK_DEBIAN_PACKAGE_DEPENDS "openssl, libavdevice58, libboost-thread1.67.0 | libboost-thread1.71.0 | libboost-thread1.74.0, libboost-filesystem1.67.0 | libboost-filesystem1.71.0 | libboost-filesystem1.74.0, libboost-log1.67.0 | libboost-log1.71.0 | libboost-log1.74.0, libpulse0, libopus0, libxcb-shm0, libxcb-xfixes0, libxtst6, libevdev2, libdrm2, libcap2") - set(CPACK_RPM_PACKAGE_REQUIRES "openssl >= 1.1, libavdevice >= 4.3, boost-thread >= 1.67.0, boost-filesystem >= 1.67.0, boost-log >= 1.67.0, pulseaudio-libs >= 10.0, libopusenc >= 0.2.1, libxcb >= 1.13, libXtst >= 1.2.3, libevdev >= 1.5.6, libdrm >= 2.4.97, libcap >= 2.22") + set(CPACK_DEBIAN_PACKAGE_DEPENDS "openssl, libavdevice58, libboost-thread1.67.0 | libboost-thread1.71.0 | libboost-thread1.74.0, libboost-filesystem1.67.0 | libboost-filesystem1.71.0 | libboost-filesystem1.74.0, libboost-log1.67.0 | libboost-log1.71.0 | libboost-log1.74.0, libcurl4, libpulse0, libopus0, libxcb-shm0, libxcb-xfixes0, libxtst6, libevdev2, libdrm2, libcap2") + set(CPACK_RPM_PACKAGE_REQUIRES "openssl >= 1.1, libavdevice >= 4.3, boost-thread >= 1.67.0, boost-filesystem >= 1.67.0, boost-log >= 1.67.0, libcurl >= 7.0, pulseaudio-libs >= 10.0, libopusenc >= 0.2.1, libxcb >= 1.13, libXtst >= 1.2.3, libevdev >= 1.5.6, libdrm >= 2.4.97, libcap >= 2.22") set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS OFF) # This should automatically figure out dependencies, doesn't work with the current config endif() endif() diff --git a/Dockerfile b/Dockerfile index b493b0d4..3f52d99e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,7 @@ RUN apt-get update -y \ libboost-log-dev=1.74.0* \ libboost-thread-dev=1.74.0* \ libcap-dev=1:2.44* \ + libcurl4-openssl-dev=7.81.0* \ libdrm-dev=2.4.110* \ libevdev-dev=1.12.1* \ libpulse-dev=1:15.99.1* \ diff --git a/packaging/linux/aur/PKGBUILD b/packaging/linux/aur/PKGBUILD index 1ef6d54b..a1188016 100644 --- a/packaging/linux/aur/PKGBUILD +++ b/packaging/linux/aur/PKGBUILD @@ -9,7 +9,7 @@ arch=('x86_64' 'i686') url=@PROJECT_HOMEPAGE_URL@ license=('GPL3') -depends=('avahi' 'boost-libs' 'ffmpeg4.4' 'libevdev' 'libpulse' 'libx11' 'libxcb' 'libxfixes' 'libxrandr' 'libxtst' 'openssl' 'opus' 'udev') +depends=('avahi' 'boost-libs' 'curl' 'ffmpeg4.4' 'libevdev' 'libpulse' 'libx11' 'libxcb' 'libxfixes' 'libxrandr' 'libxtst' 'openssl' 'opus' 'udev') makedepends=('boost' 'cmake' 'git' 'make') optdepends=('cuda' 'libcap' 'libdrm') diff --git a/packaging/macos/Portfile b/packaging/macos/Portfile index d440fad3..467fd3ee 100644 --- a/packaging/macos/Portfile +++ b/packaging/macos/Portfile @@ -32,6 +32,7 @@ post-fetch { } depends_lib port:avahi \ + port:curl \ port:ffmpeg \ port:libopus diff --git a/src/confighttp.cpp b/src/confighttp.cpp index f5f2ce09..ec38fb93 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -13,6 +13,8 @@ #include +#include + #include #include #include @@ -164,9 +166,12 @@ void getAppsPage(resp_https_t response, req_https_t request) { print_req(request); + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Access-Control-Allow-Origin", "https://images.igdb.com/"); + std::string header = read_file(WEB_DIR "header.html"); std::string content = read_file(WEB_DIR "apps.html"); - response->write(header + content); + response->write(header + content, headers); } void getClientsPage(resp_https_t response, req_https_t request) { @@ -411,6 +416,67 @@ void deleteApp(resp_https_t response, req_https_t request) { proc::refresh(config::stream.file_apps); } +void uploadCover(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; + + SimpleWeb::StatusCode code = SimpleWeb::StatusCode::success_ok; + if(outputTree.get_child_optional("error").has_value()) { + code = SimpleWeb::StatusCode::client_error_bad_request; + } + + pt::write_json(data, outputTree); + response->write(code, data.str()); + }); + pt::ptree inputTree; + try { + pt::read_json(ss, inputTree); + } + catch(std::exception &e) { + BOOST_LOG(warning) << "UploadCover: "sv << e.what(); + outputTree.put("status", "false"); + outputTree.put("error", e.what()); + return; + } + + auto key = inputTree.get("key", ""); + if(key.empty()) { + outputTree.put("error", "Cover key is required"); + return; + } + auto url = inputTree.get("url", ""); + + const std::string coverdir = platf::appdata().string() + "/covers/"; + if(!boost::filesystem::exists(coverdir)) { + boost::filesystem::create_directory(coverdir); + } + + std::basic_string path = coverdir + http::url_escape(key) + ".png"; + if(!url.empty()) { + if(http::url_get_host(url) != "images.igdb.com") { + outputTree.put("error", "Only images.igdb.com is allowed"); + return; + } + if(!http::download_file(url, path)) { + outputTree.put("error", "Failed to download cover"); + return; + } + } + else { + auto data = SimpleWeb::Crypto::Base64::decode(inputTree.get("data")); + + std::ofstream imgfile(path); + imgfile.write(data.data(), (int)data.size()); + } + outputTree.put("path", path); +} + void getConfig(resp_https_t response, req_https_t request) { if(!authenticate(response, request)) return; @@ -616,6 +682,7 @@ void start() { server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp; server.resource["^/api/clients/unpair$"]["POST"] = unpairAll; server.resource["^/api/apps/close"]["POST"] = closeApp; + server.resource["^/api/covers/upload$"]["POST"] = uploadCover; server.resource["^/images/favicon.ico$"]["GET"] = getFaviconImage; server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage; server.resource["^/third_party/bootstrap.min.css$"]["GET"] = getBootstrapCss; diff --git a/src/httpcommon.cpp b/src/httpcommon.cpp index 5f1955a6..e4747e32 100644 --- a/src/httpcommon.cpp +++ b/src/httpcommon.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include "config.h" #include "crypto.h" @@ -180,4 +181,55 @@ int create_creds(const std::string &pkey, const std::string &cert) { return 0; } + +bool download_file(const std::string &url, const std::string &file) { + CURL *curl = curl_easy_init(); + if(!curl) { + BOOST_LOG(error) << "Couldn't create CURL instance"; + return false; + } + FILE *fp = fopen(file.c_str(), "wb"); + if(!fp) { + BOOST_LOG(error) << "Couldn't open ["sv << file << ']'; + curl_easy_cleanup(curl); + return false; + } + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); +#ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); +#endif + CURLcode result = curl_easy_perform(curl); + if(result != CURLE_OK) { + BOOST_LOG(error) << "Couldn't download ["sv << url << ", code:" << result << ']'; + } + curl_easy_cleanup(curl); + fclose(fp); + return result == CURLE_OK; +} + +std::string url_escape(const std::string &url) { + CURL *curl = curl_easy_init(); + char *string = curl_easy_escape(curl, url.c_str(), url.length()); + std::string result(string); + curl_free(string); + curl_easy_cleanup(curl); + return result; +} + +std::string url_get_host(const std::string &url) { + CURLU *curlu = curl_url(); + curl_url_set(curlu, CURLUPART_URL, url.c_str(), url.length()); + char *host; + if(curl_url_get(curlu, CURLUPART_HOST, &host, 0) != CURLUE_OK) { + curl_url_cleanup(curlu); + return ""; + } + std::string result(host); + curl_free(host); + curl_url_cleanup(curlu); + return result; +} + } // namespace http diff --git a/src/httpcommon.h b/src/httpcommon.h index e1a1509a..4e8eee01 100644 --- a/src/httpcommon.h +++ b/src/httpcommon.h @@ -12,6 +12,10 @@ int save_user_creds( bool run_our_mouth = false); int reload_user_creds(const std::string &file); +bool download_file(const std::string &url, const std::string &file); +std::string url_escape(const std::string &url); +std::string url_get_host(const std::string &url); + extern std::string unique_id; extern net::net_e origin_pin_allowed; extern net::net_e origin_web_ui_allowed; diff --git a/src_assets/common/assets/web/apps.html b/src_assets/common/assets/web/apps.html index 9c21fe63..87c22c0d 100644 --- a/src_assets/common/assets/web/apps.html +++ b/src_assets/common/assets/web/apps.html @@ -169,13 +169,47 @@
- +
+ + + +
Application icon/picture/image path that will be sent to client. Image must be a PNG file. If not set, Sunshine will send default box image. @@ -196,6 +230,12 @@