1 /***
2 * Copyright (C) Microsoft. All rights reserved.
3 * Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
4 *
5 * =+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
6 *
7 * HTTP Library: Client-side APIs.
8 *
9 * This file contains a cross platform implementation based on Boost.ASIO.
10 *
11 * For the latest on this and related APIs, please see: https://github.com/Microsoft/cpprestsdk
12 *
13 * =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
14 ****/
15
16 #include "stdafx.h"
17
18 #include "../common/connection_pool_helpers.h"
19 #include "../common/internal_http_helpers.h"
20 #include "cpprest/asyncrt_utils.h"
21 #include <sstream>
22
23 #if defined(__clang__)
24 #pragma clang diagnostic push
25 #pragma clang diagnostic ignored "-Wunused-local-typedef"
26 #pragma clang diagnostic ignored "-Winfinite-recursion"
27 #endif
28 #include <boost/algorithm/string.hpp>
29 #include <boost/asio.hpp>
30 #include <boost/asio/ssl.hpp>
31 #include <boost/asio/ssl/error.hpp>
32 #include <boost/asio/steady_timer.hpp>
33 #include <boost/bind.hpp>
34 #if defined(__clang__)
35 #pragma clang diagnostic pop
36 #endif
37
38 #if defined(BOOST_NO_CXX11_SMART_PTR)
39 #error "Cpp rest SDK requires c++11 smart pointer support from boost"
40 #endif
41
42 #include "../common/x509_cert_utilities.h"
43 #include "cpprest/base_uri.h"
44 #include "cpprest/details/http_helpers.h"
45 #include "http_client_impl.h"
46 #include "pplx/threadpool.h"
47 #include <memory>
48 #include <unordered_set>
49
50 #if defined(__GNUC__) && !defined(__clang__)
51
52 #if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8)
53 #define AND_CAPTURE_MEMBER_FUNCTION_POINTERS
54 #else
55 // GCC Bug 56222 - Pointer to member in lambda should not require this to be captured
56 // See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56222
57 // GCC Bug 51494 - Legal program rejection - capturing "this" when using static method inside lambda
58 // See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=51494
59 #define AND_CAPTURE_MEMBER_FUNCTION_POINTERS , this
60 #endif
61
62 #elif defined(_MSC_VER)
63
64 #if _MSC_VER >= 1900
65 #define AND_CAPTURE_MEMBER_FUNCTION_POINTERS
66 #else
67 // This bug also afflicts VS2013 which incorrectly reports "warning C4573: the usage of 'symbol' requires the compiler
68 // to capture 'this' but the current default capture mode does not allow it"
69 #define AND_CAPTURE_MEMBER_FUNCTION_POINTERS , this
70 #endif
71
72 #else
73
74 #define AND_CAPTURE_MEMBER_FUNCTION_POINTERS
75
76 #endif
77
78 using boost::asio::ip::tcp;
79
80 #ifdef __ANDROID__
81 using utility::conversions::details::to_string;
82 #else
83 using std::to_string;
84 #endif
85
86 namespace
87 {
88 const std::string CRLF("\r\n");
89
calc_cn_host(const web::http::uri & baseUri,const web::http::http_headers & requestHeaders)90 std::string calc_cn_host(const web::http::uri& baseUri, const web::http::http_headers& requestHeaders)
91 {
92 std::string result;
93 if (baseUri.scheme() == U("https"))
94 {
95 const utility::string_t* encResult;
96 const auto hostHeader = requestHeaders.find(_XPLATSTR("Host"));
97 if (hostHeader == requestHeaders.end())
98 {
99 encResult = &baseUri.host();
100 }
101 else
102 {
103 encResult = &hostHeader->second;
104 }
105
106 result = utility::conversions::to_utf8string(*encResult);
107 utility::details::inplace_tolower(result);
108 }
109
110 return result;
111 }
112 } // namespace
113
114 namespace web
115 {
116 namespace http
117 {
118 namespace client
119 {
120 namespace details
121 {
122 enum class httpclient_errorcode_context
123 {
124 none = 0,
125 connect,
126 handshake,
127 writeheader,
128 writebody,
129 readheader,
130 readbody,
131 close
132 };
133
generate_base64_userpass(const::web::credentials & creds)134 static std::string generate_base64_userpass(const ::web::credentials& creds)
135 {
136 auto userpass = creds.username() + U(":") + *creds._internal_decrypt();
137 auto&& u8_userpass = utility::conversions::to_utf8string(userpass);
138 std::vector<unsigned char> credentials_buffer(u8_userpass.begin(), u8_userpass.end());
139 return utility::conversions::to_utf8string(utility::conversions::to_base64(credentials_buffer));
140 }
141
142 class asio_connection_pool;
143
144 class asio_connection
145 {
146 friend class asio_client;
147
148 public:
asio_connection(boost::asio::io_service & io_service)149 asio_connection(boost::asio::io_service& io_service)
150 : m_socket_lock()
151 , m_socket(io_service)
152 , m_ssl_stream()
153 , m_cn_hostname()
154 , m_is_reused(false)
155 , m_keep_alive(true)
156 , m_closed(false)
157 {
158 }
159
~asio_connection()160 ~asio_connection() { close(); }
161
162 // This simply instantiates the internal state to support ssl. It does not perform the handshake.
upgrade_to_ssl(std::string && cn_hostname,const std::function<void (boost::asio::ssl::context &)> & ssl_context_callback)163 void upgrade_to_ssl(std::string&& cn_hostname,
164 const std::function<void(boost::asio::ssl::context&)>& ssl_context_callback)
165 {
166 std::lock_guard<std::mutex> lock(m_socket_lock);
167 assert(!is_ssl());
168 boost::asio::ssl::context ssl_context(boost::asio::ssl::context::sslv23);
169 ssl_context.set_default_verify_paths();
170 ssl_context.set_options(boost::asio::ssl::context::default_workarounds);
171 if (ssl_context_callback)
172 {
173 ssl_context_callback(ssl_context);
174 }
175 m_ssl_stream = utility::details::make_unique<boost::asio::ssl::stream<boost::asio::ip::tcp::socket&>>(
176 m_socket, ssl_context);
177 m_cn_hostname = std::move(cn_hostname);
178 }
179
close()180 void close()
181 {
182 std::lock_guard<std::mutex> lock(m_socket_lock);
183
184 // Ensures closed connections owned by request_context will not be put to pool when they are released.
185 m_keep_alive = false;
186 m_closed = true;
187
188 boost::system::error_code error;
189 m_socket.shutdown(tcp::socket::shutdown_both, error);
190 m_socket.close(error);
191 }
192
cancel()193 boost::system::error_code cancel()
194 {
195 std::lock_guard<std::mutex> lock(m_socket_lock);
196 boost::system::error_code error;
197 m_socket.cancel(error);
198 return error;
199 }
200
is_reused() const201 bool is_reused() const { return m_is_reused; }
set_keep_alive(bool keep_alive)202 void set_keep_alive(bool keep_alive) { m_keep_alive = keep_alive; }
keep_alive() const203 bool keep_alive() const { return m_keep_alive; }
is_ssl() const204 bool is_ssl() const { return m_ssl_stream ? true : false; }
cn_hostname() const205 const std::string& cn_hostname() const { return m_cn_hostname; }
206
207 // Check if the error code indicates that the connection was closed by the
208 // server: this is used to detect if a connection in the pool was closed during
209 // its period of inactivity and we should reopen it.
was_reused_and_closed_by_server(const boost::system::error_code & ec) const210 bool was_reused_and_closed_by_server(const boost::system::error_code& ec) const
211 {
212 if (!is_reused())
213 {
214 // Don't bother reopening the connection if it's a new one: in this
215 // case, even if the connection was really lost, it's still a real
216 // error and we shouldn't try to reopen it.
217 return false;
218 }
219
220 // These errors tell if connection was closed.
221 if ((boost::asio::error::eof == ec) || (boost::asio::error::connection_reset == ec) ||
222 (boost::asio::error::connection_aborted == ec))
223 {
224 return true;
225 }
226
227 if (is_ssl())
228 {
229 // For SSL connections, we can also get a different error due to
230 // incorrect secure connection shutdown if it was closed by the
231 // server due to inactivity. Unfortunately, the exact error we get
232 // in this case depends on the Boost.Asio version used.
233 #if BOOST_ASIO_VERSION >= 101008
234 if (boost::asio::ssl::error::stream_truncated == ec) return true;
235 #else // Asio < 1.10.8 didn't have ssl::error::stream_truncated
236 if (boost::system::error_code(ERR_PACK(ERR_LIB_SSL, 0, SSL_R_SHORT_READ),
237 boost::asio::error::get_ssl_category()) == ec)
238 return true;
239 #endif
240 }
241
242 return false;
243 }
244
245 template<typename Iterator, typename Handler>
async_connect(const Iterator & begin,const Handler & handler)246 void async_connect(const Iterator& begin, const Handler& handler)
247 {
248 {
249 std::lock_guard<std::mutex> lock(m_socket_lock);
250 if (!m_closed)
251 {
252 m_socket.async_connect(begin, handler);
253 return;
254 }
255 } // unlock
256
257 handler(boost::asio::error::operation_aborted);
258 }
259
260 template<typename HandshakeHandler, typename CertificateHandler>
async_handshake(boost::asio::ssl::stream_base::handshake_type type,const http_client_config & config,const HandshakeHandler & handshake_handler,const CertificateHandler & cert_handler)261 void async_handshake(boost::asio::ssl::stream_base::handshake_type type,
262 const http_client_config& config,
263 const HandshakeHandler& handshake_handler,
264 const CertificateHandler& cert_handler)
265 {
266 std::lock_guard<std::mutex> lock(m_socket_lock);
267 assert(is_ssl());
268
269 // Check to turn on/off server certificate verification.
270 if (config.validate_certificates())
271 {
272 m_ssl_stream->set_verify_mode(boost::asio::ssl::context::verify_peer);
273 m_ssl_stream->set_verify_callback(cert_handler);
274 }
275 else
276 {
277 m_ssl_stream->set_verify_mode(boost::asio::ssl::context::verify_none);
278 }
279
280 // Check to set host name for Server Name Indication (SNI)
281 if (config.is_tlsext_sni_enabled())
282 {
283 SSL_set_tlsext_host_name(m_ssl_stream->native_handle(), &m_cn_hostname[0]);
284 }
285
286 m_ssl_stream->async_handshake(type, handshake_handler);
287 }
288
289 template<typename ConstBufferSequence, typename Handler>
async_write(ConstBufferSequence & buffer,const Handler & writeHandler)290 void async_write(ConstBufferSequence& buffer, const Handler& writeHandler)
291 {
292 std::lock_guard<std::mutex> lock(m_socket_lock);
293 if (m_ssl_stream)
294 {
295 boost::asio::async_write(*m_ssl_stream, buffer, writeHandler);
296 }
297 else
298 {
299 boost::asio::async_write(m_socket, buffer, writeHandler);
300 }
301 }
302
303 template<typename MutableBufferSequence, typename CompletionCondition, typename Handler>
async_read(MutableBufferSequence & buffer,const CompletionCondition & condition,const Handler & readHandler)304 void async_read(MutableBufferSequence& buffer, const CompletionCondition& condition, const Handler& readHandler)
305 {
306 std::lock_guard<std::mutex> lock(m_socket_lock);
307 if (m_ssl_stream)
308 {
309 boost::asio::async_read(*m_ssl_stream, buffer, condition, readHandler);
310 }
311 else
312 {
313 boost::asio::async_read(m_socket, buffer, condition, readHandler);
314 }
315 }
316
317 template<typename Handler>
async_read_until(boost::asio::streambuf & buffer,const std::string & delim,const Handler & readHandler)318 void async_read_until(boost::asio::streambuf& buffer, const std::string& delim, const Handler& readHandler)
319 {
320 std::lock_guard<std::mutex> lock(m_socket_lock);
321 if (m_ssl_stream)
322 {
323 boost::asio::async_read_until(*m_ssl_stream, buffer, delim, readHandler);
324 }
325 else
326 {
327 boost::asio::async_read_until(m_socket, buffer, delim, readHandler);
328 }
329 }
330
start_reuse()331 void start_reuse() { m_is_reused = true; }
332
enable_no_delay()333 void enable_no_delay()
334 {
335 boost::asio::ip::tcp::no_delay option(true);
336 boost::system::error_code error_ignored;
337 m_socket.set_option(option, error_ignored);
338 }
339
340 private:
341 // Guards concurrent access to socket/ssl::stream. This is necessary
342 // because timeouts and cancellation can touch the socket at the same time
343 // as normal message processing.
344 std::mutex m_socket_lock;
345 tcp::socket m_socket;
346 std::unique_ptr<boost::asio::ssl::stream<tcp::socket&>> m_ssl_stream;
347 std::string m_cn_hostname;
348
349 bool m_is_reused;
350 bool m_keep_alive;
351 bool m_closed;
352 };
353
354 /// <summary>Implements a connection pool with adaptive connection removal</summary>
355 /// <remarks>
356 /// Every 30 seconds, the lambda in `start_epoch_interval` fires, triggering the
357 /// cleanup of any connections that have resided in the pool since the last
358 /// cleanup phase.
359 ///
360 /// During the cleanup phase, connections are removed starting with the oldest. This
361 /// ensures that if a high intensity workload is followed by a low intensity workload,
362 /// the connection pool will correctly adapt to the low intensity workload.
363 ///
364 /// Specifically, the following code will eventually result in a maximum of one pooled
365 /// connection regardless of the initial number of pooled connections:
366 /// <code>
367 /// while(1)
368 /// {
369 /// auto conn = pool.try_acquire();
370 /// if (!conn) conn = new_conn();
371 /// pool.release(std::move(conn));
372 /// }
373 /// </code>
374 /// </remarks>
375 class asio_connection_pool final : public std::enable_shared_from_this<asio_connection_pool>
376 {
377 public:
asio_connection_pool()378 asio_connection_pool()
379 : m_lock()
380 , m_connections()
381 , m_is_timer_running(false)
382 , m_pool_epoch_timer(crossplat::threadpool::shared_instance().service())
383 {
384 }
385
386 asio_connection_pool(const asio_connection_pool&) = delete;
387 asio_connection_pool& operator=(const asio_connection_pool&) = delete;
388
try_acquire(const std::string & cn_hostname)389 std::shared_ptr<asio_connection> try_acquire(const std::string& cn_hostname)
390 {
391 std::lock_guard<std::mutex> lock(m_lock);
392 if (m_connections.empty())
393 {
394 return nullptr;
395 }
396
397 auto conn = m_connections[cn_hostname].try_acquire();
398 if (conn)
399 {
400 conn->start_reuse();
401 }
402
403 return conn;
404 }
405
release(std::shared_ptr<asio_connection> && connection)406 void release(std::shared_ptr<asio_connection>&& connection)
407 {
408 connection->cancel();
409 if (!connection->keep_alive())
410 {
411 connection.reset();
412 return;
413 }
414
415 std::lock_guard<std::mutex> lock(m_lock);
416 if (!m_is_timer_running)
417 {
418 start_epoch_interval(shared_from_this());
419 m_is_timer_running = true;
420 }
421
422 m_connections[connection->cn_hostname()].release(std::move(connection));
423 }
424
425 private:
426 // Note: must be called under m_lock
start_epoch_interval(const std::shared_ptr<asio_connection_pool> & pool)427 static void start_epoch_interval(const std::shared_ptr<asio_connection_pool>& pool)
428 {
429 auto& self = *pool;
430 std::weak_ptr<asio_connection_pool> weak_pool = pool;
431
432 self.m_pool_epoch_timer.expires_from_now(boost::posix_time::seconds(30));
433 self.m_pool_epoch_timer.async_wait([weak_pool](const boost::system::error_code& ec) {
434 if (ec)
435 {
436 return;
437 }
438
439 auto pool = weak_pool.lock();
440 if (!pool)
441 {
442 return;
443 }
444
445 auto& self = *pool;
446 std::lock_guard<std::mutex> lock(self.m_lock);
447 bool restartTimer = false;
448 for (auto& entry : self.m_connections)
449 {
450 if (entry.second.free_stale_connections())
451 {
452 restartTimer = true;
453 }
454 }
455
456 if (restartTimer)
457 {
458 start_epoch_interval(pool);
459 }
460 else
461 {
462 self.m_is_timer_running = false;
463 }
464 });
465 }
466
467 std::mutex m_lock;
468 std::map<std::string, connection_pool_stack<asio_connection>> m_connections;
469 bool m_is_timer_running;
470 boost::asio::deadline_timer m_pool_epoch_timer;
471 };
472
473 class asio_client final : public _http_client_communicator
474 {
475 public:
asio_client(http::uri && address,http_client_config && client_config)476 asio_client(http::uri&& address, http_client_config&& client_config)
477 : _http_client_communicator(std::move(address), std::move(client_config))
478 , m_pool(std::make_shared<asio_connection_pool>())
479 {
480 }
481
482 virtual void send_request(const std::shared_ptr<request_context>& request_ctx) override;
483
release_connection(std::shared_ptr<asio_connection> && conn)484 void release_connection(std::shared_ptr<asio_connection>&& conn) { m_pool->release(std::move(conn)); }
485
obtain_connection(const http_request & req)486 std::shared_ptr<asio_connection> obtain_connection(const http_request& req)
487 {
488 std::string cn_host = calc_cn_host(base_uri(), req.headers());
489 std::shared_ptr<asio_connection> conn = m_pool->try_acquire(cn_host);
490 if (conn == nullptr)
491 {
492 // Pool was empty. Create a new connection
493 conn = std::make_shared<asio_connection>(crossplat::threadpool::shared_instance().service());
494 if (base_uri().scheme() == U("https") && !this->client_config().proxy().is_specified())
495 {
496 conn->upgrade_to_ssl(std::move(cn_host), this->client_config().get_ssl_context_callback());
497 }
498 }
499
500 return conn;
501 }
502
503 virtual pplx::task<http_response> propagate(http_request request) override;
504
505 private:
506 const std::shared_ptr<asio_connection_pool> m_pool;
507 };
508
509 class asio_context final : public request_context, public std::enable_shared_from_this<asio_context>
510 {
511 friend class asio_client;
512
513 public:
asio_context(const std::shared_ptr<_http_client_communicator> & client,http_request & request,const std::shared_ptr<asio_connection> & connection)514 asio_context(const std::shared_ptr<_http_client_communicator>& client,
515 http_request& request,
516 const std::shared_ptr<asio_connection>& connection)
517 : request_context(client, request)
518 , m_content_length(0)
519 , m_needChunked(false)
520 , m_timer(client->client_config().timeout<std::chrono::microseconds>())
521 , m_resolver(crossplat::threadpool::shared_instance().service())
522 , m_connection(connection)
523 #ifdef CPPREST_PLATFORM_ASIO_CERT_VERIFICATION_AVAILABLE
524 , m_openssl_failed(false)
525 #endif // CPPREST_PLATFORM_ASIO_CERT_VERIFICATION_AVAILABLE
526 {
527 }
528
~asio_context()529 virtual ~asio_context()
530 {
531 m_timer.stop();
532 // Release connection back to the pool. If connection was not closed, it will be put to the pool for reuse.
533 std::static_pointer_cast<asio_client>(m_http_client)->release_connection(std::move(m_connection));
534 }
535
create_request_context(std::shared_ptr<_http_client_communicator> & client,http_request & request)536 static std::shared_ptr<request_context> create_request_context(std::shared_ptr<_http_client_communicator>& client,
537 http_request& request)
538 {
539 auto client_cast(std::static_pointer_cast<asio_client>(client));
540 auto connection(client_cast->obtain_connection(request));
541 auto ctx = std::make_shared<asio_context>(client, request, connection);
542 ctx->m_timer.set_ctx(std::weak_ptr<asio_context>(ctx));
543 return ctx;
544 }
545
546 class ssl_proxy_tunnel final : public std::enable_shared_from_this<ssl_proxy_tunnel>
547 {
548 public:
ssl_proxy_tunnel(std::shared_ptr<asio_context> context,std::function<void (std::shared_ptr<asio_context>)> ssl_tunnel_established)549 ssl_proxy_tunnel(std::shared_ptr<asio_context> context,
550 std::function<void(std::shared_ptr<asio_context>)> ssl_tunnel_established)
551 : m_ssl_tunnel_established(ssl_tunnel_established), m_context(context)
552 {
553 }
554
start_proxy_connect()555 void start_proxy_connect()
556 {
557 auto proxy = m_context->m_http_client->client_config().proxy();
558 auto proxy_uri = proxy.address();
559
560 utility::string_t proxy_host = proxy_uri.host();
561 int proxy_port = proxy_uri.port() == -1 ? 8080 : proxy_uri.port();
562
563 const auto& base_uri = m_context->m_http_client->base_uri();
564 const auto& host = utility::conversions::to_utf8string(base_uri.host());
565 const int portRaw = base_uri.port();
566 const int port = (portRaw != 0) ? portRaw : 443;
567
568 std::ostream request_stream(&m_request);
569 request_stream.imbue(std::locale::classic());
570
571 request_stream << "CONNECT " << host << ":" << port << " HTTP/1.1\r\n";
572 request_stream << "Host: " << host << ":" << port << CRLF;
573 request_stream << "Proxy-Connection: Keep-Alive\r\n";
574
575 if (m_context->m_http_client->client_config().proxy().credentials().is_set())
576 {
577 request_stream << m_context->generate_basic_proxy_auth_header();
578 }
579
580 request_stream << CRLF;
581
582 m_context->m_timer.start();
583
584 tcp::resolver::query query(utility::conversions::to_utf8string(proxy_host), to_string(proxy_port));
585
586 auto client = std::static_pointer_cast<asio_client>(m_context->m_http_client);
587 m_context->m_resolver.async_resolve(query,
588 boost::bind(&ssl_proxy_tunnel::handle_resolve,
589 shared_from_this(),
590 boost::asio::placeholders::error,
591 boost::asio::placeholders::iterator));
592 }
593
594 private:
handle_resolve(const boost::system::error_code & ec,tcp::resolver::iterator endpoints)595 void handle_resolve(const boost::system::error_code& ec, tcp::resolver::iterator endpoints)
596 {
597 if (ec)
598 {
599 m_context->report_error("Error resolving proxy address", ec, httpclient_errorcode_context::connect);
600 }
601 else
602 {
603 m_context->m_timer.reset();
604 auto endpoint = *endpoints;
605 m_context->m_connection->async_connect(endpoint,
606 boost::bind(&ssl_proxy_tunnel::handle_tcp_connect,
607 shared_from_this(),
608 boost::asio::placeholders::error,
609 ++endpoints));
610 }
611 }
612
handle_tcp_connect(const boost::system::error_code & ec,tcp::resolver::iterator endpoints)613 void handle_tcp_connect(const boost::system::error_code& ec, tcp::resolver::iterator endpoints)
614 {
615 if (!ec)
616 {
617 m_context->m_timer.reset();
618 m_context->m_connection->enable_no_delay();
619 m_context->m_connection->async_write(m_request,
620 boost::bind(&ssl_proxy_tunnel::handle_write_request,
621 shared_from_this(),
622 boost::asio::placeholders::error));
623 }
624 else if (endpoints == tcp::resolver::iterator())
625 {
626 m_context->report_error(
627 "Failed to connect to any resolved proxy endpoint", ec, httpclient_errorcode_context::connect);
628 }
629 else
630 {
631 m_context->m_timer.reset();
632 //// Replace the connection. This causes old connection object to go out of scope.
633 auto client = std::static_pointer_cast<asio_client>(m_context->m_http_client);
634 try
635 {
636 m_context->m_connection = client->obtain_connection(m_context->m_request);
637 }
638 catch (...)
639 {
640 m_context->report_exception(std::current_exception());
641 return;
642 }
643
644 auto endpoint = *endpoints;
645 m_context->m_connection->async_connect(endpoint,
646 boost::bind(&ssl_proxy_tunnel::handle_tcp_connect,
647 shared_from_this(),
648 boost::asio::placeholders::error,
649 ++endpoints));
650 }
651 }
652
handle_write_request(const boost::system::error_code & err)653 void handle_write_request(const boost::system::error_code& err)
654 {
655 if (!err)
656 {
657 m_context->m_timer.reset();
658 m_context->m_connection->async_read_until(m_response,
659 CRLF + CRLF,
660 boost::bind(&ssl_proxy_tunnel::handle_status_line,
661 shared_from_this(),
662 boost::asio::placeholders::error));
663 }
664 else
665 {
666 m_context->report_error(
667 "Failed to send connect request to proxy.", err, httpclient_errorcode_context::writebody);
668 }
669 }
670
handle_status_line(const boost::system::error_code & ec)671 void handle_status_line(const boost::system::error_code& ec)
672 {
673 if (!ec)
674 {
675 m_context->m_timer.reset();
676 std::istream response_stream(&m_response);
677 response_stream.imbue(std::locale::classic());
678 std::string http_version;
679 response_stream >> http_version;
680 status_code status_code;
681 response_stream >> status_code;
682
683 if (!response_stream || http_version.substr(0, 5) != "HTTP/")
684 {
685 m_context->report_error("Invalid HTTP status line during proxy connection",
686 ec,
687 httpclient_errorcode_context::readheader);
688 return;
689 }
690
691 if (status_code != 200)
692 {
693 m_context->report_error("Expected a 200 response from proxy, received: " + to_string(status_code),
694 ec,
695 httpclient_errorcode_context::readheader);
696 return;
697 }
698
699 try
700 {
701 m_context->upgrade_to_ssl();
702 }
703 catch (...)
704 {
705 m_context->report_exception(std::current_exception());
706 return;
707 }
708
709 m_ssl_tunnel_established(m_context);
710 }
711 else
712 {
713 m_context->handle_failed_read_status_line(ec, "Failed to read HTTP status line from proxy");
714 }
715 }
716
717 std::function<void(std::shared_ptr<asio_context>)> m_ssl_tunnel_established;
718 std::shared_ptr<asio_context> m_context;
719
720 boost::asio::streambuf m_request;
721 boost::asio::streambuf m_response;
722 };
723
724 enum class http_proxy_type
725 {
726 none,
727 http,
728 ssl_tunnel
729 };
730
start_request()731 void start_request()
732 {
733 if (m_request._cancellation_token().is_canceled())
734 {
735 request_context::report_error(make_error_code(std::errc::operation_canceled).value(),
736 "Request canceled by user.");
737 return;
738 }
739
740 http_proxy_type proxy_type = http_proxy_type::none;
741 std::string proxy_host;
742 int proxy_port = -1;
743
744 // There is no support for auto-detection of proxies on non-windows platforms, it must be specified explicitly
745 // from the client code.
746 if (m_http_client->client_config().proxy().is_specified())
747 {
748 proxy_type =
749 m_http_client->base_uri().scheme() == U("https") ? http_proxy_type::ssl_tunnel : http_proxy_type::http;
750 auto proxy = m_http_client->client_config().proxy();
751 auto proxy_uri = proxy.address();
752 proxy_port = proxy_uri.port() == -1 ? 8080 : proxy_uri.port();
753 proxy_host = utility::conversions::to_utf8string(proxy_uri.host());
754 }
755
756 auto start_http_request_flow = [proxy_type, proxy_host, proxy_port AND_CAPTURE_MEMBER_FUNCTION_POINTERS](
757 std::shared_ptr<asio_context> ctx) {
758 if (ctx->m_request._cancellation_token().is_canceled())
759 {
760 ctx->request_context::report_error(make_error_code(std::errc::operation_canceled).value(),
761 "Request canceled by user.");
762 return;
763 }
764
765 const auto& base_uri = ctx->m_http_client->base_uri();
766 const auto full_uri = uri_builder(base_uri).append(ctx->m_request.relative_uri()).to_uri();
767
768 // For a normal http proxy, we need to specify the full request uri, otherwise just specify the resource
769 auto encoded_resource =
770 proxy_type == http_proxy_type::http ? full_uri.to_string() : full_uri.resource().to_string();
771
772 if (encoded_resource.empty())
773 {
774 encoded_resource = U("/");
775 }
776
777 const auto& method = ctx->m_request.method();
778
779 // stop injection of headers via method
780 // resource should be ok, since it's been encoded
781 // and host won't resolve
782 if (!::web::http::details::validate_method(method))
783 {
784 ctx->report_exception(http_exception("The method string is invalid."));
785 return;
786 }
787
788 std::ostream request_stream(&ctx->m_body_buf);
789 request_stream.imbue(std::locale::classic());
790 const auto& host = utility::conversions::to_utf8string(base_uri.host());
791
792 request_stream << utility::conversions::to_utf8string(method) << " "
793 << utility::conversions::to_utf8string(encoded_resource) << " "
794 << "HTTP/1.1\r\n";
795
796 int port = base_uri.port();
797
798 if (base_uri.is_port_default())
799 {
800 port = (ctx->m_connection->is_ssl() ? 443 : 80);
801 }
802
803 // Add the Host header if user has not specified it explicitly
804 if (!ctx->m_request.headers().has(header_names::host))
805 {
806 request_stream << "Host: " << host;
807 if (!base_uri.is_port_default())
808 {
809 request_stream << ":" << port;
810 }
811 request_stream << CRLF;
812 }
813
814 // Extra request headers are constructed here.
815 std::string extra_headers;
816
817 // Add header for basic proxy authentication
818 if (proxy_type == http_proxy_type::http &&
819 ctx->m_http_client->client_config().proxy().credentials().is_set())
820 {
821 extra_headers.append(ctx->generate_basic_proxy_auth_header());
822 }
823
824 if (ctx->m_http_client->client_config().credentials().is_set())
825 {
826 extra_headers.append(ctx->generate_basic_auth_header());
827 }
828
829 extra_headers += utility::conversions::to_utf8string(ctx->get_compression_header());
830
831 // Check user specified transfer-encoding.
832 std::string transferencoding;
833 if (ctx->m_request.headers().match(header_names::transfer_encoding, transferencoding) &&
834 boost::icontains(transferencoding, U("chunked")))
835 {
836 ctx->m_needChunked = true;
837 }
838 else if (!ctx->m_request.headers().match(header_names::content_length, ctx->m_content_length))
839 {
840 // Stream without content length is the signal of requiring transfer encoding chunked.
841 if (ctx->m_request.body())
842 {
843 ctx->m_needChunked = true;
844 extra_headers.append("Transfer-Encoding:chunked\r\n");
845 }
846 else if (ctx->m_request.method() == methods::POST || ctx->m_request.method() == methods::PUT)
847 {
848 // Some servers do not accept POST/PUT requests with a content length of 0, such as
849 // lighttpd - http://serverfault.com/questions/315849/curl-post-411-length-required
850 // old apache versions - https://issues.apache.org/jira/browse/TS-2902
851 extra_headers.append("Content-Length: 0\r\n");
852 }
853 }
854
855 if (proxy_type == http_proxy_type::http)
856 {
857 extra_headers.append("Cache-Control: no-store, no-cache\r\n"
858 "Pragma: no-cache\r\n");
859 }
860
861 request_stream << utility::conversions::to_utf8string(
862 ::web::http::details::flatten_http_headers(ctx->m_request.headers()));
863 request_stream << extra_headers;
864 // Enforce HTTP connection keep alive (even for the old HTTP/1.0 protocol).
865 request_stream << "Connection: Keep-Alive\r\n\r\n";
866
867 // Start connection timeout timer.
868 if (!ctx->m_timer.has_started())
869 {
870 ctx->m_timer.start();
871 }
872
873 if (ctx->m_connection->is_reused() || proxy_type == http_proxy_type::ssl_tunnel)
874 {
875 // If socket is a reused connection or we're connected via an ssl-tunneling proxy, try to write the
876 // request directly. In both cases we have already established a tcp connection.
877 ctx->write_request();
878 }
879 else
880 {
881 // If the connection is new (unresolved and unconnected socket), then start async
882 // call to resolve first, leading eventually to request write.
883
884 // For normal http proxies, we want to connect directly to the proxy server. It will relay our request.
885 auto tcp_host = proxy_type == http_proxy_type::http ? proxy_host : host;
886 auto tcp_port = proxy_type == http_proxy_type::http ? proxy_port : port;
887
888 tcp::resolver::query query(tcp_host, to_string(tcp_port));
889 ctx->m_resolver.async_resolve(query,
890 boost::bind(&asio_context::handle_resolve,
891 ctx,
892 boost::asio::placeholders::error,
893 boost::asio::placeholders::iterator));
894 }
895
896 // Register for notification on cancellation to abort this request.
897 if (ctx->m_request._cancellation_token() != pplx::cancellation_token::none())
898 {
899 // weak_ptr prevents lambda from taking shared ownership of the context.
900 // Otherwise context replacement in the handle_status_line() would leak the objects.
901 std::weak_ptr<asio_context> ctx_weak(ctx);
902 ctx->m_cancellationRegistration = ctx->m_request._cancellation_token().register_callback([ctx_weak]() {
903 if (auto ctx_lock = ctx_weak.lock())
904 {
905 // Shut down transmissions, close the socket and prevent connection from being pooled.
906 ctx_lock->m_connection->close();
907 }
908 });
909 }
910 };
911
912 // Note that we must not try to CONNECT using an already established connection via proxy -- this would send
913 // CONNECT to the end server which is definitely not what we want.
914 if (proxy_type == http_proxy_type::ssl_tunnel && !m_connection->is_reused())
915 {
916 // The ssl_tunnel_proxy keeps the context alive and then calls back once the ssl tunnel is established via
917 // 'start_http_request_flow'
918 std::shared_ptr<ssl_proxy_tunnel> ssl_tunnel =
919 std::make_shared<ssl_proxy_tunnel>(shared_from_this(), start_http_request_flow);
920 ssl_tunnel->start_proxy_connect();
921 }
922 else
923 {
924 start_http_request_flow(shared_from_this());
925 }
926 }
927
928 template<typename _ExceptionType>
report_exception(const _ExceptionType & e)929 void report_exception(const _ExceptionType& e)
930 {
931 report_exception(std::make_exception_ptr(e));
932 }
933
report_exception(std::exception_ptr exceptionPtr)934 void report_exception(std::exception_ptr exceptionPtr) override
935 {
936 // Don't recycle connections that had an error into the connection pool.
937 m_connection->close();
938 request_context::report_exception(exceptionPtr);
939 }
940
941 private:
upgrade_to_ssl()942 void upgrade_to_ssl()
943 {
944 auto& client = static_cast<asio_client&>(*m_http_client);
945 m_connection->upgrade_to_ssl(calc_cn_host(client.base_uri(), m_request.headers()),
946 client.client_config().get_ssl_context_callback());
947 }
948
generate_basic_auth_header()949 std::string generate_basic_auth_header()
950 {
951 std::string header;
952 header.append("Authorization: Basic ");
953 header.append(generate_base64_userpass(m_http_client->client_config().credentials()));
954 header.append(CRLF);
955 return header;
956 }
957
generate_basic_proxy_auth_header()958 std::string generate_basic_proxy_auth_header()
959 {
960 std::string header;
961 header.append("Proxy-Authorization: Basic ");
962 header.append(generate_base64_userpass(m_http_client->client_config().proxy().credentials()));
963 header.append(CRLF);
964 return header;
965 }
966
report_error(const std::string & message,const boost::system::error_code & ec,httpclient_errorcode_context context=httpclient_errorcode_context::none)967 void report_error(const std::string& message,
968 const boost::system::error_code& ec,
969 httpclient_errorcode_context context = httpclient_errorcode_context::none)
970 {
971 // By default, errorcodeValue don't need to converted
972 long errorcodeValue = ec.value();
973
974 // map timer cancellation to time_out
975 if (m_timer.has_timedout())
976 {
977 errorcodeValue = make_error_code(std::errc::timed_out).value();
978 }
979 else
980 {
981 // We need to correct inaccurate ASIO error code base on context information
982 switch (context)
983 {
984 case httpclient_errorcode_context::writeheader:
985 if (ec == boost::system::errc::broken_pipe)
986 {
987 errorcodeValue = make_error_code(std::errc::host_unreachable).value();
988 }
989 break;
990 case httpclient_errorcode_context::connect:
991 if (ec == boost::system::errc::connection_refused)
992 {
993 errorcodeValue = make_error_code(std::errc::host_unreachable).value();
994 }
995 break;
996 case httpclient_errorcode_context::readheader:
997 if (ec.default_error_condition().value() ==
998 boost::system::errc::no_such_file_or_directory) // bug in boost error_code mapping
999 {
1000 errorcodeValue = make_error_code(std::errc::connection_aborted).value();
1001 }
1002 break;
1003 default: break;
1004 }
1005 }
1006 request_context::report_error(errorcodeValue, message);
1007 }
1008
handle_connect(const boost::system::error_code & ec,tcp::resolver::iterator endpoints)1009 void handle_connect(const boost::system::error_code& ec, tcp::resolver::iterator endpoints)
1010 {
1011 m_timer.reset();
1012 if (!ec)
1013 {
1014 m_connection->enable_no_delay();
1015 write_request();
1016 }
1017 else if (ec.value() == boost::system::errc::operation_canceled ||
1018 ec.value() == boost::asio::error::operation_aborted)
1019 {
1020 report_error("Request canceled by user.", ec, httpclient_errorcode_context::connect);
1021 }
1022 else if (endpoints == tcp::resolver::iterator())
1023 {
1024 report_error("Failed to connect to any resolved endpoint", ec, httpclient_errorcode_context::connect);
1025 }
1026 else
1027 {
1028 // Replace the connection. This causes old connection object to go out of scope.
1029 auto client = std::static_pointer_cast<asio_client>(m_http_client);
1030 try
1031 {
1032 m_connection = client->obtain_connection(m_request);
1033 }
1034 catch (...)
1035 {
1036 request_context::report_exception(std::current_exception());
1037 return;
1038 }
1039
1040 auto endpoint = *endpoints;
1041 m_connection->async_connect(
1042 endpoint,
1043 boost::bind(
1044 &asio_context::handle_connect, shared_from_this(), boost::asio::placeholders::error, ++endpoints));
1045 }
1046 }
1047
handle_resolve(const boost::system::error_code & ec,tcp::resolver::iterator endpoints)1048 void handle_resolve(const boost::system::error_code& ec, tcp::resolver::iterator endpoints)
1049 {
1050 if (ec)
1051 {
1052 report_error("Error resolving address", ec, httpclient_errorcode_context::connect);
1053 }
1054 else if (endpoints == tcp::resolver::iterator())
1055 {
1056 report_error("Failed to resolve address", ec, httpclient_errorcode_context::connect);
1057 }
1058 else
1059 {
1060 m_timer.reset();
1061 auto endpoint = *endpoints;
1062 m_connection->async_connect(
1063 endpoint,
1064 boost::bind(
1065 &asio_context::handle_connect, shared_from_this(), boost::asio::placeholders::error, ++endpoints));
1066 }
1067 }
1068
write_request()1069 void write_request()
1070 {
1071 // Only perform handshake if a TLS connection and not being reused.
1072 if (m_connection->is_ssl() && !m_connection->is_reused())
1073 {
1074 const auto weakCtx = std::weak_ptr<asio_context>(shared_from_this());
1075 m_connection->async_handshake(
1076 boost::asio::ssl::stream_base::client,
1077 m_http_client->client_config(),
1078 boost::bind(&asio_context::handle_handshake, shared_from_this(), boost::asio::placeholders::error),
1079
1080 // Use a weak_ptr since the verify_callback is stored until the connection is
1081 // destroyed. This avoids creating a circular reference since we pool connection
1082 // objects.
1083 [weakCtx](bool preverified, boost::asio::ssl::verify_context& verify_context) {
1084 auto this_request = weakCtx.lock();
1085 if (this_request)
1086 {
1087 return this_request->handle_cert_verification(preverified, verify_context);
1088 }
1089 return false;
1090 });
1091 }
1092 else
1093 {
1094 m_connection->async_write(
1095 m_body_buf,
1096 boost::bind(&asio_context::handle_write_headers, shared_from_this(), boost::asio::placeholders::error));
1097 }
1098 }
1099
handle_handshake(const boost::system::error_code & ec)1100 void handle_handshake(const boost::system::error_code& ec)
1101 {
1102 if (!ec)
1103 {
1104 m_connection->async_write(
1105 m_body_buf,
1106 boost::bind(&asio_context::handle_write_headers, shared_from_this(), boost::asio::placeholders::error));
1107 }
1108 else
1109 {
1110 report_error("Error in SSL handshake", ec, httpclient_errorcode_context::handshake);
1111 }
1112 }
1113
handle_cert_verification(bool preverified,boost::asio::ssl::verify_context & verifyCtx)1114 bool handle_cert_verification(bool preverified, boost::asio::ssl::verify_context& verifyCtx)
1115 {
1116 // OpenSSL calls the verification callback once per certificate in the chain,
1117 // starting with the root CA certificate. The 'leaf', non-Certificate Authority (CA)
1118 // certificate, i.e. actual server certificate is at the '0' position in the
1119 // certificate chain, the rest are optional intermediate certificates, followed
1120 // finally by the root CA self signed certificate.
1121
1122 #ifdef CPPREST_PLATFORM_ASIO_CERT_VERIFICATION_AVAILABLE
1123 // If OpenSSL fails we will doing verification at the end using the whole certificate
1124 // chain so wait until the 'leaf' cert. For now return true so OpenSSL continues down
1125 // the certificate chain.
1126 if (!preverified)
1127 {
1128 m_openssl_failed = true;
1129 }
1130
1131 if (m_openssl_failed)
1132 {
1133 return verify_cert_chain_platform_specific(verifyCtx, m_connection->cn_hostname());
1134 }
1135 #endif // CPPREST_PLATFORM_ASIO_CERT_VERIFICATION_AVAILABLE
1136
1137 boost::asio::ssl::rfc2818_verification rfc2818(m_connection->cn_hostname());
1138 return rfc2818(preverified, verifyCtx);
1139 }
1140
handle_write_headers(const boost::system::error_code & ec)1141 void handle_write_headers(const boost::system::error_code& ec)
1142 {
1143 if (ec)
1144 {
1145 report_error("Failed to write request headers", ec, httpclient_errorcode_context::writeheader);
1146 }
1147 else
1148 {
1149 if (m_needChunked)
1150 {
1151 handle_write_chunked_body(ec);
1152 }
1153 else
1154 {
1155 handle_write_large_body(ec);
1156 }
1157 }
1158 }
1159
handle_write_chunked_body(const boost::system::error_code & ec)1160 void handle_write_chunked_body(const boost::system::error_code& ec)
1161 {
1162 if (ec)
1163 {
1164 // Reuse error handling.
1165 return handle_write_body(ec);
1166 }
1167
1168 m_timer.reset();
1169 const auto& progress = m_request._get_impl()->_progress_handler();
1170 if (progress)
1171 {
1172 try
1173 {
1174 (*progress)(message_direction::upload, m_uploaded);
1175 }
1176 catch (...)
1177 {
1178 report_exception(std::current_exception());
1179 return;
1180 }
1181 }
1182
1183 const auto& chunkSize = m_http_client->client_config().chunksize();
1184 auto readbuf = _get_readbuffer();
1185 uint8_t* buf = boost::asio::buffer_cast<uint8_t*>(
1186 m_body_buf.prepare(chunkSize + http::details::chunked_encoding::additional_encoding_space));
1187 const auto this_request = shared_from_this();
1188 readbuf.getn(buf + http::details::chunked_encoding::data_offset, chunkSize)
1189 .then([this_request, buf, chunkSize AND_CAPTURE_MEMBER_FUNCTION_POINTERS](pplx::task<size_t> op) {
1190 size_t readSize = 0;
1191 try
1192 {
1193 readSize = op.get();
1194 }
1195 catch (...)
1196 {
1197 this_request->report_exception(std::current_exception());
1198 return;
1199 }
1200
1201 const size_t offset = http::details::chunked_encoding::add_chunked_delimiters(
1202 buf, chunkSize + http::details::chunked_encoding::additional_encoding_space, readSize);
1203 this_request->m_body_buf.commit(readSize + http::details::chunked_encoding::additional_encoding_space);
1204 this_request->m_body_buf.consume(offset);
1205 this_request->m_uploaded += static_cast<uint64_t>(readSize);
1206
1207 if (readSize != 0)
1208 {
1209 this_request->m_connection->async_write(this_request->m_body_buf,
1210 boost::bind(&asio_context::handle_write_chunked_body,
1211 this_request,
1212 boost::asio::placeholders::error));
1213 }
1214 else
1215 {
1216 this_request->m_connection->async_write(
1217 this_request->m_body_buf,
1218 boost::bind(&asio_context::handle_write_body, this_request, boost::asio::placeholders::error));
1219 }
1220 });
1221 }
1222
handle_write_large_body(const boost::system::error_code & ec)1223 void handle_write_large_body(const boost::system::error_code& ec)
1224 {
1225 if (ec || m_uploaded >= m_content_length)
1226 {
1227 // Reuse error handling.
1228 return handle_write_body(ec);
1229 }
1230
1231 m_timer.reset();
1232 const auto& progress = m_request._get_impl()->_progress_handler();
1233 if (progress)
1234 {
1235 try
1236 {
1237 (*progress)(message_direction::upload, m_uploaded);
1238 }
1239 catch (...)
1240 {
1241 report_exception(std::current_exception());
1242 return;
1243 }
1244 }
1245
1246 const auto this_request = shared_from_this();
1247 const auto readSize = static_cast<size_t>((std::min)(
1248 static_cast<uint64_t>(m_http_client->client_config().chunksize()), m_content_length - m_uploaded));
1249 auto readbuf = _get_readbuffer();
1250 readbuf.getn(boost::asio::buffer_cast<uint8_t*>(m_body_buf.prepare(readSize)), readSize)
1251 .then([this_request AND_CAPTURE_MEMBER_FUNCTION_POINTERS](pplx::task<size_t> op) {
1252 try
1253 {
1254 const auto actualReadSize = op.get();
1255 if (actualReadSize == 0)
1256 {
1257 this_request->report_exception(http_exception(
1258 "Unexpected end of request body stream encountered before Content-Length satisfied."));
1259 return;
1260 }
1261 this_request->m_uploaded += static_cast<uint64_t>(actualReadSize);
1262 this_request->m_body_buf.commit(actualReadSize);
1263 this_request->m_connection->async_write(this_request->m_body_buf,
1264 boost::bind(&asio_context::handle_write_large_body,
1265 this_request,
1266 boost::asio::placeholders::error));
1267 }
1268 catch (...)
1269 {
1270 this_request->report_exception(std::current_exception());
1271 return;
1272 }
1273 });
1274 }
1275
handle_write_body(const boost::system::error_code & ec)1276 void handle_write_body(const boost::system::error_code& ec)
1277 {
1278 if (!ec)
1279 {
1280 m_timer.reset();
1281 const auto& progress = m_request._get_impl()->_progress_handler();
1282 if (progress)
1283 {
1284 try
1285 {
1286 (*progress)(message_direction::upload, m_uploaded);
1287 }
1288 catch (...)
1289 {
1290 report_exception(std::current_exception());
1291 return;
1292 }
1293 }
1294
1295 // Read until the end of entire headers
1296 m_connection->async_read_until(
1297 m_body_buf,
1298 CRLF + CRLF,
1299 boost::bind(&asio_context::handle_status_line, shared_from_this(), boost::asio::placeholders::error));
1300 }
1301 else
1302 {
1303 report_error("Failed to write request body", ec, httpclient_errorcode_context::writebody);
1304 }
1305 }
1306
handle_status_line(const boost::system::error_code & ec)1307 void handle_status_line(const boost::system::error_code& ec)
1308 {
1309 if (!ec)
1310 {
1311 m_timer.reset();
1312
1313 std::istream response_stream(&m_body_buf);
1314 response_stream.imbue(std::locale::classic());
1315 std::string http_version;
1316 response_stream >> http_version;
1317 status_code status_code;
1318 response_stream >> status_code;
1319
1320 std::string status_message;
1321 std::getline(response_stream, status_message);
1322
1323 m_response.set_status_code(status_code);
1324
1325 ::web::http::details::trim_whitespace(status_message);
1326 m_response.set_reason_phrase(utility::conversions::to_string_t(std::move(status_message)));
1327
1328 if (!response_stream || http_version.substr(0, 5) != "HTTP/")
1329 {
1330 report_error("Invalid HTTP status line", ec, httpclient_errorcode_context::readheader);
1331 return;
1332 }
1333
1334 web::http::http_version parsed_version = web::http::http_version::from_string(http_version);
1335 m_response._get_impl()->_set_http_version(parsed_version);
1336
1337 // if HTTP version is 1.0 then disable 'Keep-Alive' by default
1338 if (parsed_version == web::http::http_versions::HTTP_1_0)
1339 {
1340 m_connection->set_keep_alive(false);
1341 }
1342
1343 read_headers();
1344 }
1345 else
1346 {
1347 handle_failed_read_status_line(ec, "Failed to read HTTP status line");
1348 }
1349 }
1350
handle_failed_read_status_line(const boost::system::error_code & ec,const char * generic_error_message)1351 void handle_failed_read_status_line(const boost::system::error_code& ec, const char* generic_error_message)
1352 {
1353 if (m_connection->was_reused_and_closed_by_server(ec))
1354 {
1355 // Failed to write to socket because connection was already closed while it was in the pool.
1356 // close() here ensures socket is closed in a robust way and prevents the connection from being put to the
1357 // pool again.
1358 m_connection->close();
1359
1360 // Create a new context and copy the request object, completion event and
1361 // cancellation registration to maintain the old state.
1362 // This also obtains a new connection from pool.
1363 std::shared_ptr<request_context> new_ctx;
1364 try
1365 {
1366 new_ctx = create_request_context(m_http_client, m_request);
1367 }
1368 catch (...)
1369 {
1370 report_exception(std::current_exception());
1371 return;
1372 }
1373
1374 // If the request contains a valid instream, we try to rewind it to
1375 // replay the just-failed request. Otherwise we assume that no data
1376 // was sent in the first place.
1377 const auto& instream = new_ctx->m_request._get_impl()->instream();
1378 if (instream)
1379 {
1380 // As stated in the commit message of f4f2348, we might encounter
1381 // streams that are not capable of rewinding and hence resending the
1382 // request is not possible. We cannot recover from this condition and
1383 // need to escalate it to the using code.
1384 if (!instream.can_seek())
1385 {
1386 report_error("cannot rewind input stream for connection re-establishment",
1387 ec,
1388 httpclient_errorcode_context::readheader);
1389 return;
1390 }
1391
1392 try
1393 {
1394 // Rewinding the stream might throw, in which case we cannot do the
1395 // connection re-establishment transparently. I.e. report the exception
1396 // to the calling code.
1397 instream.seek(0);
1398 }
1399 catch (...)
1400 {
1401 report_exception(std::current_exception());
1402 return;
1403 }
1404 }
1405
1406 new_ctx->m_request_completion = m_request_completion;
1407 new_ctx->m_cancellationRegistration = m_cancellationRegistration;
1408
1409 auto client = std::static_pointer_cast<asio_client>(m_http_client);
1410 // Resend the request using the new context.
1411 client->send_request(new_ctx);
1412 }
1413 else
1414 {
1415 report_error(generic_error_message, ec, httpclient_errorcode_context::readheader);
1416 }
1417 }
1418
read_headers()1419 void read_headers()
1420 {
1421 auto needChunked = false;
1422 std::istream response_stream(&m_body_buf);
1423 response_stream.imbue(std::locale::classic());
1424 std::string header;
1425 while (std::getline(response_stream, header) && header != "\r")
1426 {
1427 const auto colon = header.find(':');
1428 if (colon != std::string::npos)
1429 {
1430 auto name = header.substr(0, colon);
1431 auto value = header.substr(colon + 1, header.size() - colon - 2);
1432 boost::algorithm::trim(name);
1433 boost::algorithm::trim(value);
1434
1435 if (boost::iequals(name, header_names::transfer_encoding))
1436 {
1437 needChunked = boost::icontains(value, U("chunked"));
1438 }
1439
1440 if (boost::iequals(name, header_names::connection))
1441 {
1442 // If the server uses HTTP/1.1, then 'Keep-Alive' is the default,
1443 // so connection is explicitly closed only if we get "Connection: close".
1444 // If the server uses HTTP/1.0, it would need to respond using
1445 // 'Connection: Keep-Alive' every time.
1446 if (m_response._get_impl()->http_version() != web::http::http_versions::HTTP_1_0)
1447 m_connection->set_keep_alive(!boost::iequals(value, U("close")));
1448 else
1449 m_connection->set_keep_alive(boost::iequals(value, U("Keep-Alive")));
1450 }
1451
1452 m_response.headers().add(utility::conversions::to_string_t(std::move(name)),
1453 utility::conversions::to_string_t(std::move(value)));
1454 }
1455 }
1456
1457 m_content_length = (std::numeric_limits<size_t>::max)(); // Without Content-Length header, size should be same
1458 // as TCP stream - set it size_t max.
1459 m_response.headers().match(header_names::content_length, m_content_length);
1460
1461 if (!this->handle_compression())
1462 {
1463 // false indicates report_exception was called
1464 return;
1465 }
1466
1467 complete_headers();
1468
1469 // Check for HEAD requests and status codes which cannot contain a
1470 // message body in HTTP/1.1 (see 3.3.3/1 of the RFC 7230).
1471 //
1472 // note: need to check for 'chunked' here as well, azure storage sends both
1473 // transfer-encoding:chunked and content-length:0 (although HTTP says not to)
1474 const auto status = m_response.status_code();
1475 if (m_request.method() == U("HEAD") || (status >= 100 && status < 200) || status == status_codes::NoContent ||
1476 status == status_codes::NotModified || (!needChunked && m_content_length == 0))
1477 {
1478 // we can stop early - no body
1479 const auto& progress = m_request._get_impl()->_progress_handler();
1480 if (progress)
1481 {
1482 try
1483 {
1484 (*progress)(message_direction::download, 0);
1485 }
1486 catch (...)
1487 {
1488 report_exception(std::current_exception());
1489 return;
1490 }
1491 }
1492
1493 complete_request(0);
1494 }
1495 else
1496 {
1497 if (!needChunked)
1498 {
1499 async_read_until_buffersize(
1500 static_cast<size_t>((std::min)(m_content_length,
1501 static_cast<uint64_t>(m_http_client->client_config().chunksize()))),
1502 boost::bind(
1503 &asio_context::handle_read_content, shared_from_this(), boost::asio::placeholders::error));
1504 }
1505 else
1506 {
1507 m_connection->async_read_until(m_body_buf,
1508 CRLF,
1509 boost::bind(&asio_context::handle_chunk_header,
1510 shared_from_this(),
1511 boost::asio::placeholders::error));
1512 }
1513 }
1514 }
1515
1516 template<typename ReadHandler>
async_read_until_buffersize(size_t size,const ReadHandler & handler)1517 void async_read_until_buffersize(size_t size, const ReadHandler& handler)
1518 {
1519 size_t size_to_read = 0;
1520 if (m_body_buf.size() < size)
1521 {
1522 size_to_read = size - m_body_buf.size();
1523 }
1524
1525 m_connection->async_read(m_body_buf, boost::asio::transfer_exactly(size_to_read), handler);
1526 }
1527
handle_chunk_header(const boost::system::error_code & ec)1528 void handle_chunk_header(const boost::system::error_code& ec)
1529 {
1530 if (!ec)
1531 {
1532 m_timer.reset();
1533
1534 std::istream response_stream(&m_body_buf);
1535 response_stream.imbue(std::locale::classic());
1536 std::string line;
1537 std::getline(response_stream, line);
1538
1539 std::istringstream octetLine(std::move(line));
1540 octetLine.imbue(std::locale::classic());
1541 int octets = 0;
1542 octetLine >> std::hex >> octets;
1543
1544 if (octetLine.fail())
1545 {
1546 report_error("Invalid chunked response header",
1547 boost::system::error_code(),
1548 httpclient_errorcode_context::readbody);
1549 }
1550 else
1551 {
1552 async_read_until_buffersize(
1553 octets + CRLF.size(),
1554 boost::bind(
1555 &asio_context::handle_chunk, shared_from_this(), boost::asio::placeholders::error, octets));
1556 }
1557 }
1558 else
1559 {
1560 report_error("Retrieving message chunk header", ec, httpclient_errorcode_context::readbody);
1561 }
1562 }
1563
decompress(const uint8_t * input,size_t input_size,std::vector<uint8_t> & output)1564 bool decompress(const uint8_t* input, size_t input_size, std::vector<uint8_t>& output)
1565 {
1566 // Need to guard against attempting to decompress when we're already finished or encountered an error!
1567 if (input == nullptr || input_size == 0)
1568 {
1569 return false;
1570 }
1571
1572 size_t processed;
1573 size_t got;
1574 size_t inbytes = 0;
1575 size_t outbytes = 0;
1576 bool done;
1577
1578 try
1579 {
1580 output.resize(input_size * 3);
1581 do
1582 {
1583 if (inbytes)
1584 {
1585 output.resize(output.size() + (std::max)(input_size, static_cast<size_t>(1024)));
1586 }
1587 got = m_decompressor->decompress(input + inbytes,
1588 input_size - inbytes,
1589 output.data() + outbytes,
1590 output.size() - outbytes,
1591 web::http::compression::operation_hint::has_more,
1592 processed,
1593 done);
1594 inbytes += processed;
1595 outbytes += got;
1596 } while (got && !done);
1597 output.resize(outbytes);
1598 }
1599 catch (...)
1600 {
1601 return false;
1602 }
1603
1604 return true;
1605 }
1606
handle_chunk(const boost::system::error_code & ec,int to_read)1607 void handle_chunk(const boost::system::error_code& ec, int to_read)
1608 {
1609 if (!ec)
1610 {
1611 m_timer.reset();
1612
1613 m_downloaded += static_cast<uint64_t>(to_read);
1614 const auto& progress = m_request._get_impl()->_progress_handler();
1615 if (progress)
1616 {
1617 try
1618 {
1619 (*progress)(message_direction::download, m_downloaded);
1620 }
1621 catch (...)
1622 {
1623 report_exception(std::current_exception());
1624 return;
1625 }
1626 }
1627
1628 if (to_read == 0)
1629 {
1630 m_body_buf.consume(CRLF.size());
1631 complete_request(m_downloaded);
1632 }
1633 else
1634 {
1635 auto writeBuffer = _get_writebuffer();
1636 const auto this_request = shared_from_this();
1637 if (m_decompressor)
1638 {
1639 std::vector<uint8_t> decompressed;
1640
1641 bool boo =
1642 decompress(boost::asio::buffer_cast<const uint8_t*>(m_body_buf.data()), to_read, decompressed);
1643 if (!boo)
1644 {
1645 report_exception(std::runtime_error("Failed to decompress the response body"));
1646 return;
1647 }
1648
1649 // It is valid for the decompressor to sometimes return an empty output for a given chunk, the data
1650 // will be flushed when the next chunk is received
1651 if (decompressed.empty())
1652 {
1653 m_body_buf.consume(to_read + CRLF.size()); // consume crlf
1654 m_connection->async_read_until(m_body_buf,
1655 CRLF,
1656 boost::bind(&asio_context::handle_chunk_header,
1657 this_request,
1658 boost::asio::placeholders::error));
1659 }
1660 else
1661 {
1662 // Move the decompressed buffer into a shared_ptr to keep it alive until putn_nocopy completes.
1663 // When VS 2013 support is dropped, this should be changed to a unique_ptr plus a move capture.
1664 auto shared_decompressed = std::make_shared<std::vector<uint8_t>>(std::move(decompressed));
1665
1666 writeBuffer.putn_nocopy(shared_decompressed->data(), shared_decompressed->size())
1667 .then([this_request, to_read, shared_decompressed AND_CAPTURE_MEMBER_FUNCTION_POINTERS](
1668 pplx::task<size_t> op) {
1669 try
1670 {
1671 op.get();
1672 this_request->m_body_buf.consume(to_read + CRLF.size()); // consume crlf
1673 this_request->m_connection->async_read_until(
1674 this_request->m_body_buf,
1675 CRLF,
1676 boost::bind(&asio_context::handle_chunk_header,
1677 this_request,
1678 boost::asio::placeholders::error));
1679 }
1680 catch (...)
1681 {
1682 this_request->report_exception(std::current_exception());
1683 return;
1684 }
1685 });
1686 }
1687 }
1688 else
1689 {
1690 writeBuffer.putn_nocopy(boost::asio::buffer_cast<const uint8_t*>(m_body_buf.data()), to_read)
1691 .then([this_request, to_read AND_CAPTURE_MEMBER_FUNCTION_POINTERS](pplx::task<size_t> op) {
1692 try
1693 {
1694 op.wait();
1695 }
1696 catch (...)
1697 {
1698 this_request->report_exception(std::current_exception());
1699 return;
1700 }
1701 this_request->m_body_buf.consume(to_read + CRLF.size()); // consume crlf
1702 this_request->m_connection->async_read_until(this_request->m_body_buf,
1703 CRLF,
1704 boost::bind(&asio_context::handle_chunk_header,
1705 this_request,
1706 boost::asio::placeholders::error));
1707 });
1708 }
1709 }
1710 }
1711 else
1712 {
1713 report_error("Failed to read chunked response part", ec, httpclient_errorcode_context::readbody);
1714 }
1715 }
1716
handle_read_content(const boost::system::error_code & ec)1717 void handle_read_content(const boost::system::error_code& ec)
1718 {
1719 auto writeBuffer = _get_writebuffer();
1720
1721 if (ec)
1722 {
1723 if (ec == boost::asio::error::eof && m_content_length == (std::numeric_limits<size_t>::max)())
1724 {
1725 m_content_length = m_downloaded + m_body_buf.size();
1726 }
1727 else
1728 {
1729 report_error("Failed to read response body", ec, httpclient_errorcode_context::readbody);
1730 return;
1731 }
1732 }
1733
1734 m_timer.reset();
1735 const auto& progress = m_request._get_impl()->_progress_handler();
1736 if (progress)
1737 {
1738 try
1739 {
1740 (*progress)(message_direction::download, m_downloaded);
1741 }
1742 catch (...)
1743 {
1744 report_exception(std::current_exception());
1745 return;
1746 }
1747 }
1748
1749 if (m_downloaded < m_content_length)
1750 {
1751 // more data need to be read
1752 const auto this_request = shared_from_this();
1753
1754 auto read_size = static_cast<size_t>(
1755 (std::min)(static_cast<uint64_t>(m_body_buf.size()), m_content_length - m_downloaded));
1756
1757 if (m_decompressor)
1758 {
1759 std::vector<uint8_t> decompressed;
1760
1761 bool boo =
1762 decompress(boost::asio::buffer_cast<const uint8_t*>(m_body_buf.data()), read_size, decompressed);
1763 if (!boo)
1764 {
1765 this_request->report_exception(std::runtime_error("Failed to decompress the response body"));
1766 return;
1767 }
1768
1769 // It is valid for the decompressor to sometimes return an empty output for a given chunk, the data will
1770 // be flushed when the next chunk is received
1771 if (decompressed.empty())
1772 {
1773 try
1774 {
1775 this_request->m_downloaded += static_cast<uint64_t>(read_size);
1776
1777 this_request->async_read_until_buffersize(
1778 static_cast<size_t>((std::min)(
1779 static_cast<uint64_t>(this_request->m_http_client->client_config().chunksize()),
1780 this_request->m_content_length - this_request->m_downloaded)),
1781 boost::bind(
1782 &asio_context::handle_read_content, this_request, boost::asio::placeholders::error));
1783 }
1784 catch (...)
1785 {
1786 this_request->report_exception(std::current_exception());
1787 return;
1788 }
1789 }
1790 else
1791 {
1792 // Move the decompressed buffer into a shared_ptr to keep it alive until putn_nocopy completes.
1793 // When VS 2013 support is dropped, this should be changed to a unique_ptr plus a move capture.
1794 auto shared_decompressed = std::make_shared<std::vector<uint8_t>>(std::move(decompressed));
1795
1796 writeBuffer.putn_nocopy(shared_decompressed->data(), shared_decompressed->size())
1797 .then([this_request, read_size, shared_decompressed AND_CAPTURE_MEMBER_FUNCTION_POINTERS](
1798 pplx::task<size_t> op) {
1799 size_t writtenSize = 0;
1800 (void)writtenSize;
1801 try
1802 {
1803 writtenSize = op.get();
1804 this_request->m_downloaded += static_cast<uint64_t>(read_size);
1805 this_request->m_body_buf.consume(read_size);
1806 this_request->async_read_until_buffersize(
1807 static_cast<size_t>((std::min)(
1808 static_cast<uint64_t>(this_request->m_http_client->client_config().chunksize()),
1809 this_request->m_content_length - this_request->m_downloaded)),
1810 boost::bind(&asio_context::handle_read_content,
1811 this_request,
1812 boost::asio::placeholders::error));
1813 }
1814 catch (...)
1815 {
1816 this_request->report_exception(std::current_exception());
1817 return;
1818 }
1819 });
1820 }
1821 }
1822 else
1823 {
1824 writeBuffer.putn_nocopy(boost::asio::buffer_cast<const uint8_t*>(m_body_buf.data()), read_size)
1825 .then([this_request AND_CAPTURE_MEMBER_FUNCTION_POINTERS](pplx::task<size_t> op) {
1826 size_t writtenSize = 0;
1827 try
1828 {
1829 writtenSize = op.get();
1830 this_request->m_downloaded += static_cast<uint64_t>(writtenSize);
1831 this_request->m_body_buf.consume(writtenSize);
1832 this_request->async_read_until_buffersize(
1833 static_cast<size_t>((std::min)(
1834 static_cast<uint64_t>(this_request->m_http_client->client_config().chunksize()),
1835 this_request->m_content_length - this_request->m_downloaded)),
1836 boost::bind(&asio_context::handle_read_content,
1837 this_request,
1838 boost::asio::placeholders::error));
1839 }
1840 catch (...)
1841 {
1842 this_request->report_exception(std::current_exception());
1843 return;
1844 }
1845 });
1846 }
1847 }
1848 else
1849 {
1850 // Request is complete no more data to read.
1851 complete_request(m_downloaded);
1852 }
1853 }
1854
1855 // Simple timer class wrapping Boost deadline timer.
1856 // Closes the connection when timer fires.
1857 class timeout_timer
1858 {
1859 public:
timeout_timer(const std::chrono::microseconds & timeout)1860 timeout_timer(const std::chrono::microseconds& timeout)
1861 : m_duration(timeout.count()), m_state(created), m_timer(crossplat::threadpool::shared_instance().service())
1862 {
1863 }
1864
set_ctx(const std::weak_ptr<asio_context> & ctx)1865 void set_ctx(const std::weak_ptr<asio_context>& ctx) { m_ctx = ctx; }
1866
start()1867 void start()
1868 {
1869 assert(m_state == created);
1870 assert(!m_ctx.expired());
1871 m_state = started;
1872
1873 m_timer.expires_from_now(m_duration);
1874 auto ctx = m_ctx;
1875 m_timer.async_wait([ctx AND_CAPTURE_MEMBER_FUNCTION_POINTERS](const boost::system::error_code& ec) {
1876 handle_timeout(ec, ctx);
1877 });
1878 }
1879
reset()1880 void reset()
1881 {
1882 assert(m_state == started || m_state == timedout);
1883 assert(!m_ctx.expired());
1884 if (m_timer.expires_from_now(m_duration) > 0)
1885 {
1886 // The existing handler was canceled so schedule a new one.
1887 assert(m_state == started);
1888 auto ctx = m_ctx;
1889 m_timer.async_wait([ctx AND_CAPTURE_MEMBER_FUNCTION_POINTERS](const boost::system::error_code& ec) {
1890 handle_timeout(ec, ctx);
1891 });
1892 }
1893 }
1894
has_timedout() const1895 bool has_timedout() const { return m_state == timedout; }
1896
has_started() const1897 bool has_started() const { return m_state == started; }
1898
stop()1899 void stop()
1900 {
1901 m_state = stopped;
1902 m_timer.cancel();
1903 }
1904
handle_timeout(const boost::system::error_code & ec,const std::weak_ptr<asio_context> & ctx)1905 static void handle_timeout(const boost::system::error_code& ec, const std::weak_ptr<asio_context>& ctx)
1906 {
1907 if (!ec)
1908 {
1909 auto shared_ctx = ctx.lock();
1910 if (shared_ctx)
1911 {
1912 assert(shared_ctx->m_timer.m_state != timedout);
1913 shared_ctx->m_timer.m_state = timedout;
1914 shared_ctx->m_connection->close();
1915 }
1916 }
1917 }
1918
1919 private:
1920 enum timer_state
1921 {
1922 created,
1923 started,
1924 stopped,
1925 timedout
1926 };
1927
1928 #if (defined(ANDROID) || defined(__ANDROID__)) && !defined(_LIBCPP_VERSION)
1929 boost::chrono::microseconds m_duration;
1930 #else
1931 std::chrono::microseconds m_duration;
1932 #endif
1933 std::atomic<timer_state> m_state;
1934 std::weak_ptr<asio_context> m_ctx;
1935 boost::asio::steady_timer m_timer;
1936 };
1937
1938 uint64_t m_content_length;
1939 bool m_needChunked;
1940 timeout_timer m_timer;
1941 tcp::resolver m_resolver;
1942 boost::asio::streambuf m_body_buf;
1943 std::shared_ptr<asio_connection> m_connection;
1944
1945 #ifdef CPPREST_PLATFORM_ASIO_CERT_VERIFICATION_AVAILABLE
1946 bool m_openssl_failed;
1947 #endif // CPPREST_PLATFORM_ASIO_CERT_VERIFICATION_AVAILABLE
1948 };
1949
create_platform_final_pipeline_stage(uri && base_uri,http_client_config && client_config)1950 std::shared_ptr<_http_client_communicator> create_platform_final_pipeline_stage(uri&& base_uri,
1951 http_client_config&& client_config)
1952 {
1953 return std::make_shared<asio_client>(std::move(base_uri), std::move(client_config));
1954 }
1955
send_request(const std::shared_ptr<request_context> & request_ctx)1956 void asio_client::send_request(const std::shared_ptr<request_context>& request_ctx)
1957 {
1958 auto ctx = std::static_pointer_cast<asio_context>(request_ctx);
1959
1960 try
1961 {
1962 if (ctx->m_connection->is_ssl())
1963 {
1964 client_config().invoke_nativehandle_options(ctx->m_connection->m_ssl_stream.get());
1965 }
1966 else
1967 {
1968 client_config().invoke_nativehandle_options(&(ctx->m_connection->m_socket));
1969 }
1970 }
1971 catch (...)
1972 {
1973 request_ctx->report_exception(std::current_exception());
1974 return;
1975 }
1976
1977 ctx->start_request();
1978 }
1979
is_retrieval_redirection(status_code code)1980 static bool is_retrieval_redirection(status_code code)
1981 {
1982 // See https://tools.ietf.org/html/rfc7231#section-6.4
1983
1984 switch (code)
1985 {
1986 case status_codes::MovedPermanently:
1987 // "For historical reasons, a user agent MAY change the request method
1988 // from POST to GET for the subsequent request."
1989 return true;
1990 case status_codes::Found:
1991 // "For historical reasons, a user agent MAY change the request method
1992 // from POST to GET for the subsequent request."
1993 return true;
1994 case status_codes::SeeOther:
1995 // "A user agent can perform a [GET or HEAD] request. It is primarily
1996 // used to allow the output of a POST action to redirect the user agent
1997 // to a selected resource."
1998 return true;
1999 default:
2000 return false;
2001 }
2002 }
2003
is_unchanged_redirection(status_code code)2004 static bool is_unchanged_redirection(status_code code)
2005 {
2006 // See https://tools.ietf.org/html/rfc7231#section-6.4
2007 // and https://tools.ietf.org/html/rfc7538#section-3
2008
2009 switch (code)
2010 {
2011 case status_codes::TemporaryRedirect:
2012 // "The user agent MUST NOT change the request method if it performs an
2013 // automatic redirection to that URI."
2014 return true;
2015 case status_codes::PermanentRedirect:
2016 // This status code "does not allow changing the request method from POST
2017 // to GET."
2018 return true;
2019 default:
2020 return false;
2021 }
2022 }
2023
is_recognized_redirection(status_code code)2024 static bool is_recognized_redirection(status_code code)
2025 {
2026 // other 3xx status codes, e.g. 300 Multiple Choices, are not handled
2027 // and should be handled externally
2028 return is_retrieval_redirection(code) || is_unchanged_redirection(code);
2029 }
2030
is_retrieval_request(method method)2031 static bool is_retrieval_request(method method)
2032 {
2033 return methods::GET == method || methods::HEAD == method;
2034 }
2035
2036 static const std::vector<utility::string_t> request_body_header_names =
2037 {
2038 header_names::content_encoding,
2039 header_names::content_language,
2040 header_names::content_length,
2041 header_names::content_location,
2042 header_names::content_type
2043 };
2044
2045 // A request continuation that follows redirects according to the specified configuration.
2046 // This implementation only supports retrieval redirects, as it cannot redirect e.g. a POST request
2047 // using the same method since the request body may have been consumed.
2048 struct http_redirect_follower
2049 {
2050 http_client_config config;
2051 std::vector<uri> followed_urls;
2052 http_request redirect;
2053
2054 http_redirect_follower(http_client_config config, const http_request& request);
2055
2056 uri url_to_follow(const http_response& response) const;
2057
2058 pplx::task<http_response> operator()(http_response response);
2059 };
2060
http_redirect_follower(http_client_config config,const http_request & request)2061 http_redirect_follower::http_redirect_follower(http_client_config config, const http_request& request)
2062 : config(std::move(config))
2063 , followed_urls(1, request.absolute_uri())
2064 , redirect(request.method())
2065 {
2066 // Stash the original request URL, etc. to be prepared for an automatic redirect
2067
2068 // Basically, it makes sense to send the redirects with the same headers as the original request
2069 redirect.headers() = request.headers();
2070 // However, this implementation only supports retrieval redirects, with no body, so Content-* headers
2071 // should be removed
2072 for (const auto& content_header : request_body_header_names)
2073 {
2074 redirect.headers().remove(content_header);
2075 }
2076
2077 redirect._set_cancellation_token(request._cancellation_token());
2078 }
2079
url_to_follow(const http_response & response) const2080 uri http_redirect_follower::url_to_follow(const http_response& response) const
2081 {
2082 // Return immediately if the response is not a supported redirection
2083 if (!is_recognized_redirection(response.status_code()))
2084 return{};
2085
2086 // Although not required by RFC 7231, config may limit the number of automatic redirects
2087 // (followed_urls includes the initial request URL, hence '<' here)
2088 if (config.max_redirects() < followed_urls.size())
2089 return{};
2090
2091 // Can't very well automatically redirect if the server hasn't provided a Location
2092 const auto location = response.headers().find(header_names::location);
2093 if (response.headers().end() == location)
2094 return{};
2095
2096 uri to_follow(followed_urls.back().resolve_uri(location->second));
2097
2098 // Config may prohibit automatic redirects from HTTPS to HTTP
2099 if (!config.https_to_http_redirects() && followed_urls.back().scheme() == _XPLATSTR("https")
2100 && to_follow.scheme() != _XPLATSTR("https"))
2101 return{};
2102
2103 // "A client SHOULD detect and intervene in cyclical redirections."
2104 if (followed_urls.end() != std::find(followed_urls.begin(), followed_urls.end(), to_follow))
2105 return{};
2106
2107 return to_follow;
2108 }
2109
operator ()(http_response response)2110 pplx::task<http_response> http_redirect_follower::operator()(http_response response)
2111 {
2112 // Return immediately if the response doesn't indicate a valid automatic redirect
2113 uri to_follow = url_to_follow(response);
2114 if (to_follow.is_empty())
2115 return pplx::task_from_result(response);
2116
2117 // This implementation only supports retrieval redirects, as it cannot redirect e.g. a POST request
2118 // using the same method since the request body may have been consumed.
2119 if (!is_retrieval_request(redirect.method()) && !is_retrieval_redirection(response.status_code()))
2120 return pplx::task_from_result(response);
2121
2122 if (!is_retrieval_request(redirect.method()))
2123 redirect.set_method(methods::GET);
2124
2125 // If the reply to this request is also a redirect, we want visibility of that
2126 auto config_no_redirects = config;
2127 config_no_redirects.set_max_redirects(0);
2128 http_client client(to_follow, config_no_redirects);
2129
2130 // Stash the redirect request URL and make the request with the same continuation
2131 auto request_task = client.request(redirect, redirect._cancellation_token());
2132 followed_urls.push_back(std::move(to_follow));
2133 return request_task.then(std::move(*this));
2134 }
2135
propagate(http_request request)2136 pplx::task<http_response> asio_client::propagate(http_request request)
2137 {
2138 auto self = std::static_pointer_cast<_http_client_communicator>(shared_from_this());
2139 std::shared_ptr<request_context> context;
2140 try
2141 {
2142 context = details::asio_context::create_request_context(self, request);
2143 }
2144 catch (...)
2145 {
2146 return pplx::task_from_exception<http_response>(std::current_exception());
2147 }
2148
2149 // Use a task to externally signal the final result and completion of the task.
2150 auto result_task = pplx::create_task(context->m_request_completion);
2151
2152 // Asynchronously send the response with the HTTP client implementation.
2153 this->async_send_request(context);
2154
2155 return client_config().max_redirects() > 0
2156 ? result_task.then(http_redirect_follower(client_config(), request))
2157 : result_task;
2158 }
2159 } // namespace details
2160 } // namespace client
2161 } // namespace http
2162 } // namespace web
2163