diff options
author | Daniil Cherednik <dan.cherednik@gmail.com> | 2022-11-24 13:14:34 +0300 |
---|---|---|
committer | Daniil Cherednik <dan.cherednik@gmail.com> | 2022-11-24 14:46:00 +0300 |
commit | 87f7fceed34bcafb8aaff351dd493a35c916986f (patch) | |
tree | 26809ec8f550aba8eb019e59adc3d48e51913eb2 /library/cpp/http | |
parent | 11bc4015b8010ae201bf3eb33db7dba425aca35e (diff) | |
download | ydb-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.txt | 1 | ||||
-rw-r--r-- | library/cpp/http/simple/CMakeLists.txt | 21 | ||||
-rw-r--r-- | library/cpp/http/simple/http_client.cpp | 356 | ||||
-rw-r--r-- | library/cpp/http/simple/http_client.h | 276 | ||||
-rw-r--r-- | library/cpp/http/simple/http_client_options.h | 59 | ||||
-rw-r--r-- | library/cpp/http/simple/ut/http_ut.cpp | 439 | ||||
-rw-r--r-- | library/cpp/http/simple/ut/https_server/http_server.crt | 19 | ||||
-rw-r--r-- | library/cpp/http/simple/ut/https_server/http_server.key | 28 | ||||
-rw-r--r-- | library/cpp/http/simple/ut/https_server/main.go | 70 | ||||
-rw-r--r-- | library/cpp/http/simple/ut/https_ut.cpp | 97 |
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); + } +} |