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