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 parameters = *(lit(';') >> lit("q=") >> uint_ >> -(lit('.') >> uint_)); 51 auto typeCharset = char_("a-zA-Z.+-"); 52 auto mimeType = knownMimeType | 53 omit[+typeCharset >> lit('/') >> +typeCharset]; 54 auto parser = +(mimeType >> omit[parameters >> -char_(',') >> *space]); 55 if (!parse(header.begin(), header.end(), parser, ct)) 56 { 57 return ContentType::NoMatch; 58 } 59 60 for (const ContentType parsedType : ct) 61 { 62 if (parsedType == ContentType::ANY) 63 { 64 return parsedType; 65 } 66 auto it = std::ranges::find(preferredOrder, parsedType); 67 if (it != preferredOrder.end()) 68 { 69 return *it; 70 } 71 } 72 73 return ContentType::NoMatch; 74 } 75 76 inline bool isContentTypeAllowed(std::string_view header, ContentType type, 77 bool allowWildcard) 78 { 79 auto types = std::to_array({type}); 80 ContentType allowed = getPreferredContentType(header, types); 81 if (allowed == ContentType::ANY) 82 { 83 return allowWildcard; 84 } 85 86 return type == allowed; 87 } 88 89 enum class Encoding 90 { 91 ParseError, 92 NoMatch, 93 UnencodedBytes, 94 GZIP, 95 ZSTD, 96 ANY, // represents *. Never returned. Only used for string matching 97 }; 98 99 inline Encoding 100 getPreferredEncoding(std::string_view acceptEncoding, 101 const std::span<const Encoding> availableEncodings) 102 { 103 if (acceptEncoding.empty()) 104 { 105 return Encoding::UnencodedBytes; 106 } 107 108 using boost::spirit::x3::char_; 109 using boost::spirit::x3::lit; 110 using boost::spirit::x3::omit; 111 using boost::spirit::x3::parse; 112 using boost::spirit::x3::space; 113 using boost::spirit::x3::symbols; 114 using boost::spirit::x3::uint_; 115 116 const symbols<Encoding> knownAcceptEncoding{{"gzip", Encoding::GZIP}, 117 {"zstd", Encoding::ZSTD}, 118 {"*", Encoding::ANY}}; 119 120 std::vector<Encoding> ct; 121 122 auto parameters = *(lit(';') >> lit("q=") >> uint_ >> -(lit('.') >> uint_)); 123 auto typeCharset = char_("a-zA-Z.+-"); 124 auto encodeType = knownAcceptEncoding | omit[+typeCharset]; 125 auto parser = +(encodeType >> omit[parameters >> -char_(',') >> *space]); 126 if (!parse(acceptEncoding.begin(), acceptEncoding.end(), parser, ct)) 127 { 128 return Encoding::ParseError; 129 } 130 131 for (const Encoding parsedType : ct) 132 { 133 if (parsedType == Encoding::ANY) 134 { 135 if (!availableEncodings.empty()) 136 { 137 return *availableEncodings.begin(); 138 } 139 } 140 auto it = std::ranges::find(availableEncodings, parsedType); 141 if (it != availableEncodings.end()) 142 { 143 return *it; 144 } 145 } 146 147 // Fall back to raw bytes if it was allowed 148 auto it = std::ranges::find(availableEncodings, Encoding::UnencodedBytes); 149 if (it != availableEncodings.end()) 150 { 151 return *it; 152 } 153 154 return Encoding::NoMatch; 155 } 156 157 } // namespace http_helpers 158