fixes lol
All checks were successful
Build and Push / build-all (push) Successful in 10m54s

This commit is contained in:
doomtube 2026-01-09 16:38:24 -05:00
parent a0e6d40679
commit 954755fbc3
19 changed files with 356 additions and 321 deletions

View file

@ -0,0 +1,24 @@
#pragma once
#include <string>
#include <sstream>
#include <iomanip>
#include <array>
namespace crypto_utils {
// Convert raw bytes to lowercase hex string
inline std::string bytesToHex(const unsigned char* data, size_t length) {
std::stringstream ss;
for (size_t i = 0; i < length; ++i) {
ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(data[i]);
}
return ss.str();
}
// Overload for std::array
template<size_t N>
inline std::string bytesToHex(const std::array<unsigned char, N>& data) {
return bytesToHex(data.data(), N);
}
} // namespace crypto_utils

View file

@ -10,6 +10,7 @@
#include <sys/wait.h>
#include <fcntl.h>
#include <trantor/utils/Logger.h>
#include "CryptoUtils.h"
// Generate cryptographically secure random hex filename with extension
// Uses /dev/urandom for secure randomness instead of std::mt19937
@ -22,22 +23,17 @@ inline std::string generateRandomFilename(const std::string& ext) {
ssize_t bytesRead = read(fd, bytes.data(), bytes.size());
close(fd);
if (bytesRead == static_cast<ssize_t>(bytes.size())) {
std::stringstream ss;
for (unsigned char b : bytes) {
ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(b);
}
return ss.str() + "." + ext;
return crypto_utils::bytesToHex(bytes) + "." + ext;
}
}
// Fallback to std::random_device if /dev/urandom fails
// (shouldn't happen on Linux, but provides resilience)
std::random_device rd;
std::stringstream ss;
for (int i = 0; i < 16; ++i) {
ss << std::hex << std::setw(2) << std::setfill('0') << (rd() & 0xFF);
for (size_t i = 0; i < bytes.size(); ++i) {
bytes[i] = static_cast<unsigned char>(rd() & 0xFF);
}
return ss.str() + "." + ext;
return crypto_utils::bytesToHex(bytes) + "." + ext;
}
// Atomically create a file with exclusive access (O_CREAT | O_EXCL)

View file

@ -1,9 +1,43 @@
#pragma once
#include <drogon/HttpResponse.h>
#include <drogon/HttpRequest.h>
#include <json/json.h>
#include <algorithm>
using namespace drogon;
// Pagination helper
struct PaginationParams {
int page;
int limit;
int offset;
};
inline PaginationParams parsePagination(const HttpRequestPtr& req, int defaultLimit = 20, int maxLimit = 50) {
PaginationParams params;
params.page = 1;
params.limit = defaultLimit;
auto pageParam = req->getParameter("page");
auto limitParam = req->getParameter("limit");
if (!pageParam.empty()) {
try { params.page = std::max(1, std::stoi(pageParam)); } catch (...) {}
}
if (!limitParam.empty()) {
try { params.limit = std::min(std::stoi(limitParam), maxLimit); } catch (...) {}
}
params.offset = (params.page - 1) * params.limit;
return params;
}
// RTMP URL validation
inline bool isValidRtmpUrl(const std::string& url) {
return url.length() >= 7 &&
(url.substr(0, 7) == "rtmp://" || url.substr(0, 8) == "rtmps://");
}
inline HttpResponsePtr jsonResp(const Json::Value& j, HttpStatusCode c = k200OK) {
auto r = HttpResponse::newHttpJsonResponse(j);
r->setStatusCode(c);

View file

@ -113,20 +113,7 @@ namespace {
void AudioController::getAllAudio(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback) {
int page = 1;
int limit = 20;
auto pageParam = req->getParameter("page");
auto limitParam = req->getParameter("limit");
if (!pageParam.empty()) {
try { page = std::stoi(pageParam); } catch (...) {}
}
if (!limitParam.empty()) {
try { limit = std::min(std::stoi(limitParam), 50); } catch (...) {}
}
int offset = (page - 1) * limit;
auto pagination = parsePagination(req, 20, 50);
auto dbClient = app().getDbClient();
*dbClient << "SELECT a.id, a.title, a.description, a.file_path, a.thumbnail_path, "
@ -139,7 +126,7 @@ void AudioController::getAllAudio(const HttpRequestPtr &req,
"WHERE a.is_public = true AND a.status = 'ready' "
"ORDER BY a.created_at DESC "
"LIMIT $1 OFFSET $2"
<< static_cast<int64_t>(limit) << static_cast<int64_t>(offset)
<< static_cast<int64_t>(pagination.limit) << static_cast<int64_t>(pagination.offset)
>> [callback](const Result& r) {
Json::Value resp;
resp["success"] = true;
@ -287,20 +274,7 @@ void AudioController::getRealmAudio(const HttpRequestPtr &req,
}
// Optional pagination (backwards compatible - no params = all results, but capped at 500)
int page = 1;
int limit = 500; // Default max to prevent huge responses
auto pageParam = req->getParameter("page");
auto limitParam = req->getParameter("limit");
if (!pageParam.empty()) {
try { page = std::max(1, std::stoi(pageParam)); } catch (...) {}
}
if (!limitParam.empty()) {
try { limit = std::min(std::max(1, std::stoi(limitParam)), 500); } catch (...) {}
}
int offset = (page - 1) * limit;
auto pagination = parsePagination(req, 500, 500);
auto dbClient = app().getDbClient();
*dbClient << "SELECT r.id, r.name, r.description, r.realm_type, r.title_color, r.created_at, "
@ -309,7 +283,7 @@ void AudioController::getRealmAudio(const HttpRequestPtr &req,
"JOIN users u ON r.user_id = u.id "
"WHERE r.id = $1 AND r.is_active = true AND r.realm_type = 'audio'"
<< id
>> [callback, dbClient, id, limit, offset](const Result& realmResult) {
>> [callback, dbClient, id, pagination](const Result& realmResult) {
if (realmResult.empty()) {
callback(jsonError("Audio realm not found", k404NotFound));
return;
@ -321,7 +295,7 @@ void AudioController::getRealmAudio(const HttpRequestPtr &req,
"WHERE a.realm_id = $1 AND a.is_public = true AND a.status = 'ready' "
"ORDER BY a.created_at DESC "
"LIMIT $2 OFFSET $3"
<< id << static_cast<int64_t>(limit) << static_cast<int64_t>(offset)
<< id << static_cast<int64_t>(pagination.limit) << static_cast<int64_t>(pagination.offset)
>> [callback, realmResult](const Result& r) {
Json::Value resp;
resp["success"] = true;
@ -369,20 +343,7 @@ void AudioController::getRealmAudioByName(const HttpRequestPtr &req,
}
// Optional pagination
int page = 1;
int limit = 500;
auto pageParam = req->getParameter("page");
auto limitParam = req->getParameter("limit");
if (!pageParam.empty()) {
try { page = std::max(1, std::stoi(pageParam)); } catch (...) {}
}
if (!limitParam.empty()) {
try { limit = std::min(std::max(1, std::stoi(limitParam)), 500); } catch (...) {}
}
int offset = (page - 1) * limit;
auto pagination = parsePagination(req, 500, 500);
auto dbClient = app().getDbClient();
*dbClient << "SELECT r.id, r.name, r.description, r.realm_type, r.title_color, r.created_at, "
@ -391,7 +352,7 @@ void AudioController::getRealmAudioByName(const HttpRequestPtr &req,
"JOIN users u ON r.user_id = u.id "
"WHERE LOWER(r.name) = LOWER($1) AND r.is_active = true AND r.realm_type = 'audio'"
<< realmName
>> [callback, dbClient, limit, offset](const Result& realmResult) {
>> [callback, dbClient, pagination](const Result& realmResult) {
if (realmResult.empty()) {
callback(jsonError("Audio realm not found", k404NotFound));
return;
@ -405,7 +366,7 @@ void AudioController::getRealmAudioByName(const HttpRequestPtr &req,
"WHERE a.realm_id = $1 AND a.is_public = true AND a.status = 'ready' "
"ORDER BY a.created_at DESC "
"LIMIT $2 OFFSET $3"
<< realmId << static_cast<int64_t>(limit) << static_cast<int64_t>(offset)
<< realmId << static_cast<int64_t>(pagination.limit) << static_cast<int64_t>(pagination.offset)
>> [callback, realmResult](const Result& r) {
Json::Value resp;
resp["success"] = true;

View file

@ -22,20 +22,7 @@ using namespace drogon::orm;
void EbookController::getAllEbooks(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback) {
int page = 1;
int limit = 20;
auto pageParam = req->getParameter("page");
auto limitParam = req->getParameter("limit");
if (!pageParam.empty()) {
try { page = std::stoi(pageParam); } catch (...) {}
}
if (!limitParam.empty()) {
try { limit = std::min(std::stoi(limitParam), 50); } catch (...) {}
}
int offset = (page - 1) * limit;
auto pagination = parsePagination(req, 20, 50);
auto dbClient = app().getDbClient();
*dbClient << "SELECT e.id, e.title, e.description, e.file_path, e.cover_path, "
@ -48,7 +35,7 @@ void EbookController::getAllEbooks(const HttpRequestPtr &req,
"WHERE e.is_public = true AND e.status = 'ready' "
"ORDER BY e.created_at DESC "
"LIMIT $1 OFFSET $2"
<< static_cast<int64_t>(limit) << static_cast<int64_t>(offset)
<< static_cast<int64_t>(pagination.limit) << static_cast<int64_t>(pagination.offset)
>> [callback](const Result& r) {
Json::Value resp;
resp["success"] = true;

View file

@ -6,6 +6,7 @@
#include "../services/CensorService.h"
#include "../common/HttpHelpers.h"
#include "../common/AuthHelpers.h"
#include "../common/CryptoUtils.h"
#include <drogon/utils/Utilities.h>
#include <drogon/Cookie.h>
#include <drogon/MultiPart.h>
@ -28,12 +29,7 @@ namespace {
LOG_ERROR << "Failed to generate cryptographically secure random bytes";
throw std::runtime_error("Failed to generate secure stream key");
}
std::stringstream ss;
for (size_t i = 0; i < sizeof(bytes); ++i) {
ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(bytes[i]);
}
return ss.str();
return crypto_utils::bytesToHex(bytes, sizeof(bytes));
}
bool validateRealmName(const std::string& name) {

View file

@ -121,7 +121,7 @@ void RestreamController::addDestination(const HttpRequestPtr &req,
}
// Validate RTMP URL format
if (rtmpUrl.substr(0, 7) != "rtmp://" && rtmpUrl.substr(0, 8) != "rtmps://") {
if (!isValidRtmpUrl(rtmpUrl)) {
callback(jsonError("RTMP URL must start with rtmp:// or rtmps://"));
return;
}
@ -208,7 +208,7 @@ void RestreamController::updateDestination(const HttpRequestPtr &req,
callback(jsonError("RTMP URL must be 1-500 characters"));
return;
}
if (rtmpUrl.substr(0, 7) != "rtmp://" && rtmpUrl.substr(0, 8) != "rtmps://") {
if (!isValidRtmpUrl(rtmpUrl)) {
callback(jsonError("RTMP URL must start with rtmp:// or rtmps://"));
return;
}

View file

@ -5,6 +5,7 @@
#include "../common/AuthHelpers.h"
#include "../common/FileUtils.h"
#include "../common/FileValidation.h"
#include "../common/CryptoUtils.h"
#include <drogon/MultiPart.h>
#include <drogon/Cookie.h>
#include <fstream>
@ -23,52 +24,38 @@ namespace {
std::string hashApiKey(const std::string& apiKey) {
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256(reinterpret_cast<const unsigned char*>(apiKey.c_str()), apiKey.length(), hash);
std::stringstream ss;
for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
ss << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(hash[i]);
}
return ss.str();
return crypto_utils::bytesToHex(hash, SHA256_DIGEST_LENGTH);
}
// Helper to set httpOnly access token cookie (2.5 hours to match JWT expiry)
// Parameterized cookie helper
void setCookie(const HttpResponsePtr& resp, const std::string& name,
const std::string& value, int maxAge) {
Cookie cookie(name, value);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setSecure(false); // Set to true in production with HTTPS
cookie.setMaxAge(maxAge);
cookie.setSameSite(Cookie::SameSite::kLax);
resp->addCookie(cookie);
}
constexpr int AUTH_COOKIE_MAX_AGE = 9000; // 2.5 hours
constexpr int REFRESH_COOKIE_MAX_AGE = 90 * 24 * 60 * 60; // 90 days
void setAuthCookie(const HttpResponsePtr& resp, const std::string& token) {
Cookie authCookie("auth_token", token);
authCookie.setPath("/");
authCookie.setHttpOnly(true);
authCookie.setSecure(false); // Set to true in production with HTTPS
authCookie.setMaxAge(9000); // 2.5 hours (150 minutes, matches JWT expiry)
authCookie.setSameSite(Cookie::SameSite::kLax);
resp->addCookie(authCookie);
setCookie(resp, "auth_token", token, AUTH_COOKIE_MAX_AGE);
}
// Helper to set httpOnly refresh token cookie (long-lived: 90 days)
void setRefreshCookie(const HttpResponsePtr& resp, const std::string& token) {
Cookie refreshCookie("refresh_token", token);
refreshCookie.setPath("/");
refreshCookie.setHttpOnly(true);
refreshCookie.setSecure(false); // Set to true in production with HTTPS
refreshCookie.setMaxAge(90 * 24 * 60 * 60); // 90 days
refreshCookie.setSameSite(Cookie::SameSite::kLax);
resp->addCookie(refreshCookie);
setCookie(resp, "refresh_token", token, REFRESH_COOKIE_MAX_AGE);
}
// Helper to clear auth cookie
void clearAuthCookie(const HttpResponsePtr& resp) {
Cookie authCookie("auth_token", "");
authCookie.setPath("/");
authCookie.setHttpOnly(true);
authCookie.setMaxAge(0); // Expire immediately
resp->addCookie(authCookie);
setCookie(resp, "auth_token", "", 0);
}
// Helper to clear refresh cookie
void clearRefreshCookie(const HttpResponsePtr& resp) {
Cookie refreshCookie("refresh_token", "");
refreshCookie.setPath("/");
refreshCookie.setHttpOnly(true);
refreshCookie.setMaxAge(0); // Expire immediately
resp->addCookie(refreshCookie);
setCookie(resp, "refresh_token", "", 0);
}
}
@ -1307,7 +1294,7 @@ void UserController::getBotApiKeys(const HttpRequestPtr &req,
Json::Value key;
key["id"] = static_cast<Json::Int64>(row["id"].as<int64_t>());
key["name"] = row["name"].as<std::string>();
key["scopes"] = row["scopes"].isNull() ? "chat:write" : row["scopes"].as<std::string>();
key["scopes"] = row["scopes"].isNull() ? "chat:rw" : row["scopes"].as<std::string>();
key["createdAt"] = row["created_at"].as<std::string>();
key["lastUsedAt"] = row["last_used_at"].isNull() ? "" : row["last_used_at"].as<std::string>();
key["expiresAt"] = row["expires_at"].isNull() ? "" : row["expires_at"].as<std::string>();
@ -1386,12 +1373,7 @@ void UserController::createBotApiKey(const HttpRequestPtr &req,
callback(jsonError("Failed to generate secure API key", k500InternalServerError));
return;
}
std::stringstream ss;
ss << "key_";
for (int i = 0; i < 32; i++) {
ss << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(bytes[i]);
}
std::string apiKey = ss.str();
std::string apiKey = "key_" + crypto_utils::bytesToHex(bytes, sizeof(bytes));
// SECURITY FIX: Hash the API key before storing (plaintext never stored)
std::string apiKeyHash = hashApiKey(apiKey);

View file

@ -269,21 +269,7 @@ namespace {
void VideoController::getAllVideos(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback) {
// Get pagination parameters
int page = 1;
int limit = 20;
auto pageParam = req->getParameter("page");
auto limitParam = req->getParameter("limit");
if (!pageParam.empty()) {
try { page = std::stoi(pageParam); } catch (...) {}
}
if (!limitParam.empty()) {
try { limit = std::min(std::stoi(limitParam), 50); } catch (...) {}
}
int offset = (page - 1) * limit;
auto pagination = parsePagination(req, 20, 50);
auto dbClient = app().getDbClient();
*dbClient << "SELECT v.id, v.title, v.description, v.file_path, v.thumbnail_path, v.preview_path, "
@ -296,7 +282,7 @@ void VideoController::getAllVideos(const HttpRequestPtr &req,
"WHERE v.is_public = true AND v.status = 'ready' "
"ORDER BY v.created_at DESC "
"LIMIT $1 OFFSET $2"
<< static_cast<int64_t>(limit) << static_cast<int64_t>(offset)
<< static_cast<int64_t>(pagination.limit) << static_cast<int64_t>(pagination.offset)
>> [callback](const Result& r) {
Json::Value resp;
resp["success"] = true;

View file

@ -1,5 +1,6 @@
#include "DatabaseService.h"
#include "../services/RedisHelper.h"
#include "../common/CryptoUtils.h"
#include <drogon/orm/DbClient.h>
#include <sstream>
#include <iomanip>
@ -27,12 +28,7 @@ namespace {
LOG_ERROR << "Failed to generate cryptographically secure random bytes";
throw std::runtime_error("Failed to generate secure stream key");
}
std::stringstream ss;
for (size_t i = 0; i < sizeof(bytes); ++i) {
ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(bytes[i]);
}
return ss.str();
return crypto_utils::bytesToHex(bytes, sizeof(bytes));
}
}