xref: /openbmc/bmcweb/include/http_utility.hpp (revision 40e9b92ec19acffb46f83a6e55b18974da5d708e)
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