This commit is contained in:
parent
a0e6d40679
commit
954755fbc3
19 changed files with 356 additions and 321 deletions
24
backend/src/common/CryptoUtils.h
Normal file
24
backend/src/common/CryptoUtils.h
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue