/proc/self/cwd/source/common/grpc/common.cc
Line | Count | Source (jump to first uncovered line) |
1 | | #include "source/common/grpc/common.h" |
2 | | |
3 | | #include <atomic> |
4 | | #include <cstdint> |
5 | | #include <cstring> |
6 | | #include <string> |
7 | | |
8 | | #include "source/common/buffer/buffer_impl.h" |
9 | | #include "source/common/buffer/zero_copy_input_stream_impl.h" |
10 | | #include "source/common/common/assert.h" |
11 | | #include "source/common/common/base64.h" |
12 | | #include "source/common/common/empty_string.h" |
13 | | #include "source/common/common/enum_to_int.h" |
14 | | #include "source/common/common/fmt.h" |
15 | | #include "source/common/common/macros.h" |
16 | | #include "source/common/common/safe_memcpy.h" |
17 | | #include "source/common/common/utility.h" |
18 | | #include "source/common/grpc/codec.h" |
19 | | #include "source/common/http/header_utility.h" |
20 | | #include "source/common/http/headers.h" |
21 | | #include "source/common/http/message_impl.h" |
22 | | #include "source/common/http/utility.h" |
23 | | #include "source/common/protobuf/protobuf.h" |
24 | | |
25 | | #include "absl/container/fixed_array.h" |
26 | | #include "absl/strings/match.h" |
27 | | |
28 | | namespace Envoy { |
29 | | namespace Grpc { |
30 | | |
31 | 156k | bool Common::hasGrpcContentType(const Http::RequestOrResponseHeaderMap& headers) { |
32 | 156k | const absl::string_view content_type = headers.getContentTypeValue(); |
33 | | // Content type is gRPC if it is exactly "application/grpc" or starts with |
34 | | // "application/grpc+". Specifically, something like application/grpc-web is not gRPC. |
35 | 156k | return absl::StartsWith(content_type, Http::Headers::get().ContentTypeValues.Grpc) && |
36 | 156k | (content_type.size() == Http::Headers::get().ContentTypeValues.Grpc.size() || |
37 | 5.75k | content_type[Http::Headers::get().ContentTypeValues.Grpc.size()] == '+'); |
38 | 156k | } |
39 | | |
40 | 20 | bool Common::hasConnectProtocolVersionHeader(const Http::RequestOrResponseHeaderMap& headers) { |
41 | 20 | return !headers.get(Http::CustomHeaders::get().ConnectProtocolVersion).empty(); |
42 | 20 | } |
43 | | |
44 | 40 | bool Common::hasConnectStreamingContentType(const Http::RequestOrResponseHeaderMap& headers) { |
45 | | // Consider the request a connect request if the content type starts with "application/connect+". |
46 | 40 | static constexpr absl::string_view connect_prefix{"application/connect+"}; |
47 | 40 | const absl::string_view content_type = headers.getContentTypeValue(); |
48 | 40 | return absl::StartsWith(content_type, connect_prefix); |
49 | 40 | } |
50 | | |
51 | 0 | bool Common::hasProtobufContentType(const Http::RequestOrResponseHeaderMap& headers) { |
52 | 0 | return headers.getContentTypeValue() == Http::Headers::get().ContentTypeValues.Protobuf; |
53 | 0 | } |
54 | | |
55 | 267k | bool Common::isGrpcRequestHeaders(const Http::RequestHeaderMap& headers) { |
56 | 267k | if (!headers.Path()) { |
57 | 113k | return false; |
58 | 113k | } |
59 | 154k | return hasGrpcContentType(headers); |
60 | 267k | } |
61 | | |
62 | 20 | bool Common::isConnectRequestHeaders(const Http::RequestHeaderMap& headers) { |
63 | 20 | if (!headers.Path()) { |
64 | 0 | return false; |
65 | 0 | } |
66 | 20 | return hasConnectProtocolVersionHeader(headers); |
67 | 20 | } |
68 | | |
69 | 20 | bool Common::isConnectStreamingRequestHeaders(const Http::RequestHeaderMap& headers) { |
70 | 20 | if (!headers.Path()) { |
71 | 0 | return false; |
72 | 0 | } |
73 | 20 | return hasConnectStreamingContentType(headers); |
74 | 20 | } |
75 | | |
76 | 0 | bool Common::isProtobufRequestHeaders(const Http::RequestHeaderMap& headers) { |
77 | 0 | if (!headers.Path()) { |
78 | 0 | return false; |
79 | 0 | } |
80 | 0 | return hasProtobufContentType(headers); |
81 | 0 | } |
82 | | |
83 | 2.74k | bool Common::isGrpcResponseHeaders(const Http::ResponseHeaderMap& headers, bool end_stream) { |
84 | 2.74k | if (end_stream) { |
85 | | // Trailers-only response, only grpc-status is required. |
86 | 457 | return headers.GrpcStatus() != nullptr; |
87 | 457 | } |
88 | 2.28k | if (Http::Utility::getResponseStatus(headers) != enumToInt(Http::Code::OK)) { |
89 | 0 | return false; |
90 | 0 | } |
91 | 2.28k | return hasGrpcContentType(headers); |
92 | 2.28k | } |
93 | | |
94 | 20 | bool Common::isConnectStreamingResponseHeaders(const Http::ResponseHeaderMap& headers) { |
95 | 20 | if (Http::Utility::getResponseStatus(headers) != enumToInt(Http::Code::OK)) { |
96 | 0 | return false; |
97 | 0 | } |
98 | 20 | return hasConnectStreamingContentType(headers); |
99 | 20 | } |
100 | | |
101 | | absl::optional<Status::GrpcStatus> |
102 | 10.3k | Common::getGrpcStatus(const Http::ResponseHeaderOrTrailerMap& trailers, bool allow_user_defined) { |
103 | 10.3k | const absl::string_view grpc_status_header = trailers.getGrpcStatusValue(); |
104 | 10.3k | uint64_t grpc_status_code; |
105 | | |
106 | 10.3k | if (grpc_status_header.empty()) { |
107 | 8.85k | return absl::nullopt; |
108 | 8.85k | } |
109 | 1.46k | if (!absl::SimpleAtoi(grpc_status_header, &grpc_status_code) || |
110 | 1.46k | (grpc_status_code > Status::WellKnownGrpcStatus::MaximumKnown && !allow_user_defined)) { |
111 | 694 | return {Status::WellKnownGrpcStatus::InvalidCode}; |
112 | 694 | } |
113 | 769 | return {static_cast<Status::GrpcStatus>(grpc_status_code)}; |
114 | 1.46k | } |
115 | | |
116 | | absl::optional<Status::GrpcStatus> Common::getGrpcStatus(const Http::ResponseTrailerMap& trailers, |
117 | | const Http::ResponseHeaderMap& headers, |
118 | | const StreamInfo::StreamInfo& info, |
119 | 3.74k | bool allow_user_defined) { |
120 | | // The gRPC specification does not guarantee a gRPC status code will be returned from a gRPC |
121 | | // request. When it is returned, it will be in the response trailers. With that said, Envoy will |
122 | | // treat a trailers-only response as a headers-only response, so we have to check the following |
123 | | // in order: |
124 | | // 1. trailers gRPC status, if it exists. |
125 | | // 2. headers gRPC status, if it exists. |
126 | | // 3. Inferred from info HTTP status, if it exists. |
127 | 3.74k | absl::optional<Grpc::Status::GrpcStatus> optional_status; |
128 | 3.74k | optional_status = Grpc::Common::getGrpcStatus(trailers, allow_user_defined); |
129 | 3.74k | if (optional_status.has_value()) { |
130 | 387 | return optional_status; |
131 | 387 | } |
132 | 3.36k | optional_status = Grpc::Common::getGrpcStatus(headers, allow_user_defined); |
133 | 3.36k | if (optional_status.has_value()) { |
134 | 226 | return optional_status; |
135 | 226 | } |
136 | 3.13k | return info.responseCode() ? absl::optional<Grpc::Status::GrpcStatus>( |
137 | 2.04k | Grpc::Utility::httpToGrpcStatus(info.responseCode().value())) |
138 | 3.13k | : absl::nullopt; |
139 | 3.36k | } |
140 | | |
141 | 649 | std::string Common::getGrpcMessage(const Http::ResponseHeaderOrTrailerMap& trailers) { |
142 | 649 | const auto entry = trailers.GrpcMessage(); |
143 | 649 | return entry ? std::string(entry->value().getStringView()) : EMPTY_STRING; |
144 | 649 | } |
145 | | |
146 | | absl::optional<google::rpc::Status> |
147 | 0 | Common::getGrpcStatusDetailsBin(const Http::HeaderMap& trailers) { |
148 | 0 | const auto details_header = trailers.get(Http::Headers::get().GrpcStatusDetailsBin); |
149 | 0 | if (details_header.empty()) { |
150 | 0 | return absl::nullopt; |
151 | 0 | } |
152 | | |
153 | | // Some implementations use non-padded base64 encoding for grpc-status-details-bin. |
154 | | // This is effectively a trusted header so using the first value is fine. |
155 | 0 | auto decoded_value = Base64::decodeWithoutPadding(details_header[0]->value().getStringView()); |
156 | 0 | if (decoded_value.empty()) { |
157 | 0 | return absl::nullopt; |
158 | 0 | } |
159 | | |
160 | 0 | google::rpc::Status status; |
161 | 0 | if (!status.ParseFromString(decoded_value)) { |
162 | 0 | return absl::nullopt; |
163 | 0 | } |
164 | | |
165 | 0 | return {std::move(status)}; |
166 | 0 | } |
167 | | |
168 | 19.2k | Buffer::InstancePtr Common::serializeToGrpcFrame(const Protobuf::Message& message) { |
169 | | // http://www.grpc.io/docs/guides/wire.html |
170 | | // Reserve enough space for the entire message and the 5 byte header. |
171 | | // NB: we do not use prependGrpcFrameHeader because that would add another BufferFragment and this |
172 | | // (using a single BufferFragment) is more efficient. |
173 | 19.2k | Buffer::InstancePtr body(new Buffer::OwnedImpl()); |
174 | 19.2k | const uint32_t size = message.ByteSize(); |
175 | 19.2k | const uint32_t alloc_size = size + 5; |
176 | 19.2k | auto reservation = body->reserveSingleSlice(alloc_size); |
177 | 19.2k | ASSERT(reservation.slice().len_ >= alloc_size); |
178 | 19.2k | uint8_t* current = reinterpret_cast<uint8_t*>(reservation.slice().mem_); |
179 | 19.2k | *current++ = 0; // flags |
180 | 19.2k | const uint32_t nsize = htonl(size); |
181 | 19.2k | safeMemcpyUnsafeDst(current, &nsize); |
182 | 19.2k | current += sizeof(uint32_t); |
183 | 19.2k | Protobuf::io::ArrayOutputStream stream(current, size, -1); |
184 | 19.2k | Protobuf::io::CodedOutputStream codec_stream(&stream); |
185 | 19.2k | message.SerializeWithCachedSizes(&codec_stream); |
186 | 19.2k | reservation.commit(alloc_size); |
187 | 19.2k | return body; |
188 | 19.2k | } |
189 | | |
190 | 7.98k | Buffer::InstancePtr Common::serializeMessage(const Protobuf::Message& message) { |
191 | 7.98k | auto body = std::make_unique<Buffer::OwnedImpl>(); |
192 | 7.98k | const uint32_t size = message.ByteSize(); |
193 | 7.98k | auto reservation = body->reserveSingleSlice(size); |
194 | 7.98k | ASSERT(reservation.slice().len_ >= size); |
195 | 7.98k | uint8_t* current = reinterpret_cast<uint8_t*>(reservation.slice().mem_); |
196 | 7.98k | Protobuf::io::ArrayOutputStream stream(current, size, -1); |
197 | 7.98k | Protobuf::io::CodedOutputStream codec_stream(&stream); |
198 | 7.98k | message.SerializeWithCachedSizes(&codec_stream); |
199 | 7.98k | reservation.commit(size); |
200 | 7.98k | return body; |
201 | 7.98k | } |
202 | | |
203 | | absl::optional<std::chrono::milliseconds> |
204 | 190k | Common::getGrpcTimeout(const Http::RequestHeaderMap& request_headers) { |
205 | 190k | const Http::HeaderEntry* header_grpc_timeout_entry = request_headers.GrpcTimeout(); |
206 | 190k | std::chrono::milliseconds timeout; |
207 | 190k | if (header_grpc_timeout_entry) { |
208 | 90 | int64_t grpc_timeout; |
209 | 90 | absl::string_view timeout_entry = header_grpc_timeout_entry->value().getStringView(); |
210 | 90 | if (timeout_entry.empty()) { |
211 | | // Must be of the form TimeoutValue TimeoutUnit. See |
212 | | // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests. |
213 | 21 | return absl::nullopt; |
214 | 21 | } |
215 | | // TimeoutValue must be a positive integer of at most 8 digits. |
216 | 69 | if (absl::SimpleAtoi(timeout_entry.substr(0, timeout_entry.size() - 1), &grpc_timeout) && |
217 | 69 | grpc_timeout >= 0 && static_cast<uint64_t>(grpc_timeout) <= MAX_GRPC_TIMEOUT_VALUE) { |
218 | 64 | const char unit = timeout_entry[timeout_entry.size() - 1]; |
219 | 64 | switch (unit) { |
220 | 0 | case 'H': |
221 | 0 | return std::chrono::hours(grpc_timeout); |
222 | 0 | case 'M': |
223 | 0 | return std::chrono::minutes(grpc_timeout); |
224 | 0 | case 'S': |
225 | 0 | return std::chrono::seconds(grpc_timeout); |
226 | 24 | case 'm': |
227 | 24 | return std::chrono::milliseconds(grpc_timeout); |
228 | 0 | break; |
229 | 34 | case 'u': |
230 | 34 | timeout = std::chrono::duration_cast<std::chrono::milliseconds>( |
231 | 34 | std::chrono::microseconds(grpc_timeout)); |
232 | 34 | if (timeout < std::chrono::microseconds(grpc_timeout)) { |
233 | 18 | timeout++; |
234 | 18 | } |
235 | 34 | return timeout; |
236 | 6 | case 'n': |
237 | 6 | timeout = std::chrono::duration_cast<std::chrono::milliseconds>( |
238 | 6 | std::chrono::nanoseconds(grpc_timeout)); |
239 | 6 | if (timeout < std::chrono::nanoseconds(grpc_timeout)) { |
240 | 6 | timeout++; |
241 | 6 | } |
242 | 6 | return timeout; |
243 | 64 | } |
244 | 64 | } |
245 | 69 | } |
246 | 190k | return absl::nullopt; |
247 | 190k | } |
248 | | |
249 | | void Common::toGrpcTimeout(const std::chrono::milliseconds& timeout, |
250 | 18.6k | Http::RequestHeaderMap& headers) { |
251 | 18.6k | uint64_t time = timeout.count(); |
252 | 18.6k | static const char units[] = "mSMH"; |
253 | 18.6k | const char* unit = units; // start with milliseconds |
254 | 18.6k | if (time > MAX_GRPC_TIMEOUT_VALUE) { |
255 | 469 | time /= 1000; // Convert from milliseconds to seconds |
256 | 469 | unit++; |
257 | 469 | } |
258 | 19.2k | while (time > MAX_GRPC_TIMEOUT_VALUE) { |
259 | 605 | if (*unit == 'H') { |
260 | 0 | time = MAX_GRPC_TIMEOUT_VALUE; // No bigger unit available, clip to max 8 digit hours. |
261 | 605 | } else { |
262 | 605 | time /= 60; // Convert from seconds to minutes to hours |
263 | 605 | unit++; |
264 | 605 | } |
265 | 605 | } |
266 | 18.6k | headers.setGrpcTimeout(absl::StrCat(time, absl::string_view(unit, 1))); |
267 | 18.6k | } |
268 | | |
269 | | Http::RequestMessagePtr |
270 | | Common::prepareHeaders(const std::string& host_name, const std::string& service_full_name, |
271 | | const std::string& method_name, |
272 | 19.4k | const absl::optional<std::chrono::milliseconds>& timeout) { |
273 | 19.4k | Http::RequestMessagePtr message(new Http::RequestMessageImpl()); |
274 | 19.4k | message->headers().setReferenceMethod(Http::Headers::get().MethodValues.Post); |
275 | 19.4k | message->headers().setPath(absl::StrCat("/", service_full_name, "/", method_name)); |
276 | 19.4k | message->headers().setHost(host_name); |
277 | | // According to https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md TE should appear |
278 | | // before Timeout and ContentType. |
279 | 19.4k | message->headers().setReferenceTE(Http::Headers::get().TEValues.Trailers); |
280 | 19.4k | if (timeout) { |
281 | 0 | toGrpcTimeout(timeout.value(), message->headers()); |
282 | 0 | } |
283 | 19.4k | message->headers().setReferenceContentType(Http::Headers::get().ContentTypeValues.Grpc); |
284 | | |
285 | 19.4k | return message; |
286 | 19.4k | } |
287 | | |
288 | 8.29k | const std::string& Common::typeUrlPrefix() { |
289 | 8.29k | CONSTRUCT_ON_FIRST_USE(std::string, "type.googleapis.com"); |
290 | 8.29k | } |
291 | | |
292 | 8.22k | std::string Common::typeUrl(const std::string& qualified_name) { |
293 | 8.22k | return typeUrlPrefix() + "/" + qualified_name; |
294 | 8.22k | } |
295 | | |
296 | 1.75k | void Common::prependGrpcFrameHeader(Buffer::Instance& buffer) { |
297 | 1.75k | std::array<char, 5> header; |
298 | 1.75k | header[0] = GRPC_FH_DEFAULT; // flags |
299 | 1.75k | const uint32_t nsize = htonl(buffer.length()); |
300 | 1.75k | safeMemcpyUnsafeDst(&header[1], &nsize); |
301 | 1.75k | buffer.prepend(absl::string_view(&header[0], 5)); |
302 | 1.75k | } |
303 | | |
304 | 0 | bool Common::parseBufferInstance(Buffer::InstancePtr&& buffer, Protobuf::Message& proto) { |
305 | 0 | Buffer::ZeroCopyInputStreamImpl stream(std::move(buffer)); |
306 | 0 | return proto.ParseFromZeroCopyStream(&stream); |
307 | 0 | } |
308 | | |
309 | | absl::optional<Common::RequestNames> |
310 | 6 | Common::resolveServiceAndMethod(const Http::HeaderEntry* path) { |
311 | 6 | absl::optional<RequestNames> request_names; |
312 | 6 | if (path == nullptr) { |
313 | 0 | return request_names; |
314 | 0 | } |
315 | 6 | absl::string_view str = path->value().getStringView(); |
316 | 6 | str = str.substr(0, str.find('?')); |
317 | 6 | const auto parts = StringUtil::splitToken(str, "/"); |
318 | 6 | if (parts.size() != 2) { |
319 | 5 | return request_names; |
320 | 5 | } |
321 | 1 | request_names = RequestNames{parts[0], parts[1]}; |
322 | 1 | return request_names; |
323 | 6 | } |
324 | | |
325 | | } // namespace Grpc |
326 | | } // namespace Envoy |