mirror of
https://github.com/LizardByte/Sunshine.git
synced 2025-08-10 00:52:16 +00:00
Compare commits
8 Commits
v0.23.1
...
feat/new-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3888ec8da0 | ||
|
|
7fb8c76590 | ||
|
|
9288775351 | ||
|
|
8ba64ffa32 | ||
|
|
968b7963ee | ||
|
|
61df838356 | ||
|
|
d1845df0ea | ||
|
|
9ef63ca829 |
6
.github/workflows/CI.yml
vendored
6
.github/workflows/CI.yml
vendored
@@ -280,7 +280,7 @@ jobs:
|
||||
include: # package these differently
|
||||
- type: AppImage
|
||||
EXTRA_ARGS: '-DSUNSHINE_BUILD_APPIMAGE=ON'
|
||||
dist: 20.04
|
||||
dist: 22.04
|
||||
|
||||
steps:
|
||||
- name: Maximize build space
|
||||
@@ -323,6 +323,9 @@ jobs:
|
||||
# allow newer gcc
|
||||
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
|
||||
|
||||
# allow libfuse2 for appimage on 22.04
|
||||
sudo add-apt-repository universe
|
||||
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
cmake \
|
||||
@@ -338,6 +341,7 @@ jobs:
|
||||
libcurl4-openssl-dev \
|
||||
libdrm-dev \
|
||||
libevdev-dev \
|
||||
libfuse2 \
|
||||
libminiupnpc-dev \
|
||||
libmfx-dev \
|
||||
libnotify-dev \
|
||||
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -14,6 +14,10 @@
|
||||
path = third-party/googletest
|
||||
url = https://github.com/google/googletest/
|
||||
branch = v1.14.x
|
||||
[submodule "third-party/jwt-cpp"]
|
||||
path = third-party/jwt-cpp
|
||||
url = https://github.com/Thalhammer/jwt-cpp.git
|
||||
branch = master
|
||||
[submodule "third-party/moonlight-common-c"]
|
||||
path = third-party/moonlight-common-c
|
||||
url = https://github.com/moonlight-stream/moonlight-common-c.git
|
||||
|
||||
@@ -125,6 +125,7 @@ include_directories(
|
||||
"${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/enet/include"
|
||||
"${CMAKE_SOURCE_DIR}/third-party/nanors"
|
||||
"${CMAKE_SOURCE_DIR}/third-party/nanors/deps/obl"
|
||||
"${CMAKE_SOURCE_DIR}/third-party/jwt-cpp/include"
|
||||
${FFMPEG_INCLUDE_DIRS}
|
||||
${PLATFORM_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
@@ -8,12 +8,13 @@ link_directories(/opt/homebrew/lib)
|
||||
ADD_DEFINITIONS(-DBOOST_LOG_DYN_LINK)
|
||||
|
||||
list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
|
||||
${APP_KIT_LIBRARY}
|
||||
${APP_SERVICES_LIBRARY}
|
||||
${AV_FOUNDATION_LIBRARY}
|
||||
${CORE_MEDIA_LIBRARY}
|
||||
${CORE_VIDEO_LIBRARY}
|
||||
${VIDEO_TOOLBOX_LIBRARY}
|
||||
${FOUNDATION_LIBRARY})
|
||||
${FOUNDATION_LIBRARY}
|
||||
${VIDEO_TOOLBOX_LIBRARY})
|
||||
|
||||
set(PLATFORM_INCLUDE_DIRS
|
||||
${Boost_INCLUDE_DIR})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# macos specific dependencies
|
||||
|
||||
FIND_LIBRARY(APP_KIT_LIBRARY AppKit)
|
||||
FIND_LIBRARY(APP_SERVICES_LIBRARY ApplicationServices)
|
||||
FIND_LIBRARY(AV_FOUNDATION_LIBRARY AVFoundation)
|
||||
FIND_LIBRARY(CORE_MEDIA_LIBRARY CoreMedia)
|
||||
|
||||
@@ -36,7 +36,7 @@ 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
|
||||
set_target_properties(sunshine PROPERTIES CXX_STANDARD 20
|
||||
VERSION ${PROJECT_VERSION}
|
||||
SOVERSION ${PROJECT_VERSION_MAJOR})
|
||||
|
||||
|
||||
@@ -576,20 +576,29 @@ keybindings
|
||||
.. tip:: To find the name of the appropriate values follow these instructions.
|
||||
|
||||
**Linux**
|
||||
During Sunshine startup, you should see the list of detected monitors:
|
||||
During Sunshine startup, you should see the list of detected displays:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
Info: Detecting connected monitors
|
||||
Info: Detected monitor 0: DVI-D-0, connected: false
|
||||
Info: Detected monitor 1: HDMI-0, connected: true
|
||||
Info: Detected monitor 2: DP-0, connected: true
|
||||
Info: Detected monitor 3: DP-1, connected: false
|
||||
Info: Detected monitor 4: DVI-D-1, connected: false
|
||||
Info: Detecting displays
|
||||
Info: Detected display: DVI-D-0 (id: 0) connected: false
|
||||
Info: Detected display: HDMI-0 (id: 1) connected: true
|
||||
Info: Detected display: DP-0 (id: 2) connected: true
|
||||
Info: Detected display: DP-1 (id: 3) connected: false
|
||||
Info: Detected display: DVI-D-1 (id: 4) connected: false
|
||||
|
||||
You need to use the value before the colon in the output, e.g. ``1``.
|
||||
You need to use the id value inside the parenthesis, e.g. ``1``.
|
||||
|
||||
.. todo:: macOS
|
||||
**macOS**
|
||||
During Sunshine startup, you should see the list of detected displays:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
Info: Detecting displays
|
||||
Info: Detected display: Monitor-0 (id: 3) connected: true
|
||||
Info: Detected display: Monitor-1 (id: 2) connected: true
|
||||
|
||||
You need to use the id value inside the parenthesis, e.g. ``3``.
|
||||
|
||||
**Windows**
|
||||
.. code-block:: batch
|
||||
@@ -605,7 +614,10 @@ keybindings
|
||||
|
||||
output_name = 0
|
||||
|
||||
.. todo:: macOS
|
||||
**macOS**
|
||||
.. code-block:: text
|
||||
|
||||
output_name = 3
|
||||
|
||||
**Windows**
|
||||
.. code-block:: text
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <Simple-Web-Server/crypto.hpp>
|
||||
#include <Simple-Web-Server/server_https.hpp>
|
||||
#include <boost/asio/ssl/context_base.hpp>
|
||||
#include <jwt-cpp/jwt.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "confighttp.h"
|
||||
@@ -47,6 +48,8 @@ namespace confighttp {
|
||||
namespace fs = std::filesystem;
|
||||
namespace pt = boost::property_tree;
|
||||
|
||||
std::string jwt_key;
|
||||
|
||||
using https_server_t = SimpleWeb::Server<SimpleWeb::HTTPS>;
|
||||
|
||||
using args_t = SimpleWeb::CaseInsensitiveMultimap;
|
||||
@@ -64,7 +67,7 @@ namespace confighttp {
|
||||
BOOST_LOG(debug) << "DESTINATION :: "sv << request->path;
|
||||
|
||||
for (auto &[name, val] : request->header) {
|
||||
BOOST_LOG(debug) << name << " -- " << (name == "Authorization" ? "CREDENTIALS REDACTED" : val);
|
||||
BOOST_LOG(debug) << name << " -- " << (name == "Cookie" || name == "Authorization" ? "SENSIBLE HEADER REDACTED" : val);
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << " [--] "sv;
|
||||
@@ -80,9 +83,7 @@ namespace confighttp {
|
||||
send_unauthorized(resp_https_t response, req_https_t request) {
|
||||
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
|
||||
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv;
|
||||
const SimpleWeb::CaseInsensitiveMultimap headers {
|
||||
{ "WWW-Authenticate", R"(Basic realm="Sunshine Gamestream Host", charset="UTF-8")" }
|
||||
};
|
||||
const SimpleWeb::CaseInsensitiveMultimap headers {};
|
||||
response->write(SimpleWeb::StatusCode::client_error_unauthorized, headers);
|
||||
}
|
||||
|
||||
@@ -114,29 +115,46 @@ namespace confighttp {
|
||||
}
|
||||
|
||||
auto fg = util::fail_guard([&]() {
|
||||
send_unauthorized(response, request);
|
||||
BOOST_LOG(info) << request->path;
|
||||
std::string apiPrefix = "/api";
|
||||
if (request->path.compare(0, apiPrefix.length(), apiPrefix) == 0) {
|
||||
send_unauthorized(response, request);
|
||||
}
|
||||
// Redirect to login, but only once
|
||||
else if (request->path.compare("/login") != 0) {
|
||||
send_redirect(response, request, "/login");
|
||||
}
|
||||
});
|
||||
|
||||
auto auth = request->header.find("authorization");
|
||||
auto auth = request->header.find("cookie");
|
||||
if (auth == request->header.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto &rawAuth = auth->second;
|
||||
auto authData = SimpleWeb::Crypto::Base64::decode(rawAuth.substr("Basic "sv.length()));
|
||||
std::istringstream iss(rawAuth);
|
||||
std::string token, cookie_name = "sunshine_session=", cookie_value = "";
|
||||
|
||||
int index = authData.find(':');
|
||||
if (index >= authData.size() - 1) {
|
||||
return false;
|
||||
while (std::getline(iss, token, ';')) {
|
||||
// Left Trim Cookie
|
||||
token.erase(token.begin(), std::find_if(token.begin(), token.end(), [](unsigned char ch) {
|
||||
return !std::isspace(ch);
|
||||
}));
|
||||
// Compare that the cookie name is sunshine_session
|
||||
if (token.compare(0, cookie_name.length(), cookie_name) == 0) {
|
||||
cookie_value = token.substr(cookie_name.length());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto username = authData.substr(0, index);
|
||||
auto password = authData.substr(index + 1);
|
||||
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
|
||||
if (cookie_value.length() == 0) return false;
|
||||
auto decoded = jwt::decode(cookie_value);
|
||||
auto verifier = jwt::verify()
|
||||
.with_issuer("sunshine-" + http::unique_id)
|
||||
.with_claim("sub", jwt::claim(std::string(config::sunshine.username)))
|
||||
.allow_algorithm(jwt::algorithm::hs256 { jwt_key });
|
||||
|
||||
if (!boost::iequals(username, config::sunshine.username) || hash != config::sunshine.password) {
|
||||
return false;
|
||||
}
|
||||
verifier.verify(decoded);
|
||||
|
||||
fg.disable();
|
||||
return true;
|
||||
@@ -181,6 +199,16 @@ namespace confighttp {
|
||||
response->write(content, headers);
|
||||
}
|
||||
|
||||
void
|
||||
getLoginPage(resp_https_t response, req_https_t request) {
|
||||
print_req(request);
|
||||
|
||||
std::string content = file_handler::read_file(WEB_DIR "login.html");
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", "text/html; charset=utf-8");
|
||||
response->write(content, headers);
|
||||
}
|
||||
|
||||
void
|
||||
getAppsPage(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
@@ -655,6 +683,8 @@ namespace confighttp {
|
||||
else {
|
||||
http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);
|
||||
http::reload_user_creds(config::sunshine.credentials_file);
|
||||
// Regen the JWT Key to invalidate sessions
|
||||
jwt_key = crypto::rand_alphabet(64);
|
||||
outputTree.put("status", true);
|
||||
}
|
||||
}
|
||||
@@ -738,16 +768,112 @@ namespace confighttp {
|
||||
outputTree.put("status", true);
|
||||
}
|
||||
|
||||
void
|
||||
login(resp_https_t response, req_https_t request) {
|
||||
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
|
||||
auto ip_type = net::from_address(address);
|
||||
|
||||
if (ip_type > http::origin_web_ui_allowed) {
|
||||
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- denied"sv;
|
||||
response->write(SimpleWeb::StatusCode::client_error_forbidden);
|
||||
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);
|
||||
auto username = inputTree.get<std::string>("username");
|
||||
auto password = inputTree.get<std::string>("password");
|
||||
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
|
||||
|
||||
if (!boost::iequals(username, config::sunshine.username) || hash != config::sunshine.password) {
|
||||
outputTree.put("status", "false");
|
||||
return;
|
||||
}
|
||||
outputTree.put("status", "true");
|
||||
auto token = jwt::create().set_type("JWT").set_issued_at(std::chrono::system_clock::now()).set_expires_at(std::chrono::system_clock::now() + std::chrono::seconds { 3600 }).set_issuer("sunshine-" + http::unique_id).set_payload_claim("sub", jwt::claim(std::string(config::sunshine.username))).sign(jwt::algorithm::hs256 { jwt_key });
|
||||
std::stringstream cookie_stream;
|
||||
cookie_stream << "sunshine_session=";
|
||||
cookie_stream << token;
|
||||
cookie_stream << "; Secure; HttpOnly; SameSite=Strict; Path=/";
|
||||
const SimpleWeb::CaseInsensitiveMultimap headers {
|
||||
{ "Set-Cookie", cookie_stream.str() }
|
||||
};
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(SimpleWeb::StatusCode::success_ok, data.str(), headers);
|
||||
g.disable();
|
||||
return;
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << "SaveApp: "sv << e.what();
|
||||
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", "Invalid Input JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
outputTree.put("status", "true");
|
||||
}
|
||||
|
||||
void
|
||||
logout(resp_https_t response, req_https_t request) {
|
||||
pt::ptree outputTree;
|
||||
try {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
const SimpleWeb::CaseInsensitiveMultimap headers {
|
||||
{ "Set-Cookie", "sunshine_session=redacted; expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly; SameSite=Strict; Path=/" }
|
||||
};
|
||||
std::ostringstream data;
|
||||
outputTree.put("status", true);
|
||||
pt::write_json(data, outputTree);
|
||||
|
||||
response->write(SimpleWeb::StatusCode::success_ok, data.str(), headers);
|
||||
g.disable();
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << "SaveApp: "sv << e.what();
|
||||
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", "Invalid Input JSON");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
start() {
|
||||
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
|
||||
|
||||
// On each server start, create a randomized jwt_key
|
||||
jwt_key = crypto::rand_alphabet(64);
|
||||
|
||||
auto port_https = net::map_port(PORT_HTTPS);
|
||||
auto address_family = net::af_from_enum_string(config::sunshine.address_family);
|
||||
|
||||
https_server_t server { config::nvhttp.cert, config::nvhttp.pkey };
|
||||
server.default_resource["GET"] = not_found;
|
||||
server.resource["^/$"]["GET"] = getIndexPage;
|
||||
server.resource["^/login/?$"]["GET"] = getLoginPage;
|
||||
server.resource["^/pin/?$"]["GET"] = getPinPage;
|
||||
server.resource["^/apps/?$"]["GET"] = getAppsPage;
|
||||
server.resource["^/clients/?$"]["GET"] = getClientsPage;
|
||||
@@ -768,6 +894,8 @@ namespace confighttp {
|
||||
server.resource["^/api/clients/unpair$"]["POST"] = unpairAll;
|
||||
server.resource["^/api/apps/close$"]["POST"] = closeApp;
|
||||
server.resource["^/api/covers/upload$"]["POST"] = uploadCover;
|
||||
server.resource["^/api/logout$"]["POST"] = logout;
|
||||
server.resource["^/api/login$"]["POST"] = login;
|
||||
server.resource["^/images/sunshine.ico$"]["GET"] = getFaviconImage;
|
||||
server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage;
|
||||
server.resource["^/assets\\/.+$"]["GET"] = getNodeModules;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "process.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "logging.h"
|
||||
#include "utility.h"
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
@@ -169,7 +170,9 @@ namespace net {
|
||||
addr_to_url_escaped_string(boost::asio::ip::address address) {
|
||||
address = normalize_address(address);
|
||||
if (address.is_v6()) {
|
||||
return "["s + address.to_string() + ']';
|
||||
std::stringstream ss;
|
||||
ss << '[' << address.to_string() << ']';
|
||||
return ss.str();
|
||||
}
|
||||
else {
|
||||
return address.to_string();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
// standard includes
|
||||
#include <filesystem>
|
||||
#include <utility>
|
||||
|
||||
// lib includes
|
||||
#include <Simple-Web-Server/server_http.hpp>
|
||||
|
||||
@@ -262,7 +262,7 @@ namespace cuda {
|
||||
fs::path sysfs_dir { sysfs_path };
|
||||
for (auto &entry : fs::directory_iterator { sysfs_dir }) {
|
||||
auto file = entry.path().filename();
|
||||
auto filestring = file.generic_u8string();
|
||||
auto filestring = file.generic_string();
|
||||
if (std::string_view { filestring }.substr(0, 4) != "card"sv) {
|
||||
continue;
|
||||
}
|
||||
@@ -1049,4 +1049,4 @@ namespace platf {
|
||||
|
||||
return display_names;
|
||||
}
|
||||
} // namespace platf
|
||||
} // namespace platf
|
||||
|
||||
@@ -1510,7 +1510,7 @@ namespace platf {
|
||||
std::stringstream ss;
|
||||
ss << std::hex << std::setfill('0');
|
||||
for (const auto &ch : str) {
|
||||
ss << ch;
|
||||
ss << static_cast<uint_least32_t>(ch);
|
||||
}
|
||||
|
||||
std::string hex_unicode(ss.str());
|
||||
|
||||
@@ -614,7 +614,7 @@ namespace platf {
|
||||
for (auto &entry : fs::directory_iterator { card_dir }) {
|
||||
auto file = entry.path().filename();
|
||||
|
||||
auto filestring = file.generic_u8string();
|
||||
auto filestring = file.generic_string();
|
||||
if (filestring.size() < 4 || std::string_view { filestring }.substr(0, 4) != "card"sv) {
|
||||
continue;
|
||||
}
|
||||
@@ -1641,7 +1641,7 @@ namespace platf {
|
||||
for (auto &entry : fs::directory_iterator { card_dir }) {
|
||||
auto file = entry.path().filename();
|
||||
|
||||
auto filestring = file.generic_u8string();
|
||||
auto filestring = file.generic_string();
|
||||
if (std::string_view { filestring }.substr(0, 4) != "card"sv) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -421,7 +421,7 @@ namespace platf {
|
||||
}
|
||||
|
||||
if (streamedMonitor != -1) {
|
||||
BOOST_LOG(info) << "Configuring selected monitor ("sv << streamedMonitor << ") to stream"sv;
|
||||
BOOST_LOG(info) << "Configuring selected display ("sv << streamedMonitor << ") to stream"sv;
|
||||
screen_res_t screenr { x11::rr::GetScreenResources(xdisplay.get(), xwindow) };
|
||||
int output = screenr->noutput;
|
||||
|
||||
@@ -806,7 +806,7 @@ namespace platf {
|
||||
return {};
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "Detecting monitors"sv;
|
||||
BOOST_LOG(info) << "Detecting displays"sv;
|
||||
|
||||
x11::xdisplay_t xdisplay { x11::OpenDisplay(nullptr) };
|
||||
if (!xdisplay) {
|
||||
@@ -821,7 +821,7 @@ namespace platf {
|
||||
for (int x = 0; x < output; ++x) {
|
||||
output_info_t out_info { x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) };
|
||||
if (out_info) {
|
||||
BOOST_LOG(info) << "Detected monitor "sv << monitor << ": "sv << out_info->name << ", connected: "sv << (out_info->connection == RR_Connected);
|
||||
BOOST_LOG(info) << "Detected display: "sv << out_info->name << " (id: "sv << monitor << ")"sv << out_info->name << " connected: "sv << (out_info->connection == RR_Connected);
|
||||
++monitor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#pragma once
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
struct CaptureSession {
|
||||
AVCaptureVideoDataOutput *output;
|
||||
@@ -29,6 +30,7 @@ typedef bool (^FrameCallbackBlock)(CMSampleBufferRef);
|
||||
@property (nonatomic, assign) NSMapTable<AVCaptureConnection *, dispatch_semaphore_t> *captureSignals;
|
||||
|
||||
+ (NSArray<NSDictionary *> *)displayNames;
|
||||
+ (NSString *)getDisplayName:(CGDirectDisplayID)displayID;
|
||||
|
||||
- (id)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate;
|
||||
|
||||
|
||||
@@ -23,13 +23,24 @@
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
[result addObject:@{
|
||||
@"id": [NSNumber numberWithUnsignedInt:displays[i]],
|
||||
@"name": [NSString stringWithFormat:@"%d", displays[i]]
|
||||
@"name": [NSString stringWithFormat:@"%d", displays[i]],
|
||||
@"displayName": [self getDisplayName:displays[i]],
|
||||
}];
|
||||
}
|
||||
|
||||
return [NSArray arrayWithArray:result];
|
||||
}
|
||||
|
||||
+ (NSString *)getDisplayName:(CGDirectDisplayID)displayID {
|
||||
NSScreen *screens = [NSScreen screens];
|
||||
for (NSScreen *screen in screens) {
|
||||
if (screen.deviceDescription[@"NSScreenNumber"] == [NSNumber numberWithUnsignedInt:displayID]) {
|
||||
return screen.localizedName;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (id)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate {
|
||||
self = [super init];
|
||||
|
||||
|
||||
@@ -142,18 +142,23 @@ namespace platf {
|
||||
|
||||
auto display = std::make_shared<av_display_t>();
|
||||
|
||||
// Default to main display
|
||||
display->display_id = CGMainDisplayID();
|
||||
if (!display_name.empty()) {
|
||||
auto display_array = [AVVideo displayNames];
|
||||
|
||||
for (NSDictionary *item in display_array) {
|
||||
NSString *name = item[@"name"];
|
||||
if (name.UTF8String == display_name) {
|
||||
NSNumber *display_id = item[@"id"];
|
||||
display->display_id = [display_id unsignedIntValue];
|
||||
}
|
||||
// Print all displays available with it's name and id
|
||||
auto display_array = [AVVideo displayNames];
|
||||
BOOST_LOG(info) << "Detecting displays"sv;
|
||||
for (NSDictionary *item in display_array) {
|
||||
NSNumber *display_id = item[@"id"];
|
||||
// We need show display's product name and corresponding display number given by user
|
||||
NSString *name = item[@"displayName"];
|
||||
// We are using CGGetActiveDisplayList that only returns active displays so hardcoded connected value in log to true
|
||||
BOOST_LOG(info) << "Detected display: "sv << name.UTF8String << " (id: "sv << [NSString stringWithFormat:@"%@", display_id].UTF8String << ") connected: true"sv;
|
||||
if (!display_name.empty() && std::atoi(display_name.c_str()) == [display_id unsignedIntValue]) {
|
||||
display->display_id = [display_id unsignedIntValue];
|
||||
}
|
||||
}
|
||||
BOOST_LOG(info) << "Configuring selected display ("sv << display->display_id << ") to stream"sv;
|
||||
|
||||
display->av_capture = [[AVVideo alloc] initWithDisplay:display->display_id frameRate:config.framerate];
|
||||
|
||||
|
||||
@@ -509,9 +509,28 @@ const KeyCodeMap kKeyCodesMap[] = {
|
||||
|
||||
auto macos_input = (macos_input_t *) result.get();
|
||||
|
||||
// If we don't use the main display in the future, this has to be adapted
|
||||
// Default to main display
|
||||
macos_input->display = CGMainDisplayID();
|
||||
|
||||
auto output_name = config::video.output_name;
|
||||
// If output_name is set, try to find the display with that display id
|
||||
if (!output_name.empty()) {
|
||||
uint32_t max_display = 32;
|
||||
uint32_t display_count;
|
||||
CGDirectDisplayID displays[max_display];
|
||||
if (CGGetActiveDisplayList(max_display, displays, &display_count) != kCGErrorSuccess) {
|
||||
BOOST_LOG(error) << "Unable to get active display list , error: "sv << std::endl;
|
||||
}
|
||||
else {
|
||||
for (int i = 0; i < display_count; i++) {
|
||||
CGDirectDisplayID display_id = displays[i];
|
||||
if (display_id == std::atoi(output_name.c_str())) {
|
||||
macos_input->display = display_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Input coordinates are based on the virtual resolution not the physical, so we need the scaling factor
|
||||
CGDisplayModeRef mode = CGDisplayCopyDisplayMode(macos_input->display);
|
||||
macos_input->displayScaling = ((CGFloat) CGDisplayPixelsWide(macos_input->display)) / ((CGFloat) CGDisplayModeGetPixelWidth(mode));
|
||||
|
||||
@@ -1411,7 +1411,7 @@ namespace platf {
|
||||
ds4_update_state(gamepad_context_t &gamepad, const gamepad_state_t &gamepad_state) {
|
||||
auto &report = gamepad.report.ds4.Report;
|
||||
|
||||
report.wButtons = ds4_buttons(gamepad_state) | ds4_dpad(gamepad_state);
|
||||
report.wButtons = static_cast<uint16_t>(ds4_buttons(gamepad_state)) | static_cast<uint16_t>(ds4_dpad(gamepad_state));
|
||||
report.bSpecial = ds4_special_buttons(gamepad_state);
|
||||
|
||||
report.bTriggerL = gamepad_state.lt;
|
||||
|
||||
@@ -1691,8 +1691,8 @@ namespace platf {
|
||||
}
|
||||
int64_t
|
||||
qpc_counter() {
|
||||
LARGE_INTEGER performace_counter;
|
||||
if (QueryPerformanceCounter(&performace_counter)) return performace_counter.QuadPart;
|
||||
LARGE_INTEGER performance_counter;
|
||||
if (QueryPerformanceCounter(&performance_counter)) return performance_counter.QuadPart;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ extern "C" {
|
||||
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/bind.hpp>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* @brief todo
|
||||
*/
|
||||
#pragma once
|
||||
#include <utility>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
|
||||
61
src_assets/common/assets/web/LoginForm.vue
Normal file
61
src_assets/common/assets/web/LoginForm.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<form @submit.prevent="save">
|
||||
<div class="mb-2">
|
||||
<label for="usernameInput" class="form-label">Username:</label>
|
||||
<input type="text" class="form-control" id="usernameInput" autocomplete="username"
|
||||
v-model="passwordData.username" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="passwordInput" class="form-label">Password:</label>
|
||||
<input type="password" class="form-control" id="passwordInput" autocomplete="new-password"
|
||||
v-model="passwordData.password" required />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100 mb-2" v-bind:disabled="loading">
|
||||
Login
|
||||
</button>
|
||||
<div class="alert alert-danger" v-if="error"><b>Error: </b>{{ error }}</div>
|
||||
<div class="alert alert-success" v-if="success">
|
||||
<b>Success! </b>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
error: null,
|
||||
success: false,
|
||||
loading: false,
|
||||
passwordData: {
|
||||
username: "",
|
||||
password: ""
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
this.error = null;
|
||||
this.loading = true;
|
||||
fetch("/api/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(this.passwordData),
|
||||
}).then((r) => {
|
||||
this.loading = false;
|
||||
if (r.status == 200) {
|
||||
r.json().then((rj) => {
|
||||
if (rj.status.toString() === "true") {
|
||||
this.success = true;
|
||||
this.$emit('loggedin');
|
||||
} else {
|
||||
this.error = rj.error || "Invalid Username or Password";
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.error = "Internal Server Error";
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -29,13 +29,39 @@
|
||||
<a class="nav-link" href="/troubleshooting"><i class="fas fa-fw fa-info"></i> {{ $t('navbar.troubleshoot') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" @click="logout"><i class="fas fa-fw fa-right-from-bracket"></i> Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Modal that is shown when the user gets a 401 error -->
|
||||
<div class="modal fade" id="loginModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="exampleModalLabel">Session Expired</h1>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<LoginForm @loggedin="onLogin" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {Modal} from 'bootstrap';
|
||||
import LoginForm from './LoginForm.vue'
|
||||
export default {
|
||||
components: {
|
||||
LoginForm
|
||||
},
|
||||
data(){
|
||||
modal: null
|
||||
},
|
||||
created() {
|
||||
console.log("Header mounted!")
|
||||
},
|
||||
@@ -45,6 +71,20 @@ export default {
|
||||
let discordWidget = document.createElement('script')
|
||||
discordWidget.setAttribute('src', 'https://app.lizardbyte.dev/js/discord.js')
|
||||
document.head.appendChild(discordWidget)
|
||||
window.addEventListener("sunshine:session_expire", () => {
|
||||
this.modal.toggle();
|
||||
})
|
||||
this.modal = new Modal(document.getElementById('loginModal'), {});
|
||||
},
|
||||
methods: {
|
||||
onLogin(){
|
||||
this.modal.toggle();
|
||||
},
|
||||
logout(){
|
||||
fetch("/api/logout",{method: "POST"}).then(r => {
|
||||
document.location.href = '/';
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -359,6 +359,7 @@
|
||||
import i18n from './locale.js'
|
||||
import Navbar from './Navbar.vue'
|
||||
import {Dropdown} from 'bootstrap'
|
||||
import fetch from './fetch.js'
|
||||
|
||||
const app = createApp({
|
||||
components: {
|
||||
|
||||
@@ -392,19 +392,24 @@
|
||||
<pre>tools\dxgi-info.exe</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3" v-if="platform === 'linux'">
|
||||
<label for="output_name" class="form-label">{{ $t('config.output_name_linux') }}</label>
|
||||
<div class="mb-3" v-if="platform === 'linux' || platform === 'macos'">
|
||||
<label for="output_name" class="form-label">{{ $t('config.output_name_unix') }}</label>
|
||||
<input type="text" class="form-control" id="output_name" placeholder="0" v-model="config.output_name" />
|
||||
<div class="form-text">
|
||||
{{ $t('config.output_name_desc_linux') }}<br>
|
||||
{{ $t('config.output_name_desc_unix') }}<br>
|
||||
<br>
|
||||
<pre style="white-space: pre-line;">
|
||||
Info: Detecting connected monitors
|
||||
Info: Detected monitor 0: DVI-D-0, connected: false
|
||||
Info: Detected monitor 1: HDMI-0, connected: true
|
||||
Info: Detected monitor 2: DP-0, connected: true
|
||||
Info: Detected monitor 3: DP-1, connected: false
|
||||
Info: Detected monitor 4: DVI-D-1, connected: false
|
||||
<pre style="white-space: pre-line;" v-if="platform === 'linux'">
|
||||
Info: Detecting displays
|
||||
Info: Detected display: DVI-D-0 (id: 0) connected: false
|
||||
Info: Detected display: HDMI-0 (id: 1) connected: true
|
||||
Info: Detected display: DP-0 (id: 2) connected: true
|
||||
Info: Detected display: DP-1 (id: 3) connected: false
|
||||
Info: Detected display: DVI-D-1 (id: 4) connected: false
|
||||
</pre>
|
||||
<pre style="white-space: pre-line;" v-if="platform === 'macos'">
|
||||
Info: Detecting displays
|
||||
Info: Detected display: Monitor-0 (id: 3) connected: true
|
||||
Info: Detected display: Monitor-1 (id: 2) connected: true
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1067,6 +1072,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import i18n from './locale.js'
|
||||
import Navbar from './Navbar.vue'
|
||||
import fetch from './fetch.js'
|
||||
|
||||
const app = createApp({
|
||||
components: {
|
||||
|
||||
9
src_assets/common/assets/web/fetch.js
Normal file
9
src_assets/common/assets/web/fetch.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export default async (url,config) => {
|
||||
const response = await fetch(url, config);
|
||||
console.log(response);
|
||||
if(response.status == 401){
|
||||
const event = new Event("sunshine:session_expire");
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
return response;
|
||||
};
|
||||
@@ -76,6 +76,7 @@
|
||||
import i18n from './locale.js'
|
||||
import Navbar from './Navbar.vue'
|
||||
import ResourceCard from './ResourceCard.vue'
|
||||
import fetch from './fetch.js'
|
||||
|
||||
console.log("Hello, Sunshine!")
|
||||
let app = createApp({
|
||||
|
||||
44
src_assets/common/assets/web/login.html
Normal file
44
src_assets/common/assets/web/login.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<%- header %>
|
||||
</head>
|
||||
|
||||
<body id="app">
|
||||
<main role="main" style="max-width: 1200px; margin: 1em auto">
|
||||
<div class="d-flex justify-content-center gap-4">
|
||||
<div class="p-4 card">
|
||||
<header>
|
||||
<h1 class="mb-2">
|
||||
<img src="/images/logo-sunshine-45.png" height="45" alt="" style="vertical-align: bottom;">
|
||||
Hello, Sunshine!
|
||||
</h1>
|
||||
</header>
|
||||
<Login-Form @loggedin="onLogin"></Login-Form>
|
||||
</div>
|
||||
<div>
|
||||
<Resource-Card />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
<script type="module">
|
||||
import { createApp } from "vue"
|
||||
import ResourceCard from './ResourceCard.vue'
|
||||
import LoginForm from './LoginForm.vue'
|
||||
let app = createApp({
|
||||
components: {
|
||||
'ResourceCard': ResourceCard,
|
||||
'LoginForm': LoginForm
|
||||
},
|
||||
methods: {
|
||||
onLogin() {
|
||||
document.location.href = '/';
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log("App", app);
|
||||
app.mount("#app");
|
||||
</script>
|
||||
@@ -69,6 +69,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import i18n from './locale.js'
|
||||
import Navbar from './Navbar.vue'
|
||||
import fetch from './fetch.js'
|
||||
|
||||
const app = createApp({
|
||||
components: {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import i18n from './locale.js'
|
||||
import Navbar from './Navbar.vue'
|
||||
import fetch from './fetch.js'
|
||||
|
||||
let app = createApp({
|
||||
components: {
|
||||
|
||||
@@ -243,9 +243,9 @@
|
||||
"origin_web_ui_allowed_lan": "Only those in LAN may access Web UI",
|
||||
"origin_web_ui_allowed_pc": "Only localhost may access Web UI",
|
||||
"origin_web_ui_allowed_wan": "Anyone may access Web UI",
|
||||
"output_name_desc_linux": "During Sunshine startup, you should see the list of detected monitors. You need to use the value before the colon in the output. e.g.:",
|
||||
"output_name_desc_unix": "During Sunshine startup, you should see the list of detected displays. Note: You need to use the id value inside the parenthesis.",
|
||||
"output_name_desc_win": "Manually specify a display to use for capture. If unset, the primary display is captured. Note: If you specified a GPU above, this display must be connected to that GPU. The appropriate values can be found using the following command:",
|
||||
"output_name_linux": "Monitor number",
|
||||
"output_name_unix": "Display number",
|
||||
"output_name_win": "Output Name",
|
||||
"ping_timeout": "Ping Timeout",
|
||||
"ping_timeout_desc": "How long to wait in milliseconds for data from moonlight before shutting down the stream",
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import i18n from './locale.js'
|
||||
import Navbar from './Navbar.vue'
|
||||
import fetch from './fetch.js'
|
||||
|
||||
const app = createApp({
|
||||
components: {
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
import { createApp } from "vue"
|
||||
import i18n from './locale.js'
|
||||
import ResourceCard from './ResourceCard.vue'
|
||||
import fetch from './fetch.js'
|
||||
|
||||
|
||||
let app = createApp({
|
||||
components: {
|
||||
|
||||
@@ -107,7 +107,7 @@ list(REMOVE_ITEM SUNSHINE_SOURCES ${CMAKE_SOURCE_DIR}/src/main.cpp)
|
||||
add_executable(${PROJECT_NAME}
|
||||
${TEST_SOURCES}
|
||||
${SUNSHINE_SOURCES})
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 17)
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 20)
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
${SUNSHINE_EXTERNAL_LIBRARIES}
|
||||
gtest
|
||||
|
||||
1
third-party/jwt-cpp
vendored
Submodule
1
third-party/jwt-cpp
vendored
Submodule
Submodule third-party/jwt-cpp added at 08bcf77a68
@@ -5,7 +5,7 @@ project(sunshine_tools)
|
||||
include_directories("${CMAKE_SOURCE_DIR}")
|
||||
|
||||
add_executable(dxgi-info dxgi.cpp)
|
||||
set_target_properties(dxgi-info PROPERTIES CXX_STANDARD 17)
|
||||
set_target_properties(dxgi-info PROPERTIES CXX_STANDARD 20)
|
||||
target_link_libraries(dxgi-info
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
dxgi
|
||||
@@ -13,7 +13,7 @@ target_link_libraries(dxgi-info
|
||||
target_compile_options(dxgi-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS})
|
||||
|
||||
add_executable(audio-info audio.cpp)
|
||||
set_target_properties(audio-info PROPERTIES CXX_STANDARD 17)
|
||||
set_target_properties(audio-info PROPERTIES CXX_STANDARD 20)
|
||||
target_link_libraries(audio-info
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
ksuser
|
||||
@@ -21,7 +21,7 @@ target_link_libraries(audio-info
|
||||
target_compile_options(audio-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS})
|
||||
|
||||
add_executable(sunshinesvc sunshinesvc.cpp)
|
||||
set_target_properties(sunshinesvc PROPERTIES CXX_STANDARD 17)
|
||||
set_target_properties(sunshinesvc PROPERTIES CXX_STANDARD 20)
|
||||
target_link_libraries(sunshinesvc
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
wtsapi32
|
||||
@@ -29,7 +29,7 @@ target_link_libraries(sunshinesvc
|
||||
target_compile_options(sunshinesvc PRIVATE ${SUNSHINE_COMPILE_OPTIONS})
|
||||
|
||||
add_executable(ddprobe ddprobe.cpp)
|
||||
set_target_properties(ddprobe PROPERTIES CXX_STANDARD 17)
|
||||
set_target_properties(ddprobe PROPERTIES CXX_STANDARD 20)
|
||||
target_link_libraries(ddprobe
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
dxgi
|
||||
|
||||
@@ -47,6 +47,7 @@ export default defineConfig({
|
||||
input: {
|
||||
apps: resolve(assetsSrcPath, 'apps.html'),
|
||||
config: resolve(assetsSrcPath, 'config.html'),
|
||||
login: resolve(assetsSrcPath, 'login.html'),
|
||||
index: resolve(assetsSrcPath, 'index.html'),
|
||||
password: resolve(assetsSrcPath, 'password.html'),
|
||||
pin: resolve(assetsSrcPath, 'pin.html'),
|
||||
|
||||
Reference in New Issue
Block a user