Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"workbench.colorTheme": "Visual Studio Dark"
}
59 changes: 59 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,47 @@ Authorization: Bearer <API_KEY>
}
```

### Publisher Management


#### Disconnect Publisher

Disconnect an active publishing operation by publisher ID.

```
DELETE /api/disconnect/{publisher_id}
Authorization: Bearer <API_KEY>
```

**Required Permissions:** `admin` or `write`

**Parameters:**
- `publisher_id` (path) - The publisher ID to disconnect

**Response:**
- `200 OK` - Publisher disconnected successfully
```json
{
"status": "success",
"message": "Publisher disconnected successfully"
}
```
- `401 Unauthorized` - Invalid or missing API key
- `403 Forbidden` - Insufficient permissions
- `404 Not Found` - Publisher not found or not currently streaming
```json
{
"status": "error",
"message": "Publisher not found or not currently streaming"
}
```

**Example:**
```bash
curl -X DELETE -H "Authorization: Bearer YOUR_API_KEY" \
http://hostname:8080/api/disconnect/publisher_123
```

### Statistics

#### Get Publisher Statistics
Expand Down Expand Up @@ -209,6 +250,7 @@ The API implements rate limiting to prevent abuse. Each endpoint type has its ow
- GET /api/stream-ids
- POST /api/stream-ids
- DELETE /api/stream-ids/{player_id}
- DELETE /api/disconnect/{player_id}
- **Statistics** (`stats`): 300 requests per minute per IP (configurable via `rate_limit_stats`)
- GET /stats/{player_id}
- **Configuration** (`config`): 20 requests per minute per IP (configurable via `rate_limit_config`)
Expand Down Expand Up @@ -319,6 +361,10 @@ curl -X POST -H "Authorization: Bearer YOUR_API_KEY" \
# Get statistics
curl -H "Authorization: Bearer YOUR_API_KEY" \
http://hostname:8080/stats/live_stream

# Disconnect publisher
curl -X DELETE -H "Authorization: Bearer YOUR_API_KEY" \
http://hostname:8080/api/disconnect/live_stream
```

### Python
Expand All @@ -342,6 +388,9 @@ data = {
"description": "Main studio feed"
}
response = requests.post(f"{BASE_URL}/api/stream-ids", json=data, headers=headers)

# Disconnect publisher
response = requests.delete(f"{BASE_URL}/api/disconnect/live_stream", headers=headers)
```

### JavaScript/Fetch
Expand Down Expand Up @@ -374,6 +423,16 @@ fetch(`${BASE_URL}/api/stream-ids`, {
})
.then(response => response.json())
.then(data => console.log(data));

// Disconnect publisher
fetch(`${BASE_URL}/api/disconnect/live_stream`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${API_KEY}`
}
})
.then(response => response.json())
.then(data => console.log(data));
```

## Security Considerations
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ ENV LD_LIBRARY_PATH=/lib:/usr/lib:/usr/local/lib64

# Install runtime dependencies
RUN apk update \
&& apk add --no-cache openssl libstdc++ supervisor coreutils procps net-tools sqlite sqlite-dev \
&& apk add --no-cache openssl libstdc++ supervisor coreutils procps net-tools sqlite sqlite-dev spdlog \
&& rm -rf /var/cache/apk/*

# Copy binaries from the builder stage
Expand Down
67 changes: 67 additions & 0 deletions slscore/SLSApiServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ void CSLSApiServer::setupEndpoints() {
m_server.Post("/api/keys", [this](const httplib::Request& req, httplib::Response& res) {
handleApiKeys(req, res);
});

// Publisher disconnect endpoint
m_server.Delete(R"(/api/disconnect/(.+))", [this](const httplib::Request& req, httplib::Response& res) {
handleDisconnectPublisher(req, res);
});
}

void CSLSApiServer::handleHealth(const httplib::Request& req, httplib::Response& res) {
Expand Down Expand Up @@ -440,4 +445,66 @@ void CSLSApiServer::handleApiKeys(const httplib::Request& req, httplib::Response
error["message"] = "Failed to create API key";
res.set_content(error.dump(), "application/json");
}
}

void CSLSApiServer::handleDisconnectPublisher(const httplib::Request& req, httplib::Response& res) {
setCorsHeaders(res);

// Rate limiting
if (!checkRateLimit(req.remote_addr, "api")) {
res.status = 429;
json error;
error["status"] = "error";
error["message"] = "Rate limit exceeded";
res.set_content(error.dump(), "application/json");
return;
}

// Authentication with admin or write permissions check
std::string permissions;
if (!authenticateRequest(req, res, permissions)) {
return;
}

if (permissions != "admin" && permissions != "write") {
res.status = 403;
json error;
error["status"] = "error";
error["message"] = "Admin or write permissions required";
res.set_content(error.dump(), "application/json");
CSLSDatabase::getInstance().logAccess(req.get_header_value("Authorization").substr(7),
req.path, req.method, req.remote_addr, 403);
return;
}

std::string publisher_id = req.matches[1];

if (!m_sls_manager) {
res.status = 500;
json error;
error["status"] = "error";
error["message"] = "SLS manager not available";
res.set_content(error.dump(), "application/json");
return;
}

// Attempt to disconnect the publisher
if (m_sls_manager->disconnect_publisher(publisher_id)) {
json response;
response["status"] = "success";
response["message"] = "Publisher disconnected successfully";
res.set_content(response.dump(), "application/json");

CSLSDatabase::getInstance().logAccess(req.get_header_value("Authorization").substr(7),
req.path, req.method, req.remote_addr, 200);
} else {
res.status = 404;
json error;
error["status"] = "error";
error["message"] = "Publisher not found or not currently streaming";
res.set_content(error.dump(), "application/json");

CSLSDatabase::getInstance().logAccess(req.get_header_value("Authorization").substr(7),
req.path, req.method, req.remote_addr, 404);
}
}
1 change: 1 addition & 0 deletions slscore/SLSApiServer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ class CSLSApiServer {
void handleStats(const httplib::Request& req, httplib::Response& res);
void handleConfig(const httplib::Request& req, httplib::Response& res);
void handleApiKeys(const httplib::Request& req, httplib::Response& res);
void handleDisconnectPublisher(const httplib::Request& req, httplib::Response& res);
};

#endif // _SLS_API_SERVER_HPP_
88 changes: 34 additions & 54 deletions slscore/SLSManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ CSLSManager::CSLSManager()
m_map_data = NULL;
m_map_publisher = NULL;
m_map_puller = NULL;
m_map_pusher = NULL;

m_map_pusher = NULL;
}

CSLSManager::~CSLSManager()
Expand Down Expand Up @@ -195,62 +195,19 @@ int CSLSManager::start()

}

char* CSLSManager::find_publisher_by_player_key(char *player_key) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as in line 244: we need the player to receive our stats, so why have you deleted the code?

// First check stream ID database
std::string publisher_id = CSLSDatabase::getInstance().getPublisherFromPlayer(player_key);
if (!publisher_id.empty()) {
static thread_local char mapped_publisher[512];
strncpy(mapped_publisher, publisher_id.c_str(), sizeof(mapped_publisher) - 1);
mapped_publisher[sizeof(mapped_publisher) - 1] = '\0';

return mapped_publisher;
}

sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::find_publisher_by_player_key, player key '%s' not found in database",
this, player_key);

// If not found in database, check if it's a direct publisher key
CSLSRole* role = nullptr;
for (int i = 0; i < m_server_count; i++) {
role = m_map_publisher[i].get_publisher(player_key);
if (role != nullptr) {
break;
}
}

if (role != NULL) {
sls_log(SLS_LOG_INFO, "[%p]CSLSManager::find_publisher_by_player_key, player key '%s' is a publisher key",
this, player_key);
return player_key;
}

sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::find_publisher_by_player_key, no publisher found for player key '%s'",
this, player_key);
return NULL;
}

json CSLSManager::generate_json_for_publisher(std::string playerKey, int clear, bool legacy) {
// ...existing code...
json CSLSManager::generate_json_for_publisher(std::string publisher_id, int clear, bool legacy) {
json ret;
ret["status"] = "error";

// Validate input
if (playerKey.empty()) {
ret["message"] = "Player key is required";
sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::generate_json_for_publisher, empty player key provided", this);
if (publisher_id.empty()) {
ret["message"] = "Publisher ID is required";
sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::generate_json_for_publisher, empty publisher_id provided", this);
return ret;
}

// Validate player key and get mapped publisher key
char* mapped_publisher = find_publisher_by_player_key(const_cast<char*>(playerKey.c_str()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need the player to receive our stats, so why have you deleted the code?

if (mapped_publisher == NULL) {
ret["message"] = "Invalid player key";
sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::generate_json_for_publisher, invalid player key: %s",
this, playerKey.c_str());
return ret;
}

std::string publisher_key(mapped_publisher);

if (legacy) {
ret["publishers"] = json::object();
}
Expand All @@ -261,16 +218,16 @@ json CSLSManager::generate_json_for_publisher(std::string playerKey, int clear,
CSLSRole *role = nullptr;
for (int i = 0; i < m_server_count; i++) {
CSLSMapPublisher *publisher_map = &m_map_publisher[i];
role = publisher_map->get_publisher(publisher_key.c_str());
role = publisher_map->get_publisher(publisher_id.c_str());
if (role != nullptr) {
break;
}
}

if (role == nullptr) {
ret["message"] = "Publisher is currently not streaming";
sls_log(SLS_LOG_DEBUG, "[%p]CSLSManager::generate_json_for_publisher, publisher not found: %s (mapped from player key: %s)",
this, publisher_key.c_str(), playerKey.c_str());
sls_log(SLS_LOG_DEBUG, "[%p]CSLSManager::generate_json_for_publisher, publisher not found: %s",
this, publisher_id.c_str());
return ret;
}

Expand All @@ -282,12 +239,35 @@ json CSLSManager::generate_json_for_publisher(std::string playerKey, int clear,
}
ret.erase("message");

sls_log(SLS_LOG_DEBUG, "[%p]CSLSManager::generate_json_for_publisher, returning %s stats for publisher: %s (player key: %s)",
this, legacy ? "legacy" : "modern", publisher_key.c_str(), playerKey.c_str());
sls_log(SLS_LOG_DEBUG, "[%p]CSLSManager::generate_json_for_publisher, returning %s stats for publisher: %s",
this, legacy ? "legacy" : "modern", publisher_id.c_str());

return ret;
}

bool CSLSManager::disconnect_publisher(const std::string& player_key) {
// Search for the publisher in all server instances using publisher_id
CSLSRole *role = nullptr;
for (int i = 0; i < m_server_count; i++) {
CSLSMapPublisher *publisher_map = &m_map_publisher[i];
role = publisher_map->get_publisher(player_key); // player_key artık publisher_id olarak kullanılıyor
if (role != nullptr) {
break;
}
}
if (role == nullptr) {
sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::disconnect_publisher, publisher not found for publisher_id: %s", this, player_key.c_str());
return false;
}
// Disconnect the publisher
sls_log(SLS_LOG_INFO, "[%p]CSLSManager::disconnect_publisher, disconnecting publisher: %s", this, player_key.c_str());
// Call on_close to notify any HTTP callbacks
role->on_close();
// Mark the role as invalid to trigger cleanup in the next cycle
role->invalid_srt();
return true;
}

json CSLSManager::create_legacy_json_stats_for_publisher(CSLSRole *role, int clear) {
json ret = json::object();
SRT_TRACEBSTATS stats;
Expand Down
2 changes: 1 addition & 1 deletion slscore/SLSManager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public :
json generate_json_for_publisher(std::string publisherName, int clear, bool legacy = false);
json create_legacy_json_stats_for_publisher(CSLSRole *role, int clear);
json create_json_stats_for_publisher(CSLSRole *role, int clear);
char* find_publisher_by_player_key(char *player_key);
bool disconnect_publisher(const std::string& publisher_id);

void get_stat_info(std::string &info);
static int stat_client_callback(void *p, HTTP_CALLBACK_TYPE type, void *v, void* context);
Expand Down