|
| 1 | +//======================================================================================================== |
| 2 | +// SPDX-License-Identifier: MIT |
| 3 | +// Copyright (c) 2025 Vinny Parla |
| 4 | +// File: ContentLengthFramer.cpp |
| 5 | +// Purpose: Default Content-Length based framer for MCP stdio transport |
| 6 | +//======================================================================================================== |
| 7 | + |
| 8 | +#include <algorithm> |
| 9 | +#include <cctype> |
| 10 | +#include <limits> |
| 11 | +#include <optional> |
| 12 | +#include <string> |
| 13 | + |
| 14 | +#include "logging/Logger.h" |
| 15 | +#include "mcp/ContentFramer.h" |
| 16 | + |
| 17 | +namespace mcp { |
| 18 | + |
| 19 | +namespace { |
| 20 | +class ContentLengthFramer : public IContentFramer { |
| 21 | +public: |
| 22 | + explicit ContentLengthFramer(std::size_t maxLen) : maxContentLength(maxLen) {} |
| 23 | + |
| 24 | + std::string encode(const std::string& payload) override { |
| 25 | + std::string header = "Content-Length: " + std::to_string(payload.size()) + "\r\n\r\n"; |
| 26 | + std::string frame; frame.reserve(header.size() + payload.size()); |
| 27 | + frame.append(header); |
| 28 | + frame.append(payload); |
| 29 | + return frame; |
| 30 | + } |
| 31 | + |
| 32 | + DecodeResult tryDecodeEx(const std::string& buffer) override { |
| 33 | + const std::string sep = "\r\n\r\n"; |
| 34 | + std::size_t headerEnd = buffer.find(sep); |
| 35 | + if (headerEnd == std::string::npos) { |
| 36 | + return { DecodeStatus::Incomplete, std::nullopt, 0 }; |
| 37 | + } |
| 38 | + |
| 39 | + std::size_t pos = 0; |
| 40 | + std::size_t contentLength = 0; |
| 41 | + bool haveLength = false; |
| 42 | + while (pos < headerEnd) { |
| 43 | + std::size_t eol = buffer.find("\r\n", pos); |
| 44 | + if (eol == std::string::npos || eol > headerEnd) { |
| 45 | + break; |
| 46 | + } |
| 47 | + std::string line = buffer.substr(pos, eol - pos); |
| 48 | + auto colon = line.find(':'); |
| 49 | + if (colon != std::string::npos) { |
| 50 | + std::string name = line.substr(0, colon); |
| 51 | + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c){ return static_cast<char>(std::tolower(c)); }); |
| 52 | + std::string value = line.substr(colon + 1); |
| 53 | + value.erase(value.begin(), std::find_if(value.begin(), value.end(), [](unsigned char ch){ return !std::isspace(ch); })); |
| 54 | + if (name == "content-length") { |
| 55 | + try { |
| 56 | + unsigned long long v64 = std::stoull(value); |
| 57 | + if (v64 > maxContentLength || v64 > std::numeric_limits<std::size_t>::max()) { |
| 58 | + LOG_WARN("Content-Length {} exceeds limits (max={})", v64, maxContentLength); |
| 59 | + return { DecodeStatus::BodyTooLarge, std::nullopt, headerEnd + sep.size() }; |
| 60 | + } |
| 61 | + contentLength = static_cast<std::size_t>(v64); |
| 62 | + haveLength = true; |
| 63 | + } catch (...) { |
| 64 | + LOG_WARN("Invalid Content-Length header: {}", value); |
| 65 | + return { DecodeStatus::InvalidHeader, std::nullopt, headerEnd + sep.size() }; |
| 66 | + } |
| 67 | + } |
| 68 | + } |
| 69 | + pos = eol + 2; |
| 70 | + } |
| 71 | + |
| 72 | + if (!haveLength) { |
| 73 | + LOG_WARN("Missing Content-Length header"); |
| 74 | + return { DecodeStatus::InvalidHeader, std::nullopt, headerEnd + sep.size() }; |
| 75 | + } |
| 76 | + |
| 77 | + const std::size_t headerAndSep = headerEnd + sep.size(); |
| 78 | + if (contentLength > std::numeric_limits<std::size_t>::max() - headerAndSep) { |
| 79 | + LOG_WARN("Frame size overflow detected (header={}, len={})", headerAndSep, contentLength); |
| 80 | + return { DecodeStatus::InvalidHeader, std::nullopt, headerEnd + sep.size() }; |
| 81 | + } |
| 82 | + std::size_t frameTotal = headerAndSep + contentLength; |
| 83 | + if (buffer.size() < frameTotal) { |
| 84 | + return { DecodeStatus::Incomplete, std::nullopt, 0 }; |
| 85 | + } |
| 86 | + |
| 87 | + std::string payload = buffer.substr(headerAndSep, contentLength); |
| 88 | + return { DecodeStatus::Ok, std::make_optional(std::move(payload)), frameTotal }; |
| 89 | + } |
| 90 | + |
| 91 | + std::optional<std::string> tryDecode(std::string& buffer) override { |
| 92 | + DecodeResult r = tryDecodeEx(buffer); |
| 93 | + if (r.status == DecodeStatus::Ok && r.payload.has_value()) { |
| 94 | + if (r.bytesConsumed > 0 && r.bytesConsumed <= buffer.size()) { |
| 95 | + buffer.erase(0, r.bytesConsumed); |
| 96 | + } |
| 97 | + return r.payload; |
| 98 | + } |
| 99 | + return std::nullopt; |
| 100 | + } |
| 101 | + |
| 102 | +private: |
| 103 | + std::size_t maxContentLength; |
| 104 | +}; |
| 105 | +} // namespace |
| 106 | + |
| 107 | +std::unique_ptr<IContentFramer> MakeContentLengthFramer(std::size_t maxContentLength) { |
| 108 | + return std::make_unique<ContentLengthFramer>(maxContentLength); |
| 109 | +} |
| 110 | + |
| 111 | +} // namespace mcp |
0 commit comments