1 #pragma once 2 3 #include <boost/spirit/home/x3.hpp> 4 5 #include <algorithm> 6 #include <cctype> 7 #include <iomanip> 8 #include <ostream> 9 #include <ranges> 10 #include <span> 11 #include <string> 12 #include <string_view> 13 #include <vector> 14 15 namespace http_helpers 16 { 17 18 enum class ContentType 19 { 20 NoMatch, 21 ANY, // Accepts: */* 22 CBOR, 23 HTML, 24 JSON, 25 OctetStream, 26 EventStream, 27 }; 28 29 inline ContentType getPreferredContentType( 30 std::string_view header, std::span<const ContentType> preferredOrder) 31 { 32 using boost::spirit::x3::char_; 33 using boost::spirit::x3::lit; 34 using boost::spirit::x3::omit; 35 using boost::spirit::x3::parse; 36 using boost::spirit::x3::space; 37 using boost::spirit::x3::symbols; 38 using boost::spirit::x3::uint_; 39 40 const symbols<ContentType> knownMimeType{ 41 {"application/cbor", ContentType::CBOR}, 42 {"application/json", ContentType::JSON}, 43 {"application/octet-stream", ContentType::OctetStream}, 44 {"text/html", ContentType::HTML}, 45 {"text/event-stream", ContentType::EventStream}, 46 {"*/*", ContentType::ANY}}; 47 48 std::vector<ContentType> ct; 49 50 auto typeCharset = +(char_("a-zA-Z0-9.+-")); 51 52 auto parameters = *(lit(';') >> typeCharset >> lit("=") >> typeCharset); 53 auto mimeType = knownMimeType | 54 omit[+typeCharset >> lit('/') >> +typeCharset]; 55 auto parser = +(mimeType >> omit[parameters >> -char_(',') >> *space]); 56 if (!parse(header.begin(), header.end(), parser, ct)) 57 { 58 return ContentType::NoMatch; 59 } 60 61 for (const ContentType parsedType : ct) 62 { 63 if (parsedType == ContentType::ANY) 64 { 65 return parsedType; 66 } 67 auto it = std::ranges::find(preferredOrder, parsedType); 68 if (it != preferredOrder.end()) 69 { 70 return *it; 71 } 72 } 73 74 return ContentType::NoMatch; 75 } 76 77 inline bool isContentTypeAllowed(std::string_view header, ContentType type, 78 bool allowWildcard) 79 { 80 auto types = std::to_array({type}); 81 ContentType allowed = getPreferredContentType(header, types); 82 if (allowed == ContentType::ANY) 83 { 84 return allowWildcard; 85 } 86 87 return type == allowed; 88 } 89 90 enum class Encoding 91 { 92 ParseError, 93 NoMatch, 94 UnencodedBytes, 95 GZIP, 96 ZSTD, 97 ANY, // represents *. Never returned. Only used for string matching 98 }; 99 100 inline Encoding 101 getPreferredEncoding(std::string_view acceptEncoding, 102 const std::span<const Encoding> availableEncodings) 103 { 104 if (acceptEncoding.empty()) 105 { 106 return Encoding::UnencodedBytes; 107 } 108 109 using boost::spirit::x3::char_; 110 using boost::spirit::x3::lit; 111 using boost::spirit::x3::omit; 112 using boost::spirit::x3::parse; 113 using boost::spirit::x3::space; 114 using boost::spirit::x3::symbols; 115 using boost::spirit::x3::uint_; 116 117 const symbols<Encoding> knownAcceptEncoding{{"gzip", Encoding::GZIP}, 118 {"zstd", Encoding::ZSTD}, 119 {"*", Encoding::ANY}}; 120 121 std::vector<Encoding> ct; 122 123 auto parameters = *(lit(';') >> lit("q=") >> uint_ >> -(lit('.') >> uint_)); 124 auto typeCharset = char_("a-zA-Z.+-"); 125 auto encodeType = knownAcceptEncoding | omit[+typeCharset]; 126 auto parser = +(encodeType >> omit[parameters >> -char_(',') >> *space]); 127 if (!parse(acceptEncoding.begin(), acceptEncoding.end(), parser, ct)) 128 { 129 return Encoding::ParseError; 130 } 131 132 for (const Encoding parsedType : ct) 133 { 134 if (parsedType == Encoding::ANY) 135 { 136 if (!availableEncodings.empty()) 137 { 138 return *availableEncodings.begin(); 139 } 140 } 141 auto it = std::ranges::find(availableEncodings, parsedType); 142 if (it != availableEncodings.end()) 143 { 144 return *it; 145 } 146 } 147 148 // Fall back to raw bytes if it was allowed 149 auto it = std::ranges::find(availableEncodings, Encoding::UnencodedBytes); 150 if (it != availableEncodings.end()) 151 { 152 return *it; 153 } 154 155 return Encoding::NoMatch; 156 } 157 158 } // namespace http_helpers 159