diff options
-rw-r--r-- | http/http_connection.h | 28 | ||||
-rw-r--r-- | http/utility.h | 41 | ||||
-rw-r--r-- | include/json_html_serializer.hpp | 614 | ||||
-rw-r--r-- | static/redfish.css | 19 | ||||
-rw-r--r-- | static/styles/json.css | 10 |
5 files changed, 646 insertions, 66 deletions
diff --git a/http/http_connection.h b/http/http_connection.h index 7e23de4786..b07459df92 100644 --- a/http/http_connection.h +++ b/http/http_connection.h @@ -18,6 +18,7 @@ #include <boost/beast/http.hpp> #include <boost/beast/ssl/ssl_stream.hpp> #include <boost/beast/websocket.hpp> +#include <json_html_serializer.hpp> #include <security_headers.hpp> #include <ssl_key_handler.hpp> @@ -30,31 +31,8 @@ namespace crow inline void prettyPrintJson(crow::Response& res) { - std::string value = res.jsonValue.dump(4, ' ', true); - utility::escapeHtml(value); - utility::convertToLinks(value); - res.body() = "<html>\n" - "<head>\n" - "<title>Redfish API</title>\n" - "<link rel=\"stylesheet\" type=\"text/css\" " - "href=\"/styles/default.css\">\n" - "<script src=\"/highlight.pack.js\"></script>" - "<script>hljs.initHighlightingOnLoad();</script>" - "</head>\n" - "<body>\n" - "<div style=\"max-width: 576px;margin:0 auto;\">\n" - "<img src=\"/DMTF_Redfish_logo_2017.svg\" alt=\"redfish\" " - "height=\"406px\" " - "width=\"576px\">\n" - "<br>\n" - "<pre>\n" - "<code class=\"json\">" + - value + - "</code>\n" - "</pre>\n" - "</div>\n" - "</body>\n" - "</html>\n"; + json_html_util::dumpHtml(res.body(), res.jsonValue); + res.addHeader("Content-Type", "text/html;charset=UTF-8"); } diff --git a/http/utility.h b/http/utility.h index 02b136cd4a..ec0a88939d 100644 --- a/http/utility.h +++ b/http/utility.h @@ -733,47 +733,6 @@ inline bool base64Decode(const std::string_view input, std::string& output) return true; } -inline void escapeHtml(std::string& data) -{ - std::string buffer; - // less than 5% of characters should be larger, so reserve a buffer of the - // right size - buffer.reserve(data.size() * 11 / 10); - for (size_t pos = 0; pos != data.size(); ++pos) - { - switch (data[pos]) - { - case '&': - buffer.append("&"); - break; - case '\"': - buffer.append("""); - break; - case '\'': - buffer.append("'"); - break; - case '<': - buffer.append("<"); - break; - case '>': - buffer.append(">"); - break; - default: - buffer.append(&data[pos], 1); - break; - } - } - data.swap(buffer); -} - -inline void convertToLinks(std::string& s) -{ - // Convert anything with a redfish path into a link - const std::regex redfishPath{"("((.*))"[ \\n]*:[ " - "\\n]*)("((?!")/redfish/.*)")"}; - s = std::regex_replace(s, redfishPath, "$1<a href=\"$5\">$4</a>"); -} - /** * Method returns Date Time information according to requested format * diff --git a/include/json_html_serializer.hpp b/include/json_html_serializer.hpp new file mode 100644 index 0000000000..a7d5297b6f --- /dev/null +++ b/include/json_html_serializer.hpp @@ -0,0 +1,614 @@ +#include <nlohmann/json.hpp> + +#include <algorithm> + +namespace json_html_util +{ + +static constexpr uint8_t utf8Accept = 0; +static constexpr uint8_t utf8Reject = 1; + +inline uint8_t decode(uint8_t& state, uint32_t& codePoint, + const uint8_t byte) noexcept +{ + // clang-format off + static const std::array<std::uint8_t, 400> utf8d = + { + { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00..1F + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20..3F + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40..5F + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60..7F + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // 80..9F + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // A0..BF + 8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // C0..DF + 0xA, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, // E0..EF + 0xB, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, // F0..FF + 0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, // s0..s0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, // s1..s2 + 1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, // s3..s4 + 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, // s5..s6 + 1, 3, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // s7..s8 + } + }; + // clang-format on + + if (state > 0x8) + { + return state; + } + + const uint8_t type = utf8d[byte]; + + codePoint = (state != utf8Accept) + ? (byte & 0x3fu) | (codePoint << 6) + : static_cast<uint32_t>(0xff >> type) & (byte); + + state = utf8d[256u + state * 16u + type]; + return state; +} + +inline void dumpEscaped(std::string& out, const std::string& str) +{ + std::array<char, 512> stringBuffer{{}}; + uint32_t codePoint = 0; + uint8_t state = utf8Accept; + std::size_t bytes = 0; // number of bytes written to string_buffer + + // number of bytes written at the point of the last valid byte + std::size_t bytes_after_last_accept = 0; + std::size_t undumpedChars = 0; + + for (std::size_t i = 0; i < str.size(); ++i) + { + const uint8_t byte = static_cast<uint8_t>(str[i]); + + switch (decode(state, codePoint, byte)) + { + case utf8Accept: // decode found a new code point + { + switch (codePoint) + { + case 0x08: // backspace + { + stringBuffer[bytes++] = '\\'; + stringBuffer[bytes++] = 'b'; + break; + } + + case 0x09: // horizontal tab + { + stringBuffer[bytes++] = '\\'; + stringBuffer[bytes++] = 't'; + break; + } + + case 0x0A: // newline + { + stringBuffer[bytes++] = '\\'; + stringBuffer[bytes++] = 'n'; + break; + } + + case 0x0C: // formfeed + { + stringBuffer[bytes++] = '\\'; + stringBuffer[bytes++] = 'f'; + break; + } + + case 0x0D: // carriage return + { + stringBuffer[bytes++] = '\\'; + stringBuffer[bytes++] = 'r'; + break; + } + + case 0x22: // quotation mark + { + stringBuffer[bytes++] = '&'; + stringBuffer[bytes++] = 'q'; + stringBuffer[bytes++] = 'u'; + stringBuffer[bytes++] = 'o'; + stringBuffer[bytes++] = 't'; + stringBuffer[bytes++] = ';'; + break; + } + + case 0x27: // apostrophe + { + stringBuffer[bytes++] = '&'; + stringBuffer[bytes++] = 'a'; + stringBuffer[bytes++] = 'p'; + stringBuffer[bytes++] = 'o'; + stringBuffer[bytes++] = 's'; + stringBuffer[bytes++] = ';'; + break; + } + + case 0x26: // ampersand + { + stringBuffer[bytes++] = '&'; + stringBuffer[bytes++] = 'a'; + stringBuffer[bytes++] = 'm'; + stringBuffer[bytes++] = 'p'; + stringBuffer[bytes++] = ';'; + break; + } + + case 0x3C: // less than + { + stringBuffer[bytes++] = '\\'; + stringBuffer[bytes++] = 'l'; + stringBuffer[bytes++] = 't'; + stringBuffer[bytes++] = ';'; + break; + } + + case 0x3E: // greater than + { + stringBuffer[bytes++] = '\\'; + stringBuffer[bytes++] = 'g'; + stringBuffer[bytes++] = 't'; + stringBuffer[bytes++] = ';'; + break; + } + + default: + { + // escape control characters (0x00..0x1F) + if ((codePoint <= 0x1F) or (codePoint >= 0x7F)) + { + if (codePoint <= 0xFFFF) + { + (std::snprintf)( + stringBuffer.data() + bytes, 7, "\\u%04x", + static_cast<uint16_t>(codePoint)); + bytes += 6; + } + else + { + (std::snprintf)( + stringBuffer.data() + bytes, 13, + "\\u%04x\\u%04x", + static_cast<uint16_t>(0xD7C0 + + (codePoint >> 10)), + static_cast<uint16_t>(0xDC00 + + (codePoint & 0x3FF))); + bytes += 12; + } + } + else + { + // copy byte to buffer (all previous bytes + // been copied have in default case above) + stringBuffer[bytes++] = str[i]; + } + break; + } + } + + // write buffer and reset index; there must be 13 bytes + // left, as this is the maximal number of bytes to be + // written ("\uxxxx\uxxxx\0") for one code point + if (stringBuffer.size() - bytes < 13) + { + out.append(stringBuffer.data(), bytes); + bytes = 0; + } + + // remember the byte position of this accept + bytes_after_last_accept = bytes; + undumpedChars = 0; + break; + } + + case utf8Reject: // decode found invalid UTF-8 byte + { + // in case we saw this character the first time, we + // would like to read it again, because the byte + // may be OK for itself, but just not OK for the + // previous sequence + if (undumpedChars > 0) + { + --i; + } + + // reset length buffer to the last accepted index; + // thus removing/ignoring the invalid characters + bytes = bytes_after_last_accept; + + stringBuffer[bytes++] = '\\'; + stringBuffer[bytes++] = 'u'; + stringBuffer[bytes++] = 'f'; + stringBuffer[bytes++] = 'f'; + stringBuffer[bytes++] = 'f'; + stringBuffer[bytes++] = 'd'; + + bytes_after_last_accept = bytes; + + undumpedChars = 0; + + // continue processing the string + state = utf8Accept; + break; + + break; + } + + default: // decode found yet incomplete multi-byte code point + { + ++undumpedChars; + break; + } + } + } + + // we finished processing the string + if (state == utf8Accept) + { + // write buffer + if (bytes > 0) + { + out.append(stringBuffer.data(), bytes); + } + } + else + { + // write all accepted bytes + out.append(stringBuffer.data(), bytes_after_last_accept); + out += "\\ufffd"; + } +} + +inline unsigned int countDigits(uint64_t number) noexcept +{ + unsigned int n_digits = 1; + for (;;) + { + if (number < 10) + { + return n_digits; + } + if (number < 100) + { + return n_digits + 1; + } + if (number < 1000) + { + return n_digits + 2; + } + if (number < 10000) + { + return n_digits + 3; + } + number = number / 10000u; + n_digits += 4; + } +} + +template <typename NumberType, + std::enable_if_t<std::is_same<NumberType, uint64_t>::value or + std::is_same<NumberType, int64_t>::value, + int> = 0> +void dumpInteger(std::string& out, NumberType number) +{ + std::array<char, 64> numberbuffer{{}}; + + static constexpr std::array<std::array<char, 2>, 100> digits_to_99{{ + {'0', '0'}, {'0', '1'}, {'0', '2'}, {'0', '3'}, {'0', '4'}, {'0', '5'}, + {'0', '6'}, {'0', '7'}, {'0', '8'}, {'0', '9'}, {'1', '0'}, {'1', '1'}, + {'1', '2'}, {'1', '3'}, {'1', '4'}, {'1', '5'}, {'1', '6'}, {'1', '7'}, + {'1', '8'}, {'1', '9'}, {'2', '0'}, {'2', '1'}, {'2', '2'}, {'2', '3'}, + {'2', '4'}, {'2', '5'}, {'2', '6'}, {'2', '7'}, {'2', '8'}, {'2', '9'}, + {'3', '0'}, {'3', '1'}, {'3', '2'}, {'3', '3'}, {'3', '4'}, {'3', '5'}, + {'3', '6'}, {'3', '7'}, {'3', '8'}, {'3', '9'}, {'4', '0'}, {'4', '1'}, + {'4', '2'}, {'4', '3'}, {'4', '4'}, {'4', '5'}, {'4', '6'}, {'4', '7'}, + {'4', '8'}, {'4', '9'}, {'5', '0'}, {'5', '1'}, {'5', '2'}, {'5', '3'}, + {'5', '4'}, {'5', '5'}, {'5', '6'}, {'5', '7'}, {'5', '8'}, {'5', '9'}, + {'6', '0'}, {'6', '1'}, {'6', '2'}, {'6', '3'}, {'6', '4'}, {'6', '5'}, + {'6', '6'}, {'6', '7'}, {'6', '8'}, {'6', '9'}, {'7', '0'}, {'7', '1'}, + {'7', '2'}, {'7', '3'}, {'7', '4'}, {'7', '5'}, {'7', '6'}, {'7', '7'}, + {'7', '8'}, {'7', '9'}, {'8', '0'}, {'8', '1'}, {'8', '2'}, {'8', '3'}, + {'8', '4'}, {'8', '5'}, {'8', '6'}, {'8', '7'}, {'8', '8'}, {'8', '9'}, + {'9', '0'}, {'9', '1'}, {'9', '2'}, {'9', '3'}, {'9', '4'}, {'9', '5'}, + {'9', '6'}, {'9', '7'}, {'9', '8'}, {'9', '9'}, + }}; + + // special case for "0" + if (number == 0) + { + out += '0'; + return; + } + + // use a pointer to fill the buffer + auto bufferPtr = begin(numberbuffer); + + const bool isNegative = std::is_same<NumberType, int64_t>::value && + !(number >= 0); // see issue #755 + uint64_t absValue; + + unsigned int nChars; + + if (isNegative) + { + *bufferPtr = '-'; + absValue = static_cast<uint64_t>(0 - number); + + // account one more byte for the minus sign + nChars = 1 + countDigits(absValue); + } + else + { + absValue = static_cast<uint64_t>(number); + nChars = countDigits(absValue); + } + + // spare 1 byte for '\0' + if (nChars >= numberbuffer.size() - 1) + { + return; + } + + // jump to the end to generate the string from backward + // so we later avoid reversing the result + bufferPtr += nChars; + + // Fast int2ascii implementation inspired by "Fastware" talk by Andrei + // Alexandrescu See: https://www.youtube.com/watch?v=o4-CwDo2zpg + while (absValue >= 100) + { + const auto digitsIndex = static_cast<unsigned>((absValue % 100)); + absValue /= 100; + *(--bufferPtr) = digits_to_99[digitsIndex][1]; + *(--bufferPtr) = digits_to_99[digitsIndex][0]; + } + + if (absValue >= 10) + { + const auto digitsIndex = static_cast<unsigned>(absValue); + *(--bufferPtr) = digits_to_99[digitsIndex][1]; + *(--bufferPtr) = digits_to_99[digitsIndex][0]; + } + else + { + *(--bufferPtr) = static_cast<char>('0' + absValue); + } + + out.append(numberbuffer.data(), nChars); +} + +inline void dumpfloat(std::string& out, double number, + std::true_type /*isIeeeSingleOrDouble*/) +{ + std::array<char, 64> numberbuffer{{}}; + char* begin = numberbuffer.data(); + ::nlohmann::detail::to_chars(begin, begin + numberbuffer.size(), number); + + out += begin; +} + +inline void dumpfloat(std::string& out, double number, + std::false_type /*isIeeeSingleOrDouble*/) +{ + std::array<char, 64> numberbuffer{{}}; + // get number of digits for a float -> text -> float round-trip + static constexpr auto d = std::numeric_limits<double>::max_digits10; + + // the actual conversion + std::ptrdiff_t len = (std::snprintf)( + numberbuffer.data(), numberbuffer.size(), "%.*g", d, number); + + // negative value indicates an error + if (len <= 0) + { + return; + } + + // check if buffer was large enough + if (numberbuffer.size() < static_cast<std::size_t>(len)) + { + return; + } + + const auto end = + std::remove(numberbuffer.begin(), numberbuffer.begin() + len, ','); + std::fill(end, numberbuffer.end(), '\0'); + + if ((end - numberbuffer.begin()) > len) + { + return; + } + len = (end - numberbuffer.begin()); + + out.append(numberbuffer.data(), static_cast<std::size_t>(len)); + + // determine if need to append ".0" + const bool valueIsIntLike = + std::none_of(numberbuffer.begin(), numberbuffer.begin() + len + 1, + [](char c) { return (c == '.' or c == 'e'); }); + + if (valueIsIntLike) + { + out += ".0"; + } +} + +inline void dumpfloat(std::string& out, double number) +{ + // NaN / inf + if (!std::isfinite(number)) + { + out += "null"; + return; + } + + // If float is an IEEE-754 single or double precision number, + // use the Grisu2 algorithm to produce short numbers which are + // guaranteed to round-trip, using strtof and strtod, resp. + // + // NB: The test below works if <long double> == <double>. + static constexpr bool isIeeeSingleOrDouble = + (std::numeric_limits<double>::is_iec559 and + std::numeric_limits<double>::digits == 24 and + std::numeric_limits<double>::max_exponent == 128) or + (std::numeric_limits<double>::is_iec559 and + std::numeric_limits<double>::digits == 53 and + std::numeric_limits<double>::max_exponent == 1024); + + dumpfloat(out, number, + std::integral_constant<bool, isIeeeSingleOrDouble>()); +} + +inline void dump(std::string& out, const nlohmann::json& val) +{ + switch (val.type()) + { + case nlohmann::json::value_t::object: + { + if (val.empty()) + { + out += "{}"; + return; + } + + out += "{"; + + out += "<div class=tab>"; + for (auto i = val.begin(); i != val.end();) + { + out += """; + dumpEscaped(out, i.key()); + out += "": "; + + bool inATag = false; + if (i.key() == "@odata.id" || i.key() == "@odata.context" || + i.key() == "Members@odata.nextLink" || i.key() == "Uri") + { + inATag = true; + out += "<a href=\""; + dumpEscaped(out, i.value()); + out += "\">"; + } + dump(out, i.value()); + if (inATag) + { + out += "</a>"; + } + i++; + if (i != val.end()) + { + out += ","; + } + out += "<br>"; + } + out += "</div>"; + out += '}'; + + return; + } + + case nlohmann::json::value_t::array: + { + if (val.empty()) + { + out += "[]"; + return; + } + + out += "["; + + out += "<div class=tab>"; + + // first n-1 elements + for (auto i = val.cbegin(); i != val.cend() - 1; ++i) + { + dump(out, *i); + out += ",<br>"; + } + + // last element + dump(out, val.back()); + + out += "</div>"; + out += ']'; + + return; + } + + case nlohmann::json::value_t::string: + { + out += '\"'; + const std::string* ptr = val.get_ptr<const std::string*>(); + dumpEscaped(out, *ptr); + out += '\"'; + return; + } + + case nlohmann::json::value_t::boolean: + { + if (*(val.get_ptr<const bool*>())) + { + out += "true"; + } + else + { + out += "false"; + } + return; + } + + case nlohmann::json::value_t::number_integer: + { + dumpInteger(out, *(val.get_ptr<const int64_t*>())); + return; + } + + case nlohmann::json::value_t::number_unsigned: + { + dumpInteger(out, *(val.get_ptr<const uint64_t*>())); + return; + } + + case nlohmann::json::value_t::number_float: + { + dumpfloat(out, *(val.get_ptr<const double*>())); + return; + } + + case nlohmann::json::value_t::discarded: + { + out += "<discarded>"; + return; + } + + case nlohmann::json::value_t::null: + { + out += "null"; + return; + } + } +} + +inline void dumpHtml(std::string& out, const nlohmann::json& json) +{ + out += "<html>\n" + "<head>\n" + "<title>Redfish API</title>\n" + "<link href=\"/redfish.css\" rel=\"stylesheet\">\n" + "</head>\n" + "<body>\n" + "<div class=\"container\">\n" + "<img src=\"/DMTF_Redfish_logo_2017.svg\" alt=\"redfish\" " + "height=\"406px\" " + "width=\"576px\">\n" + "<div class=\"content\">\n"; + dump(out, json); + out += "</div>\n" + "</div>\n" + "</body>\n" + "</html>\n"; +} + +} // namespace json_html_util diff --git a/static/redfish.css b/static/redfish.css new file mode 100644 index 0000000000..4e851ebedd --- /dev/null +++ b/static/redfish.css @@ -0,0 +1,19 @@ +img { + max-width: 30em; + margin: 0 auto; +} + +.container { + width: 30em; + top: 50%; + left: 50%; + margin: 0 auto; +} + +.content { + font-family: monospace; +} + +.tab { + margin-left: 2em; +} diff --git a/static/styles/json.css b/static/styles/json.css new file mode 100644 index 0000000000..59ebb287e2 --- /dev/null +++ b/static/styles/json.css @@ -0,0 +1,10 @@ +.content { + max-width: 500px; + margin: auto; + font-family: monospace; +} + +.header { + max-width: 576px; + margin: 0 auto; +} |