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