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 getContentType(std::string_view contentTypeHeader) 30 { 31 using boost::spirit::x3::char_; 32 using boost::spirit::x3::lit; 33 using boost::spirit::x3::no_case; 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/event-stream", ContentType::EventStream}, 45 {"text/html", ContentType::HTML}}; 46 47 ContentType ct = ContentType::NoMatch; 48 49 auto typeCharset = +(char_("a-zA-Z0-9.+-")); 50 51 auto parameters = 52 *(lit(';') >> *space >> typeCharset >> lit("=") >> typeCharset); 53 auto parser = no_case[knownMimeType] >> omit[parameters]; 54 std::string_view::iterator begin = contentTypeHeader.begin(); 55 if (!parse(begin, contentTypeHeader.end(), parser, ct)) 56 { 57 return ContentType::NoMatch; 58 } 59 if (begin != contentTypeHeader.end()) 60 { 61 return ContentType::NoMatch; 62 } 63 64 return ct; 65 } 66 67 inline ContentType getPreferredContentType( 68 std::string_view acceptsHeader, std::span<const ContentType> preferredOrder) 69 { 70 using boost::spirit::x3::char_; 71 using boost::spirit::x3::lit; 72 using boost::spirit::x3::no_case; 73 using boost::spirit::x3::omit; 74 using boost::spirit::x3::parse; 75 using boost::spirit::x3::space; 76 using boost::spirit::x3::symbols; 77 using boost::spirit::x3::uint_; 78 79 const symbols<ContentType> knownMimeType{ 80 {"application/cbor", ContentType::CBOR}, 81 {"application/json", ContentType::JSON}, 82 {"application/octet-stream", ContentType::OctetStream}, 83 {"text/html", ContentType::HTML}, 84 {"text/event-stream", ContentType::EventStream}, 85 {"*/*", ContentType::ANY}}; 86 87 std::vector<ContentType> ct; 88 89 auto typeCharset = +(char_("a-zA-Z0-9.+-")); 90 91 auto parameters = *(lit(';') >> typeCharset >> lit("=") >> typeCharset); 92 auto mimeType = no_case[knownMimeType] | 93 omit[+typeCharset >> lit('/') >> +typeCharset]; 94 auto parser = +(mimeType >> omit[parameters >> -char_(',') >> *space]); 95 if (!parse(acceptsHeader.begin(), acceptsHeader.end(), parser, ct)) 96 { 97 return ContentType::NoMatch; 98 } 99 100 for (const ContentType parsedType : ct) 101 { 102 if (parsedType == ContentType::ANY) 103 { 104 return parsedType; 105 } 106 auto it = std::ranges::find(preferredOrder, parsedType); 107 if (it != preferredOrder.end()) 108 { 109 return *it; 110 } 111 } 112 113 return ContentType::NoMatch; 114 } 115 116 inline bool isContentTypeAllowed(std::string_view header, ContentType type, 117 bool allowWildcard) 118 { 119 auto types = std::to_array({type}); 120 ContentType allowed = getPreferredContentType(header, types); 121 if (allowed == ContentType::ANY) 122 { 123 return allowWildcard; 124 } 125 126 return type == allowed; 127 } 128 129 enum class Encoding 130 { 131 ParseError, 132 NoMatch, 133 UnencodedBytes, 134 GZIP, 135 ZSTD, 136 ANY, // represents *. Never returned. Only used for string matching 137 }; 138 139 inline Encoding 140 getPreferredEncoding(std::string_view acceptEncoding, 141 const std::span<const Encoding> availableEncodings) 142 { 143 if (acceptEncoding.empty()) 144 { 145 return Encoding::UnencodedBytes; 146 } 147 148 using boost::spirit::x3::char_; 149 using boost::spirit::x3::lit; 150 using boost::spirit::x3::omit; 151 using boost::spirit::x3::parse; 152 using boost::spirit::x3::space; 153 using boost::spirit::x3::symbols; 154 using boost::spirit::x3::uint_; 155 156 const symbols<Encoding> knownAcceptEncoding{{"gzip", Encoding::GZIP}, 157 {"zstd", Encoding::ZSTD}, 158 {"*", Encoding::ANY}}; 159 160 std::vector<Encoding> ct; 161 162 auto parameters = *(lit(';') >> lit("q=") >> uint_ >> -(lit('.') >> uint_)); 163 auto typeCharset = char_("a-zA-Z.+-"); 164 auto encodeType = knownAcceptEncoding | omit[+typeCharset]; 165 auto parser = +(encodeType >> omit[parameters >> -char_(',') >> *space]); 166 if (!parse(acceptEncoding.begin(), acceptEncoding.end(), parser, ct)) 167 { 168 return Encoding::ParseError; 169 } 170 171 for (const Encoding parsedType : ct) 172 { 173 if (parsedType == Encoding::ANY) 174 { 175 if (!availableEncodings.empty()) 176 { 177 return *availableEncodings.begin(); 178 } 179 } 180 auto it = std::ranges::find(availableEncodings, parsedType); 181 if (it != availableEncodings.end()) 182 { 183 return *it; 184 } 185 } 186 187 // Fall back to raw bytes if it was allowed 188 auto it = std::ranges::find(availableEncodings, Encoding::UnencodedBytes); 189 if (it != availableEncodings.end()) 190 { 191 return *it; 192 } 193 194 return Encoding::NoMatch; 195 } 196 197 } // namespace http_helpers 198