| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494 |
- #include "smartbotic/webserver/config.hpp"
- #include <nlohmann/json.hpp>
- #include <spdlog/spdlog.h>
- #include <algorithm>
- #include <cctype>
- #include <cstdlib>
- #include <filesystem>
- #include <fstream>
- namespace smartbotic::webserver {
- auto LogLevelToString(LogLevel level) -> std::string {
- switch (level) {
- case LogLevel::kTrace:
- return "trace";
- case LogLevel::kDebug:
- return "debug";
- case LogLevel::kInfo:
- return "info";
- case LogLevel::kWarn:
- return "warn";
- case LogLevel::kError:
- return "error";
- case LogLevel::kCritical:
- return "critical";
- case LogLevel::kOff:
- return "off";
- }
- return "info";
- }
- auto ParseLogLevel(const std::string& str) -> std::optional<LogLevel> {
- std::string lower;
- lower.reserve(str.size());
- std::transform(str.begin(), str.end(), std::back_inserter(lower), [](unsigned char c) { return std::tolower(c); });
- if (lower == "trace")
- return LogLevel::kTrace;
- if (lower == "debug")
- return LogLevel::kDebug;
- if (lower == "info")
- return LogLevel::kInfo;
- if (lower == "warn" || lower == "warning")
- return LogLevel::kWarn;
- if (lower == "error" || lower == "err")
- return LogLevel::kError;
- if (lower == "critical" || lower == "crit" || lower == "fatal")
- return LogLevel::kCritical;
- if (lower == "off" || lower == "none")
- return LogLevel::kOff;
- return std::nullopt;
- }
- namespace {
- auto GetEnvOrDefault(const char* name, const std::string& default_value) -> std::string {
- const char* value = std::getenv(name);
- return value != nullptr ? std::string(value) : default_value;
- }
- auto GetEnvOrDefault(const char* name, uint16_t default_value) -> uint16_t {
- const char* value = std::getenv(name);
- if (value == nullptr) {
- return default_value;
- }
- try {
- return static_cast<uint16_t>(std::stoi(value));
- } catch (...) {
- return default_value;
- }
- }
- auto GetEnvBool(const char* name, bool default_value) -> bool {
- const char* value = std::getenv(name);
- if (value == nullptr) {
- return default_value;
- }
- std::string str(value);
- std::transform(str.begin(), str.end(), str.begin(), [](unsigned char c) { return std::tolower(c); });
- return str == "true" || str == "1" || str == "yes";
- }
- } // namespace
- auto ConfigLoader::GetDefaultConfigPaths() -> std::vector<std::string> {
- std::vector<std::string> paths;
- // Current directory
- paths.emplace_back("./smartbotic-server.json");
- // Config subdirectory
- paths.emplace_back("./config/smartbotic-server.json");
- // /etc for system-wide config
- paths.emplace_back("/etc/smartbotic/smartbotic-server.json");
- // Home directory
- const char* home = std::getenv("HOME");
- if (home != nullptr) {
- paths.push_back(std::string(home) + "/.config/smartbotic/smartbotic-server.json");
- }
- return paths;
- }
- auto ConfigLoader::FindConfigFile() -> std::optional<std::string> {
- // Check environment variable for explicit config path
- const char* config_path_env = std::getenv("SMARTBOTIC_SERVER_CONFIG");
- if (config_path_env != nullptr && std::filesystem::exists(config_path_env)) {
- return std::string(config_path_env);
- }
- // Search default paths
- for (const auto& path : GetDefaultConfigPaths()) {
- if (std::filesystem::exists(path)) {
- return path;
- }
- }
- return std::nullopt;
- }
- auto ConfigLoader::LoadFromFile(const std::string& path) -> std::optional<HttpServerConfig> {
- try {
- if (!std::filesystem::exists(path)) {
- spdlog::warn("Config file not found: {}", path);
- return std::nullopt;
- }
- std::ifstream file(path);
- if (!file.is_open()) {
- spdlog::error("Failed to open config file: {}", path);
- return std::nullopt;
- }
- nlohmann::json json = nlohmann::json::parse(file);
- HttpServerConfig config;
- // Server section
- if (json.contains("server")) {
- const auto& server = json["server"];
- if (server.contains("address")) {
- config.address = server["address"].get<std::string>();
- }
- if (server.contains("port")) {
- config.port = server["port"].get<uint16_t>();
- }
- if (server.contains("webui_path")) {
- config.webui_path = server["webui_path"].get<std::string>();
- }
- }
- // WebSocket section
- if (json.contains("websocket")) {
- const auto& ws = json["websocket"];
- if (ws.contains("port")) {
- config.ws_port = ws["port"].get<uint16_t>();
- }
- if (ws.contains("path")) {
- config.ws_path = ws["path"].get<std::string>();
- }
- }
- // CORS section
- if (json.contains("cors")) {
- const auto& cors = json["cors"];
- if (cors.contains("enabled")) {
- config.cors.enabled = cors["enabled"].get<bool>();
- }
- if (cors.contains("allowed_origins")) {
- config.cors.allowed_origins.clear();
- for (const auto& origin : cors["allowed_origins"]) {
- config.cors.allowed_origins.push_back(origin.get<std::string>());
- }
- }
- if (cors.contains("allowed_methods")) {
- config.cors.allowed_methods.clear();
- for (const auto& method : cors["allowed_methods"]) {
- config.cors.allowed_methods.push_back(method.get<std::string>());
- }
- }
- if (cors.contains("allowed_headers")) {
- config.cors.allowed_headers.clear();
- for (const auto& header : cors["allowed_headers"]) {
- config.cors.allowed_headers.push_back(header.get<std::string>());
- }
- }
- if (cors.contains("allow_credentials")) {
- config.cors.allow_credentials = cors["allow_credentials"].get<bool>();
- }
- if (cors.contains("max_age")) {
- config.cors.max_age = cors["max_age"].get<int>();
- }
- }
- // Logging section
- if (json.contains("logging")) {
- const auto& logging = json["logging"];
- if (logging.contains("level")) {
- auto level_str = logging["level"].get<std::string>();
- auto level = ParseLogLevel(level_str);
- if (level) {
- config.log_level = *level;
- } else {
- spdlog::warn("Invalid log level '{}', using default", level_str);
- }
- }
- }
- // Database section
- if (json.contains("database")) {
- const auto& database = json["database"];
- if (database.contains("address")) {
- config.database_address = database["address"].get<std::string>();
- }
- }
- // JWT section
- if (json.contains("jwt")) {
- const auto& jwt = json["jwt"];
- if (jwt.contains("secret")) {
- config.jwt.secret = jwt["secret"].get<std::string>();
- }
- if (jwt.contains("issuer")) {
- config.jwt.issuer = jwt["issuer"].get<std::string>();
- }
- if (jwt.contains("access_token_expiry")) {
- config.jwt.access_token_expiry = jwt["access_token_expiry"].get<int>();
- }
- if (jwt.contains("refresh_token_expiry")) {
- config.jwt.refresh_token_expiry = jwt["refresh_token_expiry"].get<int>();
- }
- }
- spdlog::info("Loaded configuration from: {}", path);
- return config;
- } catch (const nlohmann::json::exception& e) {
- spdlog::error("Failed to parse config file {}: {}", path, e.what());
- return std::nullopt;
- } catch (const std::exception& e) {
- spdlog::error("Error loading config file {}: {}", path, e.what());
- return std::nullopt;
- }
- }
- void ConfigLoader::ApplyEnvironment(HttpServerConfig& config) {
- // Server settings from environment
- config.address = GetEnvOrDefault("SMARTBOTIC_SERVER_ADDRESS", config.address);
- config.port = GetEnvOrDefault("SMARTBOTIC_SERVER_PORT", config.port);
- config.webui_path = GetEnvOrDefault("SMARTBOTIC_SERVER_WEBUI_PATH", config.webui_path);
- config.ws_path = GetEnvOrDefault("SMARTBOTIC_SERVER_WS_PATH", config.ws_path);
- // CORS settings from environment
- config.cors.enabled = GetEnvBool("SMARTBOTIC_SERVER_CORS_ENABLED", config.cors.enabled);
- // Database settings from environment
- config.database_address = GetEnvOrDefault("SMARTBOTIC_SERVER_DATABASE_ADDRESS", config.database_address);
- // JWT settings from environment (secret from env is recommended for security)
- config.jwt.secret = GetEnvOrDefault("SMARTBOTIC_SERVER_JWT_SECRET", config.jwt.secret);
- // Log level from environment
- const char* log_level_env = std::getenv("SMARTBOTIC_SERVER_LOG_LEVEL");
- if (log_level_env != nullptr) {
- auto level = ParseLogLevel(log_level_env);
- if (level) {
- config.log_level = *level;
- }
- }
- }
- auto ConfigLoader::ParseCommandLine(int argc, char* argv[], HttpServerConfig& config) -> std::optional<bool> {
- std::string config_file_override;
- for (int i = 1; i < argc; ++i) {
- std::string arg = argv[i];
- if (arg == "--help" || arg == "-h") {
- PrintHelp(argv[0]);
- return std::nullopt;
- }
- if (arg == "--version" || arg == "-v") {
- spdlog::info("SmartBotic Web Server v0.1.0");
- return std::nullopt;
- }
- if (arg == "--config" || arg == "-c") {
- if (i + 1 < argc) {
- config_file_override = argv[++i];
- } else {
- spdlog::error("--config requires a path argument");
- return false;
- }
- continue;
- }
- if (arg == "--port" || arg == "-p") {
- if (i + 1 < argc) {
- try {
- config.port = static_cast<uint16_t>(std::stoi(argv[++i]));
- } catch (...) {
- spdlog::error("Invalid port number: {}", argv[i]);
- return false;
- }
- } else {
- spdlog::error("--port requires a number argument");
- return false;
- }
- continue;
- }
- if (arg == "--address" || arg == "-a") {
- if (i + 1 < argc) {
- config.address = argv[++i];
- } else {
- spdlog::error("--address requires an address argument");
- return false;
- }
- continue;
- }
- if (arg == "--webui-path" || arg == "-w") {
- if (i + 1 < argc) {
- config.webui_path = argv[++i];
- } else {
- spdlog::error("--webui-path requires a path argument");
- return false;
- }
- continue;
- }
- if (arg == "--database" || arg == "-d") {
- if (i + 1 < argc) {
- config.database_address = argv[++i];
- } else {
- spdlog::error("--database requires an address argument");
- return false;
- }
- continue;
- }
- if (arg == "--log-level" || arg == "-l") {
- if (i + 1 < argc) {
- auto level = ParseLogLevel(argv[++i]);
- if (level) {
- config.log_level = *level;
- } else {
- spdlog::error("Invalid log level: {}", argv[i]);
- return false;
- }
- } else {
- spdlog::error("--log-level requires a level argument");
- return false;
- }
- continue;
- }
- if (arg == "--no-cors") {
- config.cors.enabled = false;
- continue;
- }
- spdlog::error("Unknown argument: {}", arg);
- PrintHelp(argv[0]);
- return false;
- }
- // If config file was specified on command line, load it
- if (!config_file_override.empty()) {
- auto file_config = LoadFromFile(config_file_override);
- if (!file_config) {
- spdlog::error("Failed to load specified config file: {}", config_file_override);
- return false;
- }
- config = *file_config;
- // Re-parse command line to override file settings
- for (int i = 1; i < argc; ++i) {
- std::string arg = argv[i];
- if (arg == "--config" || arg == "-c") {
- ++i;
- continue;
- }
- if ((arg == "--port" || arg == "-p") && i + 1 < argc) {
- config.port = static_cast<uint16_t>(std::stoi(argv[++i]));
- } else if ((arg == "--address" || arg == "-a") && i + 1 < argc) {
- config.address = argv[++i];
- } else if ((arg == "--webui-path" || arg == "-w") && i + 1 < argc) {
- config.webui_path = argv[++i];
- } else if ((arg == "--database" || arg == "-d") && i + 1 < argc) {
- config.database_address = argv[++i];
- } else if ((arg == "--log-level" || arg == "-l") && i + 1 < argc) {
- auto level = ParseLogLevel(argv[++i]);
- if (level)
- config.log_level = *level;
- } else if (arg == "--no-cors") {
- config.cors.enabled = false;
- }
- }
- }
- return true;
- }
- auto ConfigLoader::Load(int argc, char* argv[]) -> HttpServerConfig {
- HttpServerConfig config = HttpServerConfig::Default();
- // 1. Try to find and load config file
- auto config_file = FindConfigFile();
- if (config_file) {
- auto file_config = LoadFromFile(*config_file);
- if (file_config) {
- config = *file_config;
- }
- }
- // 2. Apply environment variables
- ApplyEnvironment(config);
- // 3. Parse command-line arguments
- auto result = ParseCommandLine(argc, argv, config);
- if (!result.has_value()) {
- std::exit(0);
- }
- if (!result.value()) {
- std::exit(1);
- }
- return config;
- }
- void ConfigLoader::PrintHelp(const char* program_name) {
- spdlog::info("Usage: {} [options]", program_name);
- spdlog::info("");
- spdlog::info("SmartBotic Web Server - HTTP/WebSocket Server");
- spdlog::info("");
- spdlog::info("Options:");
- spdlog::info(" -h, --help Show this help message");
- spdlog::info(" -v, --version Show version information");
- spdlog::info(" -c, --config <path> Path to config file (default: smartbotic-server.json)");
- spdlog::info(" -p, --port <port> Port to listen on (default: 8080)");
- spdlog::info(" -a, --address <addr> Address to bind to (default: 0.0.0.0)");
- spdlog::info(" -w, --webui-path <path> Path to WebUI static files (default: ./webui/dist)");
- spdlog::info(" -d, --database <addr> Database gRPC address (default: 127.0.0.1:50051)");
- spdlog::info(" -l, --log-level <level> Log level: trace, debug, info, warn, error, critical, off");
- spdlog::info(" --no-cors Disable CORS headers");
- spdlog::info("");
- spdlog::info("Environment Variables:");
- spdlog::info(" SMARTBOTIC_SERVER_CONFIG Path to config file");
- spdlog::info(" SMARTBOTIC_SERVER_ADDRESS Address to bind to");
- spdlog::info(" SMARTBOTIC_SERVER_PORT Port to listen on");
- spdlog::info(" SMARTBOTIC_SERVER_WEBUI_PATH Path to WebUI static files");
- spdlog::info(" SMARTBOTIC_SERVER_DATABASE_ADDRESS Database gRPC address");
- spdlog::info(" SMARTBOTIC_SERVER_LOG_LEVEL Log level");
- spdlog::info(" SMARTBOTIC_SERVER_CORS_ENABLED Enable/disable CORS (true/false)");
- spdlog::info("");
- spdlog::info("Config File Search Order:");
- for (const auto& path : GetDefaultConfigPaths()) {
- spdlog::info(" {}", path);
- }
- }
- void ConfigLoader::PrintConfig(const HttpServerConfig& config) {
- spdlog::info("Configuration:");
- spdlog::info(" Address: {}", config.address);
- spdlog::info(" Port: {}", config.port);
- spdlog::info(" WebUI Path: {}", config.webui_path);
- spdlog::info(" WebSocket Port: {}", config.ws_port);
- spdlog::info(" WebSocket Path: {}", config.ws_path);
- spdlog::info(" Database Address: {}", config.database_address);
- spdlog::info(" Log Level: {}", LogLevelToString(config.log_level));
- spdlog::info(" CORS Enabled: {}", config.cors.enabled ? "yes" : "no");
- if (config.cors.enabled) {
- std::string origins;
- for (size_t i = 0; i < config.cors.allowed_origins.size(); ++i) {
- if (i > 0) {
- origins += ", ";
- }
- origins += config.cors.allowed_origins[i];
- }
- spdlog::info(" CORS Origins: {}", origins);
- }
- spdlog::info(" JWT Issuer: {}", config.jwt.issuer);
- spdlog::info(" JWT Secret: {}", config.jwt.secret.empty() ? "(not configured)" : "(configured)");
- spdlog::info(" JWT Access TTL: {}s", config.jwt.access_token_expiry);
- spdlog::info(" JWT Refresh TTL: {}s", config.jwt.refresh_token_expiry);
- }
- } // namespace smartbotic::webserver
|