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
getContentType(std::string_view contentTypeHeader)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
getPreferredContentType(std::string_view acceptsHeader,std::span<const ContentType> preferredOrder)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
isContentTypeAllowed(std::string_view header,ContentType type,bool allowWildcard)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
getPreferredEncoding(std::string_view acceptEncoding,const std::span<const Encoding> availableEncodings)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