diff --git a/bin/check-environment-variables b/bin/check-environment-variables index 07a7f27ce..9faae9137 100755 --- a/bin/check-environment-variables +++ b/bin/check-environment-variables @@ -1,5 +1,4 @@ #!/bin/sh -# TODO set -eu if grep -R -n \ @@ -10,6 +9,7 @@ if grep -R -n \ --include='*.h' \ --include='*.hpp' \ --exclude='environment.cpp' \ + --exclude='curl.cpp' \ 'std::getenv' include src; then echo "Check failed: std::getenv usage detected." exit 1 diff --git a/src/datadog/curl.cpp b/src/datadog/curl.cpp index e8f407836..10e5f8614 100644 --- a/src/datadog/curl.cpp +++ b/src/datadog/curl.cpp @@ -2,17 +2,10 @@ #include #include -#include #include -#include -#include -#include -#include #include -#include #include -#include #include #include #include @@ -21,14 +14,80 @@ #include "json.hpp" #include "string_util.h" -namespace datadog { -namespace tracing { +namespace datadog::tracing { + namespace { // `libcurl` is the default implementation: it calls `curl_*` functions under // the hood. CurlLibrary libcurl; +struct ProxyConfiguration { + Optional all_proxy; + Optional http_proxy; + Optional https_proxy; + Optional no_proxy; +}; + +Optional environment_variable(const char *name) { + const char *const value = std::getenv(name); + if (value == nullptr || *value == '\0') { + return nullopt; + } + return std::string{value}; +} + +ProxyConfiguration load_proxy_configuration() { + ProxyConfiguration config; + + config.all_proxy = environment_variable("all_proxy"); + if (!config.all_proxy) { + config.all_proxy = environment_variable("ALL_PROXY"); + } + + // Only the lowercase form for http, to avoid httpoxy (CVE-2016-5385). + config.http_proxy = environment_variable("http_proxy"); + + config.https_proxy = environment_variable("https_proxy"); + if (!config.https_proxy) { + config.https_proxy = environment_variable("HTTPS_PROXY"); + } + + config.no_proxy = environment_variable("no_proxy"); + if (!config.no_proxy) { + config.no_proxy = environment_variable("NO_PROXY"); + } + + return config; +} + +bool is_unix_socket(const HTTPClient::URL &url) { + return url.scheme == "unix" || url.scheme == "http+unix" || + url.scheme == "https+unix"; +} + +StringView resolve_proxy(const HTTPClient::URL &url, + const ProxyConfiguration &config) { + if (is_unix_socket(url)) { + return ""; + } + const Optional &scheme_proxy = + url.scheme == "https" ? config.https_proxy : config.http_proxy; + if (scheme_proxy) { + return *scheme_proxy; + } + if (config.all_proxy) { + return *config.all_proxy; + } + return ""; +} + +void throw_on_error(CURLcode result) { + if (result != CURLE_OK) { + throw result; + } +} + } // namespace CURL *CurlLibrary::easy_init() { return curl_easy_init(); } @@ -61,6 +120,10 @@ CURLcode CurlLibrary::easy_setopt_httpheader(CURL *handle, return curl_easy_setopt(handle, CURLOPT_HTTPHEADER, headers); } +CURLcode CurlLibrary::easy_setopt_noproxy(CURL *handle, const char *no_proxy) { + return curl_easy_setopt(handle, CURLOPT_NOPROXY, no_proxy); +} + CURLcode CurlLibrary::easy_setopt_post(CURL *handle, long post) { return curl_easy_setopt(handle, CURLOPT_POST, post); } @@ -77,6 +140,10 @@ CURLcode CurlLibrary::easy_setopt_private(CURL *handle, void *pointer) { return curl_easy_setopt(handle, CURLOPT_PRIVATE, pointer); } +CURLcode CurlLibrary::easy_setopt_proxy(CURL *handle, const char *proxy) { + return curl_easy_setopt(handle, CURLOPT_PROXY, proxy); +} + CURLcode CurlLibrary::easy_setopt_unix_socket_path(CURL *handle, const char *path) { return curl_easy_setopt(handle, CURLOPT_UNIX_SOCKET_PATH, path); @@ -172,6 +239,7 @@ class CurlImpl { std::list new_handles_; bool shutting_down_; int num_active_handles_; + const ProxyConfiguration proxy_config_; std::condition_variable no_requests_; std::thread event_loop_; @@ -214,6 +282,7 @@ class CurlImpl { }; void run(); + void configure_handle(CURL *handle, Request &request, const URL &url); void handle_message(const CURLMsg &); CURLcode log_on_error(CURLcode result); CURLMcode log_on_error(CURLMcode result); @@ -238,16 +307,6 @@ class CurlImpl { void clear_requests(); }; -namespace { - -void throw_on_error(CURLcode result) { - if (result != CURLE_OK) { - throw result; - } -} - -} // namespace - Curl::Curl(const std::shared_ptr &logger, const Clock &clock) : Curl(logger, clock, libcurl) {} @@ -283,7 +342,8 @@ CurlImpl::CurlImpl(const std::shared_ptr &logger, const Clock &clock, logger_(logger), clock_(clock), shutting_down_(false), - num_active_handles_(0) { + num_active_handles_(0), + proxy_config_(load_proxy_configuration()) { curl_.global_init(CURL_GLOBAL_ALL); multi_handle_ = curl_.multi_init(); if (multi_handle_ == nullptr) { @@ -360,33 +420,7 @@ Expected CurlImpl::post( "unable to initialize a curl handle for request sending"}; } - throw_on_error( - curl_.easy_setopt_httpheader(handle.get(), request->request_headers)); - throw_on_error(curl_.easy_setopt_private(handle.get(), request.get())); - throw_on_error( - curl_.easy_setopt_errorbuffer(handle.get(), request->error_buffer)); - throw_on_error(curl_.easy_setopt_post(handle.get(), 1)); - throw_on_error(curl_.easy_setopt_postfieldsize( - handle.get(), static_cast(request->request_body.size()))); - throw_on_error( - curl_.easy_setopt_postfields(handle.get(), request->request_body.data())); - throw_on_error( - curl_.easy_setopt_headerfunction(handle.get(), &on_read_header)); - throw_on_error(curl_.easy_setopt_headerdata(handle.get(), request.get())); - throw_on_error(curl_.easy_setopt_writefunction(handle.get(), &on_read_body)); - throw_on_error(curl_.easy_setopt_writedata(handle.get(), request.get())); - if (url.scheme == "unix" || url.scheme == "http+unix" || - url.scheme == "https+unix") { - throw_on_error(curl_.easy_setopt_unix_socket_path(handle.get(), - url.authority.c_str())); - // The authority section of the URL is ignored when a unix domain socket is - // to be used. - throw_on_error(curl_.easy_setopt_url( - handle.get(), ("http://localhost" + url.path).c_str())); - } else { - throw_on_error(curl_.easy_setopt_url( - handle.get(), (url.scheme + "://" + url.authority + url.path).c_str())); - } + configure_handle(handle.get(), *request, url); { std::lock_guard lock(mutex_); @@ -403,6 +437,39 @@ Expected CurlImpl::post( return Error{Error::CURL_REQUEST_SETUP_FAILED, curl_.easy_strerror(error)}; } +void CurlImpl::configure_handle(CURL *handle, Request &request, + const URL &url) { + throw_on_error(curl_.easy_setopt_httpheader(handle, request.request_headers)); + throw_on_error(curl_.easy_setopt_private(handle, &request)); + throw_on_error(curl_.easy_setopt_errorbuffer(handle, request.error_buffer)); + throw_on_error(curl_.easy_setopt_post(handle, 1)); + throw_on_error(curl_.easy_setopt_postfieldsize( + handle, static_cast(request.request_body.size()))); + throw_on_error( + curl_.easy_setopt_postfields(handle, request.request_body.data())); + throw_on_error(curl_.easy_setopt_headerfunction(handle, &on_read_header)); + throw_on_error(curl_.easy_setopt_headerdata(handle, &request)); + throw_on_error(curl_.easy_setopt_writefunction(handle, &on_read_body)); + throw_on_error(curl_.easy_setopt_writedata(handle, &request)); + + throw_on_error(curl_.easy_setopt_noproxy( + handle, proxy_config_.no_proxy ? proxy_config_.no_proxy->c_str() : "")); + const StringView proxy = resolve_proxy(url, proxy_config_); + throw_on_error(curl_.easy_setopt_proxy(handle, proxy.data())); + + if (is_unix_socket(url)) { + throw_on_error( + curl_.easy_setopt_unix_socket_path(handle, url.authority.c_str())); + // The authority section of the URL is ignored when a unix domain socket is + // to be used. + throw_on_error( + curl_.easy_setopt_url(handle, ("http://localhost" + url.path).c_str())); + } else { + throw_on_error(curl_.easy_setopt_url( + handle, (url.scheme + "://" + url.authority + url.path).c_str())); + } +} + void CurlImpl::clear_requests() { for (const auto &handle : request_handles_) { char *user_data; @@ -650,5 +717,4 @@ void CurlImpl::HeaderReader::visit( } } -} // namespace tracing -} // namespace datadog +} // namespace datadog::tracing diff --git a/src/datadog/curl.h b/src/datadog/curl.h index bbaf380a9..3ff8e2398 100644 --- a/src/datadog/curl.h +++ b/src/datadog/curl.h @@ -1,26 +1,20 @@ #pragma once // This component provides a `class`, `Curl`, that implements the `HTTPClient` -// interface in terms of [libcurl][1]. `class Curl` manages a thread that is -// used as the event loop for libcurl. +// interface in terms of [libcurl](https://curl.se/libcurl)]. `class Curl` +// manages a thread that is used as the event loop for libcurl. // // If this library was built in a mode that does not include libcurl, then this // file and its implementation, `curl.cpp`, will not be included. -// -// [1]: https://curl.se/libcurl/ #include #include #include -#include -#include #include -#include #include -namespace datadog { -namespace tracing { +namespace datadog::tracing { // `class CurlLibrary` has one member function for every libcurl function used // in the implementation of this component. @@ -50,10 +44,12 @@ class CurlLibrary { virtual CURLcode easy_setopt_headerdata(CURL *handle, void *data); virtual CURLcode easy_setopt_headerfunction(CURL *handle, HeaderCallback); virtual CURLcode easy_setopt_httpheader(CURL *handle, curl_slist *headers); + virtual CURLcode easy_setopt_noproxy(CURL *handle, const char *no_proxy); virtual CURLcode easy_setopt_post(CURL *handle, long post); virtual CURLcode easy_setopt_postfields(CURL *handle, const char *data); virtual CURLcode easy_setopt_postfieldsize(CURL *handle, long size); virtual CURLcode easy_setopt_private(CURL *handle, void *pointer); + virtual CURLcode easy_setopt_proxy(CURL *handle, const char *proxy); virtual CURLcode easy_setopt_unix_socket_path(CURL *handle, const char *path); virtual CURLcode easy_setopt_url(CURL *handle, const char *url); virtual CURLcode easy_setopt_writedata(CURL *handle, void *data); @@ -104,5 +100,4 @@ class Curl : public HTTPClient { std::string config() const override; }; -} // namespace tracing -} // namespace datadog +} // namespace datadog::tracing diff --git a/test/test_curl.cpp b/test/test_curl.cpp index 3c88ef88a..e48a9690c 100644 --- a/test/test_curl.cpp +++ b/test/test_curl.cpp @@ -1,17 +1,11 @@ -#include #include -#include -#include -#include #include #include -#include -#include -#include +#include #include -#include "datadog/clock.h" +#include "common/environment.h" #include "mocks/loggers.h" #include "null_logger.h" #include "test.h" @@ -495,3 +489,85 @@ CURL_TEST("post() deadline exceeded before request start") { REQUIRE(error_delivered->code == Error::CURL_DEADLINE_EXCEEDED_BEFORE_REQUEST_START); } + +CURL_TEST("proxy is taken from the environment and forwarded to libcurl") { + using datadog::test::EnvGuard; + + class ProxyCapturingCurlLibrary : public SingleRequestMockCurlLibrary { + public: + Optional proxy_; + Optional no_proxy_; + + CURLcode easy_setopt_proxy(CURL *, const char *proxy) override { + proxy_ = proxy; + return CURLE_OK; + } + CURLcode easy_setopt_noproxy(CURL *, const char *no_proxy) override { + no_proxy_ = no_proxy; + return CURLE_OK; + } + }; + + std::list cleared; + for (const char *name : + {"http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY", "all_proxy", + "ALL_PROXY", "no_proxy", "NO_PROXY"}) { + cleared.emplace_back(name, ""); + } + + const auto clock = default_clock; + const auto logger = std::make_shared(); + ProxyCapturingCurlLibrary library; + + const auto post_to = [&](const std::string &scheme) { + Curl client{logger, clock, library}; + const HTTPClient::URL url{scheme, "agent:8126", "/", ""}; + REQUIRE( + client.post(url, ignore, "body", ignore, ignore, clock().tick + 10s)); + client.drain(clock().tick + 1s); + }; + + SECTION("http scheme uses http_proxy") { + EnvGuard env{"http_proxy", "http://proxy:8080"}; + post_to("http"); + REQUIRE(library.proxy_ == "http://proxy:8080"); + } + + SECTION("https scheme uses https_proxy") { + EnvGuard env{"https_proxy", "http://secure:8080"}; + post_to("https"); + REQUIRE(library.proxy_ == "http://secure:8080"); + } + + SECTION("scheme-specific proxy falls back to all_proxy") { + EnvGuard env{"all_proxy", "http://any:8080"}; + post_to("http"); + REQUIRE(library.proxy_ == "http://any:8080"); + } + + SECTION("no_proxy is forwarded") { + EnvGuard env{"no_proxy", "agent,localhost"}; + post_to("http"); + REQUIRE(library.no_proxy_ == "agent,localhost"); + } + +#ifndef _WIN32 // Windows environment variable names are case-insensitive + SECTION("uppercase HTTP_PROXY is ignored (httpoxy)") { + EnvGuard env{"HTTP_PROXY", "http://attacker:8080"}; + post_to("http"); + REQUIRE(library.proxy_ == ""); + } +#endif + + SECTION("absent proxy environment yields empty options") { + post_to("http"); + REQUIRE(library.proxy_ == ""); + REQUIRE(library.no_proxy_ == ""); + } + + SECTION("unix sockets are never proxied") { + EnvGuard env{"http_proxy", "http://proxy:8080"}; + post_to("unix"); + REQUIRE(library.proxy_ == ""); + } +}