Skip to content

Commit

Permalink
Support sending requests through a web proxy
Browse files Browse the repository at this point in the history
- This commit does not yet include support
  for proxy usernames and passwords

Signed-off-by: Margo Crawford <margaretc@vmware.com>
Signed-off-by: Andrew Chang <anchang@pivotal.io>
  • Loading branch information
cfryanr authored and Changdrew committed Mar 20, 2020
1 parent cfa3529 commit 2e64d21
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 63 deletions.
16 changes: 16 additions & 0 deletions config/oidc/config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,20 @@ message OIDCConfig {
// the Token Endpoint of the OIDC Identity Provider.
// Optional.
string trusted_certificate_authority = 14;

// The Authservice makes two kinds of direct network connections directly to the OIDC Provider.
// Both are POST requests to the configured `token_uri` of the OIDC Provider.
// The first is to exchange the authorization code for tokens, and the other is to use the
// refresh token to obtain new tokens. Configure the `proxy_uri` when
// both of these requests should be made through a web proxy. The format of `proxy_uri` is
// `http://proxyserver.example.com:8080`, where `:<port_number>` is optional.
// Userinfo (usernames and passwords) in the `proxy_uri` setting are not yet supported.
// The `proxy_uri` should always start with `http://`.
// The Authservice will upgrade the connection to the OIDC provider to HTTPS using
// an HTTP CONNECT request to the proxy server. The proxy server will see the hostname and port number
// of the OIDC provider in plain text in the CONNECT request, but all other communication will occur
// over an encrypted HTTPS connection negotiated directly between the Authservice and
// the OIDC provider. See also the related `trusted_certificate_authority` configuration option.
// Optional.
string proxy_uri = 15;
}
64 changes: 55 additions & 9 deletions src/common/http/http.cc
Original file line number Diff line number Diff line change
Expand Up @@ -237,13 +237,20 @@ absl::optional<std::map<std::string, std::string>> Http::DecodeCookies(
}

Uri::Uri(absl::string_view uri) : pathQueryFragment_("/") {
if (uri.find(https_prefix_) != 0) { // must start with https://
throw std::runtime_error(absl::StrCat("uri must be https scheme: ", uri));
std::string scheme_prefix;
if (uri.find(https_prefix_) == 0) {
scheme_ = "https";
scheme_prefix = https_prefix_;
} else if (uri.find(http_prefix_) == 0) {
scheme_ = "http";
scheme_prefix = http_prefix_;
} else {
throw std::runtime_error(absl::StrCat("uri must be http or https scheme: ", uri));
}
if (uri.length() == https_prefix_.length()) {
if (uri.length() == scheme_prefix.length()) {
throw std::runtime_error(absl::StrCat("no host in uri: ", uri));
}
auto uri_without_scheme = uri.substr(https_prefix_.length());
auto uri_without_scheme = uri.substr(scheme_prefix.length());

std::string host_and_port;
auto positions = {uri_without_scheme.find('/'), uri_without_scheme.find('?'), uri_without_scheme.find('#')};
Expand Down Expand Up @@ -280,21 +287,29 @@ Uri::Uri(absl::string_view uri) : pathQueryFragment_("/") {
host_ = std::string(host_and_port.substr(0, colon_position).data(), colon_position);
} else {
host_ = host_and_port;
if (scheme_ == "http") {
port_ = 80;
} else if (scheme_ == "https") {
port_ = 443;
}
}
}

const std::string Uri::https_prefix_ = "https://";
const std::string Uri::http_prefix_ = "http://";

Uri &Uri::operator=(Uri &&uri) noexcept {
host_ = uri.host_;
port_ = uri.port_;
scheme_ = uri.scheme_;
pathQueryFragmentString_ = uri.pathQueryFragmentString_;
pathQueryFragment_ = uri.pathQueryFragment_;
return *this;
}

Uri::Uri(const Uri &uri)
: host_(uri.host_),
scheme_(uri.scheme_),
port_(uri.port_),
pathQueryFragmentString_(uri.pathQueryFragmentString_),
pathQueryFragment_(uri.pathQueryFragment_) {
Expand Down Expand Up @@ -338,8 +353,11 @@ PathQueryFragment::PathQueryFragment(absl::string_view path_query_fragment) {
}

response_t HttpImpl::Post(absl::string_view uri,
const std::map<absl::string_view, absl::string_view> &headers, absl::string_view body,
absl::string_view ca_cert, boost::asio::io_context &ioc,
const std::map<absl::string_view, absl::string_view> &headers,
absl::string_view body,
absl::string_view ca_cert,
absl::string_view proxy_uri,
boost::asio::io_context &ioc,
boost::asio::yield_context yield) const {
spdlog::trace("{}", __func__);
try {
Expand Down Expand Up @@ -368,9 +386,37 @@ response_t HttpImpl::Post(absl::string_view uri,
throw boost::system::error_code{static_cast<int>(::ERR_get_error()),
boost::asio::error::get_ssl_category()};
}
const auto results =
resolver.async_resolve(parsed_uri.GetHost(), std::to_string(parsed_uri.GetPort()), yield);
beast::get_lowest_layer(stream).async_connect(results, yield);

if (!proxy_uri.empty()) {
auto parsed_proxy_uri = http::Uri(proxy_uri);
const auto results = resolver.async_resolve(parsed_proxy_uri.GetHost(), std::to_string(parsed_proxy_uri.GetPort()), yield);
beast::get_lowest_layer(stream).async_connect(results, yield);

std::string target = absl::StrCat(parsed_uri.GetHost(), ":", std::to_string(parsed_uri.GetPort()));
beast::http::request<beast::http::string_body> http_connect_req{beast::http::verb::connect, target, version};
http_connect_req.set(beast::http::field::host, target);

// Send the HTTP connect request to the remote host
beast::http::async_write(stream.next_layer(), http_connect_req, yield);

// Read the response from the server
boost::beast::flat_buffer http_connect_buffer;
beast::http::response<beast::http::empty_body> http_connect_res;
beast::http::parser<false, beast::http::empty_body> p(http_connect_res);
p.skip(true); // skip reading the body of the response because there won't be a body

beast::http::async_read(stream.next_layer(), http_connect_buffer, p, yield);
if (http_connect_res.result() != beast::http::status::ok) {
throw std::runtime_error(
absl::StrCat("http connect failed with status: ", http_connect_res.result_int())
);
}

} else {
const auto results = resolver.async_resolve(parsed_uri.GetHost(), std::to_string(parsed_uri.GetPort()), yield);
beast::get_lowest_layer(stream).async_connect(results, yield);
}

stream.async_handshake(ssl::stream_base::client, yield);
// Set up an HTTP POST request message
beast::http::request<beast::http::string_body> req{
Expand Down
34 changes: 18 additions & 16 deletions src/common/http/http.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ class PathQueryFragment {
class Uri {
private:
static const std::string https_prefix_;
static const std::string http_prefix_;
std::string host_;
int32_t port_ = 443;
std::string scheme_;
int32_t port_;
std::string pathQueryFragmentString_; // includes the path, query, and fragment (if any)
PathQueryFragment pathQueryFragment_;

Expand All @@ -69,7 +71,7 @@ class Uri {

Uri &operator=(Uri &&uri) noexcept;

inline std::string GetScheme() { return "https"; }
inline std::string GetScheme() { return scheme_; }

inline std::string GetHost() { return host_; }

Expand Down Expand Up @@ -185,27 +187,27 @@ class Http {
* @param ca_cert the ca cert to be trusted in the http call
* @return http response.
*/
virtual response_t Post(
absl::string_view uri,
const std::map<absl::string_view, absl::string_view> &headers,
absl::string_view body,
absl::string_view ca_cert,
boost::asio::io_context &ioc,
boost::asio::yield_context yield) const = 0;
virtual response_t Post(absl::string_view uri,
const std::map<absl::string_view, absl::string_view> &headers,
absl::string_view body,
absl::string_view ca_cert,
absl::string_view proxy_uri,
boost::asio::io_context &ioc,
boost::asio::yield_context yield) const = 0;
};

/**
* HTTP request implementation
*/
class HttpImpl : public Http {
public:
response_t Post(
absl::string_view uri,
const std::map<absl::string_view, absl::string_view> &headers,
absl::string_view body,
absl::string_view ca_cert,
boost::asio::io_context &ioc,
boost::asio::yield_context yield) const override;
response_t Post(absl::string_view uri,
const std::map<absl::string_view, absl::string_view> &headers,
absl::string_view body,
absl::string_view ca_cert,
absl::string_view proxy_uri,
boost::asio::io_context &ioc,
boost::asio::yield_context yield) const override;
};

} // namespace http
Expand Down
19 changes: 14 additions & 5 deletions src/config/get_config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
#include "spdlog/spdlog.h"
#include <fstream>
#include <iostream>
#include <sstream>
#include "config/config.pb.validate.h"
#include "src/common/http/http.h"
#include "absl/strings/string_view.h"
Expand All @@ -16,16 +15,22 @@ using namespace google::protobuf::util;
namespace authservice {
namespace config {

void ValidateUri(absl::string_view uri, absl::string_view uri_name) {
void ValidateUri(absl::string_view uri, absl::string_view uri_name, absl::string_view required_scheme) {
unique_ptr<common::http::Uri> parsed_uri;
try {
parsed_uri = unique_ptr<common::http::Uri>(new common::http::Uri(uri));
} catch (runtime_error &e) {
if (std::string(e.what()).find("uri must be http or https scheme") != std::string::npos) {
throw runtime_error(fmt::format("invalid {}: uri must be {} scheme: {}", uri_name, required_scheme, uri));
}
throw runtime_error(fmt::format("invalid {}: ", uri_name) + e.what());
}
if (parsed_uri->HasQuery() || parsed_uri->HasFragment()) {
throw runtime_error(fmt::format("invalid {}: query params and fragments not allowed: {}", uri_name, uri));
}
if (parsed_uri->GetScheme() != required_scheme) {
throw runtime_error(fmt::format("invalid {}: uri must be {} scheme: {}", uri_name, required_scheme, uri));
}
}

unique_ptr<Config> GetConfig(const string &configFileName) {
Expand All @@ -49,9 +54,13 @@ unique_ptr<Config> GetConfig(const string &configFileName) {
}

for (const auto &chain : config->chains()) {
ValidateUri(chain.filters(0).oidc().authorization_uri(), "authorization_uri");
ValidateUri(chain.filters(0).oidc().callback_uri(), "callback_uri");
ValidateUri(chain.filters(0).oidc().token_uri(), "token_uri");
ValidateUri(chain.filters(0).oidc().authorization_uri(), "authorization_uri", "https");
ValidateUri(chain.filters(0).oidc().callback_uri(), "callback_uri", "https");
ValidateUri(chain.filters(0).oidc().token_uri(), "token_uri", "https");
const auto proxy_uri = chain.filters(0).oidc().proxy_uri();
if (!proxy_uri.empty()) {
ValidateUri(proxy_uri, "proxy_uri", "http");
}
}

return config;
Expand Down
4 changes: 2 additions & 2 deletions src/filters/oidc/oidc_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ std::shared_ptr<TokenResponse> OidcFilter::RefreshToken(
spdlog::info("{}: POSTing to refresh access token", __func__);
auto retrieved_token_response = http_ptr_->Post(
idp_config_.token_uri(), headers, common::http::Http::EncodeFormData(params),
idp_config_.trusted_certificate_authority(), ioc, yield);
idp_config_.trusted_certificate_authority(), idp_config_.proxy_uri(), ioc, yield);

if (retrieved_token_response == nullptr) {
spdlog::warn("{}: Received null pointer as response from identity provider.", __func__);
Expand Down Expand Up @@ -512,7 +512,7 @@ google::rpc::Code OidcFilter::RetrieveToken(

auto retrieve_token_response = http_ptr_->Post(
idp_config_.token_uri(), headers, common::http::Http::EncodeFormData(params),
idp_config_.trusted_certificate_authority(), ioc, yield);
idp_config_.trusted_certificate_authority(), idp_config_.proxy_uri(), ioc, yield);
if (retrieve_token_response == nullptr) {
spdlog::info("{}: HTTP error encountered: {}", __func__,
"IdP connection error");
Expand Down
23 changes: 21 additions & 2 deletions test/common/http/http_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,15 @@ TEST(Http, ParseUri) {
ASSERT_EQ(result.GetQuery(), "");
ASSERT_EQ(result.GetFragment(), "");

result = Uri("http://www.example.com:1234");
ASSERT_EQ(result.GetScheme(), "http");
ASSERT_EQ(result.GetHost(), "www.example.com");
ASSERT_EQ(result.GetPort(), 1234);
ASSERT_EQ(result.GetPathQueryFragment(), "/");
ASSERT_EQ(result.GetPath(), "/");
ASSERT_EQ(result.GetQuery(), "");
ASSERT_EQ(result.GetFragment(), "");

result = Uri("https://www.example.com:1234/path");
ASSERT_EQ(result.GetScheme(), "https");
ASSERT_EQ(result.GetHost(), "www.example.com");
Expand Down Expand Up @@ -236,9 +245,19 @@ TEST(Http, ParseUri) {
ASSERT_EQ(result.GetQuery(), "query");
ASSERT_EQ(result.GetFragment(), "frag/?ment");

ASSERT_THROWS_STD_RUNTIME_ERROR([]() -> void { Uri("noscheme"); }, "uri must be https scheme: noscheme");
ASSERT_THROWS_STD_RUNTIME_ERROR([]() -> void { Uri("not_https://host"); }, "uri must be https scheme: not_https://host");
result = Uri("http://www.example.com?query#frag/?ment");
ASSERT_EQ(result.GetScheme(), "http");
ASSERT_EQ(result.GetHost(), "www.example.com");
ASSERT_EQ(result.GetPort(), 80);
ASSERT_EQ(result.GetPathQueryFragment(), "/?query#frag/?ment");
ASSERT_EQ(result.GetPath(), "/");
ASSERT_EQ(result.GetQuery(), "query");
ASSERT_EQ(result.GetFragment(), "frag/?ment");

ASSERT_THROWS_STD_RUNTIME_ERROR([]() -> void { Uri("noscheme"); }, "uri must be http or https scheme: noscheme");
ASSERT_THROWS_STD_RUNTIME_ERROR([]() -> void { Uri("ftp://host"); }, "uri must be http or https scheme: ftp://host");
ASSERT_THROWS_STD_RUNTIME_ERROR([]() -> void { Uri("https://"); }, "no host in uri: https://"); // no host
ASSERT_THROWS_STD_RUNTIME_ERROR([]() -> void { Uri("http://"); }, "no host in uri: http://"); // no host
ASSERT_THROWS_STD_RUNTIME_ERROR([]() -> void { Uri("https://:80/path"); }, "no host in uri: https://:80/path"); // no host
ASSERT_THROWS_STD_RUNTIME_ERROR([]() -> void { Uri("https://host:/path"); }, "port not valid in uri: https://host:/path"); // colon, but no port
ASSERT_THROWS_STD_RUNTIME_ERROR([]() -> void { Uri("https://host:a8/path"); }, "port not valid in uri: https://host:a8/path"); // port not an int
Expand Down
3 changes: 2 additions & 1 deletion test/common/http/mocks.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ namespace common {
namespace http {
class HttpMock : public Http {
public:
MOCK_CONST_METHOD6(Post, response_t(
MOCK_CONST_METHOD7(Post, response_t(
absl::string_view uri,
const std::map<absl::string_view, absl::string_view> &headers,
absl::string_view body,
absl::string_view ca_cert,
absl::string_view proxy_uri,
boost::asio::io_context &ioc,
boost::asio::yield_context yield));
};
Expand Down

0 comments on commit 2e64d21

Please sign in to comment.