config.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. #include "smartbotic/webserver/config.hpp"
  2. #include <nlohmann/json.hpp>
  3. #include <spdlog/spdlog.h>
  4. #include <algorithm>
  5. #include <cctype>
  6. #include <cstdlib>
  7. #include <filesystem>
  8. #include <fstream>
  9. namespace smartbotic::webserver {
  10. auto LogLevelToString(LogLevel level) -> std::string {
  11. switch (level) {
  12. case LogLevel::kTrace:
  13. return "trace";
  14. case LogLevel::kDebug:
  15. return "debug";
  16. case LogLevel::kInfo:
  17. return "info";
  18. case LogLevel::kWarn:
  19. return "warn";
  20. case LogLevel::kError:
  21. return "error";
  22. case LogLevel::kCritical:
  23. return "critical";
  24. case LogLevel::kOff:
  25. return "off";
  26. }
  27. return "info";
  28. }
  29. auto ParseLogLevel(const std::string& str) -> std::optional<LogLevel> {
  30. std::string lower;
  31. lower.reserve(str.size());
  32. std::transform(str.begin(), str.end(), std::back_inserter(lower), [](unsigned char c) { return std::tolower(c); });
  33. if (lower == "trace")
  34. return LogLevel::kTrace;
  35. if (lower == "debug")
  36. return LogLevel::kDebug;
  37. if (lower == "info")
  38. return LogLevel::kInfo;
  39. if (lower == "warn" || lower == "warning")
  40. return LogLevel::kWarn;
  41. if (lower == "error" || lower == "err")
  42. return LogLevel::kError;
  43. if (lower == "critical" || lower == "crit" || lower == "fatal")
  44. return LogLevel::kCritical;
  45. if (lower == "off" || lower == "none")
  46. return LogLevel::kOff;
  47. return std::nullopt;
  48. }
  49. namespace {
  50. auto GetEnvOrDefault(const char* name, const std::string& default_value) -> std::string {
  51. const char* value = std::getenv(name);
  52. return value != nullptr ? std::string(value) : default_value;
  53. }
  54. auto GetEnvOrDefault(const char* name, uint16_t default_value) -> uint16_t {
  55. const char* value = std::getenv(name);
  56. if (value == nullptr) {
  57. return default_value;
  58. }
  59. try {
  60. return static_cast<uint16_t>(std::stoi(value));
  61. } catch (...) {
  62. return default_value;
  63. }
  64. }
  65. auto GetEnvBool(const char* name, bool default_value) -> bool {
  66. const char* value = std::getenv(name);
  67. if (value == nullptr) {
  68. return default_value;
  69. }
  70. std::string str(value);
  71. std::transform(str.begin(), str.end(), str.begin(), [](unsigned char c) { return std::tolower(c); });
  72. return str == "true" || str == "1" || str == "yes";
  73. }
  74. } // namespace
  75. auto ConfigLoader::GetDefaultConfigPaths() -> std::vector<std::string> {
  76. std::vector<std::string> paths;
  77. // Current directory
  78. paths.emplace_back("./smartbotic-server.json");
  79. // Config subdirectory
  80. paths.emplace_back("./config/smartbotic-server.json");
  81. // /etc for system-wide config
  82. paths.emplace_back("/etc/smartbotic/smartbotic-server.json");
  83. // Home directory
  84. const char* home = std::getenv("HOME");
  85. if (home != nullptr) {
  86. paths.push_back(std::string(home) + "/.config/smartbotic/smartbotic-server.json");
  87. }
  88. return paths;
  89. }
  90. auto ConfigLoader::FindConfigFile() -> std::optional<std::string> {
  91. // Check environment variable for explicit config path
  92. const char* config_path_env = std::getenv("SMARTBOTIC_SERVER_CONFIG");
  93. if (config_path_env != nullptr && std::filesystem::exists(config_path_env)) {
  94. return std::string(config_path_env);
  95. }
  96. // Search default paths
  97. for (const auto& path : GetDefaultConfigPaths()) {
  98. if (std::filesystem::exists(path)) {
  99. return path;
  100. }
  101. }
  102. return std::nullopt;
  103. }
  104. auto ConfigLoader::LoadFromFile(const std::string& path) -> std::optional<HttpServerConfig> {
  105. try {
  106. if (!std::filesystem::exists(path)) {
  107. spdlog::warn("Config file not found: {}", path);
  108. return std::nullopt;
  109. }
  110. std::ifstream file(path);
  111. if (!file.is_open()) {
  112. spdlog::error("Failed to open config file: {}", path);
  113. return std::nullopt;
  114. }
  115. nlohmann::json json = nlohmann::json::parse(file);
  116. HttpServerConfig config;
  117. // Server section
  118. if (json.contains("server")) {
  119. const auto& server = json["server"];
  120. if (server.contains("address")) {
  121. config.address = server["address"].get<std::string>();
  122. }
  123. if (server.contains("port")) {
  124. config.port = server["port"].get<uint16_t>();
  125. }
  126. if (server.contains("webui_path")) {
  127. config.webui_path = server["webui_path"].get<std::string>();
  128. }
  129. }
  130. // WebSocket section
  131. if (json.contains("websocket")) {
  132. const auto& ws = json["websocket"];
  133. if (ws.contains("port")) {
  134. config.ws_port = ws["port"].get<uint16_t>();
  135. }
  136. if (ws.contains("path")) {
  137. config.ws_path = ws["path"].get<std::string>();
  138. }
  139. }
  140. // CORS section
  141. if (json.contains("cors")) {
  142. const auto& cors = json["cors"];
  143. if (cors.contains("enabled")) {
  144. config.cors.enabled = cors["enabled"].get<bool>();
  145. }
  146. if (cors.contains("allowed_origins")) {
  147. config.cors.allowed_origins.clear();
  148. for (const auto& origin : cors["allowed_origins"]) {
  149. config.cors.allowed_origins.push_back(origin.get<std::string>());
  150. }
  151. }
  152. if (cors.contains("allowed_methods")) {
  153. config.cors.allowed_methods.clear();
  154. for (const auto& method : cors["allowed_methods"]) {
  155. config.cors.allowed_methods.push_back(method.get<std::string>());
  156. }
  157. }
  158. if (cors.contains("allowed_headers")) {
  159. config.cors.allowed_headers.clear();
  160. for (const auto& header : cors["allowed_headers"]) {
  161. config.cors.allowed_headers.push_back(header.get<std::string>());
  162. }
  163. }
  164. if (cors.contains("allow_credentials")) {
  165. config.cors.allow_credentials = cors["allow_credentials"].get<bool>();
  166. }
  167. if (cors.contains("max_age")) {
  168. config.cors.max_age = cors["max_age"].get<int>();
  169. }
  170. }
  171. // Logging section
  172. if (json.contains("logging")) {
  173. const auto& logging = json["logging"];
  174. if (logging.contains("level")) {
  175. auto level_str = logging["level"].get<std::string>();
  176. auto level = ParseLogLevel(level_str);
  177. if (level) {
  178. config.log_level = *level;
  179. } else {
  180. spdlog::warn("Invalid log level '{}', using default", level_str);
  181. }
  182. }
  183. }
  184. // Database section
  185. if (json.contains("database")) {
  186. const auto& database = json["database"];
  187. if (database.contains("address")) {
  188. config.database_address = database["address"].get<std::string>();
  189. }
  190. }
  191. // JWT section
  192. if (json.contains("jwt")) {
  193. const auto& jwt = json["jwt"];
  194. if (jwt.contains("secret")) {
  195. config.jwt.secret = jwt["secret"].get<std::string>();
  196. }
  197. if (jwt.contains("issuer")) {
  198. config.jwt.issuer = jwt["issuer"].get<std::string>();
  199. }
  200. if (jwt.contains("access_token_expiry")) {
  201. config.jwt.access_token_expiry = jwt["access_token_expiry"].get<int>();
  202. }
  203. if (jwt.contains("refresh_token_expiry")) {
  204. config.jwt.refresh_token_expiry = jwt["refresh_token_expiry"].get<int>();
  205. }
  206. }
  207. spdlog::info("Loaded configuration from: {}", path);
  208. return config;
  209. } catch (const nlohmann::json::exception& e) {
  210. spdlog::error("Failed to parse config file {}: {}", path, e.what());
  211. return std::nullopt;
  212. } catch (const std::exception& e) {
  213. spdlog::error("Error loading config file {}: {}", path, e.what());
  214. return std::nullopt;
  215. }
  216. }
  217. void ConfigLoader::ApplyEnvironment(HttpServerConfig& config) {
  218. // Server settings from environment
  219. config.address = GetEnvOrDefault("SMARTBOTIC_SERVER_ADDRESS", config.address);
  220. config.port = GetEnvOrDefault("SMARTBOTIC_SERVER_PORT", config.port);
  221. config.webui_path = GetEnvOrDefault("SMARTBOTIC_SERVER_WEBUI_PATH", config.webui_path);
  222. config.ws_path = GetEnvOrDefault("SMARTBOTIC_SERVER_WS_PATH", config.ws_path);
  223. // CORS settings from environment
  224. config.cors.enabled = GetEnvBool("SMARTBOTIC_SERVER_CORS_ENABLED", config.cors.enabled);
  225. // Database settings from environment
  226. config.database_address = GetEnvOrDefault("SMARTBOTIC_SERVER_DATABASE_ADDRESS", config.database_address);
  227. // JWT settings from environment (secret from env is recommended for security)
  228. config.jwt.secret = GetEnvOrDefault("SMARTBOTIC_SERVER_JWT_SECRET", config.jwt.secret);
  229. // Log level from environment
  230. const char* log_level_env = std::getenv("SMARTBOTIC_SERVER_LOG_LEVEL");
  231. if (log_level_env != nullptr) {
  232. auto level = ParseLogLevel(log_level_env);
  233. if (level) {
  234. config.log_level = *level;
  235. }
  236. }
  237. }
  238. auto ConfigLoader::ParseCommandLine(int argc, char* argv[], HttpServerConfig& config) -> std::optional<bool> {
  239. std::string config_file_override;
  240. for (int i = 1; i < argc; ++i) {
  241. std::string arg = argv[i];
  242. if (arg == "--help" || arg == "-h") {
  243. PrintHelp(argv[0]);
  244. return std::nullopt;
  245. }
  246. if (arg == "--version" || arg == "-v") {
  247. spdlog::info("SmartBotic Web Server v0.1.0");
  248. return std::nullopt;
  249. }
  250. if (arg == "--config" || arg == "-c") {
  251. if (i + 1 < argc) {
  252. config_file_override = argv[++i];
  253. } else {
  254. spdlog::error("--config requires a path argument");
  255. return false;
  256. }
  257. continue;
  258. }
  259. if (arg == "--port" || arg == "-p") {
  260. if (i + 1 < argc) {
  261. try {
  262. config.port = static_cast<uint16_t>(std::stoi(argv[++i]));
  263. } catch (...) {
  264. spdlog::error("Invalid port number: {}", argv[i]);
  265. return false;
  266. }
  267. } else {
  268. spdlog::error("--port requires a number argument");
  269. return false;
  270. }
  271. continue;
  272. }
  273. if (arg == "--address" || arg == "-a") {
  274. if (i + 1 < argc) {
  275. config.address = argv[++i];
  276. } else {
  277. spdlog::error("--address requires an address argument");
  278. return false;
  279. }
  280. continue;
  281. }
  282. if (arg == "--webui-path" || arg == "-w") {
  283. if (i + 1 < argc) {
  284. config.webui_path = argv[++i];
  285. } else {
  286. spdlog::error("--webui-path requires a path argument");
  287. return false;
  288. }
  289. continue;
  290. }
  291. if (arg == "--database" || arg == "-d") {
  292. if (i + 1 < argc) {
  293. config.database_address = argv[++i];
  294. } else {
  295. spdlog::error("--database requires an address argument");
  296. return false;
  297. }
  298. continue;
  299. }
  300. if (arg == "--log-level" || arg == "-l") {
  301. if (i + 1 < argc) {
  302. auto level = ParseLogLevel(argv[++i]);
  303. if (level) {
  304. config.log_level = *level;
  305. } else {
  306. spdlog::error("Invalid log level: {}", argv[i]);
  307. return false;
  308. }
  309. } else {
  310. spdlog::error("--log-level requires a level argument");
  311. return false;
  312. }
  313. continue;
  314. }
  315. if (arg == "--no-cors") {
  316. config.cors.enabled = false;
  317. continue;
  318. }
  319. spdlog::error("Unknown argument: {}", arg);
  320. PrintHelp(argv[0]);
  321. return false;
  322. }
  323. // If config file was specified on command line, load it
  324. if (!config_file_override.empty()) {
  325. auto file_config = LoadFromFile(config_file_override);
  326. if (!file_config) {
  327. spdlog::error("Failed to load specified config file: {}", config_file_override);
  328. return false;
  329. }
  330. config = *file_config;
  331. // Re-parse command line to override file settings
  332. for (int i = 1; i < argc; ++i) {
  333. std::string arg = argv[i];
  334. if (arg == "--config" || arg == "-c") {
  335. ++i;
  336. continue;
  337. }
  338. if ((arg == "--port" || arg == "-p") && i + 1 < argc) {
  339. config.port = static_cast<uint16_t>(std::stoi(argv[++i]));
  340. } else if ((arg == "--address" || arg == "-a") && i + 1 < argc) {
  341. config.address = argv[++i];
  342. } else if ((arg == "--webui-path" || arg == "-w") && i + 1 < argc) {
  343. config.webui_path = argv[++i];
  344. } else if ((arg == "--database" || arg == "-d") && i + 1 < argc) {
  345. config.database_address = argv[++i];
  346. } else if ((arg == "--log-level" || arg == "-l") && i + 1 < argc) {
  347. auto level = ParseLogLevel(argv[++i]);
  348. if (level)
  349. config.log_level = *level;
  350. } else if (arg == "--no-cors") {
  351. config.cors.enabled = false;
  352. }
  353. }
  354. }
  355. return true;
  356. }
  357. auto ConfigLoader::Load(int argc, char* argv[]) -> HttpServerConfig {
  358. HttpServerConfig config = HttpServerConfig::Default();
  359. // 1. Try to find and load config file
  360. auto config_file = FindConfigFile();
  361. if (config_file) {
  362. auto file_config = LoadFromFile(*config_file);
  363. if (file_config) {
  364. config = *file_config;
  365. }
  366. }
  367. // 2. Apply environment variables
  368. ApplyEnvironment(config);
  369. // 3. Parse command-line arguments
  370. auto result = ParseCommandLine(argc, argv, config);
  371. if (!result.has_value()) {
  372. std::exit(0);
  373. }
  374. if (!result.value()) {
  375. std::exit(1);
  376. }
  377. return config;
  378. }
  379. void ConfigLoader::PrintHelp(const char* program_name) {
  380. spdlog::info("Usage: {} [options]", program_name);
  381. spdlog::info("");
  382. spdlog::info("SmartBotic Web Server - HTTP/WebSocket Server");
  383. spdlog::info("");
  384. spdlog::info("Options:");
  385. spdlog::info(" -h, --help Show this help message");
  386. spdlog::info(" -v, --version Show version information");
  387. spdlog::info(" -c, --config <path> Path to config file (default: smartbotic-server.json)");
  388. spdlog::info(" -p, --port <port> Port to listen on (default: 8080)");
  389. spdlog::info(" -a, --address <addr> Address to bind to (default: 0.0.0.0)");
  390. spdlog::info(" -w, --webui-path <path> Path to WebUI static files (default: ./webui/dist)");
  391. spdlog::info(" -d, --database <addr> Database gRPC address (default: 127.0.0.1:50051)");
  392. spdlog::info(" -l, --log-level <level> Log level: trace, debug, info, warn, error, critical, off");
  393. spdlog::info(" --no-cors Disable CORS headers");
  394. spdlog::info("");
  395. spdlog::info("Environment Variables:");
  396. spdlog::info(" SMARTBOTIC_SERVER_CONFIG Path to config file");
  397. spdlog::info(" SMARTBOTIC_SERVER_ADDRESS Address to bind to");
  398. spdlog::info(" SMARTBOTIC_SERVER_PORT Port to listen on");
  399. spdlog::info(" SMARTBOTIC_SERVER_WEBUI_PATH Path to WebUI static files");
  400. spdlog::info(" SMARTBOTIC_SERVER_DATABASE_ADDRESS Database gRPC address");
  401. spdlog::info(" SMARTBOTIC_SERVER_LOG_LEVEL Log level");
  402. spdlog::info(" SMARTBOTIC_SERVER_CORS_ENABLED Enable/disable CORS (true/false)");
  403. spdlog::info("");
  404. spdlog::info("Config File Search Order:");
  405. for (const auto& path : GetDefaultConfigPaths()) {
  406. spdlog::info(" {}", path);
  407. }
  408. }
  409. void ConfigLoader::PrintConfig(const HttpServerConfig& config) {
  410. spdlog::info("Configuration:");
  411. spdlog::info(" Address: {}", config.address);
  412. spdlog::info(" Port: {}", config.port);
  413. spdlog::info(" WebUI Path: {}", config.webui_path);
  414. spdlog::info(" WebSocket Port: {}", config.ws_port);
  415. spdlog::info(" WebSocket Path: {}", config.ws_path);
  416. spdlog::info(" Database Address: {}", config.database_address);
  417. spdlog::info(" Log Level: {}", LogLevelToString(config.log_level));
  418. spdlog::info(" CORS Enabled: {}", config.cors.enabled ? "yes" : "no");
  419. if (config.cors.enabled) {
  420. std::string origins;
  421. for (size_t i = 0; i < config.cors.allowed_origins.size(); ++i) {
  422. if (i > 0) {
  423. origins += ", ";
  424. }
  425. origins += config.cors.allowed_origins[i];
  426. }
  427. spdlog::info(" CORS Origins: {}", origins);
  428. }
  429. spdlog::info(" JWT Issuer: {}", config.jwt.issuer);
  430. spdlog::info(" JWT Secret: {}", config.jwt.secret.empty() ? "(not configured)" : "(configured)");
  431. spdlog::info(" JWT Access TTL: {}s", config.jwt.access_token_expiry);
  432. spdlog::info(" JWT Refresh TTL: {}s", config.jwt.refresh_token_expiry);
  433. }
  434. } // namespace smartbotic::webserver