Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions include/envoy/router/router.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ class DirectResponseEntry {
* or an empty string otherwise.
*/
virtual std::string newPath(const Http::HeaderMap& headers) const PURE;

/**
* Returns the response body to send with direct responses.
* @return std::string& the response body specified in the route configuration,
* or an empty string if no response body is specified.
*/
virtual const std::string& responseBody() const PURE;
};

/**
Expand Down
9 changes: 9 additions & 0 deletions source/common/filesystem/filesystem_impl.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "common/filesystem/filesystem_impl.h"

#include <dirent.h>
#include <sys/stat.h>

#include <chrono>
#include <cstdint>
Expand Down Expand Up @@ -39,6 +40,14 @@ bool directoryExists(const std::string& path) {
return dir_exists;
}

ssize_t fileSize(const std::string& path) {
struct stat info;
if (stat(path.c_str(), &info) != 0) {
return -1;
}
return info.st_size;
}

std::string fileReadToEnd(const std::string& path) {
std::ios::sync_with_stdio(false);

Expand Down
9 changes: 9 additions & 0 deletions source/common/filesystem/filesystem_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <cstdlib>
#include <mutex>
#include <string>

Expand Down Expand Up @@ -41,8 +42,16 @@ bool fileExists(const std::string& path);
*/
bool directoryExists(const std::string& path);

/**
* @return ssize_t the size in bytes of the specified file, or -1 if the file size
* cannot be determined for any reason, including without limitation
* the non-existence of the file.
*/
ssize_t fileSize(const std::string& path);

/**
* @return full file content as a string.
* @throw EnvoyException if the file cannot be read.
* Be aware, this is not most highly performing file reading method.
*/
std::string fileReadToEnd(const std::string& path);
Expand Down
2 changes: 2 additions & 0 deletions source/common/router/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ envoy_cc_library(
"//source/common/common:assert_lib",
"//source/common/common:empty_string",
"//source/common/config:rds_json_lib",
"//source/common/filesystem:filesystem_lib",
"//source/common/http:headers_lib",
"//source/common/protobuf:utility_lib",
],
Expand Down Expand Up @@ -157,6 +158,7 @@ envoy_cc_library(
"//source/common/common:hex_lib",
"//source/common/common:logger_lib",
"//source/common/common:utility_lib",
"//source/common/filesystem:filesystem_lib",
"//source/common/grpc:common_lib",
"//source/common/http:codes_lib",
"//source/common/http:header_map_lib",
Expand Down
3 changes: 2 additions & 1 deletion source/common/router/config_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,8 @@ RouteEntryImplBase::RouteEntryImplBase(const VirtualHostImpl& vhost,
response_headers_parser_(HeaderParser::configure(route.route().response_headers_to_add(),
route.route().response_headers_to_remove())),
opaque_config_(parseOpaqueConfig(route)), decorator_(parseDecorator(route)),
direct_response_code_(ConfigUtility::parseDirectResponseCode(route)) {
direct_response_code_(ConfigUtility::parseDirectResponseCode(route)),
direct_response_body_(ConfigUtility::parseDirectResponseBody(route)) {
if (route.route().has_metadata_match()) {
const auto filter_it = route.route().metadata_match().filter_metadata().find(
Envoy::Config::MetadataFilters::get().ENVOY_LB);
Expand Down
6 changes: 6 additions & 0 deletions source/common/router/config_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class SslRedirector : public DirectResponseEntry {
// Router::DirectResponseEntry
std::string newPath(const Http::HeaderMap& headers) const override;
Http::Code responseCode() const override { return Http::Code::MovedPermanently; }
const std::string& responseBody() const override { return EMPTY_STRING; }
};

class SslRedirectRoute : public Route {
Expand Down Expand Up @@ -290,6 +291,9 @@ class RouteEntryImplBase : public RouteEntry,
public Route,
public std::enable_shared_from_this<RouteEntryImplBase> {
public:
/**
* @throw EnvoyException with reason if the route configuration contains any errors
*/
RouteEntryImplBase(const VirtualHostImpl& vhost, const envoy::api::v2::Route& route,
Runtime::Loader& loader);

Expand Down Expand Up @@ -334,6 +338,7 @@ class RouteEntryImplBase : public RouteEntry,
// Router::DirectResponseEntry
std::string newPath(const Http::HeaderMap& headers) const override;
Http::Code responseCode() const override { return direct_response_code_.value(); }
const std::string& responseBody() const override { return direct_response_body_; }

// Router::Route
const DirectResponseEntry* directResponseEntry() const override;
Expand Down Expand Up @@ -492,6 +497,7 @@ class RouteEntryImplBase : public RouteEntry,

const DecoratorConstPtr decorator_;
const Optional<Http::Code> direct_response_code_;
std::string direct_response_body_;
};

/**
Expand Down
33 changes: 33 additions & 0 deletions source/common/router/config_utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <vector>

#include "common/common/assert.h"
#include "common/filesystem/filesystem_impl.h"

namespace Envoy {
namespace Router {
Expand Down Expand Up @@ -98,6 +99,38 @@ Optional<Http::Code> ConfigUtility::parseDirectResponseCode(const envoy::api::v2
return Optional<Http::Code>();
}

std::string ConfigUtility::parseDirectResponseBody(const envoy::api::v2::Route& route) {
if (!route.has_direct_response() || !route.direct_response().has_body()) {
return EMPTY_STRING;
}
const auto& body = route.direct_response().body();
std::string filename = body.filename();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: const here and similar below

if (!filename.empty()) {
static const ssize_t kMaxFileSize = 4096;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: in general we don't prefix constants with 'k'. MaxfileSize is fine.

if (!Filesystem::fileExists(filename)) {
throw EnvoyException(fmt::format("response body file {} does not exist", filename));
}
ssize_t size = Filesystem::fileSize(filename);
if (size < 0) {
throw EnvoyException(fmt::format("cannot determine size of response body file {}", filename));
}
if (size > kMaxFileSize) {
throw EnvoyException(fmt::format("response body file {} size is {} bytes; maximum is {}",
filename, kMaxFileSize));
}
return Filesystem::fileReadToEnd(filename);
}
std::string inline_bytes = body.inline_bytes();
if (!inline_bytes.empty()) {
return inline_bytes;
}
std::string inline_string = body.inline_string();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you don't really need the extra logic below here. At this point you can just return inline_string.

if (!inline_string.empty()) {
return inline_string;
}
return EMPTY_STRING;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for being late to the party, but:

  1. Why did everybody think that 4KB limit is a good idea?
  2. Iff it's a good idea, then why does it only apply to response body from file, but not to response body inlined in the config?

cc @mattklein123 @alyssawilk

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exact number was chosen by @brian-pane. He can comment. It's probably arbitrary. The main idea here is to provide some sanity check to avoid people from streaming huge files which won't reliably work without a lot of effort around buffering, flow control, etc. We should probably apply the same limit whether inline or by file I agree. @brian-pane can you do a follow up?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PiotrSikora the idea of using a fixed limit arose from a Slack discussion in #envoy-dev on Jan 5th. My original plan was to use sendfile, but Matt had (quite reasonable) concerns about how much complexity that would add to the proxy core, so we settled on reading the file into memory but limiting its size.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, PR 2458 now contains a fix to apply the same 4KB limit to both inline and file-sourced response bodies.


Http::Code ConfigUtility::parseClusterNotFoundResponseCode(
const envoy::api::v2::RouteAction::ClusterNotFoundResponseCode& code) {
switch (code) {
Expand Down
10 changes: 10 additions & 0 deletions source/common/router/config_utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ class ConfigUtility {
*/
static Optional<Http::Code> parseDirectResponseCode(const envoy::api::v2::Route& route);

/**
* Returns the content of the response body to send with direct responses from a route.
* @param route supplies the Route configuration.
* @return Optional<std::string> the response body provided inline in the route's
* direct_response if specified, or the contents of the file named in the
* route's direct_response if specified, or an empty string otherwise.
* @throw EnvoyException if the route configuration contains an error.
*/
static std::string parseDirectResponseBody(const envoy::api::v2::Route& route);

/**
* Returns the HTTP Status Code enum parsed from proto.
* @param code supplies the ClusterNotFoundResponseCode enum.
Expand Down
10 changes: 8 additions & 2 deletions source/common/router/router.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "common/common/empty_string.h"
#include "common/common/enum_to_int.h"
#include "common/common/utility.h"
#include "common/filesystem/filesystem_impl.h"
#include "common/grpc/common.h"
#include "common/http/codes.h"
#include "common/http/header_map_impl.h"
Expand Down Expand Up @@ -219,8 +220,13 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::HeaderMap& headers, bool e
return Http::FilterHeadersStatus::StopIteration;
}
config_.stats_.rq_direct_response_.inc();
sendLocalReply(route_->directResponseEntry()->responseCode(), "", false);
// TODO(brian-pane) support sending a response body and response_headers_to_add.
std::string body;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const std::string& body = route_->directResponseEntry()->responseBody(); ?

Optional<std::string> inline_body = route_->directResponseEntry()->responseBody();
if (inline_body.valid()) {
body = inline_body.value();
}
sendLocalReply(route_->directResponseEntry()->responseCode(), body, false);
// TODO(brian-pane) support sending response_headers_to_add.
return Http::FilterHeadersStatus::StopIteration;
}

Expand Down
8 changes: 8 additions & 0 deletions test/common/filesystem/filesystem_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ TEST(FileSystemImpl, directoryExists) {
EXPECT_FALSE(Filesystem::directoryExists("/dev/blahblah"));
}

TEST(FileSystemImpl, fileSize) {
EXPECT_EQ(0, Filesystem::fileSize("/dev/null"));
EXPECT_EQ(-1, Filesystem::fileSize("/dev/blahblahblah"));
const std::string data = "test string\ntest";
const std::string file_path = TestEnvironment::writeStringToFileForTest("test_envoy", data);
EXPECT_EQ(data.length(), Filesystem::fileSize(file_path));
}

TEST(FileSystemImpl, fileReadToEndSuccess) {
const std::string data = "test string\ntest";
const std::string file_path = TestEnvironment::writeStringToFileForTest("test_envoy", data);
Expand Down
2 changes: 2 additions & 0 deletions test/common/router/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ envoy_cc_test(
"//source/common/router:config_lib",
"//test/mocks/runtime:runtime_mocks",
"//test/mocks/upstream:upstream_mocks",
"//test/test_common:environment_lib",
"//test/test_common:utility_lib",
],
)
Expand Down Expand Up @@ -91,6 +92,7 @@ envoy_cc_test(
"//test/mocks/runtime:runtime_mocks",
"//test/mocks/ssl:ssl_mocks",
"//test/mocks/upstream:upstream_mocks",
"//test/test_common:environment_lib",
"//test/test_common:utility_lib",
],
)
Expand Down
40 changes: 39 additions & 1 deletion test/common/router/config_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

#include "test/mocks/runtime/mocks.h"
#include "test/mocks/upstream/mocks.h"
#include "test/test_common/environment.h"
#include "test/test_common/printers.h"
#include "test/test_common/utility.h"

Expand Down Expand Up @@ -2188,6 +2189,9 @@ TEST(RouteMatcherTest, DirectResponse) {
}
)EOF";

const auto pathname =
TestEnvironment::writeStringToFileForTest("direct_response_body", "Example text 3");

// A superset of v1_json, with API v2 direct-response configuration added.
static const std::string v2_yaml = R"EOF(
name: foo
Expand Down Expand Up @@ -2217,7 +2221,21 @@ name: foo
domains: [direct.example.com]
routes:
- match: { prefix: /gone }
direct_response: { status: 410 }
direct_response:
status: 410
body: { inline_bytes: "RXhhbXBsZSB0ZXh0IDE=" }
- match: { prefix: /error }
direct_response:
status: 500
body: { inline_string: "Example text 2" }
- match: { prefix: /no_body }
direct_response:
status: 200
- match: { prefix: /static }
direct_response:
status: 200
body: { filename: )EOF" + pathname +
R"EOF(}
- match: { prefix: / }
route: { cluster: www2 }
)EOF";
Expand Down Expand Up @@ -2267,6 +2285,26 @@ name: foo
Http::TestHeaderMapImpl headers =
genRedirectHeaders("direct.example.com", "/gone", true, false);
EXPECT_EQ(Http::Code::Gone, config.route(headers, 0)->directResponseEntry()->responseCode());
EXPECT_EQ("Example text 1", config.route(headers, 0)->directResponseEntry()->responseBody());
}
{
Http::TestHeaderMapImpl headers =
genRedirectHeaders("direct.example.com", "/error", true, false);
EXPECT_EQ(Http::Code::InternalServerError,
config.route(headers, 0)->directResponseEntry()->responseCode());
EXPECT_EQ("Example text 2", config.route(headers, 0)->directResponseEntry()->responseBody());
}
{
Http::TestHeaderMapImpl headers =
genRedirectHeaders("direct.example.com", "/no_body", true, false);
EXPECT_EQ(Http::Code::OK, config.route(headers, 0)->directResponseEntry()->responseCode());
EXPECT_TRUE(config.route(headers, 0)->directResponseEntry()->responseBody().empty());
}
{
Http::TestHeaderMapImpl headers =
genRedirectHeaders("direct.example.com", "/static", true, false);
EXPECT_EQ(Http::Code::OK, config.route(headers, 0)->directResponseEntry()->responseCode());
EXPECT_EQ("Example text 3", config.route(headers, 0)->directResponseEntry()->responseBody());
}
{
Http::TestHeaderMapImpl headers =
Expand Down
24 changes: 23 additions & 1 deletion test/common/router/router_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <string>

#include "common/buffer/buffer_impl.h"
#include "common/common/empty_string.h"
#include "common/network/utility.h"
#include "common/router/router.h"
#include "common/upstream/upstream_impl.h"
Expand All @@ -15,6 +16,7 @@
#include "test/mocks/runtime/mocks.h"
#include "test/mocks/ssl/mocks.h"
#include "test/mocks/upstream/mocks.h"
#include "test/test_common/environment.h"
#include "test/test_common/printers.h"
#include "test/test_common/utility.h"

Expand All @@ -28,6 +30,7 @@ using testing::AtLeast;
using testing::Invoke;
using testing::MockFunction;
using testing::NiceMock;
using testing::Ref;
using testing::Return;
using testing::ReturnRef;
using testing::SaveArg;
Expand Down Expand Up @@ -1552,8 +1555,9 @@ TEST_F(RouterTest, RedirectFound) {
}

TEST_F(RouterTest, DirectResponse) {
MockDirectResponseEntry direct_response;
NiceMock<MockDirectResponseEntry> direct_response;
EXPECT_CALL(direct_response, responseCode()).WillRepeatedly(Return(Http::Code::OK));
EXPECT_CALL(direct_response, responseBody()).WillRepeatedly(ReturnRef(EMPTY_STRING));
EXPECT_CALL(*callbacks_.route_, directResponseEntry()).WillRepeatedly(Return(&direct_response));

Http::TestHeaderMapImpl response_headers{{":status", "200"}};
Expand All @@ -1565,6 +1569,24 @@ TEST_F(RouterTest, DirectResponse) {
EXPECT_EQ(1UL, config_.stats_.rq_direct_response_.value());
}

TEST_F(RouterTest, DirectResponseWithBody) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get an integration test for one of these, just to make sure end to end with bodies works smoothly?

NiceMock<MockDirectResponseEntry> direct_response;
EXPECT_CALL(direct_response, responseCode()).WillRepeatedly(Return(Http::Code::OK));
const std::string response_body("static response");
EXPECT_CALL(direct_response, responseBody()).WillRepeatedly(ReturnRef(response_body));
EXPECT_CALL(*callbacks_.route_, directResponseEntry()).WillRepeatedly(Return(&direct_response));

Http::TestHeaderMapImpl response_headers{
{":status", "200"}, {"content-length", "15"}, {"content-type", "text/plain"}};
EXPECT_CALL(callbacks_, encodeHeaders_(HeaderMapEqualRef(&response_headers), false));
EXPECT_CALL(callbacks_, encodeData(_, true));
Http::TestHeaderMapImpl headers;
HttpTestUtility::addDefaultHeaders(headers);
router_.decodeHeaders(headers, true);
EXPECT_TRUE(verifyHostUpstreamStats(0, 0));
EXPECT_EQ(1UL, config_.stats_.rq_direct_response_.value());
}

TEST(RouterFilterUtilityTest, finalTimeout) {
{
NiceMock<MockRouteEntry> route;
Expand Down
1 change: 1 addition & 0 deletions test/mocks/router/mocks.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class MockDirectResponseEntry : public DirectResponseEntry {
// DirectResponseEntry
MOCK_CONST_METHOD1(newPath, std::string(const Http::HeaderMap& headers));
MOCK_CONST_METHOD0(responseCode, Http::Code());
MOCK_CONST_METHOD0(responseBody, const std::string&());
};

class TestCorsPolicy : public CorsPolicy {
Expand Down