diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8db93fadc63f..c4ee1b7c773c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -250,6 +250,12 @@ updates: interval: daily time: "06:00" +- package-ecosystem: "gomod" + directory: "/contrib/golang/filters/http/test/test_data/property" + schedule: + interval: daily + time: "06:00" + - package-ecosystem: "gomod" directory: "/contrib/golang/router/cluster_specifier/test/test_data/simple" schedule: diff --git a/contrib/golang/common/go/api/api.h b/contrib/golang/common/go/api/api.h index 60b8e9e763f9..312916ad0528 100644 --- a/contrib/golang/common/go/api/api.h +++ b/contrib/golang/common/go/api/api.h @@ -47,6 +47,8 @@ typedef enum { // NOLINT(modernize-use-using) CAPIInvalidPhase = -4, CAPIValueNotFound = -5, CAPIYield = -6, + CAPIInternalFailure = -7, + CAPISerializationFailure = -8, } CAPIStatus; CAPIStatus envoyGoFilterHttpContinue(void* r, int status); @@ -82,6 +84,7 @@ void envoyGoConfigHttpFinalize(void* c); CAPIStatus envoyGoFilterHttpSetStringFilterState(void* r, void* key, void* value, int state_type, int life_span, int stream_sharing); CAPIStatus envoyGoFilterHttpGetStringFilterState(void* r, void* key, void* value); +CAPIStatus envoyGoFilterHttpGetStringProperty(void* r, void* key, void* value, int* rc); CAPIStatus envoyGoFilterHttpDefineMetric(void* c, uint32_t metric_type, void* name, void* metric_id); diff --git a/contrib/golang/common/go/api/capi.go b/contrib/golang/common/go/api/capi.go index e17421d74b55..6bd99ea0294e 100644 --- a/contrib/golang/common/go/api/capi.go +++ b/contrib/golang/common/go/api/capi.go @@ -54,6 +54,8 @@ type HttpCAPI interface { HttpSetStringFilterState(r unsafe.Pointer, key string, value string, stateType StateType, lifeSpan LifeSpan, streamSharing StreamSharing) HttpGetStringFilterState(r unsafe.Pointer, key string) string + HttpGetStringProperty(r unsafe.Pointer, key string) (string, error) + HttpDefineMetric(c unsafe.Pointer, metricType MetricType, name string) uint32 HttpIncrementMetric(c unsafe.Pointer, metricId uint32, offset int64) HttpGetMetric(c unsafe.Pointer, metricId uint32) uint64 diff --git a/contrib/golang/common/go/api/filter.go b/contrib/golang/common/go/api/filter.go index ad64672a7bac..7cdec9908433 100644 --- a/contrib/golang/common/go/api/filter.go +++ b/contrib/golang/common/go/api/filter.go @@ -129,6 +129,9 @@ type StreamInfo interface { FilterState() FilterState // VirtualClusterName returns the name of the virtual cluster which got matched VirtualClusterName() (string, bool) + + // Some fields in stream info can be fetched via GetProperty + // For example, startTime() is equal to GetProperty("request.time") } type StreamFilterCallbacks interface { @@ -144,6 +147,13 @@ type FilterCallbacks interface { RecoverPanic() Log(level LogType, msg string) LogLevel() LogType + // GetProperty fetch Envoy attribute and return the value as a string. + // The list of attributes can be found in https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes. + // If the fetch succeeded, a string will be returned. + // If the value is a timestamp, it is returned as a timestamp string like "2023-07-31T07:21:40.695646+00:00". + // If the fetch failed (including the value is not found), an error will be returned. + // Currently, fetching requests/response attributes are mostly unsupported. + GetProperty(key string) (string, error) // TODO add more for filter callbacks } diff --git a/contrib/golang/filters/http/source/BUILD b/contrib/golang/filters/http/source/BUILD index 35c187b84911..d15a13236aa6 100644 --- a/contrib/golang/filters/http/source/BUILD +++ b/contrib/golang/filters/http/source/BUILD @@ -37,6 +37,18 @@ envoy_cc_library( "//source/common/http:headers_lib", "//source/common/http:utility_lib", "//source/common/http/http1:codec_lib", + "//source/common/protobuf:utility_lib", + "//source/extensions/filters/common/expr:cel_state_lib", + "//source/extensions/filters/common/expr:evaluator_lib", + "@com_google_cel_cpp//eval/public:activation", + "@com_google_cel_cpp//eval/public:builtin_func_registrar", + "@com_google_cel_cpp//eval/public:cel_expr_builder_factory", + "@com_google_cel_cpp//eval/public:cel_value", + "@com_google_cel_cpp//eval/public:value_export_util", + "@com_google_cel_cpp//eval/public/containers:field_access", + "@com_google_cel_cpp//eval/public/containers:field_backed_list_impl", + "@com_google_cel_cpp//eval/public/containers:field_backed_map_impl", + "@com_google_cel_cpp//eval/public/structs:cel_proto_wrapper", "@envoy_api//contrib/envoy/extensions/filters/http/golang/v3alpha:pkg_cc_proto", ], ) @@ -77,6 +89,9 @@ envoy_cc_library( "//source/common/http:headers_lib", "//source/common/http:utility_lib", "//source/common/http/http1:codec_lib", + "//source/common/protobuf:utility_lib", + "//source/extensions/filters/common/expr:cel_state_lib", + "//source/extensions/filters/common/expr:evaluator_lib", "@envoy_api//contrib/envoy/extensions/filters/http/golang/v3alpha:pkg_cc_proto", ], ) diff --git a/contrib/golang/filters/http/source/cgo.cc b/contrib/golang/filters/http/source/cgo.cc index 34fc2fdbc4ad..e4035beed84f 100644 --- a/contrib/golang/filters/http/source/cgo.cc +++ b/contrib/golang/filters/http/source/cgo.cc @@ -268,6 +268,15 @@ CAPIStatus envoyGoFilterHttpGetStringFilterState(void* r, void* key, void* value }); } +CAPIStatus envoyGoFilterHttpGetStringProperty(void* r, void* key, void* value, int* rc) { + return envoyGoFilterHandlerWrapper( + r, [key, value, rc](std::shared_ptr& filter) -> CAPIStatus { + auto key_str = referGoString(key); + auto value_str = reinterpret_cast(value); + return filter->getStringProperty(key_str, value_str, rc); + }); +} + CAPIStatus envoyGoFilterHttpDefineMetric(void* c, uint32_t metric_type, void* name, void* metric_id) { return envoyGoConfigHandlerWrapper( diff --git a/contrib/golang/filters/http/source/go/pkg/http/capi_impl.go b/contrib/golang/filters/http/source/go/pkg/http/capi_impl.go index 8bf7473171f6..a758af0556d4 100644 --- a/contrib/golang/filters/http/source/go/pkg/http/capi_impl.go +++ b/contrib/golang/filters/http/source/go/pkg/http/capi_impl.go @@ -32,6 +32,7 @@ package http */ import "C" import ( + "errors" "reflect" "runtime" "strings" @@ -61,21 +62,37 @@ const ( type httpCApiImpl struct{} -// Only CAPIOK is expected, otherwise, it means unexpected stage when invoke C API, +// When the status means unexpected stage when invoke C API, // panic here and it will be recover in the Go entry function. func handleCApiStatus(status C.CAPIStatus) { switch status { - case C.CAPIOK: - return + case C.CAPIFilterIsGone, + C.CAPIFilterIsDestroy, + C.CAPINotInGo, + C.CAPIInvalidPhase: + panic(capiStatusToStr(status)) + } +} + +func capiStatusToStr(status C.CAPIStatus) string { + switch status { case C.CAPIFilterIsGone: - panic(errRequestFinished) + return errRequestFinished case C.CAPIFilterIsDestroy: - panic(errFilterDestroyed) + return errFilterDestroyed case C.CAPINotInGo: - panic(errNotInGo) + return errNotInGo case C.CAPIInvalidPhase: - panic(errInvalidPhase) + return errInvalidPhase + case C.CAPIValueNotFound: + return errValueNotFound + case C.CAPIInternalFailure: + return errInternalFailure + case C.CAPISerializationFailure: + return errSerializationFailure } + + return "unknown status" } func (c *httpCApiImpl) HttpContinue(r unsafe.Pointer, status uint64) { @@ -324,6 +341,31 @@ func (c *httpCApiImpl) HttpGetStringFilterState(rr unsafe.Pointer, key string) s return strings.Clone(value) } +func (c *httpCApiImpl) HttpGetStringProperty(rr unsafe.Pointer, key string) (string, error) { + r := (*httpRequest)(rr) + var value string + var rc int + r.mutex.Lock() + defer r.mutex.Unlock() + r.sema.Add(1) + res := C.envoyGoFilterHttpGetStringProperty(unsafe.Pointer(r.req), unsafe.Pointer(&key), + unsafe.Pointer(&value), (*C.int)(unsafe.Pointer(&rc))) + if res == C.CAPIYield { + atomic.AddInt32(&r.waitingOnEnvoy, 1) + r.sema.Wait() + res = C.CAPIStatus(rc) + } else { + r.sema.Done() + handleCApiStatus(res) + } + + if res == C.CAPIOK { + return strings.Clone(value), nil + } + + return "", errors.New(capiStatusToStr(res)) +} + func (c *httpCApiImpl) HttpDefineMetric(cfg unsafe.Pointer, metricType api.MetricType, name string) uint32 { var value uint32 diff --git a/contrib/golang/filters/http/source/go/pkg/http/filter.go b/contrib/golang/filters/http/source/go/pkg/http/filter.go index 1b0c71ddcd8d..e33f099f2469 100644 --- a/contrib/golang/filters/http/source/go/pkg/http/filter.go +++ b/contrib/golang/filters/http/source/go/pkg/http/filter.go @@ -130,6 +130,10 @@ func (r *httpRequest) LogLevel() api.LogType { return cAPI.HttpLogLevel() } +func (r *httpRequest) GetProperty(key string) (string, error) { + return cAPI.HttpGetStringProperty(unsafe.Pointer(r), key) +} + func (r *httpRequest) StreamInfo() api.StreamInfo { return &streamInfo{ request: r, diff --git a/contrib/golang/filters/http/source/go/pkg/http/type.go b/contrib/golang/filters/http/source/go/pkg/http/type.go index 8b975cb26368..d433f579ddb6 100644 --- a/contrib/golang/filters/http/source/go/pkg/http/type.go +++ b/contrib/golang/filters/http/source/go/pkg/http/type.go @@ -31,6 +31,10 @@ const ( errFilterDestroyed = "golang filter has been destroyed" errNotInGo = "not proccessing Go" errInvalidPhase = "invalid phase, maybe headers/buffer already continued" + + errInternalFailure = "internal failure" + errValueNotFound = "value not found" + errSerializationFailure = "serialization failure" ) // api.HeaderMap diff --git a/contrib/golang/filters/http/source/golang_filter.cc b/contrib/golang/filters/http/source/golang_filter.cc index fd36ff400829..0a3168a13f20 100644 --- a/contrib/golang/filters/http/source/golang_filter.cc +++ b/contrib/golang/filters/http/source/golang_filter.cc @@ -18,6 +18,13 @@ #include "source/common/grpc/status.h" #include "source/common/http/headers.h" #include "source/common/http/http1/codec_impl.h" +#include "source/extensions/filters/common/expr/context.h" + +#include "eval/public/cel_value.h" +#include "eval/public/containers/field_access.h" +#include "eval/public/containers/field_backed_list_impl.h" +#include "eval/public/containers/field_backed_map_impl.h" +#include "eval/public/structs/cel_proto_wrapper.h" namespace Envoy { namespace Extensions { @@ -46,6 +53,8 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, ENVOY_LOG(debug, "golang filter decodeHeaders, state: {}, phase: {}, end_stream: {}", state.stateStr(), state.phaseStr(), end_stream); + request_headers_ = &headers; + state.setEndStream(end_stream); bool done = doHeaders(state, headers, end_stream); @@ -1224,6 +1233,192 @@ CAPIStatus Filter::getStringFilterState(absl::string_view key, GoString* value_s return CAPIStatus::CAPIOK; } +CAPIStatus Filter::getStringProperty(absl::string_view path, GoString* value_str, int* rc) { + // lock until this function return since it may running in a Go thread. + Thread::LockGuard lock(mutex_); + if (has_destroyed_) { + ENVOY_LOG(debug, "golang filter has been destroyed"); + return CAPIStatus::CAPIFilterIsDestroy; + } + + auto& state = getProcessorState(); + if (!state.isProcessingInGo()) { + ENVOY_LOG(debug, "golang filter is not processing Go"); + return CAPIStatus::CAPINotInGo; + } + + // to access the headers_ and its friends we need to hold the lock + activation_request_headers_ = dynamic_cast(request_headers_); + if (enter_encoding_) { + activation_response_headers_ = dynamic_cast(headers_); + activation_response_trailers_ = dynamic_cast(trailers_); + } + + if (state.isThreadSafe()) { + return getStringPropertyCommon(path, value_str, state); + } + + auto weak_ptr = weak_from_this(); + state.getDispatcher().post([this, &state, weak_ptr, path, value_str, rc] { + if (!weak_ptr.expired() && !hasDestroyed()) { + *rc = getStringPropertyCommon(path, value_str, state); + dynamic_lib_->envoyGoRequestSemaDec(req_); + } else { + ENVOY_LOG(info, "golang filter has gone or destroyed in getStringProperty"); + } + }); + return CAPIStatus::CAPIYield; +} + +CAPIStatus Filter::getStringPropertyCommon(absl::string_view path, GoString* value_str, + ProcessorState& state) { + activation_info_ = &state.streamInfo(); + CAPIStatus status = getStringPropertyInternal(path, &req_->strValue); + if (status == CAPIStatus::CAPIOK) { + value_str->p = req_->strValue.data(); + value_str->n = req_->strValue.length(); + } + return status; +} + +absl::optional Filter::findValue(absl::string_view name, + Protobuf::Arena* arena) { + // as we already support getting/setting FilterState, we don't need to implement + // getProperty with non-attribute name & setProperty which actually work on FilterState + return StreamActivation::FindValue(name, arena); + // we don't need to call resetActivation as activation_xx_ is overridden when we get property +} + +CAPIStatus Filter::getStringPropertyInternal(absl::string_view path, std::string* result) { + using google::api::expr::runtime::CelValue; + + bool first = true; + CelValue value; + Protobuf::Arena arena; + + size_t start = 0; + while (true) { + if (start >= path.size()) { + break; + } + + size_t end = path.find('.', start); + if (end == absl::string_view::npos) { + end = start + path.size(); + } + auto part = path.substr(start, end - start); + start = end + 1; + + if (first) { + // top-level identifier + first = false; + auto top_value = findValue(toAbslStringView(part), &arena); + if (!top_value.has_value()) { + return CAPIStatus::CAPIValueNotFound; + } + value = top_value.value(); + } else if (value.IsMap()) { + auto& map = *value.MapOrDie(); + auto field = map[CelValue::CreateStringView(toAbslStringView(part))]; + if (!field.has_value()) { + return CAPIStatus::CAPIValueNotFound; + } + value = field.value(); + } else if (value.IsMessage()) { + auto msg = value.MessageOrDie(); + if (msg == nullptr) { + return CAPIStatus::CAPIValueNotFound; + } + const Protobuf::Descriptor* desc = msg->GetDescriptor(); + const Protobuf::FieldDescriptor* field_desc = desc->FindFieldByName(std::string(part)); + if (field_desc == nullptr) { + return CAPIStatus::CAPIValueNotFound; + } + if (field_desc->is_map()) { + value = CelValue::CreateMap( + Protobuf::Arena::Create( + &arena, msg, field_desc, &arena)); + } else if (field_desc->is_repeated()) { + value = CelValue::CreateList( + Protobuf::Arena::Create( + &arena, msg, field_desc, &arena)); + } else { + auto status = + google::api::expr::runtime::CreateValueFromSingleField(msg, field_desc, &arena, &value); + if (!status.ok()) { + return CAPIStatus::CAPIInternalFailure; + } + } + } else if (value.IsList()) { + auto& list = *value.ListOrDie(); + int idx = 0; + if (!absl::SimpleAtoi(toAbslStringView(part), &idx)) { + return CAPIStatus::CAPIValueNotFound; + } + if (idx < 0 || idx >= list.size()) { + return CAPIStatus::CAPIValueNotFound; + } + value = list[idx]; + } else { + return CAPIStatus::CAPIValueNotFound; + } + } + + return serializeStringValue(value, result); +} + +CAPIStatus Filter::serializeStringValue(Filters::Common::Expr::CelValue value, + std::string* result) { + using Filters::Common::Expr::CelValue; + const Protobuf::Message* out_message; + + switch (value.type()) { + case CelValue::Type::kString: + result->assign(value.StringOrDie().value().data(), value.StringOrDie().value().size()); + return CAPIStatus::CAPIOK; + case CelValue::Type::kBytes: + result->assign(value.BytesOrDie().value().data(), value.BytesOrDie().value().size()); + return CAPIStatus::CAPIOK; + case CelValue::Type::kInt64: + result->assign(absl::StrCat(value.Int64OrDie())); + return CAPIStatus::CAPIOK; + case CelValue::Type::kUint64: + result->assign(absl::StrCat(value.Uint64OrDie())); + return CAPIStatus::CAPIOK; + case CelValue::Type::kDouble: + result->assign(absl::StrCat(value.DoubleOrDie())); + return CAPIStatus::CAPIOK; + case CelValue::Type::kBool: + result->assign(value.BoolOrDie() ? "true" : "false"); + return CAPIStatus::CAPIOK; + case CelValue::Type::kDuration: + result->assign(absl::FormatDuration(value.DurationOrDie())); + return CAPIStatus::CAPIOK; + case CelValue::Type::kTimestamp: + result->assign(absl::FormatTime(value.TimestampOrDie(), absl::UTCTimeZone())); + return CAPIStatus::CAPIOK; + case CelValue::Type::kMessage: + out_message = value.MessageOrDie(); + result->clear(); + if (!out_message || out_message->SerializeToString(result)) { + return CAPIStatus::CAPIOK; + } + return CAPIStatus::CAPISerializationFailure; + case CelValue::Type::kMap: { + // so far, only headers/trailers/filter state are in Map format, and we already have API to + // fetch them + ENVOY_LOG(error, "map type property result is not supported yet"); + return CAPIStatus::CAPISerializationFailure; + } + case CelValue::Type::kList: { + ENVOY_LOG(error, "list type property result is not supported yet"); + return CAPIStatus::CAPISerializationFailure; + } + default: + return CAPIStatus::CAPISerializationFailure; + } +} + /* ConfigId */ uint64_t Filter::getMergedConfigId(ProcessorState& state) { diff --git a/contrib/golang/filters/http/source/golang_filter.h b/contrib/golang/filters/http/source/golang_filter.h index 66ffc48f7c1e..a526b3602edb 100644 --- a/contrib/golang/filters/http/source/golang_filter.h +++ b/contrib/golang/filters/http/source/golang_filter.h @@ -11,6 +11,7 @@ #include "source/common/common/thread.h" #include "source/common/grpc/context_impl.h" #include "source/common/http/utility.h" +#include "source/extensions/filters/common/expr/evaluator.h" #include "contrib/envoy/extensions/filters/http/golang/v3alpha/golang.pb.h" #include "contrib/golang/common/dso/dso.h" @@ -165,6 +166,7 @@ struct httpRequestInternal; */ class Filter : public Http::StreamFilter, public std::enable_shared_from_this, + public Filters::Common::Expr::StreamActivation, Logger::Loggable, public AccessLog::Instance { public: @@ -236,6 +238,7 @@ class Filter : public Http::StreamFilter, CAPIStatus setStringFilterState(absl::string_view key, absl::string_view value, int state_type, int life_span, int stream_sharing); CAPIStatus getStringFilterState(absl::string_view key, GoString* value_str); + CAPIStatus getStringProperty(absl::string_view path, GoString* value_str, GoInt32* rc); private: bool hasDestroyed() { @@ -270,6 +273,13 @@ class Filter : public Http::StreamFilter, void populateSliceWithMetadata(ProcessorState& state, const std::string& filter_name, GoSlice* buf_slice); + CAPIStatus getStringPropertyCommon(absl::string_view path, GoString* value_str, + ProcessorState& state); + CAPIStatus getStringPropertyInternal(absl::string_view path, std::string* result); + absl::optional findValue(absl::string_view name, + Protobuf::Arena* arena); + CAPIStatus serializeStringValue(Filters::Common::Expr::CelValue value, std::string* result); + const FilterConfigSharedPtr config_; Dso::HttpFilterDsoPtr dynamic_lib_; @@ -280,6 +290,10 @@ class Filter : public Http::StreamFilter, Http::RequestOrResponseHeaderMap* local_headers_{nullptr}; Http::HeaderMap* local_trailers_{nullptr}; + // save temp values for fetching request attributes in the later phase, + // like getting request size + Http::RequestOrResponseHeaderMap* request_headers_{nullptr}; + // The state of the filter on both the encoding and decoding side. DecodingProcessorState decoding_state_; EncodingProcessorState encoding_state_; diff --git a/contrib/golang/filters/http/test/BUILD b/contrib/golang/filters/http/test/BUILD index 42db8c7bcd8c..7c6519b150cd 100644 --- a/contrib/golang/filters/http/test/BUILD +++ b/contrib/golang/filters/http/test/BUILD @@ -57,6 +57,7 @@ envoy_cc_test( "//contrib/golang/filters/http/test/test_data/echo:filter.so", "//contrib/golang/filters/http/test/test_data/metric:filter.so", "//contrib/golang/filters/http/test/test_data/passthrough:filter.so", + "//contrib/golang/filters/http/test/test_data/property:filter.so", "//contrib/golang/filters/http/test/test_data/routeconfig:filter.so", ], env = {"GODEBUG": "cgocheck=0"}, diff --git a/contrib/golang/filters/http/test/golang_integration_test.cc b/contrib/golang/filters/http/test/golang_integration_test.cc index 46af2cfc35a8..ed0bea121535 100644 --- a/contrib/golang/filters/http/test/golang_integration_test.cc +++ b/contrib/golang/filters/http/test/golang_integration_test.cc @@ -234,6 +234,46 @@ name: golang initializeBasicFilter(so_id, "test.com"); } + void initializePropertyConfig(const std::string& lib_id, const std::string& lib_path, + const std::string& plugin_name) { + const auto yaml_fmt = R"EOF( +name: golang +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config + library_id: %s + library_path: %s + plugin_name: %s + plugin_config: + "@type": type.googleapis.com/xds.type.v3.TypedStruct +)EOF"; + + auto yaml_string = absl::StrFormat(yaml_fmt, lib_id, lib_path, plugin_name); + config_helper_.prependFilter(yaml_string); + config_helper_.skipPortUsageValidation(); + + config_helper_.addConfigModifier( + [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) { + hcm.mutable_route_config() + ->mutable_virtual_hosts(0) + ->mutable_routes(0) + ->mutable_match() + ->set_prefix("/property"); + + // setting route name for testing + hcm.mutable_route_config()->mutable_virtual_hosts(0)->mutable_routes(0)->set_name( + "test-route-name"); + hcm.mutable_route_config() + ->mutable_virtual_hosts(0) + ->mutable_routes(0) + ->mutable_route() + ->set_cluster("cluster_0"); + }); + + config_helper_.addConfigModifier(setEnableDownstreamTrailersHttp1()); + config_helper_.addConfigModifier(setEnableUpstreamTrailersHttp1()); + } + void testBasic(std::string path) { initializeBasicFilter(BASIC, "test.com"); @@ -626,6 +666,7 @@ name: golang const std::string BASIC{"basic"}; const std::string PASSTHROUGH{"passthrough"}; const std::string ROUTECONFIG{"routeconfig"}; + const std::string PROPERTY{"property"}; const std::string ACCESSLOG{"access_log"}; const std::string METRIC{"metric"}; }; @@ -697,6 +738,35 @@ TEST_P(GolangIntegrationTest, Passthrough) { cleanup(); } +TEST_P(GolangIntegrationTest, Property) { + initializePropertyConfig(PROPERTY, genSoPath(PROPERTY), PROPERTY); + initialize(); + registerTestServerPorts({"http"}); + + auto path = "/property?a=1"; + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "POST"}, {":path", path}, {":scheme", "http"}, {":authority", "test.com"}, + {"User-Agent", "ua"}, {"Referer", "r"}, {"X-Request-Id", "xri"}, + }; + + auto encoder_decoder = codec_client_->startRequest(request_headers); + Http::RequestEncoder& request_encoder = encoder_decoder.first; + auto response = std::move(encoder_decoder.second); + codec_client_->sendData(request_encoder, "helloworld", true); + + waitForNextUpstreamRequest(); + + Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}}; + upstream_request_->encodeHeaders(response_headers, false); + Buffer::OwnedImpl response_data("goodbye"); + upstream_request_->encodeData(response_data, true); + + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_EQ("200", response->headers().getStatusValue()); + cleanup(); +} + TEST_P(GolangIntegrationTest, AccessLog) { initializeBasicFilter(ACCESSLOG, "test.com"); diff --git a/contrib/golang/filters/http/test/test_data/property/BUILD b/contrib/golang/filters/http/test/test_data/property/BUILD new file mode 100644 index 000000000000..cbbb582905da --- /dev/null +++ b/contrib/golang/filters/http/test/test_data/property/BUILD @@ -0,0 +1,23 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary") + +licenses(["notice"]) # Apache 2 + +go_binary( + name = "filter.so", + srcs = [ + "config.go", + "filter.go", + ], + out = "filter.so", + cgo = True, + importpath = "github.com/envoyproxy/envoy/contrib/golang/filters/http/test/test_data/property", + linkmode = "c-shared", + visibility = ["//visibility:public"], + deps = [ + "//contrib/golang/common/go/api", + "//contrib/golang/filters/http/source/go/pkg/http", + "@com_github_cncf_xds_go//xds/type/v3:type", + "@org_golang_google_protobuf//types/known/anypb", + "@org_golang_google_protobuf//types/known/structpb", + ], +) diff --git a/contrib/golang/filters/http/test/test_data/property/config.go b/contrib/golang/filters/http/test/test_data/property/config.go new file mode 100644 index 000000000000..21cc3ccd1fd8 --- /dev/null +++ b/contrib/golang/filters/http/test/test_data/property/config.go @@ -0,0 +1,44 @@ +package main + +import ( + "google.golang.org/protobuf/types/known/anypb" + + "github.com/envoyproxy/envoy/contrib/golang/common/go/api" + "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http" +) + +const Name = "property" + +func init() { + http.RegisterHttpFilterConfigFactoryAndParser(Name, ConfigFactory, &parser{}) +} + +type config struct { +} + +type parser struct { +} + +func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (interface{}, error) { + conf := &config{} + return conf, nil +} + +func (p *parser) Merge(parent interface{}, child interface{}) interface{} { + return child +} + +func ConfigFactory(c interface{}) api.StreamFilterFactory { + conf, ok := c.(*config) + if !ok { + panic("unexpected config type") + } + return func(callbacks api.FilterCallbackHandler) api.StreamFilter { + return &filter{ + callbacks: callbacks, + config: conf, + } + } +} + +func main() {} diff --git a/contrib/golang/filters/http/test/test_data/property/filter.go b/contrib/golang/filters/http/test/test_data/property/filter.go new file mode 100644 index 000000000000..c22f6d7e174f --- /dev/null +++ b/contrib/golang/filters/http/test/test_data/property/filter.go @@ -0,0 +1,124 @@ +package main + +import ( + "strconv" + "time" + + "github.com/envoyproxy/envoy/contrib/golang/common/go/api" +) + +type filter struct { + api.PassThroughStreamFilter + + callbacks api.FilterCallbackHandler + path string + config *config + + failed bool +} + +func (f *filter) assertProperty(name, exp string) { + act, err := f.callbacks.GetProperty(name) + if err != nil { + act = err.Error() + } + if exp != act { + f.callbacks.Log(api.Critical, name+" expect "+exp+" got "+act) + f.failed = true + } +} + +func (f *filter) panicIfFailed() { + if f.failed { + panic("Check the critical log for the failed cases") + } +} + +func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType { + ts, _ := f.callbacks.GetProperty("request.time") + ymd := ts[:len("2023-07-31T00:00:00")] + startTime, _ := time.Parse("2006-01-02T15:04:05", ymd) + if time.Now().UTC().Sub(startTime) > 1*time.Minute { + f.callbacks.Log(api.Critical, "got request.time "+ts) + f.failed = true + } + + f.assertProperty("request.protocol", "HTTP/1.1") + f.assertProperty("request.path", "/property?a=1") + f.assertProperty("request.url_path", "/property") + f.assertProperty("request.query", "a=1") + f.assertProperty("request.host", "test.com") + f.assertProperty("request.scheme", "http") + f.assertProperty("request.method", "POST") + f.assertProperty("request.referer", "r") + f.assertProperty("request.useragent", "ua") + f.assertProperty("request.id", "xri") + + f.assertProperty("request.duration", "value not found") // available only when the request is finished + + f.assertProperty("source.address", f.callbacks.StreamInfo().DownstreamRemoteAddress()) + f.assertProperty("destination.address", f.callbacks.StreamInfo().DownstreamLocalAddress()) + f.assertProperty("connection.mtls", "false") + // route name can be determinated in the decode phase + f.assertProperty("xds.route_name", "test-route-name") + + // non-existed attribute + f.assertProperty("request.user_agent", "value not found") + + // access response attribute in the decode phase + f.assertProperty("response.total_size", "0") + + // bad case + // strange input + for _, attr := range []string{ + ".", + ".total_size", + } { + f.assertProperty(attr, "value not found") + } + // unsupported value type + for _, attr := range []string{ + // unknown type + "", + // map type + "request", + "request.", + } { + f.assertProperty(attr, "serialization failure") + } + return api.Continue +} + +func (f *filter) EncodeHeaders(header api.ResponseHeaderMap, endStream bool) api.StatusType { + f.assertProperty("xds.route_name", "test-route-name") + f.assertProperty("xds.cluster_name", "cluster_0") + f.assertProperty("xds.cluster_metadata", "") + + // response.code is available only after the response has started to send + code, _ := f.callbacks.StreamInfo().ResponseCode() + exp := "value not found" + if code != 0 { + exp = strconv.Itoa(int(code)) + } + f.assertProperty("response.code", exp) + f.assertProperty("response.code_details", "via_upstream") + + f.assertProperty("request.size", "10") // "helloworld" + size, _ := f.callbacks.GetProperty("request.total_size") + intSize, _ := strconv.Atoi(size) + if intSize <= 10 { + f.callbacks.Log(api.Critical, "got request.total_size "+size) + f.failed = true + } + f.assertProperty("request.referer", "r") + + return api.Continue +} + +func (f *filter) EncodeData(buffer api.BufferInstance, endStream bool) api.StatusType { + f.assertProperty("response.code", "200") + + // panic if any condition is not met + f.panicIfFailed() + return api.Continue +} diff --git a/contrib/golang/filters/http/test/test_data/property/go.mod b/contrib/golang/filters/http/test/test_data/property/go.mod new file mode 100644 index 000000000000..4bcd808d5e6f --- /dev/null +++ b/contrib/golang/filters/http/test/test_data/property/go.mod @@ -0,0 +1,10 @@ +module example.com/property + +go 1.18 + +require ( + github.com/envoyproxy/envoy v1.24.0 + google.golang.org/protobuf v1.31.0 +) + +replace github.com/envoyproxy/envoy => ../../../../../../../