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 @@ -33,6 +33,7 @@
#include "nvhttp.h"
#include "platform/common.h"
#include "process.h"
#include "stream.h"
#include "utility.h"
#include "uuid.h"

Expand Down Expand Up @@ -337,6 +338,26 @@ namespace confighttp {
response->write(content, headers);
}

/**
* @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 the configuration page.
* @param response The HTTP response object.
Expand Down Expand Up @@ -825,6 +846,88 @@ namespace confighttp {
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 @@ -1195,6 +1298,7 @@ namespace confighttp {
server.resource["^/pin/?$"]["GET"] = getPinPage;
server.resource["^/apps/?$"]["GET"] = getAppsPage;
server.resource["^/clients/?$"]["GET"] = getClientsPage;
server.resource["^/sessions/?$"]["GET"] = getSessionsPage;
server.resource["^/config/?$"]["GET"] = getConfigPage;
server.resource["^/password/?$"]["GET"] = getPasswordPage;
server.resource["^/welcome/?$"]["GET"] = getWelcomePage;
Expand All @@ -1213,6 +1317,8 @@ namespace confighttp {
server.resource["^/api/clients/unpair-all$"]["POST"] = unpairAll;
server.resource["^/api/clients/list$"]["GET"] = getClients;
server.resource["^/api/clients/unpair$"]["POST"] = unpair;
server.resource["^/api/sessions$"]["GET"] = getSessions;
server.resource["^/api/sessions/disconnect$"]["POST"] = disconnectSession;
server.resource["^/api/apps/close$"]["POST"] = closeApp;
server.resource["^/api/covers/upload$"]["POST"] = uploadCover;
server.resource["^/images/sunshine.ico$"]["GET"] = getFaviconImage;
Expand Down
7 changes: 4 additions & 3 deletions src/nvhttp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -266,12 +266,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 @@ -485,7 +486,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 @@ extern "C" {
#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 @@ namespace stream {
} 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 @@ -1979,6 +1982,7 @@ namespace stream {
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 @@ -2003,6 +2007,7 @@ namespace stream {

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 @@ -2067,5 +2072,115 @@ namespace stream {

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();
for (auto *session : *ref->control_server._sessions) {
if (session->state.load(std::memory_order_relaxed) != state_e::RUNNING) {
continue;
}

session_info_t info;

// 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")) {
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 &) {
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();
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) {
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
3 changes: 3 additions & 0 deletions src_assets/common/assets/web/Navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
<li class="nav-item">
<a class="nav-link" href="./apps"><i class="fas fa-fw fa-stream"></i> {{ $t('navbar.applications') }}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="./sessions"><i class="fas fa-fw fa-satellite-dish"></i> {{ $t('navbar.sessions') }}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="./config"><i class="fas fa-fw fa-cog"></i> {{ $t('navbar.configuration') }}</a>
</li>
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 @@ -396,6 +396,7 @@
"home": "Home",
"password": "Change Password",
"pin": "PIN",
"sessions": "Sessions",
"theme_auto": "Auto",
"theme_dark": "Dark",
"theme_light": "Light",
Expand Down Expand Up @@ -428,6 +429,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