diff --git a/README.md b/README.md index 7a3deb3..bb7ae48 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ Please refer to the [tutorial](doc/Tutorial.md) for more examples. - Write directly to the outgoing DataWriter when data is required. - Just provide a C++ object and let the library serialize it directly to the wire. - HTTP Proxy support -- SOCKS5 Proxy support (naive implementatin for now, no support for authentication). +- HTTPS Proxy (using CONNECT method), SOCKS5 Proxy support (naive implementatin for now, no support for authentication). # Current Status The project has been in public BETA since April 11th 2017. @@ -262,7 +262,7 @@ These are the operating systems where my Continues Integration (Jenkins) servers - Ubuntu Xenial (LTS) - Ubuntu Jammy (LTS) -Support for MacOS has been removed after Apples announcement that their love for privacy was just +Support for MacOS has been removed after Apples announcement that their love for privacy was just a marketing gimmick. Fedora is currently disabled in my CI because of failures to start their Docker containers. (Work in progress). diff --git a/ci/mock-backends/docker-compose.yml b/ci/mock-backends/docker-compose.yml index 33b9acd..197e5c5 100644 --- a/ci/mock-backends/docker-compose.yml +++ b/ci/mock-backends/docker-compose.yml @@ -8,6 +8,7 @@ services: build: nginx ports: - "3001:80" + - "3002:443" links: - "json:api" squid: diff --git a/ci/mock-backends/nginx/Dockerfile b/ci/mock-backends/nginx/Dockerfile index 48cc9e7..a6e1c2c 100644 --- a/ci/mock-backends/nginx/Dockerfile +++ b/ci/mock-backends/nginx/Dockerfile @@ -1,7 +1,10 @@ FROM nginx RUN rm /etc/nginx/conf.d/default.conf COPY proxy.conf.bin /etc/nginx/conf.d/proxy.conf +COPY proxy-https.conf.bin /etc/nginx/conf.d/proxy-https.conf COPY htpasswd.bin /etc/nginx/htpasswd +COPY test.pem /etc/nginx/ssl/test.pem +COPY test-key.pem /etc/nginx/ssl/test-key.pem RUN mkdir -p /etc/nginx/html/upload RUN chmod 777 /etc/nginx/html/upload diff --git a/ci/mock-backends/nginx/create_cert.sh b/ci/mock-backends/nginx/create_cert.sh new file mode 100755 index 0000000..d383304 --- /dev/null +++ b/ci/mock-backends/nginx/create_cert.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +openssl req -x509 -newkey rsa:4096 -keyout test-key.pem -out test.pem -sha256 -days 3650 -nodes -subj "/C=XX/ST=Test/L=Test/OU=Test/O=Test/CN=localhost" + +#openssl x509 -in test.pem -text diff --git a/ci/mock-backends/nginx/proxy-https.conf.bin b/ci/mock-backends/nginx/proxy-https.conf.bin new file mode 100644 index 0000000..3092d25 --- /dev/null +++ b/ci/mock-backends/nginx/proxy-https.conf.bin @@ -0,0 +1,62 @@ +server { + listen 443 ssl; + server_name localhost; + gzip on; + gzip_proxied any; + ssl_certificate /etc/nginx/ssl/test.pem; + ssl_certificate_key /etc/nginx/ssl/test-key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + + + location ~ ^/cookies(/?)(.*)$ { + add_header Content-Type "application/json; charset=utf-8"; + add_header Set-Cookie test1=yes; + add_header Set-Cookie test2=maybe; + add_header Set-Cookie test3=no; + return 200 '{}'; + } + + location ~ ^/normal(/?)(.*)$ { + proxy_pass http://json/$2$is_args$args; + } + + location ~ ^/close(/?)(.*)$ { + proxy_pass http://json/$2$is_args$args; + keepalive_timeout 0; + } + + # Force nginx to resolve 'json' + location /dns_workaround { + proxy_pass http://json; + } + + location ~ ^/loop(/?)(.*)$ { + return 301 $scheme://$host:3001/loop/$2$is_args$args; + } + + location ~ ^/redirect(/?)(.*)$ { + return 301 $scheme://$host:3001/normal/$2$is_args$args; + } + + location ~ ^/reredirect(/?)(.*)$ { + return 301 $scheme://$host:3001/redirect/$2$is_args$args; + } + + location ~ ^/restricted(/?)(.*)$ { + auth_basic "Restricted Area"; + auth_basic_user_file /etc/nginx/htpasswd; + proxy_pass http://json/$2$is_args$args; + } + + location ~ ^/upload_raw(/?)(.*)$ { + limit_except POST { deny all; } + return 200 "OK"; + } + + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} + diff --git a/ci/mock-backends/nginx/test-key.pem b/ci/mock-backends/nginx/test-key.pem new file mode 100644 index 0000000..8d01ea1 --- /dev/null +++ b/ci/mock-backends/nginx/test-key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +stBsKJU1QibpCwO3/NmBYwjRitw1E2c= +-----END PRIVATE KEY----- diff --git a/ci/mock-backends/nginx/test.pem b/ci/mock-backends/nginx/test.pem new file mode 100644 index 0000000..2fd997a --- /dev/null +++ b/ci/mock-backends/nginx/test.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFmzCCA4OgAwIBAgIUJFXivxxXEIY3rp6fz2wgE3rsQzYwDQYJKoZIhvcNAQEL +BQAwXTELMAkGA1UEBhMCWFgxDTALBgNVBAgMBFRlc3QxDTALBgNVBAcMBFRlc3Qx +DTALBgNVBAsMBFRlc3QxDTALBgNVBAoMBFRlc3QxEjAQBgNVBAMMCWxvY2FsaG9z +dDAeFw0yMzA4MzAxMTM2MjJaFw0zMzA4MjcxMTM2MjJaMF0xCzAJBgNVBAYTAlhY +MQ0wCwYDVQQIDARUZXN0MQ0wCwYDVQQHDARUZXN0MQ0wCwYDVQQLDARUZXN0MQ0w +CwYDVQQKDARUZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDPlSJHl/g59nyiHvVblS5eWCTRxFFueCIEbYGfUdeL ++Jc+YP20fJs3u5Tq7gCvuYqGTMztXfERRk3HNrHy1uXndh0fGVUusn5hYz4XNB8A +mRfZf1wDTpd4SjVe26Da52sIGppj1bGgPJWR9KK+7zDPNeEVQ4HPYnh8rTfHBG0v +YlfHnHR4EGwS6znKA3YIgwHqHIrdm/cOmMK3hDMmjG8l01U5YHIBOadTh1QZvqhp +EToJXnV949CvN12HiyawSQ3nG3HTC94PnrV7UtOLGf3rh+OxN8D9d+QgBVSAI8d7 +VqT+7xxU8wvzL5gWcQLuhY208cZW3Y6gC4U6ZpfHxeqUtAUw4ZK4bJmKr6HFdasX +aPkKPsUZsppeiGYtXRwt6yuHJuI3I1JTwcuASYCHEhus5biMyRZWZo5PLbCX1sXY +bbAEt/qVpg1jeTwrkeZ62lC79lAKjd0runTENMvZYKZwnJntSUz6vq6uMYoM0BzS +MSioFu2fqWBFH9263Tje/wwz/aNNAKx2+3SNavC7//gSM/0gl/9bVRgsWLyxc5qx +CTU8uQ1P5zNx07YNmv6C8UI44craA7KdxcYgqgdUStKDzpDB48eOvvbRRAZO55KA +YzCS/1dgfFUcsugP7DklblI3kTNHTIjarYYXJjUNl0Tpcx3Tk6hfgBBMX1Re/Gtv +TwIDAQABo1MwUTAdBgNVHQ4EFgQUtH5zl0rC27e1JxtdMbIxwqkg0NQwHwYDVR0j +BBgwFoAUtH5zl0rC27e1JxtdMbIxwqkg0NQwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAgEAw7sQ1KIyHCYHJUzYdX9I/l0ylbzrKpfJUABq3OOmgjUM +k6InH6bXvAEP7bj4zC3UDKj1hf5WVHQM0yUeLx8ZmzHzsgfS6C8MiSt+47V0NboQ +5aGHY21ceMivlNDihf0cjPg+h4NsAZkzMWR771Nxe3WLT20Wo1ehKqCjIC7HnCJ3 +IAKbholWJwaiu1Y9tLn21sNMC0Z75hx/JWokP8OuZUyLxsp5RDLLvPA4N41z1Tep +CXpBct6jdRVzq7b5bUgPIQJ4XVdnaIYtdWIZVavulbr0vo4P7dayhf7ZWGcFVyPh +GmjHsPiurZy5BShEOgyYlwrSf9guDJfLR0gclKysKF1I4O9EWysRntvxuDMg0bys +jdUkp25EjQzIP/gA5J6NhTUFYnt04cLIebxs8CU/ZqZ0QZ11wT3VnvTm9ukTsaT4 +lKzmBjlejZVLwlki6r3IAj/CD2v9pJddt950usj9/I9C6quw1XpWG6TYZlCyM9lb +ucUfPttVknOM8c/eenXYAECR/f9MqlFBHO3Bj9YfO4hGTZtE7qaiY2f6yhcLRCOt +Bd649G/bfKT0KvLp+6SdTJxMYFC70FUqoqniEfxT+AhsKP+2k0MfU4MvJ/neLcGA +l/fM+TeIJWTzadl/DCZeE0FyQXAzeqHICnNQOieH6spMpjiXd6cv0aPO1zSqpV4= +-----END CERTIFICATE----- diff --git a/ci/mock-backends/squid/squid.conf.bin b/ci/mock-backends/squid/squid.conf.bin index 5e1aedf..2ce71e2 100644 --- a/ci/mock-backends/squid/squid.conf.bin +++ b/ci/mock-backends/squid/squid.conf.bin @@ -1,5 +1,9 @@ - +reply_header_add Test-proxy "THIS IS TEST PROXY" all +acl CONNECT method CONNECT +acl connect_denied_ports port 444 http_access allow localhost manager http_access deny manager +http_access deny CONNECT connect_denied_ports http_access allow all http_port 3128 + diff --git a/create-and-run-containers.sh b/create-and-run-containers.sh index ba68e84..df88d48 100755 --- a/create-and-run-containers.sh +++ b/create-and-run-containers.sh @@ -13,8 +13,8 @@ else exit -1 fi -docker-compose stop -docker-compose build -docker-compose up -d +docker compose stop +docker compose build +docker compose up -d docker ps popd diff --git a/doc/Tutorial.md b/doc/Tutorial.md index affbb85..dfd2cf1 100644 --- a/doc/Tutorial.md +++ b/doc/Tutorial.md @@ -288,10 +288,12 @@ use the *RequestBuilder*'s *Argument()* method for this. ``` -## Send a request going trough a HTTP Proxy +## Send a request going trough a HTTP or HTTPS Proxy ```C++ // Add the proxy information to the properties used by the client Request::Properties properties; + // Use proxy type Request::Proxy::Type::HTTP for simple HTTP proxing, + // Request::Proxy::Type::HTTPS for HTTPS proxing over CONNECT method properties.proxy.type = Request::Proxy::Type::HTTP; properties.proxy.address = "http://127.0.0.1:3003"; @@ -313,7 +315,7 @@ use the *RequestBuilder*'s *Argument()* method for this. }).get(); ``` -## Use an existing thread in stead of a new worker thread +## Use an existing thread instead of a new worker thread This example is slightly more advanced. Here we take responsibility to run the io-service used internally by diff --git a/include/restc-cpp/logging.h b/include/restc-cpp/logging.h index 7e2515a..54af752 100644 --- a/include/restc-cpp/logging.h +++ b/include/restc-cpp/logging.h @@ -143,10 +143,6 @@ inline void RestcCppTestStartLogger(const std::string& level = "info") { #elif defined RESTC_CPP_LOG_WITH_BOOST_LOG -#ifndef WIN32 -# define BOOST_LOG_DYN_LINK 1 -#endif - #include #include #include diff --git a/include/restc-cpp/restc-cpp.h b/include/restc-cpp/restc-cpp.h index 867035d..0383070 100644 --- a/include/restc-cpp/restc-cpp.h +++ b/include/restc-cpp/restc-cpp.h @@ -66,8 +66,6 @@ class RequestBody; class Connection; class ConnectionPool; class Socket; -class Request; -class Reply; class Context; class DataWriter; @@ -136,11 +134,12 @@ class Request { }; struct Proxy { - enum class Type { NONE, HTTP, SOCKS5 }; + enum class Type { NONE, HTTP, HTTPS, SOCKS5 }; Type type = Type::NONE; std::string address; const std::string& GetName(); + Type detect(); }; using args_t = std::deque; @@ -160,7 +159,7 @@ class Request { class Properties { public: using ptr_t = std::shared_ptr; - using redirect_fn_t = std::function; using general_callback_t = std::function; @@ -417,7 +416,7 @@ class RestClient { done_handler.reset(); }); - return move(future); + return future; } /*! Process from within an existing coroutine */ diff --git a/include/restc-cpp/url_encode.h b/include/restc-cpp/url_encode.h index 9731313..d4415ef 100644 --- a/include/restc-cpp/url_encode.h +++ b/include/restc-cpp/url_encode.h @@ -1,8 +1,5 @@ #pragma once -#ifndef RESTC_CPP_URL_ENCODE_H_ -#define RESTC_CPP_URL_ENCODE_H_ - #include "restc-cpp.h" #include @@ -13,4 +10,3 @@ std::string url_encode(const boost::string_ref& src); } // namespace -#endif // RESTC_CPP_URL_ENCODE_H_ diff --git a/src/ConnectionPoolImpl.cpp b/src/ConnectionPoolImpl.cpp index c2627be..28182cf 100644 --- a/src/ConnectionPoolImpl.cpp +++ b/src/ConnectionPoolImpl.cpp @@ -240,7 +240,7 @@ class ConnectionPoolImpl idle_.erase(current); } else { RESTC_CPP_LOG_TRACE_("Keeping << " << *current->second->GetConnection() - << " expieres in " + << " expires in " << std::chrono::duration_cast(expires - now).count() << " seconds "); } @@ -365,7 +365,9 @@ class ConnectionPoolImpl } else { #ifdef RESTC_CPP_WITH_TLS - socket = make_unique(owner_.GetIoService(), owner_.GetTLSContext()); + socket = make_unique(owner_.GetIoService(), owner_.GetTLSContext(), + /*allow_sending_over_unupgraded_socket to send plain data over tls socket before handshake*/ + properties_->proxy.type == Request::Proxy::Type::HTTPS); #else throw NotImplementedException( "restc_cpp is compiled without TLS support"); diff --git a/src/DataReaderStream.cpp b/src/DataReaderStream.cpp index 5d23cc1..ab534bf 100644 --- a/src/DataReaderStream.cpp +++ b/src/DataReaderStream.cpp @@ -71,6 +71,7 @@ DataReaderStream::GetData(size_t maxBytes) { void DataReaderStream::ReadServerResponse(Reply::HttpResponse& response) { + static const string http_1_0{"HTTP/1.0"}; //some proxies use HTTP/1.0 static const string http_1_1{"HTTP/1.1"}; constexpr size_t max_version_len = 16; constexpr size_t max_phrase_len = 256; @@ -91,7 +92,8 @@ void DataReaderStream::ReadServerResponse(Reply::HttpResponse& response) if (value.empty()) { throw ProtocolException("ReadHeaders(): No HTTP version"); } - if (ciEqLibC()(value, http_1_1)) { + if (ciEqLibC()(value, http_1_1) || + ciEqLibC()(value, http_1_0)) { ; // Do nothing HTTP 1.1 is the default value } else { throw ProtocolException( diff --git a/src/RequestImpl.cpp b/src/RequestImpl.cpp index 1139209..ef580f2 100644 --- a/src/RequestImpl.cpp +++ b/src/RequestImpl.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include @@ -15,6 +17,7 @@ #include "restc-cpp/error.h" #include "restc-cpp/url_encode.h" #include "restc-cpp/RequestBody.h" +#include "restc-cpp/DataReaderStream.h" #include "ReplyImpl.h" using namespace std; @@ -66,13 +69,34 @@ boost::asio::ip::address make_address(const char* str, namespace restc_cpp { const std::string& Request::Proxy::GetName() { - static const array names = { - "NONE", "HTTP", "SOCKS5" + static const array names = { + "NONE", "HTTP", "HTTPS", "SOCKS5" }; return names.at(static_cast(type)); } +Request::Proxy::Type Request::Proxy::detect() { + char* p; + + if ( (p = std::getenv("https_proxy")) + || (p = std::getenv("HTTPS_PROXY")) + && strlen(p) > 0 ) { + type = Request::Proxy::Type::HTTPS; + address = p; + return type; + } + if ( (p = std::getenv("http_proxy")) + || (p = std::getenv("HTTP_PROXY")) + && strlen(p) > 0 ) { + type = Request::Proxy::Type::HTTP; + address = p; + return type; + } + address.clear(); + return Request::Proxy::Type::NONE; +} + namespace { constexpr char SOCKS5_VERSION = 0x05; @@ -122,7 +146,7 @@ pair ParseAddress(const std::string addr) { } /*! Parse the address and write the socks5 connect request */ -void ParseAddressIntoSocke5ConnectRequest(const std::string& addr, +void ParseAddressIntoSocks5ConnectRequest(const std::string& addr, vector& out) { out.push_back(SOCKS5_VERSION); @@ -221,13 +245,13 @@ size_t ValidateCompleteSocks5ConnectReply(uint8_t *buf, size_t len) { return hdr_len; } -void DoSocks5Handshake(Connection& connection, +void DoSocks5Handshake(const Connection::ptr_t& connection, const Url& url, - const Request::Properties properties, + const Request::Properties::ptr_t& properties, Context& ctx) { - assert(properties.proxy.type == Request::Proxy::Type::SOCKS5); - auto& sck = connection.GetSocket(); + assert(properties->proxy.type == Request::Proxy::Type::SOCKS5); + auto& sck = connection->GetSocket(); // Send no-auth handshake { @@ -255,7 +279,7 @@ void DoSocks5Handshake(Connection& connection, auto addr = url.GetHost().to_string() + ":" + to_string(url.GetPort()); - ParseAddressIntoSocke5ConnectRequest(addr, params); + ParseAddressIntoSocks5ConnectRequest(addr, params); RESTC_CPP_LOG_TRACE_("DoSocks5Handshake - saying connect to " << url.GetHost().to_string() << ":" << url.GetPort()); sck.AsyncWriteT(params, ctx.GetYield()); } @@ -283,6 +307,98 @@ void DoSocks5Handshake(Connection& connection, } RESTC_CPP_LOG_TRACE_("DoSocks5Handshake - done"); } + + +void _throwHttpExceptionWhenNot200(const Reply::HttpResponse& response) { + // Silence the cursed clang tidy! + constexpr auto magic_2 = 2; + constexpr auto magic_100 = 100; + constexpr auto http_401 = 401; + constexpr auto http_403 = 403; + constexpr auto http_404 = 404; + constexpr auto http_405 = 405; + constexpr auto http_406 = 406; + constexpr auto http_407 = 407; + constexpr auto http_408 = 408; + + if ((response.status_code / magic_100) > magic_2) switch(response.status_code) { + case http_401: + throw HttpAuthenticationException(response); + case http_403: + throw HttpForbiddenException(response); + case http_404: + throw HttpNotFoundException(response); + case http_405: + throw HttpMethodNotAllowedException(response); + case http_406: + throw HttpNotAcceptableException(response); + case http_407: + throw HttpProxyAuthenticationRequiredException(response); + case http_408: + throw HttpRequestTimeOutException(response); + default: + throw RequestFailedWithErrorException(response); + } +} + + +void DoProxyConnect(const Connection::ptr_t& connection, + const Url& url, + const Request::Properties::ptr_t& properties, + Context& ctx) { + + assert(properties->proxy.type == Request::Proxy::Type::HTTPS); + static const string crlf{"\r\n"}; + auto& sck = connection->GetSocket(); + + const string host(url.GetHost().to_string() + ":" + to_string(url.GetPort())); + ostringstream request_buffer; + request_buffer << "CONNECT "; + request_buffer << host; + request_buffer << " HTTP/1.1" << crlf; + request_buffer << "Host: " << host << crlf << crlf; + RESTC_CPP_LOG_DEBUG_("DoProxyConnect - send CONNECT " << host << " HTTP/1.1"); + sck.AsyncWriteT(request_buffer.str(), ctx.GetYield()); + + RESTC_CPP_LOG_TRACE_("DoProxyConnect: starting receiving from proxy"); + //struct { http_version=HTTP_1_1; int status_code=0; string reason_phrase; } + Reply::HttpResponse proxy_response; + + try { + DataReader::ReadConfig cfg; //one element struct + cfg.msReadTimeout = properties->recvTimeout;//1000*21 + + //make_unique(connection, ctx, cfg); + unique_ptr io_reader = DataReader::CreateIoReader(connection, ctx, cfg); + + //get from reply->StartReceiveFromServer(io_reader); + auto timer = IoTimer::Create("ReceivingFromProxy"s, + properties->replyTimeoutMs,//1000*21 + connection); + + auto stream = make_unique(move(io_reader)); + + //sets status_code, reason_phrase in response + stream->ReadServerResponse(proxy_response); + stream->ReadHeaderLines( + [](std::string&& name, std::string&& value) { + RESTC_CPP_LOG_TRACE_("Read proxy header: " << name); + }); + + } catch (const exception& ex) { + RESTC_CPP_LOG_DEBUG_("DoProxyConnect: exception from ReceivingFromProxy: " << ex.what()); + throw; + } + + RESTC_CPP_LOG_DEBUG_("DoProxyConnect: Returned from ReceivingFromProxy. code=" << proxy_response.status_code); + + //check for response code 200 + RESTC_CPP_LOG_TRACE_("DoProxyConnect: validating proxy reply"); + _throwHttpExceptionWhenNot200(proxy_response); + + RESTC_CPP_LOG_TRACE_("DoProxyConnect - done"); +} + } // anonumous ns class RequestImpl : public Request { @@ -407,13 +523,13 @@ class RequestImpl : public Request { try { return DoExecute((ctx)); } catch(const RedirectException& ex) { - + auto url = ex.GetUrl(); - + if (properties_->redirectFn) { properties_->redirectFn(ex.GetCode(), url, ex.GetRedirectReply()); } - + if ((properties_->maxRedirects >= 0) && (++redirects > properties_->maxRedirects)) { throw ConstraintException("Too many redirects."); @@ -435,36 +551,7 @@ class RequestImpl : public Request { private: void ValidateReply(const Reply& reply) { - // Silence the cursed clang tidy! - constexpr auto magic_2 = 2; - constexpr auto magic_100 = 100; - constexpr auto http_401 = 401; - constexpr auto http_403 = 403; - constexpr auto http_404 = 404; - constexpr auto http_405 = 405; - constexpr auto http_406 = 406; - constexpr auto http_407 = 407; - constexpr auto http_408 = 408; - - const auto& response = reply.GetHttpResponse(); - if ((response.status_code / magic_100) > magic_2) switch(response.status_code) { - case http_401: - throw HttpAuthenticationException(response); - case http_403: - throw HttpForbiddenException(response); - case http_404: - throw HttpNotFoundException(response); - case http_405: - throw HttpMethodNotAllowedException(response); - case http_406: - throw HttpNotAcceptableException(response); - case http_407: - throw HttpProxyAuthenticationRequiredException(response); - case http_408: - throw HttpRequestTimeOutException(response); - default: - throw RequestFailedWithErrorException(response); - } + _throwHttpExceptionWhenNot200(reply.GetHttpResponse()); } std::string BuildOutgoingRequest() { @@ -527,9 +614,15 @@ class RequestImpl : public Request { return request_buffer.str(); } + //boost1.83: returns {host, service} instead of deprecated ip::resolver::query + //boost1.83: tuple GetRequestEndpoint() { boost::asio::ip::tcp::resolver::query GetRequestEndpoint() { const auto proxy_type = properties_->proxy.type; + //https connections via http proxy is not possible + assert(!(proxy_type == Request::Proxy::Type::HTTP + && parsed_url_.GetProtocol() == Url::Protocol::HTTPS)); + if (proxy_type == Request::Proxy::Type::SOCKS5) { string host; uint16_t port = 0; @@ -539,22 +632,25 @@ class RequestImpl : public Request { << " Proxy at: " << host << ':' << port); - return {host, to_string(port)}; + return { host, to_string(port) }; } - if (proxy_type == Request::Proxy::Type::HTTP) { + if ( (proxy_type == Request::Proxy::Type::HTTP + && parsed_url_.GetProtocol() == Url::Protocol::HTTP) + || proxy_type == Request::Proxy::Type::HTTPS ) { Url proxy {properties_->proxy.address.c_str()}; RESTC_CPP_LOG_TRACE_("Using " << properties_->proxy.GetName() + << ((proxy_type == Request::Proxy::Type::HTTPS) ? "(CONNECT)":"") << " Proxy at: " << proxy.GetHost() << ':' << proxy.GetPort()); return { proxy.GetHost().to_string(), - proxy.GetPort().to_string()}; + proxy.GetPort().to_string() }; } return { parsed_url_.GetHost().to_string(), - parsed_url_.GetPort().to_string()}; + parsed_url_.GetPort().to_string() }; } /* If we are redirected, we need to reset the body @@ -656,18 +752,22 @@ class RequestImpl : public Request { auto prot_filter = GetBindProtocols(properties_->bindToLocalAddress, ctx); const Connection::Type protocol_type = - (parsed_url_.GetProtocol() == Url::Protocol::HTTPS) + (parsed_url_.GetProtocol() == Url::Protocol::HTTPS || + (properties_->proxy.type == Request::Proxy::Type::HTTPS + && parsed_url_.GetProtocol() == Url::Protocol::HTTPS)) ? Connection::Type::HTTPS : Connection::Type::HTTP; boost::asio::ip::tcp::resolver resolver(owner_.GetIoService()); // Resolve the hostname - const auto query = GetRequestEndpoint(); + const auto query = GetRequestEndpoint(); //{host, service=port} RESTC_CPP_LOG_TRACE_("Resolving " << query.host_name() << ":" << query.service_name()); auto address_it = resolver.async_resolve(query, + /*boost1.83: host get<0>(ep_tuple),*/ + /*boost1.83: port get<1>(ep_tuple),*/ ctx.GetYield()); const decltype(address_it) addr_end; @@ -746,15 +846,27 @@ class RequestImpl : public Request { connection->GetSocket().SetAfterConnectCallback([&]() { RESTC_CPP_LOG_TRACE_("RequestImpl::Connect: In Socks5 callback"); - DoSocks5Handshake(*connection, parsed_url_, *properties_, ctx); + DoSocks5Handshake(connection, parsed_url_, properties_, ctx); RESTC_CPP_LOG_TRACE_("RequestImpl::Connect: Leaving Socks5 callback"); }); + } else if (properties_->proxy.type == Proxy::Type::HTTPS) { + connection->GetSocket().SetAfterConnectCallback([&]() { + RESTC_CPP_LOG_TRACE_("RequestImpl::Connect: In Https(connect)-proxy callback"); + + DoProxyConnect(connection, parsed_url_, properties_, ctx); + + RESTC_CPP_LOG_TRACE_("RequestImpl::Connect: Leaving Https(connect)-proxy callback"); + }); } + const string sni_host = (protocol_type == Connection::Type::HTTPS) + ? parsed_url_.GetHost().to_string() + : address_it->host_name(); + RESTC_CPP_LOG_TRACE_("RequestImpl::Connect: calling AsyncConnect --> " << endpoint); connection->GetSocket().AsyncConnect( - endpoint, address_it->host_name(), + endpoint, sni_host, properties_->tcpNodelay, ctx.GetYield()); RESTC_CPP_LOG_TRACE_("RequestImpl::Connect: OK AsyncConnect --> " << endpoint); return connection; @@ -764,7 +876,7 @@ class RequestImpl : public Request { connection->GetSocket().GetSocket().close(); if (ex.code() == boost::system::errc::resource_unavailable_try_again) { - if ( retries < 8) { + if ( retries < 8 ) { RESTC_CPP_LOG_DEBUG_( "RequestImpl::Connect:: Caught boost::system::system_error exception: \"" << ex.what() @@ -778,7 +890,18 @@ class RequestImpl : public Request { << ex.what() << "\" while connecting to " << endpoint); break; // Go to the next endpoint - } catch(const exception& ex) { + + } catch (const RequestFailedWithErrorException& ex) { + RESTC_CPP_LOG_WARN_("Connect to " + << endpoint + << " failed with HTTP protocol exception type: " + << typeid(ex).name() + << ", message: " << ex.what()); + + connection->GetSocket().GetSocket().close(); + break; // Go to the next endpoint + + } catch (const exception& ex) { RESTC_CPP_LOG_WARN_("Connect to " << endpoint << " failed with exception type: " @@ -977,7 +1100,7 @@ class RequestImpl : public Request { std::uint64_t bytes_sent_ = 0; bool dirty_ = false; bool add_url_args_ = true; -}; +}; //class RequestImpl std::unique_ptr diff --git a/src/SocketImpl.h b/src/SocketImpl.h index 747debf..f5ea3a0 100644 --- a/src/SocketImpl.h +++ b/src/SocketImpl.h @@ -56,7 +56,7 @@ class SocketImpl : public Socket, protected ExceptionWrapper { } void AsyncConnect(const boost::asio::ip::tcp::endpoint& ep, - const std::string &host, + const std::string &host, bool tcpNodelay, boost::asio::yield_context& yield) override { return WrapException([&] { diff --git a/src/TlsSocketImpl.h b/src/TlsSocketImpl.h index a7033f5..aece0ef 100644 --- a/src/TlsSocketImpl.h +++ b/src/TlsSocketImpl.h @@ -24,9 +24,12 @@ class TlsSocketImpl : public Socket, protected ExceptionWrapper { using ssl_socket_t = boost::asio::ssl::stream; - TlsSocketImpl(boost::asio::io_service& io_service, shared_ptr ctx) + TlsSocketImpl(boost::asio::io_service& io_service, + shared_ptr ctx, + bool allow_sending_over_unupgraded_socket = false) { ssl_socket_ = std::make_unique(io_service, *ctx); + can_send_over_unupgraded_socket_ = allow_sending_over_unupgraded_socket; } boost::asio::ip::tcp::socket& GetSocket() override { @@ -42,6 +45,8 @@ class TlsSocketImpl : public Socket, protected ExceptionWrapper { std::size_t AsyncReadSome(boost::asio::mutable_buffers_1 buffers, boost::asio::yield_context& yield) override { return WrapException([&] { + if (can_send_over_unupgraded_socket_) + return ssl_socket_->next_layer().async_read_some(buffers, yield); return ssl_socket_->async_read_some(buffers, yield); }); } @@ -49,19 +54,27 @@ class TlsSocketImpl : public Socket, protected ExceptionWrapper { std::size_t AsyncRead(boost::asio::mutable_buffers_1 buffers, boost::asio::yield_context& yield) override { return WrapException([&] { + if (can_send_over_unupgraded_socket_) + return boost::asio::async_read(ssl_socket_->next_layer(), buffers, yield); return boost::asio::async_read(*ssl_socket_, buffers, yield); }); } void AsyncWrite(const boost::asio::const_buffers_1& buffers, boost::asio::yield_context& yield) override { - boost::asio::async_write(*ssl_socket_, buffers, yield); + if (can_send_over_unupgraded_socket_) + boost::asio::async_write(ssl_socket_->next_layer(), buffers, yield); + else + boost::asio::async_write(*ssl_socket_, buffers, yield); } void AsyncWrite(const write_buffers_t& buffers, boost::asio::yield_context& yield) override { return WrapException([&] { - boost::asio::async_write(*ssl_socket_, buffers, yield); + if (can_send_over_unupgraded_socket_) + boost::asio::async_write(ssl_socket_->next_layer(), buffers, yield); + else + boost::asio::async_write(*ssl_socket_, buffers, yield); }); } @@ -90,6 +103,7 @@ class TlsSocketImpl : public Socket, protected ExceptionWrapper { RESTC_CPP_LOG_TRACE_("AsyncConnect - Calling async_handshake"); ssl_socket_->async_handshake(boost::asio::ssl::stream_base::client, yield); + can_send_over_unupgraded_socket_ = false; RESTC_CPP_LOG_TRACE_("AsyncConnect - Done"); }); @@ -135,6 +149,7 @@ class TlsSocketImpl : public Socket, protected ExceptionWrapper { private: std::unique_ptr ssl_socket_; + bool can_send_over_unupgraded_socket_; }; } // restc_cpp diff --git a/stop-containers.sh b/stop-containers.sh index 77950e6..22b110f 100755 --- a/stop-containers.sh +++ b/stop-containers.sh @@ -1,5 +1,5 @@ #!/bin/bash pushd ci/mock-backends -docker-compose down +docker compose down popd diff --git a/tests/functional/ProxyTests.cpp b/tests/functional/ProxyTests.cpp index cda65f9..e7f563e 100644 --- a/tests/functional/ProxyTests.cpp +++ b/tests/functional/ProxyTests.cpp @@ -13,11 +13,12 @@ using namespace std; using namespace restc_cpp; -static const string defunct_proxy_address = GetDockerUrl("http://localhost:0"); +static const string defunct_proxy_address = GetDockerUrl("http://localhost:3777"); static const string http_proxy_address = GetDockerUrl("http://localhost:3003"); +static const string https_proxy_address = GetDockerUrl("http://localhost:3003"); static const string socks5_proxy_address = GetDockerUrl("localhost:3004"); -TEST(Proxy, FailToConnect) +TEST(Proxy, FailToConnect_HTTP_Proxy) { Request::Properties properties; properties.proxy.type = Request::Proxy::Type::HTTP; @@ -33,10 +34,11 @@ TEST(Proxy, FailToConnect) .Execute(); }); - EXPECT_ANY_THROW(f.get()); + //EXPECT_ANY_THROW(f.get()); + EXPECT_THROW(f.get(), FailedToConnectException); } -TEST(Proxy, WithHttpProxy) +TEST(Proxy, With_HTTP_Proxy) { Request::Properties properties; properties.proxy.type = Request::Proxy::Type::HTTP; @@ -50,14 +52,98 @@ TEST(Proxy, WithHttpProxy) .Get("http://api.example.com/normal/posts/1") .Execute(); - EXPECT_HTTP_OK(reply->GetResponseCode()); - cout << "Got: " << reply->GetBodyAsString() << endl; + EXPECT_HTTP_OK(reply->GetResponseCode()); + cout << "Got: " << reply->GetBodyAsString() << endl; }); EXPECT_NO_THROW(f.get()); } -TEST(Proxy, WithSocks5Proxy) + +TEST(Proxy, FailToConnect_HTTPSCONNECT_Proxy) +{ + Request::Properties properties; + properties.proxy.type = Request::Proxy::Type::HTTPS; + properties.proxy.address = defunct_proxy_address; + + // Create the client with our configuration + auto rest_client = RestClient::Create(properties); + + + auto f = rest_client->ProcessWithPromise([&](Context& ctx) { + auto reply = RequestBuilder(ctx) + .Get("https://api.example.com/normal/posts/1") + .Execute(); + }); + + EXPECT_THROW(f.get(), FailedToConnectException); +} + +TEST(Proxy, With_HTTPSCONNECT_ProxyToHttps) +{ + Request::Properties properties; + properties.proxy.type = Request::Proxy::Type::HTTPS; + properties.proxy.address = https_proxy_address; + + // Create the client with our configuration + auto rest_client = RestClient::Create(properties); + + auto f = rest_client->ProcessWithPromise([&](Context& ctx) { + auto reply = RequestBuilder(ctx) + .Get("https://api.example.com/normal/posts/1") + .Execute(); + + EXPECT_HTTP_OK(reply->GetResponseCode()); + cout << "Got: " << reply->GetBodyAsString() << endl; + }); + + EXPECT_NO_THROW(f.get()); +} + +// WARNING: passing plain http over a CONNECT tunnel is denied by default in squid +// we enabled it in squid.conf for the sake of our test +TEST(Proxy, With_HTTPSCONNECT_ProxyToHttp) +{ + Request::Properties properties; + properties.proxy.type = Request::Proxy::Type::HTTPS; + properties.proxy.address = https_proxy_address; + + // Create the client with our configuration + auto rest_client = RestClient::Create(properties); + + auto f = rest_client->ProcessWithPromise([&](Context& ctx) { + auto reply = RequestBuilder(ctx) + .Get("http://api.example.com/normal/posts/1") + .Execute(); + + EXPECT_HTTP_OK(reply->GetResponseCode()); + cout << "Got: " << reply->GetBodyAsString() << endl; + }); + + EXPECT_NO_THROW(f.get()); +} + +// we denied CONNECT method to port 444 in our squid.conf +TEST(Proxy, With_HTTPSCONNECT_ProxyToDeniedPort) +{ + Request::Properties properties; + properties.proxy.type = Request::Proxy::Type::HTTPS; + properties.proxy.address = https_proxy_address; + + // Create the client with our configuration + auto rest_client = RestClient::Create(properties); + + auto f = rest_client->ProcessWithPromise([&](Context& ctx) { + auto reply = RequestBuilder(ctx) + .Get("https://api.example.com:444/normal/posts/1") + .Execute(); + }); + + EXPECT_THROW(f.get(), FailedToConnectException); +} + + +TEST(Proxy, With_SOCKS5_Proxy) { Request::Properties properties; properties.proxy.type = Request::Proxy::Type::SOCKS5; @@ -71,8 +157,8 @@ TEST(Proxy, WithSocks5Proxy) .Get("http://api.example.com/normal/posts/1") .Execute(); - EXPECT_HTTP_OK(reply->GetResponseCode()); - cout << "Got: " << reply->GetBodyAsString() << endl; + EXPECT_HTTP_OK(reply->GetResponseCode()); + cout << "Got: " << reply->GetBodyAsString() << endl; }); EXPECT_NO_THROW(f.get()); diff --git a/tests/mock-nginx/Dockerfile b/tests/mock-nginx/Dockerfile index 48cc9e7..a6e1c2c 100644 --- a/tests/mock-nginx/Dockerfile +++ b/tests/mock-nginx/Dockerfile @@ -1,7 +1,10 @@ FROM nginx RUN rm /etc/nginx/conf.d/default.conf COPY proxy.conf.bin /etc/nginx/conf.d/proxy.conf +COPY proxy-https.conf.bin /etc/nginx/conf.d/proxy-https.conf COPY htpasswd.bin /etc/nginx/htpasswd +COPY test.pem /etc/nginx/ssl/test.pem +COPY test-key.pem /etc/nginx/ssl/test-key.pem RUN mkdir -p /etc/nginx/html/upload RUN chmod 777 /etc/nginx/html/upload diff --git a/tests/mock-nginx/create_cert.sh b/tests/mock-nginx/create_cert.sh new file mode 100755 index 0000000..d383304 --- /dev/null +++ b/tests/mock-nginx/create_cert.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +openssl req -x509 -newkey rsa:4096 -keyout test-key.pem -out test.pem -sha256 -days 3650 -nodes -subj "/C=XX/ST=Test/L=Test/OU=Test/O=Test/CN=localhost" + +#openssl x509 -in test.pem -text diff --git a/tests/mock-nginx/proxy-https.conf.bin b/tests/mock-nginx/proxy-https.conf.bin new file mode 100644 index 0000000..3092d25 --- /dev/null +++ b/tests/mock-nginx/proxy-https.conf.bin @@ -0,0 +1,62 @@ +server { + listen 443 ssl; + server_name localhost; + gzip on; + gzip_proxied any; + ssl_certificate /etc/nginx/ssl/test.pem; + ssl_certificate_key /etc/nginx/ssl/test-key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + + + location ~ ^/cookies(/?)(.*)$ { + add_header Content-Type "application/json; charset=utf-8"; + add_header Set-Cookie test1=yes; + add_header Set-Cookie test2=maybe; + add_header Set-Cookie test3=no; + return 200 '{}'; + } + + location ~ ^/normal(/?)(.*)$ { + proxy_pass http://json/$2$is_args$args; + } + + location ~ ^/close(/?)(.*)$ { + proxy_pass http://json/$2$is_args$args; + keepalive_timeout 0; + } + + # Force nginx to resolve 'json' + location /dns_workaround { + proxy_pass http://json; + } + + location ~ ^/loop(/?)(.*)$ { + return 301 $scheme://$host:3001/loop/$2$is_args$args; + } + + location ~ ^/redirect(/?)(.*)$ { + return 301 $scheme://$host:3001/normal/$2$is_args$args; + } + + location ~ ^/reredirect(/?)(.*)$ { + return 301 $scheme://$host:3001/redirect/$2$is_args$args; + } + + location ~ ^/restricted(/?)(.*)$ { + auth_basic "Restricted Area"; + auth_basic_user_file /etc/nginx/htpasswd; + proxy_pass http://json/$2$is_args$args; + } + + location ~ ^/upload_raw(/?)(.*)$ { + limit_except POST { deny all; } + return 200 "OK"; + } + + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} + diff --git a/tests/mock-nginx/test-key.pem b/tests/mock-nginx/test-key.pem new file mode 100644 index 0000000..8d01ea1 --- /dev/null +++ b/tests/mock-nginx/test-key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +stBsKJU1QibpCwO3/NmBYwjRitw1E2c= +-----END PRIVATE KEY----- diff --git a/tests/mock-nginx/test.pem b/tests/mock-nginx/test.pem new file mode 100644 index 0000000..2fd997a --- /dev/null +++ b/tests/mock-nginx/test.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFmzCCA4OgAwIBAgIUJFXivxxXEIY3rp6fz2wgE3rsQzYwDQYJKoZIhvcNAQEL +BQAwXTELMAkGA1UEBhMCWFgxDTALBgNVBAgMBFRlc3QxDTALBgNVBAcMBFRlc3Qx +DTALBgNVBAsMBFRlc3QxDTALBgNVBAoMBFRlc3QxEjAQBgNVBAMMCWxvY2FsaG9z +dDAeFw0yMzA4MzAxMTM2MjJaFw0zMzA4MjcxMTM2MjJaMF0xCzAJBgNVBAYTAlhY +MQ0wCwYDVQQIDARUZXN0MQ0wCwYDVQQHDARUZXN0MQ0wCwYDVQQLDARUZXN0MQ0w +CwYDVQQKDARUZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDPlSJHl/g59nyiHvVblS5eWCTRxFFueCIEbYGfUdeL ++Jc+YP20fJs3u5Tq7gCvuYqGTMztXfERRk3HNrHy1uXndh0fGVUusn5hYz4XNB8A +mRfZf1wDTpd4SjVe26Da52sIGppj1bGgPJWR9KK+7zDPNeEVQ4HPYnh8rTfHBG0v +YlfHnHR4EGwS6znKA3YIgwHqHIrdm/cOmMK3hDMmjG8l01U5YHIBOadTh1QZvqhp +EToJXnV949CvN12HiyawSQ3nG3HTC94PnrV7UtOLGf3rh+OxN8D9d+QgBVSAI8d7 +VqT+7xxU8wvzL5gWcQLuhY208cZW3Y6gC4U6ZpfHxeqUtAUw4ZK4bJmKr6HFdasX +aPkKPsUZsppeiGYtXRwt6yuHJuI3I1JTwcuASYCHEhus5biMyRZWZo5PLbCX1sXY +bbAEt/qVpg1jeTwrkeZ62lC79lAKjd0runTENMvZYKZwnJntSUz6vq6uMYoM0BzS +MSioFu2fqWBFH9263Tje/wwz/aNNAKx2+3SNavC7//gSM/0gl/9bVRgsWLyxc5qx +CTU8uQ1P5zNx07YNmv6C8UI44craA7KdxcYgqgdUStKDzpDB48eOvvbRRAZO55KA +YzCS/1dgfFUcsugP7DklblI3kTNHTIjarYYXJjUNl0Tpcx3Tk6hfgBBMX1Re/Gtv +TwIDAQABo1MwUTAdBgNVHQ4EFgQUtH5zl0rC27e1JxtdMbIxwqkg0NQwHwYDVR0j +BBgwFoAUtH5zl0rC27e1JxtdMbIxwqkg0NQwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAgEAw7sQ1KIyHCYHJUzYdX9I/l0ylbzrKpfJUABq3OOmgjUM +k6InH6bXvAEP7bj4zC3UDKj1hf5WVHQM0yUeLx8ZmzHzsgfS6C8MiSt+47V0NboQ +5aGHY21ceMivlNDihf0cjPg+h4NsAZkzMWR771Nxe3WLT20Wo1ehKqCjIC7HnCJ3 +IAKbholWJwaiu1Y9tLn21sNMC0Z75hx/JWokP8OuZUyLxsp5RDLLvPA4N41z1Tep +CXpBct6jdRVzq7b5bUgPIQJ4XVdnaIYtdWIZVavulbr0vo4P7dayhf7ZWGcFVyPh +GmjHsPiurZy5BShEOgyYlwrSf9guDJfLR0gclKysKF1I4O9EWysRntvxuDMg0bys +jdUkp25EjQzIP/gA5J6NhTUFYnt04cLIebxs8CU/ZqZ0QZ11wT3VnvTm9ukTsaT4 +lKzmBjlejZVLwlki6r3IAj/CD2v9pJddt950usj9/I9C6quw1XpWG6TYZlCyM9lb +ucUfPttVknOM8c/eenXYAECR/f9MqlFBHO3Bj9YfO4hGTZtE7qaiY2f6yhcLRCOt +Bd649G/bfKT0KvLp+6SdTJxMYFC70FUqoqniEfxT+AhsKP+2k0MfU4MvJ/neLcGA +l/fM+TeIJWTzadl/DCZeE0FyQXAzeqHICnNQOieH6spMpjiXd6cv0aPO1zSqpV4= +-----END CERTIFICATE----- diff --git a/tests/mock-squid/squid.conf.bin b/tests/mock-squid/squid.conf.bin index 5e1aedf..2ce71e2 100644 --- a/tests/mock-squid/squid.conf.bin +++ b/tests/mock-squid/squid.conf.bin @@ -1,5 +1,9 @@ - +reply_header_add Test-proxy "THIS IS TEST PROXY" all +acl CONNECT method CONNECT +acl connect_denied_ports port 444 http_access allow localhost manager http_access deny manager +http_access deny CONNECT connect_denied_ports http_access allow all http_port 3128 + diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index bcc2580..b2616bb 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -85,3 +85,15 @@ target_link_libraries(request_builder_tests add_dependencies(request_builder_tests restc-cpp ${DEPENDS_GTEST}) ADD_AND_RUN_UNITTEST(REQUEST_BUILDER_TESTS request_builder_tests) + +# ====================================== + +add_executable(proxy_detection_tests ProxyDetectionTests.cpp) +target_link_libraries(proxy_detection_tests + ${GTEST_LIBRARIES} + restc-cpp + ${DEFAULT_LIBRARIES} +) +add_dependencies(proxy_detection_tests restc-cpp ${DEPENDS_GTEST}) +ADD_AND_RUN_UNITTEST(PROXY_DETECTION_TESTS proxy_detection_tests) + diff --git a/tests/unit/ProxyDetectionTests.cpp b/tests/unit/ProxyDetectionTests.cpp new file mode 100644 index 0000000..a88b594 --- /dev/null +++ b/tests/unit/ProxyDetectionTests.cpp @@ -0,0 +1,70 @@ +// Include before boost::log headers +#include "restc-cpp/logging.h" + +#include "restc-cpp/restc-cpp.h" + +#include "gtest/gtest.h" +#include "restc-cpp/test_helper.h" + +#include + +using namespace std; +using namespace restc_cpp; + +void clear_env() { + unsetenv("https_proxy"); + unsetenv("HTTPS_PROXY"); + unsetenv("http_proxy"); + unsetenv("HTTP_PROXY"); +} + +TEST(ProxyDetection, ProxyTypeHTTP) +{ + clear_env(); + setenv("HTTP_PROXY", "http://1.2.3.4:8088", 1); + + Request::Properties properties; + EXPECT_EQ(Request::Proxy::Type::HTTP, properties.proxy.detect()); + EXPECT_EQ(Request::Proxy::Type::HTTP, properties.proxy.type); + EXPECT_EQ("http://1.2.3.4:8088", properties.proxy.address); + + setenv("http_proxy", "http://1.2.3.4:8089", 1); + EXPECT_EQ(Request::Proxy::Type::HTTP, properties.proxy.detect()); + EXPECT_EQ(Request::Proxy::Type::HTTP, properties.proxy.type); + EXPECT_EQ("http://1.2.3.4:8089", properties.proxy.address); +} + +TEST(ProxyDetection, ProxyTypeHTTPS) +{ + clear_env(); + setenv("HTTP_PROXY", "http://1.2.3.4:8088", 1); + setenv("HTTPS_PROXY", "http://1.2.3.4:8090", 1); + + Request::Properties properties; + EXPECT_EQ(Request::Proxy::Type::HTTPS, properties.proxy.detect()); + EXPECT_EQ(Request::Proxy::Type::HTTPS, properties.proxy.type); + EXPECT_EQ("http://1.2.3.4:8090", properties.proxy.address); + + setenv("https_proxy", "http://1.2.3.4:8091", 1); + EXPECT_EQ(Request::Proxy::Type::HTTPS, properties.proxy.detect()); + EXPECT_EQ(Request::Proxy::Type::HTTPS, properties.proxy.type); + EXPECT_EQ("http://1.2.3.4:8091", properties.proxy.address); +} + +TEST(ProxyDetection, ProxyTypeNONE) +{ + clear_env(); + + Request::Properties properties; + EXPECT_EQ(Request::Proxy::Type::NONE, properties.proxy.detect()); + EXPECT_EQ(Request::Proxy::Type::NONE, properties.proxy.type); + EXPECT_EQ("", properties.proxy.address); +} + + +int main( int argc, char * argv[] ) +{ + RESTC_CPP_TEST_LOGGING_SETUP("debug"); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}