Skip to content
Snippets Groups Projects
client.hh 10.8 KiB
Newer Older
  • Learn to ignore specific revisions
  • #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