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