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