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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
.DS_Store
obj/*.o

.github/copilot-instructions.md

memory-bank
old-files
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 or player ID.

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

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

**Parameters:**
- `publisher_id` (path) - Publisher ID **or** player ID associated with the stream

**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/live_stream
```

### 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_
54 changes: 49 additions & 5 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 @@ -196,6 +196,11 @@ int CSLSManager::start()
}

char* CSLSManager::find_publisher_by_player_key(char *player_key) {
Copy link
Copy Markdown
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?

if (player_key == NULL || strlen(player_key) == 0) {
sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::find_publisher_by_player_key, empty player key provided", this);
return NULL;
}

// First check stream ID database
std::string publisher_id = CSLSDatabase::getInstance().getPublisherFromPlayer(player_key);
if (!publisher_id.empty()) {
Expand All @@ -205,10 +210,10 @@ char* CSLSManager::find_publisher_by_player_key(char *player_key) {

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++) {
Expand All @@ -217,13 +222,13 @@ char* CSLSManager::find_publisher_by_player_key(char *player_key) {
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;
Expand Down Expand Up @@ -288,6 +293,45 @@ json CSLSManager::generate_json_for_publisher(std::string playerKey, int clear,
return ret;
}

bool CSLSManager::disconnect_publisher(const std::string& key) {
if (key.empty()) {
sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::disconnect_publisher, empty key provided", this);
return false;
}

char* mapped_publisher = find_publisher_by_player_key(const_cast<char*>(key.c_str()));
if (mapped_publisher == NULL) {
sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::disconnect_publisher, unable to resolve publisher for key: %s", this, key.c_str());
return false;
}

std::string publisher_key(mapped_publisher);

// Search for the publisher in all server instances using resolved publisher key
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());
if (role != nullptr) {
break;
}
}
if (role == nullptr) {
sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::disconnect_publisher, publisher not found for key: %s (resolved publisher: %s)",
this, key.c_str(), publisher_key.c_str());
return false;
}

// Disconnect the publisher
sls_log(SLS_LOG_INFO, "[%p]CSLSManager::disconnect_publisher, disconnecting publisher: %s (requested key: %s)",
this, publisher_key.c_str(), 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
6 changes: 4 additions & 2 deletions slscore/SLSManager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ public :
bool is_single_thread();
int check_invalid();

json generate_json_for_publisher(std::string publisherName, int clear, bool legacy = false);
json generate_json_for_publisher(std::string playerKey, 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& key);

void get_stat_info(std::string &info);
static int stat_client_callback(void *p, HTTP_CALLBACK_TYPE type, void *v, void* context);
Expand All @@ -118,6 +118,8 @@ public :

CSLSRoleList * m_list_role;
CSLSGroup * m_single_group;

char* find_publisher_by_player_key(char *player_key);
};


Expand Down
25 changes: 19 additions & 6 deletions slscore/conf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,25 @@ vector<string> sls_conf_string_split(const string& str, const string& delim)

string& trim(string &s)
{
if (s.empty())
{
if (s.empty()) {
return s;
}

const char *whitespace = " \t\r\n";

size_t start = s.find_first_not_of(whitespace);
if (start == string::npos) {
s.clear();
return s;
}
s.erase(0,s.find_first_not_of(" "));
s.erase(s.find_last_not_of(" ") + 1);
if (start > 0) {
s.erase(0, start);
}

size_t end = s.find_last_not_of(whitespace);
if (end != string::npos && end + 1 < s.size()) {
s.erase(end + 1);
}
return s;
}

Expand Down Expand Up @@ -306,7 +319,7 @@ int sls_conf_parse_block(ifstream& ifs, int& line, sls_conf_base_t * b, bool& ch
}
} else if (line_end_flag == "}" ) {
if (str_line != line_end_flag) {
sls_log(SLS_LOG_ERROR, "line:%d=‘%s’, end indicator ‘}’ with more info.", str_line.c_str(), line);
sls_log(SLS_LOG_ERROR, "line:%d=‘%s’, end indicator ‘}’ with more info.", line, str_line.c_str());
ret = SLS_ERROR;
break;
}
Expand All @@ -316,7 +329,7 @@ int sls_conf_parse_block(ifstream& ifs, int& line, sls_conf_base_t * b, bool& ch
break;

} else {
sls_log(SLS_LOG_ERROR, "line:%d='%s', invalid end flag, except ';', '{', '}',", str_line.c_str(), line);
sls_log(SLS_LOG_ERROR, "line:%d='%s', invalid end flag, except ';', '{', '}',", line, str_line.c_str());
ret = SLS_ERROR;
break;
}
Expand Down