aboutsummaryrefslogtreecommitdiffstats
path: root/library/cpp/http
diff options
context:
space:
mode:
authorDaniil Cherednik <dan.cherednik@gmail.com>2022-11-24 13:14:34 +0300
committerDaniil Cherednik <dan.cherednik@gmail.com>2022-11-24 14:46:00 +0300
commit87f7fceed34bcafb8aaff351dd493a35c916986f (patch)
tree26809ec8f550aba8eb019e59adc3d48e51913eb2 /library/cpp/http
parent11bc4015b8010ae201bf3eb33db7dba425aca35e (diff)
downloadydb-87f7fceed34bcafb8aaff351dd493a35c916986f.tar.gz
Ydb stable 22-4-4322.4.43
x-stable-origin-commit: 8d49d46cc834835bf3e50870516acd7376a63bcf
Diffstat (limited to 'library/cpp/http')
-rw-r--r--library/cpp/http/CMakeLists.txt1
-rw-r--r--library/cpp/http/simple/CMakeLists.txt21
-rw-r--r--library/cpp/http/simple/http_client.cpp356
-rw-r--r--library/cpp/http/simple/http_client.h276
-rw-r--r--library/cpp/http/simple/http_client_options.h59
-rw-r--r--library/cpp/http/simple/ut/http_ut.cpp439
-rw-r--r--library/cpp/http/simple/ut/https_server/http_server.crt19
-rw-r--r--library/cpp/http/simple/ut/https_server/http_server.key28
-rw-r--r--library/cpp/http/simple/ut/https_server/main.go70
-rw-r--r--library/cpp/http/simple/ut/https_ut.cpp97
10 files changed, 1366 insertions, 0 deletions
diff --git a/library/cpp/http/CMakeLists.txt b/library/cpp/http/CMakeLists.txt
index 44fdf1d060..8ce2e73037 100644
--- a/library/cpp/http/CMakeLists.txt
+++ b/library/cpp/http/CMakeLists.txt
@@ -10,3 +10,4 @@ add_subdirectory(fetch)
add_subdirectory(io)
add_subdirectory(misc)
add_subdirectory(server)
+add_subdirectory(simple)
diff --git a/library/cpp/http/simple/CMakeLists.txt b/library/cpp/http/simple/CMakeLists.txt
new file mode 100644
index 0000000000..317a748a89
--- /dev/null
+++ b/library/cpp/http/simple/CMakeLists.txt
@@ -0,0 +1,21 @@
+
+# This file was gererated by the build system used internally in the Yandex monorepo.
+# Only simple modifications are allowed (adding source-files to targets, adding simple properties
+# like target_include_directories). These modifications will be ported to original
+# ya.make files by maintainers. Any complex modifications which can't be ported back to the
+# original buildsystem will not be accepted.
+
+
+
+add_library(cpp-http-simple)
+target_link_libraries(cpp-http-simple PUBLIC
+ contrib-libs-cxxsupp
+ yutil
+ cpp-http-io
+ cpp-openssl-io
+ cpp-string_utils-url
+ library-cpp-uri
+)
+target_sources(cpp-http-simple PRIVATE
+ ${CMAKE_SOURCE_DIR}/library/cpp/http/simple/http_client.cpp
+)
diff --git a/library/cpp/http/simple/http_client.cpp b/library/cpp/http/simple/http_client.cpp
new file mode 100644
index 0000000000..818dc048ad
--- /dev/null
+++ b/library/cpp/http/simple/http_client.cpp
@@ -0,0 +1,356 @@
+#include "http_client.h"
+
+#include <library/cpp/string_utils/url/url.h>
+#include <library/cpp/uri/http_url.h>
+
+#include <util/stream/output.h>
+#include <util/string/cast.h>
+#include <util/string/join.h>
+#include <util/string/split.h>
+
+TKeepAliveHttpClient::TKeepAliveHttpClient(const TString& host,
+ ui32 port,
+ TDuration socketTimeout,
+ TDuration connectTimeout)
+ : Host(CutHttpPrefix(host))
+ , Port(port)
+ , SocketTimeout(socketTimeout)
+ , ConnectTimeout(connectTimeout)
+ , IsHttps(host.StartsWith("https"))
+ , IsClosingRequired(false)
+ , HttpsVerification(TVerifyCert{Host})
+ , IfResponseRequired([](const THttpInput&) { return true; })
+{
+}
+
+TKeepAliveHttpClient::THttpCode TKeepAliveHttpClient::DoGet(const TStringBuf relativeUrl,
+ IOutputStream* output,
+ const THeaders& headers,
+ THttpHeaders* outHeaders) {
+ return DoRequest(TStringBuf("GET"),
+ relativeUrl,
+ {},
+ output,
+ headers,
+ outHeaders);
+}
+
+TKeepAliveHttpClient::THttpCode TKeepAliveHttpClient::DoPost(const TStringBuf relativeUrl,
+ const TStringBuf body,
+ IOutputStream* output,
+ const THeaders& headers,
+ THttpHeaders* outHeaders) {
+ return DoRequest(TStringBuf("POST"),
+ relativeUrl,
+ body,
+ output,
+ headers,
+ outHeaders);
+}
+
+TKeepAliveHttpClient::THttpCode TKeepAliveHttpClient::DoRequest(const TStringBuf method,
+ const TStringBuf relativeUrl,
+ const TStringBuf body,
+ IOutputStream* output,
+ const THeaders& inHeaders,
+ THttpHeaders* outHeaders) {
+ const TString contentLength = IntToString<10, size_t>(body.size());
+ return DoRequestReliable(FormRequest(method, relativeUrl, body, inHeaders, contentLength), output, outHeaders);
+}
+
+TKeepAliveHttpClient::THttpCode TKeepAliveHttpClient::DoRequestRaw(const TStringBuf raw,
+ IOutputStream* output,
+ THttpHeaders* outHeaders) {
+ return DoRequestReliable(raw, output, outHeaders);
+}
+
+void TKeepAliveHttpClient::DisableVerificationForHttps() {
+ HttpsVerification.Clear();
+ Connection.Reset();
+}
+
+void TKeepAliveHttpClient::SetClientCertificate(const TOpenSslClientIO::TOptions::TClientCert& options) {
+ ClientCertificate = options;
+}
+
+void TKeepAliveHttpClient::ResetConnection() {
+ Connection.Reset();
+}
+
+TVector<IOutputStream::TPart> TKeepAliveHttpClient::FormRequest(TStringBuf method,
+ const TStringBuf relativeUrl,
+ TStringBuf body,
+ const TKeepAliveHttpClient::THeaders& headers,
+ TStringBuf contentLength) const {
+ TVector<IOutputStream::TPart> parts;
+
+ parts.reserve(16 + 4 * headers.size());
+ parts.push_back(method);
+ parts.push_back(TStringBuf(" "));
+ parts.push_back(relativeUrl);
+ parts.push_back(TStringBuf(" HTTP/1.1"));
+ parts.push_back(IOutputStream::TPart::CrLf());
+ parts.push_back(TStringBuf("Host: "));
+ parts.push_back(TStringBuf(Host));
+ parts.push_back(IOutputStream::TPart::CrLf());
+ parts.push_back(TStringBuf("Content-Length: "));
+ parts.push_back(contentLength);
+ parts.push_back(IOutputStream::TPart::CrLf());
+
+ for (const auto& entry : headers) {
+ parts.push_back(IOutputStream::TPart(entry.first));
+ parts.push_back(IOutputStream::TPart(TStringBuf(": ")));
+ parts.push_back(IOutputStream::TPart(entry.second));
+ parts.push_back(IOutputStream::TPart::CrLf());
+ }
+
+ parts.push_back(IOutputStream::TPart::CrLf());
+ if (body) {
+ parts.push_back(IOutputStream::TPart(body));
+ }
+
+ return parts;
+}
+
+TKeepAliveHttpClient::THttpCode TKeepAliveHttpClient::ReadAndTransferHttp(THttpInput& input,
+ IOutputStream* output,
+ THttpHeaders* outHeaders) const {
+ TKeepAliveHttpClient::THttpCode statusCode;
+ try {
+ statusCode = ParseHttpRetCode(input.FirstLine());
+ } catch (TFromStringException& e) {
+ TString rest = input.ReadAll();
+ ythrow THttpRequestException() << "Failed parse status code in response of " << Host << ": " << e.what() << " (" << input.FirstLine() << ")"
+ << "\nFull http response:\n"
+ << rest;
+ }
+
+ auto canContainBody = [](auto statusCode) {
+ return statusCode != HTTP_NOT_MODIFIED && statusCode != HTTP_NO_CONTENT;
+ };
+
+ if (output && canContainBody(statusCode) && IfResponseRequired(input)) {
+ TransferData(&input, output);
+ }
+ if (outHeaders) {
+ *outHeaders = input.Headers();
+ }
+
+ return statusCode;
+}
+
+THttpInput* TKeepAliveHttpClient::GetHttpInput() {
+ return Connection ? Connection->GetHttpInput() : nullptr;
+}
+
+bool TKeepAliveHttpClient::CreateNewConnectionIfNeeded() {
+ if (IsClosingRequired || (Connection && !Connection->IsOk())) {
+ Connection.Reset();
+ }
+ if (!Connection) {
+ Connection = MakeHolder<NPrivate::THttpConnection>(Host,
+ Port,
+ SocketTimeout,
+ ConnectTimeout,
+ IsHttps,
+ ClientCertificate,
+ HttpsVerification);
+ IsClosingRequired = false;
+ return true;
+ }
+ return false;
+}
+
+THttpRequestException::THttpRequestException(int statusCode)
+ : StatusCode(statusCode)
+{
+}
+
+int THttpRequestException::GetStatusCode() const {
+ return StatusCode;
+}
+
+TSimpleHttpClient::TSimpleHttpClient(const TOptions& options)
+ : Host(options.Host())
+ , Port(options.Port())
+ , SocketTimeout(options.SocketTimeout())
+ , ConnectTimeout(options.ConnectTimeout())
+{
+}
+
+TSimpleHttpClient::TSimpleHttpClient(const TString& host, ui32 port, TDuration socketTimeout, TDuration connectTimeout)
+ : Host(host)
+ , Port(port)
+ , SocketTimeout(socketTimeout)
+ , ConnectTimeout(connectTimeout)
+{
+}
+
+void TSimpleHttpClient::EnableVerificationForHttps() {
+ HttpsVerification = true;
+}
+
+void TSimpleHttpClient::DoGet(const TStringBuf relativeUrl, IOutputStream* output, const THeaders& headers) const {
+ TKeepAliveHttpClient cl = CreateClient();
+
+ TKeepAliveHttpClient::THttpCode code = cl.DoGet(relativeUrl, output, headers);
+
+ Y_ENSURE(cl.GetHttpInput());
+ ProcessResponse(relativeUrl, *cl.GetHttpInput(), output, code);
+}
+
+void TSimpleHttpClient::DoPost(const TStringBuf relativeUrl, TStringBuf body, IOutputStream* output, const THashMap<TString, TString>& headers) const {
+ TKeepAliveHttpClient cl = CreateClient();
+
+ TKeepAliveHttpClient::THttpCode code = cl.DoPost(relativeUrl, body, output, headers);
+
+ Y_ENSURE(cl.GetHttpInput());
+ ProcessResponse(relativeUrl, *cl.GetHttpInput(), output, code);
+}
+
+void TSimpleHttpClient::DoPostRaw(const TStringBuf relativeUrl, const TStringBuf rawRequest, IOutputStream* output) const {
+ TKeepAliveHttpClient cl = CreateClient();
+
+ TKeepAliveHttpClient::THttpCode code = cl.DoRequestRaw(rawRequest, output);
+
+ Y_ENSURE(cl.GetHttpInput());
+ ProcessResponse(relativeUrl, *cl.GetHttpInput(), output, code);
+}
+
+namespace NPrivate {
+ THttpConnection::THttpConnection(const TString& host,
+ ui32 port,
+ TDuration sockTimeout,
+ TDuration connTimeout,
+ bool isHttps,
+ const TMaybe<TOpenSslClientIO::TOptions::TClientCert>& clientCert,
+ const TMaybe<TOpenSslClientIO::TOptions::TVerifyCert>& verifyCert)
+ : Addr(Resolve(host, port))
+ , Socket(Connect(Addr, sockTimeout, connTimeout, host, port))
+ , SocketIn(Socket)
+ , SocketOut(Socket)
+ {
+ if (isHttps) {
+ TOpenSslClientIO::TOptions opts;
+ if (clientCert) {
+ opts.ClientCert_ = clientCert;
+ }
+ if (verifyCert) {
+ opts.VerifyCert_ = verifyCert;
+ }
+
+ Ssl = MakeHolder<TOpenSslClientIO>(&SocketIn, &SocketOut, opts);
+ HttpOut = MakeHolder<THttpOutput>(Ssl.Get());
+ } else {
+ HttpOut = MakeHolder<THttpOutput>(&SocketOut);
+ }
+
+ HttpOut->EnableKeepAlive(true);
+ }
+
+ TNetworkAddress THttpConnection::Resolve(const TString& host, ui32 port) {
+ try {
+ return TNetworkAddress(host, port);
+ } catch (const yexception& e) {
+ ythrow THttpRequestException() << "Resolve of " << host << ": " << e.what();
+ }
+ }
+
+ TSocket THttpConnection::Connect(TNetworkAddress& addr,
+ TDuration sockTimeout,
+ TDuration connTimeout,
+ const TString& host,
+ ui32 port) {
+ try {
+ TSocket socket(addr, connTimeout);
+ TDuration socketTimeout = Max(sockTimeout, TDuration::MilliSeconds(1)); // timeout less than 1ms will be interpreted as 0 in SetSocketTimeout() call below and will result in infinite wait
+
+ ui32 seconds = socketTimeout.Seconds();
+ ui32 milliSeconds = (socketTimeout - TDuration::Seconds(seconds)).MilliSeconds();
+ socket.SetSocketTimeout(seconds, milliSeconds);
+ return socket;
+ } catch (const yexception& e) {
+ ythrow THttpRequestException() << "Connect to " << host << ':' << port << " failed: " << e.what();
+ }
+ }
+}
+
+void TSimpleHttpClient::ProcessResponse(const TStringBuf relativeUrl, THttpInput& input, IOutputStream*, const unsigned statusCode) const {
+ if (!(statusCode >= 200 && statusCode < 300)) {
+ TString rest = input.ReadAll();
+ ythrow THttpRequestException(statusCode) << "Got " << statusCode << " at " << Host << relativeUrl << "\nFull http response:\n"
+ << rest;
+ }
+}
+
+TSimpleHttpClient::~TSimpleHttpClient() {
+}
+
+TKeepAliveHttpClient TSimpleHttpClient::CreateClient() const {
+ TKeepAliveHttpClient cl(Host, Port, SocketTimeout, ConnectTimeout);
+
+ if (!HttpsVerification) {
+ cl.DisableVerificationForHttps();
+ }
+
+ PrepareClient(cl);
+
+ return cl;
+}
+
+void TSimpleHttpClient::PrepareClient(TKeepAliveHttpClient&) const {
+}
+
+TRedirectableHttpClient::TRedirectableHttpClient(const TString& host, ui32 port, TDuration socketTimeout, TDuration connectTimeout)
+ : TSimpleHttpClient(host, port, socketTimeout, connectTimeout)
+{
+}
+
+void TRedirectableHttpClient::PrepareClient(TKeepAliveHttpClient& cl) const {
+ cl.IfResponseRequired = [](const THttpInput& input) {
+ return !input.Headers().HasHeader("Location");
+ };
+}
+
+void TRedirectableHttpClient::ProcessResponse(const TStringBuf relativeUrl, THttpInput& input, IOutputStream* output, const unsigned statusCode) const {
+ for (auto i = input.Headers().Begin(), e = input.Headers().End(); i != e; ++i) {
+ if (0 == TString::compare(i->Name(), TStringBuf("Location"))) {
+ TVector<TString> request_url_parts, request_body_parts;
+
+ size_t splitted_index = 0;
+ for (auto& iter : StringSplitter(i->Value()).Split('/')) {
+ if (splitted_index < 3) {
+ request_url_parts.push_back(TString(iter.Token()));
+ } else {
+ request_body_parts.push_back(TString(iter.Token()));
+ }
+ ++splitted_index;
+ }
+
+ TString url = JoinSeq("/", request_url_parts);
+ ui16 port = 443;
+
+ THttpURL u;
+ if (THttpURL::ParsedOK == u.Parse(url)) {
+ const char* p = u.Get(THttpURL::FieldPort);
+ if (p) {
+ port = FromString<ui16>(p);
+ url = u.PrintS(THttpURL::FlagScheme | THttpURL::FlagHost);
+ }
+ }
+
+ TRedirectableHttpClient cl(url, port, TDuration::Seconds(60), TDuration::Seconds(60));
+ if (HttpsVerification) {
+ cl.EnableVerificationForHttps();
+ }
+ cl.DoGet(TString("/") + JoinSeq("/", request_body_parts), output);
+ return;
+ }
+ }
+ if (!(statusCode >= 200 && statusCode < 300)) {
+ TString rest = input.ReadAll();
+ ythrow THttpRequestException(statusCode) << "Got " << statusCode << " at " << Host << relativeUrl << "\nFull http response:\n"
+ << rest;
+ }
+ TransferData(&input, output);
+}
diff --git a/library/cpp/http/simple/http_client.h b/library/cpp/http/simple/http_client.h
new file mode 100644
index 0000000000..94ee487202
--- /dev/null
+++ b/library/cpp/http/simple/http_client.h
@@ -0,0 +1,276 @@
+#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:
+ 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;
+};
+
+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_FAIL(); // We should never be here.
+ return 0;
+}
diff --git a/library/cpp/http/simple/http_client_options.h b/library/cpp/http/simple/http_client_options.h
new file mode 100644
index 0000000000..f2e964a462
--- /dev/null
+++ b/library/cpp/http/simple/http_client_options.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include <util/datetime/base.h>
+#include <library/cpp/string_utils/url/url.h>
+
+class TSimpleHttpClientOptions {
+ using TSelf = TSimpleHttpClientOptions;
+
+public:
+ TSimpleHttpClientOptions() = default;
+
+ explicit TSimpleHttpClientOptions(TStringBuf url) {
+ TStringBuf scheme, host;
+ GetSchemeHostAndPort(url, scheme, host, Port_);
+ Host_ = url.Head(scheme.size() + host.size());
+ }
+
+ TSelf& Host(TStringBuf host) {
+ Host_ = host;
+ return *this;
+ }
+
+ const TString& Host() const noexcept {
+ return Host_;
+ }
+
+ TSelf& Port(ui16 port) {
+ Port_ = port;
+ return *this;
+ }
+
+ ui16 Port() const noexcept {
+ return Port_;
+ }
+
+ TSelf& SocketTimeout(TDuration timeout) {
+ SocketTimeout_ = timeout;
+ return *this;
+ }
+
+ TDuration SocketTimeout() const noexcept {
+ return SocketTimeout_;
+ }
+
+ TSelf& ConnectTimeout(TDuration timeout) {
+ ConnectTimeout_ = timeout;
+ return *this;
+ }
+
+ TDuration ConnectTimeout() const noexcept {
+ return ConnectTimeout_;
+ }
+
+private:
+ TString Host_;
+ ui16 Port_;
+ TDuration SocketTimeout_ = TDuration::Seconds(5);
+ TDuration ConnectTimeout_ = TDuration::Seconds(30);
+};
diff --git a/library/cpp/http/simple/ut/http_ut.cpp b/library/cpp/http/simple/ut/http_ut.cpp
new file mode 100644
index 0000000000..bf7e767428
--- /dev/null
+++ b/library/cpp/http/simple/ut/http_ut.cpp
@@ -0,0 +1,439 @@
+#include <library/cpp/http/simple/http_client.h>
+
+#include <library/cpp/http/server/response.h>
+
+#include <library/cpp/testing/mock_server/server.h>
+#include <library/cpp/testing/unittest/registar.h>
+#include <library/cpp/testing/unittest/tests_data.h>
+
+#include <util/system/event.h>
+#include <util/system/thread.h>
+
+#include <thread>
+
+Y_UNIT_TEST_SUITE(SimpleHttp) {
+ static THttpServerOptions createOptions(ui16 port, bool keepAlive) {
+ THttpServerOptions o;
+ o.AddBindAddress("localhost", port);
+ o.SetThreads(1);
+ o.SetMaxConnections(1);
+ o.SetMaxQueueSize(1);
+ o.EnableKeepAlive(keepAlive);
+ return o;
+ }
+
+ class TPong: public TRequestReplier {
+ TDuration Sleep_;
+ ui16 Port_;
+
+ public:
+ TPong(TDuration sleep = TDuration(), ui16 port = 80)
+ : Sleep_(sleep)
+ , Port_(port)
+ {
+ }
+
+ bool DoReply(const TReplyParams& params) override {
+ TStringBuf path = TParsedHttpFull(params.Input.FirstLine()).Path;
+ params.Input.ReadAll();
+ if (path == "/redirect") {
+ params.Output << "HTTP/1.1 307 Internal Redirect\r\n"
+ "Location: http://localhost:"
+ << Port_
+ << "/redirect2?some_param=qwe\r\n"
+ "Non-Authoritative-Reason: HSTS\r\n\r\n"
+ "must be missing";
+ return true;
+ }
+
+ if (path == "/redirect2") {
+ UNIT_ASSERT_VALUES_EQUAL("some_param=qwe", TParsedHttpFull(params.Input.FirstLine()).Cgi);
+ params.Output << "HTTP/1.1 307 Internal Redirect\r\n"
+ "Location: http://localhost:"
+ << Port_
+ << "/ping\r\n"
+ "Non-Authoritative-Reason: HSTS\r\n\r\n"
+ "must be missing too";
+ return true;
+ }
+
+ if (path != "/ping") {
+ UNIT_ASSERT_C(false, "path is incorrect: '" << path << "'");
+ }
+
+ Sleep(Sleep_);
+
+ THttpResponse resp(HTTP_OK);
+ resp.SetContent("pong");
+ resp.OutTo(params.Output);
+
+ return true;
+ }
+ };
+
+ class TCodedPong: public TRequestReplier {
+ HttpCodes Code_;
+
+ public:
+ TCodedPong(HttpCodes code)
+ : Code_(code)
+ {
+ }
+
+ bool DoReply(const TReplyParams& params) override {
+ if (TParsedHttpFull(params.Input.FirstLine()).Path != "/ping") {
+ UNIT_ASSERT(false);
+ }
+
+ THttpResponse resp(Code_);
+ resp.SetContent("pong");
+ resp.OutTo(params.Output);
+
+ return true;
+ }
+ };
+
+ class T500: public TRequestReplier {
+ ui16 Port_;
+
+ public:
+ T500(ui16 port)
+ : Port_(port)
+ {
+ }
+
+ bool DoReply(const TReplyParams& params) override {
+ TStringBuf path = TParsedHttpFull(params.Input.FirstLine()).Path;
+
+ if (path == "/bad_redirect") {
+ params.Output << "HTTP/1.1 500 Internal Redirect\r\n"
+ "Location: http://localhost:1/qwerty\r\n"
+ "Non-Authoritative-Reason: HSTS\r\n\r\n";
+ return true;
+ }
+
+ if (path == "/redirect_to_500") {
+ params.Output << "HTTP/1.1 307 Internal Redirect\r\n"
+ "Location: http://localhost:"
+ << Port_
+ << "/500\r\n"
+ "Non-Authoritative-Reason: HSTS\r\n\r\n";
+ return true;
+ }
+
+ THttpResponse resp(HTTP_INTERNAL_SERVER_ERROR);
+ resp.SetContent("bang");
+ resp.OutTo(params.Output);
+
+ return true;
+ }
+ };
+
+ Y_UNIT_TEST(simpleSuccessful) {
+ TPortManager pm;
+ ui16 port = pm.GetPort(80);
+ NMock::TMockServer server(createOptions(port, false), []() { return new TPong; });
+
+ TSimpleHttpClient cl("localhost", port);
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+
+ {
+ TStringStream s;
+ UNIT_ASSERT_NO_EXCEPTION(cl.DoGet("/ping", &s));
+ UNIT_ASSERT_VALUES_EQUAL("pong", s.Str());
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+ }
+ {
+ TStringStream s;
+ UNIT_ASSERT_NO_EXCEPTION(cl.DoGet("/ping", &s));
+ UNIT_ASSERT_VALUES_EQUAL("pong", s.Str());
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+ }
+
+ {
+ TStringStream s;
+ UNIT_ASSERT_NO_EXCEPTION(cl.DoPost("/ping", "", &s));
+ UNIT_ASSERT_VALUES_EQUAL("pong", s.Str());
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+ }
+ {
+ TStringStream s;
+ UNIT_ASSERT_NO_EXCEPTION(cl.DoPost("/ping", "", &s));
+ UNIT_ASSERT_VALUES_EQUAL("pong", s.Str());
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+ }
+ }
+
+ Y_UNIT_TEST(simpleMessages) {
+ TPortManager pm;
+ ui16 port = pm.GetPort(80);
+ NMock::TMockServer server(createOptions(port, false), []() { return new TPong; });
+
+ TSimpleHttpClient cl("localhost", port);
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+
+ {
+ TStringStream s;
+ UNIT_ASSERT_NO_EXCEPTION(cl.DoGet("/ping", &s));
+ UNIT_ASSERT_VALUES_EQUAL("pong", s.Str());
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+ }
+ {
+ UNIT_ASSERT_NO_EXCEPTION(cl.DoGet("/ping", nullptr));
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+ }
+
+ server.SetGenerator([]() { return new TCodedPong(HTTP_CONTINUE); });
+ {
+ TStringStream s;
+ UNIT_ASSERT_EXCEPTION_CONTAINS(cl.DoPost("/ping", "", &s),
+ THttpRequestException,
+ "Got 100 at localhost/ping\n"
+ "Full http response:\n");
+ UNIT_ASSERT_VALUES_EQUAL("pong", s.Str());
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+ }
+ {
+ UNIT_ASSERT_EXCEPTION_CONTAINS(cl.DoPost("/ping", "", nullptr),
+ THttpRequestException,
+ "Got 100 at localhost/ping\n"
+ "Full http response:\n"
+ "pong");
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+ }
+ }
+
+ Y_UNIT_TEST(simpleTimeout) {
+ TPortManager pm;
+ ui16 port = pm.GetPort(80);
+ NMock::TMockServer server(createOptions(port, true), []() { return new TPong(TDuration::MilliSeconds(300)); });
+
+ TSimpleHttpClient cl("localhost", port, TDuration::MilliSeconds(50), TDuration::MilliSeconds(50));
+
+ TStringStream s;
+ UNIT_ASSERT_EXCEPTION_CONTAINS(cl.DoGet("/ping", &s),
+ TSystemError,
+ "Resource temporarily unavailable");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(cl.DoPost("/ping", "", &s),
+ TSystemError,
+ "Resource temporarily unavailable");
+ }
+
+ Y_UNIT_TEST(simpleError) {
+ TPortManager pm;
+ ui16 port = pm.GetPort(80);
+ NMock::TMockServer server(createOptions(port, true), []() { return new TPong; });
+
+ TSimpleHttpClient cl("localhost", port);
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+
+ {
+ TStringStream s;
+ server.SetGenerator([]() { return new TCodedPong(HTTP_CONTINUE); });
+ UNIT_ASSERT_EXCEPTION_CONTAINS(cl.DoGet("/ping", &s),
+ THttpRequestException,
+ "Got 100 at localhost/ping\n"
+ "Full http response:");
+ UNIT_ASSERT_VALUES_EQUAL("pong", s.Str());
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+ }
+
+ {
+ TStringStream s;
+ server.SetGenerator([]() { return new TCodedPong(HTTP_OK); });
+ UNIT_ASSERT_NO_EXCEPTION(cl.DoGet("/ping", &s));
+ UNIT_ASSERT_VALUES_EQUAL("pong", s.Str());
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+
+ server.SetGenerator([]() { return new TCodedPong(HTTP_PARTIAL_CONTENT); });
+ UNIT_ASSERT_NO_EXCEPTION(cl.DoGet("/ping", &s));
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+ }
+
+ {
+ TStringStream s;
+ server.SetGenerator([]() { return new TCodedPong(HTTP_MULTIPLE_CHOICES); });
+ UNIT_ASSERT_EXCEPTION_CONTAINS(cl.DoGet("/ping", &s),
+ THttpRequestException,
+ "Got 300 at localhost/ping\n"
+ "Full http response:");
+ UNIT_ASSERT_VALUES_EQUAL("pong", s.Str());
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+ }
+ }
+
+ Y_UNIT_TEST(redirectable) {
+ TPortManager pm;
+ ui16 port = pm.GetPort(80);
+ NMock::TMockServer server(createOptions(port, true), [port]() { return new TPong(TDuration(), port); });
+
+ TRedirectableHttpClient cl("localhost", port);
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+
+ {
+ TStringStream s;
+ UNIT_ASSERT_NO_EXCEPTION(cl.DoGet("/redirect", &s));
+ UNIT_ASSERT_VALUES_EQUAL("pong", s.Str());
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+ }
+
+ server.SetGenerator([port]() { return new T500(port); });
+
+ TStringStream s;
+ UNIT_ASSERT_EXCEPTION_CONTAINS(cl.DoGet("/bad_redirect", &s),
+ THttpRequestException,
+ "can not connect to ");
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+
+ UNIT_ASSERT_EXCEPTION_CONTAINS(cl.DoGet("/redirect_to_500", &s),
+ THttpRequestException,
+ "Got 500 at http://localhost/500\n"
+ "Full http response:\n");
+ UNIT_ASSERT_VALUES_EQUAL("bang", s.Str());
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+ }
+
+ Y_UNIT_TEST(keepaliveSuccessful) {
+ auto test = [](bool keepalive, i64 clientCount) {
+ TPortManager pm;
+ ui16 port = pm.GetPort(80);
+ NMock::TMockServer server(createOptions(port, keepalive), []() { return new TPong; });
+
+ TKeepAliveHttpClient cl("localhost", port);
+ UNIT_ASSERT_VALUES_EQUAL(0, server.GetClientCount());
+ {
+ TStringStream s;
+ int code = -1;
+ UNIT_ASSERT_NO_EXCEPTION_C(code = cl.DoGet("/ping", &s), keepalive);
+ UNIT_ASSERT_VALUES_EQUAL_C(200, code, keepalive);
+ UNIT_ASSERT_VALUES_EQUAL_C("pong", s.Str(), keepalive);
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(clientCount, server.GetClientCount());
+ }
+ {
+ TStringStream s;
+ int code = -1;
+ UNIT_ASSERT_NO_EXCEPTION_C(code = cl.DoGet("/ping", &s), keepalive);
+ UNIT_ASSERT_VALUES_EQUAL_C(200, code, keepalive);
+ UNIT_ASSERT_VALUES_EQUAL_C("pong", s.Str(), keepalive);
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(clientCount, server.GetClientCount());
+ }
+
+ {
+ TStringStream s;
+ int code = -1;
+ UNIT_ASSERT_NO_EXCEPTION_C(code = cl.DoPost("/ping", "", &s), keepalive);
+ UNIT_ASSERT_VALUES_EQUAL_C(200, code, keepalive);
+ UNIT_ASSERT_VALUES_EQUAL_C("pong", s.Str(), keepalive);
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(clientCount, server.GetClientCount());
+ }
+ {
+ TStringStream s;
+ int code = -1;
+ UNIT_ASSERT_NO_EXCEPTION_C(code = cl.DoPost("/ping", "", &s), keepalive);
+ UNIT_ASSERT_VALUES_EQUAL_C(200, code, keepalive);
+ UNIT_ASSERT_VALUES_EQUAL_C("pong", s.Str(), keepalive);
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_VALUES_EQUAL(clientCount, server.GetClientCount());
+ }
+ };
+
+ test(true, 1);
+ test(false, 0);
+ }
+
+ Y_UNIT_TEST(keepaliveTimeout) {
+ TPortManager pm;
+ ui16 port = pm.GetPort(80);
+ NMock::TMockServer server(createOptions(port, true), []() { return new TPong(TDuration::MilliSeconds(300)); });
+
+ TKeepAliveHttpClient cl("localhost", port, TDuration::MilliSeconds(50), TDuration::MilliSeconds(50));
+
+ TStringStream s;
+ UNIT_ASSERT_EXCEPTION_CONTAINS(cl.DoGet("/ping", &s),
+ TSystemError,
+ "Resource temporarily unavailable");
+ UNIT_ASSERT_EXCEPTION_CONTAINS(cl.DoPost("/ping", "", &s),
+ TSystemError,
+ "Resource temporarily unavailable");
+ }
+
+ Y_UNIT_TEST(keepaliveHeaders) {
+ TPortManager pm;
+ ui16 port = pm.GetPort(80);
+ NMock::TMockServer server(createOptions(port, true), []() { return new TPong; });
+
+ TKeepAliveHttpClient cl("localhost", port);
+
+ TStringStream s;
+ THttpHeaders h;
+ UNIT_ASSERT_VALUES_EQUAL(200, cl.DoGet("/ping", &s, {}, &h));
+ TStringStream hs;
+ h.OutTo(&hs);
+ UNIT_ASSERT_VALUES_EQUAL("Content-Length: 4\r\nConnection: Keep-Alive\r\n", hs.Str());
+ }
+
+ Y_UNIT_TEST(keepaliveRaw) {
+ TPortManager pm;
+ ui16 port = pm.GetPort(80);
+ NMock::TMockServer server(createOptions(port, true), []() { return new TPong; });
+
+ TKeepAliveHttpClient cl("localhost", port);
+
+ TStringStream s;
+ THttpHeaders h;
+
+ TString raw = "POST /ping HTTP/1.1\r\n"
+ "Connection: Keep-Alive\r\n"
+ "Accept-Encoding: gzip, deflate\r\n"
+ "Content-Length: 9\r\n"
+ "Content-Type: application/x-www-form-urlencoded\r\n"
+ "User-Agent: Python-urllib/2.6\r\n"
+ "\r\n"
+ "some body";
+
+ UNIT_ASSERT_VALUES_EQUAL(200, cl.DoRequestRaw(raw, &s, &h));
+ TStringStream hs;
+ h.OutTo(&hs);
+ UNIT_ASSERT_VALUES_EQUAL("Content-Length: 4\r\nConnection: Keep-Alive\r\n", hs.Str());
+
+ raw = "GET /ping HT TP/1.1\r\n";
+ UNIT_ASSERT_EXCEPTION_CONTAINS(cl.DoRequestRaw(raw, &s, &h), TSystemError, "can not read from socket input stream");
+ }
+
+ Y_UNIT_TEST(keepaliveWithClosedByPeer) {
+ TPortManager pm;
+ ui16 port = pm.GetPort(80);
+ NMock::TMockServer::TGenerator gen = []() { return new TPong; };
+ THolder<NMock::TMockServer> server = MakeHolder<NMock::TMockServer>(createOptions(port, true), gen);
+
+ TKeepAliveHttpClient cl("localhost", port);
+ UNIT_ASSERT_NO_EXCEPTION(cl.DoGet("/ping"));
+
+ server.Reset();
+ server = MakeHolder<NMock::TMockServer>(createOptions(port, true), gen);
+ UNIT_ASSERT_NO_EXCEPTION(cl.DoGet("/ping"));
+
+ TKeepAliveHttpClient cl2("localhost", port);
+ UNIT_ASSERT_NO_EXCEPTION(cl2.DoGet("/ping"));
+ Sleep(TDuration::MilliSeconds(500));
+ UNIT_ASSERT_NO_EXCEPTION(cl.DoGet("/ping"));
+ }
+}
diff --git a/library/cpp/http/simple/ut/https_server/http_server.crt b/library/cpp/http/simple/ut/https_server/http_server.crt
new file mode 100644
index 0000000000..74d74fafea
--- /dev/null
+++ b/library/cpp/http/simple/ut/https_server/http_server.crt
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDATCCAemgAwIBAgIJAKnfUOUcLEqUMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV
+BAMMDGxvY2FsaG9zdC5teTAeFw0xODA1MDgwOTIxMDZaFw0xOTA1MDgwOTIxMDZa
+MBcxFTATBgNVBAMMDGxvY2FsaG9zdC5teTCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAMVe3pFwlPCrniAAsDyhoolnwv0gOQ4SX81nA0NggabKbUBJwwfN
+nKP5dvFNHCo100fzoiWbFmZnu9pUMtjeucQzaA38i501rXCkiPmTkE+tDdIJqO8J
+lLV+oaNvFtaAVcRIiuU9fTp/MdZhG3tLj/AXx9dcc1xHRjg/tngepAsvZ2oRoBVU
+ijvkOSCm1xwew+ZTzazLARnLOvHok1tJPepMCVlGaEaL9r1aJ86hMUSg+sli2ayW
+myI4Pt7ZrsyrHpHDYF9ecWWGbmHfgLdaAdyulrPuvtwavl6KtgSuy3SxwigOfdBI
+h4Xw2u6gq4v40OuZGWgkNdJ000ddwurWfosCAwEAAaNQME4wHQYDVR0OBBYEFAd+
+0uv5elelwrjB/0C7EDO7VauqMB8GA1UdIwQYMBaAFAd+0uv5elelwrjB/0C7EDO7
+VauqMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEauDMNWqCIIZXmY
+HLqkoPmy+BDX7N4F2ZuWntes8D/igFhZOYQfD+ksJEv3zgs6N5Qd8HbSCbZR0Hh+
+1g+RjVBu8T67h6+vIDZuu0jORjknUp2XbD+aWG+7UcuUjDY8KF9St50ZniSieiSA
+dV09VrJ/JFwxaeFzgOHnk9oP5eggwZjEZJqSc4qzL0JlhFcxV8R4OVUCjRyHG73p
+cN7nUDL9xN5XZY+6t6+rzdYi4UAhEW0odFVfyXqhOhupSgQkBBdIjxVuov+3h/aV
+D2YweTg6cKtuaISsFmDEPht7cVQuy5z3PPkV6kQBeECA9vTFP3wCxA0n7Iyyn2IK
+8gvWZXk=
+-----END CERTIFICATE-----
diff --git a/library/cpp/http/simple/ut/https_server/http_server.key b/library/cpp/http/simple/ut/https_server/http_server.key
new file mode 100644
index 0000000000..f58ab049fd
--- /dev/null
+++ b/library/cpp/http/simple/ut/https_server/http_server.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDFXt6RcJTwq54g
+ALA8oaKJZ8L9IDkOEl/NZwNDYIGmym1AScMHzZyj+XbxTRwqNdNH86IlmxZmZ7va
+VDLY3rnEM2gN/IudNa1wpIj5k5BPrQ3SCajvCZS1fqGjbxbWgFXESIrlPX06fzHW
+YRt7S4/wF8fXXHNcR0Y4P7Z4HqQLL2dqEaAVVIo75DkgptccHsPmU82sywEZyzrx
+6JNbST3qTAlZRmhGi/a9WifOoTFEoPrJYtmslpsiOD7e2a7Mqx6Rw2BfXnFlhm5h
+34C3WgHcrpaz7r7cGr5eirYErst0scIoDn3QSIeF8NruoKuL+NDrmRloJDXSdNNH
+XcLq1n6LAgMBAAECggEAdN+wvD8Gc12szRabRcwRC3y+IlYqcwK+aEtPy14iaDoG
+Z8NGEiDXWOIoZMtcmkI1Uq4anlov8YQL4UVqtrFtH5mxTFb39agLhGBqHCAdnJDF
+VlMSDjqGLNNHtBfcVji4kPrEBOtcdH9Na70lIOWl3m62j/jW9xXdpwFTc93xFg14
+Ivtjtv7KHZAPgN0pdgsqen1js6Z3O5tkcy4yFLldBl+8/ZbYSMM+Rh4GbR5qvWfA
+23vBu9EprJKPhFQlNZPbesEKe8EA+SCuLo0RzAZq1E2nZRH0HasKT2hhr/kobkN6
+oLIo2dNgIYL7xMhHLcBt1/08CXKZIqEAfA9Tx/eVgQKBgQD7/oN/XA0pMVCqS8f6
+8Z9VI4NxHJoPFLskrXjXhbxgLgUjuz28cuoyaIKcv8f9Qo7f+hOwR2F3zjwyVexB
+G+0fuyIbqD8Po43F+SBJCVSE3EV5k0AQJJN74a+UuKC39NhGaXsmj+s6ArWrURV5
+thay+308pF5HvYCnmQD3UfOJiQKBgQDIghDarcID6/Q0Nv8xvfd8p9kUu5vX/Tw0
+W22JDDMxpUoYCGXvOEx+IoVzqLOTw+NcEXSmDA41VqXlphYopwZkfNV6kIXVymdu
+oNKisgfe4Hrfrq9BUl5p8gvU/Ev5zY7N4kVirUJgNvRHDElp8h6Ek/KRTv8Q0xRX
+ZW6UqmKGcwKBgDsQZ7/1UnxiO7b+tivicGcjQM7FVnLMeCTbqCRUC1g70SaT35+J
+C82u41ZcOULqU9S5p928jWLoawGdVBfatNSoJxF2ePlwa22IvAGCd1YAzyP02KIw
+AIWb22yvbbRQZlTyqlPajdb2BaDXC4KQpHdlLPCG0jZce4hM+4X8pmmJAoGALW4S
+5YlTGVJf7Wi8n4ecSJk7PVBYujJ9bpt8kP27p7b8t79HYVFPO5EUzaTes09B931Z
+AbpficRNKGBeSu21LBWAxRlzyYHnt5AmyYgu8lfIX2AUA2fnTnfyKFrV2A60GX/4
+GqiJDoXFCUgGZkPemElxP203q5c316l6yaJlWnMCgYAqk1G65THRmdTKcnUEOqo8
+pD3SWuBvbOHYLyg+f0zNAqpnTFbaPVmsWfx3CsX2m8WdH3dD28SGfvepQlWj1yp/
+TmXs14nFUuJWir2VbPgp8W/uZl8bQ0YlI8UPUbN3XbLkVIno+jXuUopcgrXmi7Gb
+Y2QnQfHePgpszWR0o+WiYg==
+-----END PRIVATE KEY-----
diff --git a/library/cpp/http/simple/ut/https_server/main.go b/library/cpp/http/simple/ut/https_server/main.go
new file mode 100644
index 0000000000..4282810675
--- /dev/null
+++ b/library/cpp/http/simple/ut/https_server/main.go
@@ -0,0 +1,70 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/pflag"
+)
+
+type Opts struct {
+ Port uint16
+ KeyFile string
+ CertFile string
+}
+
+func handler(writer http.ResponseWriter, request *http.Request) {
+ res := "pong.my"
+
+ writer.Header().Set("Content-Type", "text/plain")
+ writer.WriteHeader(http.StatusOK)
+
+ _, _ = writer.Write([]byte(res))
+}
+
+func runServer(opts *Opts) error {
+ mainMux := http.NewServeMux()
+ mainMux.Handle("/ping", http.HandlerFunc(handler))
+
+ server := &http.Server{
+ Addr: fmt.Sprintf("localhost:%d", opts.Port),
+ Handler: mainMux,
+ ErrorLog: log.New(os.Stdout, "", log.LstdFlags),
+ }
+
+ return server.ListenAndServeTLS(opts.CertFile, opts.KeyFile)
+}
+
+func markFlagRequired(flags *pflag.FlagSet, names ...string) {
+ for _, n := range names {
+ name := n
+ if err := cobra.MarkFlagRequired(flags, name); err != nil {
+ panic(err)
+ }
+ }
+}
+
+func main() {
+ opts := Opts{}
+
+ cmd := cobra.Command{
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return runServer(&opts)
+ },
+ }
+
+ flags := cmd.Flags()
+ flags.Uint16Var(&opts.Port, "port", 0, "")
+ flags.StringVar(&opts.KeyFile, "keyfile", "", "path to key file")
+ flags.StringVar(&opts.CertFile, "certfile", "", "path to cert file")
+
+ markFlagRequired(flags, "port", "keyfile", "certfile")
+
+ if err := cmd.Execute(); err != nil {
+ _, _ = fmt.Fprintf(os.Stderr, "Exit with err: %s", err)
+ os.Exit(1)
+ }
+}
diff --git a/library/cpp/http/simple/ut/https_ut.cpp b/library/cpp/http/simple/ut/https_ut.cpp
new file mode 100644
index 0000000000..3849b9ac9a
--- /dev/null
+++ b/library/cpp/http/simple/ut/https_ut.cpp
@@ -0,0 +1,97 @@
+#include <library/cpp/http/simple/http_client.h>
+
+#include <library/cpp/http/server/response.h>
+
+#include <library/cpp/testing/unittest/registar.h>
+#include <library/cpp/testing/unittest/tests_data.h>
+
+#include <util/system/shellcommand.h>
+
+Y_UNIT_TEST_SUITE(Https) {
+ using TShellCommandPtr = std::unique_ptr<TShellCommand>;
+
+ static TShellCommandPtr start(ui16 port) {
+ const TString data = ArcadiaSourceRoot() + "/library/cpp/http/simple/ut/https_server";
+
+ const TString command =
+ TStringBuilder()
+ << BuildRoot() << "/library/cpp/http/simple/ut/https_server/https_server"
+ << " --port " << port
+ << " --keyfile " << data << "/http_server.key"
+ << " --certfile " << data << "/http_server.crt";
+
+ auto res = std::make_unique<TShellCommand>(
+ command,
+ TShellCommandOptions()
+ .SetAsync(true)
+ .SetLatency(50)
+ .SetErrorStream(&Cerr));
+
+ res->Run();
+
+ i32 tries = 100000;
+ while (tries-- > 0) {
+ try {
+ TKeepAliveHttpClient client("https://localhost", port);
+ client.DisableVerificationForHttps();
+ client.DoGet("/ping");
+ break;
+ } catch (const std::exception& e) {
+ Cout << "== failed to connect to new server: " << e.what() << Endl;
+ Sleep(TDuration::MilliSeconds(1));
+ }
+ }
+
+ return res;
+ }
+
+ static void get(TKeepAliveHttpClient & client) {
+ TStringStream out;
+ ui32 code = 0;
+
+ UNIT_ASSERT_NO_EXCEPTION(code = client.DoGet("/ping", &out));
+ UNIT_ASSERT_VALUES_EQUAL_C(code, 200, out.Str());
+ UNIT_ASSERT_VALUES_EQUAL(out.Str(), "pong.my");
+ }
+
+ Y_UNIT_TEST(keepAlive) {
+ TPortManager pm;
+ ui16 port = pm.GetPort(443);
+ TShellCommandPtr httpsServer = start(port);
+
+ TKeepAliveHttpClient client("https://localhost",
+ port,
+ TDuration::Seconds(40),
+ TDuration::Seconds(40));
+ client.DisableVerificationForHttps();
+
+ get(client);
+ get(client);
+
+ httpsServer->Terminate().Wait();
+ httpsServer = start(port);
+
+ get(client);
+ }
+
+ static void get(TSimpleHttpClient & client) {
+ TStringStream out;
+
+ UNIT_ASSERT_NO_EXCEPTION_C(client.DoGet("/ping", &out), out.Str());
+ UNIT_ASSERT_VALUES_EQUAL(out.Str(), "pong.my");
+ }
+
+ Y_UNIT_TEST(simple) {
+ TPortManager pm;
+ ui16 port = pm.GetPort(443);
+ TShellCommandPtr httpsServer = start(port);
+
+ TSimpleHttpClient client("https://localhost",
+ port,
+ TDuration::Seconds(40),
+ TDuration::Seconds(40));
+
+ get(client);
+ get(client);
+ }
+}