-
Notifications
You must be signed in to change notification settings - Fork 81
/
Copy pathclient_utils.cpp
341 lines (302 loc) · 16.9 KB
/
client_utils.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
#include "nmos/client_utils.h"
// cf. preprocessor conditions in nmos::details::make_client_ssl_context_callback and nmos::details::make_client_nativehandle_options
#if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO)
#if defined(__linux__)
#include <boost/asio/ip/tcp.hpp>
#endif
#include "boost/asio/ssl/set_cipher_list.hpp"
#include "cpprest/host_utils.h"
#endif
#include "cpprest/basic_utils.h"
#include "cpprest/details/system_error.h"
#include "cpprest/http_utils.h"
#include "cpprest/response_type.h"
#include "cpprest/ws_client.h"
#include "nmos/certificate_settings.h"
#include "nmos/json_fields.h"
#include "nmos/slog.h"
#include "nmos/ssl_context_options.h"
// Utility types, constants and functions for implementing NMOS REST API clients
namespace nmos
{
web::uri proxy_uri(const nmos::settings& settings)
{
const auto& proxy_address = nmos::experimental::fields::proxy_address(settings);
if (proxy_address.empty()) return{};
return web::uri_builder()
.set_scheme(U("http"))
.set_host(proxy_address)
.set_port(nmos::experimental::fields::proxy_port(settings))
.to_uri();
}
namespace details
{
// cf. preprocessor conditions in nmos::make_http_client_config and nmos::make_websocket_client_config
#if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO)
template <typename ExceptionType>
inline std::function<void(boost::asio::ssl::context&)> make_client_ssl_context_callback(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate)
{
if (!load_ca_certificates)
{
load_ca_certificates = make_load_ca_certificates_handler(settings, gate);
}
return [load_ca_certificates](boost::asio::ssl::context& ctx)
{
try
{
ctx.set_options(nmos::details::ssl_context_options);
const auto cacerts = utility::us2s(load_ca_certificates());
ctx.add_certificate_authority(boost::asio::buffer(cacerts.data(), cacerts.size()));
set_cipher_list(ctx, nmos::details::ssl_cipher_list);
}
catch (const boost::system::system_error& e)
{
throw web::details::from_boost_system_system_error<ExceptionType>(e);
}
};
}
#endif
#if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO)
// bind socket to a specific network interface
// for now, only supporting Linux because SO_BINDTODEVICE is not defined on Windows and Mac
inline void bind_to_device(const utility::string_t& interface_name, bool secure, void* native_handle)
{
#if defined(__linux__)
int socket_fd;
// hmm, frustrating that native_handle type has been erased so we need secure flag
if (secure)
{
auto socket = (boost::asio::ssl::stream<boost::asio::ip::tcp::socket&>*)native_handle;
if (!socket->lowest_layer().is_open())
{
// for now, limited to IPv4
socket->lowest_layer().open(boost::asio::ip::tcp::v4());
}
socket_fd = socket->lowest_layer().native_handle();
}
else
{
auto socket = (boost::asio::ip::tcp::socket*)native_handle;
if (!socket->is_open())
{
// for now, limited to IPv4
socket->open(boost::asio::ip::tcp::v4());
}
socket_fd = socket->lowest_layer().native_handle();
}
const auto interface_name_ = utility::us2s(interface_name);
if (0 != setsockopt(socket_fd, SOL_SOCKET, SO_BINDTODEVICE, interface_name_.data(), interface_name_.length()))
{
char error[1024];
throw std::runtime_error(strerror_r(errno, error, sizeof(error)));
}
#else
throw std::logic_error("unsupported");
#endif
}
inline std::function<void(web::http::client::native_handle)> make_client_nativehandle_options(bool secure, const utility::string_t& client_address, slog::base_gate& gate)
{
if (client_address.empty()) return {};
// get the associated network interface name from IP address
const auto interface_name = web::hosts::experimental::get_interface_name(client_address);
if (interface_name.empty())
{
slog::log<slog::severities::error>(gate, SLOG_FLF) << "No network interface found for " << client_address << " to bind for the HTTP client connection";
return {};
}
return [interface_name, secure, &gate](web::http::client::native_handle native_handle)
{
try
{
bind_to_device(interface_name, secure, native_handle);
}
catch (const std::exception& e)
{
slog::log<slog::severities::error>(gate, SLOG_FLF) << "Unable to bind HTTP client connection to " << interface_name << ": " << e.what();
}
};
}
#ifdef CPPRESTSDK_ENABLE_BIND_WEBSOCKET_CLIENT
// The current version of the C++ REST SDK 2.10.19 does not provide the callback to enable the custom websocket setting
inline std::function<void(web::websockets::client::native_handle)> make_ws_client_nativehandle_options(bool secure, const utility::string_t& client_address, slog::base_gate& gate)
{
if (client_address.empty()) return {};
// get the associated network interface name from IP address
const auto interface_name = web::hosts::experimental::get_interface_name(client_address);
if (interface_name.empty())
{
slog::log<slog::severities::error>(gate, SLOG_FLF) << "No network interface found for " << client_address << " to bind for the websocket client connection";
return {};
}
return [interface_name, secure, &gate](web::websockets::client::native_handle native_handle)
{
try
{
bind_to_device(interface_name, secure, native_handle);
}
catch (const std::exception& e)
{
slog::log<slog::severities::error>(gate, SLOG_FLF) << "Unable to bind websocket client connection to " << interface_name << ": " << e.what();
}
};
}
#endif
#endif
}
// construct client config based on specified secure flag and settings, e.g. using the specified proxy and OCSP config
// with the remaining options defaulted, e.g. request timeout
web::http::client::http_client_config make_http_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate)
{
web::http::client::http_client_config config;
const auto proxy = proxy_uri(settings);
if (!proxy.is_empty()) config.set_proxy(proxy);
if (secure) config.set_validate_certificates(nmos::experimental::fields::validate_certificates(settings));
#if !defined(_WIN32) && !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO)
if (secure) config.set_ssl_context_callback(details::make_client_ssl_context_callback<web::http::http_exception>(settings, load_ca_certificates, gate));
config.set_nativehandle_options(details::make_client_nativehandle_options(secure, nmos::experimental::fields::client_address(settings), gate));
#endif
return config;
}
// construct client config based on settings, e.g. using the specified proxy and OCSP config
// with the remaining options defaulted, e.g. request timeout
web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate)
{
return make_http_client_config(nmos::experimental::fields::client_secure(settings), settings, load_ca_certificates, gate);
}
// construct oauth2 config with the bearer token
web::http::oauth2::experimental::oauth2_config make_oauth2_config(const web::http::oauth2::experimental::oauth2_token& bearer_token)
{
web::http::oauth2::experimental::oauth2_config config(U(""), U(""), U(""), U(""), U(""), U(""));
config.set_token(bearer_token);
return config;
}
// construct client config including OAuth 2.0 config based on settings, e.g. using the specified proxy and OCSP config
// with the remaining options defaulted, e.g. authorization request timeout
web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate)
{
auto config = make_http_client_config(settings, load_ca_certificates, gate);
if (bearer_token.is_valid_access_token())
{
config.set_oauth2(make_oauth2_config(bearer_token));
}
return config;
}
// construct client config including OAuth 2.0 config based on settings, e.g. using the specified proxy and OCSP config
// with the remaining options defaulted, e.g. authorization request timeout
web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate)
{
web::http::oauth2::experimental::oauth2_token bearer_token;
if (get_authorization_bearer_token)
{
bearer_token = get_authorization_bearer_token();
}
return make_http_client_config(settings, load_ca_certificates, bearer_token, gate);
}
// construct client config based on specified secure flag and settings, e.g. using the specified proxy
// with the remaining options defaulted
web::websockets::client::websocket_client_config make_websocket_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate)
{
web::websockets::client::websocket_client_config config;
const auto proxy = proxy_uri(settings);
if (!proxy.is_empty()) config.set_proxy(proxy);
if (secure) config.set_validate_certificates(nmos::experimental::fields::validate_certificates(settings));
#if !defined(_WIN32) || !defined(__cplusplus_winrt)
if (secure) config.set_ssl_context_callback(details::make_client_ssl_context_callback<web::websockets::client::websocket_exception>(settings, load_ca_certificates, gate));
#ifdef CPPRESTSDK_ENABLE_BIND_WEBSOCKET_CLIENT
config.set_nativehandle_options(details::make_ws_client_nativehandle_options(secure, nmos::experimental::fields::client_address(settings), gate));
#endif
#endif
return config;
}
// construct client config based on settings, e.g. using the specified proxy
// with the remaining options defaulted
web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate)
{
return make_websocket_client_config(nmos::experimental::fields::client_secure(settings), settings, load_ca_certificates, gate);
}
// construct client config based on settings and access token, e.g. using the specified proxy
// with the remaining options defaulted
web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate)
{
auto config = make_websocket_client_config(settings, std::move(load_ca_certificates), gate);
if (get_authorization_bearer_token)
{
const auto bearer_token = get_authorization_bearer_token();
config.headers().add(web::http::header_names::authorization, U("Bearer ") + bearer_token.access_token());
}
return config;
}
namespace details
{
// make a client for the specified base_uri and config, with host name for the Host header sneakily stashed in user info
std::unique_ptr<web::http::client::http_client> make_http_client(const web::uri& base_uri, const web::http::client::http_client_config& client_config)
{
// unstash the host name for the Host header
// cf. nmos::details::resolve_service
// don't bother clearing user_info since http_client makes no use of it
// see https://github.com/microsoft/cpprestsdk/issues/3
std::unique_ptr<web::http::client::http_client> client(new web::http::client::http_client(base_uri, client_config));
if (!base_uri.user_info().empty())
{
auto host = base_uri.user_info();
// hmm, in secure mode, don't append the port to the Host header
// because both calc_cn_host in cpprestsdk/Release/src/http/client/http_client_asio.cpp
// and winhttp_client::send_request in cpprestsdk/Release/src/http/client/http_client_winhttp.cpp
// compare the entire Host header value with the certificate Common Name
// which causes an SSL handshake error
// see https://github.com/microsoft/cpprestsdk/issues/1790
if (base_uri.port() > 0 && !web::is_secure_uri_scheme(base_uri.scheme()))
{
host.append(U(":")).append(utility::conversions::details::to_string_t(base_uri.port()));
}
client->add_handler([host](web::http::http_request request, std::shared_ptr<web::http::http_pipeline_stage> next_stage) -> pplx::task<web::http::http_response>
{
request.headers().add(web::http::header_names::host, host);
return next_stage->propagate(request);
});
}
return client;
}
}
// make a request with logging
pplx::task<web::http::http_response> api_request(web::http::client::http_client client, web::http::http_request request, slog::base_gate& gate, const pplx::cancellation_token& token)
{
slog::log<slog::severities::too_much_info>(gate, SLOG_FLF) << "Sending request";
// see https://developer.mozilla.org/en-US/docs/Web/API/Resource_Timing_API#Resource_loading_timestamps
const auto start_time = std::chrono::steady_clock::now();
return client.request(request, token).then([start_time, &gate](web::http::http_response res)
{
const auto response_start = std::chrono::steady_clock::now();
const auto request_dur = std::chrono::duration_cast<std::chrono::microseconds>(response_start - start_time).count() / 1000.0;
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing
const auto server_timing = res.headers().find(web::http::experimental::header_names::server_timing);
if (res.headers().end() != server_timing)
{
slog::log<slog::severities::too_much_info>(gate, SLOG_FLF) << "Response received after " << request_dur << "ms (server-timing: " << server_timing->second << ")";
}
else
{
slog::log<slog::severities::too_much_info>(gate, SLOG_FLF) << "Response received after " << request_dur << "ms";
}
return res;
});
}
pplx::task<web::http::http_response> api_request(web::http::client::http_client client, const web::http::method& mtd, slog::base_gate& gate, const pplx::cancellation_token& token)
{
web::http::http_request msg(mtd);
return api_request(client, msg, gate, token);
}
pplx::task<web::http::http_response> api_request(web::http::client::http_client client, const web::http::method& mtd, const utility::string_t& path_query_fragment, slog::base_gate& gate, const pplx::cancellation_token& token)
{
web::http::http_request msg(mtd);
msg.set_request_uri(path_query_fragment);
return api_request(client, msg, gate, token);
}
pplx::task<web::http::http_response> api_request(web::http::client::http_client client, const web::http::method& mtd, const utility::string_t& path_query_fragment, const web::json::value& body_data, slog::base_gate& gate, const pplx::cancellation_token& token)
{
web::http::http_request msg(mtd);
msg.set_request_uri(path_query_fragment);
msg.set_body(body_data);
return api_request(client, msg, gate, token);
}
}