| // Copyright 2016 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifndef GARNET_BIN_HTTP_HTTP_NEW_CLIENT_H_ |
| #define GARNET_BIN_HTTP_HTTP_NEW_CLIENT_H_ |
| |
| #include <zircon/status.h> |
| |
| #include <set> |
| |
| #include "garnet/bin/http/http_errors.h" |
| #include "garnet/bin/http/upload_element_reader.h" |
| #include "lib/fsl/vmo/sized_vmo.h" |
| #include "lib/fxl/logging.h" |
| #include "lib/fxl/strings/ascii.h" |
| |
| #include <asio.hpp> |
| #include <asio/ssl.hpp> |
| |
| using asio::ip::tcp; |
| |
| namespace http { |
| |
| typedef asio::ssl::stream<tcp::socket> ssl_socket_t; |
| typedef tcp::socket nonssl_socket_t; |
| |
| template <typename T> |
| class URLLoaderImpl::HTTPClient { |
| static_assert(std::is_same<T, ssl_socket_t>::value || |
| std::is_same<T, nonssl_socket_t>::value, |
| "requires either ssl_socket_t or nonssl_socket_t"); |
| |
| public: |
| static const std::set<std::string> ALLOWED_METHODS; |
| |
| static bool IsMethodAllowed(const std::string& method); |
| |
| HTTPClient<T>(URLLoaderImpl* loader, asio::io_service& io_service, |
| asio::ssl::context& context); |
| |
| HTTPClient<T>(URLLoaderImpl* loader, asio::io_service& io_service); |
| |
| zx_status_t CreateRequest( |
| const std::string& server, const std::string& path, |
| const std::string& method, |
| const std::map<std::string, std::string>& extra_headers, |
| std::unique_ptr<http::UploadElementReader> request_body_reader); |
| void Start(const std::string& server, const std::string& port); |
| |
| private: |
| using TransferBuffer = char[64 * 1024]; |
| |
| void SetHostName(const std::string& server); |
| |
| void OnResolve(const asio::error_code& err, |
| tcp::resolver::iterator endpoint_iterator); |
| bool OnVerifyCertificate(bool preverified, asio::ssl::verify_context& ctx); |
| void OnConnect(const asio::error_code& err); |
| void OnHandShake(const asio::error_code& err); |
| void OnWriteRequestHeaders(const asio::error_code& err, |
| std::size_t transferred); |
| void WriteRequestBody(); |
| void OnWriteRequestBody(const asio::error_code& err, std::size_t transferred); |
| void OnReadStatusLine(const asio::error_code& err); |
| zx_status_t SendStreamedBody(); |
| zx_status_t SendBufferedBody(); |
| void ParseHeaderField(const std::string& header, std::string* name, |
| std::string* value); |
| void OnReadHeaders(const asio::error_code& err); |
| void OnStreamBody(const asio::error_code& err); |
| void OnBufferBody(const asio::error_code& err); |
| |
| void SendResponse(::fuchsia::net::oldhttp::URLResponse response); |
| void SendError(int error_code); |
| |
| public: |
| unsigned int status_code_; |
| std::string redirect_location_; |
| |
| private: |
| URLLoaderImpl* loader_; |
| |
| tcp::resolver resolver_; |
| T socket_; |
| asio::streambuf request_header_buf_; |
| std::unique_ptr<http::UploadElementReader> request_body_reader_; |
| asio::streambuf request_body_buf_; |
| std::ostream request_body_stream_; |
| asio::streambuf response_buf_; |
| |
| std::string http_version_; |
| std::string status_message_; |
| |
| ::fuchsia::net::oldhttp::URLResponse |
| response_; // used for buffered responses |
| zx::socket response_body_stream_; // used for streamed responses (default) |
| }; |
| |
| template <typename T> |
| const std::set<std::string> URLLoaderImpl::HTTPClient<T>::ALLOWED_METHODS{ |
| "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", "PATCH"}; |
| |
| template <typename T> |
| bool URLLoaderImpl::HTTPClient<T>::IsMethodAllowed(const std::string& method) { |
| return ALLOWED_METHODS.find(method) != ALLOWED_METHODS.end(); |
| } |
| |
| template <> |
| void URLLoaderImpl::HTTPClient<ssl_socket_t>::OnResolve( |
| const asio::error_code& err, tcp::resolver::iterator endpoint_iterator); |
| template <> |
| void URLLoaderImpl::HTTPClient<nonssl_socket_t>::OnResolve( |
| const asio::error_code& err, tcp::resolver::iterator endpoint_iterator); |
| template <> |
| void URLLoaderImpl::HTTPClient<ssl_socket_t>::OnConnect( |
| const asio::error_code& err); |
| template <> |
| void URLLoaderImpl::HTTPClient<nonssl_socket_t>::OnConnect( |
| const asio::error_code& err); |
| |
| template <> |
| URLLoaderImpl::HTTPClient<ssl_socket_t>::HTTPClient( |
| URLLoaderImpl* loader, asio::io_service& io_service, |
| asio::ssl::context& context) |
| : loader_(loader), |
| resolver_(io_service), |
| socket_(io_service, context), |
| request_body_stream_(&request_body_buf_) {} |
| |
| template <> |
| URLLoaderImpl::HTTPClient<nonssl_socket_t>::HTTPClient( |
| URLLoaderImpl* loader, asio::io_service& io_service) |
| : loader_(loader), |
| resolver_(io_service), |
| socket_(io_service), |
| request_body_stream_(&request_body_buf_) {} |
| |
| template <typename T> |
| zx_status_t URLLoaderImpl::HTTPClient<T>::CreateRequest( |
| const std::string& server, const std::string& path, |
| const std::string& method, |
| const std::map<std::string, std::string>& extra_headers, |
| std::unique_ptr<http::UploadElementReader> request_body_reader) { |
| if (!IsMethodAllowed(method)) { |
| FXL_VLOG(1) << "Method " << method << " is not allowed"; |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| SetHostName(server); |
| |
| std::ostream request_header_stream(&request_header_buf_); |
| |
| bool has_accept = false; |
| request_header_stream << method << " " << path << " HTTP/1.1\r\n"; |
| request_header_stream << "Host: " << server << "\r\n"; |
| // TODO(toshik): should we make this work without closing the connection? |
| request_header_stream << "Connection: close\r\n"; |
| |
| for (auto it = extra_headers.begin(); it != extra_headers.end(); ++it) { |
| request_header_stream << it->first << ": " << it->second << "\r\n"; |
| has_accept = |
| has_accept || fxl::EqualsCaseInsensitiveASCII(it->first, "accept"); |
| } |
| if (!has_accept) |
| request_header_stream << "Accept: */*\r\n"; |
| |
| request_body_reader_ = std::move(request_body_reader); |
| if (request_body_reader_) { |
| size_t content_length = request_body_reader_->size(); |
| if (request_body_reader_->err() != ZX_OK) { |
| return request_body_reader_->err(); |
| } |
| if (content_length != http::UploadElementReader::kUnknownSize) { |
| request_header_stream << "Content-Length: " << content_length << "\r\n"; |
| } |
| } |
| |
| request_header_stream << "\r\n"; |
| |
| return ZX_OK; |
| } |
| |
| template <> |
| void URLLoaderImpl::HTTPClient<ssl_socket_t>::SetHostName( |
| const std::string& server) { |
| ::SSL_set_tlsext_host_name(socket_.native_handle(), server.c_str()); |
| asio::detail::throw_error(asio::error_code(), "set_tlsext_host_name"); |
| } |
| |
| template <> |
| void URLLoaderImpl::HTTPClient<nonssl_socket_t>::SetHostName( |
| const std::string& server) {} |
| |
| template <typename T> |
| void URLLoaderImpl::HTTPClient<T>::Start(const std::string& server, |
| const std::string& port) { |
| tcp::resolver::query query(server, port); |
| resolver_.async_resolve( |
| query, std::bind(&HTTPClient<T>::OnResolve, this, std::placeholders::_1, |
| std::placeholders::_2)); |
| } |
| |
| template <> |
| void URLLoaderImpl::HTTPClient<ssl_socket_t>::OnResolve( |
| const asio::error_code& err, tcp::resolver::iterator endpoint_iterator) { |
| if (!err) { |
| #ifdef NETWORK_SERVICE_DISABLE_CERT_VERIFY |
| socket_.set_verify_mode(asio::ssl::verify_none); |
| #else |
| socket_.set_verify_mode(asio::ssl::verify_peer); |
| #endif |
| socket_.set_verify_callback( |
| std::bind(&HTTPClient<ssl_socket_t>::OnVerifyCertificate, this, |
| std::placeholders::_1, std::placeholders::_2)); |
| asio::async_connect(socket_.lowest_layer(), endpoint_iterator, |
| std::bind(&HTTPClient<ssl_socket_t>::OnConnect, this, |
| std::placeholders::_1)); |
| } else { |
| FXL_VLOG(1) << "Resolve(SSL): " << err.message(); |
| SendError(HTTP_ERR_NAME_NOT_RESOLVED); |
| } |
| } |
| |
| template <> |
| void URLLoaderImpl::HTTPClient<nonssl_socket_t>::OnResolve( |
| const asio::error_code& err, tcp::resolver::iterator endpoint_iterator) { |
| if (!err) { |
| asio::async_connect(socket_, endpoint_iterator, |
| std::bind(&HTTPClient<nonssl_socket_t>::OnConnect, this, |
| std::placeholders::_1)); |
| } else { |
| FXL_VLOG(1) << "Resolve(NonSSL): " << err.message(); |
| SendError(HTTP_ERR_NAME_NOT_RESOLVED); |
| } |
| } |
| |
| template <typename T> |
| bool URLLoaderImpl::HTTPClient<T>::OnVerifyCertificate( |
| bool preverified, asio::ssl::verify_context& ctx) { |
| // TODO(toshik): RFC 2818 describes the steps involved in doing this for |
| // HTTPS. |
| char subject_name[256]; |
| X509* cert = X509_STORE_CTX_get_current_cert(ctx.native_handle()); |
| X509_NAME_oneline(X509_get_subject_name(cert), subject_name, 256); |
| |
| #ifdef NETWORK_SERVICE_HTTPS_CERT_HACK |
| preverified = true; |
| #endif |
| return preverified; |
| } |
| |
| template <> |
| void URLLoaderImpl::HTTPClient<ssl_socket_t>::OnConnect( |
| const asio::error_code& err) { |
| if (!err) { |
| socket_.async_handshake(asio::ssl::stream_base::client, |
| std::bind(&HTTPClient<ssl_socket_t>::OnHandShake, |
| this, std::placeholders::_1)); |
| } else { |
| FXL_VLOG(1) << "Connect(SSL): " << err.message(); |
| SendError(HTTP_ERR_CONNECTION_FAILED); |
| } |
| } |
| |
| template <> |
| void URLLoaderImpl::HTTPClient<nonssl_socket_t>::OnConnect( |
| const asio::error_code& err) { |
| if (!err) { |
| asio::async_write( |
| socket_, request_header_buf_, |
| std::bind(&HTTPClient<nonssl_socket_t>::OnWriteRequestHeaders, this, |
| std::placeholders::_1, std::placeholders::_2)); |
| } else { |
| FXL_VLOG(1) << "Connect(NonSSL): " << err.message(); |
| SendError(HTTP_ERR_CONNECTION_FAILED); |
| } |
| } |
| |
| template <typename T> |
| void URLLoaderImpl::HTTPClient<T>::OnHandShake(const asio::error_code& err) { |
| if (!err) { |
| asio::async_write(socket_, request_header_buf_, |
| std::bind(&HTTPClient<T>::OnWriteRequestHeaders, this, |
| std::placeholders::_1, std::placeholders::_2)); |
| } else { |
| FXL_VLOG(1) << "HandShake: " << err.message(); |
| SendError(HTTP_ERR_SSL_HANDSHAKE_NOT_COMPLETED); |
| } |
| } |
| |
| template <typename T> |
| void URLLoaderImpl::HTTPClient<T>::OnWriteRequestHeaders( |
| const asio::error_code& err, std::size_t transferred) { |
| if (!err) { |
| request_header_buf_.consume(transferred); |
| |
| if (request_header_buf_.size() > 0) { |
| asio::async_write( |
| socket_, request_header_buf_, |
| std::bind(&HTTPClient<T>::OnWriteRequestHeaders, this, |
| std::placeholders::_1, std::placeholders::_2)); |
| } else { |
| WriteRequestBody(); |
| } |
| } else { |
| FXL_VLOG(1) << "WriteRequestHeaders: " << err.message(); |
| // TODO(toshik): better error code? |
| SendError(HTTP_ERR_FAILED); |
| } |
| } |
| |
| template <typename T> |
| void URLLoaderImpl::HTTPClient<T>::WriteRequestBody() { |
| if (request_body_buf_.size() > 0 || |
| (request_body_reader_ && |
| request_body_reader_->ReadAvailable(&request_body_stream_))) { |
| asio::async_write(socket_, request_body_buf_, |
| std::bind(&HTTPClient<T>::OnWriteRequestBody, this, |
| std::placeholders::_1, std::placeholders::_2)); |
| } else if (request_body_reader_ && request_body_reader_->err() != ZX_OK) { |
| SendError(HTTP_ERR_FAILED); |
| } else { |
| // TODO(toshik): The response_ streambuf will automatically grow |
| // The growth may be limited by passing a maximum size to the |
| // streambuf constructor. |
| asio::async_read_until(socket_, response_buf_, "\r\n", |
| std::bind(&HTTPClient<T>::OnReadStatusLine, this, |
| std::placeholders::_1)); |
| } |
| } |
| |
| template <typename T> |
| void URLLoaderImpl::HTTPClient<T>::OnWriteRequestBody( |
| const asio::error_code& err, std::size_t transferred) { |
| if (!err) { |
| request_body_buf_.consume(transferred); |
| WriteRequestBody(); |
| } else { |
| FXL_VLOG(1) << "WriteRequestBody: " << err.message(); |
| // TODO(toshik): better error code? |
| SendError(HTTP_ERR_FAILED); |
| } |
| } |
| |
| template <typename T> |
| void URLLoaderImpl::HTTPClient<T>::OnReadStatusLine( |
| const asio::error_code& err) { |
| if (!err) { |
| std::istream response_stream(&response_buf_); |
| response_stream >> http_version_; |
| response_stream >> status_code_; |
| std::string status_message; |
| std::getline(response_stream, status_message_); |
| if (!response_stream || http_version_.substr(0, 5) != "HTTP/") { |
| FXL_VLOG(1) << "ReadStatusLine: Invalid response\n"; |
| SendError(HTTP_ERR_INVALID_RESPONSE); |
| return; |
| } |
| // TODO(toshik): we don't treat any status code as an NETWORK_ERR for now |
| |
| asio::async_read_until( |
| socket_, response_buf_, "\r\n\r\n", |
| std::bind(&HTTPClient<T>::OnReadHeaders, this, std::placeholders::_1)); |
| } else { |
| FXL_VLOG(1) << "ReadStatusLine: " << err.message(); |
| } |
| } |
| |
| template <typename T> |
| zx_status_t URLLoaderImpl::HTTPClient<T>::SendStreamedBody() { |
| size_t size = response_buf_.size(); |
| |
| if (size > 0) { |
| std::istream response_stream(&response_buf_); |
| size_t done = 0; |
| do { |
| TransferBuffer buffer; |
| size_t todo = std::min(sizeof(buffer), size - done); |
| FXL_DCHECK(todo > 0); |
| response_stream.read(buffer, todo); |
| size_t offset = 0; |
| do { |
| size_t written = 0; |
| zx_status_t result = response_body_stream_.write( |
| 0, buffer + offset, todo - offset, &written); |
| if (result == ZX_ERR_SHOULD_WAIT) { |
| result = response_body_stream_.wait_one( |
| ZX_SOCKET_WRITABLE | ZX_SOCKET_PEER_CLOSED, zx::time::infinite(), |
| nullptr); |
| if (result == ZX_OK) |
| continue; // retry now that the socket is ready |
| } |
| if (result != ZX_OK) { |
| // If the other end closes the socket, ZX_ERR_PEER_CLOSED |
| // can happen. |
| if (result != ZX_ERR_PEER_CLOSED) |
| FXL_VLOG(1) << "SendStreamedBody: result=" << result; |
| return result; |
| } |
| offset += written; |
| } while (offset < todo); |
| FXL_DCHECK(offset == todo); |
| done += todo; |
| } while (done < size); |
| } |
| return ZX_OK; |
| } |
| |
| template <typename T> |
| zx_status_t URLLoaderImpl::HTTPClient<T>::SendBufferedBody() { |
| size_t size = response_buf_.size(); |
| |
| if (size > 0) { |
| // TODO(rosswang): For now, wait until we have the entire body to begin |
| // writing to the VMO so that we know the size. Eventually to support larger |
| // VMOs without burdening the memory unduly, perhaps we'll want to create |
| // the VMO earlier and resize it as we buffer more to take advantage of |
| // VMO virtualization. |
| zx::vmo vmo; |
| zx_status_t result = zx::vmo::create(size, 0u, &vmo); |
| if (result != ZX_OK) { |
| FXL_VLOG(1) << "SendBufferedBody: Unable to create vmo: " << result; |
| return result; |
| } |
| |
| std::istream response_stream(&response_buf_); |
| size_t done = 0; |
| do { |
| TransferBuffer buffer; |
| size_t todo = std::min(sizeof(buffer), size - done); |
| FXL_DCHECK(todo > 0); |
| response_stream.read(buffer, todo); |
| result = vmo.write(buffer, done, todo); |
| if (result != ZX_OK) { |
| FXL_VLOG(1) << "SendBufferedBody: result=" << result; |
| return result; |
| } |
| done += todo; |
| } while (done < size); |
| |
| FXL_DCHECK(loader_->response_body_mode_ == |
| ::fuchsia::net::oldhttp::ResponseBodyMode::BUFFER); |
| response_.body->set_buffer( |
| fsl::SizedVmo(std::move(vmo), size).ToTransport()); |
| } |
| return ZX_OK; |
| } |
| |
| template <typename T> |
| void URLLoaderImpl::HTTPClient<T>::ParseHeaderField(const std::string& header, |
| std::string* name, |
| std::string* value) { |
| std::string::const_iterator name_end = |
| std::find(header.begin(), header.end(), ':'); |
| *name = std::string(header.begin(), name_end); |
| |
| std::string::const_iterator value_begin = |
| std::find_if(name_end + 1, header.end(), [](int c) { return c != ' '; }); |
| std::string::const_iterator value_end = |
| std::find_if(name_end + 1, header.end(), [](int c) { return c == '\r'; }); |
| *value = std::string(value_begin, value_end); |
| } |
| |
| template <typename T> |
| void URLLoaderImpl::HTTPClient<T>::OnReadHeaders(const asio::error_code& err) { |
| if (!err) { |
| std::istream response_stream(&response_buf_); |
| std::string header; |
| |
| if (status_code_ == 301 || status_code_ == 302) { |
| redirect_location_.clear(); |
| |
| while (std::getline(response_stream, header) && header != "\r") { |
| ::fuchsia::net::oldhttp::HttpHeaderPtr hdr = |
| ::fuchsia::net::oldhttp::HttpHeader::New(); |
| std::string name, value; |
| ParseHeaderField(header, &name, &value); |
| if (name == "Location") { |
| redirect_location_ = value; |
| FXL_VLOG(1) << "Redirecting to " << redirect_location_; |
| } |
| } |
| } else { |
| ::fuchsia::net::oldhttp::URLResponse response; |
| response.status_code = status_code_; |
| response.status_line = |
| http_version_ + " " + std::to_string(status_code_) + status_message_; |
| response.url = loader_->current_url_.spec(); |
| |
| while (std::getline(response_stream, header) && header != "\r") { |
| ::fuchsia::net::oldhttp::HttpHeader hdr; |
| std::string name, value; |
| ParseHeaderField(header, &name, &value); |
| hdr.name = std::move(name); |
| hdr.value = std::move(value); |
| response.headers.push_back(std::move(hdr)); |
| } |
| |
| response.body = std::make_unique<::fuchsia::net::oldhttp::URLBody>(); |
| |
| switch (loader_->response_body_mode_) { |
| case ::fuchsia::net::oldhttp::ResponseBodyMode::BUFFER: |
| response_ = std::move(response); |
| |
| asio::async_read(socket_, response_buf_, |
| std::bind(&HTTPClient<T>::OnBufferBody, this, |
| std::placeholders::_1)); |
| break; |
| case ::fuchsia::net::oldhttp::ResponseBodyMode::STREAM: |
| case ::fuchsia::net::oldhttp::ResponseBodyMode::BUFFER_OR_STREAM: |
| zx::socket consumer; |
| zx::socket producer; |
| zx_status_t status = zx::socket::create(0u, &producer, &consumer); |
| if (status != ZX_OK) { |
| FXL_VLOG(1) << "Unable to create socket:" |
| << zx_status_get_string(status); |
| return; |
| } |
| response_body_stream_ = std::move(producer); |
| response.body->set_stream(std::move(consumer)); |
| |
| loader_->SendResponse(std::move(response)); |
| |
| if (SendStreamedBody() != ZX_OK) { |
| response_body_stream_.reset(); |
| return; |
| } |
| |
| asio::async_read(socket_, response_buf_, asio::transfer_at_least(1), |
| std::bind(&HTTPClient<T>::OnStreamBody, this, |
| std::placeholders::_1)); |
| break; |
| } |
| } |
| } else { |
| FXL_VLOG(1) << "ReadHeaders: " << err.message(); |
| } |
| } |
| |
| template <typename T> |
| void URLLoaderImpl::HTTPClient<T>::OnBufferBody(const asio::error_code& err) { |
| // asio::error::eof happens if the other side closed their connection. |
| if (err && err != asio::ssl::error::stream_truncated && |
| err != asio::error::eof) { |
| // TODO: if EOF, should probably confirm we read all of the bytes (see |
| // Content-Length header). |
| FXL_VLOG(1) << "OnBufferBody: " << err.message() << " (" << err << ")"; |
| // TODO(somebody who knows asio/network errors): real translation |
| SendError(HTTP_ERR_FAILED); |
| } else { |
| SendBufferedBody(); |
| loader_->SendResponse(std::move(response_)); |
| } |
| } |
| |
| template <typename T> |
| void URLLoaderImpl::HTTPClient<T>::OnStreamBody(const asio::error_code& err) { |
| if (!err && SendStreamedBody() == ZX_OK) { |
| asio::async_read( |
| socket_, response_buf_, asio::transfer_at_least(1), |
| std::bind(&HTTPClient<T>::OnStreamBody, this, std::placeholders::_1)); |
| } else { |
| // EOF is handled here. |
| // TODO(toshik): print the error code if it is unexpected. |
| // FXL_VLOG(1) << "OnStreamBody: " << err.message(); |
| response_body_stream_.reset(); |
| } |
| } |
| |
| template <typename T> |
| void URLLoaderImpl::HTTPClient<T>::SendResponse( |
| ::fuchsia::net::oldhttp::URLResponse response) { |
| loader_->SendResponse(std::move(response)); |
| } |
| |
| template <typename T> |
| void URLLoaderImpl::HTTPClient<T>::SendError(int error_code) { |
| loader_->SendError(error_code); |
| } |
| |
| } // namespace http |
| |
| #if defined(ASIO_NO_EXCEPTIONS) |
| // If C++ exception is disabled, ASIO calls this function instead of throwing |
| // an exception. Do not simply return from this function. The execution should |
| // be aborted immediately. |
| // |
| // TODO: Abort the current thread only. |
| template <typename Exception> |
| void asio::detail::throw_exception(const Exception& e) { |
| FXL_LOG(FATAL) << "Exception occurred: " << e.what(); |
| } |
| #endif |
| |
| #endif // GARNET_BIN_HTTP_HTTP_NEW_CLIENT_H_ |