Skip to content

Commit

Permalink
Serve sound files from the API, add health check
Browse files Browse the repository at this point in the history
  • Loading branch information
opsnlops committed Oct 19, 2024
1 parent d9fddfe commit 5e88b66
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 8 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25)

project(creature-server
VERSION "2.2.11"
VERSION "2.2.12"
DESCRIPTION "Server for April's Creatures"
HOMEPAGE_URL https://github.com/opsnlops/creature-server
LANGUAGES C CXX)
Expand Down
9 changes: 9 additions & 0 deletions src/server/metrics/counters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace creatures {
playlistsEventsProcessed = 0;
playlistStatusRequests = 0;
restRequestsProcessed = 0;
soundFilesServed = 0;
websocketConnectionsProcessed = 0;
websocketMessagesReceived = 0;
websocketMessagesSent = 0;
Expand Down Expand Up @@ -67,6 +68,10 @@ namespace creatures {
restRequestsProcessed++;
}

void SystemCounters::incrementSoundFilesServed() {
soundFilesServed++;
}

void SystemCounters::incrementWebsocketConnectionsProcessed() {
websocketConnectionsProcessed++;
}
Expand Down Expand Up @@ -136,6 +141,10 @@ namespace creatures {
return restRequestsProcessed.load();
}

uint64_t SystemCounters::getSoundFilesServed() {
return soundFilesServed.load();
}

uint64_t SystemCounters::getWebsocketConnectionsProcessed() {
return websocketConnectionsProcessed.load();
}
Expand Down
8 changes: 8 additions & 0 deletions src/server/metrics/counters.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ namespace creatures {
}
DTO_FIELD(UInt64, restRequestsProcessed);

DTO_FIELD_INFO(soundFilesServed) {
info->description = "Number of sound files that have been served";
}
DTO_FIELD(UInt64, soundFilesServed);

DTO_FIELD_INFO(websocketConnectionsProcessed) {
info->description = "Number of websocket connections that have been processed";
}
Expand Down Expand Up @@ -126,6 +131,7 @@ namespace creatures {
void incrementPlaylistsEventsProcessed();
void incrementPlaylistStatusRequests();
void incrementRestRequestsProcessed();
void incrementSoundFilesServed();
void incrementWebsocketConnectionsProcessed();
void incrementWebsocketMessagesReceived();
void incrementWebsocketMessagesSent();
Expand All @@ -145,6 +151,7 @@ namespace creatures {
uint64_t getPlaylistsEventsProcessed();
uint64_t getPlaylistStatusRequests();
uint64_t getRestRequestsProcessed();
uint64_t getSoundFilesServed();
uint64_t getWebsocketConnectionsProcessed();
uint64_t getWebsocketMessagesReceived();
uint64_t getWebsocketMessagesSent();
Expand All @@ -168,6 +175,7 @@ namespace creatures {
std::atomic<uint64_t> playlistsEventsProcessed;
std::atomic<uint64_t> playlistStatusRequests;
std::atomic<uint64_t> restRequestsProcessed;
std::atomic<uint64_t> soundFilesServed;
std::atomic<uint64_t> websocketConnectionsProcessed;
std::atomic<uint64_t> websocketMessagesReceived;
std::atomic<uint64_t> websocketMessagesSent;
Expand Down
4 changes: 0 additions & 4 deletions src/server/ws/App.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,6 @@ namespace creatures ::ws {
appLogger->info("Running on port {}", connectionProvider->getProperty("port").toString()->c_str());
server.run();

/* stop db connection pool */
//OATPP_COMPONENT(std::shared_ptr<oatpp::provider::Provider<oatpp::sqlite::Connection>>, dbConnectionProvider);
//dbConnectionProvider->stop();

}

}
2 changes: 1 addition & 1 deletion src/server/ws/controller/CreatureController.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ namespace creatures :: ws {
info->addResponse<Object<StatusDto>>(Status::CODE_404, "application/json; charset=utf-8");
info->addResponse<Object<StatusDto>>(Status::CODE_500, "application/json; charset=utf-8");

info->pathParams["creatureId"].description = "Creature ID in the form of a MongoDB OID";
info->pathParams["creatureId"].description = "Creature ID in the form of an UUID";
}
ENDPOINT("GET", "api/v1/creature/{creatureId}", getCreature,
PATH(String, creatureId))
Expand Down
145 changes: 143 additions & 2 deletions src/server/ws/controller/SoundController.h
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@

#pragma once

#include <oatpp/web/server/api/ApiController.hpp>
#include <oatpp/parser/json/mapping/ObjectMapper.hpp>
#include <filesystem>
#include <fstream>
#include <regex>

#include <oatpp/core/macro/codegen.hpp>
#include <oatpp/core/macro/component.hpp>
#include <oatpp/parser/json/mapping/ObjectMapper.hpp>
#include <oatpp/web/server/api/ApiController.hpp>
#include <oatpp/web/protocol/http/outgoing/ResponseFactory.hpp>


#include "server/database.h"
Expand All @@ -18,9 +23,11 @@
#include "server/metrics/counters.h"

namespace creatures {
extern std::shared_ptr<creatures::Configuration> config;
extern std::shared_ptr<SystemCounters> metrics;
}


#include OATPP_CODEGEN_BEGIN(ApiController) //<- Begin Codegen

namespace creatures :: ws {
Expand Down Expand Up @@ -71,6 +78,140 @@ namespace creatures :: ws {
}


static std::string sanitizeFilename(const std::string& filename) {
// Reject any filename containing "../"
if (filename.find("..") != std::string::npos) {
throw std::invalid_argument("Invalid filename: Path traversal detected.");
}

// Ensure the filename contains only safe characters (e.g., alphanumeric, underscores, hyphens)
std::regex validFilenameRegex("^[a-zA-Z0-9_-]+\\.[a-zA-Z0-9]+$");
if (!std::regex_match(filename, validFilenameRegex)) {
throw std::invalid_argument("Invalid filename: Contains unsafe characters.");
}

return filename;
}


static std::string getMimeType(const std::string& filename) {
if (filename.ends_with(".mp3")) return "audio/mpeg";
if (filename.ends_with(".wav")) return "audio/wav";
if (filename.ends_with(".ogg")) return "audio/ogg";
return "application/octet-stream"; // Default for unknown types
}


ENDPOINT_INFO(getSound) {
info->summary = "Retrieve a sound file";

info->addResponse<String>(Status::CODE_200, "audio/mpeg");
info->addResponse<String>(Status::CODE_200, "audio/ogg");
info->addResponse<String>(Status::CODE_200, "audio/wav");
info->addResponse<String>(Status::CODE_200, "application/octet-stream");
info->addResponse<Object<StatusDto>>(Status::CODE_403, "application/json; charset=utf-8");
info->addResponse<Object<StatusDto>>(Status::CODE_404, "application/json; charset=utf-8");
info->addResponse<Object<StatusDto>>(Status::CODE_500, "application/json; charset=utf-8");
}
ENDPOINT("GET", "/api/v1/sound/{filename}", getSound,
PATH(String, filename))
{

debug("Request to serve sound file: {}", std::string(filename));
creatures::metrics->incrementRestRequestsProcessed();

// Sanitize the filename
std::string safeFilename;
try {
safeFilename = sanitizeFilename(filename);
} catch (const std::invalid_argument& e) {

warn("Attempt to serve {} failed: {}", std::string(filename), e.what());

auto response = StatusDto::createShared();
response->status = "Forbidden";
response->message = e.what();
response->code = 403;
return createDtoResponse(Status::CODE_403, response);
}

// Assemble the path
auto filePath = config->getSoundFileLocation() + "/" + safeFilename;

// Resolve the canonical path
std::filesystem::path canonicalPath;
std::filesystem::path baseDir;
try {
// Try to resolve canonical paths
canonicalPath = std::filesystem::canonical(filePath);
baseDir = std::filesystem::canonical(config->getSoundFileLocation());
} catch (const std::filesystem::filesystem_error& e) {

warn("Attempt to serve {} failed: {}", std::string(filename), e.what());

auto response = StatusDto::createShared();
response->status = "Not Found";
response->message = e.what();
response->code = 404;
return createDtoResponse(Status::CODE_404, response);
}

// Ensure the resolved path is within the base directory
if (canonicalPath.string().find(baseDir.string()) != 0) {

warn("Attempt to serve {} failed: {}", std::string(filename), "Path traversal attempt.");

auto response = StatusDto::createShared();
response->status = "Forbidden";
response->message = "Forbidden: Path traversal attempt.";
response->code = 403;
return createDtoResponse(Status::CODE_403, response);
}

// Open the file
std::ifstream file(canonicalPath.string(), std::ios::binary | std::ios::ate);
if (!file.is_open()) {

info("Attempt to serve {} failed: {}", std::string(filename), "Not found.");

auto response = StatusDto::createShared();
response->status = "Not Found";
response->message = "File not found.";
response->code = 404;
return createDtoResponse(Status::CODE_404, response);

}

// Get file size
std::streamsize fileSize = file.tellg();
file.seekg(0, std::ios::beg); // Go back to the start of the file

// Get MIME type
auto mimeType = getMimeType(filePath);

// Read file content
std::vector<char> buffer(fileSize);
if (!file.read(buffer.data(), fileSize)) {

auto response = StatusDto::createShared();
response->status = "Internal Server Error";
response->message = "Error reading file.";
response->code = 500;
return createDtoResponse(Status::CODE_500, response);
}

// Increment metrics and log
creatures::metrics->incrementSoundFilesServed();
info("Serving sound file: {} ({}, {} bytes)", std::string(filename), mimeType, fileSize);

// Create response
auto response = ResponseFactory::createResponse(Status::CODE_200, oatpp::String((const char*)buffer.data(), fileSize));
response->putHeader("Content-Type", mimeType.c_str());
// content-length is automatically added by oatpp
return response;
}


};

}
Expand Down
17 changes: 17 additions & 0 deletions src/server/ws/controller/StaticController.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ class StaticController : public oatpp::web::server::api::ApiController {
return response;
}


ENDPOINT_INFO(health) {
info->summary = "Returns OK if the server is healthy";

info->addResponse<Object<StatusDto>>(Status::CODE_200, "application/json; charset=utf-8");
}
ENDPOINT("GET", "api/v1/health", health) {

auto response = StatusDto::createShared();
response->status = "OK";
response->message = "HOP! I'm hopping!";
response->code = 200;

creatures::metrics->incrementRestRequestsProcessed();
return createDtoResponse(Status::CODE_200, response);
}

};

#include OATPP_CODEGEN_END(ApiController) //<- End Codegen
Expand Down

0 comments on commit 5e88b66

Please sign in to comment.