main.cc 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. //
  2. // main.cc
  3. //
  4. // Copyright (c) 2026 Yuji Hirose. All rights reserved.
  5. // MIT License
  6. //
  7. #include <atomic>
  8. #include <chrono>
  9. #include <ctime>
  10. #include <format>
  11. #include <iomanip>
  12. #include <iostream>
  13. #include <signal.h>
  14. #include <sstream>
  15. #include <httplib.h>
  16. using namespace httplib;
  17. const auto SERVER_NAME =
  18. std::format("cpp-httplib-server/{}", CPPHTTPLIB_VERSION);
  19. Server svr;
  20. void signal_handler(int signal) {
  21. if (signal == SIGINT || signal == SIGTERM) {
  22. std::cout << "\nReceived signal, shutting down gracefully...\n";
  23. svr.stop();
  24. }
  25. }
  26. std::string get_time_format() {
  27. auto now = std::chrono::system_clock::now();
  28. auto time_t = std::chrono::system_clock::to_time_t(now);
  29. std::stringstream ss;
  30. ss << std::put_time(std::localtime(&time_t), "%d/%b/%Y:%H:%M:%S %z");
  31. return ss.str();
  32. }
  33. std::string get_error_time_format() {
  34. auto now = std::chrono::system_clock::now();
  35. auto time_t = std::chrono::system_clock::to_time_t(now);
  36. std::stringstream ss;
  37. ss << std::put_time(std::localtime(&time_t), "%Y/%m/%d %H:%M:%S");
  38. return ss.str();
  39. }
  40. // Escape a value for a log line the way NGINX does: '"', '\\', control
  41. // bytes and non-ASCII bytes become \xHH. Request fields are attacker-controlled
  42. // (e.g. a raw CR in the request target or a decoded %0D%0A in req.path), so
  43. // writing them verbatim would let a client forge extra log lines.
  44. std::string escape_log(const std::string &s) {
  45. std::string out;
  46. out.reserve(s.size());
  47. for (unsigned char c : s) {
  48. if (c == '"' || c == '\\' || c < 0x20 || c >= 0x7f) {
  49. out += std::format("\\x{:02X}", c);
  50. } else {
  51. out += static_cast<char>(c);
  52. }
  53. }
  54. return out;
  55. }
  56. // NGINX Combined log format:
  57. // $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
  58. // "$http_referer" "$http_user_agent"
  59. void nginx_access_logger(const Request &req, const Response &res) {
  60. std::string remote_user =
  61. "-"; // cpp-httplib doesn't have built-in auth user tracking
  62. auto time_local = get_time_format();
  63. // $request is the original request line, so log the raw target rather than
  64. // the percent-decoded req.path.
  65. auto request = std::format("{} {} {}", req.method, req.target, req.version);
  66. auto status = res.status;
  67. // A static file is sent by a content provider, which leaves res.body empty.
  68. auto body_bytes_sent =
  69. req.method == "HEAD" ? 0 : res.get_header_value_u64("Content-Length");
  70. auto http_referer = req.get_header_value("Referer");
  71. if (http_referer.empty()) http_referer = "-";
  72. auto http_user_agent = req.get_header_value("User-Agent");
  73. if (http_user_agent.empty()) http_user_agent = "-";
  74. std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"",
  75. req.remote_addr, remote_user, time_local,
  76. escape_log(request), status, body_bytes_sent,
  77. escape_log(http_referer), escape_log(http_user_agent))
  78. << std::endl;
  79. }
  80. // NGINX Error log format:
  81. // YYYY/MM/DD HH:MM:SS [level] message, client: client_ip, request: "request",
  82. // host: "host"
  83. void nginx_error_logger(const Error &err, const Request *req) {
  84. auto time_local = get_error_time_format();
  85. std::string level = "error";
  86. if (req) {
  87. auto request =
  88. std::format("{} {} {}", req->method, req->target, req->version);
  89. auto host = req->get_header_value("Host");
  90. if (host.empty()) host = "-";
  91. std::cerr << std::format("{} [{}] {}, client: {}, request: "
  92. "\"{}\", host: \"{}\"",
  93. time_local, level, to_string(err),
  94. req->remote_addr, escape_log(request),
  95. escape_log(host))
  96. << std::endl;
  97. } else {
  98. // If no request context, just log the error
  99. std::cerr << std::format("{} [{}] {}", time_local, level, to_string(err))
  100. << std::endl;
  101. }
  102. }
  103. void print_usage(const char *program_name) {
  104. std::cout << "Usage: " << program_name << " [OPTIONS]" << std::endl;
  105. std::cout << std::endl;
  106. std::cout << "Options:" << std::endl;
  107. std::cout << " --host <hostname> Server hostname (default: localhost)"
  108. << std::endl;
  109. std::cout << " --port <port> Server port (default: 8080)"
  110. << std::endl;
  111. std::cout << " --mount <mount:path> Mount point and document root"
  112. << std::endl;
  113. std::cout << " Format: mount_point:document_root"
  114. << std::endl;
  115. std::cout << " (default: /:./html)" << std::endl;
  116. std::cout << " --trusted-proxy <ip> Add trusted proxy IP address"
  117. << std::endl;
  118. std::cout << " (can be specified multiple times)"
  119. << std::endl;
  120. std::cout << " --version Show version information"
  121. << std::endl;
  122. std::cout << " --help Show this help message" << std::endl;
  123. std::cout << std::endl;
  124. std::cout << "Examples:" << std::endl;
  125. std::cout << " " << program_name
  126. << " --host localhost --port 8080 --mount /:./html" << std::endl;
  127. std::cout << " " << program_name
  128. << " --host 0.0.0.0 --port 3000 --mount /api:./api" << std::endl;
  129. std::cout << " " << program_name
  130. << " --trusted-proxy 192.168.1.100 --trusted-proxy 10.0.0.1"
  131. << std::endl;
  132. }
  133. struct ServerConfig {
  134. std::string hostname = "localhost";
  135. int port = 8080;
  136. std::string mount_point = "/";
  137. std::string document_root = "./html";
  138. std::vector<std::string> trusted_proxies;
  139. };
  140. enum class ParseResult { SUCCESS, HELP_REQUESTED, VERSION_REQUESTED, ERROR };
  141. ParseResult parse_command_line(int argc, char *argv[], ServerConfig &config) {
  142. for (int i = 1; i < argc; i++) {
  143. if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
  144. print_usage(argv[0]);
  145. return ParseResult::HELP_REQUESTED;
  146. } else if (strcmp(argv[i], "--host") == 0) {
  147. if (i + 1 >= argc) {
  148. std::cerr << "Error: --host requires a hostname argument" << std::endl;
  149. print_usage(argv[0]);
  150. return ParseResult::ERROR;
  151. }
  152. config.hostname = argv[++i];
  153. } else if (strcmp(argv[i], "--port") == 0) {
  154. if (i + 1 >= argc) {
  155. std::cerr << "Error: --port requires a port number argument"
  156. << std::endl;
  157. print_usage(argv[0]);
  158. return ParseResult::ERROR;
  159. }
  160. config.port = std::atoi(argv[++i]);
  161. if (config.port <= 0 || config.port > 65535) {
  162. std::cerr << "Error: Invalid port number. Must be between 1 and 65535"
  163. << std::endl;
  164. return ParseResult::ERROR;
  165. }
  166. } else if (strcmp(argv[i], "--mount") == 0) {
  167. if (i + 1 >= argc) {
  168. std::cerr
  169. << "Error: --mount requires mount_point:document_root argument"
  170. << std::endl;
  171. print_usage(argv[0]);
  172. return ParseResult::ERROR;
  173. }
  174. std::string mount_arg = argv[++i];
  175. auto colon_pos = mount_arg.find(':');
  176. if (colon_pos == std::string::npos) {
  177. std::cerr << "Error: --mount argument must be in format "
  178. "mount_point:document_root"
  179. << std::endl;
  180. print_usage(argv[0]);
  181. return ParseResult::ERROR;
  182. }
  183. config.mount_point = mount_arg.substr(0, colon_pos);
  184. config.document_root = mount_arg.substr(colon_pos + 1);
  185. if (config.mount_point.empty() || config.document_root.empty()) {
  186. std::cerr
  187. << "Error: Both mount_point and document_root must be non-empty"
  188. << std::endl;
  189. return ParseResult::ERROR;
  190. }
  191. } else if (strcmp(argv[i], "--version") == 0) {
  192. std::cout << CPPHTTPLIB_VERSION << std::endl;
  193. return ParseResult::VERSION_REQUESTED;
  194. } else if (strcmp(argv[i], "--trusted-proxy") == 0) {
  195. if (i + 1 >= argc) {
  196. std::cerr << "Error: --trusted-proxy requires an IP address argument"
  197. << std::endl;
  198. print_usage(argv[0]);
  199. return ParseResult::ERROR;
  200. }
  201. config.trusted_proxies.push_back(argv[++i]);
  202. } else {
  203. std::cerr << "Error: Unknown option '" << argv[i] << "'" << std::endl;
  204. print_usage(argv[0]);
  205. return ParseResult::ERROR;
  206. }
  207. }
  208. return ParseResult::SUCCESS;
  209. }
  210. bool setup_server(Server &svr, const ServerConfig &config) {
  211. svr.set_logger(nginx_access_logger);
  212. svr.set_error_logger(nginx_error_logger);
  213. // Set trusted proxies if specified
  214. if (!config.trusted_proxies.empty()) {
  215. svr.set_trusted_proxies(config.trusted_proxies);
  216. }
  217. auto ret = svr.set_mount_point(config.mount_point, config.document_root);
  218. if (!ret) {
  219. std::cerr
  220. << std::format(
  221. "Error: Cannot mount '{}' to '{}'. Directory may not exist.",
  222. config.mount_point, config.document_root)
  223. << std::endl;
  224. return false;
  225. }
  226. svr.set_file_extension_and_mimetype_mapping("html", "text/html");
  227. svr.set_file_extension_and_mimetype_mapping("htm", "text/html");
  228. svr.set_file_extension_and_mimetype_mapping("css", "text/css");
  229. svr.set_file_extension_and_mimetype_mapping("js", "text/javascript");
  230. svr.set_file_extension_and_mimetype_mapping("json", "application/json");
  231. svr.set_file_extension_and_mimetype_mapping("xml", "application/xml");
  232. svr.set_file_extension_and_mimetype_mapping("png", "image/png");
  233. svr.set_file_extension_and_mimetype_mapping("jpg", "image/jpeg");
  234. svr.set_file_extension_and_mimetype_mapping("jpeg", "image/jpeg");
  235. svr.set_file_extension_and_mimetype_mapping("gif", "image/gif");
  236. svr.set_file_extension_and_mimetype_mapping("svg", "image/svg+xml");
  237. svr.set_file_extension_and_mimetype_mapping("ico", "image/x-icon");
  238. svr.set_file_extension_and_mimetype_mapping("pdf", "application/pdf");
  239. svr.set_file_extension_and_mimetype_mapping("zip", "application/zip");
  240. svr.set_file_extension_and_mimetype_mapping("txt", "text/plain");
  241. svr.set_error_handler([](const Request & /*req*/, Response &res) {
  242. if (res.status == 404) {
  243. res.set_content(
  244. std::format(
  245. "<html><head><title>404 Not Found</title></head>"
  246. "<body><h1>404 Not Found</h1>"
  247. "<p>The requested resource was not found on this server.</p>"
  248. "<hr><p>{}</p></body></html>",
  249. SERVER_NAME),
  250. "text/html");
  251. }
  252. });
  253. svr.set_pre_routing_handler([](const Request & /*req*/, Response &res) {
  254. res.set_header("Server", SERVER_NAME);
  255. return Server::HandlerResponse::Unhandled;
  256. });
  257. signal(SIGINT, signal_handler);
  258. signal(SIGTERM, signal_handler);
  259. return true;
  260. }
  261. int main(int argc, char *argv[]) {
  262. ServerConfig config;
  263. auto result = parse_command_line(argc, argv, config);
  264. switch (result) {
  265. case ParseResult::HELP_REQUESTED:
  266. case ParseResult::VERSION_REQUESTED: return 0;
  267. case ParseResult::ERROR: return 1;
  268. case ParseResult::SUCCESS: break;
  269. }
  270. if (!setup_server(svr, config)) { return 1; }
  271. std::cout << "Serving HTTP on " << config.hostname << ":" << config.port
  272. << std::endl;
  273. std::cout << "Mount point: " << config.mount_point << " -> "
  274. << config.document_root << std::endl;
  275. if (!config.trusted_proxies.empty()) {
  276. std::cout << "Trusted proxies: ";
  277. for (size_t i = 0; i < config.trusted_proxies.size(); ++i) {
  278. if (i > 0) std::cout << ", ";
  279. std::cout << config.trusted_proxies[i];
  280. }
  281. std::cout << std::endl;
  282. }
  283. std::cout << "Press Ctrl+C to shutdown gracefully..." << std::endl;
  284. auto ret = svr.listen(config.hostname, config.port);
  285. std::cout << "Server has been shut down." << std::endl;
  286. return ret ? 0 : 1;
  287. }