2025-08-03 21:53:15 -04:00
|
|
|
#include <drogon/drogon.h>
|
|
|
|
|
#include <drogon/HttpAppFramework.h>
|
|
|
|
|
#include "controllers/StreamController.h"
|
|
|
|
|
#include "controllers/UserController.h"
|
|
|
|
|
#include "controllers/AdminController.h"
|
|
|
|
|
#include "controllers/RealmController.h"
|
2026-01-10 01:50:20 -05:00
|
|
|
#include "controllers/RestreamController.h"
|
2025-08-03 21:53:15 -04:00
|
|
|
#include "services/DatabaseService.h"
|
|
|
|
|
#include "services/StatsService.h"
|
|
|
|
|
#include "services/AuthService.h"
|
2026-01-05 22:54:27 -05:00
|
|
|
#include "services/CensorService.h"
|
|
|
|
|
#include "services/TreasuryService.h"
|
2025-08-03 21:53:15 -04:00
|
|
|
#include <exception>
|
|
|
|
|
#include <csignal>
|
|
|
|
|
#include <sys/stat.h>
|
|
|
|
|
|
|
|
|
|
using namespace drogon;
|
|
|
|
|
|
|
|
|
|
int main() {
|
|
|
|
|
// Simplified signal handlers
|
|
|
|
|
signal(SIGSEGV, [](int s){ LOG_ERROR << "Signal " << s; exit(s); });
|
|
|
|
|
signal(SIGABRT, [](int s){ LOG_ERROR << "Signal " << s; exit(s); });
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
LOG_INFO << "Starting streaming backend server...";
|
|
|
|
|
|
|
|
|
|
// Create upload directories
|
|
|
|
|
mkdir("./uploads", 0755);
|
|
|
|
|
mkdir("./uploads/avatars", 0755);
|
|
|
|
|
|
|
|
|
|
// Initialize DatabaseService
|
|
|
|
|
LOG_INFO << "Initializing DatabaseService...";
|
|
|
|
|
DatabaseService::getInstance().initialize();
|
|
|
|
|
|
|
|
|
|
// Load config
|
|
|
|
|
LOG_INFO << "Loading configuration...";
|
|
|
|
|
app().loadConfigFile("config.json");
|
|
|
|
|
|
2025-08-09 13:51:36 -04:00
|
|
|
// Initialize StatsService BEFORE registering callbacks
|
|
|
|
|
LOG_INFO << "Initializing StatsService...";
|
|
|
|
|
StatsService::getInstance().initialize();
|
2026-01-05 22:54:27 -05:00
|
|
|
|
|
|
|
|
// Register a pre-routing advice to handle CORS and CSRF protection
|
2025-08-03 21:53:15 -04:00
|
|
|
app().registerPreRoutingAdvice([](const HttpRequestPtr &req,
|
|
|
|
|
AdviceCallback &&acb,
|
|
|
|
|
AdviceChainCallback &&accb) {
|
|
|
|
|
// Handle CORS preflight requests
|
|
|
|
|
if (req->getMethod() == Options) {
|
|
|
|
|
auto resp = HttpResponse::newHttpResponse();
|
|
|
|
|
resp->setStatusCode(k204NoContent);
|
2026-01-05 22:54:27 -05:00
|
|
|
|
2025-08-03 21:53:15 -04:00
|
|
|
// Get origin from request
|
|
|
|
|
std::string origin = req->getHeader("Origin");
|
|
|
|
|
if (origin.empty()) {
|
|
|
|
|
origin = "*";
|
|
|
|
|
}
|
2026-01-05 22:54:27 -05:00
|
|
|
|
2025-08-03 21:53:15 -04:00
|
|
|
resp->addHeader("Access-Control-Allow-Origin", origin);
|
|
|
|
|
resp->addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
|
|
|
resp->addHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
|
|
|
resp->addHeader("Access-Control-Allow-Credentials", "true");
|
|
|
|
|
resp->addHeader("Access-Control-Max-Age", "86400");
|
|
|
|
|
acb(resp);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-05 22:54:27 -05:00
|
|
|
|
|
|
|
|
// SECURITY FIX #18: CSRF protection for state-changing requests
|
|
|
|
|
// Require Origin or Referer header for POST/PUT/DELETE requests
|
|
|
|
|
if (req->getMethod() == Post || req->getMethod() == Put || req->getMethod() == Delete) {
|
|
|
|
|
std::string origin = req->getHeader("Origin");
|
|
|
|
|
std::string referer = req->getHeader("Referer");
|
|
|
|
|
std::string path = req->getPath();
|
|
|
|
|
|
|
|
|
|
// Skip CSRF check for API endpoints that use Bearer token auth
|
|
|
|
|
// (Bearer tokens are not automatically sent by browsers, so CSRF is not a concern)
|
|
|
|
|
std::string authHeader = req->getHeader("Authorization");
|
|
|
|
|
bool hasBearerToken = !authHeader.empty() && authHeader.substr(0, 7) == "Bearer ";
|
|
|
|
|
|
|
|
|
|
// Skip CSRF check for internal endpoints (server-to-server calls)
|
|
|
|
|
bool isInternalEndpoint = path.find("/api/webhook/") == 0 ||
|
|
|
|
|
path.find("/api/internal/") == 0;
|
|
|
|
|
|
|
|
|
|
// If not using Bearer auth and not an internal endpoint, require Origin or Referer header
|
|
|
|
|
if (!hasBearerToken && !isInternalEndpoint && origin.empty() && referer.empty()) {
|
|
|
|
|
LOG_WARN << "CSRF protection: Blocked request without Origin/Referer to "
|
|
|
|
|
<< req->getPath() << " from " << req->getPeerAddr().toIpPort();
|
|
|
|
|
auto resp = HttpResponse::newHttpResponse();
|
|
|
|
|
resp->setStatusCode(k403Forbidden);
|
|
|
|
|
resp->setBody("Missing Origin or Referer header");
|
|
|
|
|
acb(resp);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-03 21:53:15 -04:00
|
|
|
accb();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Register post-handling advice to add CORS headers to all responses
|
|
|
|
|
app().registerPostHandlingAdvice([](const HttpRequestPtr &req,
|
|
|
|
|
const HttpResponsePtr &resp) {
|
|
|
|
|
// Get origin from request
|
|
|
|
|
std::string origin = req->getHeader("Origin");
|
|
|
|
|
if (origin.empty()) {
|
|
|
|
|
origin = "*";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp->addHeader("Access-Control-Allow-Origin", origin);
|
|
|
|
|
resp->addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
|
|
|
resp->addHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
|
|
|
resp->addHeader("Access-Control-Allow-Credentials", "true");
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-09 13:51:36 -04:00
|
|
|
// Register beginning advice to start the stats timer
|
2025-08-03 21:53:15 -04:00
|
|
|
app().registerBeginningAdvice([]() {
|
|
|
|
|
LOG_INFO << "Application started successfully";
|
2026-01-05 22:54:27 -05:00
|
|
|
|
|
|
|
|
// Clean up stuck audio processing jobs on startup
|
|
|
|
|
LOG_INFO << "Cleaning up stuck audio processing jobs...";
|
|
|
|
|
auto dbClient = app().getDbClient();
|
|
|
|
|
*dbClient << "UPDATE audio_files SET status = 'failed' "
|
|
|
|
|
"WHERE status = 'processing' AND created_at < NOW() - INTERVAL '5 minutes'"
|
|
|
|
|
>> [](const drogon::orm::Result& r) {
|
|
|
|
|
if (r.affectedRows() > 0) {
|
|
|
|
|
LOG_INFO << "Marked " << r.affectedRows() << " stuck audio jobs as failed";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
>> [](const drogon::orm::DrogonDbException& e) {
|
|
|
|
|
LOG_WARN << "Failed to clean up stuck audio jobs: " << e.base().what();
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-09 13:51:36 -04:00
|
|
|
// Start the stats polling timer
|
|
|
|
|
LOG_INFO << "Starting stats polling...";
|
|
|
|
|
StatsService::getInstance().startPolling();
|
2026-01-05 22:54:27 -05:00
|
|
|
|
|
|
|
|
// Load censored words from database
|
|
|
|
|
LOG_INFO << "Loading censored words...";
|
|
|
|
|
CensorService::getInstance().loadCensoredWords();
|
|
|
|
|
|
|
|
|
|
// Start treasury scheduler (hourly check for growth/distribution)
|
|
|
|
|
LOG_INFO << "Starting treasury scheduler...";
|
|
|
|
|
TreasuryService::getInstance().initialize();
|
|
|
|
|
TreasuryService::getInstance().startScheduler();
|
2025-08-03 21:53:15 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app().setTermSignalHandler([]() {
|
|
|
|
|
LOG_INFO << "Received termination signal, shutting down...";
|
|
|
|
|
StatsService::getInstance().shutdown();
|
2026-01-05 22:54:27 -05:00
|
|
|
TreasuryService::getInstance().shutdown();
|
2025-08-03 21:53:15 -04:00
|
|
|
app().quit();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Start the application
|
|
|
|
|
LOG_INFO << "Starting Drogon framework...";
|
|
|
|
|
app().run();
|
|
|
|
|
|
|
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
LOG_ERROR << "Exception caught in main: " << e.what();
|
|
|
|
|
return 1;
|
|
|
|
|
} catch (...) {
|
|
|
|
|
LOG_ERROR << "Unknown exception caught in main";
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|