blob: 183566ad79cf6d4f3f61b1030ebe86b1e878e97a [file] [log] [blame]
// 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 "src/lib/fxl/logging.h"
#include "src/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_