#ifndef __VEREIGN_RESTAPI_CLIENT_HH #define __VEREIGN_RESTAPI_CLIENT_HH #include <chrono> #include <type_traits> #include <vereign/restapi/detail/post_task.hh> #include <vereign/restapi/http_header.hh> #include <boost/asio/io_context.hpp> #include <boost/asio/ssl/context.hpp> #include <boost/beast/core/tcp_stream.hpp> #include <boost/beast/ssl.hpp> #include <boost/beast/http.hpp> #include <google/protobuf/message.h> #include <string> #include <future> #include <deque> namespace vereign { namespace restapi { namespace beast = boost::beast; namespace asio = boost::asio; using tcp = asio::ip::tcp; using RequestPtr = std::unique_ptr<google::protobuf::Message>; namespace detail { class HttpReader; } // namespace detail /** * Client is a http client for the Vereign Restful API. * * Internally the Client uses the [boost::beast][] library. * * The client provides both async and blocking APIs for making POST requests. * * The blocking methods return futures, and must not be called inside the * async callbacks.Their purpose is to be used in threads outside of the * Client's executor. * * The POST requests are queued and executed sequentially reusing single * connection, reconnecting if needed. * * The connection is kept alive until there are requests, or when idle until * specified timeout is expired. * * [boost::beast]: \boostlib/libs/beast/doc/html/index.html * * ### Thread Safety * * All public methods are thread safe. */ class Client { public: /** * Constructs Client instance with default timeout (30s). * * @param ioc IO context. * @param ssl_ctx SSL context. * @param host Vereign restapi host. * @param port Vereign restapi port. For example "https" or "443". * @returns Client instance. */ Client( asio::io_context& ioc, asio::ssl::context& ssl_ctx, const std::string& host, const std::string& port ); /** * Constructs Client instance. * * @param ioc IO context. * @param ssl_ctx SSL context. * @param host Vereign restapi host. * @param port Vereign restapi port. For example "https" or "443". * @param expiry_time Connect, read, write expiration time. * @returns Client instance. */ Client( asio::io_context& ioc, asio::ssl::context& ssl_ctx, const std::string& host, const std::string& port, std::chrono::nanoseconds expiry_time ); /** * Closes the connection. * * @see Client::Close */ ~Client() noexcept; // Disable copying Client(const Client&) = delete; Client& operator=(const Client&) = delete; /** * Close closes the connection. * * The connection to the server is closed, and the current pending request * is cancelled with an error. All other requests in the queue will be also * cancelled. Subsequent calls to Client::Post or Client::PostAsync methods * will return with error, i.e. the client won't execute any requests anymore. */ void Close(); /** * GetExecutor returns the client's strand executor. * * @returns the client's strand executor. */ const asio::executor& GetExecutor() const; /** * Post makes a blocking post request. * * The passed `req` and `resp` parameters are moved in and returned back once, * the returned future is resolved. * * @param path HTTP path, for example `/api/identity/loginWithPreviouslyAddedDevice`. * @param req Request object that will be serialized as JSON body. * It can be a const pointer or std::unique_ptr to protobuf message. * * @param resp Response object that will be used to decode the response. * It can be a pointer or std::unique_ptr to protobuf message. * * @returns future that will be resolved with a result containing both * the request and response objects originally passed to the `Post` call. */ template <class RequestPtrType, class ResponsePtrType> auto Post( const std::string& path, RequestPtrType&& req, ResponsePtrType&& resp ) { using RequestPtr = typename std::remove_reference<RequestPtrType>::type; using ResponsePtr = typename std::remove_reference<ResponsePtrType>::type; using Result = PostResult<RequestPtr, ResponsePtr>; std::promise<Result> promise{}; auto future = promise.get_future(); PostAsync( path, std::move(req), std::move(resp), [promise = std::move(promise)] (Result&& result) mutable { promise.set_value(std::move(result)); } ); return future; } /** * Post makes a blocking post request with additional http headers. * * The passed `req` and `resp` parameters are moved in and returned back once, * the returned future is resolved. * * @param path HTTP path, for example `/api/identity/loginWithPreviouslyAddedDevice`. * @param req Request object that will be serialized as JSON body. * It can be a const pointer or std::unique_ptr to protobuf message. * * @param resp Response object that will be used to decode the response. * It can be a pointer or std::unique_ptr to protobuf message. * * @param headers HTTP headers to include in the request. * * @returns future that will be resolved with a result containing both * the request and response objects originally passed to the `Post` call. */ template <class RequestPtrType, class ResponsePtrType> auto Post( const std::string& path, RequestPtrType&& req, ResponsePtrType&& resp, std::vector<HttpHeader>&& headers ) { using RequestPtr = typename std::remove_reference<RequestPtrType>::type; using ResponsePtr = typename std::remove_reference<ResponsePtrType>::type; using Result = PostResult<RequestPtr, ResponsePtr>; std::promise<Result> promise{}; auto future = promise.get_future(); PostAsync( path, std::move(req), std::move(resp), std::move(headers), [promise = std::move(promise)] (Result&& result) mutable { promise.set_value(std::move(result)); } ); return future; } /** * PostAsync makes non blocking post request. * * The passed `req` and `resp` parameters are moved in and returned back once * the CompletionFunc is called. * * @tparam CompletionFunc A completion functor - `void (Result&&)`, where * the `Result` is restapi::PostResult <RequestPtr, ResponsePtr>, where * RequestPtr is `std::remove_reference<RequestPtrType>::type` and * ResponsePtr is `std::remove_reference<RequestPtrType>::type`. * * @param path HTTP path, for example `/api/identity/loginWithPreviouslyAddedDevice`. * @param req Request object that will be serialized as JSON body. * It can be a const pointer or std::unique_ptr to protobuf message. * * @param resp Response object that will be used to decode the response. * It can be a pointer or std::unique_ptr to protobuf message. * * @param func Completion func, that will be called when the post request * is finished. */ template <class RequestPtrType, class ResponsePtrType, class CompletionFunc> void PostAsync( const std::string& path, RequestPtrType&& req, ResponsePtrType&& resp, CompletionFunc&& func ) { using RequestPtr = typename std::remove_reference<RequestPtrType>::type; using ResponsePtr = typename std::remove_reference<ResponsePtrType>::type; addPostTask( std::make_unique<detail::PostTask<RequestPtr, ResponsePtr, CompletionFunc>>( path, std::move(req), std::move(resp), std::move(func) ) ); } /** * PostAsync makes non blocking post request with additional http headers. * * The passed `req` and `resp` parameters are moved in and returned back once * the CompletionFunc is called. * * @tparam CompletionFunc A completion functor - `void (Result&&)`, where * the `Result` is restapi::PostResult <RequestPtr, ResponsePtr>, where * RequestPtr is `std::remove_reference<RequestPtrType>::type` and * ResponsePtr is `std::remove_reference<RequestPtrType>::type`. * * @param path HTTP path, for example `/api/identity/loginWithPreviouslyAddedDevice`. * @param req Request object that will be serialized as JSON body. * It can be a const pointer or std::unique_ptr to protobuf message. * * @param resp Response object that will be used to decode the response. * It can be a pointer or std::unique_ptr to protobuf message. * * @param headers HTTP headers to include in the request. * * @param func Completion func, that will be called when the post request * is finished. */ template <class RequestPtrType, class ResponsePtrType, class CompletionFunc> void PostAsync( const std::string& path, RequestPtrType&& req, ResponsePtrType&& resp, std::vector<HttpHeader>&& headers, CompletionFunc&& func ) { using RequestPtr = typename std::remove_reference<RequestPtrType>::type; using ResponsePtr = typename std::remove_reference<ResponsePtrType>::type; addPostTask( std::make_unique<detail::PostTask<RequestPtr, ResponsePtr, CompletionFunc>>( path, std::move(req), std::move(resp), std::move(headers), std::move(func) ) ); } private: using streamType = beast::ssl_stream<beast::tcp_stream>; void handleError(const boost::optional<std::string>& err); void addPostTask(detail::PostTaskBasePtr&& task); void completeTask( const boost::optional<detail::HttpResponse>& resp, const boost::optional<std::string>& err ); void doPost(); void resolve(); void connect(tcp::resolver::results_type results); void handshake(); void readResponse(); void resetStream(); void cancelAllTasks(const detail::PostError& err); private: // a strand that calls all completion handlers in single thread. asio::executor executor_; // ssl context used for the https connection. asio::ssl::context& ssl_ctx_; // all post requests are added as tasks in this queue. std::deque<detail::PostTaskBasePtr> task_queue_; // ssl stream used for the http requests. std::shared_ptr<streamType> stream_; // used for resolving the host name into ip address. tcp::resolver resolver_; // request object used for the async_write calls of the post requests. boost::optional<detail::HttpRequest> req_; // reads the requests responses. std::shared_ptr<detail::HttpReader> reader_; // http server host name. std::string host_; // http server port - https, 443 ... std::string port_; // connecting, read, write expiration time. std::chrono::nanoseconds expiry_time_; // indicates that the client is currently in connecting state. bool connecting_; // indicates that the client is currently writing a post request. bool writing_; // indicates that the client is currently waiting for response to be read. bool reading_; // indicates that the client is closed. bool closed_; }; } // namespace restapi } // namespace vereign #endif // __VEREIGN_RESTAPI_CLIENT_HH