diff --git a/api/BUILD b/api/BUILD index 90bc3f56470a2..e44af2a291cf3 100644 --- a/api/BUILD +++ b/api/BUILD @@ -73,6 +73,7 @@ proto_library( visibility = ["//visibility:public"], deps = [ "//contrib/envoy/extensions/filters/http/dynamo/v3:pkg", + "//contrib/envoy/extensions/filters/http/golang/v3alpha:pkg", "//contrib/envoy/extensions/filters/http/language/v3alpha:pkg", "//contrib/envoy/extensions/filters/http/squash/v3:pkg", "//contrib/envoy/extensions/filters/http/sxg/v3alpha:pkg", diff --git a/api/contrib/envoy/extensions/filters/http/golang/v3alpha/BUILD b/api/contrib/envoy/extensions/filters/http/golang/v3alpha/BUILD new file mode 100644 index 0000000000000..ec1e778e06e5c --- /dev/null +++ b/api/contrib/envoy/extensions/filters/http/golang/v3alpha/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "@com_github_cncf_udpa//udpa/annotations:pkg", + "@com_github_cncf_udpa//xds/annotations/v3:pkg", + ], +) diff --git a/api/contrib/envoy/extensions/filters/http/golang/v3alpha/golang.proto b/api/contrib/envoy/extensions/filters/http/golang/v3alpha/golang.proto new file mode 100644 index 0000000000000..db53f0b7c2961 --- /dev/null +++ b/api/contrib/envoy/extensions/filters/http/golang/v3alpha/golang.proto @@ -0,0 +1,214 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.golang.v3alpha; + +import "google/protobuf/any.proto"; + +import "xds/annotations/v3/status.proto"; + +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.golang.v3alpha"; +option java_outer_classname = "GolangProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/golang/v3alpha"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; +option (xds.annotations.v3.file_status).work_in_progress = true; + +// [#protodoc-title: golang extension filter] +// Golang :ref:`configuration overview `. +// [#extension: envoy.filters.http.golang] +// +// In the below example, we configured the go plugin 'auth' and 'limit' dynamic libraries into +// Envoy, which can avoid rebuilding Envoy. +// +// * Develop go-plugin +// +// We can implement the interface of ``StreamFilter `` +// API by the GO language to achieve the effects of Envoy native filter. +// +// The filter based on the APIs implementation ``StreamFilter `` +// For details, take a look at the :repo:`/contrib/golang/filters/http/test/test_data/echo`. +// +// Then put the GO plugin source code into the ${OUTPUT}/src/ directory with the name of the plugin +// for GO plugin builds. +// The following examples implement limit and auth GO plugins. +// +// .. code-block:: bash +// +// $ tree /home/admin/envoy/go-plugins/src/ +// |--auth +// | |--config.go +// | |--filter.go +// ---limit +// |--config.go +// |--filter.go +// +// * Build go-plugin +// +// Build the Go plugin so by `go_plugin_generate.sh` script, below example the `liblimit.so` and +// `libauth.so` will be generated in the `/home/admin/envoy/go-plugins/` directory. +// +// .. code-block:: bash +// +// #!/bin/bash +// if [ $# != 2 ]; then +// echo "need input the go plugin name" +// exit 1 +// fi +// +// PLUGINNAME=$1 +// OUTPUT=/home/admin/envoy/go-plugins/ +// PLUGINSRCDIR=${OUTPUT}/src/${PLUGINNAME} +// go build --buildmode=c-shared -v -o $OUTPUT/lib${PLUGINNAME}.so $PLUGINSRCDIR +// +// .. code-block:: bash +// +// $ go_plugin_generate.sh limit +// $ go_plugin_generate.sh auth +// +// * Configure go-plugin +// +// Use the http filter of :ref: `golang ` to specify +// :ref: `library` in ingress and egress to enable the plugin. +// +// Example: +// +// .. code-block:: yaml +// +// static_resources: +// listeners: +// - name: ingress +// address: +// socket_address: +// protocol: TCP +// address: 0.0.0.0 +// port_value: 8080 +// filter_chains: +// - filters: +// - name: envoy.filters.network.http_connection_manager +// ...... +// http_filters: +// - name: envoy.filters.http.golang +// typed_config: +// "@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config +// library_id: limit-id +// library_path: "/home/admin/envoy/go-plugins/liblimit.so" +// plugine_name: limit +// plugin_config: +// "@type": type.googleapis.com/envoy.extensions.filters.http.golang.plugins.limit.v3.Config +// xxx1: xx1 +// xxx2: xx2 +// - name: envoy.filters.http.header_to_metadata +// - name: envoy.filters.http.golang +// typed_config: +// "@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config +// library_id: auth-id +// library_path: "/home/admin/envoy/go-plugins/libauth.so" +// plugine_name: auth +// plugin_config: +// "@type": type.googleapis.com/envoy.extensions.filters.http.golang.plugins.auth.v3.Config +// xxx1: xx1 +// xxx2: xx2 +// - name: envoy.filters.http.router +// - name: egress +// address: +// socket_address: +// protocol: TCP +// address: 0.0.0.0 +// port_value: 8081 +// filter_chains: +// - filters: +// - name: envoy.filters.network.http_connection_manager +// ...... +// http_filters: +// - name: envoy.filters.http.golang +// typed_config: +// "@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config +// library_id: auth-id +// library_path: "/home/admin/envoy/go-plugins/libauth.so" +// plugine_name: auth +// plugin_config: +// "@type": type.googleapis.com/envoy.extensions.filters.http.golang.plugins.auth.v3.Config +// xxx1: xx1 +// xxx2: xx2 +// - name: envoy.filters.http.router +// [#next-free-field: 6] +message Config { + enum MergePolicy { + MERGE_VIRTUALHOST_ROUTER_FILTER = 0; + MERGE_VIRTUALHOST_ROUTER = 1; + OVERRIDE = 3; + } + + // library_id is a unique ID for a dynamic library file, must be unique globally. + string library_id = 1 [(validate.rules).string = {min_len: 1}]; + + // Dynamic library implementing the interface of + // ``StreamFilter ``. + // [#comment:TODO(wangfakang): Support for downloading libraries from remote repositories.] + string library_path = 2 [(validate.rules).string = {min_len: 1}]; + + // plugin_name is the name of the go plugin, which needs to be consistent with the name + // registered in http::RegisterHttpFilterConfigFactory. + string plugin_name = 3 [(validate.rules).string = {min_bytes: 1}]; + + // plugin_config is the configuration of the go plugin, note that this configuration is + // only parsed in the go plugin. + google.protobuf.Any plugin_config = 4; + + // merge_policy is the merge policy configured by the go plugin. + // go plugin configuration supports three dimensions: the virtual host’s typed_per_filter_config, + // the route’s typed_per_filter_config or filter's config. + // The meanings are as follows: + // MERGE_VIRTUALHOST_ROUTER_FILTER: pass all configuration into go plugin. + // MERGE_VIRTUALHOST_ROUTER: pass Virtual-Host and Router configuration into go plugin. + // OVERRIDE: override according to Router > Virtual_host > Filter priority and pass the + // configuration to the go plugin. + MergePolicy merge_policy = 5 [(validate.rules).enum = {defined_only: true}]; +} + +message RouterPlugin { + // Example + // + // .. code-block:: yaml + // + // typed_per_filter_config: + // envoy.filters.http.golang: + // "@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.ConfigsPerRoute + // plugins_config: + // plugin1: + // disabled: true + oneof override { + option (validate.required) = true; + + // [#not-implemented-hide:] + // Disable the filter for this particular vhost or route. + // If disabled is specified in multiple per-filter-configs, the most specific one will be used. + bool disabled = 1 [(validate.rules).bool = {const: true}]; + + // The config field is used to setting per-route plugin config. + google.protobuf.Any config = 2; + } +} + +message ConfigsPerRoute { + // plugins_config is the configuration of the go plugin at the per-router, and + // key is the name of the go plugin. + // Example + // + // .. code-block:: yaml + // + // typed_per_filter_config: + // envoy.filters.http.golang: + // "@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.ConfigsPerRoute + // plugins_config: + // plugin1: + // disabled: true + // plugin2: + // config: + // "@type": type.googleapis.com/golang.http.plugin2 + // xxx: xxx + map plugins_config = 1; +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index ff07909f36c71..7bb75588f38a6 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -11,6 +11,7 @@ proto_library( deps = [ "//contrib/envoy/extensions/config/v3alpha:pkg", "//contrib/envoy/extensions/filters/http/dynamo/v3:pkg", + "//contrib/envoy/extensions/filters/http/golang/v3alpha:pkg", "//contrib/envoy/extensions/filters/http/language/v3alpha:pkg", "//contrib/envoy/extensions/filters/http/squash/v3:pkg", "//contrib/envoy/extensions/filters/http/sxg/v3alpha:pkg", diff --git a/bazel/dependency_imports.bzl b/bazel/dependency_imports.bzl index 64190a5946a1c..a253433e028ef 100644 --- a/bazel/dependency_imports.bzl +++ b/bazel/dependency_imports.bzl @@ -15,7 +15,7 @@ load("@aspect_bazel_lib//lib:repositories.bzl", "register_jq_toolchains", "regis load("@com_google_cel_cpp//bazel:deps.bzl", "parser_deps") # go version for rules_go -GO_VERSION = "1.17.5" +GO_VERSION = "1.18" JQ_VERSION = "1.6" YQ_VERSION = "4.24.4" @@ -94,6 +94,20 @@ def envoy_dependency_imports(go_version = GO_VERSION, jq_version = JQ_VERSION, y # use_category = ["api"], # source = "https://github.com/bufbuild/protoc-gen-validate/blob/v0.6.1/dependencies.bzl#L148-L153" ) + go_repository( + name = "org_golang_google_protobuf", + importpath = "google.golang.org/protobuf", + sum = "h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=", + version = "v1.28.1", + build_external = "external", + ) + go_repository( + name = "com_github_cncf_xds_go", + importpath = "github.com/cncf/xds/go", + sum = "h1:B/lvg4tQ5hfFZd4V2hcSfFVfUvAK6GSFKxIIzwnkv8g=", + version = "v0.0.0-20220520190051-1e77728a1eaa", + build_external = "external", + ) go_repository( name = "com_github_spf13_afero", importpath = "github.com/spf13/afero", diff --git a/bazel/exported_symbols.txt b/bazel/exported_symbols.txt index 8bd43f34bd54f..1fe02d54b59b6 100644 --- a/bazel/exported_symbols.txt +++ b/bazel/exported_symbols.txt @@ -1,3 +1,4 @@ { lua*; + envoyGoFilterHttp*; }; diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 6b5fe925faea6..96810de76d54b 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -167,6 +167,9 @@ new_features: change: | added :ref:`dubbo codec support ` to the :ref:`generic_proxy filter `. +- area: golang + change: | + added new :ref:`HTTP golang extension filter `. - area: custom response http filter change: | added :ref:`custom response http filter ` which adds the ability to customize responses sent to downstreams using local or remote sources. diff --git a/contrib/contrib_build_config.bzl b/contrib/contrib_build_config.bzl index 419dfff657f05..f2724aeb7df35 100644 --- a/contrib/contrib_build_config.bzl +++ b/contrib/contrib_build_config.bzl @@ -5,6 +5,7 @@ CONTRIB_EXTENSIONS = { # "envoy.filters.http.dynamo": "//contrib/dynamo/filters/http/source:config", + "envoy.filters.http.golang": "//contrib/golang/filters/http/source:config", "envoy.filters.http.language": "//contrib/language/filters/http/source:config_lib", "envoy.filters.http.squash": "//contrib/squash/filters/http/source:config", "envoy.filters.http.sxg": "//contrib/sxg/filters/http/source:config", diff --git a/contrib/extensions_metadata.yaml b/contrib/extensions_metadata.yaml index d388cba032f85..9300e8e4d4719 100644 --- a/contrib/extensions_metadata.yaml +++ b/contrib/extensions_metadata.yaml @@ -3,6 +3,11 @@ envoy.filters.http.dynamo: - envoy.filters.http security_posture: requires_trusted_downstream_and_upstream status: stable +envoy.filters.http.golang: + categories: + - envoy.filters.http + security_posture: requires_trusted_downstream_and_upstream + status: alpha envoy.filters.http.squash: categories: - envoy.filters.http diff --git a/contrib/golang/filters/http/source/BUILD b/contrib/golang/filters/http/source/BUILD new file mode 100644 index 0000000000000..b79b0b2df46fd --- /dev/null +++ b/contrib/golang/filters/http/source/BUILD @@ -0,0 +1,79 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_contrib_extension", + "envoy_cc_library", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +# Golang extensions filter. +# Public docs: https://envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/golang_filter + +envoy_contrib_package() + +envoy_cc_library( + name = "golang_filter_lib", + srcs = [ + "golang_filter.cc", + "processor_state.cc", + ], + hdrs = [ + "golang_filter.h", + "processor_state.h", + ], + deps = [ + ":cgo", + "//contrib/golang/filters/http/source/common/dso:dso_lib", + "//envoy/http:codes_interface", + "//envoy/http:filter_interface", + "//source/common/buffer:watermark_buffer_lib", + "//source/common/common:enum_to_int", + "//source/common/common:linked_object", + "//source/common/common:thread_lib", + "//source/common/common:utility_lib", + "//source/common/grpc:context_lib", + "//source/common/http:headers_lib", + "//source/common/http:utility_lib", + "//source/common/http/http1:codec_lib", + "@envoy_api//contrib/envoy/extensions/filters/http/golang/v3alpha:pkg_cc_proto", + ], +) + +envoy_cc_contrib_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + deps = [ + ":golang_filter_lib", + "//contrib/golang/filters/http/source/common/dso:dso_lib", + "//envoy/registry", + "//envoy/server:filter_config_interface", + "//envoy/server:lifecycle_notifier_interface", + "//source/extensions/filters/http/common:factory_base_lib", + "@envoy_api//contrib/envoy/extensions/filters/http/golang/v3alpha:pkg_cc_proto", + ], +) + +envoy_cc_library( + name = "cgo", + srcs = ["cgo.cc"], + hdrs = [ + "golang_filter.h", + "processor_state.h", + ], + deps = [ + "//contrib/golang/filters/http/source/common/dso:dso_lib", + "//envoy/http:codes_interface", + "//envoy/http:filter_interface", + "//source/common/buffer:watermark_buffer_lib", + "//source/common/common:enum_to_int", + "//source/common/common:linked_object", + "//source/common/common:utility_lib", + "//source/common/grpc:context_lib", + "//source/common/http:headers_lib", + "//source/common/http:utility_lib", + "//source/common/http/http1:codec_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 new file mode 100644 index 0000000000000..ba4cc297618c3 --- /dev/null +++ b/contrib/golang/filters/http/source/cgo.cc @@ -0,0 +1,139 @@ +#include "contrib/golang/filters/http/source/golang_filter.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Golang { + +// +// These functions may be invoked in another go thread, +// which means may introduce race between go thread and envoy thread. +// So we use the envoy's dispatcher in the filter to post it, and make it only executes in the envoy +// thread. +// + +absl::string_view copyGoString(void* str) { + if (str == nullptr) { + return ""; + } + auto goStr = reinterpret_cast(str); + return absl::string_view(goStr->p, goStr->n); // NOLINT(modernize-return-braced-init-list) +} + +extern "C" { + +void envoyGoFilterHandlerWrapper(void* r, std::function&)> f) { + auto req = reinterpret_cast(r); + auto weakFilter = req->weakFilter(); + if (auto filter = weakFilter.lock()) { + f(filter); + } +} + +void envoyGoFilterHttpContinue(void* r, int status) { + envoyGoFilterHandlerWrapper(r, [status](std::shared_ptr& filter) { + filter->continueStatus(static_cast(status)); + }); +} + +void envoyGoFilterHttpSendLocalReply(void* r, int response_code, void* body_text, void* headers, + long long int grpc_status, void* details) { + envoyGoFilterHandlerWrapper(r, [response_code, body_text, headers, grpc_status, + details](std::shared_ptr& filter) { + UNREFERENCED_PARAMETER(headers); + auto grpcStatus = static_cast(grpc_status); + filter->sendLocalReply(static_cast(response_code), copyGoString(body_text), nullptr, + grpcStatus, copyGoString(details)); + }); +} + +// unsafe API, without copy memory from c to go. +void envoyGoFilterHttpGetHeader(void* r, void* key, void* value) { + envoyGoFilterHandlerWrapper(r, [key, value](std::shared_ptr& filter) { + auto keyStr = copyGoString(key); + auto v = filter->getHeader(keyStr); + if (v.has_value()) { + auto goValue = reinterpret_cast(value); + goValue->p = v.value().data(); + goValue->n = v.value().length(); + } + }); +} + +void envoyGoFilterHttpCopyHeaders(void* r, void* strs, void* buf) { + envoyGoFilterHandlerWrapper(r, [strs, buf](std::shared_ptr& filter) { + auto goStrs = reinterpret_cast(strs); + auto goBuf = reinterpret_cast(buf); + filter->copyHeaders(goStrs, goBuf); + }); +} + +void envoyGoFilterHttpSetHeader(void* r, void* key, void* value) { + envoyGoFilterHandlerWrapper(r, [key, value](std::shared_ptr& filter) { + auto keyStr = copyGoString(key); + auto valueStr = copyGoString(value); + filter->setHeader(keyStr, valueStr); + }); +} + +void envoyGoFilterHttpRemoveHeader(void* r, void* key) { + envoyGoFilterHandlerWrapper(r, [key](std::shared_ptr& filter) { + // TODO: it's safe to skip copy + auto keyStr = copyGoString(key); + filter->removeHeader(keyStr); + }); +} + +void envoyGoFilterHttpGetBuffer(void* r, unsigned long long int buffer_ptr, void* data) { + envoyGoFilterHandlerWrapper(r, [buffer_ptr, data](std::shared_ptr& filter) { + auto buffer = reinterpret_cast(buffer_ptr); + filter->copyBuffer(buffer, reinterpret_cast(data)); + }); +} + +void envoyGoFilterHttpSetBufferHelper(void* r, unsigned long long int buffer_ptr, void* data, + int length, bufferAction action) { + envoyGoFilterHandlerWrapper( + r, [buffer_ptr, data, length, action](std::shared_ptr& filter) { + auto buffer = reinterpret_cast(buffer_ptr); + auto value = absl::string_view(reinterpret_cast(data), length); + filter->setBufferHelper(buffer, value, action); + }); +} + +void envoyGoFilterHttpCopyTrailers(void* r, void* strs, void* buf) { + envoyGoFilterHandlerWrapper(r, [strs, buf](std::shared_ptr& filter) { + auto goStrs = reinterpret_cast(strs); + auto goBuf = reinterpret_cast(buf); + filter->copyTrailers(goStrs, goBuf); + }); +} + +void envoyGoFilterHttpSetTrailer(void* r, void* key, void* value) { + envoyGoFilterHandlerWrapper(r, [key, value](std::shared_ptr& filter) { + auto keyStr = copyGoString(key); + auto valueStr = copyGoString(value); + filter->setTrailer(keyStr, valueStr); + }); +} + +void envoyGoFilterHttpGetStringValue(void* r, int id, void* value) { + envoyGoFilterHandlerWrapper(r, [id, value](std::shared_ptr& filter) { + auto valueStr = reinterpret_cast(value); + filter->getStringValue(id, valueStr); + }); +} + +void envoyGoFilterHttpFinalize(void* r, int reason) { + UNREFERENCED_PARAMETER(reason); + // req is used by go, so need to use raw memory and then it is safe to release at the gc finalize + // phase of the go object. + auto req = reinterpret_cast(r); + delete req; +} +} + +} // namespace Golang +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/golang/filters/http/source/common/dso/BUILD b/contrib/golang/filters/http/source/common/dso/BUILD new file mode 100644 index 0000000000000..1a9facd9bd62d --- /dev/null +++ b/contrib/golang/filters/http/source/common/dso/BUILD @@ -0,0 +1,23 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_library( + name = "dso_lib", + srcs = ["dso.cc"], + hdrs = [ + "api.h", + "dso.h", + "libgolang.h", + ], + deps = [ + "//source/common/common:minimal_logger_lib", + "//source/common/common:utility_lib", + ], +) diff --git a/contrib/golang/filters/http/source/common/dso/api.h b/contrib/golang/filters/http/source/common/dso/api.h new file mode 120000 index 0000000000000..2f469bb6e277c --- /dev/null +++ b/contrib/golang/filters/http/source/common/dso/api.h @@ -0,0 +1 @@ +../../go/pkg/api/api.h \ No newline at end of file diff --git a/contrib/golang/filters/http/source/common/dso/dso.cc b/contrib/golang/filters/http/source/common/dso/dso.cc new file mode 100644 index 0000000000000..cd560759ceb8b --- /dev/null +++ b/contrib/golang/filters/http/source/common/dso/dso.cc @@ -0,0 +1,107 @@ +#include "contrib/golang/filters/http/source/common/dso/dso.h" + +#include "source/common/common/assert.h" + +namespace Envoy { +namespace Dso { + +bool DsoInstanceManager::load(std::string dso_id, std::string dso_name) { + ENVOY_LOG_MISC(debug, "load {} {} dso instance.", dso_id, dso_name); + if (getDsoInstanceByID(dso_id) != nullptr) { + return true; + } + + DsoStoreType& dsoStore = DsoInstanceManager::getDsoStore(); + absl::WriterMutexLock lock(&dsoStore.mutex_); + DsoInstancePtr dso(new DsoInstance(dso_name)); + if (!dso->loaded()) { + return false; + } + dsoStore.map_[dso_id] = std::move(dso); + return true; +} + +DsoInstancePtr DsoInstanceManager::getDsoInstanceByID(std::string dso_id) { + DsoStoreType& dsoStore = DsoInstanceManager::getDsoStore(); + absl::ReaderMutexLock lock(&dsoStore.mutex_); + auto it = dsoStore.map_.find(dso_id); + if (it != dsoStore.map_.end()) { + return it->second; + } + + return nullptr; +} + +template +bool dlsymInternal(T& fn, void* handler, const std::string name, const std::string symbol) { + if (!handler) { + return false; + } + + fn = reinterpret_cast(dlsym(handler, symbol.c_str())); + if (!fn) { + ENVOY_LOG_MISC(error, "lib: {}, cannot find symbol: {}, err: {}", name, symbol, dlerror()); + return false; + } + + return true; +} + +DsoInstance::DsoInstance(const std::string dso_name) : dso_name_(dso_name) { + ENVOY_LOG_MISC(debug, "loading symbols from so file: {}", dso_name); + + handler_ = dlopen(dso_name.c_str(), RTLD_LAZY); + if (!handler_) { + ENVOY_LOG_MISC(error, "cannot load : {} error: {}", dso_name, dlerror()); + return; + } + + loaded_ = dlsymInternal( + envoy_go_filter_new_http_plugin_config_, handler_, dso_name, + "envoyGoFilterNewHttpPluginConfig"); + loaded_ = dlsymInternal( + envoy_go_filter_merge_http_plugin_config_, handler_, dso_name, + "envoyGoFilterMergeHttpPluginConfig"); + loaded_ = dlsymInternal( + envoy_go_filter_on_http_header_, handler_, dso_name, "envoyGoFilterOnHttpHeader"); + loaded_ = dlsymInternal( + envoy_go_filter_on_http_data_, handler_, dso_name, "envoyGoFilterOnHttpData"); + loaded_ = dlsymInternal( + envoy_go_filter_on_http_destroy_, handler_, dso_name, "envoyGoFilterOnHttpDestroy"); +} + +DsoInstance::~DsoInstance() { + if (handler_ != nullptr) { + dlclose(handler_); + } +} + +GoUint64 DsoInstance::envoyGoFilterNewHttpPluginConfig(GoUint64 p0, GoUint64 p1) { + ASSERT(envoy_go_filter_new_http_plugin_config_ != nullptr); + return envoy_go_filter_new_http_plugin_config_(p0, p1); +} + +GoUint64 DsoInstance::envoyGoFilterMergeHttpPluginConfig(GoUint64 p0, GoUint64 p1) { + ASSERT(envoy_go_filter_merge_http_plugin_config_ != nullptr); + return envoy_go_filter_merge_http_plugin_config_(p0, p1); +} + +GoUint64 DsoInstance::envoyGoFilterOnHttpHeader(httpRequest* p0, GoUint64 p1, GoUint64 p2, + GoUint64 p3) { + ASSERT(envoy_go_filter_on_http_header_ != nullptr); + return envoy_go_filter_on_http_header_(p0, p1, p2, p3); +} + +GoUint64 DsoInstance::envoyGoFilterOnHttpData(httpRequest* p0, GoUint64 p1, GoUint64 p2, + GoUint64 p3) { + ASSERT(envoy_go_filter_on_http_data_ != nullptr); + return envoy_go_filter_on_http_data_(p0, p1, p2, p3); +} + +void DsoInstance::envoyGoFilterOnHttpDestroy(httpRequest* p0, int p1) { + ASSERT(envoy_go_filter_on_http_destroy_ != nullptr); + envoy_go_filter_on_http_destroy_(p0, GoUint64(p1)); +} + +} // namespace Dso +} // namespace Envoy diff --git a/contrib/golang/filters/http/source/common/dso/dso.h b/contrib/golang/filters/http/source/common/dso/dso.h new file mode 100644 index 0000000000000..28f3e48e8807e --- /dev/null +++ b/contrib/golang/filters/http/source/common/dso/dso.h @@ -0,0 +1,79 @@ +#pragma once + +#include + +#include +#include + +#include "source/common/common/logger.h" + +#include "absl/synchronization/mutex.h" +#include "contrib/golang/filters/http/source/common/dso/libgolang.h" + +namespace Envoy { +namespace Dso { + +class DsoInstance { +public: + DsoInstance(const std::string dso_name); + ~DsoInstance(); + + GoUint64 envoyGoFilterNewHttpPluginConfig(GoUint64 p0, GoUint64 p1); + GoUint64 envoyGoFilterMergeHttpPluginConfig(GoUint64 p0, GoUint64 p1); + + GoUint64 envoyGoFilterOnHttpHeader(httpRequest* p0, GoUint64 p1, GoUint64 p2, GoUint64 p3); + GoUint64 envoyGoFilterOnHttpData(httpRequest* p0, GoUint64 p1, GoUint64 p2, GoUint64 p3); + + void envoyGoFilterOnHttpDestroy(httpRequest* p0, int p1); + + bool loaded() { return loaded_; } + +private: + const std::string dso_name_; + void* handler_{nullptr}; + bool loaded_{false}; + + GoUint64 (*envoy_go_filter_new_http_plugin_config_)(GoUint64 p0, GoUint64 p1) = {nullptr}; + GoUint64 (*envoy_go_filter_merge_http_plugin_config_)(GoUint64 p0, GoUint64 p1) = {nullptr}; + + GoUint64 (*envoy_go_filter_on_http_header_)(httpRequest* p0, GoUint64 p1, GoUint64 p2, + GoUint64 p3) = {nullptr}; + GoUint64 (*envoy_go_filter_on_http_data_)(httpRequest* p0, GoUint64 p1, GoUint64 p2, + GoUint64 p3) = {nullptr}; + + void (*envoy_go_filter_on_http_destroy_)(httpRequest* p0, GoUint64 p1) = {nullptr}; +}; + +using DsoInstancePtr = std::shared_ptr; + +class DsoInstanceManager { +public: + /** + * Load the go plugin dynamic library. + * @param dso_id is unique ID for dynamic library. + * @param dso_name used to specify the absolute path of the dynamic library. + * @return false if load are invalid. Otherwise, return true. + */ + static bool load(std::string dso_id, std::string dso_name); + + /** + * Get the go plugin dynamic library. + * @param dso_id is unique ID for dynamic library. + * @return nullptr if get failed. Otherwise, return the DSO instance. + */ + static DsoInstancePtr getDsoInstanceByID(std::string dso_id); + +private: + using DsoMapType = std::map; + struct DsoStoreType { + DsoMapType map_ ABSL_GUARDED_BY(mutex_){{ + {"", nullptr}, + }}; + absl::Mutex mutex_; + }; + + static DsoStoreType& getDsoStore() { MUTABLE_CONSTRUCT_ON_FIRST_USE(DsoStoreType); } +}; + +} // namespace Dso +} // namespace Envoy diff --git a/contrib/golang/filters/http/source/common/dso/libgolang.h b/contrib/golang/filters/http/source/common/dso/libgolang.h new file mode 100644 index 0000000000000..f202427825e79 --- /dev/null +++ b/contrib/golang/filters/http/source/common/dso/libgolang.h @@ -0,0 +1,133 @@ +/* Code generated by cmd/cgo; DO NOT EDIT. */ + +/* package http */ + +// NOLINT(namespace-envoy) + +#line 1 "cgo-builtin-export-prolog" + +#include // NOLINT(modernize-deprecated-headers) + +#ifndef GO_CGO_EXPORT_PROLOGUE_H +#define GO_CGO_EXPORT_PROLOGUE_H + +#ifndef GO_CGO_GOSTRING_TYPEDEF +typedef struct { // NOLINT(modernize-use-using) + const char* p; + ptrdiff_t n; +} _GoString_; +#endif + +#endif + +/* Start of preamble from import "C" comments. */ + +#line 20 "export.go" + +// ref https://github.com/golang/go/issues/25832 + +#include // NOLINT(modernize-deprecated-headers) +#include // NOLINT(modernize-deprecated-headers) + +#include "api.h" + +#line 1 "cgo-generated-wrapper" + +/* End of preamble from import "C" comments. */ + +/* Start of boilerplate cgo prologue. */ +#line 1 "cgo-gcc-export-header-prolog" + +#ifndef GO_CGO_PROLOGUE_H +#define GO_CGO_PROLOGUE_H + +typedef signed char GoInt8; // NOLINT(modernize-use-using) +typedef unsigned char GoUint8; // NOLINT(modernize-use-using) +typedef short GoInt16; // NOLINT(modernize-use-using) +typedef unsigned short GoUint16; // NOLINT(modernize-use-using) +typedef int GoInt32; // NOLINT(modernize-use-using) +typedef unsigned int GoUint32; // NOLINT(modernize-use-using) +typedef long long GoInt64; // NOLINT(modernize-use-using) +typedef unsigned long long GoUint64; // NOLINT(modernize-use-using) +typedef GoInt64 GoInt; // NOLINT(modernize-use-using) +typedef GoUint64 GoUint; // NOLINT(modernize-use-using) +typedef size_t GoUintptr; // NOLINT(modernize-use-using) +typedef float GoFloat32; // NOLINT(modernize-use-using) +typedef double GoFloat64; // NOLINT(modernize-use-using) +#ifdef _MSC_VER +#include +typedef _Fcomplex GoComplex64; // NOLINT(modernize-use-using) +typedef _Dcomplex GoComplex128; // NOLINT(modernize-use-using) +#else +typedef float _Complex GoComplex64; // NOLINT(modernize-use-using) +typedef double _Complex GoComplex128; // NOLINT(modernize-use-using) +#endif + +/* + static assertion to make sure the file is being used on architecture + at least with matching size of GoInt. +*/ +typedef char // NOLINT(modernize-use-using) + _check_for_64_bit_pointer_matching_GoInt[sizeof(void*) == 64 / 8 ? 1 : -1]; + +#ifndef GO_CGO_GOSTRING_TYPEDEF +typedef _GoString_ GoString; // NOLINT(modernize-use-using) +#endif +typedef void* GoMap; // NOLINT(modernize-use-using) +typedef void* GoChan; // NOLINT(modernize-use-using) +typedef struct { // NOLINT(modernize-use-using) + void* t; + void* v; +} GoInterface; +typedef struct { // NOLINT(modernize-use-using) + void* data; + GoInt len; + GoInt cap; +} GoSlice; + +#endif + +/* End of boilerplate cgo prologue. */ + +#ifdef __cplusplus +extern "C" { +#endif + +// go:linkname envoyGoFilterNewHttpPluginConfig +// github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http.envoyGoFilterNewHttpPluginConfig +extern GoUint64 +envoyGoFilterNewHttpPluginConfig(GoUint64 configPtr, // NOLINT(readability-identifier-naming) + GoUint64 configLen); // NOLINT(readability-identifier-naming) + +// go:linkname envoyGoFilterDestroyHttpPluginConfig +// github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http.envoyGoFilterDestroyHttpPluginConfig +extern void envoyGoFilterDestroyHttpPluginConfig(GoUint64 id); + +// go:linkname envoyGoFilterMergeHttpPluginConfig +// github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http.envoyGoFilterMergeHttpPluginConfig +extern GoUint64 +envoyGoFilterMergeHttpPluginConfig(GoUint64 parentId, // NOLINT(readability-identifier-naming) + GoUint64 childId); // NOLINT(readability-identifier-naming) + +// go:linkname envoyGoFilterOnHttpHeader +// github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http.envoyGoFilterOnHttpHeader +extern GoUint64 +envoyGoFilterOnHttpHeader(httpRequest* r, + GoUint64 endStream, // NOLINT(readability-identifier-naming) + GoUint64 headerNum, // NOLINT(readability-identifier-naming) + GoUint64 headerBytes); // NOLINT(readability-identifier-naming) + +// go:linkname envoyGoFilterOnHttpData +// github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http.envoyGoFilterOnHttpData +extern GoUint64 envoyGoFilterOnHttpData(httpRequest* r, + GoUint64 endStream, // NOLINT(readability-identifier-naming) + GoUint64 buffer, + GoUint64 length); // NOLINT(readability-identifier-naming) + +// go:linkname envoyGoFilterOnHttpDestroy +// github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http.envoyGoFilterOnHttpDestroy +extern void envoyGoFilterOnHttpDestroy(httpRequest* r, GoUint64 reason); + +#ifdef __cplusplus +} +#endif diff --git a/contrib/golang/filters/http/source/config.cc b/contrib/golang/filters/http/source/config.cc new file mode 100644 index 0000000000000..cb64194f94f88 --- /dev/null +++ b/contrib/golang/filters/http/source/config.cc @@ -0,0 +1,57 @@ +#include "contrib/golang/filters/http/source/config.h" + +#include "envoy/registry/registry.h" + +#include "source/common/common/fmt.h" + +#include "contrib/golang/filters/http/source/common/dso/dso.h" +#include "contrib/golang/filters/http/source/golang_filter.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Golang { + +Http::FilterFactoryCb GolangFilterConfig::createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::golang::v3alpha::Config& proto_config, + const std::string&, Server::Configuration::FactoryContext&) { + + FilterConfigSharedPtr config = std::make_shared(proto_config); + + ENVOY_LOG_MISC(debug, "load golang library at parse config: {} {}", config->soId(), + config->soPath()); + + // loads DSO store a static map and a open handles leak will occur when the filter gets loaded and + // unloaded. + // TODO: unload DSO when filter updated. + auto res = Envoy::Dso::DsoInstanceManager::load(config->soId(), config->soPath()); + if (!res) { + throw EnvoyException( + fmt::format("golang_filter: load library failed: {} {}", config->soId(), config->soPath())); + } + + return [config](Http::FilterChainFactoryCallbacks& callbacks) { + auto filter = std::make_shared( + config, Dso::DsoInstanceManager::getDsoInstanceByID(config->soId())); + callbacks.addStreamFilter(filter); + callbacks.addAccessLogHandler(filter); + }; +} + +Router::RouteSpecificFilterConfigConstSharedPtr +GolangFilterConfig::createRouteSpecificFilterConfigTyped( + const envoy::extensions::filters::http::golang::v3alpha::ConfigsPerRoute& proto_config, + Server::Configuration::ServerFactoryContext& context, ProtobufMessage::ValidationVisitor&) { + return std::make_shared(proto_config, context); +} + +/** + * Static registration for the golang extensions filter. @see RegisterFactory. + */ +REGISTER_FACTORY(GolangFilterConfig, + Server::Configuration::NamedHttpFilterConfigFactory){"envoy.golang"}; + +} // namespace Golang +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/golang/filters/http/source/config.h b/contrib/golang/filters/http/source/config.h new file mode 100644 index 0000000000000..0b8ed1e4ae16d --- /dev/null +++ b/contrib/golang/filters/http/source/config.h @@ -0,0 +1,44 @@ +#pragma once + +#include "envoy/server/lifecycle_notifier.h" + +#include "source/extensions/filters/http/common/factory_base.h" + +#include "contrib/envoy/extensions/filters/http/golang/v3alpha/golang.pb.h" +#include "contrib/envoy/extensions/filters/http/golang/v3alpha/golang.pb.validate.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Golang { + +constexpr char CanonicalName[] = "envoy.filters.http.golang"; + +/** + * Config registration for the golang extentions filter. @see + * NamedHttpFilterConfigFactory. + */ +class GolangFilterConfig : public Common::FactoryBase< + envoy::extensions::filters::http::golang::v3alpha::Config, + envoy::extensions::filters::http::golang::v3alpha::ConfigsPerRoute> { +public: + GolangFilterConfig() : FactoryBase(CanonicalName) {} + +private: + Server::ServerLifecycleNotifier::HandlePtr handler_{}; + + Http::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::golang::v3alpha::Config& proto_config, + const std::string& stats_prefix, + Server::Configuration::FactoryContext& factory_context) override; + + Router::RouteSpecificFilterConfigConstSharedPtr createRouteSpecificFilterConfigTyped( + const envoy::extensions::filters::http::golang::v3alpha::ConfigsPerRoute& proto_config, + Server::Configuration::ServerFactoryContext& context, + ProtobufMessage::ValidationVisitor& validator) override; +}; + +} // namespace Golang +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/golang/filters/http/source/go/BUILD b/contrib/golang/filters/http/source/go/BUILD new file mode 100644 index 0000000000000..3d2a952589095 --- /dev/null +++ b/contrib/golang/filters/http/source/go/BUILD @@ -0,0 +1,24 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary") + +licenses(["notice"]) # Apache 2 + +go_binary( + name = "libgolang.so", + srcs = [ + "api.h", + "export.go", + "main.go", + ], + out = "libgolang.so", + cgo = True, + importpath = "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go", + linkmode = "c-shared", + visibility = ["//visibility:public"], + deps = [ + "//contrib/golang/filters/http/source/go/pkg/api", + "//contrib/golang/filters/http/source/go/pkg/http", + "//contrib/golang/filters/http/source/go/pkg/utils", + "@com_github_cncf_xds_go//udpa/type/v1:type", + "@org_golang_google_protobuf//types/known/anypb", + ], +) diff --git a/contrib/golang/filters/http/source/go/api.h b/contrib/golang/filters/http/source/go/api.h new file mode 120000 index 0000000000000..eb16a258f7744 --- /dev/null +++ b/contrib/golang/filters/http/source/go/api.h @@ -0,0 +1 @@ +./pkg/api/api.h \ No newline at end of file diff --git a/contrib/golang/filters/http/source/go/export.go b/contrib/golang/filters/http/source/go/export.go new file mode 100644 index 0000000000000..6f1f4840ff8ff --- /dev/null +++ b/contrib/golang/filters/http/source/go/export.go @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +/* +// ref https://github.com/golang/go/issues/25832 + +#cgo CFLAGS: -Ipkg/api -Ivendor/github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/api +#cgo linux LDFLAGS: -Wl,-unresolved-symbols=ignore-all +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup + +#include +#include + +#include "api.h" + +*/ +import "C" + +import ( + _ "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/api" + _ "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http" +) + +//go:linkname envoyGoFilterNewHttpPluginConfig github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http.envoyGoFilterNewHttpPluginConfig +//export envoyGoFilterNewHttpPluginConfig +func envoyGoFilterNewHttpPluginConfig(configPtr uint64, configLen uint64) uint64 + +//go:linkname envoyGoFilterDestroyHttpPluginConfig github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http.envoyGoFilterDestroyHttpPluginConfig +//export envoyGoFilterDestroyHttpPluginConfig +func envoyGoFilterDestroyHttpPluginConfig(id uint64) + +//go:linkname envoyGoFilterMergeHttpPluginConfig github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http.envoyGoFilterMergeHttpPluginConfig +//export envoyGoFilterMergeHttpPluginConfig +func envoyGoFilterMergeHttpPluginConfig(parentId uint64, childId uint64) uint64 + +//go:linkname envoyGoFilterOnHttpHeader github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http.envoyGoFilterOnHttpHeader +//export envoyGoFilterOnHttpHeader +func envoyGoFilterOnHttpHeader(r *C.httpRequest, endStream, headerNum, headerBytes uint64) uint64 + +//go:linkname envoyGoFilterOnHttpData github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http.envoyGoFilterOnHttpData +//export envoyGoFilterOnHttpData +func envoyGoFilterOnHttpData(r *C.httpRequest, endStream, buffer, length uint64) uint64 + +//go:linkname envoyGoFilterOnHttpDestroy github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http.envoyGoFilterOnHttpDestroy +//export envoyGoFilterOnHttpDestroy +func envoyGoFilterOnHttpDestroy(r *C.httpRequest, reason uint64) diff --git a/contrib/golang/filters/http/source/go/go.mod b/contrib/golang/filters/http/source/go/go.mod new file mode 100644 index 0000000000000..1b5a8eb97889e --- /dev/null +++ b/contrib/golang/filters/http/source/go/go.mod @@ -0,0 +1,5 @@ +module github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go + +go 1.18 + +require google.golang.org/protobuf v1.28.1 diff --git a/contrib/golang/filters/http/source/go/go.sum b/contrib/golang/filters/http/source/go/go.sum new file mode 100644 index 0000000000000..00f5993c956c4 --- /dev/null +++ b/contrib/golang/filters/http/source/go/go.sum @@ -0,0 +1,8 @@ +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/contrib/golang/filters/http/source/go/main.go b/contrib/golang/filters/http/source/go/main.go new file mode 100644 index 0000000000000..da29a2cadf1e0 --- /dev/null +++ b/contrib/golang/filters/http/source/go/main.go @@ -0,0 +1,4 @@ +package main + +func main() { +} diff --git a/contrib/golang/filters/http/source/go/pkg/api/BUILD b/contrib/golang/filters/http/source/go/pkg/api/BUILD new file mode 100644 index 0000000000000..45b8bc46cb1a9 --- /dev/null +++ b/contrib/golang/filters/http/source/go/pkg/api/BUILD @@ -0,0 +1,17 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +licenses(["notice"]) # Apache 2 + +go_library( + name = "api", + srcs = [ + "capi.go", + "filter.go", + "type.go", + ], + importpath = "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/api", + visibility = ["//visibility:public"], + deps = [ + "@org_golang_google_protobuf//types/known/anypb", + ], +) diff --git a/contrib/golang/filters/http/source/go/pkg/api/api.h b/contrib/golang/filters/http/source/go/pkg/api/api.h new file mode 100644 index 0000000000000..ebfeb23d77ee5 --- /dev/null +++ b/contrib/golang/filters/http/source/go/pkg/api/api.h @@ -0,0 +1,48 @@ +#pragma once + +// NOLINT(namespace-envoy) + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { // NOLINT(modernize-use-using) + const char* data; + unsigned long long int len; +} Cstring; + +typedef struct { // NOLINT(modernize-use-using) + Cstring plugin_name; + unsigned long long int configId; + int phase; +} httpRequest; + +typedef enum { // NOLINT(modernize-use-using) + Set, + Append, + Prepend, +} bufferAction; + +void envoyGoFilterHttpContinue(void* r, int status); +void envoyGoFilterHttpSendLocalReply(void* r, int response_code, void* body_text, void* headers, + long long int grpc_status, void* details); + +void envoyGoFilterHttpGetHeader(void* r, void* key, void* value); +void envoyGoFilterHttpCopyHeaders(void* r, void* strs, void* buf); +void envoyGoFilterHttpSetHeader(void* r, void* key, void* value); +void envoyGoFilterHttpRemoveHeader(void* r, void* key); + +void envoyGoFilterHttpGetBuffer(void* r, unsigned long long int buffer, void* value); +void envoyGoFilterHttpSetBufferHelper(void* r, unsigned long long int buffer, void* data, + int length, bufferAction action); + +void envoyGoFilterHttpCopyTrailers(void* r, void* strs, void* buf); +void envoyGoFilterHttpSetTrailer(void* r, void* key, void* value); + +void envoyGoFilterHttpGetStringValue(void* r, int id, void* value); + +void envoyGoFilterHttpFinalize(void* r, int reason); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/contrib/golang/filters/http/source/go/pkg/api/capi.go b/contrib/golang/filters/http/source/go/pkg/api/capi.go new file mode 100644 index 0000000000000..f201e67a2261f --- /dev/null +++ b/contrib/golang/filters/http/source/go/pkg/api/capi.go @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package api + +import "unsafe" + +type HttpCAPI interface { + HttpContinue(r unsafe.Pointer, status uint64) + HttpSendLocalReply(r unsafe.Pointer, responseCode int, bodyText string, headers map[string]string, grpcStatus int64, details string) + + // experience api, memory unsafe + HttpGetHeader(r unsafe.Pointer, key *string, value *string) + HttpCopyHeaders(r unsafe.Pointer, num uint64, bytes uint64) map[string]string + HttpSetHeader(r unsafe.Pointer, key *string, value *string) + HttpRemoveHeader(r unsafe.Pointer, key *string) + + HttpGetBuffer(r unsafe.Pointer, bufferPtr uint64, value *string, length uint64) + HttpSetBufferHelper(r unsafe.Pointer, bufferPtr uint64, value string, action BufferAction) + + HttpCopyTrailers(r unsafe.Pointer, num uint64, bytes uint64) map[string]string + HttpSetTrailer(r unsafe.Pointer, key *string, value *string) + + HttpGetRouteName(r unsafe.Pointer) string + + HttpFinalize(r unsafe.Pointer, reason int) +} diff --git a/contrib/golang/filters/http/source/go/pkg/api/export.txt b/contrib/golang/filters/http/source/go/pkg/api/export.txt new file mode 120000 index 0000000000000..b391c787d07e3 --- /dev/null +++ b/contrib/golang/filters/http/source/go/pkg/api/export.txt @@ -0,0 +1 @@ +../../export.go \ No newline at end of file diff --git a/contrib/golang/filters/http/source/go/pkg/api/filter.go b/contrib/golang/filters/http/source/go/pkg/api/filter.go new file mode 100644 index 0000000000000..072ef4a4c0ab4 --- /dev/null +++ b/contrib/golang/filters/http/source/go/pkg/api/filter.go @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package api + +import "google.golang.org/protobuf/types/known/anypb" + +// request +type StreamDecoderFilter interface { + DecodeHeaders(RequestHeaderMap, bool) StatusType + DecodeData(BufferInstance, bool) StatusType + DecodeTrailers(RequestTrailerMap) StatusType + // TODO add more for metadata +} + +// TODO merge it to StreamFilterConfigFactory +type StreamFilterConfigParser interface { + Parse(any *anypb.Any) interface{} + Merge(parentConfig interface{}, childConfig interface{}) interface{} +} + +type StreamFilterConfigFactory func(config interface{}) StreamFilterFactory +type StreamFilterFactory func(callbacks FilterCallbackHandler) StreamFilter + +type StreamFilter interface { + // http request + StreamDecoderFilter + // response stream + StreamEncoderFilter + // destroy filter + OnDestroy(DestroyReason) + // TODO add more for stream complete and log phase +} + +// response +type StreamEncoderFilter interface { + EncodeHeaders(ResponseHeaderMap, bool) StatusType + EncodeData(BufferInstance, bool) StatusType + EncodeTrailers(ResponseTrailerMap) StatusType + // TODO add more for metadata +} + +// stream info +// refer https://github.com/envoyproxy/envoy/blob/main/envoy/stream_info/stream_info.h +type StreamInfo interface { + GetRouteName() string + // TODO add more for stream info +} + +type StreamFilterCallbacks interface { + StreamInfo() StreamInfo +} + +type FilterCallbacks interface { + StreamFilterCallbacks + // Continue or SendLocalReply should be last API invoked, no more code after them. + Continue(StatusType) + SendLocalReply(responseCode int, bodyText string, headers map[string]string, grpcStatus int64, details string) + // TODO add more for filter callbacks +} + +type FilterCallbackHandler interface { + FilterCallbacks +} diff --git a/contrib/golang/filters/http/source/go/pkg/api/type.go b/contrib/golang/filters/http/source/go/pkg/api/type.go new file mode 100644 index 0000000000000..1160ed01fbad2 --- /dev/null +++ b/contrib/golang/filters/http/source/go/pkg/api/type.go @@ -0,0 +1,259 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package api + +// ****************** filter status start ******************// +type StatusType int + +const ( + Running StatusType = 0 + LocalReply StatusType = 1 + Continue StatusType = 2 + StopAndBuffer StatusType = 3 + StopAndBufferWatermark StatusType = 4 + StopNoBuffer StatusType = 5 +) + +// header status +// refer https://github.com/envoyproxy/envoy/blob/main/envoy/http/filter.h +const ( + HeaderContinue StatusType = 100 + HeaderStopIteration StatusType = 101 + HeaderContinueAndDontEndStream StatusType = 102 + HeaderStopAllIterationAndBuffer StatusType = 103 + HeaderStopAllIterationAndWatermark StatusType = 104 +) + +// data status +// refer https://github.com/envoyproxy/envoy/blob/main/envoy/http/filter.h +const ( + DataContinue StatusType = 200 + DataStopIterationAndBuffer StatusType = 201 + DataStopIterationAndWatermark StatusType = 202 + DataStopIterationNoBuffer StatusType = 203 +) + +// Trailer status +// refer https://github.com/envoyproxy/envoy/blob/main/envoy/http/filter.h +const ( + TrailerContinue StatusType = 300 + TrailerStopIteration StatusType = 301 +) + +//****************** filter status end ******************// + +// ****************** log level start ******************// +type LogType int + +// refer https://github.com/envoyproxy/envoy/blob/main/source/common/common/base_logger.h +const ( + Trace LogType = 0 + Debug LogType = 1 + Info LogType = 2 + Warn LogType = 3 + Error LogType = 4 + Critical LogType = 5 +) + +//******************* log level end *******************// + +// ****************** HeaderMap start ******************// + +// refer https://github.com/envoyproxy/envoy/blob/main/envoy/http/header_map.h +type HeaderMap interface { + // GetRaw is unsafe, reuse the memory from Envoy + GetRaw(name string) string + + // Get value of key + // If multiple values associated with this key, first one will be returned. + Get(key string) (string, bool) + + // Set key-value pair in header map, the previous pair will be replaced if exists + Set(key, value string) + + // Add value for given key. + // Multiple headers with the same key may be added with this function. + // Use Set for setting a single header for the given key. + Add(key, value string) + + // Del delete pair of specified key + Del(key string) + + // Range calls f sequentially for each key and value present in the map. + // If f returns false, range stops the iteration. + Range(f func(key, value string) bool) + + // ByteSize return size of HeaderMap + ByteSize() uint64 +} + +type RequestHeaderMap interface { + HeaderMap + // others +} + +type RequestTrailerMap interface { + HeaderMap + // others +} + +type ResponseHeaderMap interface { + HeaderMap + // others +} + +type ResponseTrailerMap interface { + HeaderMap + // others +} + +type MetadataMap interface { +} + +//****************** HeaderMap end ******************// + +// *************** BufferInstance start **************// +type BufferAction int + +const ( + SetBuffer BufferAction = 0 + AppendBuffer BufferAction = 1 + PrependBuffer BufferAction = 2 +) + +type DataBufferBase interface { + // Write appends the contents of p to the buffer, growing the buffer as + // needed. The return value n is the length of p; err is always nil. If the + // buffer becomes too large, Write will panic with ErrTooLarge. + Write(p []byte) (n int, err error) + + // WriteString appends the string to the buffer, growing the buffer as + // needed. The return value n is the length of s; err is always nil. If the + // buffer becomes too large, Write will panic with ErrTooLarge. + WriteString(s string) (n int, err error) + + // WriteByte appends the byte to the buffer, growing the buffer as + // needed. The return value n is the length of s; err is always nil. If the + // buffer becomes too large, Write will panic with ErrTooLarge. + WriteByte(p byte) error + + // WriteUint16 appends the uint16 to the buffer, growing the buffer as + // needed. The return value n is the length of s; err is always nil. If the + // buffer becomes too large, Write will panic with ErrTooLarge. + WriteUint16(p uint16) error + + // WriteUint32 appends the uint32 to the buffer, growing the buffer as + // needed. The return value n is the length of s; err is always nil. If the + // buffer becomes too large, Write will panic with ErrTooLarge. + WriteUint32(p uint32) error + + // WriteUint64 appends the uint64 to the buffer, growing the buffer as + // needed. The return value n is the length of s; err is always nil. If the + // buffer becomes too large, Write will panic with ErrTooLarge. + WriteUint64(p uint64) error + + // Peek returns n bytes from buffer, without draining any buffered data. + // If n > readable buffer, nil will be returned. + // It can be used in codec to check first-n-bytes magic bytes + // Note: do not change content in return bytes, use write instead + Peek(n int) []byte + + // Bytes returns all bytes from buffer, without draining any buffered data. + // It can be used to get fixed-length content, such as headers, body. + // Note: do not change content in return bytes, use write instead + Bytes() []byte + + // Drain drains a offset length of bytes in buffer. + // It can be used with Bytes(), after consuming a fixed-length of data + Drain(offset int) + + // Len returns the number of bytes of the unread portion of the buffer; + // b.Len() == len(b.Bytes()). + Len() int + + // Reset resets the buffer to be empty. + Reset() + + // String returns the contents of the buffer as a string. + String() string + + // Append append the contents of the slice data to the buffer. + Append(data []byte) error +} + +type BufferInstance interface { + DataBufferBase + + // Set overwrite the whole buffer content with byte slice. + Set([]byte) error + + // SetString overwrite the whole buffer content with string. + SetString(string) error + + // Prepend prepend the contents of the slice data to the buffer. + Prepend(data []byte) error + + // Prepend prepend the contents of the string data to the buffer. + PrependString(s string) error + + // Append append the contents of the string data to the buffer. + AppendString(s string) error +} + +//*************** BufferInstance end **************// + +type DestroyReason int + +const ( + Normal DestroyReason = 0 + Terminate DestroyReason = 1 +) + +const ( + NormalFinalize int = 0 // normal, finalize on destroy + GCFinalize int = 1 // finalize in GC sweep +) + +type EnvoyRequestPhase int + +const ( + DecodeHeaderPhase EnvoyRequestPhase = iota + 1 + DecodeDataPhase + DecodeTrailerPhase + EncodeHeaderPhase + EncodeDataPhase + EncodeTrailerPhase +) + +func (e EnvoyRequestPhase) String() string { + switch e { + case DecodeHeaderPhase: + return "DecodeHeader" + case DecodeDataPhase: + return "DecodeData" + case DecodeTrailerPhase: + return "DecodeTrailer" + case EncodeHeaderPhase: + return "EncodeHeader" + case EncodeDataPhase: + return "EncodeData" + case EncodeTrailerPhase: + return "EncodeTrailer" + } + return "unknown phase" +} diff --git a/contrib/golang/filters/http/source/go/pkg/http/BUILD b/contrib/golang/filters/http/source/go/pkg/http/BUILD new file mode 100644 index 0000000000000..a03bf4b8b9e98 --- /dev/null +++ b/contrib/golang/filters/http/source/go/pkg/http/BUILD @@ -0,0 +1,41 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +licenses(["notice"]) # Apache 2 + +go_library( + name = "http", + srcs = [ + "api.h", + "capi_impl.go", + "config.go", + "filter.go", + "filtermanager.go", + "passthrough.go", + "shim.go", + "type.go", + ], + cgo = True, + clinkopts = select({ + "@io_bazel_rules_go//go/platform:android": [ + "-Wl,-unresolved-symbols=ignore-all", + ], + "@io_bazel_rules_go//go/platform:darwin": [ + "-Wl,-undefined,dynamic_lookup", + ], + "@io_bazel_rules_go//go/platform:ios": [ + "-Wl,-undefined,dynamic_lookup", + ], + "@io_bazel_rules_go//go/platform:linux": [ + "-Wl,-unresolved-symbols=ignore-all", + ], + "//conditions:default": [], + }), + importpath = "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http", + visibility = ["//visibility:public"], + deps = [ + "//contrib/golang/filters/http/source/go/pkg/api", + "//contrib/golang/filters/http/source/go/pkg/utils", + "@org_golang_google_protobuf//proto", + "@org_golang_google_protobuf//types/known/anypb", + ], +) diff --git a/contrib/golang/filters/http/source/go/pkg/http/api.h b/contrib/golang/filters/http/source/go/pkg/http/api.h new file mode 120000 index 0000000000000..7ccbc18e959f7 --- /dev/null +++ b/contrib/golang/filters/http/source/go/pkg/http/api.h @@ -0,0 +1 @@ +../api/api.h \ No newline at end of file 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 new file mode 100644 index 0000000000000..a7e540d01f5a4 --- /dev/null +++ b/contrib/golang/filters/http/source/go/pkg/http/capi_impl.go @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package http + +/* +// ref https://github.com/golang/go/issues/25832 + +#cgo CFLAGS: -I../api +#cgo linux LDFLAGS: -Wl,-unresolved-symbols=ignore-all +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup + +#include +#include + +#include "api.h" + +*/ +import "C" +import ( + "reflect" + "runtime" + "strings" + "unsafe" + + "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/api" +) + +const ( + ValueRouteName = 1 +) + +type httpCApiImpl struct{} + +func (c *httpCApiImpl) HttpContinue(r unsafe.Pointer, status uint64) { + C.envoyGoFilterHttpContinue(r, C.int(status)) +} + +func (c *httpCApiImpl) HttpSendLocalReply(r unsafe.Pointer, response_code int, body_text string, headers map[string]string, grpc_status int64, details string) { + hLen := len(headers) + strs := make([]string, 0, hLen) + for k, v := range headers { + strs = append(strs, k, v) + } + C.envoyGoFilterHttpSendLocalReply(r, C.int(response_code), unsafe.Pointer(&body_text), unsafe.Pointer(&strs), C.longlong(grpc_status), unsafe.Pointer(&details)) +} + +func (c *httpCApiImpl) HttpGetHeader(r unsafe.Pointer, key *string, value *string) { + C.envoyGoFilterHttpGetHeader(r, unsafe.Pointer(key), unsafe.Pointer(value)) +} + +func (c *httpCApiImpl) HttpCopyHeaders(r unsafe.Pointer, num uint64, bytes uint64) map[string]string { + // TODO: use a memory pool for better performance, + // since these go strings in strs, will be copied into the following map. + strs := make([]string, num*2) + // but, this buffer can not be reused safely, + // since strings may refer to this buffer as string data, and string is const in go. + // we have to make sure the all strings is not using before reusing, + // but strings may be alive beyond the request life. + buf := make([]byte, bytes) + sHeader := (*reflect.SliceHeader)(unsafe.Pointer(&strs)) + bHeader := (*reflect.SliceHeader)(unsafe.Pointer(&buf)) + + C.envoyGoFilterHttpCopyHeaders(r, unsafe.Pointer(sHeader.Data), unsafe.Pointer(bHeader.Data)) + + m := make(map[string]string, num) + for i := uint64(0); i < num*2; i += 2 { + key := strs[i] + value := strs[i+1] + m[key] = value + } + runtime.KeepAlive(buf) + return m +} + +func (c *httpCApiImpl) HttpSetHeader(r unsafe.Pointer, key *string, value *string) { + C.envoyGoFilterHttpSetHeader(r, unsafe.Pointer(key), unsafe.Pointer(value)) +} + +func (c *httpCApiImpl) HttpRemoveHeader(r unsafe.Pointer, key *string) { + C.envoyGoFilterHttpRemoveHeader(r, unsafe.Pointer(key)) +} + +func (c *httpCApiImpl) HttpGetBuffer(r unsafe.Pointer, bufferPtr uint64, value *string, length uint64) { + buf := make([]byte, length) + bHeader := (*reflect.SliceHeader)(unsafe.Pointer(&buf)) + sHeader := (*reflect.StringHeader)(unsafe.Pointer(value)) + sHeader.Data = bHeader.Data + sHeader.Len = int(length) + C.envoyGoFilterHttpGetBuffer(r, C.ulonglong(bufferPtr), unsafe.Pointer(bHeader.Data)) +} + +func (c *httpCApiImpl) HttpSetBufferHelper(r unsafe.Pointer, bufferPtr uint64, value string, action api.BufferAction) { + sHeader := (*reflect.StringHeader)(unsafe.Pointer(&value)) + var act C.bufferAction + switch action { + case api.SetBuffer: + act = C.Set + case api.AppendBuffer: + act = C.Append + case api.PrependBuffer: + act = C.Prepend + } + C.envoyGoFilterHttpSetBufferHelper(r, C.ulonglong(bufferPtr), unsafe.Pointer(sHeader.Data), C.int(sHeader.Len), act) +} + +func (c *httpCApiImpl) HttpCopyTrailers(r unsafe.Pointer, num uint64, bytes uint64) map[string]string { + // TODO: use a memory pool for better performance, + // but, should be very careful, since string is const in go, + // and we have to make sure the strings is not using before reusing, + // strings may be alive beyond the request life. + strs := make([]string, num*2) + buf := make([]byte, bytes) + sHeader := (*reflect.SliceHeader)(unsafe.Pointer(&strs)) + bHeader := (*reflect.SliceHeader)(unsafe.Pointer(&buf)) + + C.envoyGoFilterHttpCopyTrailers(r, unsafe.Pointer(sHeader.Data), unsafe.Pointer(bHeader.Data)) + + m := make(map[string]string, num) + for i := uint64(0); i < num*2; i += 2 { + key := strs[i] + value := strs[i+1] + m[key] = value + } + return m +} + +func (c *httpCApiImpl) HttpSetTrailer(r unsafe.Pointer, key *string, value *string) { + C.envoyGoFilterHttpSetTrailer(r, unsafe.Pointer(key), unsafe.Pointer(value)) +} + +func (c *httpCApiImpl) HttpGetRouteName(r unsafe.Pointer) string { + var value string + C.envoyGoFilterHttpGetStringValue(r, ValueRouteName, unsafe.Pointer(&value)) + // copy the memory from c to Go. + return strings.Clone(value) +} + +func (c *httpCApiImpl) HttpFinalize(r unsafe.Pointer, reason int) { + C.envoyGoFilterHttpFinalize(r, C.int(reason)) +} + +var cAPI api.HttpCAPI = &httpCApiImpl{} + +// SetHttpCAPI for mock cAPI +func SetHttpCAPI(api api.HttpCAPI) { + cAPI = api +} diff --git a/contrib/golang/filters/http/source/go/pkg/http/config.go b/contrib/golang/filters/http/source/go/pkg/http/config.go new file mode 100644 index 0000000000000..926357aa99ef5 --- /dev/null +++ b/contrib/golang/filters/http/source/go/pkg/http/config.go @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package http + +/* +// ref https://github.com/golang/go/issues/25832 + +#cgo linux LDFLAGS: -Wl,-unresolved-symbols=ignore-all +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup + +#include +#include + +#include "api.h" + +*/ +import "C" + +import ( + "fmt" + "sync" + "sync/atomic" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/utils" +) + +var ( + configNumGenerator uint64 + configCache = &sync.Map{} // uint64 -> *anypb.Any +) + +func envoyGoFilterNewHttpPluginConfig(configPtr uint64, configLen uint64) uint64 { + buf := utils.BytesToSlice(configPtr, configLen) + var any anypb.Any + proto.Unmarshal(buf, &any) + + configNum := atomic.AddUint64(&configNumGenerator, 1) + if httpFilterConfigParser != nil { + configCache.Store(configNum, httpFilterConfigParser.Parse(&any)) + } else { + configCache.Store(configNum, &any) + } + + return configNum +} + +func envoyGoFilterDestroyHttpPluginConfig(id uint64) { + configCache.Delete(id) +} + +func envoyGoFilterMergeHttpPluginConfig(parentId uint64, childId uint64) uint64 { + if httpFilterConfigParser != nil { + parent, ok := configCache.Load(parentId) + if !ok { + panic(fmt.Sprintf("merge config: get parentId: %d config failed", parentId)) + } + child, ok := configCache.Load(childId) + if !ok { + panic(fmt.Sprintf("merge config: get childId: %d config failed", childId)) + } + + new := httpFilterConfigParser.Merge(parent, child) + configNum := atomic.AddUint64(&configNumGenerator, 1) + configCache.Store(configNum, new) + return configNum + + } else { + // child override parent by default + return childId + } +} diff --git a/contrib/golang/filters/http/source/go/pkg/http/filter.go b/contrib/golang/filters/http/source/go/pkg/http/filter.go new file mode 100644 index 0000000000000..8f21baff970c5 --- /dev/null +++ b/contrib/golang/filters/http/source/go/pkg/http/filter.go @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package http + +/* +// ref https://github.com/golang/go/issues/25832 + +#cgo CFLAGS: -I../api +#cgo linux LDFLAGS: -Wl,-unresolved-symbols=ignore-all +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup + +#include +#include + +#include "api.h" + +*/ +import "C" +import ( + "fmt" + "unsafe" + + "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/api" +) + +type httpRequest struct { + req *C.httpRequest + httpFilter api.StreamFilter +} + +func (r *httpRequest) Continue(status api.StatusType) { + if status == api.LocalReply { + fmt.Printf("warning: LocalReply status is useless after sendLocalReply, ignoring") + return + } + cAPI.HttpContinue(unsafe.Pointer(r.req), uint64(status)) +} + +func (r *httpRequest) SendLocalReply(responseCode int, bodyText string, headers map[string]string, grpcStatus int64, details string) { + cAPI.HttpSendLocalReply(unsafe.Pointer(r.req), responseCode, bodyText, headers, grpcStatus, details) +} + +func (r *httpRequest) StreamInfo() api.StreamInfo { + return &streamInfo{ + request: r, + } +} + +func (r *httpRequest) Finalize(reason int) { + cAPI.HttpFinalize(unsafe.Pointer(r.req), reason) +} + +type streamInfo struct { + request *httpRequest +} + +func (s *streamInfo) GetRouteName() string { + return cAPI.HttpGetRouteName(unsafe.Pointer(s.request.req)) +} diff --git a/contrib/golang/filters/http/source/go/pkg/http/filtermanager.go b/contrib/golang/filters/http/source/go/pkg/http/filtermanager.go new file mode 100644 index 0000000000000..32bc359f8ccdb --- /dev/null +++ b/contrib/golang/filters/http/source/go/pkg/http/filtermanager.go @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package http + +import ( + "fmt" + "sync" + + "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/api" +) + +var httpFilterConfigFactory = sync.Map{} + +func RegisterHttpFilterConfigFactory(name string, f api.StreamFilterConfigFactory) { + httpFilterConfigFactory.Store(name, f) +} + +// no parser by default +var httpFilterConfigParser api.StreamFilterConfigParser = nil + +// TODO merge it to api.HttpFilterConfigFactory +func RegisterHttpFilterConfigParser(parser api.StreamFilterConfigParser) { + httpFilterConfigParser = parser +} + +func getOrCreateHttpFilterFactory(name string, configId uint64) api.StreamFilterFactory { + config, ok := configCache.Load(configId) + if !ok { + panic(fmt.Sprintf("get config failed, plugin: %s, configId: %d", name, configId)) + } + + if v, ok := httpFilterConfigFactory.Load(name); ok { + return (v.(api.StreamFilterConfigFactory))(config) + } + + // pass through by default + return PassThroughFactory(config) +} + +// streaming and async supported by default +func RegisterStreamingHttpFilterConfigFactory(name string, f api.StreamFilterConfigFactory) { + httpFilterConfigFactory.Store(name, f) +} diff --git a/contrib/golang/filters/http/source/go/pkg/http/passthrough.go b/contrib/golang/filters/http/source/go/pkg/http/passthrough.go new file mode 100644 index 0000000000000..3079d4e39f725 --- /dev/null +++ b/contrib/golang/filters/http/source/go/pkg/http/passthrough.go @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package http + +import ( + "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/api" +) + +type passThroughFilter struct { + callbacks api.FilterCallbackHandler +} + +func (f *passThroughFilter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType { + return api.Continue +} + +func (f *passThroughFilter) DecodeData(buffer api.BufferInstance, endStream bool) api.StatusType { + return api.Continue +} + +func (f *passThroughFilter) DecodeTrailers(trailers api.RequestTrailerMap) api.StatusType { + return api.Continue +} + +func (f *passThroughFilter) EncodeHeaders(header api.ResponseHeaderMap, endStream bool) api.StatusType { + return api.Continue +} + +func (f *passThroughFilter) EncodeData(buffer api.BufferInstance, endStream bool) api.StatusType { + return api.Continue +} + +func (f *passThroughFilter) EncodeTrailers(trailers api.ResponseTrailerMap) api.StatusType { + return api.Continue +} + +func (f *passThroughFilter) OnDestroy(reason api.DestroyReason) { +} + +func PassThroughFactory(interface{}) api.StreamFilterFactory { + return func(callbacks api.FilterCallbackHandler) api.StreamFilter { + return &passThroughFilter{ + callbacks: callbacks, + } + } +} diff --git a/contrib/golang/filters/http/source/go/pkg/http/shim.go b/contrib/golang/filters/http/source/go/pkg/http/shim.go new file mode 100644 index 0000000000000..5849ac9c8a4b2 --- /dev/null +++ b/contrib/golang/filters/http/source/go/pkg/http/shim.go @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package http + +/* +// ref https://github.com/golang/go/issues/25832 + +#cgo CFLAGS: -I../api +#cgo linux LDFLAGS: -Wl,-unresolved-symbols=ignore-all +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup + +#include +#include + +#include "api.h" + +*/ +import "C" + +import ( + "errors" + "fmt" + "runtime" + "sync" + + "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/api" +) + +var ErrDupRequestKey = errors.New("dup request key") + +var Requests = &requestMap{} + +type requestMap struct { + m sync.Map // *C.httpRequest -> *httpRequest +} + +func (f *requestMap) StoreReq(key *C.httpRequest, req *httpRequest) error { + if _, loaded := f.m.LoadOrStore(key, req); loaded { + return ErrDupRequestKey + } + return nil +} + +func (f *requestMap) GetReq(key *C.httpRequest) *httpRequest { + if v, ok := f.m.Load(key); ok { + return v.(*httpRequest) + } + return nil +} + +func (f *requestMap) DeleteReq(key *C.httpRequest) { + f.m.Delete(key) +} + +func (f *requestMap) Clear() { + f.m.Range(func(key, _ interface{}) bool { + f.m.Delete(key) + return true + }) +} + +func requestFinalize(r *httpRequest) { + r.Finalize(api.NormalFinalize) +} + +func createRequest(r *C.httpRequest) *httpRequest { + req := &httpRequest{ + req: r, + } + // NP: make sure filter will be deleted. + runtime.SetFinalizer(req, requestFinalize) + + err := Requests.StoreReq(r, req) + if err != nil { + panic(fmt.Sprintf("createRequest failed, err: %s", err.Error())) + } + + configId := uint64(r.configId) + filterFactory := getOrCreateHttpFilterFactory(C.GoStringN(r.plugin_name.data, C.int(r.plugin_name.len)), configId) + f := filterFactory(req) + req.httpFilter = f + + return req +} + +func getRequest(r *C.httpRequest) *httpRequest { + return Requests.GetReq(r) +} + +func envoyGoFilterOnHttpHeader(r *C.httpRequest, endStream, headerNum, headerBytes uint64) uint64 { + var req *httpRequest + phase := api.EnvoyRequestPhase(r.phase) + if phase == api.DecodeHeaderPhase { + req = createRequest(r) + } else { + req = getRequest(r) + // early sendLocalReply may skip the whole decode phase + if req == nil { + req = createRequest(r) + } + } + f := req.httpFilter + + header := &httpHeaderMap{ + request: req, + headerNum: headerNum, + headerBytes: headerBytes, + isTrailer: phase == api.DecodeTrailerPhase || phase == api.EncodeTrailerPhase, + } + + var status api.StatusType + switch phase { + case api.DecodeHeaderPhase: + status = f.DecodeHeaders(header, endStream == 1) + case api.DecodeTrailerPhase: + status = f.DecodeTrailers(header) + case api.EncodeHeaderPhase: + status = f.EncodeHeaders(header, endStream == 1) + case api.EncodeTrailerPhase: + status = f.EncodeTrailers(header) + } + return uint64(status) +} + +func envoyGoFilterOnHttpData(r *C.httpRequest, endStream, buffer, length uint64) uint64 { + req := getRequest(r) + + f := req.httpFilter + isDecode := api.EnvoyRequestPhase(r.phase) == api.DecodeDataPhase + + buf := &httpBuffer{ + request: req, + envoyBufferInstance: buffer, + length: length, + } + + var status api.StatusType + if isDecode { + status = f.DecodeData(buf, endStream == 1) + } else { + status = f.EncodeData(buf, endStream == 1) + } + return uint64(status) +} + +func envoyGoFilterOnHttpDestroy(r *C.httpRequest, reason uint64) { + req := getRequest(r) + + v := api.DestroyReason(reason) + + f := req.httpFilter + f.OnDestroy(v) + + Requests.DeleteReq(r) + + // no one is using req now, we can remove it manually, for better performance. + if v == api.Normal { + runtime.SetFinalizer(req, nil) + req.Finalize(api.GCFinalize) + } +} diff --git a/contrib/golang/filters/http/source/go/pkg/http/type.go b/contrib/golang/filters/http/source/go/pkg/http/type.go new file mode 100644 index 0000000000000..873eba45d6762 --- /dev/null +++ b/contrib/golang/filters/http/source/go/pkg/http/type.go @@ -0,0 +1,195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package http + +import ( + "strconv" + "unsafe" + + "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/api" +) + +type httpHeaderMap struct { + request *httpRequest + headers map[string]string + headerNum uint64 + headerBytes uint64 + isTrailer bool +} + +var _ api.HeaderMap = (*httpHeaderMap)(nil) + +func (h *httpHeaderMap) GetRaw(key string) string { + if h.isTrailer { + panic("unsupported yet") + } + var value string + cAPI.HttpGetHeader(unsafe.Pointer(h.request.req), &key, &value) + return value +} + +func (h *httpHeaderMap) Get(key string) (string, bool) { + if h.headers == nil { + if h.isTrailer { + h.headers = cAPI.HttpCopyTrailers(unsafe.Pointer(h.request.req), h.headerNum, h.headerBytes) + } else { + h.headers = cAPI.HttpCopyHeaders(unsafe.Pointer(h.request.req), h.headerNum, h.headerBytes) + } + } + value, ok := h.headers[key] + return value, ok +} + +func (h *httpHeaderMap) Set(key, value string) { + if h.headers != nil { + h.headers[key] = value + } + if h.isTrailer { + cAPI.HttpSetTrailer(unsafe.Pointer(h.request.req), &key, &value) + } else { + cAPI.HttpSetHeader(unsafe.Pointer(h.request.req), &key, &value) + } +} + +func (h *httpHeaderMap) Add(key, value string) { + panic("unsupported yet") +} + +func (h *httpHeaderMap) Del(key string) { + if h.headers != nil { + delete(h.headers, key) + } + if h.isTrailer { + panic("unsupported yet") + } else { + cAPI.HttpRemoveHeader(unsafe.Pointer(h.request.req), &key) + } +} + +func (h *httpHeaderMap) Range(f func(key, value string) bool) { + panic("unsupported yet") +} + +// ByteSize return size of HeaderMap +func (h *httpHeaderMap) ByteSize() uint64 { + return h.headerBytes +} + +type httpBuffer struct { + request *httpRequest + envoyBufferInstance uint64 + length uint64 + value string +} + +var _ api.BufferInstance = (*httpBuffer)(nil) + +func (b *httpBuffer) Write(p []byte) (n int, err error) { + cAPI.HttpSetBufferHelper(unsafe.Pointer(b.request.req), b.envoyBufferInstance, string(p), api.AppendBuffer) + return len(p), nil +} + +func (b *httpBuffer) WriteString(s string) (n int, err error) { + cAPI.HttpSetBufferHelper(unsafe.Pointer(b.request.req), b.envoyBufferInstance, s, api.AppendBuffer) + return len(s), nil +} + +func (b *httpBuffer) WriteByte(p byte) error { + cAPI.HttpSetBufferHelper(unsafe.Pointer(b.request.req), b.envoyBufferInstance, string(p), api.AppendBuffer) + return nil +} + +func (b *httpBuffer) WriteUint16(p uint16) error { + s := strconv.FormatUint(uint64(p), 10) + _, err := b.WriteString(s) + return err +} + +func (b *httpBuffer) WriteUint32(p uint32) error { + s := strconv.FormatUint(uint64(p), 10) + _, err := b.WriteString(s) + return err +} + +func (b *httpBuffer) WriteUint64(p uint64) error { + s := strconv.FormatUint(uint64(p), 10) + _, err := b.WriteString(s) + return err +} + +func (b *httpBuffer) Peek(n int) []byte { + panic("implement me") +} + +func (b *httpBuffer) Bytes() []byte { + if b.length == 0 { + return nil + } + cAPI.HttpGetBuffer(unsafe.Pointer(b.request.req), b.envoyBufferInstance, &b.value, b.length) + return []byte(b.value) +} + +func (b *httpBuffer) Drain(offset int) { + panic("implement me") +} + +func (b *httpBuffer) Len() int { + return int(b.length) +} + +func (b *httpBuffer) Reset() { + panic("implement me") +} + +func (b *httpBuffer) String() string { + if b.length == 0 { + return "" + } + cAPI.HttpGetBuffer(unsafe.Pointer(b.request.req), b.envoyBufferInstance, &b.value, b.length) + return b.value +} + +func (b *httpBuffer) Append(data []byte) error { + cAPI.HttpSetBufferHelper(unsafe.Pointer(b.request.req), b.envoyBufferInstance, string(data), api.AppendBuffer) + return nil +} + +func (b *httpBuffer) Prepend(data []byte) error { + cAPI.HttpSetBufferHelper(unsafe.Pointer(b.request.req), b.envoyBufferInstance, string(data), api.PrependBuffer) + return nil +} + +func (b *httpBuffer) AppendString(s string) error { + cAPI.HttpSetBufferHelper(unsafe.Pointer(b.request.req), b.envoyBufferInstance, s, api.AppendBuffer) + return nil +} + +func (b *httpBuffer) PrependString(s string) error { + cAPI.HttpSetBufferHelper(unsafe.Pointer(b.request.req), b.envoyBufferInstance, s, api.PrependBuffer) + return nil +} + +func (b *httpBuffer) Set(data []byte) error { + cAPI.HttpSetBufferHelper(unsafe.Pointer(b.request.req), b.envoyBufferInstance, string(data), api.SetBuffer) + return nil +} + +func (b *httpBuffer) SetString(s string) error { + cAPI.HttpSetBufferHelper(unsafe.Pointer(b.request.req), b.envoyBufferInstance, s, api.SetBuffer) + return nil +} diff --git a/contrib/golang/filters/http/source/go/pkg/utils/BUILD b/contrib/golang/filters/http/source/go/pkg/utils/BUILD new file mode 100644 index 0000000000000..8b60bf500876d --- /dev/null +++ b/contrib/golang/filters/http/source/go/pkg/utils/BUILD @@ -0,0 +1,10 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +licenses(["notice"]) # Apache 2 + +go_library( + name = "utils", + srcs = ["string.go"], + importpath = "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/utils", + visibility = ["//visibility:public"], +) diff --git a/contrib/golang/filters/http/source/go/pkg/utils/string.go b/contrib/golang/filters/http/source/go/pkg/utils/string.go new file mode 100644 index 0000000000000..d117dc0b70b35 --- /dev/null +++ b/contrib/golang/filters/http/source/go/pkg/utils/string.go @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "reflect" + "unsafe" +) + +func BytesToString(ptr uint64, len uint64) string { + var s string + var sHdr = (*reflect.StringHeader)(unsafe.Pointer(&s)) + sHdr.Data = uintptr(ptr) + sHdr.Len = int(len) + return s +} + +func BytesToSlice(ptr uint64, len uint64) []byte { + var s []byte + var sHdr = (*reflect.SliceHeader)(unsafe.Pointer(&s)) + sHdr.Data = uintptr(ptr) + sHdr.Len = int(len) + sHdr.Cap = int(len) + return s +} diff --git a/contrib/golang/filters/http/source/golang_filter.cc b/contrib/golang/filters/http/source/golang_filter.cc new file mode 100644 index 0000000000000..40a515c8f00a3 --- /dev/null +++ b/contrib/golang/filters/http/source/golang_filter.cc @@ -0,0 +1,790 @@ +#include "contrib/golang/filters/http/source/golang_filter.h" + +#include +#include +#include + +#include "envoy/http/codes.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/base64.h" +#include "source/common/common/enum_to_int.h" +#include "source/common/common/lock_guard.h" +#include "source/common/common/utility.h" +#include "source/common/grpc/common.h" +#include "source/common/grpc/context_impl.h" +#include "source/common/grpc/status.h" +#include "source/common/http/headers.h" +#include "source/common/http/http1/codec_impl.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Golang { + +void Filter::onHeadersModified() { + // Any changes to request headers can affect how the request is going to be + // routed. If we are changing the headers we also need to clear the route + // cache. + decoding_state_.getFilterCallbacks()->downstreamCallbacks()->clearRouteCache(); +} + +Http::LocalErrorStatus Filter::onLocalReply(const LocalReplyData& data) { + auto& state = getProcessorState(); + ASSERT(state.isThreadSafe()); + ENVOY_LOG(debug, "golang filter onLocalReply, state: {}, phase: {}, code: {}", state.stateStr(), + state.phaseStr(), int(data.code_)); + + return Http::LocalErrorStatus::Continue; +} + +Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, bool end_stream) { + ProcessorState& state = decoding_state_; + + ENVOY_LOG(debug, "golang filter decodeHeaders, state: {}, phase: {}, end_stream: {}", + state.stateStr(), state.phaseStr(), end_stream); + + state.setEndStream(end_stream); + + bool done = doHeaders(state, headers, end_stream); + + return done ? Http::FilterHeadersStatus::Continue : Http::FilterHeadersStatus::StopIteration; +} + +Http::FilterDataStatus Filter::decodeData(Buffer::Instance& data, bool end_stream) { + ProcessorState& state = decoding_state_; + ENVOY_LOG(debug, + "golang filter decodeData, state: {}, phase: {}, data length: {}, end_stream: {}", + state.stateStr(), state.phaseStr(), data.length(), end_stream); + + state.setEndStream(end_stream); + + bool done = doData(state, data, end_stream); + + if (done) { + state.doDataList.moveOut(data); + return Http::FilterDataStatus::Continue; + } + + return Http::FilterDataStatus::StopIterationNoBuffer; +} + +Http::FilterTrailersStatus Filter::decodeTrailers(Http::RequestTrailerMap& trailers) { + ProcessorState& state = decoding_state_; + ENVOY_LOG(debug, "golang filter decodeTrailers, state: {}, phase: {}", state.stateStr(), + state.phaseStr()); + + state.setSeenTrailers(); + + bool done = doTrailer(state, trailers); + + return done ? Http::FilterTrailersStatus::Continue : Http::FilterTrailersStatus::StopIteration; +} + +Http::FilterHeadersStatus Filter::encodeHeaders(Http::ResponseHeaderMap& headers, bool end_stream) { + ProcessorState& state = getProcessorState(); + ENVOY_LOG(debug, "golang filter encodeHeaders, state: {}, phase: {}, end_stream: {}", + state.stateStr(), state.phaseStr(), end_stream); + + encoding_state_.setEndStream(end_stream); + + // NP: may enter encodeHeaders in any phase & any state_, + // since other filters or filtermanager could call encodeHeaders or sendLocalReply in any time. + // eg. filtermanager may invoke sendLocalReply, when scheme is invalid, + // with "Sending local reply with details // http1.invalid_scheme" details. + if (state.state() != FilterState::Done) { + ENVOY_LOG(debug, + "golang filter enter encodeHeaders early, maybe sendLocalReply or encodeHeaders " + "happened, current state: {}, phase: {}", + state.stateStr(), state.phaseStr()); + + ENVOY_LOG(debug, "golang filter drain data buffer since enter encodeHeaders early"); + // NP: is safe to overwrite it since go code won't read it directly + // need drain buffer to enable read when it's high watermark + state.drainBufferData(); + + // get the state before changing it. + bool in_go = state.isProcessingInGo(); + + if (in_go) { + // NP: wait go returns to avoid concurrency conflict in go side. + local_reply_waiting_go_ = true; + ENVOY_LOG(debug, "waiting go returns before handle the local reply from other filter"); + + // NP: save to another local_headers_ variable to avoid conflict, + // since the headers_ may be used in Go side. + local_headers_ = &headers; + + // can not use "StopAllIterationAndWatermark" here, since Go decodeHeaders may return + // stopAndBuffer, that means it need data buffer and not continue header. + return Http::FilterHeadersStatus::StopIteration; + + } else { + ENVOY_LOG(debug, "golang filter clear do data buffer before continue encodeHeader, " + "since no go code is running"); + state.doDataList.clearAll(); + } + } + + enter_encoding_ = true; + + bool done = doHeaders(encoding_state_, headers, end_stream); + + return done ? Http::FilterHeadersStatus::Continue : Http::FilterHeadersStatus::StopIteration; +} + +Http::FilterDataStatus Filter::encodeData(Buffer::Instance& data, bool end_stream) { + ProcessorState& state = getProcessorState(); + ENVOY_LOG(debug, + "golang filter encodeData, state: {}, phase: {}, data length: {}, end_stream: {}", + state.stateStr(), state.phaseStr(), data.length(), end_stream); + + encoding_state_.setEndStream(end_stream); + + if (local_reply_waiting_go_) { + ENVOY_LOG(debug, "golang filter appending data to buffer"); + encoding_state_.addBufferData(data); + return Http::FilterDataStatus::StopIterationNoBuffer; + } + + bool done = doData(encoding_state_, data, end_stream); + + if (done) { + state.doDataList.moveOut(data); + return Http::FilterDataStatus::Continue; + } + + return Http::FilterDataStatus::StopIterationNoBuffer; +} + +Http::FilterTrailersStatus Filter::encodeTrailers(Http::ResponseTrailerMap& trailers) { + ProcessorState& state = getProcessorState(); + ENVOY_LOG(debug, "golang filter encodeTrailers, state: {}, phase: {}", state.stateStr(), + state.phaseStr()); + + encoding_state_.setSeenTrailers(); + + if (local_reply_waiting_go_) { + // NP: save to another local_trailers_ variable to avoid conflict, + // since the trailers_ may be used in Go side. + local_trailers_ = &trailers; + return Http::FilterTrailersStatus::StopIteration; + } + + bool done = doTrailer(encoding_state_, trailers); + + return done ? Http::FilterTrailersStatus::Continue : Http::FilterTrailersStatus::StopIteration; +} + +void Filter::onDestroy() { + ENVOY_LOG(debug, "golang filter on destroy"); + + { + Thread::LockGuard lock(mutex_); + if (has_destroyed_) { + ENVOY_LOG(debug, "golang filter has been destroyed"); + return; + } + has_destroyed_ = true; + } + + ASSERT(req_ != nullptr); + auto& state = getProcessorState(); + auto reason = state.isProcessingInGo() ? DestroyReason::Terminate : DestroyReason::Normal; + + dynamic_lib_->envoyGoFilterOnHttpDestroy(req_, int(reason)); +} + +// access_log is executed before the log of the stream filter +void Filter::log(const Http::RequestHeaderMap*, const Http::ResponseHeaderMap*, + const Http::ResponseTrailerMap*, const StreamInfo::StreamInfo&) { + // Todo log phase of stream filter +} + +/*** common APIs for filter, both decode and encode ***/ + +GolangStatus Filter::doHeadersGo(ProcessorState& state, Http::RequestOrResponseHeaderMap& headers, + bool end_stream) { + ENVOY_LOG(debug, "golang filter passing data to golang, state: {}, phase: {}, end_stream: {}", + state.stateStr(), state.phaseStr(), end_stream); + + if (req_ == nullptr) { + // req is used by go, so need to use raw memory and then it is safe to release at the gc + // finalize phase of the go object. + req_ = new httpRequestInternal(weak_from_this()); + req_->configId = getMergedConfigId(state); + req_->plugin_name.data = config_->pluginName().data(); + req_->plugin_name.len = config_->pluginName().length(); + } + + req_->phase = static_cast(state.phase()); + { + Thread::LockGuard lock(mutex_); + headers_ = &headers; + } + auto status = dynamic_lib_->envoyGoFilterOnHttpHeader(req_, end_stream ? 1 : 0, headers.size(), + headers.byteSize()); + return static_cast(status); +} + +bool Filter::doHeaders(ProcessorState& state, Http::RequestOrResponseHeaderMap& headers, + bool end_stream) { + ENVOY_LOG(debug, "golang filter doHeaders, state: {}, phase: {}, end_stream: {}", + state.stateStr(), state.phaseStr(), end_stream); + + ASSERT(state.isBufferDataEmpty()); + + state.processHeader(end_stream); + auto status = doHeadersGo(state, headers, end_stream); + auto done = state.handleHeaderGolangStatus(status); + if (done) { + Thread::LockGuard lock(mutex_); + headers_ = nullptr; + } + return done; +} + +bool Filter::doDataGo(ProcessorState& state, Buffer::Instance& data, bool end_stream) { + ENVOY_LOG(debug, "golang filter passing data to golang, state: {}, phase: {}, end_stream: {}", + state.stateStr(), state.phaseStr(), end_stream); + + state.processData(end_stream); + + Buffer::Instance& buffer = state.doDataList.push(data); + + ASSERT(req_ != nullptr); + req_->phase = static_cast(state.phase()); + auto status = dynamic_lib_->envoyGoFilterOnHttpData( + req_, end_stream ? 1 : 0, reinterpret_cast(&buffer), buffer.length()); + + return state.handleDataGolangStatus(static_cast(status)); +} + +bool Filter::doData(ProcessorState& state, Buffer::Instance& data, bool end_stream) { + ENVOY_LOG(debug, "golang filter doData, state: {}, phase: {}, end_stream: {}", state.stateStr(), + state.phaseStr(), end_stream); + + bool done = false; + switch (state.state()) { + case FilterState::WaitingData: + done = doDataGo(state, data, end_stream); + break; + case FilterState::WaitingAllData: + if (end_stream) { + if (!state.isBufferDataEmpty()) { + // NP: new data = data_buffer_ + data + state.addBufferData(data); + data.move(state.getBufferData()); + } + // check state again since data_buffer may be full and sendLocalReply with 413. + // TODO: better not trigger 413 here. + if (state.state() == FilterState::WaitingAllData) { + done = doDataGo(state, data, end_stream); + } + break; + } + // NP: not break, continue + [[fallthrough]]; + case FilterState::ProcessingHeader: + case FilterState::ProcessingData: + ENVOY_LOG(debug, "golang filter appending data to buffer"); + state.addBufferData(data); + break; + default: + ENVOY_LOG(error, "unexpected state: {}", state.stateStr()); + // TODO: terminate stream? + break; + } + + ENVOY_LOG(debug, "golang filter doData, return: {}", done); + + return done; +} + +bool Filter::doTrailerGo(ProcessorState& state, Http::HeaderMap& trailers) { + ENVOY_LOG(debug, "golang filter passing trailers to golang, state: {}, phase: {}", + state.stateStr(), state.phaseStr()); + + state.processTrailer(); + + ASSERT(req_ != nullptr); + req_->phase = static_cast(state.phase()); + auto status = + dynamic_lib_->envoyGoFilterOnHttpHeader(req_, 1, trailers.size(), trailers.byteSize()); + + return state.handleTrailerGolangStatus(static_cast(status)); +} + +bool Filter::doTrailer(ProcessorState& state, Http::HeaderMap& trailers) { + ENVOY_LOG(debug, "golang filter doTrailer, state: {}, phase: {}", state.stateStr(), + state.phaseStr()); + + ASSERT(!state.getEndStream() && !state.isProcessingEndStream()); + + { + Thread::LockGuard lock(mutex_); + trailers_ = &trailers; + } + + bool done = false; + switch (state.state()) { + case FilterState::WaitingTrailer: + done = doTrailerGo(state, trailers); + break; + case FilterState::WaitingData: + done = doTrailerGo(state, trailers); + break; + case FilterState::WaitingAllData: + ENVOY_LOG(debug, "golang filter data buffer is empty: {}", state.isBufferDataEmpty()); + // do data first + if (!state.isBufferDataEmpty()) { + done = doDataGo(state, state.getBufferData(), false); + } + // NP: can not use done as condition here, since done will be false + // maybe we can remove the done variable totally? by using state_ only? + // continue trailers + if (state.state() == FilterState::WaitingTrailer) { + done = doTrailerGo(state, trailers); + } + break; + case FilterState::ProcessingHeader: + case FilterState::ProcessingData: + // do nothing, wait previous task + break; + default: + ENVOY_LOG(error, "unexpected state: {}", state.stateStr()); + // TODO: terminate stream? + break; + } + + ENVOY_LOG(debug, "golang filter doTrailer, return: {}", done); + + return done; +} + +/*** APIs for go call C ***/ + +void Filter::continueEncodeLocalReply(ProcessorState& state) { + ENVOY_LOG(debug, + "golang filter continue encodeHeader(local reply from other filters) after return from " + "go, current state: {}, phase: {}", + state.stateStr(), state.phaseStr()); + + ENVOY_LOG(debug, "golang filter drain do data buffer before continueEncodeLocalReply"); + state.doDataList.clearAll(); + + local_reply_waiting_go_ = false; + // should use encoding_state_ now + enter_encoding_ = true; + + auto header_end_stream = encoding_state_.getEndStream(); + if (local_trailers_ != nullptr) { + Thread::LockGuard lock(mutex_); + trailers_ = local_trailers_; + header_end_stream = false; + } + if (!encoding_state_.isBufferDataEmpty()) { + header_end_stream = false; + } + // NP: we not overwrite state end_stream in doHeadersGo + encoding_state_.processHeader(header_end_stream); + auto status = doHeadersGo(encoding_state_, *local_headers_, header_end_stream); + continueStatusInternal(status); +} + +void Filter::continueStatusInternal(GolangStatus status) { + ProcessorState& state = getProcessorState(); + ASSERT(state.isThreadSafe()); + auto saved_state = state.state(); + + if (local_reply_waiting_go_) { + ENVOY_LOG(debug, + "other filter already trigger sendLocalReply, ignoring the continue status: {}, " + "state: {}, phase: {}", + int(status), state.stateStr(), state.phaseStr()); + + continueEncodeLocalReply(state); + return; + } + + auto done = state.handleGolangStatus(status); + if (done) { + switch (saved_state) { + case FilterState::ProcessingHeader: + // NP: should process data first filter seen the stream is end but go doesn't, + // otherwise, the next filter will continue with end_stream = true. + + // NP: it is safe to continueDoData after continueProcessing + // that means injectDecodedDataToFilterChain after continueDecoding while stream is not end + if (state.isProcessingEndStream() || !state.isStreamEnd()) { + state.continueProcessing(); + } + break; + + case FilterState::ProcessingData: + state.continueDoData(); + break; + + case FilterState::ProcessingTrailer: + state.continueDoData(); + state.continueProcessing(); + break; + + default: + ASSERT(0, "unexpected state"); + } + } + + // TODO: state should also grow in this case + // state == WaitingData && bufferData is empty && seen trailers + + auto current_state = state.state(); + if ((current_state == FilterState::WaitingData && + (!state.isBufferDataEmpty() || state.getEndStream())) || + (current_state == FilterState::WaitingAllData && state.isStreamEnd())) { + auto done = doDataGo(state, state.getBufferData(), state.getEndStream()); + if (done) { + state.continueDoData(); + } else { + // do not process trailers when data is not finished + return; + } + } + + Thread::ReleasableLockGuard lock(mutex_); + if (state.state() == FilterState::WaitingTrailer && trailers_ != nullptr) { + auto trailers = trailers_; + lock.release(); + auto done = doTrailerGo(state, *trailers); + if (done) { + state.continueProcessing(); + } + } +} + +void Filter::sendLocalReplyInternal( + Http::Code response_code, absl::string_view body_text, + std::function modify_headers, + Grpc::Status::GrpcStatus grpc_status, absl::string_view details) { + ENVOY_LOG(debug, "sendLocalReply Internal, response code: {}", int(response_code)); + + ProcessorState& state = getProcessorState(); + + if (local_reply_waiting_go_) { + ENVOY_LOG(debug, + "other filter already invoked sendLocalReply or encodeHeaders, ignoring the local " + "reply from go, code: {}, body: {}, details: {}", + int(response_code), body_text, details); + + continueEncodeLocalReply(state); + return; + } + + ENVOY_LOG(debug, "golang filter drain do data buffer before sendLocalReply"); + state.doDataList.clearAll(); + + // drain buffer data if it's not empty, before sendLocalReply + state.drainBufferData(); + + state.sendLocalReply(response_code, body_text, modify_headers, grpc_status, details); +} + +void Filter::sendLocalReply(Http::Code response_code, absl::string_view body_text, + std::function modify_headers, + Grpc::Status::GrpcStatus grpc_status, absl::string_view details) { + ENVOY_LOG(debug, "sendLocalReply, response code: {}", int(response_code)); + + auto& state = getProcessorState(); + auto weak_ptr = weak_from_this(); + state.getDispatcher().post( + [this, &state, weak_ptr, response_code, body_text, modify_headers, grpc_status, details] { + ASSERT(state.isThreadSafe()); + // TODO: do not need lock here, since it's the work thread now. + Thread::ReleasableLockGuard lock(mutex_); + if (!weak_ptr.expired() && !has_destroyed_) { + lock.release(); + sendLocalReplyInternal(response_code, body_text, modify_headers, grpc_status, details); + } else { + ENVOY_LOG(debug, "golang filter has gone or destroyed in sendLocalReply"); + } + }); +}; + +void Filter::continueStatus(GolangStatus status) { + // TODO: skip post event to dispatcher, and return continue in the caller, + // when it's invoked in the current envoy thread, for better performance & latency. + auto& state = getProcessorState(); + ENVOY_LOG(debug, "golang filter continue from Go, status: {}, state: {}, phase: {}", int(status), + state.stateStr(), state.phaseStr()); + + auto weak_ptr = weak_from_this(); + state.getDispatcher().post([this, &state, weak_ptr, status] { + ASSERT(state.isThreadSafe()); + // TODO: do not need lock here, since it's the work thread now. + Thread::ReleasableLockGuard lock(mutex_); + if (!weak_ptr.expired() && !has_destroyed_) { + lock.release(); + continueStatusInternal(status); + } else { + ENVOY_LOG(debug, "golang filter has gone or destroyed in continueStatus event"); + } + }); +} + +absl::optional Filter::getHeader(absl::string_view key) { + Thread::LockGuard lock(mutex_); + if (has_destroyed_) { + ENVOY_LOG(debug, "golang filter has been destroyed"); + return ""; + } + auto& state = getProcessorState(); + auto m = state.isProcessingHeader() ? headers_ : trailers_; + auto result = m->get(Http::LowerCaseString(key)); + + if (result.empty()) { + return absl::nullopt; + } + return result[0]->value().getStringView(); +} + +void copyHeaderMapToGo(Http::HeaderMap& m, GoString* go_strs, char* go_buf) { + auto i = 0; + m.iterate([&i, &go_strs, &go_buf](const Http::HeaderEntry& header) -> Http::HeaderMap::Iterate { + auto key = std::string(header.key().getStringView()); + auto value = std::string(header.value().getStringView()); + + auto len = key.length(); + // go_strs is the heap memory of go, and the length is twice the number of headers. So range it + // is safe. + go_strs[i].n = len; + go_strs[i].p = go_buf; + // go_buf is the heap memory of go, and the length is the total length of all keys and values in + // the header. So use memcpy is safe. + memcpy(go_buf, key.data(), len); // NOLINT(safe-memcpy) + go_buf += len; + i++; + + len = value.length(); + go_strs[i].n = len; + go_strs[i].p = go_buf; + memcpy(go_buf, value.data(), len); // NOLINT(safe-memcpy) + go_buf += len; + i++; + return Http::HeaderMap::Iterate::Continue; + }); +} + +void Filter::copyHeaders(GoString* go_strs, char* go_buf) { + Thread::LockGuard lock(mutex_); + if (has_destroyed_) { + ENVOY_LOG(debug, "golang filter has been destroyed"); + return; + } + ASSERT(headers_ != nullptr, "headers is empty, may already continue to next filter"); + copyHeaderMapToGo(*headers_, go_strs, go_buf); +} + +void Filter::setHeader(absl::string_view key, absl::string_view value) { + Thread::LockGuard lock(mutex_); + if (has_destroyed_) { + ENVOY_LOG(debug, "golang filter has been destroyed"); + return; + } + ASSERT(headers_ != nullptr, "headers is empty, may already continue to next filter"); + headers_->setCopy(Http::LowerCaseString(key), value); + onHeadersModified(); +} + +void Filter::removeHeader(absl::string_view key) { + Thread::LockGuard lock(mutex_); + if (has_destroyed_) { + ENVOY_LOG(debug, "golang filter has been destroyed"); + return; + } + ASSERT(headers_ != nullptr, "headers is empty, may already continue to next filter"); + headers_->remove(Http::LowerCaseString(key)); + onHeadersModified(); +} + +void Filter::copyBuffer(Buffer::Instance* buffer, char* data) { + Thread::LockGuard lock(mutex_); + if (has_destroyed_) { + ENVOY_LOG(debug, "golang filter has been destroyed"); + return; + } + for (const Buffer::RawSlice& slice : buffer->getRawSlices()) { + // data is the heap memory of go, and the length is the total length of buffer. So use memcpy is + // safe. + memcpy(data, static_cast(slice.mem_), slice.len_); // NOLINT(safe-memcpy) + data += slice.len_; + } +} + +void Filter::setBufferHelper(Buffer::Instance* buffer, absl::string_view& value, + bufferAction action) { + Thread::LockGuard lock(mutex_); + if (has_destroyed_) { + ENVOY_LOG(debug, "golang filter has been destroyed"); + return; + } + if (action == bufferAction::Set) { + buffer->drain(buffer->length()); + } else if (action == bufferAction::Prepend) { + buffer->prepend(value); + return; + } + buffer->add(value); +} + +void Filter::copyTrailers(GoString* go_strs, char* go_buf) { + Thread::LockGuard lock(mutex_); + if (has_destroyed_) { + ENVOY_LOG(debug, "golang filter has been destroyed"); + return; + } + ASSERT(trailers_ != nullptr, "trailers is empty"); + copyHeaderMapToGo(*trailers_, go_strs, go_buf); +} + +void Filter::setTrailer(absl::string_view key, absl::string_view value) { + Thread::LockGuard lock(mutex_); + if (has_destroyed_) { + ENVOY_LOG(debug, "golang filter has been destroyed"); + return; + } + ASSERT(trailers_ != nullptr, "trailers is empty"); + trailers_->setCopy(Http::LowerCaseString(key), value); +} + +void Filter::getStringValue(int id, GoString* value_str) { + Thread::LockGuard lock(mutex_); + if (has_destroyed_) { + ENVOY_LOG(debug, "golang filter has been destroyed"); + return; + } + auto& state = getProcessorState(); + switch (static_cast(id)) { + case StringValue::RouteName: + // string will copy to req->strValue, but not deep copy + req_->strValue = state.getRouteName(); + break; + default: + ASSERT(false, "invalid string value id"); + } + + value_str->p = req_->strValue.data(); + value_str->n = req_->strValue.length(); +} + +/* ConfigId */ + +uint64_t Filter::getMergedConfigId(ProcessorState& state) { + Http::StreamFilterCallbacks* callbacks = state.getFilterCallbacks(); + + // get all of the per route config + std::list route_config_list; + callbacks->traversePerFilterConfig( + [&route_config_list](const Router::RouteSpecificFilterConfig& cfg) { + route_config_list.push_back(dynamic_cast(&cfg)); + }); + + ENVOY_LOG(debug, "golang filter route config list length: {}.", route_config_list.size()); + + auto id = config_->getConfigId(); + for (auto it : route_config_list) { + auto route_config = *it; + id = route_config.getPluginConfigId(id, config_->pluginName(), config_->soId()); + } + + return id; +} + +/*** FilterConfig ***/ + +FilterConfig::FilterConfig( + const envoy::extensions::filters::http::golang::v3alpha::Config& proto_config) + : plugin_name_(proto_config.plugin_name()), so_id_(proto_config.library_id()), + so_path_(proto_config.library_path()), plugin_config_(proto_config.plugin_config()) { + ENVOY_LOG(debug, "initilizing golang filter config"); + // NP: dso may not loaded yet, can not invoke envoyGoFilterNewHttpPluginConfig yet. +}; + +uint64_t FilterConfig::getConfigId() { + if (config_id_ != 0) { + return config_id_; + } + auto dlib = Dso::DsoInstanceManager::getDsoInstanceByID(so_id_); + ASSERT(dlib != nullptr, "load at the config parse phase, so it should not be null"); + + std::string str; + ASSERT(plugin_config_.SerializeToString(&str)); + auto ptr = reinterpret_cast(str.data()); + auto len = str.length(); + config_id_ = dlib->envoyGoFilterNewHttpPluginConfig(ptr, len); + ASSERT(config_id_, "config id is always grows"); + + return config_id_; +} + +FilterConfigPerRoute::FilterConfigPerRoute( + const envoy::extensions::filters::http::golang::v3alpha::ConfigsPerRoute& config, + Server::Configuration::ServerFactoryContext&) { + // NP: dso may not loaded yet, can not invoke envoyGoFilterNewHttpPluginConfig yet. + ENVOY_LOG(debug, "initilizing per route golang filter config"); + + for (const auto& it : config.plugins_config()) { + auto plugin_name = it.first; + auto route_plugin = it.second; + RoutePluginConfigPtr conf(new RoutePluginConfig(route_plugin)); + ENVOY_LOG(debug, "per route golang filter config, type_url: {}", + route_plugin.config().type_url()); + plugins_config_.insert({plugin_name, std::move(conf)}); + } +} + +uint64_t FilterConfigPerRoute::getPluginConfigId(uint64_t parent_id, std::string plugin_name, + std::string so_id) const { + auto it = plugins_config_.find(plugin_name); + if (it != plugins_config_.end()) { + return it->second->getMergedConfigId(parent_id, so_id); + } + ENVOY_LOG(debug, "golang filter not found plugin config: {}", plugin_name); + // not found + return parent_id; +} + +uint64_t RoutePluginConfig::getMergedConfigId(uint64_t parent_id, std::string so_id) { + if (merged_config_id_ > 0) { + return merged_config_id_; + } + + auto dlib = Dso::DsoInstanceManager::getDsoInstanceByID(so_id); + ASSERT(dlib != nullptr, "load at the config parse phase, so it should not be null"); + + if (config_id_ == 0) { + std::string str; + ASSERT(plugin_config_.SerializeToString(&str)); + auto ptr = reinterpret_cast(str.data()); + auto len = str.length(); + config_id_ = dlib->envoyGoFilterNewHttpPluginConfig(ptr, len); + ASSERT(config_id_, "config id is always grows"); + ENVOY_LOG(debug, "golang filter new plugin config, id: {}", config_id_); + } + + merged_config_id_ = dlib->envoyGoFilterMergeHttpPluginConfig(parent_id, config_id_); + ASSERT(merged_config_id_, "config id is always grows"); + ENVOY_LOG(debug, "golang filter merge plugin config, from {} + {} to {}", parent_id, config_id_, + merged_config_id_); + return merged_config_id_; +}; + +/* ProcessorState */ +ProcessorState& Filter::getProcessorState() { + return enter_encoding_ ? dynamic_cast(encoding_state_) + : dynamic_cast(decoding_state_); +}; + +} // namespace Golang +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/golang/filters/http/source/golang_filter.h b/contrib/golang/filters/http/source/golang_filter.h new file mode 100644 index 0000000000000..2c59f38a3aa19 --- /dev/null +++ b/contrib/golang/filters/http/source/golang_filter.h @@ -0,0 +1,225 @@ +#pragma once + +#include + +#include "envoy/access_log/access_log.h" +#include "envoy/http/filter.h" +#include "envoy/upstream/cluster_manager.h" + +#include "source/common/buffer/watermark_buffer.h" +#include "source/common/common/linked_object.h" +#include "source/common/common/thread.h" +#include "source/common/grpc/context_impl.h" +#include "source/common/http/utility.h" + +#include "contrib/envoy/extensions/filters/http/golang/v3alpha/golang.pb.h" +#include "contrib/golang/filters/http/source/common/dso/dso.h" +#include "contrib/golang/filters/http/source/processor_state.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Golang { + +/** + * Configuration for the HTTP golang extension filter. + */ +class FilterConfig : Logger::Loggable { +public: + FilterConfig(const envoy::extensions::filters::http::golang::v3alpha::Config& proto_config); + // TODO: delete config in Go + virtual ~FilterConfig() = default; + + const std::string& soId() const { return so_id_; } + const std::string& soPath() const { return so_path_; } + const std::string& pluginName() const { return plugin_name_; } + uint64_t getConfigId(); + +private: + const std::string plugin_name_; + const std::string so_id_; + const std::string so_path_; + const ProtobufWkt::Any plugin_config_; + uint64_t config_id_{0}; +}; + +using FilterConfigSharedPtr = std::shared_ptr; + +class RoutePluginConfig : Logger::Loggable { +public: + RoutePluginConfig(const envoy::extensions::filters::http::golang::v3alpha::RouterPlugin& config) + : plugin_config_(config.config()) { + ENVOY_LOG(debug, "initilizing golang filter route plugin config, type_url: {}", + config.config().type_url()); + }; + // TODO: delete plugin config in Go + ~RoutePluginConfig() = default; + uint64_t getMergedConfigId(uint64_t parent_id, std::string so_id); + +private: + const ProtobufWkt::Any plugin_config_; + uint64_t config_id_{0}; + uint64_t merged_config_id_{0}; +}; + +using RoutePluginConfigPtr = std::shared_ptr; + +/** + * Route configuration for the filter. + */ +class FilterConfigPerRoute : public Router::RouteSpecificFilterConfig, + Logger::Loggable { +public: + FilterConfigPerRoute(const envoy::extensions::filters::http::golang::v3alpha::ConfigsPerRoute&, + Server::Configuration::ServerFactoryContext&); + uint64_t getPluginConfigId(uint64_t parent_id, std::string plugin_name, std::string so_id) const; + + ~FilterConfigPerRoute() override { plugins_config_.clear(); } + +private: + std::map plugins_config_; +}; + +enum class DestroyReason { + Normal, + Terminate, +}; + +enum class StringValue { + RouteName = 1, +}; + +struct httpRequestInternal; + +/** + * See docs/configuration/http_filters/golang_extension_filter.rst + */ +class Filter : public Http::StreamFilter, + public std::enable_shared_from_this, + Logger::Loggable, + public AccessLog::Instance { +public: + explicit Filter(FilterConfigSharedPtr config, Dso::DsoInstancePtr dynamic_lib) + : config_(config), dynamic_lib_(dynamic_lib), decoding_state_(*this), encoding_state_(*this) { + } + + // Http::StreamFilterBase + void onDestroy() ABSL_LOCKS_EXCLUDED(mutex_) override; + Http::LocalErrorStatus onLocalReply(const LocalReplyData&) override; + + // Http::StreamDecoderFilter + Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers, + bool end_stream) override; + Http::FilterDataStatus decodeData(Buffer::Instance&, bool) override; + Http::FilterTrailersStatus decodeTrailers(Http::RequestTrailerMap&) override; + void setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callbacks) override { + decoding_state_.setDecoderFilterCallbacks(callbacks); + } + + // Http::StreamEncoderFilter + Http::Filter1xxHeadersStatus encode1xxHeaders(Http::ResponseHeaderMap&) override { + return Http::Filter1xxHeadersStatus::Continue; + } + Http::FilterHeadersStatus encodeHeaders(Http::ResponseHeaderMap& headers, + bool end_stream) override; + Http::FilterDataStatus encodeData(Buffer::Instance& data, bool end_stream) override; + Http::FilterTrailersStatus encodeTrailers(Http::ResponseTrailerMap& trailers) override; + Http::FilterMetadataStatus encodeMetadata(Http::MetadataMap&) override { + return Http::FilterMetadataStatus::Continue; + } + + void setEncoderFilterCallbacks(Http::StreamEncoderFilterCallbacks& callbacks) override { + encoding_state_.setEncoderFilterCallbacks(callbacks); + } + + // AccessLog::Instance + void log(const Http::RequestHeaderMap* request_headers, + const Http::ResponseHeaderMap* response_headers, + const Http::ResponseTrailerMap* response_trailers, + const StreamInfo::StreamInfo& stream_info) override; + + void onStreamComplete() override {} + + void continueStatus(GolangStatus status); + + void sendLocalReply(Http::Code response_code, absl::string_view body_text, + std::function modify_headers, + Grpc::Status::GrpcStatus grpc_status, absl::string_view details); + + absl::optional getHeader(absl::string_view key); + void copyHeaders(GoString* go_strs, char* go_buf); + void setHeader(absl::string_view key, absl::string_view value); + void removeHeader(absl::string_view key); + void copyBuffer(Buffer::Instance* buffer, char* data); + void setBufferHelper(Buffer::Instance* buffer, absl::string_view& value, bufferAction action); + void copyTrailers(GoString* go_strs, char* go_buf); + void setTrailer(absl::string_view key, absl::string_view value); + void getStringValue(int id, GoString* value_str); + +private: + ProcessorState& getProcessorState(); + + bool doHeaders(ProcessorState& state, Http::RequestOrResponseHeaderMap& headers, bool end_stream); + GolangStatus doHeadersGo(ProcessorState& state, Http::RequestOrResponseHeaderMap& headers, + bool end_stream); + bool doData(ProcessorState& state, Buffer::Instance&, bool); + bool doDataGo(ProcessorState& state, Buffer::Instance& data, bool end_stream); + bool doTrailer(ProcessorState& state, Http::HeaderMap& trailers); + bool doTrailerGo(ProcessorState& state, Http::HeaderMap& trailers); + + uint64_t getMergedConfigId(ProcessorState& state); + + void continueEncodeLocalReply(ProcessorState& state); + void continueStatusInternal(GolangStatus status); + void continueData(ProcessorState& state); + + void onHeadersModified(); + + void sendLocalReplyInternal(Http::Code response_code, absl::string_view body_text, + std::function modify_headers, + Grpc::Status::GrpcStatus grpc_status, absl::string_view details); + + const FilterConfigSharedPtr config_; + Dso::DsoInstancePtr dynamic_lib_; + + Http::RequestOrResponseHeaderMap* headers_ ABSL_GUARDED_BY(mutex_){nullptr}; + Http::HeaderMap* trailers_ ABSL_GUARDED_BY(mutex_){nullptr}; + + // save temp values from local reply + Http::RequestOrResponseHeaderMap* local_headers_{nullptr}; + Http::HeaderMap* local_trailers_{nullptr}; + + // The state of the filter on both the encoding and decoding side. + DecodingProcessorState decoding_state_; + EncodingProcessorState encoding_state_; + + httpRequestInternal* req_{nullptr}; + + // lock for has_destroyed_ and the functions get/set/copy/remove/etc that operate on the + // headers_/trailers_/etc, to avoid race between envoy c thread and go thread (when calling back + // from go). it should also be okay without this lock in most cases, just for extreme case. + Thread::MutexBasicLockable mutex_{}; + bool has_destroyed_ ABSL_GUARDED_BY(mutex_){false}; + + // other filter trigger sendLocalReply during go processing in async. + // will wait go return before continue. + // this variable is read/write in safe thread, do no need lock. + bool local_reply_waiting_go_{false}; + + // the filter enter encoding phase + bool enter_encoding_{false}; +}; + +// Go code only touch the fields in httpRequest +struct httpRequestInternal : httpRequest { + std::weak_ptr filter_; + // anchor a string temporarily, make sure it won't be freed before copied to Go. + std::string strValue; + httpRequestInternal(std::weak_ptr f) { filter_ = f; } + std::weak_ptr weakFilter() { return filter_; } +}; + +} // namespace Golang +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/golang/filters/http/source/processor_state.cc b/contrib/golang/filters/http/source/processor_state.cc new file mode 100644 index 0000000000000..a02b881a453f2 --- /dev/null +++ b/contrib/golang/filters/http/source/processor_state.cc @@ -0,0 +1,371 @@ +#include "contrib/golang/filters/http/source/processor_state.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/protobuf/utility.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Golang { + +Buffer::Instance& BufferList::push(Buffer::Instance& data) { + bytes_ += data.length(); + + auto ptr = std::make_unique(); + Buffer::Instance& buffer = *ptr.get(); + buffer.move(data); + queue_.push_back(std::move(ptr)); + + return buffer; +} + +void BufferList::moveOut(Buffer::Instance& data) { + for (auto it = queue_.begin(); it != queue_.end(); it = queue_.erase(it)) { + data.move(**it); + } + bytes_ = 0; +}; + +void BufferList::clearLatest() { + auto buffer = std::move(queue_.back()); + bytes_ -= buffer->length(); + queue_.pop_back(); +}; + +void BufferList::clearAll() { + bytes_ = 0; + queue_.clear(); +}; + +// headers_ should set to nullptr when return true. +bool ProcessorState::handleHeaderGolangStatus(const GolangStatus status) { + ENVOY_LOG(debug, "golang filter handle header status, state: {}, phase: {}, status: {}", + stateStr(), phaseStr(), int(status)); + + ASSERT(state_ == FilterState::ProcessingHeader); + bool done = false; + + switch (status) { + case GolangStatus::LocalReply: + // already invoked sendLocalReply, do nothing + break; + + case GolangStatus::Running: + // do nothing, go side turn to async mode + break; + + case GolangStatus::Continue: + if (do_end_stream_) { + state_ = FilterState::Done; + } else { + state_ = FilterState::WaitingData; + } + done = true; + break; + + case GolangStatus::StopAndBuffer: + state_ = FilterState::WaitingAllData; + break; + + case GolangStatus::StopAndBufferWatermark: + state_ = FilterState::WaitingData; + break; + + default: + ENVOY_LOG(error, "unexpected status: {}", int(status)); + break; + } + + ENVOY_LOG(debug, "golang filter after handle header status, state: {}, phase: {}, status: {}", + stateStr(), phaseStr(), int(status)); + + return done; +}; + +bool ProcessorState::handleDataGolangStatus(const GolangStatus status) { + ENVOY_LOG(debug, "golang filter handle data status, state: {}, phase: {}, status: {}", stateStr(), + phaseStr(), int(status)); + + ASSERT(state_ == FilterState::ProcessingData); + + bool done = false; + + switch (status) { + case GolangStatus::LocalReply: + // already invoked sendLocalReply, do nothing + // return directly to skip phase grow by checking trailers + return false; + + case GolangStatus::Running: + // do nothing, go side turn to async mode + // return directly to skip phase grow by checking trailers + return false; + + case GolangStatus::Continue: + if (do_end_stream_) { + state_ = FilterState::Done; + } else { + state_ = FilterState::WaitingData; + } + done = true; + break; + + case GolangStatus::StopAndBuffer: + if (do_end_stream_) { + ENVOY_LOG(error, "want more data while stream is end"); + // TODO: terminate the stream? + } + state_ = FilterState::WaitingAllData; + break; + + case GolangStatus::StopAndBufferWatermark: + if (do_end_stream_) { + ENVOY_LOG(error, "want more data while stream is end"); + // TODO: terminate the stream? + } + state_ = FilterState::WaitingData; + break; + + case GolangStatus::StopNoBuffer: + if (do_end_stream_) { + ENVOY_LOG(error, "want more data while stream is end"); + // TODO: terminate the stream? + } + doDataList.clearLatest(); + state_ = FilterState::WaitingData; + break; + + default: + // TODO: terminate the stream? + ENVOY_LOG(error, "unexpected status: {}", int(status)); + break; + } + + // see trailers and no buffered data + if (seen_trailers_ && isBufferDataEmpty()) { + ENVOY_LOG(error, "see trailers and buffer is empty"); + state_ = FilterState::WaitingTrailer; + } + + ENVOY_LOG(debug, "golang filter after handle data status, state: {}, phase: {}, status: {}", + int(state_), phaseStr(), int(status)); + + return done; +}; + +// should set trailers_ to nullptr when return true. +// means we should not read/write trailers then, since trailers will pass to next fitler. +bool ProcessorState::handleTrailerGolangStatus(const GolangStatus status) { + ENVOY_LOG(debug, "golang filter handle trailer status, state: {}, phase: {}, status: {}", + stateStr(), phaseStr(), int(status)); + + ASSERT(state_ == FilterState::ProcessingTrailer); + + auto done = false; + + switch (status) { + case GolangStatus::LocalReply: + // already invoked sendLocalReply, do nothing + break; + + case GolangStatus::Running: + // do nothing, go side turn to async mode + break; + + case GolangStatus::Continue: + state_ = FilterState::Done; + done = true; + break; + + default: + // TODO: terminate the stream? + ENVOY_LOG(error, "unexpected status: {}", int(status)); + break; + } + + ENVOY_LOG(debug, "golang filter after handle trailer status, state: {}, phase: {}, status: {}", + stateStr(), phaseStr(), int(status)); + + return done; +}; + +// must in envoy thread. +bool ProcessorState::handleGolangStatus(GolangStatus status) { + ASSERT(isThreadSafe()); + ASSERT(isProcessingInGo(), "unexpected state"); + + ENVOY_LOG(debug, + "before handle golang status, status: {}, state: {}, phase: {}, " + "do_end_stream_: {}", + int(status), stateStr(), phaseStr(), do_end_stream_); + + bool done = false; + switch (state_) { + case FilterState::ProcessingHeader: + done = handleHeaderGolangStatus(status); + break; + + case FilterState::ProcessingData: + done = handleDataGolangStatus(status); + break; + + case FilterState::ProcessingTrailer: + done = handleTrailerGolangStatus(status); + break; + + default: + ASSERT(0, "unexpected state"); + } + + ENVOY_LOG(debug, + "after handle golang status, status: {}, state: {}, phase: {}, " + "do_end_stream_: {}", + int(status), stateStr(), phaseStr(), do_end_stream_); + + return done; +} + +void ProcessorState::drainBufferData() { + if (data_buffer_ != nullptr) { + auto len = data_buffer_->length(); + if (len > 0) { + ENVOY_LOG(debug, "golang filter drain buffer data"); + data_buffer_->drain(len); + } + } +} + +std::string ProcessorState::stateStr() { + switch (state_) { + case FilterState::WaitingHeader: + return "WaitingHeader"; + case FilterState::ProcessingHeader: + return "ProcessingHeader"; + case FilterState::WaitingData: + return "WaitingData"; + case FilterState::WaitingAllData: + return "WaitingAllData"; + case FilterState::ProcessingData: + return "ProcessingData"; + case FilterState::WaitingTrailer: + return "WaitingTrailer"; + case FilterState::ProcessingTrailer: + return "ProcessingTrailer"; + case FilterState::Done: + return "Done"; + default: + return "unknown"; + } +} + +Phase ProcessorState::state2Phase() { + Phase phase; + switch (state_) { + case FilterState::WaitingHeader: + case FilterState::ProcessingHeader: + phase = Phase::DecodeHeader; + break; + case FilterState::WaitingData: + case FilterState::WaitingAllData: + case FilterState::ProcessingData: + phase = Phase::DecodeData; + break; + case FilterState::WaitingTrailer: + case FilterState::ProcessingTrailer: + phase = Phase::DecodeTrailer; + break; + // decode Done state means encode header phase, encode done state means done phase + case FilterState::Done: + phase = Phase::EncodeHeader; + break; + } + return phase; +}; + +std::string ProcessorState::phaseStr() { + switch (phase()) { + case Phase::DecodeHeader: + return "DecodeHeader"; + case Phase::DecodeData: + return "DecodeData"; + case Phase::DecodeTrailer: + return "DecodeTrailer"; + case Phase::EncodeHeader: + return "EncodeHeader"; + case Phase::EncodeData: + return "EncodeData"; + case Phase::EncodeTrailer: + return "EncodeTrailer"; + default: + return "unknown"; + } +} + +void DecodingProcessorState::addBufferData(Buffer::Instance& data) { + if (data_buffer_ == nullptr) { + data_buffer_ = decoder_callbacks_->dispatcher().getWatermarkFactory().createBuffer( + [this]() -> void { + if (watermark_requested_) { + watermark_requested_ = false; + ENVOY_LOG(debug, "golang filter decode data buffer want more data"); + decoder_callbacks_->onDecoderFilterBelowWriteBufferLowWatermark(); + } + }, + [this]() -> void { + if (state_ == FilterState::WaitingAllData) { + // On the request path exceeding buffer limits will result in a 413. + ENVOY_LOG(debug, "golang filter decode data buffer is full, reply with 413"); + decoder_callbacks_->sendLocalReply( + Http::Code::PayloadTooLarge, + Http::CodeUtility::toString(Http::Code::PayloadTooLarge), nullptr, absl::nullopt, + StreamInfo::ResponseCodeDetails::get().RequestPayloadTooLarge); + return; + } + if (!watermark_requested_) { + watermark_requested_ = true; + ENVOY_LOG(debug, "golang filter decode data buffer is full, disable reading"); + decoder_callbacks_->onDecoderFilterAboveWriteBufferHighWatermark(); + } + }, + []() -> void { /* TODO: Handle overflow watermark */ }); + data_buffer_->setWatermarks(decoder_callbacks_->decoderBufferLimit()); + } + data_buffer_->move(data); +} + +void EncodingProcessorState::addBufferData(Buffer::Instance& data) { + if (data_buffer_ == nullptr) { + data_buffer_ = encoder_callbacks_->dispatcher().getWatermarkFactory().createBuffer( + [this]() -> void { + if (watermark_requested_) { + watermark_requested_ = false; + ENVOY_LOG(debug, "golang filter encode data buffer want more data"); + encoder_callbacks_->onEncoderFilterBelowWriteBufferLowWatermark(); + } + }, + [this]() -> void { + if (state_ == FilterState::WaitingAllData) { + // On the request path exceeding buffer limits will result in a 413. + ENVOY_LOG(debug, "golang filter encode data buffer is full, reply with 413"); + encoder_callbacks_->sendLocalReply( + Http::Code::PayloadTooLarge, + Http::CodeUtility::toString(Http::Code::PayloadTooLarge), nullptr, absl::nullopt, + StreamInfo::ResponseCodeDetails::get().RequestPayloadTooLarge); + return; + } + if (!watermark_requested_) { + watermark_requested_ = true; + ENVOY_LOG(debug, "golang filter encode data buffer is full, disable reading"); + encoder_callbacks_->onEncoderFilterAboveWriteBufferHighWatermark(); + } + }, + []() -> void { /* TODO: Handle overflow watermark */ }); + data_buffer_->setWatermarks(encoder_callbacks_->encoderBufferLimit()); + } + data_buffer_->move(data); +} + +} // namespace Golang +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/golang/filters/http/source/processor_state.h b/contrib/golang/filters/http/source/processor_state.h new file mode 100644 index 0000000000000..0cb44d7c2c3a3 --- /dev/null +++ b/contrib/golang/filters/http/source/processor_state.h @@ -0,0 +1,256 @@ +#pragma once + +#include +#include + +#include "envoy/buffer/buffer.h" +#include "envoy/http/filter.h" +#include "envoy/http/header_map.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/logger.h" +#include "source/common/http/codes.h" +#include "source/common/http/utility.h" + +#include "absl/status/status.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Golang { + +class Filter; + +class BufferList : public NonCopyable { +public: + BufferList() = default; + + bool empty() const { return bytes_ == 0; } + // return a new buffer instance, it will existing until moveOut or drain. + Buffer::Instance& push(Buffer::Instance& data); + // move all buffer into data, the list is empty then. + void moveOut(Buffer::Instance& data); + // clear the latest push in buffer. + void clearLatest(); + // clear all. + void clearAll(); + +private: + std::deque queue_; + // The total size of buffers in the list. + uint32_t bytes_{0}; +}; + +// This describes the processor state. +enum class FilterState { + // Waiting header + WaitingHeader, + // Processing header in Go + ProcessingHeader, + // Waiting data + WaitingData, + // Waiting all data + WaitingAllData, + // Processing data in Go + ProcessingData, + // Waiting trailer + WaitingTrailer, + // Processing trailer in Go + ProcessingTrailer, + // All done + Done, +}; + +/* + * request phase + */ +enum class Phase { + DecodeHeader = 1, + DecodeData, + DecodeTrailer, + EncodeHeader, + EncodeData, + EncodeTrailer, + Done, +}; + +/** + * An enum specific for Golang status. + */ +enum class GolangStatus { + Running, + // after called sendLocalReply + LocalReply, + // Continue filter chain iteration. + Continue, + StopAndBuffer, + StopAndBufferWatermark, + StopNoBuffer, +}; + +class ProcessorState : public Logger::Loggable, NonCopyable { +public: + explicit ProcessorState(Filter& filter) : filter_(filter) {} + virtual ~ProcessorState() = default; + + FilterState state() const { return state_; } + std::string stateStr(); + + virtual Phase phase() PURE; + std::string phaseStr(); + + bool isProcessingInGo() { + return state_ == FilterState::ProcessingHeader || state_ == FilterState::ProcessingData || + state_ == FilterState::ProcessingTrailer; + } + bool isProcessingHeader() { return state_ == FilterState::ProcessingHeader; } + Http::StreamFilterCallbacks* getFilterCallbacks() { return filter_callbacks_; }; + + bool isThreadSafe() { return filter_callbacks_->dispatcher().isThreadSafe(); }; + Event::Dispatcher& getDispatcher() { return filter_callbacks_->dispatcher(); } + + /* data buffer */ + // add data to state buffer + virtual void addBufferData(Buffer::Instance& data) PURE; + // get state buffer + Buffer::Instance& getBufferData() { return *data_buffer_.get(); }; + bool isBufferDataEmpty() { return data_buffer_ == nullptr || data_buffer_->length() == 0; }; + void drainBufferData(); + + void setSeenTrailers() { seen_trailers_ = true; } + bool isProcessingEndStream() { return do_end_stream_; } + + virtual void continueProcessing() PURE; + virtual void injectDataToFilterChain(Buffer::Instance& data, bool end_stream) PURE; + void continueDoData() { + if (!end_stream_ && doDataList.empty()) { + return; + } + Buffer::OwnedImpl data_to_write; + doDataList.moveOut(data_to_write); + + injectDataToFilterChain(data_to_write, do_end_stream_); + } + + void processHeader(bool end_stream) { + ASSERT(state_ == FilterState::WaitingHeader); + state_ = FilterState::ProcessingHeader; + do_end_stream_ = end_stream; + } + + void processData(bool end_stream) { + ASSERT(state_ == FilterState::WaitingData || + (state_ == FilterState::WaitingAllData && (end_stream || seen_trailers_))); + state_ = FilterState::ProcessingData; + do_end_stream_ = end_stream; + } + + void processTrailer() { + ASSERT(state_ == FilterState::WaitingTrailer || state_ == FilterState::WaitingData || + state_ == FilterState::WaitingAllData); + state_ = FilterState::ProcessingTrailer; + do_end_stream_ = true; + } + + bool handleHeaderGolangStatus(const GolangStatus status); + bool handleDataGolangStatus(const GolangStatus status); + bool handleTrailerGolangStatus(const GolangStatus status); + bool handleGolangStatus(GolangStatus status); + + virtual void sendLocalReply(Http::Code response_code, absl::string_view body_text, + std::function modify_headers, + Grpc::Status::GrpcStatus grpc_status, absl::string_view details) PURE; + + std::string getRouteName() { return filter_callbacks_->streamInfo().getRouteName(); } + + void setEndStream(bool end_stream) { end_stream_ = end_stream; } + bool getEndStream() { return end_stream_; } + // seen trailers also means stream is end + bool isStreamEnd() { return end_stream_ || seen_trailers_; } + + BufferList doDataList; + +protected: + Phase state2Phase(); + Filter& filter_; + Http::StreamFilterCallbacks* filter_callbacks_{nullptr}; + bool watermark_requested_{false}; + Buffer::InstancePtr data_buffer_{nullptr}; + FilterState state_{FilterState::WaitingHeader}; + bool end_stream_{false}; + bool do_end_stream_{false}; + bool seen_trailers_{false}; +}; + +class DecodingProcessorState : public ProcessorState { +public: + explicit DecodingProcessorState(Filter& filter) : ProcessorState(filter) {} + + void setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callbacks) { + decoder_callbacks_ = &callbacks; + filter_callbacks_ = &callbacks; + } + + void injectDataToFilterChain(Buffer::Instance& data, bool end_stream) override { + decoder_callbacks_->injectDecodedDataToFilterChain(data, end_stream); + } + + Phase phase() override { return state2Phase(); }; + + void addBufferData(Buffer::Instance& data) override; + + void continueProcessing() override { + ENVOY_LOG(debug, "golang filter callback continue, continueDecoding"); + decoder_callbacks_->continueDecoding(); + } + void sendLocalReply(Http::Code response_code, absl::string_view body_text, + std::function modify_headers, + Grpc::Status::GrpcStatus grpc_status, absl::string_view details) override { + // it's safe to reset state_, since it is read/write in safe thread. + ENVOY_LOG(debug, "golang filter phase grow to EncodeHeader and state grow to WaitHeader before " + "sendLocalReply"); + state_ = FilterState::WaitingHeader; + decoder_callbacks_->sendLocalReply(response_code, body_text, modify_headers, grpc_status, + details); + }; + +private: + Http::StreamDecoderFilterCallbacks* decoder_callbacks_{nullptr}; +}; + +class EncodingProcessorState : public ProcessorState { +public: + explicit EncodingProcessorState(Filter& filter) : ProcessorState(filter) {} + + void setEncoderFilterCallbacks(Http::StreamEncoderFilterCallbacks& callbacks) { + encoder_callbacks_ = &callbacks; + filter_callbacks_ = &callbacks; + } + + void injectDataToFilterChain(Buffer::Instance& data, bool end_stream) override { + encoder_callbacks_->injectEncodedDataToFilterChain(data, end_stream); + } + + Phase phase() override { return static_cast(static_cast(state2Phase()) + 3); }; + + void addBufferData(Buffer::Instance& data) override; + + void continueProcessing() override { + ENVOY_LOG(debug, "golang filter callback continue, continueEncoding"); + encoder_callbacks_->continueEncoding(); + } + void sendLocalReply(Http::Code response_code, absl::string_view body_text, + std::function modify_headers, + Grpc::Status::GrpcStatus grpc_status, absl::string_view details) override { + encoder_callbacks_->sendLocalReply(response_code, body_text, modify_headers, grpc_status, + details); + }; + +private: + Http::StreamEncoderFilterCallbacks* encoder_callbacks_{nullptr}; +}; + +} // namespace Golang +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/golang/filters/http/test/BUILD b/contrib/golang/filters/http/test/BUILD new file mode 100644 index 0000000000000..f35b251d80e46 --- /dev/null +++ b/contrib/golang/filters/http/test/BUILD @@ -0,0 +1,65 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + data = [ + "//contrib/golang/filters/http/test/test_data/passthrough:filter.so", + ], + env = {"GODEBUG": "cgocheck=0"}, + deps = [ + "//contrib/golang/filters/http/source:config", + "//test/mocks/server:factory_context_mocks", + "//test/test_common:utility_lib", + ], +) + +envoy_cc_test( + name = "golang_filter_test", + srcs = ["golang_filter_test.cc"], + data = [ + "//contrib/golang/filters/http/test/test_data/passthrough:filter.so", + ], + env = {"GODEBUG": "cgocheck=0"}, + deps = [ + "//contrib/golang/filters/http/source:golang_filter_lib", + "//source/common/stream_info:stream_info_lib", + "//test/mocks/api:api_mocks", + "//test/mocks/http:http_mocks", + "//test/mocks/network:network_mocks", + "//test/mocks/server:factory_context_mocks", + "//test/mocks/ssl:ssl_mocks", + "//test/mocks/thread_local:thread_local_mocks", + "//test/mocks/upstream:cluster_manager_mocks", + "//test/test_common:logging_lib", + "//test/test_common:test_runtime_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + ], +) + +envoy_cc_test( + name = "golang_integration_test", + srcs = ["golang_integration_test.cc"], + data = [ + "//contrib/golang/filters/http/test/test_data/echo:filter.so", + "//contrib/golang/filters/http/test/test_data/passthrough:filter.so", + ], + env = {"GODEBUG": "cgocheck=0"}, + deps = [ + "//contrib/golang/filters/http/source:config", + "//source/exe:main_common_lib", + "//test/config:v2_link_hacks", + "//test/integration:http_integration_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", + ], +) diff --git a/contrib/golang/filters/http/test/common/dso/BUILD b/contrib/golang/filters/http/test/common/dso/BUILD new file mode 100644 index 0000000000000..6678ea81be7d4 --- /dev/null +++ b/contrib/golang/filters/http/test/common/dso/BUILD @@ -0,0 +1,21 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_test( + name = "dso_test", + srcs = ["dso_test.cc"], + data = [ + "//contrib/golang/filters/http/test/common/dso/test_data:simple.so", + ], + deps = [ + "//contrib/golang/filters/http/source/common/dso:dso_lib", + "//test/test_common:utility_lib", + ], +) diff --git a/contrib/golang/filters/http/test/common/dso/dso_test.cc b/contrib/golang/filters/http/test/common/dso/dso_test.cc new file mode 100644 index 0000000000000..223f9b423eec4 --- /dev/null +++ b/contrib/golang/filters/http/test/common/dso/dso_test.cc @@ -0,0 +1,50 @@ +#include + +#include "envoy/registry/registry.h" + +#include "test/test_common/environment.h" +#include "test/test_common/utility.h" + +#include "contrib/golang/filters/http/source/common/dso/dso.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Dso { +namespace { + +std::string genSoPath(std::string name) { + return TestEnvironment::substitute( + "{{ test_rundir }}/contrib/golang/filters/http/test/common/dso/test_data/" + name); +} + +TEST(DsoInstanceTest, SimpleAPI) { + auto path = genSoPath("simple.so"); + DsoInstancePtr dso(new DsoInstance(path)); + EXPECT_EQ(dso->envoyGoFilterNewHttpPluginConfig(0, 0), 100); +} + +TEST(DsoInstanceManagerTest, Pub) { + auto id = "simple.so"; + auto path = genSoPath(id); + + // get before load + auto dso = DsoInstanceManager::getDsoInstanceByID(id); + EXPECT_EQ(dso, nullptr); + + // first time load + auto res = DsoInstanceManager::load(id, path); + EXPECT_EQ(res, true); + + // get after load + dso = DsoInstanceManager::getDsoInstanceByID(id); + EXPECT_NE(dso, nullptr); + + // second time load + res = DsoInstanceManager::load(id, path); + EXPECT_EQ(res, true); +} + +} // namespace +} // namespace Dso +} // namespace Envoy diff --git a/contrib/golang/filters/http/test/common/dso/test_data/BUILD b/contrib/golang/filters/http/test/common/dso/test_data/BUILD new file mode 100644 index 0000000000000..9fe958abff2c7 --- /dev/null +++ b/contrib/golang/filters/http/test/common/dso/test_data/BUILD @@ -0,0 +1,15 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary") + +licenses(["notice"]) # Apache 2 + +go_binary( + name = "simple.so", + srcs = [ + "simple.go", + ], + out = "simple.so", + cgo = True, + importpath = "github.com/envoyproxy/envoy/contrib/golang/filters/http/test/common/dso/test_data", + linkmode = "c-shared", + visibility = ["//visibility:public"], +) diff --git a/contrib/golang/filters/http/test/common/dso/test_data/simple.go b/contrib/golang/filters/http/test/common/dso/test_data/simple.go new file mode 100644 index 0000000000000..a1661ddbbca23 --- /dev/null +++ b/contrib/golang/filters/http/test/common/dso/test_data/simple.go @@ -0,0 +1,39 @@ +package main + +/* +typedef struct { + int foo; +} httpRequest; +*/ +import "C" + +//export envoyGoFilterNewHttpPluginConfig +func envoyGoFilterNewHttpPluginConfig(configPtr uint64, configLen uint64) uint64 { + return 100 +} + +//export envoyGoFilterDestroyHttpPluginConfig +func envoyGoFilterDestroyHttpPluginConfig(id uint64) { +} + +//export envoyGoFilterMergeHttpPluginConfig +func envoyGoFilterMergeHttpPluginConfig(parentId uint64, childId uint64) uint64 { + return 0 +} + +//export envoyGoFilterOnHttpHeader +func envoyGoFilterOnHttpHeader(r *C.httpRequest, endStream, headerNum, headerBytes uint64) uint64 { + return 0 +} + +//export envoyGoFilterOnHttpData +func envoyGoFilterOnHttpData(r *C.httpRequest, endStream, buffer, length uint64) uint64 { + return 0 +} + +//export envoyGoFilterOnHttpDestroy +func envoyGoFilterOnHttpDestroy(r *C.httpRequest, reason uint64) { +} + +func main() { +} diff --git a/contrib/golang/filters/http/test/config_test.cc b/contrib/golang/filters/http/test/config_test.cc new file mode 100644 index 0000000000000..d85792ee35b1c --- /dev/null +++ b/contrib/golang/filters/http/test/config_test.cc @@ -0,0 +1,94 @@ +#include + +#include "envoy/registry/registry.h" + +#include "test/mocks/server/factory_context.h" +#include "test/test_common/environment.h" +#include "test/test_common/utility.h" + +#include "absl/strings/str_format.h" +#include "contrib/golang/filters/http/source/config.h" +#include "contrib/golang/filters/http/source/golang_filter.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Golang { +namespace { + +std::string genSoPath(std::string name) { + return TestEnvironment::substitute( + "{{ test_rundir }}/contrib/golang/filters/http/test/test_data/" + name + "/filter.so"); +} + +TEST(GolangFilterConfigTest, InvalidateEmptyConfig) { + NiceMock context; + EXPECT_THROW_WITH_REGEX( + GolangFilterConfig().createFilterFactoryFromProto( + envoy::extensions::filters::http::golang::v3alpha::Config(), "stats", context), + Envoy::ProtoValidationException, + "ConfigValidationError.LibraryId: value length must be at least 1 characters"); +} + +TEST(GolangFilterConfigTest, GolangFilterWithValidConfig) { + const auto yaml_fmt = R"EOF( + library_id: %s + library_path: %s + plugin_name: xxx + merge_policy: MERGE_VIRTUALHOST_ROUTER_FILTER + plugin_config: + "@type": type.googleapis.com/udpa.type.v1.TypedStruct + type_url: typexx + value: + key: value + int: 10 + )EOF"; + + const std::string PASSTHROUGH{"passthrough"}; + auto yaml_string = absl::StrFormat(yaml_fmt, PASSTHROUGH, genSoPath(PASSTHROUGH)); + envoy::extensions::filters::http::golang::v3alpha::Config proto_config; + TestUtility::loadFromYaml(yaml_string, proto_config); + NiceMock context; + GolangFilterConfig factory; + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context); + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)); + EXPECT_CALL(filter_callback, addAccessLogHandler(_)); + auto plugin_config = proto_config.plugin_config(); + std::string str; + EXPECT_TRUE(plugin_config.SerializeToString(&str)); + cb(filter_callback); +} + +TEST(GolangFilterConfigTest, GolangFilterWithNilPluginConfig) { + const auto yaml_fmt = R"EOF( + library_id: %s + library_path: %s + plugin_name: xxx + )EOF"; + + const std::string PASSTHROUGH{"passthrough"}; + auto yaml_string = absl::StrFormat(yaml_fmt, PASSTHROUGH, genSoPath(PASSTHROUGH)); + envoy::extensions::filters::http::golang::v3alpha::Config proto_config; + TestUtility::loadFromYaml(yaml_string, proto_config); + NiceMock context; + GolangFilterConfig factory; + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "stats", context); + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)); + EXPECT_CALL(filter_callback, addAccessLogHandler(_)); + auto plugin_config = proto_config.plugin_config(); + std::string str; + EXPECT_TRUE(plugin_config.SerializeToString(&str)); + cb(filter_callback); +} + +} // namespace +} // namespace Golang +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/golang/filters/http/test/golang_filter_test.cc b/contrib/golang/filters/http/test/golang_filter_test.cc new file mode 100644 index 0000000000000..0a6255f0bd4c7 --- /dev/null +++ b/contrib/golang/filters/http/test/golang_filter_test.cc @@ -0,0 +1,168 @@ +#include +#include + +#include "envoy/config/core/v3/base.pb.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/http/message_impl.h" +#include "source/common/stream_info/stream_info_impl.h" + +#include "test/common/stats/stat_test_utility.h" +#include "test/mocks/api/mocks.h" +#include "test/mocks/http/mocks.h" +#include "test/mocks/network/mocks.h" +#include "test/mocks/server/factory_context.h" +#include "test/mocks/ssl/mocks.h" +#include "test/mocks/thread_local/mocks.h" +#include "test/mocks/upstream/cluster_manager.h" +#include "test/test_common/environment.h" +#include "test/test_common/logging.h" +#include "test/test_common/printers.h" +#include "test/test_common/test_runtime.h" +#include "test/test_common/utility.h" + +#include "absl/strings/str_format.h" +#include "contrib/golang/filters/http/source/golang_filter.h" +#include "gmock/gmock.h" + +using testing::_; +using testing::AtLeast; +using testing::InSequence; +using testing::Invoke; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Golang { +namespace { + +class TestFilter : public Filter { +public: + using Filter::Filter; +}; + +class GolangHttpFilterTest : public testing::Test { +public: + GolangHttpFilterTest() { + cluster_manager_.initializeThreadLocalClusters({"cluster"}); + + // Avoid strict mock failures for the following calls. We want strict for other calls. + EXPECT_CALL(decoder_callbacks_, addDecodedData(_, _)) + .Times(AtLeast(0)) + .WillRepeatedly(Invoke([this](Buffer::Instance& data, bool) { + if (decoder_callbacks_.buffer_ == nullptr) { + decoder_callbacks_.buffer_ = std::make_unique(); + } + decoder_callbacks_.buffer_->move(data); + })); + + EXPECT_CALL(decoder_callbacks_, activeSpan()).Times(AtLeast(0)); + EXPECT_CALL(decoder_callbacks_, decodingBuffer()).Times(AtLeast(0)); + EXPECT_CALL(decoder_callbacks_, route()).Times(AtLeast(0)); + + EXPECT_CALL(encoder_callbacks_, addEncodedData(_, _)) + .Times(AtLeast(0)) + .WillRepeatedly(Invoke([this](Buffer::Instance& data, bool) { + if (encoder_callbacks_.buffer_ == nullptr) { + encoder_callbacks_.buffer_ = std::make_unique(); + } + encoder_callbacks_.buffer_->move(data); + })); + EXPECT_CALL(encoder_callbacks_, activeSpan()).Times(AtLeast(0)); + EXPECT_CALL(encoder_callbacks_, encodingBuffer()).Times(AtLeast(0)); + EXPECT_CALL(decoder_callbacks_, streamInfo()).Times(testing::AnyNumber()); + } + + ~GolangHttpFilterTest() override { filter_->onDestroy(); } + + void setup(const std::string& lib_id, const std::string& lib_path, + const std::string& plugin_name) { + const auto yaml_fmt = R"EOF( + library_id: %s + library_path: %s + plugin_name: %s + merge_policy: MERGE_VIRTUALHOST_ROUTER_FILTER + plugin_config: + "@type": type.googleapis.com/udpa.type.v1.TypedStruct + type_url: typexx + value: + key: value + int: 10 + )EOF"; + + auto yaml_string = absl::StrFormat(yaml_fmt, lib_id, lib_path, plugin_name); + envoy::extensions::filters::http::golang::v3alpha::Config proto_config; + TestUtility::loadFromYaml(yaml_string, proto_config); + + envoy::extensions::filters::http::golang::v3alpha::ConfigsPerRoute per_route_proto_config; + setupDso(); + setupConfig(proto_config, per_route_proto_config); + setupFilter(lib_id); + } + + std::string genSoPath(std::string name) { + return TestEnvironment::substitute( + "{{ test_rundir }}/contrib/golang/filters/http/test/test_data/" + name + "/filter.so"); + } + + void setupDso() { Dso::DsoInstanceManager::load(PASSTHROUGH, genSoPath(PASSTHROUGH)); } + + void setupConfig( + envoy::extensions::filters::http::golang::v3alpha::Config& proto_config, + envoy::extensions::filters::http::golang::v3alpha::ConfigsPerRoute& per_route_proto_config) { + // Setup filter config for Golang filter. + config_ = std::make_shared(proto_config); + // Setup per route config for Golang filter. + per_route_config_ = + std::make_shared(per_route_proto_config, server_factory_context_); + } + + void setupFilter(const std::string& so_id) { + Event::SimulatedTimeSystem test_time; + test_time.setSystemTime(std::chrono::microseconds(1583879145572237)); + + filter_ = + std::make_unique(config_, Dso::DsoInstanceManager::getDsoInstanceByID(so_id)); + filter_->setDecoderFilterCallbacks(decoder_callbacks_); + filter_->setEncoderFilterCallbacks(encoder_callbacks_); + } + + void setupMetadata(const std::string& yaml) { + TestUtility::loadFromYaml(yaml, metadata_); + ON_CALL(*decoder_callbacks_.route_, metadata()).WillByDefault(testing::ReturnRef(metadata_)); + } + + NiceMock server_factory_context_; + NiceMock tls_; + NiceMock api_; + Upstream::MockClusterManager cluster_manager_; + std::shared_ptr config_; + std::shared_ptr per_route_config_; + std::unique_ptr filter_; + NiceMock decoder_callbacks_; + NiceMock encoder_callbacks_; + envoy::config::core::v3::Metadata metadata_; + std::shared_ptr> ssl_; + NiceMock connection_; + NiceMock stream_info_; + Tracing::MockSpan child_span_; + Stats::TestUtil::TestStore stats_store_; + + const std::string PASSTHROUGH{"passthrough"}; +}; + +// request that is headers only. +TEST_F(GolangHttpFilterTest, ScriptHeadersOnlyRequestHeadersOnly) { + InSequence s; + setup(PASSTHROUGH, genSoPath(PASSTHROUGH), PASSTHROUGH); + + Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, true)); + EXPECT_EQ(0, stats_store_.counter("test.golang.errors").value()); +} + +} // namespace +} // namespace Golang +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/golang/filters/http/test/golang_integration_test.cc b/contrib/golang/filters/http/test/golang_integration_test.cc new file mode 100644 index 0000000000000..c34782efb9931 --- /dev/null +++ b/contrib/golang/filters/http/test/golang_integration_test.cc @@ -0,0 +1,135 @@ +#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" + +#include "test/config/v2_link_hacks.h" +#include "test/integration/http_integration.h" +#include "test/test_common/utility.h" + +#include "contrib/golang/filters/http/source/golang_filter.h" +#include "gtest/gtest.h" + +namespace Envoy { +class GolangIntegrationTest : public testing::TestWithParam, + public HttpIntegrationTest { +public: + GolangIntegrationTest() : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, GetParam()) {} + + void createUpstreams() override { + HttpIntegrationTest::createUpstreams(); + addFakeUpstream(Http::CodecType::HTTP1); + addFakeUpstream(Http::CodecType::HTTP1); + } + + std::string genSoPath(std::string name) { + return TestEnvironment::substitute( + "{{ test_rundir }}/contrib/golang/filters/http/test/test_data/" + name + "/filter.so"); + } + + void initializeConfig(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 + merge_policy: MERGE_VIRTUALHOST_ROUTER_FILTER + plugin_config: + "@type": type.googleapis.com/udpa.type.v1.TypedStruct + type_url: typexx + value: + remove: x-test-header-0 + set: foo +)EOF"; + + auto yaml_string = absl::StrFormat(yaml_fmt, lib_id, lib_path, plugin_name); + config_helper_.prependFilter(yaml_string); + config_helper_.skipPortUsageValidation(); + } + + const std::string ECHO{"echo"}; + const std::string PASSTHROUGH{"passthrough"}; +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, GolangIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +TEST_P(GolangIntegrationTest, Echo) { + initializeConfig(ECHO, genSoPath(ECHO), ECHO); + initialize(); + registerTestServerPorts({"http"}); + + auto path = "/localreply"; + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "POST"}, {":path", path}, {":scheme", "http"}, {":authority", "test.com"}}; + + 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); + + ASSERT_TRUE(response->waitForEndStream()); + + // check status for echo + EXPECT_EQ("403", response->headers().getStatusValue()); + + // check body for echo + auto body = StringUtil::toUpper(absl::StrFormat("forbidden from go, path: %s\r\n", path)); + EXPECT_EQ(body, StringUtil::toUpper(response->body())); + + codec_client_->close(); + + if (fake_upstream_connection_ != nullptr) { + AssertionResult result = fake_upstream_connection_->close(); + RELEASE_ASSERT(result, result.message()); + result = fake_upstream_connection_->waitForDisconnect(); + RELEASE_ASSERT(result, result.message()); + } +} + +TEST_P(GolangIntegrationTest, Passthrough) { + initializeConfig(PASSTHROUGH, genSoPath(PASSTHROUGH), PASSTHROUGH); + initialize(); + registerTestServerPorts({"http"}); + + auto path = "/"; + auto good = "good"; + auto bye = "bye"; + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "POST"}, {":path", path}, {":scheme", "http"}, {":authority", "test.com"}}; + + 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_data1(good); + upstream_request_->encodeData(response_data1, false); + Buffer::OwnedImpl response_data2(bye); + upstream_request_->encodeData(response_data2, true); + + ASSERT_TRUE(response->waitForEndStream()); + + // check status for pasthrough + EXPECT_EQ("200", response->headers().getStatusValue()); + + // check body for pasthrough + auto body = StringUtil::toUpper(absl::StrFormat("%s%s", good, bye)); + EXPECT_EQ(body, StringUtil::toUpper(response->body())); + + codec_client_->close(); + + if (fake_upstream_connection_ != nullptr) { + AssertionResult result = fake_upstream_connection_->close(); + RELEASE_ASSERT(result, result.message()); + result = fake_upstream_connection_->waitForDisconnect(); + RELEASE_ASSERT(result, result.message()); + } +} +} // namespace Envoy diff --git a/contrib/golang/filters/http/test/test_data/echo/BUILD b/contrib/golang/filters/http/test/test_data/echo/BUILD new file mode 100644 index 0000000000000..eff1b5798c1f1 --- /dev/null +++ b/contrib/golang/filters/http/test/test_data/echo/BUILD @@ -0,0 +1,25 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary") + +licenses(["notice"]) # Apache 2 + +go_binary( + name = "filter.so", + srcs = [ + "api.h", + "config.go", + "export.go", + "filter.go", + ], + out = "filter.so", + cgo = True, + importpath = "github.com/envoyproxy/envoy/contrib/golang/filters/http/test/test_data/echo", + linkmode = "c-shared", + visibility = ["//visibility:public"], + deps = [ + "//contrib/golang/filters/http/source/go/pkg/api", + "//contrib/golang/filters/http/source/go/pkg/http", + "//contrib/golang/filters/http/source/go/pkg/utils", + "@com_github_cncf_xds_go//udpa/type/v1:type", + "@org_golang_google_protobuf//types/known/anypb", + ], +) diff --git a/contrib/golang/filters/http/test/test_data/echo/api.h b/contrib/golang/filters/http/test/test_data/echo/api.h new file mode 120000 index 0000000000000..6c9e1bfaf30a5 --- /dev/null +++ b/contrib/golang/filters/http/test/test_data/echo/api.h @@ -0,0 +1 @@ +../../../source/go/pkg/api/api.h \ No newline at end of file diff --git a/contrib/golang/filters/http/test/test_data/echo/config.go b/contrib/golang/filters/http/test/test_data/echo/config.go new file mode 100644 index 0000000000000..933f08bf4f782 --- /dev/null +++ b/contrib/golang/filters/http/test/test_data/echo/config.go @@ -0,0 +1,22 @@ +package main + +import ( + "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/api" + "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http" +) + +const Name = "echo" + +func init() { + http.RegisterHttpFilterConfigFactory(Name, ConfigFactory) +} + +func ConfigFactory(interface{}) api.StreamFilterFactory { + return func(callbacks api.FilterCallbackHandler) api.StreamFilter { + return &filter{ + callbacks: callbacks, + } + } +} + +func main() {} diff --git a/contrib/golang/filters/http/test/test_data/echo/export.go b/contrib/golang/filters/http/test/test_data/echo/export.go new file mode 120000 index 0000000000000..f22fbe9abc396 --- /dev/null +++ b/contrib/golang/filters/http/test/test_data/echo/export.go @@ -0,0 +1 @@ +../../../source/go/export.go \ No newline at end of file diff --git a/contrib/golang/filters/http/test/test_data/echo/filter.go b/contrib/golang/filters/http/test/test_data/echo/filter.go new file mode 100644 index 0000000000000..61134766699a0 --- /dev/null +++ b/contrib/golang/filters/http/test/test_data/echo/filter.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + + "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/api" +) + +type filter struct { + callbacks api.FilterCallbackHandler + path string +} + +func (f *filter) sendLocalReply() api.StatusType { + headers := make(map[string]string) + body := fmt.Sprintf("forbidden from go, path: %s\r\n", f.path) + f.callbacks.SendLocalReply(403, body, headers, -1, "test-from-go") + return api.LocalReply +} + +func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType { + header.Del("x-test-header-1") + f.path, _ = header.Get(":path") + header.Set("rsp-header-from-go", "foo-test") + if f.path == "/localreply" { + return f.sendLocalReply() + } + return api.Continue +} + +func (f *filter) DecodeData(buffer api.BufferInstance, endStream bool) api.StatusType { + return api.Continue +} + +func (f *filter) DecodeTrailers(trailers api.RequestTrailerMap) api.StatusType { + return api.Continue +} + +func (f *filter) EncodeHeaders(header api.ResponseHeaderMap, endStream bool) api.StatusType { + header.Set("Rsp-Header-From-Go", "bar-test") + return api.Continue +} + +func (f *filter) EncodeData(buffer api.BufferInstance, endStream bool) api.StatusType { + return api.Continue +} + +func (f *filter) EncodeTrailers(trailers api.ResponseTrailerMap) api.StatusType { + return api.Continue +} + +func (f *filter) OnDestroy(reason api.DestroyReason) { +} + +func (f *filter) Callbacks() api.FilterCallbacks { + return f.callbacks +} diff --git a/contrib/golang/filters/http/test/test_data/passthrough/BUILD b/contrib/golang/filters/http/test/test_data/passthrough/BUILD new file mode 100644 index 0000000000000..bcc7dc4bd8daf --- /dev/null +++ b/contrib/golang/filters/http/test/test_data/passthrough/BUILD @@ -0,0 +1,24 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary") + +licenses(["notice"]) # Apache 2 + +go_binary( + name = "filter.so", + srcs = [ + "api.h", + "export.go", + "filter.go", + ], + out = "filter.so", + cgo = True, + importpath = "github.com/envoyproxy/envoy/contrib/golang/filters/http/test/test_data/passthrough", + linkmode = "c-shared", + visibility = ["//visibility:public"], + deps = [ + "//contrib/golang/filters/http/source/go/pkg/api", + "//contrib/golang/filters/http/source/go/pkg/http", + "//contrib/golang/filters/http/source/go/pkg/utils", + "@com_github_cncf_xds_go//udpa/type/v1:type", + "@org_golang_google_protobuf//types/known/anypb", + ], +) diff --git a/contrib/golang/filters/http/test/test_data/passthrough/api.h b/contrib/golang/filters/http/test/test_data/passthrough/api.h new file mode 120000 index 0000000000000..6c9e1bfaf30a5 --- /dev/null +++ b/contrib/golang/filters/http/test/test_data/passthrough/api.h @@ -0,0 +1 @@ +../../../source/go/pkg/api/api.h \ No newline at end of file diff --git a/contrib/golang/filters/http/test/test_data/passthrough/export.go b/contrib/golang/filters/http/test/test_data/passthrough/export.go new file mode 120000 index 0000000000000..f22fbe9abc396 --- /dev/null +++ b/contrib/golang/filters/http/test/test_data/passthrough/export.go @@ -0,0 +1 @@ +../../../source/go/export.go \ No newline at end of file diff --git a/contrib/golang/filters/http/test/test_data/passthrough/filter.go b/contrib/golang/filters/http/test/test_data/passthrough/filter.go new file mode 100644 index 0000000000000..40c4dc93f4e63 --- /dev/null +++ b/contrib/golang/filters/http/test/test_data/passthrough/filter.go @@ -0,0 +1,10 @@ +package main + +import "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http" + +func init() { + http.RegisterHttpFilterConfigFactory("", http.PassThroughFactory) +} + +func main() { +} diff --git a/contrib/golang/filters/http/test/test_data/passthrough/go.mod b/contrib/golang/filters/http/test/test_data/passthrough/go.mod new file mode 100644 index 0000000000000..761231be3f6a4 --- /dev/null +++ b/contrib/golang/filters/http/test/test_data/passthrough/go.mod @@ -0,0 +1,7 @@ +module example.com/passthrough + +go 1.18 + +require github.com/envoyproxy/envoy v1.24.0 + +require google.golang.org/protobuf v1.28.1 // indirect diff --git a/contrib/golang/filters/http/test/test_data/passthrough/go.sum b/contrib/golang/filters/http/test/test_data/passthrough/go.sum new file mode 100644 index 0000000000000..28803a9185fa3 --- /dev/null +++ b/contrib/golang/filters/http/test/test_data/passthrough/go.sum @@ -0,0 +1,10 @@ +github.com/envoyproxy/envoy v1.24.0 h1:MjBA9/3ZWuoTf1JaQU8imn5GIHhmF54klBPVfcmTF4U= +github.com/envoyproxy/envoy v1.24.0/go.mod h1:h4LHzNYz/dk4kj2No4DgPaH3a087hHIKz/3DGL6m1v8= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/docs/root/configuration/http/http_filters/golang_filter.rst b/docs/root/configuration/http/http_filters/golang_filter.rst new file mode 100644 index 0000000000000..b5c3b6c38bdce --- /dev/null +++ b/docs/root/configuration/http/http_filters/golang_filter.rst @@ -0,0 +1,39 @@ +.. _config_http_filters_golang: + +Golang +====== + +Overview +-------- + +The HTTP Golang filter allows `Golang `_ to be run during both the request +and response flows and makes it easier to extend Envoy. See the `Envoy's GoLang extension proposal documentation +`_ for more details. + + +Configuration +------------- + +* This filter should be configured with the type URL ``type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config``. +* :ref:`v3 API reference ` + +A simple example of configuring Golang HTTP filter that default `echo` go plugin as follow: + +.. code-block:: yaml + + name: envoy.filters.http.golang + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config + library_id: echo-id + library_path: "contrib/golang/filters/http/test/test_data/echo/filter.so" + plugin_name: echo + +.. attention:: + + The go plugin dynamic library built needs to be consistent with the envoy version of glibc. + +Complete example +---------------- + +A complete example using Docker is available in :repo:`/contrib/golang/filters/http/test/test_data/echo` and run +``bazel build //contrib/golang/filters/http/test/test_data/echo:filter.so``. diff --git a/docs/root/configuration/http/http_filters/http_filters.rst b/docs/root/configuration/http/http_filters/http_filters.rst index ac7a21256c063..e6931acbe02b5 100644 --- a/docs/root/configuration/http/http_filters/http_filters.rst +++ b/docs/root/configuration/http/http_filters/http_filters.rst @@ -27,6 +27,7 @@ HTTP filters fault_filter file_system_buffer_filter gcp_authn_filter + golang_filter grpc_http1_bridge_filter grpc_http1_reverse_bridge_filter grpc_json_transcoder_filter diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index 35e9baab1e753..be3be358f78a2 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -165,6 +165,7 @@ dnsresolvers endpos fo ghi +golang guarddog GC GCC