194 lines
8.0 KiB
C++
194 lines
8.0 KiB
C++
#include "spore/core/ApiServer.h"
|
|
#include "spore/Service.h"
|
|
#include "spore/util/Logging.h"
|
|
#include <algorithm>
|
|
|
|
const char* ApiServer::methodToStr(int method) {
|
|
switch (method) {
|
|
case HTTP_GET: return "GET";
|
|
case HTTP_POST: return "POST";
|
|
case HTTP_PUT: return "PUT";
|
|
case HTTP_DELETE: return "DELETE";
|
|
case HTTP_PATCH: return "PATCH";
|
|
default: return "UNKNOWN";
|
|
}
|
|
}
|
|
|
|
ApiServer::ApiServer(NodeContext& ctx, TaskManager& taskMgr, uint16_t port) : server(port), ctx(ctx), taskManager(taskMgr) {}
|
|
|
|
void ApiServer::registerEndpoint(const String& uri, int method,
|
|
const std::vector<ParamSpec>& params,
|
|
const String& serviceName) {
|
|
// Add to local endpoints
|
|
endpoints.push_back(EndpointInfo{uri, method, params, serviceName, true});
|
|
|
|
// Update cluster if needed
|
|
if (ctx.memberList && !ctx.memberList->empty()) {
|
|
auto it = ctx.memberList->find(ctx.hostname);
|
|
if (it != ctx.memberList->end()) {
|
|
it->second.endpoints.push_back(EndpointInfo{uri, method, params, serviceName, true});
|
|
}
|
|
}
|
|
}
|
|
|
|
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler) {
|
|
// Get current service name if available
|
|
String serviceName = "unknown";
|
|
if (!services.empty()) {
|
|
serviceName = services.back().get().getName();
|
|
}
|
|
registerEndpoint(uri, method, {}, serviceName);
|
|
server.on(uri.c_str(), method, requestHandler);
|
|
}
|
|
|
|
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
|
|
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler) {
|
|
// Get current service name if available
|
|
String serviceName = "unknown";
|
|
if (!services.empty()) {
|
|
serviceName = services.back().get().getName();
|
|
}
|
|
registerEndpoint(uri, method, {}, serviceName);
|
|
server.on(uri.c_str(), method, requestHandler, uploadHandler);
|
|
}
|
|
|
|
// Overloads that also record minimal capability specs
|
|
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
|
|
const std::vector<ParamSpec>& params) {
|
|
// Get current service name if available
|
|
String serviceName = "unknown";
|
|
if (!services.empty()) {
|
|
serviceName = services.back().get().getName();
|
|
}
|
|
registerEndpoint(uri, method, params, serviceName);
|
|
server.on(uri.c_str(), method, requestHandler);
|
|
}
|
|
|
|
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
|
|
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler,
|
|
const std::vector<ParamSpec>& params) {
|
|
// Get current service name if available
|
|
String serviceName = "unknown";
|
|
if (!services.empty()) {
|
|
serviceName = services.back().get().getName();
|
|
}
|
|
registerEndpoint(uri, method, params, serviceName);
|
|
server.on(uri.c_str(), method, requestHandler, uploadHandler);
|
|
}
|
|
|
|
void ApiServer::addService(Service& service) {
|
|
services.push_back(service);
|
|
LOG_INFO("API", "Added service: " + String(service.getName()));
|
|
}
|
|
|
|
void ApiServer::serveStatic(const String& uri, fs::FS& fs, const String& path, const String& cache_header) {
|
|
server.serveStatic(uri.c_str(), fs, path.c_str(), cache_header.c_str()).setDefaultFile("index.html");
|
|
LOG_INFO("API", "Registered static file serving: " + uri + " -> " + path);
|
|
}
|
|
|
|
void ApiServer::begin() {
|
|
// Setup streaming API (WebSocket)
|
|
setupWebSocket();
|
|
server.addHandler(&ws);
|
|
|
|
// Register all service endpoints
|
|
for (auto& service : services) {
|
|
service.get().registerEndpoints(*this);
|
|
LOG_INFO("API", "Registered endpoints for service: " + String(service.get().getName()));
|
|
}
|
|
|
|
server.begin();
|
|
}
|
|
|
|
void ApiServer::setupWebSocket() {
|
|
ws.onEvent([this](AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) {
|
|
if (type == WS_EVT_DATA) {
|
|
AwsFrameInfo* info = (AwsFrameInfo*)arg;
|
|
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
|
|
// Parse directly from the raw buffer with explicit length
|
|
JsonDocument doc;
|
|
DeserializationError err = deserializeJson(doc, (const char*)data, len);
|
|
if (!err) {
|
|
LOG_DEBUG("API", "Received event: " + String(doc["event"].as<String>()));
|
|
String eventName = doc["event"].as<String>();
|
|
String payloadStr;
|
|
if (doc["payload"].is<const char*>()) {
|
|
payloadStr = doc["payload"].as<const char*>();
|
|
} else if (!doc["payload"].isNull()) {
|
|
// If payload is an object/array, serialize it
|
|
String tmp; serializeJson(doc["payload"], tmp); payloadStr = tmp;
|
|
}
|
|
// Allow empty payload; services may treat it as defaults
|
|
if (eventName.length() > 0) {
|
|
// Inject origin tag into payload JSON if possible
|
|
String enriched = payloadStr;
|
|
if (payloadStr.length() > 0) {
|
|
JsonDocument pd;
|
|
if (!deserializeJson(pd, payloadStr)) {
|
|
pd["_origin"] = String("ws:") + String(client->id());
|
|
String tmp; serializeJson(pd, tmp); enriched = tmp;
|
|
} else {
|
|
// If payload is plain string, leave as-is (no origin)
|
|
}
|
|
}
|
|
std::string ev = eventName.c_str();
|
|
ctx.fire(ev, &enriched);
|
|
// Acknowledge
|
|
client->text("{\"ok\":true}");
|
|
} else {
|
|
client->text("{\"error\":\"Missing 'event'\"}");
|
|
}
|
|
} else {
|
|
client->text("{\"error\":\"Invalid JSON\"}");
|
|
}
|
|
}
|
|
} else if (type == WS_EVT_CONNECT) {
|
|
client->text("{\"hello\":\"ws connected\"}");
|
|
wsClients.push_back(client);
|
|
} else if (type == WS_EVT_DISCONNECT) {
|
|
wsClients.erase(std::remove(wsClients.begin(), wsClients.end(), client), wsClients.end());
|
|
}
|
|
});
|
|
|
|
// Subscribe to all local events and forward to websocket clients
|
|
ctx.onAny([this](const std::string& event, void* dataPtr) {
|
|
// Ignore raw UDP frames
|
|
if (event == "udp/raw") {
|
|
return;
|
|
}
|
|
|
|
String* payloadStrPtr = static_cast<String*>(dataPtr);
|
|
String payloadStr = payloadStrPtr ? *payloadStrPtr : String("");
|
|
|
|
// Extract and strip origin if present
|
|
String origin;
|
|
String cleanedPayload = payloadStr;
|
|
if (payloadStr.length() > 0) {
|
|
JsonDocument pd;
|
|
if (!deserializeJson(pd, payloadStr)) {
|
|
if (pd["_origin"].is<const char*>()) {
|
|
origin = pd["_origin"].as<const char*>();
|
|
pd.remove("_origin");
|
|
String tmp; serializeJson(pd, tmp); cleanedPayload = tmp;
|
|
}
|
|
}
|
|
}
|
|
|
|
JsonDocument outDoc;
|
|
outDoc["event"] = event.c_str();
|
|
outDoc["payload"] = cleanedPayload;
|
|
String out; serializeJson(outDoc, out);
|
|
|
|
if (origin.startsWith("ws:")) {
|
|
uint32_t originId = (uint32_t)origin.substring(3).toInt();
|
|
for (auto* c : wsClients) {
|
|
if (c && c->id() != originId) {
|
|
c->text(out);
|
|
}
|
|
}
|
|
} else {
|
|
ws.textAll(out);
|
|
}
|
|
});
|
|
}
|