1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 #pragma once
4
5 #include "boost_formatters.hpp"
6 #include "http_body.hpp"
7 #include "http_response.hpp"
8 #include "http_utility.hpp"
9 #include "json_html_serializer.hpp"
10 #include "logging.hpp"
11 #include "security_headers.hpp"
12
13 #include <boost/beast/http/field.hpp>
14 #include <nlohmann/json.hpp>
15
16 #include <array>
17 #include <string>
18 #include <string_view>
19 #include <utility>
20
21 namespace crow
22 {
23
attemptZstdCompression(Response & res)24 inline bool attemptZstdCompression(Response& res)
25 {
26 using bmcweb::CompressionType;
27 using enum bmcweb::CompressionType;
28 using http_helpers::Encoding;
29 using enum http_helpers::Encoding;
30
31 std::string& strBody = res.response.body().str();
32 if (strBody.empty())
33 {
34 // No need to compress an empty body
35 return true;
36 }
37 bmcweb::ZstdCompressor zstdCompressor;
38 if (!zstdCompressor.init(strBody.size()))
39 {
40 BMCWEB_LOG_ERROR("Failed to initialize Zstd Compressor");
41 return false;
42 }
43
44 const uint8_t* dataIn = std::bit_cast<const uint8_t*>(strBody.data());
45 std::span<const uint8_t> spanIn(dataIn, strBody.size());
46 bool more = false;
47 std::optional<std::span<const uint8_t>> compressed =
48 zstdCompressor.compress(spanIn, more);
49 if (!compressed)
50 {
51 BMCWEB_LOG_ERROR("Failed to compress content with zstd.");
52 return false;
53 }
54 const char* dataOut = std::bit_cast<const char*>(compressed->data());
55 strBody = std::string(dataOut, compressed->size());
56
57 res.addHeader(boost::beast::http::field::content_encoding, "zstd");
58 res.response.body().clientCompressionType = Zstd;
59 res.response.body().compressionType = Zstd;
60 return true;
61 }
62
handleEncoding(std::string_view acceptEncoding,Response & res)63 inline void handleEncoding(std::string_view acceptEncoding, Response& res)
64 {
65 using bmcweb::CompressionType;
66 using enum bmcweb::CompressionType;
67 using http_helpers::Encoding;
68 using enum http_helpers::Encoding;
69 // If the payload is currently compressed, see if we can avoid
70 // decompressing it by sending it to the client directly
71 switch (res.response.body().compressionType)
72 {
73 case Zstd:
74 {
75 std::array<Encoding, 1> allowedEnc{ZSTD};
76 Encoding encoding =
77 http_helpers::getPreferredEncoding(acceptEncoding, allowedEnc);
78
79 if (encoding == ZSTD)
80 {
81 // If the client supports returning zstd directly, allow that.
82 BMCWEB_LOG_DEBUG(
83 "Content is already ztd compressed. Setting client compression type to Zstd");
84 res.response.body().clientCompressionType = Zstd;
85 }
86 }
87 break;
88 case Gzip:
89 {
90 std::array<Encoding, 1> allowedEnc{GZIP};
91 Encoding encoding =
92 http_helpers::getPreferredEncoding(acceptEncoding, allowedEnc);
93 if (encoding != GZIP)
94 {
95 BMCWEB_LOG_WARNING(
96 "Unimplemented: Returning gzip payload to client that did not explicitly allow it.");
97 }
98 }
99 break;
100 case Raw:
101 {
102 BMCWEB_LOG_DEBUG(
103 "Content is raw bytes. Checking if it can be compressed.");
104
105 std::array<Encoding, 1> allowedEnc{ZSTD};
106 Encoding encoding =
107 http_helpers::getPreferredEncoding(acceptEncoding, allowedEnc);
108 if (encoding == ZSTD)
109 {
110 BMCWEB_LOG_ERROR("Content can be compressed with zstd.");
111 if (!attemptZstdCompression(res))
112 {
113 BMCWEB_LOG_ERROR(
114 "Failed to compress content with zstd. Continuing.");
115 }
116 }
117 }
118 break;
119 default:
120 break;
121 }
122 }
123
completeResponseFields(std::string_view accepts,std::string_view acceptEncoding,Response & res)124 inline void completeResponseFields(
125 std::string_view accepts, std::string_view acceptEncoding, Response& res)
126 {
127 BMCWEB_LOG_INFO("Response: {}", res.resultInt());
128 addSecurityHeaders(res);
129
130 res.setResponseEtagAndHandleNotModified();
131 if (res.jsonValue.is_structured())
132 {
133 using http_helpers::ContentType;
134 std::array<ContentType, 3> allowed{ContentType::CBOR, ContentType::JSON,
135 ContentType::HTML};
136 ContentType preferred = getPreferredContentType(accepts, allowed);
137
138 if (preferred == ContentType::HTML)
139 {
140 json_html_util::prettyPrintJson(res);
141 }
142 else if (preferred == ContentType::CBOR)
143 {
144 res.addHeader(boost::beast::http::field::content_type,
145 "application/cbor");
146 std::string cbor;
147 nlohmann::json::to_cbor(res.jsonValue, cbor);
148 res.write(std::move(cbor));
149 }
150 else
151 {
152 // Technically preferred could also be NoMatch here, but we'd
153 // like to default to something rather than return 400 for
154 // backward compatibility.
155 res.addHeader(boost::beast::http::field::content_type,
156 "application/json");
157 res.write(res.jsonValue.dump(
158 2, ' ', true, nlohmann::json::error_handler_t::replace));
159 }
160 }
161
162 handleEncoding(acceptEncoding, res);
163 }
164 } // namespace crow
165