1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors 3 #pragma once 4 5 #include <boost/spirit/home/x3/char/char.hpp> 6 #include <boost/spirit/home/x3/char/char_class.hpp> 7 #include <boost/spirit/home/x3/core/parse.hpp> 8 #include <boost/spirit/home/x3/directive/no_case.hpp> 9 #include <boost/spirit/home/x3/directive/omit.hpp> 10 #include <boost/spirit/home/x3/numeric/uint.hpp> 11 #include <boost/spirit/home/x3/operator/alternative.hpp> 12 #include <boost/spirit/home/x3/operator/kleene.hpp> 13 #include <boost/spirit/home/x3/operator/optional.hpp> 14 #include <boost/spirit/home/x3/operator/plus.hpp> 15 #include <boost/spirit/home/x3/operator/sequence.hpp> 16 #include <boost/spirit/home/x3/string/literal_string.hpp> 17 #include <boost/spirit/home/x3/string/symbols.hpp> 18 19 #include <algorithm> 20 #include <array> 21 #include <cctype> 22 #include <ranges> 23 #include <span> 24 #include <string_view> 25 #include <vector> 26 27 namespace http_helpers 28 { 29 30 enum class ContentType 31 { 32 NoMatch, 33 ANY, // Accepts: */* 34 CBOR, 35 HTML, 36 JSON, 37 OctetStream, 38 EventStream, 39 }; 40 41 inline ContentType getContentType(std::string_view contentTypeHeader) 42 { 43 using boost::spirit::x3::char_; 44 using boost::spirit::x3::lit; 45 using boost::spirit::x3::no_case; 46 using boost::spirit::x3::omit; 47 using boost::spirit::x3::parse; 48 using boost::spirit::x3::space; 49 using boost::spirit::x3::symbols; 50 51 const symbols<ContentType> knownMimeType{ 52 {"application/cbor", ContentType::CBOR}, 53 {"application/json", ContentType::JSON}, 54 {"application/octet-stream", ContentType::OctetStream}, 55 {"text/event-stream", ContentType::EventStream}, 56 {"text/html", ContentType::HTML}}; 57 58 ContentType ct = ContentType::NoMatch; 59 60 auto typeCharset = +(char_("a-zA-Z0-9.+-")); 61 62 auto parameters = 63 *(lit(';') >> *space >> typeCharset >> lit("=") >> typeCharset); 64 auto parser = no_case[knownMimeType] >> omit[parameters]; 65 std::string_view::iterator begin = contentTypeHeader.begin(); 66 if (!parse(begin, contentTypeHeader.end(), parser, ct)) 67 { 68 return ContentType::NoMatch; 69 } 70 if (begin != contentTypeHeader.end()) 71 { 72 return ContentType::NoMatch; 73 } 74 75 return ct; 76 } 77 78 inline ContentType getPreferredContentType( 79 std::string_view acceptsHeader, std::span<const ContentType> preferredOrder) 80 { 81 using boost::spirit::x3::char_; 82 using boost::spirit::x3::lit; 83 using boost::spirit::x3::no_case; 84 using boost::spirit::x3::omit; 85 using boost::spirit::x3::parse; 86 using boost::spirit::x3::space; 87 using boost::spirit::x3::symbols; 88 89 const symbols<ContentType> knownMimeType{ 90 {"application/cbor", ContentType::CBOR}, 91 {"application/json", ContentType::JSON}, 92 {"application/octet-stream", ContentType::OctetStream}, 93 {"text/html", ContentType::HTML}, 94 {"text/event-stream", ContentType::EventStream}, 95 {"*/*", ContentType::ANY}}; 96 97 std::vector<ContentType> ct; 98 99 auto typeCharset = +(char_("a-zA-Z0-9.+-")); 100 101 auto parameters = *(lit(';') >> typeCharset >> lit("=") >> typeCharset); 102 auto mimeType = no_case[knownMimeType] | 103 omit[+typeCharset >> lit('/') >> +typeCharset]; 104 auto parser = +(mimeType >> omit[parameters >> -char_(',') >> *space]); 105 if (!parse(acceptsHeader.begin(), acceptsHeader.end(), parser, ct)) 106 { 107 return ContentType::NoMatch; 108 } 109 110 for (const ContentType parsedType : ct) 111 { 112 if (parsedType == ContentType::ANY) 113 { 114 return parsedType; 115 } 116 auto it = std::ranges::find(preferredOrder, parsedType); 117 if (it != preferredOrder.end()) 118 { 119 return *it; 120 } 121 } 122 123 return ContentType::NoMatch; 124 } 125 126 inline bool isContentTypeAllowed(std::string_view header, ContentType type, 127 bool allowWildcard) 128 { 129 auto types = std::to_array({type}); 130 ContentType allowed = getPreferredContentType(header, types); 131 if (allowed == ContentType::ANY) 132 { 133 return allowWildcard; 134 } 135 136 return type == allowed; 137 } 138 139 enum class Encoding 140 { 141 ParseError, 142 NoMatch, 143 UnencodedBytes, 144 GZIP, 145 ZSTD, 146 ANY, // represents *. Never returned. Only used for string matching 147 }; 148 149 inline Encoding getPreferredEncoding( 150 std::string_view acceptEncoding, 151 const std::span<const Encoding> availableEncodings) 152 { 153 if (acceptEncoding.empty()) 154 { 155 return Encoding::UnencodedBytes; 156 } 157 158 using boost::spirit::x3::char_; 159 using boost::spirit::x3::lit; 160 using boost::spirit::x3::omit; 161 using boost::spirit::x3::parse; 162 using boost::spirit::x3::space; 163 using boost::spirit::x3::symbols; 164 using boost::spirit::x3::uint_; 165 166 const symbols<Encoding> knownAcceptEncoding{{"gzip", Encoding::GZIP}, 167 {"zstd", Encoding::ZSTD}, 168 {"*", Encoding::ANY}}; 169 170 std::vector<Encoding> ct; 171 172 auto parameters = *(lit(';') >> lit("q=") >> uint_ >> -(lit('.') >> uint_)); 173 auto typeCharset = char_("a-zA-Z.+-"); 174 auto encodeType = knownAcceptEncoding | omit[+typeCharset]; 175 auto parser = +(encodeType >> omit[parameters >> -char_(',') >> *space]); 176 if (!parse(acceptEncoding.begin(), acceptEncoding.end(), parser, ct)) 177 { 178 return Encoding::ParseError; 179 } 180 181 for (const Encoding parsedType : ct) 182 { 183 if (parsedType == Encoding::ANY) 184 { 185 if (!availableEncodings.empty()) 186 { 187 return *availableEncodings.begin(); 188 } 189 } 190 auto it = std::ranges::find(availableEncodings, parsedType); 191 if (it != availableEncodings.end()) 192 { 193 return *it; 194 } 195 } 196 197 // Fall back to raw bytes if it was allowed 198 auto it = std::ranges::find(availableEncodings, Encoding::UnencodedBytes); 199 if (it != availableEncodings.end()) 200 { 201 return *it; 202 } 203 204 return Encoding::NoMatch; 205 } 206 207 } // namespace http_helpers 208