Compare commits

...

8 Commits

Author SHA1 Message Date
TheElixZammuto
3888ec8da0 Merge branch 'nightly' into feat/new-session-system 2024-04-27 15:09:45 +02:00
Tejas Rao
7fb8c76590 Use C++20. (#2322) 2024-04-26 15:49:15 -04:00
TimmyOVO
9288775351 feat(macos/capture): support for capture display other than main display (#2449) 2024-04-22 14:16:26 -04:00
Elia Zammuto
8ba64ffa32 Made C++ format Happy 2024-03-17 16:33:30 +01:00
Elia Zammuto
968b7963ee Migrated jwt-cpp to stable release, style fixes 2024-03-17 16:33:30 +01:00
Elia Zammuto
61df838356 clang-format 2024-03-17 16:33:30 +01:00
Elia Zammuto
d1845df0ea Logout 2024-03-17 16:33:30 +01:00
Elia Zammuto
9ef63ca829 First Working Draft of JWT Login System 2024-03-17 16:33:30 +01:00
41 changed files with 435 additions and 69 deletions

View File

@@ -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
View File

@@ -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

View File

@@ -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}
)

View File

@@ -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})

View File

@@ -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)

View File

@@ -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})

View File

@@ -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

View File

@@ -9,6 +9,7 @@
#include <iostream>
#include <thread>
#include <unordered_map>
#include <utility>
#include <boost/asio.hpp>
#include <boost/filesystem.hpp>

View File

@@ -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;

View File

@@ -7,6 +7,7 @@
#include "process.h"
#include <filesystem>
#include <utility>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>

View File

@@ -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();

View File

@@ -5,6 +5,7 @@
#pragma once
#include <tuple>
#include <utility>
#include <boost/asio.hpp>

View File

@@ -8,6 +8,7 @@
// standard includes
#include <filesystem>
#include <utility>
// lib includes
#include <Simple-Web-Server/server_http.hpp>

View File

@@ -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

View File

@@ -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());

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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];

View File

@@ -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];

View File

@@ -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));

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -11,6 +11,7 @@ extern "C" {
#include <array>
#include <cctype>
#include <utility>
#include <boost/asio.hpp>
#include <boost/bind.hpp>

View File

@@ -3,6 +3,7 @@
* @brief todo
*/
#pragma once
#include <utility>
#include <boost/asio.hpp>

View 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>

View File

@@ -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>

View File

@@ -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: {

View File

@@ -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: {

View 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;
};

View File

@@ -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({

View 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>

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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",

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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

Submodule third-party/jwt-cpp added at 08bcf77a68

View File

@@ -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

View File

@@ -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'),