Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions src/confighttp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#include "platform/common.h"
#include "process.h"
#include "rtsp.h"
#include "stream.h"
#include "utility.h"
#include "uuid.h"

Expand Down Expand Up @@ -441,6 +442,26 @@
return true;
}

/**
* @brief Get the sessions page.
* @param response The HTTP response object.
* @param request The HTTP request object.
*/
void getSessionsPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) {
return;
}

print_req(request);

std::string content = file_handler::read_file(WEB_DIR "sessions.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
headers.emplace("X-Frame-Options", "DENY");
headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
response->write(content, headers);
}

/**
* @brief Get an HTML page.
* @param response The HTTP response object.
Expand Down Expand Up @@ -973,6 +994,88 @@
send_response(response, output_tree);
}

/**
* @brief Get the list of active streaming sessions.
* @param response The HTTP response object.
* @param request The HTTP request object.
*
* @api_examples{/api/sessions| GET| null}
*/
void getSessions(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) {
return;
}

print_req(request);

auto sessions = stream::session::get_all_sessions();

nlohmann::json sessions_json = nlohmann::json::array();
auto now = std::chrono::steady_clock::now();

for (const auto &session : sessions) {
nlohmann::json session_json;
session_json["id"] = session.id;
session_json["client_name"] = session.client_name;
session_json["ip_address"] = session.ip_address;

// Calculate duration in seconds
auto duration = std::chrono::duration_cast<std::chrono::seconds>(now - session.start_time);
session_json["duration_seconds"] = duration.count();

sessions_json.push_back(session_json);
}

nlohmann::json output_tree;
output_tree["sessions"] = sessions_json;
output_tree["status"] = true;
send_response(response, output_tree);
}

/**
* @brief Disconnect an active streaming session.
* @param response The HTTP response object.
* @param request The HTTP request object.
* The body for the post request should be JSON serialized in the following format:
* @code{.json}
* {
* "session_id": "<id>"
* }
* @endcode
*
* @api_examples{/api/sessions/disconnect| POST| {"session_id":"1234"}}
*/
void disconnectSession(resp_https_t response, req_https_t request) {
if (!check_content_type(response, request, "application/json")) {
return;
}
if (!authenticate(response, request)) {
return;
}

print_req(request);

std::stringstream ss;
ss << request->content.rdbuf();

try {
nlohmann::json output_tree;
const nlohmann::json input_tree = nlohmann::json::parse(ss);
const std::string session_id = input_tree.value("session_id", "");

if (session_id.empty()) {
bad_request(response, request, "Missing session_id");
return;
}

output_tree["status"] = stream::session::disconnect(session_id);
send_response(response, output_tree);
} catch (std::exception &e) {
BOOST_LOG(warning) << "DisconnectSession: "sv << e.what();
bad_request(response, request, e.what());
}
}

/**
* @brief Get the configuration settings.
* @param response The HTTP response object.
Expand Down Expand Up @@ -1299,7 +1402,7 @@
}

send_response(response, output_tree);
} catch (std::exception &e) {

Check warning on line 1405 in src/confighttp.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Catch a more specific exception instead of a generic one.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ405vLw55rYU4SfkTDg&open=AZ405vLw55rYU4SfkTDg&pullRequest=5137
BOOST_LOG(warning) << "SavePassword: "sv << e.what();
bad_request(response, request, e.what());
}
Expand Down Expand Up @@ -1753,6 +1856,7 @@
server.resource["^/$"]["GET"] = page_handler("index.html");
server.resource["^/apps/?$"]["GET"] = page_handler("apps.html");
server.resource["^/clients/?$"]["GET"] = page_handler("clients.html");
server.resource["^/sessions/?$"]["GET"] = getSessionsPage;
server.resource["^/config/?$"]["GET"] = page_handler("config.html");
server.resource["^/featured/?$"]["GET"] = page_handler("featured.html");
server.resource["^/logout/?$"]["GET"] = page_handler("logout.html", false);
Expand All @@ -1771,6 +1875,8 @@
server.resource["^/api/clients/unpair$"]["POST"] = unpair;
server.resource["^/api/clients/unpair-all$"]["POST"] = unpairAll;
server.resource["^/api/clients/update$"]["POST"] = updateClient;
server.resource["^/api/sessions$"]["GET"] = getSessions;
server.resource["^/api/sessions/disconnect$"]["POST"] = disconnectSession;
server.resource["^/api/config$"]["GET"] = getConfig;
server.resource["^/api/config$"]["POST"] = saveConfig;
server.resource["^/api/configLocale$"]["GET"] = getLocale;
Expand Down
7 changes: 4 additions & 3 deletions src/nvhttp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,13 @@ namespace nvhttp {
client_root = client;
}

void add_authorized_client(const std::string &name, std::string &&cert) {
void add_authorized_client(const std::string &name, std::string &&cert, const std::string &client_unique_id) {
client_t &client = client_root;
named_cert_t named_cert;
named_cert.name = name;
named_cert.cert = std::move(cert);
named_cert.uuid = uuid_util::uuid_t::generate().string();
// Use the client's uniqueID so we can match it during session lookup
named_cert.uuid = client_unique_id;
client.named_devices.emplace_back(named_cert);

if (!config::sunshine.flags[config::flag::FRESH_STATE]) {
Expand Down Expand Up @@ -488,7 +489,7 @@ namespace nvhttp {
add_cert->raise(crypto::x509(client.cert));

// The client is now successfully paired and will be authorized to connect
add_authorized_client(client.name, std::move(client.cert));
add_authorized_client(client.name, std::move(client.cert), client.uniqueID);
} else {
tree.put("root.paired", 0);
}
Expand Down
115 changes: 115 additions & 0 deletions src/stream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "input.h"
#include "logging.h"
#include "network.h"
#include "nvhttp.h"
#include "platform/common.h"
#include "process.h"
#include "stream.h"
Expand Down Expand Up @@ -405,6 +406,8 @@
} control;

std::uint32_t launch_session_id;
std::string client_unique_id;
std::chrono::steady_clock::time_point start_time;

safe::mail_raw_t::event_t<bool> shutdown_event;
safe::signal_t controlEnd;
Expand Down Expand Up @@ -1985,6 +1988,7 @@
session.audio.peer.port(0);

session.pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout;
session.start_time = std::chrono::steady_clock::now();

session.audioThread = std::thread {audioThread, &session};
session.videoThread = std::thread {videoThread, &session};
Expand All @@ -2009,6 +2013,7 @@

session->shutdown_event = mail->event<bool>(mail::shutdown);
session->launch_session_id = launch_session.id;
session->client_unique_id = launch_session.unique_id;

session->config = config;

Expand Down Expand Up @@ -2073,5 +2078,115 @@

return session;
}

/**
* @brief Get information about all active streaming sessions.
*
* This function retrieves a list of all currently running streaming sessions,
* including client name, IP address, and session start time. It looks up
* friendly client names from the paired clients database.
*
* @return A vector of session_info_t structures describing each active session.
* Returns an empty vector if no sessions are active.
*/
std::vector<session_info_t> get_all_sessions() {
std::vector<session_info_t> result;

// Check if any app is running before trying to access broadcast context
// This avoids triggering broadcast initialization when there's no active stream
if (proc::proc.running() == 0) {
return result;
}

// Get the paired clients to look up names
auto clients_json = nvhttp::get_all_clients();

// Access the broadcast context to get sessions
auto ref = broadcast.ref();
if (!ref) {
return result;
}

auto lg = ref->control_server._sessions.lock();

Check warning on line 2110 in src/stream.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this use of "std::lock_guard" with "std::scoped_lock"

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ405vHE55rYU4SfkTDa&open=AZ405vHE55rYU4SfkTDa&pullRequest=5137
for (auto *session : *ref->control_server._sessions) {
if (session->state.load(std::memory_order_relaxed) != state_e::RUNNING) {

Check failure on line 2112 in src/stream.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'std::memory_order::seq_cst' (or remove this argument to use its default value) to ensure sequential consistency.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ405vHE55rYU4SfkTDb&open=AZ405vHE55rYU4SfkTDb&pullRequest=5137
continue;
}

session_info_t info;

Check warning on line 2116 in src/stream.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Declaration shadows a global variable "info" in the outer scope.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ405vHE55rYU4SfkTDc&open=AZ405vHE55rYU4SfkTDc&pullRequest=5137

// Generate a unique ID from the session's launch_session_id
info.id = std::to_string(session->launch_session_id);

// Look up client name from paired clients list
info.client_name = session->client_unique_id; // Default to unique_id
for (const auto &client : clients_json) {
if (client.contains("uuid") && client["uuid"] == session->client_unique_id) {
if (client.contains("name")) {

Check failure on line 2125 in src/stream.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ405vHE55rYU4SfkTDZ&open=AZ405vHE55rYU4SfkTDZ&pullRequest=5137
info.client_name = client["name"];
}
break;
}
}

// Get IP address from the control peer address
info.ip_address = session->control.expected_peer_address;

// Get start time
info.start_time = session->start_time;

result.push_back(std::move(info));
}

return result;
}

/**
* @brief Disconnect an active streaming session by its ID.
*
* This function allows administrators to forcefully terminate a streaming
* session from the web UI. It finds the session by its launch_session_id
* and calls stop() to cleanly shut it down.
*
* @param session_id The session ID (as a string) to disconnect.
* @return true if the session was found and disconnected, false otherwise.
*/
bool disconnect(const std::string &session_id) {
// Check if any app is running before trying to access broadcast context
if (proc::proc.running() == 0) {
BOOST_LOG(warning) << "No active streaming session to disconnect";
return false;
}

// Convert session_id to launch_session_id
uint32_t launch_id;
try {
launch_id = std::stoul(session_id);
} catch (const std::exception &) {

Check warning on line 2165 in src/stream.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Catch a more specific exception instead of a generic one.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ405vHE55rYU4SfkTDd&open=AZ405vHE55rYU4SfkTDd&pullRequest=5137
BOOST_LOG(warning) << "Invalid session ID: " << session_id;
return false;
}

// Access the broadcast context to find and stop the session
auto ref = broadcast.ref();
if (!ref) {
return false;
}

auto lg = ref->control_server._sessions.lock();

Check warning on line 2176 in src/stream.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this use of "std::lock_guard" with "std::scoped_lock"

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ405vHE55rYU4SfkTDe&open=AZ405vHE55rYU4SfkTDe&pullRequest=5137
for (auto *session : *ref->control_server._sessions) {
if (session->launch_session_id == launch_id) {
if (session->state.load(std::memory_order_relaxed) == state_e::RUNNING) {

Check failure on line 2179 in src/stream.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'std::memory_order::seq_cst' (or remove this argument to use its default value) to ensure sequential consistency.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AZ405vHE55rYU4SfkTDf&open=AZ405vHE55rYU4SfkTDf&pullRequest=5137
BOOST_LOG(info) << "Disconnecting session " << session_id << " by admin request";
stop(*session);
return true;
}
break;
}
}

BOOST_LOG(warning) << "Session not found or not running: " << session_id;
return false;
}
} // namespace session
} // namespace stream
23 changes: 23 additions & 0 deletions src/stream.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,33 @@ namespace stream {
RUNNING, ///< The session is running
};

/**
* @brief Information about an active streaming session.
*/
struct session_info_t {
std::string id; ///< Unique session identifier
std::string client_name; ///< Name of the connected client
std::string ip_address; ///< Client's IP address
std::chrono::steady_clock::time_point start_time; ///< When the session started
};

std::shared_ptr<session_t> alloc(config_t &config, rtsp_stream::launch_session_t &launch_session);
int start(session_t &session, const std::string &addr_string);
void stop(session_t &session);
void join(session_t &session);
state_e state(session_t &session);

/**
* @brief Get a list of all active streaming sessions.
* @return Vector of session information.
*/
std::vector<session_info_t> get_all_sessions();

/**
* @brief Disconnect a session by its ID.
* @param session_id The unique session identifier.
* @return true if the session was found and stopped, false otherwise.
*/
bool disconnect(const std::string &session_id);
} // namespace session
} // namespace stream
6 changes: 6 additions & 0 deletions src_assets/common/assets/web/Navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
{{ $t('navbar.applications') }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="./sessions">
<Layers :size="18" class="icon"></Layers>
{{ $t('navbar.sessions') }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="./featured">
<Star :size="18" class="icon"></Star>
Expand Down
17 changes: 17 additions & 0 deletions src_assets/common/assets/web/public/assets/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@
"logout": "Log Out",
"password": "Change Password",
"pin": "PIN",
"sessions": "Sessions",
"theme_auto": "Auto",
"theme_dark": "Dark",
"theme_ember": "Ember",
Expand Down Expand Up @@ -478,6 +479,22 @@
"resources_desc": "Resources for Sunshine!",
"third_party_notice": "Third Party Notice"
},
"sessions": {
"actions": "Actions",
"auto_refresh": "Auto-refresh",
"cancel": "Cancel",
"client": "Client",
"confirm_disconnect": "Are you sure you want to disconnect",
"confirm_disconnect_title": "Confirm Disconnect",
"disconnect": "Disconnect",
"disconnect_error": "Failed to disconnect session",
"disconnected": "Session disconnected successfully",
"duration": "Duration",
"ip_address": "IP Address",
"no_sessions": "No active streaming sessions",
"refresh": "Refresh",
"title": "Active Sessions"
},
"troubleshooting": {
"dd_reset": "Reset Persistent Display Device Settings",
"dd_reset_desc": "If Sunshine is stuck trying to restore the changed display device settings, you can reset the settings and proceed to restore the display state manually.",
Expand Down
Loading