xref: /openbmc/bmcweb/http/utility.hpp (revision 2c6ffdb08b2207ff7c31041f77cc3755508d45c4)
1 #pragma once
2 
3 #include "bmcweb_config.h"
4 
5 #include <openssl/crypto.h>
6 
7 #include <boost/callable_traits.hpp>
8 #include <boost/url/parse.hpp>
9 #include <boost/url/url.hpp>
10 #include <boost/url/url_view.hpp>
11 #include <nlohmann/json.hpp>
12 
13 #include <array>
14 #include <chrono>
15 #include <cstddef>
16 #include <cstdint>
17 #include <ctime>
18 #include <functional>
19 #include <iomanip>
20 #include <limits>
21 #include <stdexcept>
22 #include <string>
23 #include <string_view>
24 #include <tuple>
25 #include <type_traits>
26 #include <utility>
27 #include <variant>
28 
29 namespace crow
30 {
31 namespace utility
32 {
33 
34 enum class TypeCode : uint8_t
35 {
36     Unspecified = 0,
37     String = 1,
38     Path = 2,
39     Max = 3,
40 };
41 
42 // Remove when we have c++23
43 template <typename E>
44 constexpr typename std::underlying_type<E>::type toUnderlying(E e) noexcept
45 {
46     return static_cast<typename std::underlying_type<E>::type>(e);
47 }
48 
49 constexpr inline uint64_t getParameterTag(std::string_view url)
50 {
51     uint64_t tagValue = 0;
52     size_t urlSegmentIndex = std::string_view::npos;
53 
54     size_t paramIndex = 0;
55 
56     for (size_t urlIndex = 0; urlIndex < url.size(); urlIndex++)
57     {
58         char character = url[urlIndex];
59         if (character == '<')
60         {
61             if (urlSegmentIndex != std::string_view::npos)
62             {
63                 return 0;
64             }
65             urlSegmentIndex = urlIndex;
66         }
67         if (character == '>')
68         {
69             if (urlSegmentIndex == std::string_view::npos)
70             {
71                 return 0;
72             }
73             std::string_view tag = url.substr(urlSegmentIndex,
74                                               urlIndex + 1 - urlSegmentIndex);
75 
76             // Note, this is a really lame way to do std::pow(6, paramIndex)
77             // std::pow doesn't work in constexpr in clang.
78             // Ideally in the future we'd move this to use a power of 2 packing
79             // (probably 8 instead of 6) so that these just become bit shifts
80             uint64_t insertIndex = 1;
81             for (size_t unused = 0; unused < paramIndex; unused++)
82             {
83                 insertIndex *= 3;
84             }
85 
86             if (tag == "<str>" || tag == "<string>")
87             {
88                 tagValue += insertIndex * toUnderlying(TypeCode::String);
89             }
90             if (tag == "<path>")
91             {
92                 tagValue += insertIndex * toUnderlying(TypeCode::Path);
93             }
94             paramIndex++;
95             urlSegmentIndex = std::string_view::npos;
96         }
97     }
98     if (urlSegmentIndex != std::string_view::npos)
99     {
100         return 0;
101     }
102     return tagValue;
103 }
104 
105 constexpr size_t numArgsFromTag(int tag)
106 {
107     size_t ret = 0;
108     while (tag > 0)
109     {
110         // Move to the next tag by removing the bottom bits from the number
111         tag /= toUnderlying(TypeCode::Max);
112         ret++;
113     }
114     return ret;
115 };
116 
117 inline std::string base64encode(std::string_view data)
118 {
119     const std::array<char, 64> key = {
120         'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
121         'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
122         'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
123         'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
124         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};
125 
126     size_t size = data.size();
127     std::string ret;
128     ret.resize((size + 2) / 3 * 4);
129     auto it = ret.begin();
130 
131     size_t i = 0;
132     while (i < size)
133     {
134         size_t keyIndex = 0;
135 
136         keyIndex = static_cast<size_t>(data[i] & 0xFC) >> 2;
137         *it++ = key[keyIndex];
138 
139         if (i + 1 < size)
140         {
141             keyIndex = static_cast<size_t>(data[i] & 0x03) << 4;
142             keyIndex += static_cast<size_t>(data[i + 1] & 0xF0) >> 4;
143             *it++ = key[keyIndex];
144 
145             if (i + 2 < size)
146             {
147                 keyIndex = static_cast<size_t>(data[i + 1] & 0x0F) << 2;
148                 keyIndex += static_cast<size_t>(data[i + 2] & 0xC0) >> 6;
149                 *it++ = key[keyIndex];
150 
151                 keyIndex = static_cast<size_t>(data[i + 2] & 0x3F);
152                 *it++ = key[keyIndex];
153             }
154             else
155             {
156                 keyIndex = static_cast<size_t>(data[i + 1] & 0x0F) << 2;
157                 *it++ = key[keyIndex];
158                 *it++ = '=';
159             }
160         }
161         else
162         {
163             keyIndex = static_cast<size_t>(data[i] & 0x03) << 4;
164             *it++ = key[keyIndex];
165             *it++ = '=';
166             *it++ = '=';
167         }
168 
169         i += 3;
170     }
171 
172     return ret;
173 }
174 
175 // TODO this is temporary and should be deleted once base64 is refactored out of
176 // crow
177 inline bool base64Decode(std::string_view input, std::string& output)
178 {
179     static const char nop = static_cast<char>(-1);
180     // See note on encoding_data[] in above function
181     static const std::array<char, 256> decodingData = {
182         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
183         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
184         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
185         nop, 62,  nop, nop, nop, 63,  52,  53,  54,  55,  56,  57,  58,  59,
186         60,  61,  nop, nop, nop, nop, nop, nop, nop, 0,   1,   2,   3,   4,
187         5,   6,   7,   8,   9,   10,  11,  12,  13,  14,  15,  16,  17,  18,
188         19,  20,  21,  22,  23,  24,  25,  nop, nop, nop, nop, nop, nop, 26,
189         27,  28,  29,  30,  31,  32,  33,  34,  35,  36,  37,  38,  39,  40,
190         41,  42,  43,  44,  45,  46,  47,  48,  49,  50,  51,  nop, nop, nop,
191         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
192         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
193         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
194         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
195         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
196         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
197         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
198         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
199         nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
200         nop, nop, nop, nop};
201 
202     size_t inputLength = input.size();
203 
204     // allocate space for output string
205     output.clear();
206     output.reserve(((inputLength + 2) / 3) * 4);
207 
208     auto getCodeValue = [](char c) {
209         auto code = static_cast<unsigned char>(c);
210         // Ensure we cannot index outside the bounds of the decoding array
211         static_assert(std::numeric_limits<decltype(code)>::max() <
212                       decodingData.size());
213         return decodingData[code];
214     };
215 
216     // for each 4-bytes sequence from the input, extract 4 6-bits sequences by
217     // dropping first two bits
218     // and regenerate into 3 8-bits sequences
219 
220     for (size_t i = 0; i < inputLength; i++)
221     {
222         char base64code0 = 0;
223         char base64code1 = 0;
224         char base64code2 = 0; // initialized to 0 to suppress warnings
225 
226         base64code0 = getCodeValue(input[i]);
227         if (base64code0 == nop)
228         { // non base64 character
229             return false;
230         }
231         if (!(++i < inputLength))
232         { // we need at least two input bytes for first
233           // byte output
234             return false;
235         }
236         base64code1 = getCodeValue(input[i]);
237         if (base64code1 == nop)
238         { // non base64 character
239             return false;
240         }
241         output +=
242             static_cast<char>((base64code0 << 2) | ((base64code1 >> 4) & 0x3));
243 
244         if (++i < inputLength)
245         {
246             char c = input[i];
247             if (c == '=')
248             { // padding , end of input
249                 return (base64code1 & 0x0f) == 0;
250             }
251             base64code2 = getCodeValue(input[i]);
252             if (base64code2 == nop)
253             { // non base64 character
254                 return false;
255             }
256             output += static_cast<char>(((base64code1 << 4) & 0xf0) |
257                                         ((base64code2 >> 2) & 0x0f));
258         }
259 
260         if (++i < inputLength)
261         {
262             char c = input[i];
263             if (c == '=')
264             { // padding , end of input
265                 return (base64code2 & 0x03) == 0;
266             }
267             char base64code3 = getCodeValue(input[i]);
268             if (base64code3 == nop)
269             { // non base64 character
270                 return false;
271             }
272             output +=
273                 static_cast<char>((((base64code2 << 6) & 0xc0) | base64code3));
274         }
275     }
276 
277     return true;
278 }
279 
280 inline bool constantTimeStringCompare(std::string_view a, std::string_view b)
281 {
282     // Important note, this function is ONLY constant time if the two input
283     // sizes are the same
284     if (a.size() != b.size())
285     {
286         return false;
287     }
288     return CRYPTO_memcmp(a.data(), b.data(), a.size()) == 0;
289 }
290 
291 struct ConstantTimeCompare
292 {
293     bool operator()(std::string_view a, std::string_view b) const
294     {
295         return constantTimeStringCompare(a, b);
296     }
297 };
298 
299 namespace details
300 {
301 inline boost::urls::url
302     appendUrlPieces(boost::urls::url& url,
303                     const std::initializer_list<std::string_view> args)
304 {
305     for (std::string_view arg : args)
306     {
307         url.segments().push_back(arg);
308     }
309     return url;
310 }
311 
312 } // namespace details
313 
314 class OrMorePaths
315 {};
316 
317 template <typename... AV>
318 inline void appendUrlPieces(boost::urls::url& url, const AV... args)
319 {
320     details::appendUrlPieces(url, {args...});
321 }
322 
323 namespace details
324 {
325 
326 // std::reference_wrapper<std::string> - extracts segment to variable
327 //                    std::string_view - checks if segment is equal to variable
328 using UrlSegment = std::variant<std::reference_wrapper<std::string>,
329                                 std::string_view, OrMorePaths>;
330 
331 enum class UrlParseResult
332 {
333     Continue,
334     Fail,
335     Done,
336 };
337 
338 class UrlSegmentMatcherVisitor
339 {
340   public:
341     UrlParseResult operator()(std::string& output)
342     {
343         output = segment;
344         return UrlParseResult::Continue;
345     }
346 
347     UrlParseResult operator()(std::string_view expected)
348     {
349         if (segment == expected)
350         {
351             return UrlParseResult::Continue;
352         }
353         return UrlParseResult::Fail;
354     }
355 
356     UrlParseResult operator()(OrMorePaths /*unused*/)
357     {
358         return UrlParseResult::Done;
359     }
360 
361     explicit UrlSegmentMatcherVisitor(std::string_view segmentIn) :
362         segment(segmentIn)
363     {}
364 
365   private:
366     std::string_view segment;
367 };
368 
369 inline bool readUrlSegments(boost::urls::url_view url,
370                             std::initializer_list<UrlSegment>&& segments)
371 {
372     boost::urls::segments_view urlSegments = url.segments();
373 
374     if (!urlSegments.is_absolute())
375     {
376         return false;
377     }
378 
379     boost::urls::segments_view::iterator it = urlSegments.begin();
380     boost::urls::segments_view::iterator end = urlSegments.end();
381 
382     for (const auto& segment : segments)
383     {
384         if (it == end)
385         {
386             // If the request ends with an "any" path, this was successful
387             return std::holds_alternative<OrMorePaths>(segment);
388         }
389         UrlParseResult res = std::visit(UrlSegmentMatcherVisitor(*it), segment);
390         if (res == UrlParseResult::Done)
391         {
392             return true;
393         }
394         if (res == UrlParseResult::Fail)
395         {
396             return false;
397         }
398         it++;
399     }
400 
401     // There will be an empty segment at the end if the URI ends with a "/"
402     // e.g. /redfish/v1/Chassis/
403     if ((it != end) && urlSegments.back().empty())
404     {
405         it++;
406     }
407     return it == end;
408 }
409 
410 } // namespace details
411 
412 template <typename... Args>
413 inline bool readUrlSegments(boost::urls::url_view url, Args&&... args)
414 {
415     return details::readUrlSegments(url, {std::forward<Args>(args)...});
416 }
417 
418 inline boost::urls::url replaceUrlSegment(boost::urls::url_view urlView,
419                                           const uint replaceLoc,
420                                           std::string_view newSegment)
421 {
422     boost::urls::segments_view urlSegments = urlView.segments();
423     boost::urls::url url("/");
424 
425     if (!urlSegments.is_absolute())
426     {
427         return url;
428     }
429 
430     boost::urls::segments_view::iterator it = urlSegments.begin();
431     boost::urls::segments_view::iterator end = urlSegments.end();
432 
433     for (uint idx = 0; it != end; it++, idx++)
434     {
435         if (idx == replaceLoc)
436         {
437             url.segments().push_back(newSegment);
438         }
439         else
440         {
441             url.segments().push_back(*it);
442         }
443     }
444 
445     return url;
446 }
447 
448 inline std::string setProtocolDefaults(boost::urls::url_view urlView)
449 {
450     if (urlView.scheme() == "https")
451     {
452         return "https";
453     }
454     if (urlView.scheme() == "http")
455     {
456         if (bmcwebInsecureEnableHttpPushStyleEventing)
457         {
458             return "http";
459         }
460         return "";
461     }
462     if (urlView.scheme() == "snmp")
463     {
464         return "snmp";
465     }
466     return "";
467 }
468 
469 inline uint16_t setPortDefaults(boost::urls::url_view url)
470 {
471     uint16_t port = url.port_number();
472     if (port != 0)
473     {
474         // user picked a port already.
475         return port;
476     }
477 
478     // If the user hasn't explicitly stated a port, pick one explicitly for them
479     // based on the protocol defaults
480     if (url.scheme() == "http")
481     {
482         return 80;
483     }
484     if (url.scheme() == "https")
485     {
486         return 443;
487     }
488     if (url.scheme() == "snmp")
489     {
490         return 162;
491     }
492     return 0;
493 }
494 
495 inline bool validateAndSplitUrl(std::string_view destUrl, std::string& urlProto,
496                                 std::string& host, uint16_t& port,
497                                 std::string& path)
498 {
499     boost::urls::result<boost::urls::url_view> url =
500         boost::urls::parse_uri(destUrl);
501     if (!url)
502     {
503         return false;
504     }
505     urlProto = setProtocolDefaults(url.value());
506     if (urlProto.empty())
507     {
508         return false;
509     }
510 
511     port = setPortDefaults(url.value());
512 
513     host = url->encoded_host();
514 
515     path = url->encoded_path();
516     if (path.empty())
517     {
518         path = "/";
519     }
520     if (url->has_fragment())
521     {
522         path += '#';
523         path += url->encoded_fragment();
524     }
525 
526     if (url->has_query())
527     {
528         path += '?';
529         path += url->encoded_query();
530     }
531 
532     return true;
533 }
534 
535 } // namespace utility
536 } // namespace crow
537 
538 namespace nlohmann
539 {
540 template <>
541 struct adl_serializer<boost::urls::url>
542 {
543     // nlohmann requires a specific casing to look these up in adl
544     // NOLINTNEXTLINE(readability-identifier-naming)
545     static void to_json(json& j, const boost::urls::url& url)
546     {
547         j = url.buffer();
548     }
549 };
550 
551 template <>
552 struct adl_serializer<boost::urls::url_view>
553 {
554     // NOLINTNEXTLINE(readability-identifier-naming)
555     static void to_json(json& j, boost::urls::url_view url)
556     {
557         j = url.buffer();
558     }
559 };
560 } // namespace nlohmann
561