aboutsummaryrefslogblamecommitdiffstats
path: root/library/cpp/http/simple/http_client.h
blob: eab92d42da5ace6abc1c91daae2b171bc82dd6f9 (plain) (tree)

























































































































































































                                                                                                                                          


                                                              




                                                                                                                                           

                  














































































                                                                                                                           
                                          
             
#pragma once

#include "http_client_options.h"

#include <util/datetime/base.h>
#include <util/generic/hash.h>
#include <util/generic/ptr.h>
#include <util/generic/strbuf.h>
#include <util/generic/yexception.h>
#include <util/network/socket.h>

#include <library/cpp/http/io/stream.h>
#include <library/cpp/http/misc/httpcodes.h>
#include <library/cpp/openssl/io/stream.h>

class TNetworkAddress;
class IOutputStream;
class TSocket;

namespace NPrivate {
    class THttpConnection;
}

/*!
 * HTTPS is supported in two modes.
 * HTTPS verification enabled by default in TKeepAliveHttpClient and disabled by default in TSimpleHttpClient.
 * HTTPS verification requires valid private certificate on server side and valid public certificate on client side.
 *
 * For client:
 * Uses builtin certs.
 * Also uses default CA path /etc/ssl/certs/ - can be provided with debian package: ca-certificates.deb.
 * It can be expanded with ENV: SSL_CERT_DIR.
 */

/*!
 * TKeepAliveHttpClient can keep connection alive with HTTP and HTTPS only if you use the same instance of class.
 * It closes connection on every socket/network error and throws error.
 * For example, HTTP code == 500 is NOT error - connection will be still open.
 * It is THREAD UNSAFE because it stores connection state in attributes.
 * If you need thread safe client, look at TSimpleHttpClient
 */

class TKeepAliveHttpClient {
public:
    using THeaders = THashMap<TString, TString>;
    using THttpCode = unsigned;

public:
    TKeepAliveHttpClient(const TString& host,
                         ui32 port,
                         TDuration socketTimeout = TDuration::Seconds(5),
                         TDuration connectTimeout = TDuration::Seconds(30));

    THttpCode DoGet(const TStringBuf relativeUrl,
                    IOutputStream* output = nullptr,
                    const THeaders& headers = THeaders(),
                    THttpHeaders* outHeaders = nullptr);

    // builds post request from headers and body
    THttpCode DoPost(const TStringBuf relativeUrl,
                     const TStringBuf body,
                     IOutputStream* output = nullptr,
                     const THeaders& headers = THeaders(),
                     THttpHeaders* outHeaders = nullptr);

    // builds request with any HTTP method from headers and body
    THttpCode DoRequest(const TStringBuf method,
                        const TStringBuf relativeUrl,
                        const TStringBuf body,
                        IOutputStream* output = nullptr,
                        const THeaders& inHeaders = THeaders(),
                        THttpHeaders* outHeaders = nullptr);

    // requires already well-formed request
    THttpCode DoRequestRaw(const TStringBuf raw,
                           IOutputStream* output = nullptr,
                           THttpHeaders* outHeaders = nullptr);

    void DisableVerificationForHttps();
    void SetClientCertificate(const TOpenSslClientIO::TOptions::TClientCert& options);

    void ResetConnection();

    const TString& GetHost() const {
        return Host;
    }

    ui32 GetPort() const {
        return Port;
    }

private:
    template <class T>
    THttpCode DoRequestReliable(const T& raw,
                                IOutputStream* output,
                                THttpHeaders* outHeaders);

    TVector<IOutputStream::TPart> FormRequest(TStringBuf method, const TStringBuf relativeUrl,
                                              TStringBuf body,
                                              const THeaders& headers, TStringBuf contentLength) const;

    THttpCode ReadAndTransferHttp(THttpInput& input, IOutputStream* output, THttpHeaders* outHeaders) const;

    bool CreateNewConnectionIfNeeded(); // Returns true if now we have a new connection.

private:
    using TVerifyCert = TOpenSslClientIO::TOptions::TVerifyCert;
    using TClientCert = TOpenSslClientIO::TOptions::TClientCert;

    const TString Host;
    const ui32 Port;
    const TDuration SocketTimeout;
    const TDuration ConnectTimeout;
    const bool IsHttps;

    THolder<NPrivate::THttpConnection> Connection;
    bool IsClosingRequired;
    TMaybe<TClientCert> ClientCertificate;
    TMaybe<TVerifyCert> HttpsVerification;

private:
    THttpInput* GetHttpInput();

    using TIfResponseRequired = std::function<bool(const THttpInput&)>;
    TIfResponseRequired IfResponseRequired;

    friend class TSimpleHttpClient;
    friend class TRedirectableHttpClient;
};

class THttpRequestException: public yexception {
private:
    int StatusCode;

public:
    THttpRequestException(int statusCode = 0);
    int GetStatusCode() const;
};

/*!
 * TSimpleHttpClient can NOT keep connection alive.
 * It closes connection after each request.
 * HTTP code < 200 || code >= 300 is error - exception will be thrown.
 * It is THREAD SAFE because it stores only consts.
 */

class TSimpleHttpClient {
protected:
    using TVerifyCert = TKeepAliveHttpClient::TVerifyCert;

    const TString Host;
    const ui32 Port;
    const TDuration SocketTimeout;
    const TDuration ConnectTimeout;
    bool HttpsVerification = false;

public:
    using THeaders = TKeepAliveHttpClient::THeaders;
    using TOptions = TSimpleHttpClientOptions;

public:
    explicit TSimpleHttpClient(const TOptions& options);

    TSimpleHttpClient(const TString& host, ui32 port,
                      TDuration socketTimeout = TDuration::Seconds(5), TDuration connectTimeout = TDuration::Seconds(30));

    void EnableVerificationForHttps();

    void DoGet(const TStringBuf relativeUrl, IOutputStream* output, const THeaders& headers = THeaders()) const;

    // builds post request from headers and body
    void DoPost(const TStringBuf relativeUrl, TStringBuf body, IOutputStream* output, const THeaders& headers = THeaders()) const;

    // requires already well-formed post request
    void DoPostRaw(const TStringBuf relativeUrl, TStringBuf rawRequest, IOutputStream* output) const;

    virtual ~TSimpleHttpClient();

private:
    TKeepAliveHttpClient CreateClient() const;

    virtual void PrepareClient(TKeepAliveHttpClient& cl) const;
    virtual void ProcessResponse(const TStringBuf relativeUrl, THttpInput& input, IOutputStream* output, const unsigned statusCode) const;
};

class TRedirectableHttpClient: public TSimpleHttpClient {
public:
    using TOptions = TSimpleHttpClientOptions;

    explicit TRedirectableHttpClient(const TOptions& options);

    TRedirectableHttpClient(const TString& host, ui32 port, TDuration socketTimeout = TDuration::Seconds(5),
                            TDuration connectTimeout = TDuration::Seconds(30));

private:
    void PrepareClient(TKeepAliveHttpClient& cl) const override;
    void ProcessResponse(const TStringBuf relativeUrl, THttpInput& input, IOutputStream* output, const unsigned statusCode) const override;

private:
    TOptions Opts;
};

namespace NPrivate {
    class THttpConnection {
    public:
        THttpConnection(const TString& host,
                        ui32 port,
                        TDuration sockTimeout,
                        TDuration connTimeout,
                        bool isHttps,
                        const TMaybe<TOpenSslClientIO::TOptions::TClientCert>& clientCert,
                        const TMaybe<TOpenSslClientIO::TOptions::TVerifyCert>& verifyCert);

        bool IsOk() const {
            return IsNotSocketClosedByOtherSide(Socket);
        }

        template <typename TContainer>
        void Write(const TContainer& request) {
            HttpOut->Write(request.data(), request.size());
            HttpIn = Ssl ? MakeHolder<THttpInput>(Ssl.Get())
                         : MakeHolder<THttpInput>(&SocketIn);
            HttpOut->Flush();
        }

        THttpInput* GetHttpInput() {
            return HttpIn.Get();
        }

    private:
        static TNetworkAddress Resolve(const TString& host, ui32 port);

        static TSocket Connect(TNetworkAddress& addr,
                               TDuration sockTimeout,
                               TDuration connTimeout,
                               const TString& host,
                               ui32 port);

    private:
        TNetworkAddress Addr;
        TSocket Socket;
        TSocketInput SocketIn;
        TSocketOutput SocketOut;
        THolder<TOpenSslClientIO> Ssl;
        THolder<THttpInput> HttpIn;
        THolder<THttpOutput> HttpOut;
    };
}

template <class T>
TKeepAliveHttpClient::THttpCode TKeepAliveHttpClient::DoRequestReliable(const T& raw,
                                                                        IOutputStream* output,
                                                                        THttpHeaders* outHeaders) {
    for (int i = 0; i < 2; ++i) {
        const bool haveNewConnection = CreateNewConnectionIfNeeded();
        const bool couldRetry = !haveNewConnection && i == 0; // Actually old connection could be already closed by server,
                                                              // so we should try one more time in this case.
        try {
            Connection->Write(raw);

            THttpCode code = ReadAndTransferHttp(*Connection->GetHttpInput(), output, outHeaders);
            if (!Connection->GetHttpInput()->IsKeepAlive()) {
                IsClosingRequired = true;
            }
            return code;
        } catch (const TSystemError& e) {
            Connection.Reset();
            if (!couldRetry || e.Status() != EPIPE) {
                throw;
            }
        } catch (const THttpReadException&) { // Actually old connection is already closed by server
            Connection.Reset();
            if (!couldRetry) {
                throw;
            }
        } catch (const std::exception&) {
            Connection.Reset();
            throw;
        }
    }
    Y_ABORT(); // We should never be here.
    return 0;
}