1 /*
2 * Copyright 2021 Google LLC
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 #include "fcp/client/http/http_client_util.h"
17
18 #include <algorithm>
19 #include <functional>
20 #include <optional>
21 #include <string>
22 #include <vector>
23
24 #include "absl/status/status.h"
25 #include "absl/status/statusor.h"
26 #include "absl/strings/ascii.h"
27 #include "absl/strings/str_cat.h"
28 #include "absl/strings/str_format.h"
29 #include "absl/strings/string_view.h"
30 #include "absl/strings/strip.h"
31 #include "absl/strings/substitute.h"
32 #include "fcp/base/monitoring.h"
33 #include "fcp/client/http/http_client.h"
34 #include "fcp/protos/federatedcompute/common.pb.h"
35 // #include "google/rpc/status.pb.h"
36
37 namespace fcp {
38 namespace client {
39 namespace http {
40
41 namespace {
42
43 using ::google::internal::federatedcompute::v1::Status;
44
ConvertHttpCodeToStatusCode(int code)45 absl::StatusCode ConvertHttpCodeToStatusCode(int code) {
46 switch (code) {
47 case kHttpBadRequest:
48 return absl::StatusCode::kInvalidArgument;
49 case kHttpForbidden:
50 return absl::StatusCode::kPermissionDenied;
51 case kHttpNotFound:
52 return absl::StatusCode::kNotFound;
53 case kHttpConflict:
54 return absl::StatusCode::kAborted;
55 case kHttpTooManyRequests:
56 return absl::StatusCode::kResourceExhausted;
57 case kHttpClientClosedRequest:
58 return absl::StatusCode::kCancelled;
59 case kHttpGatewayTimeout:
60 return absl::StatusCode::kDeadlineExceeded;
61 case kHttpNotImplemented:
62 return absl::StatusCode::kUnimplemented;
63 case kHttpServiceUnavailable:
64 return absl::StatusCode::kUnavailable;
65 case kHttpUnauthorized:
66 return absl::StatusCode::kUnauthenticated;
67 default: {
68 // Importantly this range ensures that we treat not only "200 OK" as OK,
69 // but also other codes such as "201 Created" etc.
70 if (code >= 200 && code < 300) {
71 return absl::StatusCode::kOk;
72 }
73 if (code >= 400 && code < 500) {
74 return absl::StatusCode::kFailedPrecondition;
75 }
76 if (code >= 500 && code < 600) {
77 return absl::StatusCode::kInternal;
78 }
79 return absl::StatusCode::kUnknown;
80 }
81 }
82 }
83
84 // Converts a `Status` error code into an `absl::StatusCode`
85 // (there is a 1:1 mapping).
ConvertRpcCodeToStatusCode(int code)86 absl::StatusCode ConvertRpcCodeToStatusCode(int code) {
87 switch (code) {
88 case static_cast<int>(absl::StatusCode::kOk):
89 return absl::StatusCode::kOk;
90 case static_cast<int>(absl::StatusCode::kCancelled):
91 return absl::StatusCode::kCancelled;
92 case static_cast<int>(absl::StatusCode::kUnknown):
93 return absl::StatusCode::kUnknown;
94 case static_cast<int>(absl::StatusCode::kInvalidArgument):
95 return absl::StatusCode::kInvalidArgument;
96 case static_cast<int>(absl::StatusCode::kDeadlineExceeded):
97 return absl::StatusCode::kDeadlineExceeded;
98 case static_cast<int>(absl::StatusCode::kNotFound):
99 return absl::StatusCode::kNotFound;
100 case static_cast<int>(absl::StatusCode::kAlreadyExists):
101 return absl::StatusCode::kAlreadyExists;
102 case static_cast<int>(absl::StatusCode::kPermissionDenied):
103 return absl::StatusCode::kPermissionDenied;
104 case static_cast<int>(absl::StatusCode::kResourceExhausted):
105 return absl::StatusCode::kResourceExhausted;
106 case static_cast<int>(absl::StatusCode::kFailedPrecondition):
107 return absl::StatusCode::kFailedPrecondition;
108 case static_cast<int>(absl::StatusCode::kAborted):
109 return absl::StatusCode::kAborted;
110 case static_cast<int>(absl::StatusCode::kOutOfRange):
111 return absl::StatusCode::kOutOfRange;
112 case static_cast<int>(absl::StatusCode::kUnimplemented):
113 return absl::StatusCode::kUnimplemented;
114 case static_cast<int>(absl::StatusCode::kInternal):
115 return absl::StatusCode::kInternal;
116 case static_cast<int>(absl::StatusCode::kUnavailable):
117 return absl::StatusCode::kUnavailable;
118 case static_cast<int>(absl::StatusCode::kDataLoss):
119 return absl::StatusCode::kDataLoss;
120 case static_cast<int>(absl::StatusCode::kUnauthenticated):
121 return absl::StatusCode::kUnauthenticated;
122 default:
123 // This should never be reached, since there should be a 1:1 mapping
124 // between Absl and Google RPC status codes.
125 return absl::StatusCode::kUnknown;
126 }
127 }
128
PercentEncode(absl::string_view input,std::function<bool (char c)> unencoded_chars)129 absl::StatusOr<std::string> PercentEncode(
130 absl::string_view input, std::function<bool(char c)> unencoded_chars) {
131 std::string result;
132 for (unsigned char c : input) {
133 // We limit URIs only to ASCII characters.
134 if (!absl::ascii_isascii(c)) {
135 return absl::InvalidArgumentError(absl::StrCat(
136 "Encountered unsupported char during URI encoding: ", c));
137 }
138 // The following characters are *not* percent-encoded.
139 if (unencoded_chars(c)) {
140 result.push_back(c);
141 continue;
142 }
143 // Any other character is percent-encoded.
144 result.append(absl::StrFormat("%%%X", c));
145 }
146 return result;
147 }
148
149 } // namespace
150
ConvertHttpCodeToStatus(int code)151 absl::Status ConvertHttpCodeToStatus(int code) {
152 absl::StatusCode status_code = ConvertHttpCodeToStatusCode(code);
153 if (status_code == absl::StatusCode::kOk) {
154 return absl::OkStatus();
155 }
156 std::string error_message =
157 absl::StrCat("Request returned non-OK response (code: ", code, ")");
158 return absl::Status(status_code, error_message);
159 }
160
ConvertRpcStatusToAbslStatus(Status rpc_status)161 absl::Status ConvertRpcStatusToAbslStatus(Status rpc_status) {
162 return absl::Status(ConvertRpcCodeToStatusCode(rpc_status.code()),
163 rpc_status.message());
164 }
165
ConvertAbslStatusToRpcStatus(absl::Status status)166 Status ConvertAbslStatusToRpcStatus(absl::Status status) {
167 Status rpc_status;
168 rpc_status.set_code(static_cast<int>(status.code()));
169 rpc_status.set_message(std::string(status.message()));
170 return rpc_status;
171 }
172
ConvertMethodToString(HttpRequest::Method method)173 std::string ConvertMethodToString(HttpRequest::Method method) {
174 switch (method) {
175 case HttpRequest::Method::kGet:
176 return "GET";
177 case HttpRequest::Method::kHead:
178 return "HEAD";
179 case HttpRequest::Method::kDelete:
180 return "DELETE";
181 case HttpRequest::Method::kPatch:
182 return "PATCH";
183 case HttpRequest::Method::kPost:
184 return "POST";
185 case HttpRequest::Method::kPut:
186 return "PUT";
187 }
188 }
189
FindHeader(const HeaderList & headers,absl::string_view needle)190 std::optional<std::string> FindHeader(const HeaderList& headers,
191 absl::string_view needle) {
192 // Normalize the needle (since header names are case insensitive, as per RFC
193 // 2616 section 4.2).
194 const std::string normalized_needle = absl::AsciiStrToLower(needle);
195 const auto& header_entry = std::find_if(
196 headers.begin(), headers.end(), [&normalized_needle](const Header& x) {
197 // AsciiStrToLower safely handles non-ASCII data, and comparing
198 // non-ASCII data w/ our needle is safe as well.
199 return absl::AsciiStrToLower(std::get<0>(x)) == normalized_needle;
200 });
201
202 if (header_entry == headers.end()) {
203 return std::nullopt;
204 }
205 return std::get<1>(*header_entry);
206 }
207
JoinBaseUriWithSuffix(absl::string_view base_uri,absl::string_view uri_suffix)208 absl::StatusOr<std::string> JoinBaseUriWithSuffix(
209 absl::string_view base_uri, absl::string_view uri_suffix) {
210 if (!uri_suffix.empty() && uri_suffix[0] != '/') {
211 return absl::InvalidArgumentError(
212 "uri_suffix be empty or must have a leading '/'");
213 }
214 // Construct the full URI by joining the base URI we should use with the given
215 // suffix, ensuring that there's always a single '/' in between the two parts.
216 return absl::StrCat(absl::StripSuffix(base_uri, "/"), "/",
217 absl::StripPrefix(uri_suffix, "/"));
218 }
219
EncodeUriSinglePathSegment(absl::string_view input)220 absl::StatusOr<std::string> EncodeUriSinglePathSegment(
221 absl::string_view input) {
222 return PercentEncode(input, [](char c) {
223 return absl::ascii_isalnum(c) || c == '-' || c == '_' || c == '.' ||
224 c == '~';
225 });
226 }
227
EncodeUriMultiplePathSegments(absl::string_view input)228 absl::StatusOr<std::string> EncodeUriMultiplePathSegments(
229 absl::string_view input) {
230 return PercentEncode(input, [](char c) {
231 return absl::ascii_isalnum(c) || c == '-' || c == '_' || c == '.' ||
232 c == '~' || c == '/';
233 });
234 }
235
CreateByteStreamUploadUriSuffix(absl::string_view resource_name)236 absl::StatusOr<std::string> CreateByteStreamUploadUriSuffix(
237 absl::string_view resource_name) {
238 constexpr absl::string_view pattern = "/upload/v1/media/$0";
239 FCP_ASSIGN_OR_RETURN(std::string encoded_resource_name,
240 EncodeUriMultiplePathSegments(resource_name));
241 // Construct the URI suffix.
242 return absl::Substitute(pattern, encoded_resource_name);
243 }
244 } // namespace http
245 } // namespace client
246 } // namespace fcp
247