#include "smartbotic/webserver/config.hpp" #include #include #include #include #include #include #include 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 { 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(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::vector 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 { // 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 { 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(); } if (server.contains("port")) { config.port = server["port"].get(); } if (server.contains("webui_path")) { config.webui_path = server["webui_path"].get(); } } // WebSocket section if (json.contains("websocket")) { const auto& ws = json["websocket"]; if (ws.contains("port")) { config.ws_port = ws["port"].get(); } if (ws.contains("path")) { config.ws_path = ws["path"].get(); } } // CORS section if (json.contains("cors")) { const auto& cors = json["cors"]; if (cors.contains("enabled")) { config.cors.enabled = cors["enabled"].get(); } 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()); } } 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()); } } 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()); } } if (cors.contains("allow_credentials")) { config.cors.allow_credentials = cors["allow_credentials"].get(); } if (cors.contains("max_age")) { config.cors.max_age = cors["max_age"].get(); } } // Logging section if (json.contains("logging")) { const auto& logging = json["logging"]; if (logging.contains("level")) { auto level_str = logging["level"].get(); 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(); } } // JWT section if (json.contains("jwt")) { const auto& jwt = json["jwt"]; if (jwt.contains("secret")) { config.jwt.secret = jwt["secret"].get(); } if (jwt.contains("issuer")) { config.jwt.issuer = jwt["issuer"].get(); } if (jwt.contains("access_token_expiry")) { config.jwt.access_token_expiry = jwt["access_token_expiry"].get(); } if (jwt.contains("refresh_token_expiry")) { config.jwt.refresh_token_expiry = jwt["refresh_token_expiry"].get(); } } 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 { 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(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(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 to config file (default: smartbotic-server.json)"); spdlog::info(" -p, --port Port to listen on (default: 8080)"); spdlog::info(" -a, --address Address to bind to (default: 0.0.0.0)"); spdlog::info(" -w, --webui-path Path to WebUI static files (default: ./webui/dist)"); spdlog::info(" -d, --database Database gRPC address (default: 127.0.0.1:50051)"); spdlog::info(" -l, --log-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