#pragma once #include #include #include #include #include #include "wireframe/l7/dissector.hpp" // Best-effort, single-segment HTTP/1.x request/status-line parsing (plus // the Host: header for requests). No TCP stream reassembly, so a // message split across multiple packets is only partially visible here // - the same scope DNS already has (single UDP datagram, no // reassembly). Good enough for a one-line summary, not a full dissector. namespace wireframe::net { inline constexpr std::uint16_t kHttpPort = 80; struct HttpMessage { bool is_request; std::string method_or_version; // request: method (GET); response: "HTTP/1.1" std::string target_or_status; // request: target path; response: status code std::optional host; // request only, from a Host: header if present }; inline std::optional parse_http(std::span payload) { std::string_view text(reinterpret_cast(payload.data()), payload.size()); std::size_t line_end = text.find("\r\n"); std::size_t term_len = 2; if (line_end == std::string_view::npos) { line_end = text.find('\n'); term_len = 1; if (line_end == std::string_view::npos) return std::nullopt; } std::string_view first_line = text.substr(0, line_end); std::size_t sp1 = first_line.find(' '); if (sp1 == std::string_view::npos) return std::nullopt; std::size_t sp2 = first_line.find(' ', sp1 + 1); if (sp2 == std::string_view::npos) return std::nullopt; std::string_view field1 = first_line.substr(0, sp1); std::string_view field2 = first_line.substr(sp1 + 1, sp2 - sp1 - 1); HttpMessage msg; if (field1.substr(0, 5) == "HTTP/") { msg.is_request = false; msg.method_or_version = std::string(field1); msg.target_or_status = std::string(field2); return msg; } static constexpr std::string_view kMethods[] = {"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH", "CONNECT", "TRACE"}; bool known_method = false; for (auto method : kMethods) { if (field1 == method) { known_method = true; break; } } if (!known_method) return std::nullopt; msg.is_request = true; msg.method_or_version = std::string(field1); msg.target_or_status = std::string(field2); // Best-effort Host: header scan, bounded by whatever this one // packet contains and terminated at the first blank line (end of // headers) or the end of the payload - never loops past text.size(). std::size_t pos = line_end + term_len; while (pos < text.size()) { std::size_t next_end = text.find("\r\n", pos); std::size_t header_len = (next_end == std::string_view::npos) ? text.size() - pos : next_end - pos; std::string_view header_line = text.substr(pos, header_len); if (header_line.empty()) break; // blank line: end of headers if (header_line.size() > 5 && (header_line.substr(0, 5) == "Host:" || header_line.substr(0, 5) == "host:")) { std::size_t value_start = 5; while (value_start < header_line.size() && header_line[value_start] == ' ') { ++value_start; } msg.host = std::string(header_line.substr(value_start)); } if (next_end == std::string_view::npos) break; pos = next_end + 2; } return msg; } class HttpDissector : public L7Dissector { public: std::uint16_t port() const override { return kHttpPort; } std::optional summarize(std::span payload) const override { auto msg = parse_http(payload); if (!msg) return std::nullopt; std::string out = "HTTP " + msg->method_or_version + " " + msg->target_or_status; if (msg->host) out += " Host: " + *msg->host; return out; } }; } // namespace wireframe::net