From 063d18e147b619d415880e6be6765c6652531c90 Mon Sep 17 00:00:00 2001 From: Bjorn Date: Mon, 8 Mar 2021 16:12:18 +0800 Subject: [PATCH] initial commit --- .bazelrc | 1 + .bazelversion | 1 + .clang-format | 1 + .clang-tidy | 1 + .gitignore | 38 ++ .gitmodules | 3 + BUILD.bazel | 48 ++ CHANGELOG | 5 + CONTRIBUTING.md | 30 + DCO | 37 ++ EGo.png | Bin 0 -> 58008 bytes LICENSE | 202 ++++++ NOTICE | 5 + OWNERS.md | 5 + README.md | 218 ++++++- VERSION | 1 + WORKSPACE | 362 +++++++++++ bazel/get_workspace_status | 1 + ego-demo.sh | 6 + ego/.gitignore | 36 ++ ego/README.md | 11 + ego/src/cc/README.md | 33 + ego/src/cc/filter/http/BUILD.bazel | 118 ++++ ego/src/cc/filter/http/cgo-proxy.cc | 55 ++ ego/src/cc/filter/http/cgo-proxy.h | 53 ++ ego/src/cc/filter/http/factory.cc | 92 +++ ego/src/cc/filter/http/filter-cgo.cc | 218 +++++++ ego/src/cc/filter/http/filter-goc.cc | 134 ++++ ego/src/cc/filter/http/filter-native.cc | 48 ++ ego/src/cc/filter/http/filter.h | 230 +++++++ ego/src/cc/filter/http/filter.proto | 61 ++ ego/src/cc/filter/http/integration_test.cc | 51 ++ ego/src/cc/filter/http/native.cc | 12 + ego/src/cc/filter/http/span-group.cc | 69 ++ ego/src/cc/filter/http/span-group.h | 43 ++ ego/src/cc/goc/BUILD.bazel | 40 ++ ego/src/cc/goc/bufferinstance.cc | 47 ++ ego/src/cc/goc/envoy.h | 203 ++++++ ego/src/cc/goc/goc.cc | 259 ++++++++ ego/src/cc/goc/goc.h | 28 + ego/src/cc/goc/gohttpfilter.cc | 309 +++++++++ ego/src/cc/goc/log.cc | 46 ++ ego/src/cc/goc/proto/BUILD.bazel | 45 ++ ego/src/cc/goc/proto/dto.proto | 24 + ego/src/cc/goc/requestheadermap.cc | 217 +++++++ ego/src/cc/goc/requesttrailermap.cc | 28 + ego/src/cc/goc/responseheadermap.cc | 139 +++++ ego/src/cc/goc/stats.cc | 114 ++++ ego/src/go/BUILD.bazel | 25 + ego/src/go/README.md | 39 ++ ego/src/go/envoy/BUILD.bazel | 21 + ego/src/go/envoy/datastatus/BUILD.bazel | 13 + ego/src/go/envoy/datastatus/const.go | 20 + ego/src/go/envoy/envoy.go | 238 +++++++ ego/src/go/envoy/headersstatus/BUILD.bazel | 13 + ego/src/go/envoy/headersstatus/const.go | 20 + ego/src/go/envoy/lifespan/BUILD.bazel | 13 + ego/src/go/envoy/lifespan/const.go | 20 + ego/src/go/envoy/loglevel/BUILD.bazel | 13 + ego/src/go/envoy/loglevel/const.go | 20 + ego/src/go/envoy/statetype/BUILD.bazel | 13 + ego/src/go/envoy/statetype/const.go | 18 + ego/src/go/envoy/stats/BUILD.bazel | 13 + ego/src/go/envoy/stats/const.go | 39 ++ ego/src/go/envoy/trailersstatus/BUILD.bazel | 13 + ego/src/go/envoy/trailersstatus/const.go | 17 + ego/src/go/httpfilter.go | 127 ++++ ego/src/go/internal/cgo/BUILD.bazel | 86 +++ ego/src/go/internal/cgo/bufferinstance.go | 68 ++ ego/src/go/internal/cgo/clutch.go | 590 ++++++++++++++++++ ego/src/go/internal/cgo/clutch_test.go | 452 ++++++++++++++ ego/src/go/internal/cgo/cutils.go | 115 ++++ ego/src/go/internal/cgo/decoder_callbacks.go | 111 ++++ ego/src/go/internal/cgo/encoder_callbacks.go | 67 ++ ego/src/go/internal/cgo/filter_state.go | 31 + ego/src/go/internal/cgo/gohttpfilter.go | 312 +++++++++ ego/src/go/internal/cgo/gohttpfilterconfig.go | 218 +++++++ ego/src/go/internal/cgo/logger.go | 25 + ego/src/go/internal/cgo/main.go | 29 + ego/src/go/internal/cgo/requestheadermap.go | 115 ++++ ego/src/go/internal/cgo/requesttrailermap.go | 48 ++ ego/src/go/internal/cgo/responseheadermap.go | 89 +++ ego/src/go/internal/cgo/route.go | 64 ++ ego/src/go/internal/cgo/span.go | 60 ++ ego/src/go/internal/cgo/stats.go | 108 ++++ ego/src/go/internal/cgo/stream_info.go | 46 ++ ego/src/go/logger/BUILD.bazel | 14 + ego/src/go/logger/logger.go | 93 +++ ego/src/go/registry.go | 30 + ego/src/go/volatile/BUILD.bazel | 14 + ego/src/go/volatile/volatile.go | 32 + ego/test/cc/filter/http/BUILD.bazel | 40 ++ ego/test/cc/filter/http/filter_test.cc | 311 +++++++++ ego/test/cc/filter/http/linkopts.bzl | 131 ++++ ego/test/cc/filter/http/mocks.h | 35 ++ ego/test/cc/filter/http/span-group_test.cc | 136 ++++ ego/test/cc/goc/BUILD.bazel | 23 + ego/test/cc/goc/requestheadermap_test.cc | 98 +++ ego/test/go/mock/BUILD.bazel | 22 + ego/test/go/mock/doc.go | 3 + ego/test/go/mock/envoy.go | 19 + ego/test/go/mock/gen/envoy/BUILD.bazel | 51 ++ ego/test/go/mock/gen/envoy/buffer_instance.go | 75 +++ ego/test/go/mock/gen/envoy/counter.go | 53 ++ .../gen/envoy/decoder_filter_callbacks.go | 113 ++++ .../gen/envoy/encoder_filter_callbacks.go | 101 +++ ego/test/go/mock/gen/envoy/filter_state.go | 43 ++ ego/test/go/mock/gen/envoy/gauge.go | 49 ++ .../envoy/generic_secret_config_provider.go | 27 + ego/test/go/mock/gen/envoy/go_http_filter.go | 99 +++ .../mock/gen/envoy/go_http_filter_config.go | 47 ++ ego/test/go/mock/gen/envoy/header_map.go | 47 ++ .../go/mock/gen/envoy/header_map_read_only.go | 27 + .../go/mock/gen/envoy/header_map_updatable.go | 30 + ego/test/go/mock/gen/envoy/histogram.go | 32 + .../go/mock/gen/envoy/path_match_criterion.go | 57 ++ .../go/mock/gen/envoy/request_header_map.go | 124 ++++ .../gen/envoy/request_header_map_read_only.go | 99 +++ .../gen/envoy/request_header_map_updatable.go | 35 ++ .../envoy/request_or_response_header_map.go | 61 ++ ...equest_or_response_header_map_read_only.go | 41 ++ ...equest_or_response_header_map_updatable.go | 30 + .../go/mock/gen/envoy/request_trailer_map.go | 47 ++ .../envoy/request_trailer_map_read_only.go | 27 + .../envoy/request_trailer_map_updatable.go | 30 + .../go/mock/gen/envoy/response_header_map.go | 80 +++ .../envoy/response_header_map_read_only.go | 55 ++ .../envoy/response_header_map_updatable.go | 35 ++ ego/test/go/mock/gen/envoy/route.go | 29 + ego/test/go/mock/gen/envoy/route_entry.go | 29 + ego/test/go/mock/gen/envoy/scope.go | 63 ++ ego/test/go/mock/gen/envoy/span.go | 50 ++ .../mock/gen/envoy/stream_filter_callbacks.go | 75 +++ ego/test/go/mock/gen/envoy/stream_info.go | 89 +++ ego/test/go/mock/logger.go | 19 + egofilters/BUILD.bazel | 34 + egofilters/filters.go | 18 + egofilters/http/getheader/BUILD.bazel | 24 + egofilters/http/getheader/factory.go | 42 ++ egofilters/http/getheader/filter.go | 81 +++ egofilters/http/getheader/proto/BUILD.bazel | 40 ++ .../http/getheader/proto/getheader.proto | 16 + egofilters/http/security/BUILD.bazel | 58 ++ egofilters/http/security/config.go | 96 +++ egofilters/http/security/config_test.go | 189 ++++++ egofilters/http/security/context/BUILD.bazel | 33 + egofilters/http/security/context/context.go | 12 + .../http/security/context/request_context.go | 141 +++++ .../security/context/request_context_test.go | 40 ++ .../http/security/context/response_context.go | 100 +++ .../security/context/response_context_test.go | 67 ++ egofilters/http/security/factory.go | 47 ++ egofilters/http/security/factory_test.go | 127 ++++ egofilters/http/security/filter.go | 331 ++++++++++ egofilters/http/security/filter_sign_test.go | 390 ++++++++++++ .../http/security/filter_verify_test.go | 495 +++++++++++++++ egofilters/http/security/http/BUILD.bazel | 27 + egofilters/http/security/http/http_client.go | 44 ++ .../http/security/http/http_client_test.go | 110 ++++ egofilters/http/security/proto/BUILD.bazel | 40 ++ egofilters/http/security/proto/security.proto | 75 +++ egofilters/http/security/verifier/BUILD.bazel | 50 ++ .../http/security/verifier/base_provider.go | 19 + egofilters/http/security/verifier/consts.go | 16 + .../security/verifier/custom_hmac_provider.go | 264 ++++++++ .../custom_hmac_provider_factory_test.go | 23 + ...custom_hmac_provider_sign_required_test.go | 158 +++++ .../custom_hmac_provider_sign_test.go | 328 ++++++++++ .../custom_hmac_provider_verify_test.go | 468 ++++++++++++++ .../verifier/custom_hmac_validator.go | 36 ++ .../verifier/custom_hmac_validator_test.go | 141 +++++ egofilters/http/security/verifier/verifier.go | 25 + egofilters/mock/BUILD.bazel | 8 + egofilters/mock/doc.go | 3 + .../gen/http/security/context/BUILD.bazel | 20 + .../gen/http/security/context/callbacks.go | 18 + .../mock/gen/http/security/context/context.go | 29 + .../http/security/context/request_context.go | 132 ++++ .../security/context/response_callbacks.go | 18 + .../http/security/context/response_context.go | 162 +++++ .../mock/gen/http/security/http/BUILD.bazel | 15 + .../gen/http/security/http/http_client.go | 37 ++ .../security/http/http_client_with_ctx.go | 62 ++ .../mock/gen/http/security/proto/BUILD.bazel | 12 + .../proto/is_provider__provider_type.go | 15 + .../proto/is_requirement__requires_type.go | 15 + .../gen/http/security/verifier/BUILD.bazel | 18 + .../security/verifier/get_current_time_opt.go | 28 + .../verifier/is_valid_hmac_signature_opt.go | 35 ++ .../mock/gen/http/security/verifier/signer.go | 34 + .../gen/http/security/verifier/verifier.go | 32 + envoy | 1 + envoy.yaml | 122 ++++ external/gomockery.patch | 25 + go.mod | 11 + go.sum | 107 ++++ services/echo/BUILD.bazel | 15 + services/echo/main.go | 34 + services/hmac/BUILD.bazel | 15 + services/hmac/main.go | 41 ++ tools/copy_pb_go.py | 18 + tools/gen_compilation_database.py | 132 ++++ tools/generate_test_coverage_report.sh | 25 + tools/sync.sh | 12 + 204 files changed, 15382 insertions(+), 1 deletion(-) create mode 100644 .bazelrc create mode 120000 .bazelversion create mode 120000 .clang-format create mode 120000 .clang-tidy create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 BUILD.bazel create mode 100644 DCO create mode 100644 EGo.png create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 OWNERS.md create mode 100644 VERSION create mode 100644 WORKSPACE create mode 120000 bazel/get_workspace_status create mode 100755 ego-demo.sh create mode 100644 ego/.gitignore create mode 100644 ego/README.md create mode 100644 ego/src/cc/README.md create mode 100644 ego/src/cc/filter/http/BUILD.bazel create mode 100644 ego/src/cc/filter/http/cgo-proxy.cc create mode 100644 ego/src/cc/filter/http/cgo-proxy.h create mode 100644 ego/src/cc/filter/http/factory.cc create mode 100644 ego/src/cc/filter/http/filter-cgo.cc create mode 100644 ego/src/cc/filter/http/filter-goc.cc create mode 100644 ego/src/cc/filter/http/filter-native.cc create mode 100644 ego/src/cc/filter/http/filter.h create mode 100644 ego/src/cc/filter/http/filter.proto create mode 100644 ego/src/cc/filter/http/integration_test.cc create mode 100644 ego/src/cc/filter/http/native.cc create mode 100644 ego/src/cc/filter/http/span-group.cc create mode 100644 ego/src/cc/filter/http/span-group.h create mode 100644 ego/src/cc/goc/BUILD.bazel create mode 100644 ego/src/cc/goc/bufferinstance.cc create mode 100644 ego/src/cc/goc/envoy.h create mode 100644 ego/src/cc/goc/goc.cc create mode 100644 ego/src/cc/goc/goc.h create mode 100644 ego/src/cc/goc/gohttpfilter.cc create mode 100644 ego/src/cc/goc/log.cc create mode 100644 ego/src/cc/goc/proto/BUILD.bazel create mode 100644 ego/src/cc/goc/proto/dto.proto create mode 100644 ego/src/cc/goc/requestheadermap.cc create mode 100644 ego/src/cc/goc/requesttrailermap.cc create mode 100644 ego/src/cc/goc/responseheadermap.cc create mode 100644 ego/src/cc/goc/stats.cc create mode 100644 ego/src/go/BUILD.bazel create mode 100644 ego/src/go/README.md create mode 100644 ego/src/go/envoy/BUILD.bazel create mode 100644 ego/src/go/envoy/datastatus/BUILD.bazel create mode 100644 ego/src/go/envoy/datastatus/const.go create mode 100644 ego/src/go/envoy/envoy.go create mode 100644 ego/src/go/envoy/headersstatus/BUILD.bazel create mode 100644 ego/src/go/envoy/headersstatus/const.go create mode 100644 ego/src/go/envoy/lifespan/BUILD.bazel create mode 100644 ego/src/go/envoy/lifespan/const.go create mode 100644 ego/src/go/envoy/loglevel/BUILD.bazel create mode 100644 ego/src/go/envoy/loglevel/const.go create mode 100644 ego/src/go/envoy/statetype/BUILD.bazel create mode 100644 ego/src/go/envoy/statetype/const.go create mode 100644 ego/src/go/envoy/stats/BUILD.bazel create mode 100644 ego/src/go/envoy/stats/const.go create mode 100644 ego/src/go/envoy/trailersstatus/BUILD.bazel create mode 100644 ego/src/go/envoy/trailersstatus/const.go create mode 100644 ego/src/go/httpfilter.go create mode 100644 ego/src/go/internal/cgo/BUILD.bazel create mode 100644 ego/src/go/internal/cgo/bufferinstance.go create mode 100644 ego/src/go/internal/cgo/clutch.go create mode 100644 ego/src/go/internal/cgo/clutch_test.go create mode 100644 ego/src/go/internal/cgo/cutils.go create mode 100644 ego/src/go/internal/cgo/decoder_callbacks.go create mode 100644 ego/src/go/internal/cgo/encoder_callbacks.go create mode 100644 ego/src/go/internal/cgo/filter_state.go create mode 100644 ego/src/go/internal/cgo/gohttpfilter.go create mode 100644 ego/src/go/internal/cgo/gohttpfilterconfig.go create mode 100644 ego/src/go/internal/cgo/logger.go create mode 100644 ego/src/go/internal/cgo/main.go create mode 100644 ego/src/go/internal/cgo/requestheadermap.go create mode 100644 ego/src/go/internal/cgo/requesttrailermap.go create mode 100644 ego/src/go/internal/cgo/responseheadermap.go create mode 100644 ego/src/go/internal/cgo/route.go create mode 100644 ego/src/go/internal/cgo/span.go create mode 100644 ego/src/go/internal/cgo/stats.go create mode 100644 ego/src/go/internal/cgo/stream_info.go create mode 100644 ego/src/go/logger/BUILD.bazel create mode 100644 ego/src/go/logger/logger.go create mode 100644 ego/src/go/registry.go create mode 100644 ego/src/go/volatile/BUILD.bazel create mode 100644 ego/src/go/volatile/volatile.go create mode 100644 ego/test/cc/filter/http/BUILD.bazel create mode 100644 ego/test/cc/filter/http/filter_test.cc create mode 100644 ego/test/cc/filter/http/linkopts.bzl create mode 100644 ego/test/cc/filter/http/mocks.h create mode 100644 ego/test/cc/filter/http/span-group_test.cc create mode 100644 ego/test/cc/goc/BUILD.bazel create mode 100644 ego/test/cc/goc/requestheadermap_test.cc create mode 100644 ego/test/go/mock/BUILD.bazel create mode 100644 ego/test/go/mock/doc.go create mode 100644 ego/test/go/mock/envoy.go create mode 100644 ego/test/go/mock/gen/envoy/BUILD.bazel create mode 100644 ego/test/go/mock/gen/envoy/buffer_instance.go create mode 100644 ego/test/go/mock/gen/envoy/counter.go create mode 100644 ego/test/go/mock/gen/envoy/decoder_filter_callbacks.go create mode 100644 ego/test/go/mock/gen/envoy/encoder_filter_callbacks.go create mode 100644 ego/test/go/mock/gen/envoy/filter_state.go create mode 100644 ego/test/go/mock/gen/envoy/gauge.go create mode 100644 ego/test/go/mock/gen/envoy/generic_secret_config_provider.go create mode 100644 ego/test/go/mock/gen/envoy/go_http_filter.go create mode 100644 ego/test/go/mock/gen/envoy/go_http_filter_config.go create mode 100644 ego/test/go/mock/gen/envoy/header_map.go create mode 100644 ego/test/go/mock/gen/envoy/header_map_read_only.go create mode 100644 ego/test/go/mock/gen/envoy/header_map_updatable.go create mode 100644 ego/test/go/mock/gen/envoy/histogram.go create mode 100644 ego/test/go/mock/gen/envoy/path_match_criterion.go create mode 100644 ego/test/go/mock/gen/envoy/request_header_map.go create mode 100644 ego/test/go/mock/gen/envoy/request_header_map_read_only.go create mode 100644 ego/test/go/mock/gen/envoy/request_header_map_updatable.go create mode 100644 ego/test/go/mock/gen/envoy/request_or_response_header_map.go create mode 100644 ego/test/go/mock/gen/envoy/request_or_response_header_map_read_only.go create mode 100644 ego/test/go/mock/gen/envoy/request_or_response_header_map_updatable.go create mode 100644 ego/test/go/mock/gen/envoy/request_trailer_map.go create mode 100644 ego/test/go/mock/gen/envoy/request_trailer_map_read_only.go create mode 100644 ego/test/go/mock/gen/envoy/request_trailer_map_updatable.go create mode 100644 ego/test/go/mock/gen/envoy/response_header_map.go create mode 100644 ego/test/go/mock/gen/envoy/response_header_map_read_only.go create mode 100644 ego/test/go/mock/gen/envoy/response_header_map_updatable.go create mode 100644 ego/test/go/mock/gen/envoy/route.go create mode 100644 ego/test/go/mock/gen/envoy/route_entry.go create mode 100644 ego/test/go/mock/gen/envoy/scope.go create mode 100644 ego/test/go/mock/gen/envoy/span.go create mode 100644 ego/test/go/mock/gen/envoy/stream_filter_callbacks.go create mode 100644 ego/test/go/mock/gen/envoy/stream_info.go create mode 100644 ego/test/go/mock/logger.go create mode 100644 egofilters/BUILD.bazel create mode 100644 egofilters/filters.go create mode 100644 egofilters/http/getheader/BUILD.bazel create mode 100644 egofilters/http/getheader/factory.go create mode 100644 egofilters/http/getheader/filter.go create mode 100644 egofilters/http/getheader/proto/BUILD.bazel create mode 100644 egofilters/http/getheader/proto/getheader.proto create mode 100644 egofilters/http/security/BUILD.bazel create mode 100644 egofilters/http/security/config.go create mode 100644 egofilters/http/security/config_test.go create mode 100644 egofilters/http/security/context/BUILD.bazel create mode 100644 egofilters/http/security/context/context.go create mode 100644 egofilters/http/security/context/request_context.go create mode 100644 egofilters/http/security/context/request_context_test.go create mode 100644 egofilters/http/security/context/response_context.go create mode 100644 egofilters/http/security/context/response_context_test.go create mode 100644 egofilters/http/security/factory.go create mode 100644 egofilters/http/security/factory_test.go create mode 100644 egofilters/http/security/filter.go create mode 100644 egofilters/http/security/filter_sign_test.go create mode 100644 egofilters/http/security/filter_verify_test.go create mode 100644 egofilters/http/security/http/BUILD.bazel create mode 100644 egofilters/http/security/http/http_client.go create mode 100644 egofilters/http/security/http/http_client_test.go create mode 100644 egofilters/http/security/proto/BUILD.bazel create mode 100644 egofilters/http/security/proto/security.proto create mode 100644 egofilters/http/security/verifier/BUILD.bazel create mode 100644 egofilters/http/security/verifier/base_provider.go create mode 100644 egofilters/http/security/verifier/consts.go create mode 100644 egofilters/http/security/verifier/custom_hmac_provider.go create mode 100644 egofilters/http/security/verifier/custom_hmac_provider_factory_test.go create mode 100644 egofilters/http/security/verifier/custom_hmac_provider_sign_required_test.go create mode 100644 egofilters/http/security/verifier/custom_hmac_provider_sign_test.go create mode 100644 egofilters/http/security/verifier/custom_hmac_provider_verify_test.go create mode 100644 egofilters/http/security/verifier/custom_hmac_validator.go create mode 100644 egofilters/http/security/verifier/custom_hmac_validator_test.go create mode 100644 egofilters/http/security/verifier/verifier.go create mode 100644 egofilters/mock/BUILD.bazel create mode 100644 egofilters/mock/doc.go create mode 100644 egofilters/mock/gen/http/security/context/BUILD.bazel create mode 100644 egofilters/mock/gen/http/security/context/callbacks.go create mode 100644 egofilters/mock/gen/http/security/context/context.go create mode 100644 egofilters/mock/gen/http/security/context/request_context.go create mode 100644 egofilters/mock/gen/http/security/context/response_callbacks.go create mode 100644 egofilters/mock/gen/http/security/context/response_context.go create mode 100644 egofilters/mock/gen/http/security/http/BUILD.bazel create mode 100644 egofilters/mock/gen/http/security/http/http_client.go create mode 100644 egofilters/mock/gen/http/security/http/http_client_with_ctx.go create mode 100644 egofilters/mock/gen/http/security/proto/BUILD.bazel create mode 100644 egofilters/mock/gen/http/security/proto/is_provider__provider_type.go create mode 100644 egofilters/mock/gen/http/security/proto/is_requirement__requires_type.go create mode 100644 egofilters/mock/gen/http/security/verifier/BUILD.bazel create mode 100644 egofilters/mock/gen/http/security/verifier/get_current_time_opt.go create mode 100644 egofilters/mock/gen/http/security/verifier/is_valid_hmac_signature_opt.go create mode 100644 egofilters/mock/gen/http/security/verifier/signer.go create mode 100644 egofilters/mock/gen/http/security/verifier/verifier.go create mode 160000 envoy create mode 100644 envoy.yaml create mode 100644 external/gomockery.patch create mode 100644 go.mod create mode 100644 go.sum create mode 100644 services/echo/BUILD.bazel create mode 100644 services/echo/main.go create mode 100644 services/hmac/BUILD.bazel create mode 100644 services/hmac/main.go create mode 100755 tools/copy_pb_go.py create mode 100755 tools/gen_compilation_database.py create mode 100755 tools/generate_test_coverage_report.sh create mode 100755 tools/sync.sh diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 0000000..0d9d9da --- /dev/null +++ b/.bazelrc @@ -0,0 +1 @@ +import %workspace%/envoy/.bazelrc diff --git a/.bazelversion b/.bazelversion new file mode 120000 index 0000000..9da24ef --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +envoy/.bazelversion \ No newline at end of file diff --git a/.clang-format b/.clang-format new file mode 120000 index 0000000..30fec7d --- /dev/null +++ b/.clang-format @@ -0,0 +1 @@ +envoy/.clang-format \ No newline at end of file diff --git a/.clang-tidy b/.clang-tidy new file mode 120000 index 0000000..b64b4a3 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1 @@ +envoy/.clang-tidy \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a6c51a --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +/bazel-* +BROWSE +/build +/build_* +*.bzlc +.cache +.clangd +.classpath +.clwb/ +/ci/bazel-* +compile_commands.json +cscope.* +.deps +.devcontainer.json +/docs/landing_source/.bundle +/generated +.history/ +.idea/ +.project +*.pyc +**/pyformat +SOURCE_VERSION +.settings/ +*.sw* +tags +TAGS +/test/coverage/BUILD +/tools/spelling/.aspell.en.pws +.vimrc +.vs +.vscode +clang-tidy-fixes.yaml +clang.bazelrc +user.bazelrc +CMakeLists.txt +cmake-build-debug +*.pb.go +*.pb.validate.go diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ab95282 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "envoy"] + path = envoy + url = https://github.com/envoyproxy/envoy diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 0000000..3e263a0 --- /dev/null +++ b/BUILD.bazel @@ -0,0 +1,48 @@ +package(default_visibility = ["//visibility:public"]) + +load( + "@envoy//bazel:envoy_build_system.bzl", + "envoy_cc_binary", + "envoy_cc_library", + "envoy_cc_test", +) + +envoy_cc_binary( + name = "envoy", + repository = "@envoy", + linkopts = select({ + "@bazel_tools//src/conditions:darwin": ["-framework Security"], + "//conditions:default": [], + }), + deps = [ + "//egofilters:ego_filter_protos", + "//ego/src/cc/filter/http:factory", + "//ego/src/cc/goc:goc", + "@envoy//source/exe:envoy_main_entry_lib", + ], +) + +sh_test( + name = "ego-demo", + srcs = [ + "ego-demo.sh", + ], + data = [ + "//:envoy.yaml", + "//:envoy", + "//services/echo:echo", + "//services/hmac:hmac", + ], +) + +load("@bazel_gazelle//:def.bzl", "gazelle") + +# Exclude vscode history plugin folder ;-) +# gazelle:exclude .history +# gazelle:exclude envoy +# gazelle:exclude tools + +# gazelle:prefix github.com/grab/ego + +# run gazelle to udate bazel build files +gazelle(name = "gazelle") diff --git a/CHANGELOG b/CHANGELOG index 8b13789..5269f18 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1 +1,6 @@ +v0.0.1 +====== + +This is the initial public release. It works with Envoy 1.14.6 and supports +Go-routines and zero-copy interactions. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8b13789..2bfbab8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1 +1,31 @@ +# Communication +Before making a proposal, please spend some time investigating / prototyping. +The devil is in the details, and the feedback from Bazel, linker, and compilers +has proven more convincing than our feeble human voices. + +If you find a bug, please create an issue, and if you want to pick up a larger +chunk of work (like exposing a new C++ interface in Go), please do the same for +better visibility. + +# Coding style + +In the absence of documents, please do stick to the style you find in existing +code. For Go, this is rather canonical, for C/C++, we have added .clang-format +and .clang-tidy configurations to avoid major accidents. + +Besides that, please keep in mind that this is not a golf course. Simple, and +possibly repetitive code is favoured over ingenious meta tricks that do little +more than make maintainance and debugging more difficult. + +# DCO: Sign your work + +Please do sign your work certifying that you have the right to pass it on as an +open-source patch. The rules are pretty simple: if you can certify the contents +of the [DCO](DCO) document contained in this repository, then you just add a +line to every git commit message: + + Signed-off-by: Joe Smith + +using your _real name_. You can also add the sign off via `git commit -s`. Note +that _every_ commit needs to have proper sign-off. diff --git a/DCO b/DCO new file mode 100644 index 0000000..8201f99 --- /dev/null +++ b/DCO @@ -0,0 +1,37 @@ +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +1 Letterman Drive +Suite D4700 +San Francisco, CA, 94129 + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. diff --git a/EGo.png b/EGo.png new file mode 100644 index 0000000000000000000000000000000000000000..872375cb1936ec1f23df7c97e267733947d51c8f GIT binary patch literal 58008 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%0A8+SQMsE;@0o8J25E?h6-^Me5qXsyGoGt#v#WS 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 L>I!9YrvCo}-2tXi literal 0 HcmV?d00001 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..1a99c72 --- /dev/null +++ b/NOTICE @@ -0,0 +1,5 @@ +EGo -- Writing Envoy filters in Go + +Copyright 2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. + +Use of this source code is governed by the Apache License 2.0 that can be found in the LICENSE file \ No newline at end of file diff --git a/OWNERS.md b/OWNERS.md new file mode 100644 index 0000000..ac9cf22 --- /dev/null +++ b/OWNERS.md @@ -0,0 +1,5 @@ +Maintainers + +* Bjorn Karge ([bkgs](https://github.com/bkgs)) (bjorn.karge@grab.com) +* Tien Nguyen Van ([tiennv147](https://github.com/tiennv147)) (tien.nguyenvan@grab.com) +* Xuan Nguyen Duc ([xuanit](https://github.com/xuanit)) (xuan.nguyen@grab.com) diff --git a/README.md b/README.md index 3c65567..6d432d1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,219 @@ # EGo-Demo -Envoy filters in Go \ No newline at end of file +![EGo Splash](EGo.png) + +This is a demo of how to build a Golang filter for Envoy, based on the [Envoy +Filter Example project](https://github.com/envoyproxy/envoy-filter-example), +by using Go's [CGo](https://golang.org/cmd/cgo/) feature. + +It is still a little rough on the edges, though probably good enough to help +understand most of the challenges and benefits of this approach. + +## Design Principles + +* Provide a **Go-native experience** for Go developers +* Support **zero-copy** data access where possible +* **Unopinionated** interface: reflect the envoy C++ interface classes as close as possible + +## Successes + +* Parsing and passing filter configuration from C to Go with zero-copy and leveraging Envoy's protobuf validation mechanism +* Communication between C and Go with zero-copy and strongly parallel interface design +* Allow the use of goroutines with minimal discipline required (`Pin`/`Post`/`Unpin`) +* Creating new filters without having to touch C code (except when "lifting up" new envoy interfaces) +* Support interactions with SDS via Envoy's built-in client + +## Challenges + +This project is still in its infancy, and there are a couple of technical +challenges still waiting to be solved. + +### Dependency Cycles + +In order to call Go functions from C, the go libraries need to be built first +(as this produces the required C headers). However, the Go libraries also have a +dependency on C libraries (to facilitate calls to envoy functions). + +This can, in most cases, be mitigated by careful separation of the functions +needed for the Go->C interactions from the functions implementing the envoy L7 +filter interfaces. + +When there are functions that are needed on both sides, this often requires +creativity. A good example for this is the `onPost` handler, which needs to be +passed as a callback on the Go->C side, but has an implementation that needs to +make a C->Go call. + +Since this obviously isn't a limitation of linkers or C, the situation could +probably mitigated by extending the Bazel tooling (for example, by separating +the generation of Go C headers and the Go library). + +### CGo limitations + +Currently, there can only be one CGo library, and its `main()` function needs to +reside in the same package containing the CGo code. This means that `egofilters` +needs to be referenced from the `ego` package and can't contain CGo code. This +is probably the most significant issue because it is in direct opposition to +the desired architecture. + +### Replication of Envoy classes as Go interfaces + +This is a work intensive process, and the only reason we chose to take on this +challenge is that it typically is a one-off effort that will become less notable +over time. + +Keeping up with Envoy's changing C++ interface also requires regular effort, and +in fact, this project currently doesn't account for different Envoy versions, +although it probably should. + +## Prerequisites + +### Bazel + +#### MacOS + +To set up bazel on MacOS, [install Homebrew](https://docs.brew.sh/Installation) +and then + +```bash +brew install bazelbuild/tap/bazelisk +brew install coreutils wget cmake libtool go bazel automake ninja clang-format autoconf aspell +``` + +(Don't worry if you get a warning for a symlink that couldn't be updated because +bazelisk is sitting on it -- this is by design) + +## Building + +```bash +git submodule update --init # clone the linked envoy tag into /envoy +bazel build //:envoy # build envoy with the new libraries added +``` + +Note: This version requires Envoy v1.14 + +## Trying It Out + +This demo includes two example filters: + +* [getheader](egofilters/http/getheader) is a very basic filter that showcases + using go-routines and the Go runtime library for HTTP requests. Its sole + purpose is to retrieve response header `hdr` from a GET request for URL `src`, + and inject it as request header `key` into the request to the upstream + service. + +* [security](egofilters/http/security) is a more elaborate piece outlining a + framework to accomodate many different custom authentication methods, to show + how things might pan out at scale. This is showcased via a simple pseudo-HMAC + filter with the actual checksum calculations delegated to a separate [REST + service](services/hmac/main.go) to once more demonstrate how to align + Go-routines and envoy worker threads as well as to motivate the use of SDS + based secrets. + +To play with it, go to the repository root, and do + +```bash +bazel run ego-demo +``` + +This spins up a simple [echo service](services/echo/main.go) as upstream for all +requests, an [HMAC authentication provider sidecar](services/hmac/main.go), and +the Envoy proxy [configured](envoy.yaml) to showcase the Go-based filters +provided with this demo. + +```bash +curl -iX GET 'http://127.0.0.1:8080/Hello,world!' +``` + +Note the `X-Getheader-Result` in the response which was injected into the +request by the `getheader` filter + +```bash +curl -iX GET 'http://127.0.0.1:8080/hmac/unsigned' -H 'Authorization: client_id:123' +``` + +This should produce a `401 Unauthorized` response + +```bash +curl -iX GET 'http://127.0.0.1:8080/hmac/unsigned' -H 'Authorization: client_id:17' +``` + +The expectation would be a `200 OK` response. + +```bash +curl -iX GET 'http://127.0.0.1:8080/hmac/signed' -H 'Authorization: client_id:15' +``` + +This should also result in a `200 OK` response, and a +`x-custom-auth-signature-hmac-sha256: 200_nnn` response header (which clearly +isn't the SHA256 HMAC but just the status code with the length of the response +body appended). + +## Tinkering + +The Go code is integrated with bazel via `rules_go`. The bazel rules +areautomatically generated by `gazelle`, and interfaces mocks are generated via +`mockery`. + +### Running Tests + +The EGo interface layer still deserves a bit more rigorous testing, only some +mind-numbing permutation testing for the most critical pieces has already been +implemented so far, please budget a couple of minutes for this to run. + +```bash +bazel test //ego/... +``` + +Since the HMAC filter is a gutted version of a more elaborate thing, coverage +is relatively good (although not very pretty to read). + +```bash +bazel test //egofilters/... +``` + +#### Note for MacOS users + +Please see [Envoy issue #10478](https://github.com/envoyproxy/envoy/issues/10478) +if some of the C++ tests fail with this message: + +```plain +dyld: Symbol not found: _program_invocation_name +``` + +This is most likely means XCode needs to be installed. + +### Updating the Go Rules + +From the repository root, do + +```bash +bazel run gazelle +``` + +### Adding new Go Dependencies + +From the repository root, do + +```bash +bazel run //:gazelle -- update-repos -from_file=go.mod +``` + +### Bazel and Go Interface Mocks + +Make sure you have installed [mockery](https://github.com/vektra/mockery/releases). +Then, from the repository root, do + +```bash +tools/copy_pb_go.py # update generated protobuf bindings + +cd egofilters/mock +go generate + +cd ../../ego/test/go/mock +go generate + +cd ../../../.. +bazel run gazelle # just in case +``` + +Bazel integration of this is work in progress... diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..c8aa910 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.70 diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 0000000..88784a7 --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,362 @@ +workspace(name = "ego_demo") + +local_repository( + name = "envoy", + path = "envoy", +) + +load("@envoy//bazel:api_binding.bzl", "envoy_api_binding") + +envoy_api_binding() + +load("@envoy//bazel:api_repositories.bzl", "envoy_api_dependencies") + +envoy_api_dependencies() + +load("@envoy//bazel:repositories.bzl", "envoy_dependencies") + +envoy_dependencies() + +# load("@envoy//bazel:repositories_extra.bzl", "envoy_dependencies_extra") +# +# envoy_dependencies_extra() + +load("@envoy//bazel:dependency_imports.bzl", "envoy_dependency_imports") + +envoy_dependency_imports() + +# configure Gazelle +# see https://github.com/bazelbuild/bazel-gazelle#running-gazelle-with-bazel +# +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "io_bazel_rules_go", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.25.1/rules_go-v0.25.1.tar.gz", + "https://github.com/bazelbuild/rules_go/releases/download/v0.25.1/rules_go-v0.25.1.tar.gz", + ], + sha256 = "7904dbecbaffd068651916dce77ff3437679f9d20e1a7956bff43826e7645fcc", +) + +load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") + +go_rules_dependencies() + +go_register_toolchains() + +http_archive( + name = "bazel_gazelle", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.22.3/bazel-gazelle-v0.22.3.tar.gz", + "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.22.3/bazel-gazelle-v0.22.3.tar.gz", + ], + sha256 = "222e49f034ca7a1d1231422cdb67066b885819885c356673cb1f72f748a3c9d4", +) + +load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository") + +gazelle_dependencies() + +# configure bazel_mockery +# see https://github.com/Helcaraxan/bazel_mockery +# +http_archive( + name = "bazel_mockery", + urls = [ + "https://github.com/Helcaraxan/bazel_mockery/archive/51bf6832ff56764ccf33411c55eb6bea883868fd.zip", + ], + sha256 = "41fef0d4f36a460aec43d8c7fada0cd54866bf59f5a2e1714d11c9302a7f4284", + strip_prefix = "bazel_mockery-51bf6832ff56764ccf33411c55eb6bea883868fd", + patches = ["gomockery.patch"], + patch_args = ["-p1"], +) + +load("@bazel_mockery//:gomockery.bzl", "go_mockery") + +go_repository( + name = "co_honnef_go_tools", + importpath = "honnef.co/go/tools", + sum = "h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=", + version = "v0.0.0-20190523083050-ea95bdfd59fc", +) + +go_repository( + name = "com_github_burntsushi_toml", + importpath = "github.com/BurntSushi/toml", + sum = "h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=", + version = "v0.3.1", +) + +go_repository( + name = "com_github_census_instrumentation_opencensus_proto", + importpath = "github.com/census-instrumentation/opencensus-proto", + sum = "h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=", + version = "v0.2.1", +) + +go_repository( + name = "com_github_client9_misspell", + importpath = "github.com/client9/misspell", + sum = "h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=", + version = "v0.3.4", +) + +go_repository( + name = "com_github_davecgh_go_spew", + importpath = "github.com/davecgh/go-spew", + sum = "h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=", + version = "v1.1.0", +) + +go_repository( + name = "com_github_envoyproxy_go_control_plane", + importpath = "github.com/envoyproxy/go-control-plane", + sum = "h1:4cmBvAEBNJaGARUEs3/suWRyfyBfhf7I60WBZq+bv2w=", + version = "v0.9.1-0.20191026205805-5f8ba28d4473", +) + +go_repository( + name = "com_github_envoyproxy_protoc_gen_validate", + importpath = "github.com/envoyproxy/protoc-gen-validate", + sum = "h1:7dLaJvASGRD7X49jSCSXXHwKPm0ZN9r9kJD+p+vS7dM=", + version = "v0.4.1", +) + +go_repository( + name = "com_github_golang_glog", + importpath = "github.com/golang/glog", + sum = "h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=", + version = "v0.0.0-20160126235308-23def4e6c14b", +) + +go_repository( + name = "com_github_golang_mock", + importpath = "github.com/golang/mock", + sum = "h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=", + version = "v1.1.1", +) + +go_repository( + name = "com_github_golang_protobuf", + importpath = "github.com/golang/protobuf", + sum = "h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=", + version = "v1.4.3", +) + +go_repository( + name = "com_github_google_go_cmp", + importpath = "github.com/google/go-cmp", + sum = "h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=", + version = "v0.5.0", +) + +go_repository( + name = "com_github_iancoleman_strcase", + importpath = "github.com/iancoleman/strcase", + sum = "h1:ux/56T2xqZO/3cP1I2F86qpeoYPCOzk+KF/UH/Ar+lk=", + version = "v0.0.0-20180726023541-3605ed457bf7", +) + +go_repository( + name = "com_github_kr_fs", + importpath = "github.com/kr/fs", + sum = "h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=", + version = "v0.1.0", +) + +go_repository( + name = "com_github_lyft_protoc_gen_star", + importpath = "github.com/lyft/protoc-gen-star", + sum = "h1:sImehRT+p7lW9n6R7MQc5hVgzWGEkDVZU4AsBQ4Isu8=", + version = "v0.5.1", +) + +go_repository( + name = "com_github_pkg_errors", + importpath = "github.com/pkg/errors", + sum = "h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=", + version = "v0.8.1", +) + +go_repository( + name = "com_github_pkg_sftp", + importpath = "github.com/pkg/sftp", + sum = "h1:VasscCm72135zRysgrJDKsntdmPN+OuU3+nnHYA9wyc=", + version = "v1.10.1", +) + +go_repository( + name = "com_github_pmezard_go_difflib", + importpath = "github.com/pmezard/go-difflib", + sum = "h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=", + version = "v1.0.0", +) + +go_repository( + name = "com_github_prometheus_client_model", + importpath = "github.com/prometheus/client_model", + sum = "h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=", + version = "v0.0.0-20190812154241-14fe0d1b01d4", +) + +go_repository( + name = "com_github_spf13_afero", + importpath = "github.com/spf13/afero", + sum = "h1:8q6vk3hthlpb2SouZcnBVKboxWQWMDNF38bwholZrJc=", + version = "v1.3.4", +) + +go_repository( + name = "com_github_stretchr_objx", + importpath = "github.com/stretchr/objx", + sum = "h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=", + version = "v0.1.0", +) + +go_repository( + name = "com_github_stretchr_testify", + importpath = "github.com/stretchr/testify", + sum = "h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=", + version = "v1.7.0", +) + +go_repository( + name = "com_github_yuin_goldmark", + importpath = "github.com/yuin/goldmark", + sum = "h1:nqDD4MMMQA0lmWq03Z2/myGPYLQoXtmi0rGVs95ntbo=", + version = "v1.1.27", +) + +go_repository( + name = "com_google_cloud_go", + importpath = "cloud.google.com/go", + sum = "h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=", + version = "v0.26.0", +) + +go_repository( + name = "in_gopkg_check_v1", + importpath = "gopkg.in/check.v1", + sum = "h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=", + version = "v0.0.0-20161208181325-20d25e280405", +) + +go_repository( + name = "in_gopkg_yaml_v2", + importpath = "gopkg.in/yaml.v2", + sum = "h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=", + version = "v2.2.2", +) + +go_repository( + name = "in_gopkg_yaml_v3", + importpath = "gopkg.in/yaml.v3", + sum = "h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=", + version = "v3.0.0-20200313102051-9f266ea9e77c", +) + +go_repository( + name = "org_golang_google_appengine", + importpath = "google.golang.org/appengine", + sum = "h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=", + version = "v1.4.0", +) + +go_repository( + name = "org_golang_google_genproto", + importpath = "google.golang.org/genproto", + sum = "h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=", + version = "v0.0.0-20200526211855-cb27e3aa2013", +) + +go_repository( + name = "org_golang_google_grpc", + importpath = "google.golang.org/grpc", + sum = "h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=", + version = "v1.27.0", +) + +go_repository( + name = "org_golang_google_protobuf", + importpath = "google.golang.org/protobuf", + sum = "h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=", + version = "v1.25.0", +) + +go_repository( + name = "org_golang_x_crypto", + importpath = "golang.org/x/crypto", + sum = "h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=", + version = "v0.0.0-20191011191535-87dc89f01550", +) + +go_repository( + name = "org_golang_x_exp", + importpath = "golang.org/x/exp", + sum = "h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=", + version = "v0.0.0-20190121172915-509febef88a4", +) + +go_repository( + name = "org_golang_x_lint", + importpath = "golang.org/x/lint", + sum = "h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=", + version = "v0.0.0-20200302205851-738671d3881b", +) + +go_repository( + name = "org_golang_x_mod", + importpath = "golang.org/x/mod", + sum = "h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=", + version = "v0.2.0", +) + +go_repository( + name = "org_golang_x_net", + importpath = "golang.org/x/net", + sum = "h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=", + version = "v0.0.0-20200226121028-0de0cce0169b", +) + +go_repository( + name = "org_golang_x_oauth2", + importpath = "golang.org/x/oauth2", + sum = "h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=", + version = "v0.0.0-20180821212333-d2e6202438be", +) + +go_repository( + name = "org_golang_x_sync", + importpath = "golang.org/x/sync", + sum = "h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=", + version = "v0.0.0-20190911185100-cd5d95a43a6e", +) + +go_repository( + name = "org_golang_x_sys", + importpath = "golang.org/x/sys", + sum = "h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=", + version = "v0.0.0-20190412213103-97732733099d", +) + +go_repository( + name = "org_golang_x_text", + importpath = "golang.org/x/text", + sum = "h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=", + version = "v0.3.3", +) + +go_repository( + name = "org_golang_x_tools", + importpath = "golang.org/x/tools", + sum = "h1:SjQ2+AKWgZLc1xej6WSzL+Dfs5Uyd5xcZH1mGC411IA=", + version = "v0.0.0-20200522201501-cb1345f3a375", +) + +go_repository( + name = "org_golang_x_xerrors", + importpath = "golang.org/x/xerrors", + sum = "h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=", + version = "v0.0.0-20191204190536-9bdfabe68543", +) diff --git a/bazel/get_workspace_status b/bazel/get_workspace_status new file mode 120000 index 0000000..7de9dd6 --- /dev/null +++ b/bazel/get_workspace_status @@ -0,0 +1 @@ +../envoy/bazel/get_workspace_status \ No newline at end of file diff --git a/ego-demo.sh b/ego-demo.sh new file mode 100755 index 0000000..c471e66 --- /dev/null +++ b/ego-demo.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# + +set -e + +services/echo/echo & services/hmac/hmac & envoy -c envoy.yaml \ No newline at end of file diff --git a/ego/.gitignore b/ego/.gitignore new file mode 100644 index 0000000..c1cdbe1 --- /dev/null +++ b/ego/.gitignore @@ -0,0 +1,36 @@ +/bazel-* +BROWSE +/build +/build_* +*.bzlc +.cache +.clangd +.classpath +.clwb/ +/ci/bazel-* +compile_commands.json +cscope.* +.deps +.devcontainer.json +/docs/landing_source/.bundle +/generated +.history/ +.idea/ +.project +*.pyc +**/pyformat +SOURCE_VERSION +.settings/ +*.sw* +tags +TAGS +/test/coverage/BUILD +/tools/spelling/.aspell.en.pws +.vimrc +.vs +.vscode +clang-tidy-fixes.yaml +clang.bazelrc +user.bazelrc +CMakeLists.txt +cmake-build-debug diff --git a/ego/README.md b/ego/README.md new file mode 100644 index 0000000..e57a60f --- /dev/null +++ b/ego/README.md @@ -0,0 +1,11 @@ +This repository contains a shim for simplifying development of envoy filters using Golang. + +Due to poorly understood build mechanics, this presently isn't a Bazel workspace in its own right. + +Rather, it needs to be added as git submodule into a repository that also embeds envoy and provides the actual filter implementations. + +In fact, this repository even has a reference to a directory in the embedding project (egofilters), see `src/go/internal/cgo/cutils.go`. + +The reason for this is that the filters in the embedding project need to register themselves via package init() calls that would never be run as part of the cgo static library initialization, otherwise. + +This is aggravated by the fact that Bazel's `rules_go` presently only allows one Cgo package. \ No newline at end of file diff --git a/ego/src/cc/README.md b/ego/src/cc/README.md new file mode 100644 index 0000000..d26291e --- /dev/null +++ b/ego/src/cc/README.md @@ -0,0 +1,33 @@ +# Envoy Golang Filter Demo + +This project demonstrates the linking of additional HTTP filters implemented in Go with the Envoy binary. + +A new filter `add-header` which adds a HTTP header is introduced. + +Integration tests demonstrating the filter's end-to-end behavior are +also provided. + +## Building + +To build the Envoy static binary: + +1. `git submodule update --init` +2. `bazel build //:envoy` + +## Testing + +To run the `add-header` integration test: + +`bazel test //src/cc/filter/http/addheader:integration_test` + +# Layout / Packages + +# cgo + +This package provides two libraries: one for supporting upcalls to Go (`cgo`) and one for supporting downcalls from Go (`native`). + +In order to avoid circular dependencies, the `cgo` library must not be used as dependency for the Golang packages. Rather, these need to depend on the `native` package only. + +Both libraries use the file `envoy.h`: one indirectly (via Go code) and one directly (included by the downcall proxies). + +# filter \ No newline at end of file diff --git a/ego/src/cc/filter/http/BUILD.bazel b/ego/src/cc/filter/http/BUILD.bazel new file mode 100644 index 0000000..e32ba82 --- /dev/null +++ b/ego/src/cc/filter/http/BUILD.bazel @@ -0,0 +1,118 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") + +# gazelle:ignore + +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +package(default_visibility = ["//visibility:public"]) + +load( + "@envoy//bazel:envoy_build_system.bzl", + "envoy_cc_binary", + "envoy_cc_library", + "envoy_cc_test", +) +load( + "@envoy_api//bazel:api_build_system.bzl", + "api_proto_package", +) +load( + "@envoy//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +api_proto_package( + deps = [ + "@envoy_api//envoy/extensions/transport_sockets/tls/v3:pkg", + ], +) + +# :native contains code that does not (directly or indirectly) interact with Go +# This library is referenced by the Go code (so that we avoid circular deps) +envoy_cc_library( + name = "native", + srcs = [ + "filter-native.cc", + "native.cc", + "span-group.cc", + ], + hdrs = [ + "filter.h", + "cgo-proxy.h", + "span-group.h", + ], + repository = "@envoy", + deps = [ + ":pkg_cc_proto", + "@envoy//source/exe:envoy_common_lib", + ], +) + +# :cgo contains code that directly or indirectly interacts with Go, or code +# that is only needed by such code. Don't reference this from the Go packages +# as this may create circular dependencies. +envoy_cc_library( + name = "cgo", + srcs = [ + "filter-cgo.cc", + "cgo-proxy.cc", + ], + repository = "@envoy", + deps = [ + ":native", + "//ego/src/go/internal/cgo:cgo.cc", + "//ego/src/cc/goc:goc", + ], +) + +# :goc contains code that is called from Go ("downcalls"). We need to separate +# this from the code that calls Go ("upcalls") in order to avoid circular deps. +# There is one circle here which is created by a downcall that needs to trigger +# an upcall. This has been decoupled for the compiler by wrapping the upcall in +# a virtual function. +envoy_cc_library( + name = "goc", + srcs = [ + "filter-goc.cc", + ], + hdrs = [ + "filter.h", + ], + repository = "@envoy", + deps = [ + ":native", + # ":cgo", we're not declaring post->onPost dependency, but that's fine. + ":pkg_cc_proto", + "@envoy//source/exe:envoy_common_lib", + ], +) + +# :factory contains everything needed by the filter factory. Typically pulled +# in by top-level packages +envoy_cc_library( + name = "factory", + srcs = ["factory.cc"], + repository = "@envoy", + deps = [ + ":cgo", + ":native", + "@envoy//include/envoy/server:filter_config_interface", + ], +) + +# :integration_test can be used to conveniently run the integration tests: +# `bazel test //src/cc/filter/http/getheader:integration_test` +# envoy_cc_test( +# name = "integration_test", +# srcs = ["integration_test.cc"], +# repository = "@envoy", +# deps = [ +# ":factory", +# ":native", +# "@envoy//test/integration:http_integration_lib", +# ], +# ) diff --git a/ego/src/cc/filter/http/cgo-proxy.cc b/ego/src/cc/filter/http/cgo-proxy.cc new file mode 100644 index 0000000..6dd7470 --- /dev/null +++ b/ego/src/cc/filter/http/cgo-proxy.cc @@ -0,0 +1,55 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include "cgo-proxy.h" + +#include "ego/src/cc/goc/goc.h" +#include "ego/src/go/internal/cgo/cgo.h" + +namespace Envoy { +namespace Http { + +CgoProxyImpl::CgoProxyImpl() = default; + +CgoProxyImpl::~CgoProxyImpl() = default; + +unsigned long long CgoProxyImpl::GoHttpFilterCreate(void* native, unsigned long long factory_tag, + unsigned long long filter_slot) { + return Cgo_GoHttpFilter_Create(native, factory_tag, filter_slot); +} + +void CgoProxyImpl::GoHttpFilterOnDestroy(unsigned long long filter_tag) { + Cgo_GoHttpFilter_OnDestroy(filter_tag); +} + +long long CgoProxyImpl::GoHttpFilterDecodeHeaders(unsigned long long filter_tag, void* headers, + int end_stream) { + return Cgo_GoHttpFilter_DecodeHeaders(filter_tag, headers, end_stream); +} + +long long CgoProxyImpl::GoHttpFilterDecodeData(unsigned long long filter_tag, void* buffer, + int end_stream) { + return Cgo_GoHttpFilter_DecodeData(filter_tag, buffer, end_stream); +} + +long long CgoProxyImpl::GoHttpFilterDecodeTrailers(unsigned long long filter_tag, void* trailers) { + return Cgo_GoHttpFilter_DecodeTrailers(filter_tag, trailers); +} + +long long CgoProxyImpl::GoHttpFilterEncodeHeaders(unsigned long long filter_tag, void* headers, + int end_stream) { + return Cgo_GoHttpFilter_EncodeHeaders(filter_tag, headers, end_stream); +} + +long long CgoProxyImpl::GoHttpFilterEncodeData(unsigned long long filter_tag, void* buffer, + int end_stream) { + return Cgo_GoHttpFilter_EncodeData(filter_tag, buffer, end_stream); +} + +void CgoProxyImpl::GoHttpFilterOnPost(unsigned long long filter_tag, unsigned long long post_tag) { + return Cgo_GoHttpFilter_OnPost(filter_tag, post_tag); +} +} // namespace Http +} // namespace Envoy \ No newline at end of file diff --git a/ego/src/cc/filter/http/cgo-proxy.h b/ego/src/cc/filter/http/cgo-proxy.h new file mode 100644 index 0000000..2c99df7 --- /dev/null +++ b/ego/src/cc/filter/http/cgo-proxy.h @@ -0,0 +1,53 @@ +#pragma once +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include + +namespace Envoy { +namespace Http { + +class CgoProxy { +public: + virtual ~CgoProxy(); + + virtual unsigned long long GoHttpFilterCreate(void* native, unsigned long long factory_tag, + unsigned long long filter_slot) = 0; + virtual void GoHttpFilterOnDestroy(unsigned long long filter_tag) = 0; + virtual long long GoHttpFilterDecodeHeaders(unsigned long long filter_tag, void* headers, + int end_stream) = 0; + virtual long long GoHttpFilterDecodeData(unsigned long long filter_tag, void* buffer, + int end_stream) = 0; + virtual long long GoHttpFilterDecodeTrailers(unsigned long long filter_tag, void* trailers) = 0; + virtual long long GoHttpFilterEncodeHeaders(unsigned long long filter_tag, void* headers, + int end_stream) = 0; + virtual long long GoHttpFilterEncodeData(unsigned long long filter_tag, void* headers, + int end_stream) = 0; + virtual void GoHttpFilterOnPost(unsigned long long filter_tag, unsigned long long post_tag) = 0; +}; + +class CgoProxyImpl : public CgoProxy { +public: + CgoProxyImpl(); + ~CgoProxyImpl() override; + + unsigned long long GoHttpFilterCreate(void* native, unsigned long long factory_tag, + unsigned long long filter_slot) override; + void GoHttpFilterOnDestroy(unsigned long long filter_tag) override; + long long GoHttpFilterDecodeHeaders(unsigned long long filter_tag, void* headers, + int end_stream) override; + long long GoHttpFilterDecodeData(unsigned long long filter_tag, void* buffer, + int end_stream) override; + long long GoHttpFilterDecodeTrailers(unsigned long long filter_tag, void* trailers) override; + long long GoHttpFilterEncodeHeaders(unsigned long long filter_tag, void* headers, + int end_stream) override; + long long GoHttpFilterEncodeData(unsigned long long filter_tag, void* headers, + int end_stream) override; + void GoHttpFilterOnPost(unsigned long long filter_tag, unsigned long long post_tag) override; +}; + +using CgoProxyPtr = std::shared_ptr; +} // namespace Http +} // namespace Envoy \ No newline at end of file diff --git a/ego/src/cc/filter/http/factory.cc b/ego/src/cc/filter/http/factory.cc new file mode 100644 index 0000000..2a111e6 --- /dev/null +++ b/ego/src/cc/filter/http/factory.cc @@ -0,0 +1,92 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include + +#include "envoy/registry/registry.h" + +#include "extensions/filters/http/common/factory_base.h" + +#include "ego/src/cc/filter/http/filter.pb.validate.h" +#include "filter.h" + +namespace Envoy { +namespace Server { +namespace Configuration { + +// GoHttpFilterCf is the config factory registered with envoy core +// +class GoHttpFilterCf + : public Envoy::Extensions::HttpFilters::Common::FactoryBase { +public: + GoHttpFilterCf() : FactoryBase(Http::GoHttpConstants::get().FilterName) {} + + Http::FilterFactoryCb + createFilterFactoryFromProtoTyped(const ego::http::Settings& settings, + const std::string& stats_prefix, + Server::Configuration::FactoryContext& context) { + + auto cfg = std::make_shared( + settings, + context.scope().createScope(fmt::format( + "{}{}.{}.", stats_prefix, Http::GoHttpConstants::get().FilterName, settings.filter()))); + + Secret::GenericSecretConfigProviderSharedPtr secret_provider = nullptr; + + // Seems like it's possible to hit a pure virtual call when calling + // Envoy::Secret::SecretManagerImpl::findOrCreateGenericSecretProvider during filter + // construction with a file-based SDS secret: https://github.com/envoyproxy/envoy/issues/12013 + // So if we want to config static resource then setting it via static_resources, instead of + // sds_config: + // path: /etc/envoy/secret-resource.yaml + + // A filter can be configured without secret + if (settings.has_sds_secret_config()) { + // Follow this config logic + // https://github.com/envoyproxy/envoy/blob/v1.14.1/source/extensions/transport_sockets/tls/context_config_impl.cc#L61 + // For working with static resource + if (settings.sds_secret_config().has_sds_config()) { + secret_provider = + context.clusterManager() + .clusterManagerFactory() + .secretManager() + .findOrCreateGenericSecretProvider(settings.sds_secret_config().sds_config(), + settings.sds_secret_config().name(), + context.getTransportSocketFactoryContext()); + } else { + secret_provider = context.clusterManager() + .clusterManagerFactory() + .secretManager() + .findStaticGenericSecretProvider(settings.sds_secret_config().name()); + } + } + + auto cgo_proxy = std::make_shared(); + + return [cfg, &context, secret_provider, + cgo_proxy](Http::FilterChainFactoryCallbacks& callbacks) -> void { + auto span_group = std::make_unique(); + auto filter = new Http::GoHttpFilter(cfg, context.api(), secret_provider, cgo_proxy, std::move(span_group)); + callbacks.addStreamFilter(filter->ref()); + }; + } + + Router::RouteSpecificFilterConfigConstSharedPtr + createRouteSpecificFilterConfigTyped(const ego::http::SettingsPerRoute& settings, + Server::Configuration::ServerFactoryContext&, + ProtobufMessage::ValidationVisitor&) { + return std::make_shared(settings); + } +}; + +/** + * Static registration for the GoHttp filter. @see RegisterFactory + */ +REGISTER_FACTORY(GoHttpFilterCf, Server::Configuration::NamedHttpFilterConfigFactory); + +} // namespace Configuration +} // namespace Server +} // namespace Envoy diff --git a/ego/src/cc/filter/http/filter-cgo.cc b/ego/src/cc/filter/http/filter-cgo.cc new file mode 100644 index 0000000..b773368 --- /dev/null +++ b/ego/src/cc/filter/http/filter-cgo.cc @@ -0,0 +1,218 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + + +// This file contains upcall proxies to the Go filter implementation. Please +// avoid having too much custom logic in here: the intent is to handle as much +// of the filter logic as possible in Go! + +#include "common/common/lock_guard.h" + +#include "ego/src/cc/goc/goc.h" +#include "ego/src/go/internal/cgo/cgo.h" +#include "filter.h" + +namespace Envoy { +namespace Http { + +static thread_local struct CgoHttpFilterFactorySlot_ { + uint64_t value; + ~CgoHttpFilterFactorySlot_() { Cgo_ReleaseHttpFilterFactorySlot(value); } +} cgoHttpFilterFactorySlot{Cgo_AcquireHttpFilterFactorySlot()}; + +GoHttpFilterConfig::GoHttpFilterConfig(const ego::http::Settings& proto, + const Stats::ScopeSharedPtr& scope) + : cgoSlot_(cgoHttpFilterFactorySlot.value), filter_(proto.filter()), scope_(scope) { + auto filter = proto.filter(); + auto settings = proto.settings().value(); + cgoTag_ = Cgo_GoHttpFilterFactory_Create(cgoSlot_, const_cast(filter.c_str()), + filter.size(), const_cast(settings.c_str()), + settings.size(), scope.get()); + + if (cgoTag_ == 0) { + auto errMsg = std::string( + "invoke Cgo_GoHttpFilterFactory_Create failed, check error from factory for detail"); + if (proto.crash_on_errors()) { + throw EnvoyException(errMsg); + } else { + ENVOY_LOG(error, "[ego_http][{}] {}", proto.filter(), errMsg); + } + } +} + +inline bool GoHttpFilterConfig::cgoSafe() { return cgoSlot_ == cgoHttpFilterFactorySlot.value; } + +GoHttpFilterConfig::~GoHttpFilterConfig() { + ASSERT(cgoSafe()); + Cgo_GoHttpFilterFactory_OnDestroy(cgoTag_); +} + +static thread_local struct CgoHttpRouteSpecificFilterConfigSlot_ { + uint64_t value; + ~CgoHttpRouteSpecificFilterConfigSlot_() { Cgo_ReleaseRouteSpecificFilterConfigSlot(value); } +} cgoHttpRouteSpecificFilterConfigSlot_{Cgo_AcquireRouteSpecificFilterConfigSlot()}; + +GoHttpRouteSpecificFilterConfig::GoHttpRouteSpecificFilterConfig( + const ego::http::SettingsPerRoute& proto) + : cgoSlot_(cgoHttpRouteSpecificFilterConfigSlot_.value) { + + // onDestroy_ is called in destructor to clean up resources at Go side. + // this is a work-around for issue that destructor of RouteSpecificFilterConfig is a virtual + // functions. Because of that, the destructor can't be implemented in this file. + onDestroy_ = [this]() { + ASSERT(this->cgoSafe()); + for (const auto& it : this->filters_) { + Cgo_RouteSpecificFilterConfig_dtor(it.second); + } + }; + for (const auto& it : proto.filters()) { + auto filterName = it.first; + auto filterConfig = it.second; + auto cgoTag = Cgo_RouteSpecificFilterConfig_Create( + cgoSlot_, const_cast(filterName.c_str()), filterName.size(), + const_cast(filterConfig.value().c_str()), filterConfig.value().size()); + // handle invalid filter name or configuration + ASSERT(0 != cgoTag); + + filters_.insert(std::pair(filterName, cgoTag)); + } +} + +inline bool GoHttpRouteSpecificFilterConfig::cgoSafe() { + return cgoSlot_ == cgoHttpRouteSpecificFilterConfigSlot_.value; +} + +static thread_local struct CgoHttpFilterSlot_ { + uint64_t value; + ~CgoHttpFilterSlot_() { Cgo_ReleaseHttpFilterSlot(value); } +} cgoHttpFilterSlot{Cgo_AcquireHttpFilterSlot()}; + +GoHttpFilter::GoHttpFilter(std::shared_ptr config, Api::Api& api, + Secret::GenericSecretConfigProviderSharedPtr secret_provider, + CgoProxyPtr cgo_proxy, SpanGroupPtr span_group) + : config_(config), decoderCallbacks_(0), encoderCallbacks_(0), dispatcher_(0), pins_(1), + self_(this), api_(api), secret_provider_(secret_provider), cgo_proxy_(cgo_proxy), span_group_(std::move(span_group)) { + cgoSlot_ = cgoHttpFilterSlot.value; + cgoTag_ = cgo_proxy_->GoHttpFilterCreate(this, config->cgoTag_, cgoSlot_); + // cgoTag_ == 0 means can not create a instance of filter on Go-side + // we should sendLocalReply here but we don't have decodeCallbacks now + // Let handle it on setDecoderFilterCallbacks +}; + +inline bool GoHttpFilter::cgoSafe() { + return (cgoSlot_ == cgoHttpFilterSlot.value) && dispatcher_ && decoderCallbacks_ && + encoderCallbacks_; +} + +FilterHeadersStatus GoHttpFilter::decodeHeaders(RequestHeaderMap& headers, bool end_stream) { + ASSERT(cgoSafe()); + // Get x-request-id for logging + if (headers.RequestId() != nullptr && x_request_id_ == "") { + x_request_id_ = headers.RequestId()->value().getStringView(); + } + + return Goc_FilterHeadersStatus( + cgo_proxy_->GoHttpFilterDecodeHeaders(cgoTag_, &headers, end_stream ? 1 : 0)); +} + +FilterDataStatus GoHttpFilter::decodeData(Buffer::Instance& buffer, bool end_stream) { + ASSERT(cgoSafe()); + return Goc_FilterDataStatus( + cgo_proxy_->GoHttpFilterDecodeData(cgoTag_, &buffer, end_stream ? 1 : 0)); +} + +FilterTrailersStatus GoHttpFilter::decodeTrailers(RequestTrailerMap& trailers) { + ASSERT(cgoSafe()); + return Goc_FilterTrailersStatus(cgo_proxy_->GoHttpFilterDecodeTrailers(cgoTag_, &trailers)); +} + +FilterHeadersStatus GoHttpFilter::encode100ContinueHeaders(ResponseHeaderMap&) { + // TODO: call to Go + return Envoy::Http::FilterHeadersStatus::Continue; +} + +FilterHeadersStatus GoHttpFilter::encodeHeaders(ResponseHeaderMap& headers, bool end_stream) { + ASSERT(cgoSafe()); + return Goc_FilterHeadersStatus( + cgo_proxy_->GoHttpFilterEncodeHeaders(cgoTag_, &headers, end_stream ? 1 : 0)); +} + +FilterDataStatus GoHttpFilter::encodeData(Buffer::Instance& buffer, bool end_stream) { + ASSERT(cgoSafe()); + + return Goc_FilterDataStatus( + cgo_proxy_->GoHttpFilterEncodeData(cgoTag_, &buffer, end_stream ? 1 : 0)); +} + +FilterTrailersStatus GoHttpFilter::encodeTrailers(ResponseTrailerMap&) { + // TODO: call to Go + return Envoy::Http::FilterTrailersStatus::Continue; +} + +FilterMetadataStatus GoHttpFilter::encodeMetadata(MetadataMap&) { + // TODO: call to Go + return Envoy::Http::FilterMetadataStatus::Continue; +} + +void GoHttpFilter::encodeComplete() { + // TODO: call to Go +} + +void GoHttpFilter::onDestroy() { + ASSERT(cgoSafe()); + + // best effort to terminate go-routines and other asynchronous activities + cgo_proxy_->GoHttpFilterOnDestroy(cgoTag_); + cgoTag_ = 0; + cgoSlot_ = 0; + + ASSERT(0 < pins_.load()); + if (0 < --pins_) { + // We're not the last ones to go, so we need to wait until all activity on + // this filter has ended. This important to implement the contract for + // StreamDecoderFilter::onDestroy, because callbacks->dispatcher().post() + // is used to trigger callbacks from go-routines, and we have promised that + // "Callbacks will not be invoked by the filter after onDestroy() is + // called.". Moreover, "Every filter is responsible for making sure that any + // async events are cleaned up in the context of this routine. This includes + // timers, network calls, etc. [...] Filters must not invoke either encoder + // or decoder filter callbacks after having onDestroy() invoked." + // + // NOTE: Already scheduled callbacks will be inhibited by remembering if + // onDestroy() was called (e.g., "if (callbacks) cb();"). However, this + // complicates reasoning at the scheduling site, so the decision to abort + // is left to the callback. + + // wait for the ultimate unpin() + Thread::LockGuard lk_m(m_); + while (pins_.load()) + // CondVar::wait() does not throw, so it's safe to pass the mutex rather than the guard. + cv_.wait(m_); + } + + // release self_. This is safe now, because if there were concurrent calls to + // post(), pins_ shouldn't be zero. + self_.reset(); + + // Fret not: we are still alive! Someone called onDestroy(), after all... + + // bluntly ensure there is no more asynchronous access + dispatcher_ = 0; + decoderCallbacks_ = 0; + encoderCallbacks_ = 0; +} + +void GoHttpFilter::onPost(uint64_t postTag) { + if (!cgoTag_) { + // TODO: Log dropped onPost() + return; + } + + ASSERT(cgoSafe()); + cgo_proxy_->GoHttpFilterOnPost(cgoTag_, postTag); +} + +} // namespace Http +} // namespace Envoy diff --git a/ego/src/cc/filter/http/filter-goc.cc b/ego/src/cc/filter/http/filter-goc.cc new file mode 100644 index 0000000..8df3e95 --- /dev/null +++ b/ego/src/cc/filter/http/filter-goc.cc @@ -0,0 +1,134 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include "common/common/lock_guard.h" +#include "common/http/utility.h" + +#include "filter.h" + +namespace Envoy { +namespace Http { + +void GoHttpFilter::pin() { + ASSERT(0 < pins_.load()); + + pins_++; +} + +void GoHttpFilter::unpin() { + ASSERT(0 < pins_.load()); + + if (0 == --pins_) { + + // If we got here, onDestroy() must already be waiting for this! + + // "Even if the shared variable is atomic, it must be modified under the + // mutex in order to correctly publish the modification to the waiting + // thread." [https://en.cppreference.com/w/cpp/thread/condition_variable] + Envoy::Thread::LockGuard lk_m(m_); + + // notify onDestroy() + cv_.notifyOne(); + } +} + +void GoHttpFilter::post(uint64_t tag) { + ASSERT(0 < pins_.load()); + + // ref() and dispatcher_ are guarded by 0 < pins_ + dispatcher_->post([this, tag, keepalive = ref()]() { onPost(tag); }); +} + +void GoHttpFilter::log(uint32_t level, absl::string_view message) { + switch (static_cast(level)) { + case spdlog::level::trace: + ENVOY_LOG(trace, "[ego_http][{}] [{}] {}", config_->filter(), x_request_id_, message); + return; + case spdlog::level::debug: + ENVOY_LOG(debug, "[ego_http][{}] [{}] {}", config_->filter(), x_request_id_, message); + return; + case spdlog::level::info: + ENVOY_LOG(info, "[ego_http][{}] [{}] {}", config_->filter(), x_request_id_, message); + return; + case spdlog::level::warn: + ENVOY_LOG(warn, "[ego_http][{}] [{}] {}", config_->filter(), x_request_id_, message); + return; + case spdlog::level::err: + ENVOY_LOG(error, "[ego_http][{}] [{}] {}", config_->filter(), x_request_id_, message); + return; + case spdlog::level::critical: + ENVOY_LOG(critical, "[ego_http][{}] [{}] {}", config_->filter(), x_request_id_, message); + return; + case spdlog::level::off: + return; + } + ENVOY_LOG(warn, "[ego_http][{}] [{}] UNDEFINED LOG LEVEL {}: {}", config_->filter(), + x_request_id_, level, message); +} + +StreamDecoderFilterCallbacks* GoHttpFilter::decoderCallbacks() { + ASSERT(0 != decoderCallbacks_); + return decoderCallbacks_; +} + +StreamEncoderFilterCallbacks* GoHttpFilter::encoderCallbacks() { + ASSERT(0 != encoderCallbacks_); + return encoderCallbacks_; +} + +StreamFilterCallbacks* GoHttpFilter::streamFilterCallbacks(int encoder) { + if(encoder != 0) { + return encoderCallbacks(); + } + return decoderCallbacks(); +} + +Secret::GenericSecretConfigProviderSharedPtr GoHttpFilter::genericSecretConfigProvider() { + return secret_provider_; +} + +Api::Api& GoHttpFilter::api() { return api_; } + +uint64_t GoHttpFilter::resolveMostSpecificPerGoFilterConfigTag() { + if (decoderCallbacks_ == nullptr) { + return 0; + } + + // decoderCallbacks_ is set when requests start. + // and it's safe to use until onDestroy is called based on envoy/include/envoy/http/filter.h + // Therefore, resolveMostSpecificPerGoFilterConfigTag can be called in Encode* method. + auto route = decoderCallbacks_->route(); + if (route == nullptr || route->routeEntry() == nullptr) { + return 0; + } + + const auto* config = + Http::Utility::resolveMostSpecificPerFilterConfig( + GoHttpConstants::get().FilterName, route); + + if (config == nullptr) { + return 0; + } + + return config->cgoTag(config_->filter()); +} + + +intptr_t GoHttpFilter::spawnChildSpan(const intptr_t parent_span_id, std::string &name) { + return span_group_->spawnChildSpan(parent_span_id, name); +} + +Envoy::Tracing::Span& GoHttpFilter::getSpan(const intptr_t span_id) { + return span_group_->getSpan(span_id); +} + +void GoHttpFilter::finishSpan(const intptr_t span_id) { + span_group_->getSpan(span_id).finishSpan(); + span_group_->removeSpan(span_id); +} + + +} // namespace Http +} // namespace Envoy \ No newline at end of file diff --git a/ego/src/cc/filter/http/filter-native.cc b/ego/src/cc/filter/http/filter-native.cc new file mode 100644 index 0000000..8269b58 --- /dev/null +++ b/ego/src/cc/filter/http/filter-native.cc @@ -0,0 +1,48 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include "common/common/empty_string.h" + +#include "filter.h" + +namespace Envoy { +namespace Http { + +void GoHttpFilter::setDecoderFilterCallbacks(StreamDecoderFilterCallbacks& callbacks) { + ASSERT(0 < pins_.load()); + ASSERT(0 == decoderCallbacks_); + // Assuming dispatcher is shared between callbacks + // https://github.com/envoyproxy/envoy/blob/master/include/envoy/thread_local/thread_local.h + ASSERT(nullptr == dispatcher_); + + decoderCallbacks_ = &callbacks; + dispatcher_ = &callbacks.dispatcher(); + + // Handle logic can not create fitler on Go-side + if (cgoTag_ == 0) { + decoderCallbacks_->sendLocalReply(Code::InternalServerError, EMPTY_STRING, nullptr, + absl::nullopt, EMPTY_STRING); + } +} + +void GoHttpFilter::setEncoderFilterCallbacks(StreamEncoderFilterCallbacks& callbacks) { + ASSERT(0 < pins_.load()); + ASSERT(0 == encoderCallbacks_); + + encoderCallbacks_ = &callbacks; +} + +uint64_t GoHttpRouteSpecificFilterConfig::cgoTag(std::string filterName) const { + auto it = filters_.find(filterName); + if (it != filters_.cend()) { + return it->second; + } + return 0; +} + +GoHttpRouteSpecificFilterConfig::~GoHttpRouteSpecificFilterConfig() { onDestroy_(); } + +} // namespace Http +} // namespace Envoy \ No newline at end of file diff --git a/ego/src/cc/filter/http/filter.h b/ego/src/cc/filter/http/filter.h new file mode 100644 index 0000000..176e7b7 --- /dev/null +++ b/ego/src/cc/filter/http/filter.h @@ -0,0 +1,230 @@ +#pragma once +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#ifndef FILTER_HTTP_GETHEADER_FILTER_H +#define FILTER_HTTP_GETHEADER_FILTER_H + +#include + +#include "envoy/server/filter_config.h" +#include "envoy/stats/scope.h" + +#include "common/common/thread.h" +#include "common/singleton/const_singleton.h" + +#include "cgo-proxy.h" +#include "ego/src/cc/filter/http/filter.pb.h" +#include "span-group.h" + +namespace Envoy { +namespace Http { + +struct GoHttpConstantValues { + const std::string FilterName = "ego_http"; +}; + +using GoHttpConstants = ConstSingleton; + +// This class represents the proto configuration declared in filter.proto +// +class GoHttpFilterConfig : public Logger::Loggable { +public: + GoHttpFilterConfig(const ego::http::Settings& proto, const Stats::ScopeSharedPtr& scope); + ~GoHttpFilterConfig(); + +private: + friend class GoHttpFilter; + uint64_t cgoTag_; + + uint64_t cgoSlot_; + inline bool cgoSafe(); + + const std::string filter_; + std::string filter() const { return filter_; } + + // hold the scope_ for using from go side + Stats::ScopeSharedPtr scope_; +}; + +class GoHttpRouteSpecificFilterConfig : public Router::RouteSpecificFilterConfig { +public: + GoHttpRouteSpecificFilterConfig(const ego::http::SettingsPerRoute& config); + ~GoHttpRouteSpecificFilterConfig(); + + uint64_t cgoTag(std::string name) const; + +private: + uint64_t cgoSlot_; + inline bool cgoSafe(); + + std::map filters_; + std::function onDestroy_; +}; + +// This class implements the actual filter logic +// +class GoHttpFilter : public StreamFilter, public Logger::Loggable { +public: + GoHttpFilter(std::shared_ptr config, Api::Api& api, + Secret::GenericSecretConfigProviderSharedPtr secret_provider, CgoProxyPtr cgo_proxy, SpanGroupPtr span_group); + ~GoHttpFilter() override{}; + + Http::StreamFilterSharedPtr ref() { return self_; } + void pin(); + void unpin(); + void post(uint64_t tag); + void log(uint32_t level, absl::string_view message); + + // Public decoderCallbacks_ to let GOC calling to continueDecoding, sendLocalReply, ... + StreamDecoderFilterCallbacks* decoderCallbacks(); + + // Public encoderCallbacks_ to let GOC calling to continueEncoding, ... + StreamEncoderFilterCallbacks* encoderCallbacks(); + + StreamFilterCallbacks* streamFilterCallbacks(int encoder); + + // Public interface to access secret provider from Go-side + Secret::GenericSecretConfigProviderSharedPtr genericSecretConfigProvider(); + Api::Api& api(); + std::string secret_holder; + +public: + // Http::StreamFilterBase + void onDestroy() override; + + // Http::StreamDecoderFilter + FilterHeadersStatus decodeHeaders(RequestHeaderMap&, bool) override; + FilterDataStatus decodeData(Buffer::Instance&, bool) override; + FilterTrailersStatus decodeTrailers(RequestTrailerMap&) override; + void setDecoderFilterCallbacks(StreamDecoderFilterCallbacks&) override; + + // Http::StreamEncoderFilter + + /** + * Called with 100-continue headers. + * + * This is not folded into encodeHeaders because most Envoy users and filters + * will not be proxying 100-continue and with it split out, can ignore the + * complexity of multiple encodeHeaders calls. + * + * @param headers supplies the 100-continue response headers to be encoded. + * @return FilterHeadersStatus determines how filter chain iteration proceeds. + * + */ + FilterHeadersStatus encode100ContinueHeaders(ResponseHeaderMap& headers) override; + + /** + * Called with headers to be encoded, optionally indicating end of stream. + * @param headers supplies the headers to be encoded. + * @param end_stream supplies whether this is a header only request/response. + * @return FilterHeadersStatus determines how filter chain iteration proceeds. + */ + FilterHeadersStatus encodeHeaders(ResponseHeaderMap& headers, bool end_stream) override; + + /** + * Called with data to be encoded, optionally indicating end of stream. + * @param data supplies the data to be encoded. + * @param end_stream supplies whether this is the last data frame. + * @return FilterDataStatus determines how filter chain iteration proceeds. + */ + FilterDataStatus encodeData(Buffer::Instance& data, bool end_stream) override; + + /** + * Called with trailers to be encoded, implicitly ending the stream. + * @param trailers supplies the trailers to be encoded. + */ + FilterTrailersStatus encodeTrailers(ResponseTrailerMap& trailers) override; + + /** + * Called with metadata to be encoded. New metadata should be added directly to metadata_map. DO + * NOT call StreamDecoderFilterCallbacks::encodeMetadata() interface to add new metadata. + * + * @param metadata_map supplies the metadata to be encoded. + * @return FilterMetadataStatus, which currently is always FilterMetadataStatus::Continue; + */ + FilterMetadataStatus encodeMetadata(MetadataMap& metadata_map) override; + + /** + * Called by the filter manager once to initialize the filter callbacks that the filter should + * use. Callbacks will not be invoked by the filter after onDestroy() is called. + */ + void setEncoderFilterCallbacks(StreamEncoderFilterCallbacks& callbacks) override; + + /** + * Called at the end of the stream, when all data has been encoded. + */ + void encodeComplete() override; + + // gets route specific filter config cgo tag. + uint64_t resolveMostSpecificPerGoFilterConfigTag(); + + intptr_t spawnChildSpan(const intptr_t parent_span_id, std::string &name); + void finishSpan(const intptr_t span_id); + Envoy::Tracing::Span& getSpan(const intptr_t span_id); + + +private: + // config containing the few bits interesting on the C++ side of things + const std::shared_ptr config_; + + // Do only access from dispatcher context. Do check if non-0 before use. + StreamDecoderFilterCallbacks* decoderCallbacks_; + + // Do only access from dispatcher context. Do check if non-0 before use. + StreamEncoderFilterCallbacks* encoderCallbacks_; + + // Do only access from dispatcher context and for calling post(). + // Do check if non-0 before use. + Event::Dispatcher* dispatcher_; + + // the ID of the Go filter object kept alive by the clutch kludge. + uint64_t cgoTag_; + + // ref counting state for asynchronous requests. We trust this is upped + // before every go-routine start and decreased every time a filter go + // routine returns. + std::atomic pins_; + + // This one is only needed for keeping the filter object alive in case of + // scheduled post() callbacks. The ref counter is shared with the filter + // factory's call to addStreamDecoderFilter(). + Http::StreamFilterSharedPtr self_; + + // C++11 semaphore surrogate + Thread::MutexBasicLockable m_; + Thread::CondVar cv_; + void done(); + + // onPost is virtual to work around dependency cycles: the implementation + // performs an upcall to a Go function, but it also needs to be referenced + // by post() when it schedules the callback. post() is invoked via a + // downcall from Go, and thus, the Go library becomes a cyclic dependency. + // Making onPost virtual relaxes this because now, post only needs to know + // the location of onPost in the virtual function table, which can be + // determined based on the header file only. + virtual void onPost(uint64_t tag); + + // we're still learning, so better check twice + uint64_t cgoSlot_; + inline bool cgoSafe(); + + // hold the secret provider on C-side + // and provide a interface to access secret from Go-side + Api::Api& api_; + Secret::GenericSecretConfigProviderSharedPtr secret_provider_; + + // private x-request-id for logging + absl::string_view x_request_id_ = ""; + + CgoProxyPtr cgo_proxy_; + + SpanGroupPtr span_group_; +}; + +} // namespace Http +} // namespace Envoy + +#endif // FILTER_HTTP_GETHEADER_FILTER_H diff --git a/ego/src/cc/filter/http/filter.proto b/ego/src/cc/filter/http/filter.proto new file mode 100644 index 0000000..a61cb8f --- /dev/null +++ b/ego/src/cc/filter/http/filter.proto @@ -0,0 +1,61 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +syntax = "proto3"; + +package ego.http; + +import "validate/validate.proto"; +import "google/protobuf/any.proto"; +import "envoy/extensions/transport_sockets/tls/v3/cert.proto"; + +message Settings { + + // The filter name used with ego.RegisterHttpFilter + string filter = 1 [(validate.rules).string.min_bytes = 1]; + + // For bools, the default value is false. so if it available it should be true + // And it will be generated with crash_on_errors = true in envoy.yaml for + // testing, canary deploy to let Envoy crash by throwing an exception + bool crash_on_errors = 2; + + // An Any that must match the structure expected by the respective filter. + // usually annotated with a @type attribute to avoid accidents. + google.protobuf.Any settings = 3; + + // We support access to SDS via the envoy runtime, so here is where the + // coordinates for the secrets needed by the filter can be configured. See + // https://github.com/envoyproxy/envoy/blob/v1.14.1/api/envoy/api/v2/auth/cert.proto + // (message SdsSecretConfig) for more information. + // + // Example 1: remote SDS + // --- + // sds_config: + // api_config_source: + // api_type: GRPC + // grpc_services: + // envoy_grpc: + // cluster_name: sds_server_uds + // + // Example 2: static file; this can not be configured via dynamic resources + // --- + // sds_config: + // path: ./test/secret-resource.yaml + // + // Example 3: static resources, leave sds_config empty + // --- + // static_resources: + // secrets: + // - name: "/ego-demo/v1/secret" + // generic_secret: + // secret: + // inline_string: + // + envoy.extensions.transport_sockets.tls.v3.SdsSecretConfig sds_secret_config = 4; +} + +message SettingsPerRoute { + map filters = 1; +} \ No newline at end of file diff --git a/ego/src/cc/filter/http/integration_test.cc b/ego/src/cc/filter/http/integration_test.cc new file mode 100644 index 0000000..12329d5 --- /dev/null +++ b/ego/src/cc/filter/http/integration_test.cc @@ -0,0 +1,51 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include "test/integration/http_integration.h" + +namespace Envoy { +class HttpFilterSampleIntegrationTest : public HttpIntegrationTest, + public testing::TestWithParam { +public: + HttpFilterSampleIntegrationTest() + : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, GetParam(), realTime()) {} + /** + * Initializer for an individual integration test. + */ + void SetUp() override { initialize(); } + + void initialize() override { + config_helper_.addFilter("{ name: get-header, config: { key: add-header-key, src: " + "'https://google.com', hdr: add-header-value } }"); + HttpIntegrationTest::initialize(); + } +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, HttpFilterSampleIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest())); + +TEST_P(HttpFilterSampleIntegrationTest, Test1) { + Http::TestRequestHeaderMapImpl headers{ + {":method", "GET"}, {":path", "/"}, {":authority", "host"}}; + + IntegrationCodecClientPtr codec_client; + FakeHttpConnectionPtr fake_upstream_connection; + FakeStreamPtr request_stream; + + codec_client = makeHttpConnection(lookupPort("http")); + auto response = codec_client->makeHeaderOnlyRequest(headers); + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection)); + ASSERT_TRUE(fake_upstream_connection->waitForNewStream(*dispatcher_, request_stream)); + ASSERT_TRUE(request_stream->waitForEndStream(*dispatcher_)); + response->waitForEndStream(); + + EXPECT_EQ("add-header-value", request_stream->headers() + .get(Http::LowerCaseString("add-header-key")) + ->value() + .getStringView()); + + codec_client->close(); +} +} // namespace Envoy diff --git a/ego/src/cc/filter/http/native.cc b/ego/src/cc/filter/http/native.cc new file mode 100644 index 0000000..328ab5f --- /dev/null +++ b/ego/src/cc/filter/http/native.cc @@ -0,0 +1,12 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include "cgo-proxy.h" + +namespace Envoy { +namespace Http { +CgoProxy::~CgoProxy() = default; +} +} // namespace Envoy \ No newline at end of file diff --git a/ego/src/cc/filter/http/span-group.cc b/ego/src/cc/filter/http/span-group.cc new file mode 100644 index 0000000..ee31ed6 --- /dev/null +++ b/ego/src/cc/filter/http/span-group.cc @@ -0,0 +1,69 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include "span-group.h" +#include "common/tracing/http_tracer_impl.h" + +namespace Envoy { +namespace Http { + + SpanGroup::SpanGroup():decoderCallbacks_(nullptr),encoderCallbacks_(nullptr){} + + intptr_t SpanGroup::spawnChildSpan(const intptr_t parent_span_id, std::string &name) { + auto span = getSpan(parent_span_id).spawnChild(Envoy::Tracing::EgressConfig::get(), name, decoderCallbacks_->dispatcher().timeSource().systemTime()); + auto id = reinterpret_cast(span.get()); + setSpan(id, std::move(span)); + return id; + } + + void SpanGroup::setSpan(const intptr_t id, Envoy::Tracing::SpanPtr span){ + std::lock_guard guard(spans_mutex); + spans_[id] = std::move(span); + } + + Envoy::Tracing::Span& SpanGroup::getSpan_(const intptr_t span_id){ + std::lock_guard guard(spans_mutex); + auto search = spans_.find(span_id); + + if (search != spans_.end()) { + return *search->second; + } + + return Envoy::Tracing::NullSpan::instance(); + } + + void SpanGroup::removeSpan(const intptr_t span_id){ + std::lock_guard guard(spans_mutex); + spans_.erase(span_id); + } + + Envoy::Tracing::Span& SpanGroup::getSpan(const intptr_t span_id) { + if (SpanGroupConstants::get().DecoderActiveSpan == span_id) { + if (nullptr == decoderCallbacks_) { + return Envoy::Tracing::NullSpan::instance(); + } + return decoderCallbacks_->activeSpan(); + } + + if (SpanGroupConstants::get().EncoderActiveSpan == span_id) { + if (nullptr == encoderCallbacks_) { + return Envoy::Tracing::NullSpan::instance(); + } + return encoderCallbacks_->activeSpan(); + } + + return getSpan_(span_id); + } + + void SpanGroup::setDecoderFilterCallbacks(StreamDecoderFilterCallbacks& callbacks) { + decoderCallbacks_ = &callbacks; + } + + void SpanGroup::setEncoderFilterCallbacks(StreamEncoderFilterCallbacks& callbacks) { + encoderCallbacks_ = &callbacks; + } + +} +} \ No newline at end of file diff --git a/ego/src/cc/filter/http/span-group.h b/ego/src/cc/filter/http/span-group.h new file mode 100644 index 0000000..1e88c32 --- /dev/null +++ b/ego/src/cc/filter/http/span-group.h @@ -0,0 +1,43 @@ +#pragma once +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include +#include "envoy/server/filter_config.h" + +namespace Envoy { +namespace Http { + + struct SpanGroupConstantValues { + const int EncoderActiveSpan = 0; + const int DecoderActiveSpan = -1; + }; + + using SpanGroupConstants = ConstSingleton; + + class SpanGroup { + public: + SpanGroup(); + intptr_t spawnChildSpan(const intptr_t parent_span_id, std::string &name); + Envoy::Tracing::Span& getSpan(const intptr_t span_id); + void removeSpan(const intptr_t span_id); + void setDecoderFilterCallbacks(StreamDecoderFilterCallbacks& callbacks); + void setEncoderFilterCallbacks(StreamEncoderFilterCallbacks& callbacks); + + private: + void setSpan(const intptr_t id, Envoy::Tracing::SpanPtr span); + Envoy::Tracing::Span& getSpan_(const intptr_t span_id); + + std::map> spans_; + std::mutex spans_mutex; + // Do only access from dispatcher context. Do check if non-0 before use. + StreamDecoderFilterCallbacks* decoderCallbacks_; + StreamEncoderFilterCallbacks* encoderCallbacks_; + }; + + using SpanGroupPtr = std::unique_ptr; + +} +} \ No newline at end of file diff --git a/ego/src/cc/goc/BUILD.bazel b/ego/src/cc/goc/BUILD.bazel new file mode 100644 index 0000000..f70dc8a --- /dev/null +++ b/ego/src/cc/goc/BUILD.bazel @@ -0,0 +1,40 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +package(default_visibility = ["//visibility:public"]) + +load( + "@envoy//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_cc_test", +) + +# :goc contains code that is called from Go ("downcalls"). We need to separate +# this from the code that calls Go ("upcalls") in order to avoid circular deps. +# +envoy_cc_library( + name = "goc", + srcs = [ + "bufferinstance.cc", + "goc.cc", + "gohttpfilter.cc", + "log.cc", + "requestheadermap.cc", + "responseheadermap.cc", + "requesttrailermap.cc", + "stats.cc", + ], + hdrs = [ + "goc.h", + "envoy.h", + ], + repository = "@envoy", + deps = [ + "//ego/src/cc/filter/http:goc", + "//ego/src/cc/goc/proto:pkg_cc_proto", + "@envoy//include/envoy/http:filter_interface", + "@envoy//source/common/router:string_accessor_lib", + ], +) diff --git a/ego/src/cc/goc/bufferinstance.cc b/ego/src/cc/goc/bufferinstance.cc new file mode 100644 index 0000000..c80c255 --- /dev/null +++ b/ego/src/cc/goc/bufferinstance.cc @@ -0,0 +1,47 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include + +#include "envoy/buffer/buffer.h" + +#include "envoy.h" + +// BufferInstance +uint64_t BufferInstance_copyOut(void* bufferInstance, size_t start, GoBuf buf) { + auto that = static_cast(bufferInstance); + if (that->length() <= start) + return 0; + + auto size = that->length() - start; + if (buf.len < size) + size = buf.len; + + that->copyOut(start, size, buf.data); + return size; +} + +uint64_t BufferInstance_length(void* bufferInstance) { + auto that = static_cast(bufferInstance); + return that->length(); +} + +uint64_t BufferInstance_getRawSlicesCount(void* bufferInstance) { + auto that = static_cast(bufferInstance); + return that->getRawSlices().size(); +} + +uint64_t BufferInstance_getRawSlices(void* bufferInstance, uint64_t max, GoBuf* dest) { + auto that = static_cast(bufferInstance); + + auto len = 0; + for (const auto& slice : that->getRawSlices(max)) { + dest->data = slice.mem_; + dest->len = dest->cap = slice.len_; + dest++; + len++; + } + return len; +} diff --git a/ego/src/cc/goc/envoy.h b/ego/src/cc/goc/envoy.h new file mode 100644 index 0000000..bbc5e24 --- /dev/null +++ b/ego/src/cc/goc/envoy.h @@ -0,0 +1,203 @@ +#pragma once +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#ifndef CGO_ENVOY_H +#define CGO_ENVOY_H + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +#include +#include + +// Generic C interfacing + +typedef const char* GoError; + +typedef struct { + size_t len; + char* data; +} GoStr; + +typedef struct { + size_t len; + size_t cap; + void* data; +} GoBuf; + +// GoHttpFilter +void GoHttpFilter_pin(void* goHttpFilter); +void GoHttpFilter_unpin(void* goHttpFilter); +void GoHttpFilter_post(void* goHttpFilter, uint64_t tag); +void GoHttpFilter_log(void* goHttpFilter, uint32_t logLevel, GoStr message); +uint64_t GoHttpFilter_ResolveMostSpecificPerGoFilterConfig(void* goHttpFilter, GoStr name); + +// DecoderFilterCallbacks +void GoHttpFilter_DecoderCallbacks_continueDecoding(void* goHttpFilter); +int GoHttpFilter_DecoderCallbacks_sendLocalReply(void* goHttpFilter, int responseCode, GoStr body, + GoBuf headersBuf, GoStr details); +const void* GoHttpFilter_DecoderCallbacks_decodingBuffer(void* goHttpFilter); +void GoHttpFilter_DecoderCallbacks_addDecodedData(void* goHttpFilter, void* bufferInstance, + int streamingFilter); +void GoHttpFilter_DecoderCallbacks_StreamInfo_FilterState_setData(void* goHttpFilter, GoStr name, + GoStr value, int stateType, + int lifeSpan); +int GoHttpFilter_DecoderCallbacks_encodeHeaders(void* goHttpFilter, int responseCode, + GoBuf headersBuf, int endStream); + +// EncoderFilterCallbacks +const void* GoHttpFilter_EncoderCallbacks_encodingBuffer(void* goHttpFilter); +void GoHttpFilter_EncoderCallbacks_addEncodedData(void* goHttpFilter, void* bufferInstance, + int streamingFilter); +void GoHttpFilter_EncoderCallbacks_continueEncoding(void* goHttpFilter); + + +// StreamFilterCallbacks +int GoHttpFilter_StreamFilterCallbacks_StreamInfo_FilterState_getDataReadOnly(void* goHttpFilter,int encoder, + GoStr name, GoStr* value); +int64_t GoHttpFilter_StreamFilterCallbacks_StreamInfo_lastDownstreamTxByteSent(void* goHttpFilter,int encoder); +const void * GoHttpFilter_StreamFilterCallbacks_StreamInfo_getRequestHeaders(void* goHttpFilter,int encoder); +int GoHttpFilter_StreamFilterCallbacks_StreamInfo_responseCode(void* goHttpFilter,int encoder); +void GoHttpFilter_StreamFilterCallbacks_StreamInfo_responseCodeDetails(void* goHttpFilter,int encoder, GoStr* value); + +// Returns 0 if route isn't existing. Otherwise, returns non-zero. +int GoHttpFilter_StreamFilterCallbacks_routeExisting(void* goHttpFilter, int encoder); + +// Returns 0 if there is no error. Otherwise, returns non-zero. +int GoHttpFilter_StreamFilterCallbacks_route_routeEntry_pathMatchCriterion_matcher(void* goHttpFilter, int encoder, GoStr* value); +// Returns match type as following. If there is an error, returns a negative number. +// case Envoy::Router::PathMatchType::None: +// return 0; +// break; +// case Envoy::Router::PathMatchType::Prefix: +// return 1; +// break; +// case Envoy::Router::PathMatchType::Exact: +// return 2; +// break; +// case Envoy::Router::PathMatchType::Regex: +// return 3; +int GoHttpFilter_StreamFilterCallbacks_route_routeEntry_pathMatchCriterion_matchType(void* goHttpFilter, int encoder); + +// GenericSecretConfigProvider +void GoHttpFilter_GenericSecretConfigProvider_secret(void* goHttpFilter, GoStr* value); + + +// Two specicial spanIDs. See ego/src/cc/filter/http/span-group.h +// -1 : activeSpan of decoderCallBacks +// -0: activeSpan of encoderCallbacks +// Span is used exclusively by a single Go routine. +int GoHttpFilter_Span_getContext(void *goHttpFilter, const intptr_t spanID, GoBuf buf); +intptr_t GoHttpFilter_Span_spawnChild(void *goHttpFilter, intptr_t parentSpanID, GoStr name); +void GoHttpFilter_Span_finishSpan(void *goHttpFilter, intptr_t spanID); + + +uint64_t BufferInstance_copyOut(void* bufferInstance, size_t start, GoBuf buf); +uint64_t BufferInstance_length(void* bufferInstance); +uint64_t BufferInstance_getRawSlicesCount(void* bufferInstance); +uint64_t BufferInstance_getRawSlices(void* bufferInstance, uint64_t max, GoBuf* dest); + +// RequestHeaderMap +void RequestHeaderMap_add(void* requestHeaderMap, GoStr name, GoStr value); +void RequestHeaderMap_set(void* requestHeaderMap, GoStr name, GoStr value); +void RequestHeaderMap_append(void* requestHeaderMap, GoStr name, GoStr value); +size_t RequestHeaderMap_getByPrefix(void* requestHeaderMap, GoStr prefix, GoBuf buf); +void RequestHeaderMap_remove(void* requestHeaderMap, GoStr name); + +/** + * Gets Path header. Header value is stored in value parameter. + * Header value is managed by RequestHeaderMap. + * Callers should not free memory returned in value. + * @param requestHeaderMap pointer to Envoy::Http::RequestHeaderMap object. + * @param value placeholder containing info about header value. + */ +void RequestHeaderMap_Path(void* requestHeaderMap, GoStr* value); + +void RequestHeaderMap_setPath(void* requestHeaderMap, GoStr value); + +/** + * Gets Method header. Header value is stored in value parameter. + * Header value is managed by RequestHeaderMap. + * Callers should not free memory returned in value. + * @param requestHeaderMap pointer to Envoy::Http::RequestHeaderMap object. + * @param value placeholder containing info about header value. + */ +void RequestHeaderMap_Method(void* requestHeaderMap, GoStr* value); + +/** + * Gets ContentType header. Header value is stored in value parameter. + * Header value is managed by RequestHeaderMap. + * Callers should not free memory returned in value. + * @param requestHeaderMap pointer to Envoy::Http::RequestHeaderMap object. + * @param value placeholder containing info about header value. + */ +void RequestHeaderMap_ContentType(void* requestHeaderMap, GoStr* value); + +/** + * Gets Authorization header. Header value is stored in value parameter. + * Header value is managed by RequestHeaderMap. + * Callers should not free memory returned in value. + * @param requestHeaderMap pointer to Envoy::Http::RequestHeaderMap object. + * @param value placeholder containing info about header value. + */ +void RequestHeaderMap_Authorization(void* requestHeaderMap, GoStr* value); + +/** + * Gets header by name. Header value is stored in value parameter. + * Header value is managed by RequestHeaderMap. + * Callers should not free memory returned in value. + * @param requestHeaderMap pointer to Envoy::Http::RequestHeaderMap object. + * @param name header name. It's caller's responsibility to manage name's memory. + * @param value placeholder containing info about header value. + */ +void RequestHeaderMap_get(void* requestHeaderMap, GoStr name, GoStr* value); + +// RequestTrailerMap +void RequestTrailerMap_add(void* requestTrailerMap, GoStr name, GoStr value); + +// ResponseHeaderMap +void ResponseHeaderMap_add(void* responseHeaderMap, GoStr name, GoStr value); +void ResponseHeaderMap_set(void* responseHeaderMap, GoStr name, GoStr value); +void ResponseHeaderMap_append(void* responseHeaderMap, GoStr name, GoStr value); +void ResponseHeaderMap_remove(void* responseHeaderMap, GoStr name); +void ResponseHeaderMap_get(void* responseHeaderMap, GoStr name, GoStr* value); +void ResponseHeaderMap_ContentType(void* responseHeaderMap, GoStr* value); +void ResponseHeaderMap_Status(void* responseHeaderMap, GoStr* value); +void ResponseHeaderMap_setStatus(void* responseHeaderMap, int status); + +// Static functions will be call from from Go ("downcalls") without a pointer +// +void Envoy_log_misc(uint32_t level, GoStr tag, GoStr message); + +// Stats::Scope +const void* Stats_Scope_counterFromStatName(void* scope, GoStr name); +const void* Stats_Scope_gaugeFromStatName(void* scope, GoStr name, int importMode); +const void* Stats_Scope_histogramFromStatName(void* scope, GoStr name, int unit); + +// Stats::Counter +void Stats_Counter_add(void* counter, uint64_t amount); +void Stats_Counter_inc(void* counter); +uint64_t Stats_Counter_latch(void* counter); +void Stats_Counter_reset(void* counter); +uint64_t Stats_Counter_value(void* counter); + +// Stats::Gauge +void Stats_Gauge_add(void* gauge, uint64_t amount); +void Stats_Gauge_dec(void* gauge); +void Stats_Gauge_inc(void* gauge); +void Stats_Gauge_set(void* gauge, uint64_t value); +void Stats_Gauge_sub(void* gauge, uint64_t amount); +uint64_t Stats_Gauge_value(void* gauge); + +// Stats::Histogram +int Stats_Histogram_unit(void* histogram); +void Stats_Histogram_recordValue(void* histogram, uint64_t value); + +#ifdef __cplusplus +} +#endif // __cplusplus +#endif // CGO_ENVOY_H diff --git a/ego/src/cc/goc/goc.cc b/ego/src/cc/goc/goc.cc new file mode 100644 index 0000000..d910d11 --- /dev/null +++ b/ego/src/cc/goc/goc.cc @@ -0,0 +1,259 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include "goc.h" + +Envoy::Http::FilterHeadersStatus Goc_FilterHeadersStatus(int status) { + switch (status) { + case 100: + return Envoy::Http::FilterHeadersStatus::Continue; + case 101: + return Envoy::Http::FilterHeadersStatus::StopIteration; + case 102: + return Envoy::Http::FilterHeadersStatus::ContinueAndEndStream; + case 104: + return Envoy::Http::FilterHeadersStatus::StopAllIterationAndBuffer; + case 105: + return Envoy::Http::FilterHeadersStatus::StopAllIterationAndWatermark; + default: + // TODO: log error + return Envoy::Http::FilterHeadersStatus::StopIteration; + } +} + +Envoy::Http::FilterTrailersStatus Goc_FilterTrailersStatus(int status) { + switch (status) { + case 200: + return Envoy::Http::FilterTrailersStatus::Continue; + case 201: + return Envoy::Http::FilterTrailersStatus::StopIteration; + default: + // TODO: log error + return Envoy::Http::FilterTrailersStatus::StopIteration; + } +} + +Envoy::Http::FilterDataStatus Goc_FilterDataStatus(int status) { + switch (status) { + case 300: + return Envoy::Http::FilterDataStatus::Continue; + case 301: + return Envoy::Http::FilterDataStatus::StopIterationAndBuffer; + case 302: + return Envoy::Http::FilterDataStatus::StopIterationAndWatermark; + case 303: + return Envoy::Http::FilterDataStatus::StopIterationNoBuffer; + default: + // TODO: log error + return Envoy::Http::FilterDataStatus::StopIterationNoBuffer; + } +} + +Envoy::Http::Code Goc_HttpResonseCode(int status) { + switch (status) { + case 100: + return Envoy::Http::Code::Continue; + case 101: + return Envoy::Http::Code::SwitchingProtocols; + + case 200: + return Envoy::Http::Code::OK; + case 201: + return Envoy::Http::Code::Created; + case 202: + return Envoy::Http::Code::Accepted; + case 203: + return Envoy::Http::Code::NonAuthoritativeInformation; + case 204: + return Envoy::Http::Code::NoContent; + case 205: + return Envoy::Http::Code::ResetContent; + case 206: + return Envoy::Http::Code::PartialContent; + case 207: + return Envoy::Http::Code::MultiStatus; + case 208: + return Envoy::Http::Code::AlreadyReported; + case 226: + return Envoy::Http::Code::IMUsed; + + case 300: + return Envoy::Http::Code::MultipleChoices; + case 301: + return Envoy::Http::Code::MovedPermanently; + case 302: + return Envoy::Http::Code::Found; + case 303: + return Envoy::Http::Code::SeeOther; + case 304: + return Envoy::Http::Code::NotModified; + case 305: + return Envoy::Http::Code::UseProxy; + case 306: + return Envoy::Http::Code::TemporaryRedirect; + case 307: + return Envoy::Http::Code::PermanentRedirect; + + case 400: + return Envoy::Http::Code::BadRequest; + case 401: + return Envoy::Http::Code::Unauthorized; + case 402: + return Envoy::Http::Code::PaymentRequired; + case 403: + return Envoy::Http::Code::Forbidden; + case 404: + return Envoy::Http::Code::NotFound; + case 405: + return Envoy::Http::Code::MethodNotAllowed; + case 406: + return Envoy::Http::Code::NotAcceptable; + case 407: + return Envoy::Http::Code::ProxyAuthenticationRequired; + case 408: + return Envoy::Http::Code::RequestTimeout; + case 409: + return Envoy::Http::Code::Conflict; + case 410: + return Envoy::Http::Code::Gone; + case 411: + return Envoy::Http::Code::LengthRequired; + case 412: + return Envoy::Http::Code::PreconditionFailed; + case 413: + return Envoy::Http::Code::PayloadTooLarge; + case 414: + return Envoy::Http::Code::URITooLong; + case 415: + return Envoy::Http::Code::UnsupportedMediaType; + case 416: + return Envoy::Http::Code::RangeNotSatisfiable; + case 417: + return Envoy::Http::Code::ExpectationFailed; + case 421: + return Envoy::Http::Code::MisdirectedRequest; + case 422: + return Envoy::Http::Code::UnprocessableEntity; + case 423: + return Envoy::Http::Code::Locked; + case 424: + return Envoy::Http::Code::FailedDependency; + case 426: + return Envoy::Http::Code::UpgradeRequired; + case 428: + return Envoy::Http::Code::PreconditionRequired; + case 429: + return Envoy::Http::Code::TooManyRequests; + case 431: + return Envoy::Http::Code::RequestHeaderFieldsTooLarge; + + case 500: + return Envoy::Http::Code::InternalServerError; + case 501: + return Envoy::Http::Code::NotImplemented; + case 502: + return Envoy::Http::Code::BadGateway; + case 503: + return Envoy::Http::Code::ServiceUnavailable; + case 504: + return Envoy::Http::Code::GatewayTimeout; + case 505: + return Envoy::Http::Code::HTTPVersionNotSupported; + case 506: + return Envoy::Http::Code::VariantAlsoNegotiates; + case 507: + return Envoy::Http::Code::InsufficientStorage; + case 508: + return Envoy::Http::Code::LoopDetected; + case 510: + return Envoy::Http::Code::NotExtended; + case 511: + return Envoy::Http::Code::NetworkAuthenticationRequired; + + default: + // TODO: log error + return Envoy::Http::Code::InternalServerError; + } +} + +Envoy::StreamInfo::FilterState::StateType Goc_FilterStateType(int stateType) { + switch (stateType) { + case 1: + return Envoy::StreamInfo::FilterState::StateType::ReadOnly; + case 2: + return Envoy::StreamInfo::FilterState::StateType::Mutable; + + default: + // TODO: log error + return Envoy::StreamInfo::FilterState::StateType::ReadOnly; + } +} + +Envoy::StreamInfo::FilterState::LifeSpan Goc_FilterStateLifeSpan(int lifeSpan) { + switch (lifeSpan) { + case 1: + return Envoy::StreamInfo::FilterState::LifeSpan::FilterChain; + case 2: + return Envoy::StreamInfo::FilterState::LifeSpan::DownstreamRequest; + case 3: + return Envoy::StreamInfo::FilterState::LifeSpan::DownstreamConnection; + case 4: + return Envoy::StreamInfo::FilterState::LifeSpan::TopSpan; + + default: + // TODO: log error + return Envoy::StreamInfo::FilterState::LifeSpan::FilterChain; + } +} + +Envoy::Stats::Gauge::ImportMode Goc_Stats_ImportMode(int importMode) { + switch (importMode) { + case 1: + return Envoy::Stats::Gauge::ImportMode::Uninitialized; + case 2: + return Envoy::Stats::Gauge::ImportMode::NeverImport; + case 3: + return Envoy::Stats::Gauge::ImportMode::Accumulate; + default: + // TODO: log error + return Envoy::Stats::Gauge::ImportMode::Uninitialized; + } +} + +Envoy::Stats::Histogram::Unit Goc_Stats_Unit(int unit) { + switch (unit) { + case 1: + return Envoy::Stats::Histogram::Unit::Null; + case 2: + return Envoy::Stats::Histogram::Unit::Unspecified; + case 3: + return Envoy::Stats::Histogram::Unit::Bytes; + case 4: + return Envoy::Stats::Histogram::Unit::Microseconds; + case 5: + return Envoy::Stats::Histogram::Unit::Milliseconds; + default: + // TODO: log error + return Envoy::Stats::Histogram::Unit::Null; + } +} + +int Goc_Stats_Unit_Value(Envoy::Stats::Histogram::Unit unit) { + switch (unit) { + case Envoy::Stats::Histogram::Unit::Null: + return 1; + case Envoy::Stats::Histogram::Unit::Unspecified: + return 2; + case Envoy::Stats::Histogram::Unit::Bytes: + return 3; + case Envoy::Stats::Histogram::Unit::Microseconds: + return 4; + case Envoy::Stats::Histogram::Unit::Milliseconds: + return 5; + default: + // TODO: log error + return 0; + } +} diff --git a/ego/src/cc/goc/goc.h b/ego/src/cc/goc/goc.h new file mode 100644 index 0000000..e9969f4 --- /dev/null +++ b/ego/src/cc/goc/goc.h @@ -0,0 +1,28 @@ +#pragma once +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#ifndef CGO_GOC_H +#define CGO_GOC_H + +#include "envoy/http/filter.h" +#include "envoy/stats/stats.h" +#include "envoy/stream_info/filter_state.h" + +// FilterStatus conversion +Envoy::Http::FilterHeadersStatus Goc_FilterHeadersStatus(int status); +Envoy::Http::FilterTrailersStatus Goc_FilterTrailersStatus(int status); +Envoy::Http::FilterDataStatus Goc_FilterDataStatus(int status); +// Http status code conversion +Envoy::Http::Code Goc_HttpResonseCode(int responseCode); +// FilterState enums conversion +Envoy::StreamInfo::FilterState::StateType Goc_FilterStateType(int stateType); +Envoy::StreamInfo::FilterState::LifeSpan Goc_FilterStateLifeSpan(int lifeSpan); +// Stats enums conversion +Envoy::Stats::Gauge::ImportMode Goc_Stats_ImportMode(int importMode); +Envoy::Stats::Histogram::Unit Goc_Stats_Unit(int unit); +int Goc_Stats_Unit_Value(Envoy::Stats::Histogram::Unit unit); + +#endif // CGO_GOC_H diff --git a/ego/src/cc/goc/gohttpfilter.cc b/ego/src/cc/goc/gohttpfilter.cc new file mode 100644 index 0000000..ff8fa46 --- /dev/null +++ b/ego/src/cc/goc/gohttpfilter.cc @@ -0,0 +1,309 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include "common/common/empty_string.h" +#include "common/config/datasource.h" +#include "common/http/header_map_impl.h" +#include "common/router/string_accessor_impl.h" + +#include "ego/src/cc/filter/http/filter.h" +#include "ego/src/cc/goc/proto/dto.pb.validate.h" +#include "envoy.h" +#include "goc.h" + +void GoHttpFilter_pin(void* goHttpFilter) { + static_cast(goHttpFilter)->pin(); +} + +void GoHttpFilter_unpin(void* goHttpFilter) { + static_cast(goHttpFilter)->unpin(); +} + +void GoHttpFilter_post(void* goHttpFilter, uint64_t tag) { + static_cast(goHttpFilter)->post(tag); +} + +void GoHttpFilter_log(void* goHttpFilter, uint32_t logLevel, GoStr messsage) { + + auto c_message = absl::string_view(messsage.data, messsage.len); + + static_cast(goHttpFilter)->log(logLevel, c_message); +} + +void GoHttpFilter_DecoderCallbacks_continueDecoding(void* goHttpFilter) { + static_cast(goHttpFilter)->decoderCallbacks()->continueDecoding(); +} + +int GoHttpFilter_DecoderCallbacks_sendLocalReply(void* goHttpFilter, int responseCode, GoStr body, + GoBuf headersBuf, GoStr details) { + auto c_body = absl::string_view(body.data, body.len); + auto c_details = absl::string_view(details.data, details.len); + auto headers = ego::http::RequestHeaderMap{}; + if (!headers.ParseFromArray(headersBuf.data, headersBuf.len)) { + // non-zero returned value means errors. + return 1; + } + + static_cast(goHttpFilter) + ->decoderCallbacks() + ->sendLocalReply( + Goc_HttpResonseCode(responseCode), c_body, + [headers](Envoy::Http::HeaderMap& response_headers) -> void { + for (const auto& h : headers.headers()) { + response_headers.setCopy(Envoy::Http::LowerCaseString(h.key()), h.value()); + } + }, + absl::nullopt /*grpc_status*/, c_details); + return 0; +} + +void GoHttpFilter_DecoderCallbacks_StreamInfo_FilterState_setData(void* goHttpFilter, GoStr name, + GoStr value, int stateType, + int lifeSpan) { + auto c_name = absl::string_view(name.data, name.len); + auto c_value = absl::string_view(value.data, value.len); + + // StringAccessorImpl copies c_value data + std::shared_ptr value_object = + std::make_unique(c_value); + + static_cast(goHttpFilter) + ->decoderCallbacks() + ->streamInfo() + .filterState() + ->setData(c_name, value_object, Goc_FilterStateType(stateType), + Goc_FilterStateLifeSpan(lifeSpan)); +} + +int GoHttpFilter_StreamFilterCallbacks_StreamInfo_FilterState_getDataReadOnly(void* goHttpFilter, int encoder, + GoStr name, GoStr* value) { + ASSERT(nullptr != goHttpFilter); + ASSERT(nullptr != value); + + auto c_name = absl::string_view(name.data, name.len); + + auto filter_state = static_cast(goHttpFilter) + ->streamFilterCallbacks(encoder) + ->streamInfo() + .filterState(); + if (!filter_state->hasData(c_name)) { + return 0; + } + + auto c_value = + filter_state->getDataReadOnly(c_name).asString(); + value->len = c_value.size(); + value->data = const_cast(c_value.data()); + return 1; +} + +void GoHttpFilter_StreamFilterCallbacks_StreamInfo_responseCodeDetails(void* goHttpFilter, int encoder, GoStr* value) { + ASSERT(nullptr != goHttpFilter); + ASSERT(nullptr != value); + + auto & response_code_details = static_cast(goHttpFilter) + ->streamFilterCallbacks(encoder) + ->streamInfo() + .responseCodeDetails() + .value(); + value->len = response_code_details.size(); + value->data = const_cast(response_code_details.data()); +} + +int64_t GoHttpFilter_StreamFilterCallbacks_StreamInfo_lastDownstreamTxByteSent(void* goHttpFilter,int encoder){ + auto request_time = static_cast(goHttpFilter) + ->streamFilterCallbacks(encoder) + ->streamInfo() + .lastDownstreamTxByteSent(); + return request_time.value_or(std::chrono::nanoseconds(-1)).count(); +} + +const void * GoHttpFilter_StreamFilterCallbacks_StreamInfo_getRequestHeaders(void* goHttpFilter,int encoder){ + return static_cast(goHttpFilter) + ->streamFilterCallbacks(encoder) + ->streamInfo() + .getRequestHeaders(); +} + +int GoHttpFilter_StreamFilterCallbacks_StreamInfo_responseCode(void* goHttpFilter,int encoder){ + return static_cast(goHttpFilter) + ->streamFilterCallbacks(encoder) + ->streamInfo() + .responseCode().value_or(0); +} + +int GoHttpFilter_StreamFilterCallbacks_routeExisting(void* goHttpFilter, int encoder) { + ASSERT(nullptr != goHttpFilter); + + auto route = + static_cast(goHttpFilter)->streamFilterCallbacks(encoder)->route(); + return nullptr == route? 0: 1; +} + +int GoHttpFilter_StreamFilterCallbacks_route_routeEntry_pathMatchCriterion_matcher(void* goHttpFilter, int encoder, GoStr* value) { + ASSERT(nullptr != goHttpFilter); + ASSERT(nullptr != value); + + auto route = + static_cast(goHttpFilter)->streamFilterCallbacks(encoder)->route(); + if (nullptr == route || nullptr == route->routeEntry()) { + return 1; + } + + const auto &matcher = route->routeEntry()->pathMatchCriterion().matcher(); + value->len = matcher.size(); + value->data = const_cast(matcher.data()); + return 0; +} + +int GoHttpFilter_StreamFilterCallbacks_route_routeEntry_pathMatchCriterion_matchType(void* goHttpFilter, int encoder) { + ASSERT(nullptr != goHttpFilter); + + auto route = + static_cast(goHttpFilter)->streamFilterCallbacks(encoder)->route(); + if (nullptr == route || nullptr == route->routeEntry()) { + return -1; + } + + auto matchType = route->routeEntry()->pathMatchCriterion().matchType(); + switch(matchType) { + case Envoy::Router::PathMatchType::None: + return 0; + break; + case Envoy::Router::PathMatchType::Prefix: + return 1; + break; + case Envoy::Router::PathMatchType::Exact: + return 2; + break; + case Envoy::Router::PathMatchType::Regex: + return 3; + break; + } + return -1; +} + +const void* GoHttpFilter_DecoderCallbacks_decodingBuffer(void* goHttpFilter) { + return static_cast(goHttpFilter) + ->decoderCallbacks() + ->decodingBuffer(); +} + +void GoHttpFilter_DecoderCallbacks_addDecodedData(void* goHttpFilter, void* bufferInstance, + int streamingFilter) { + + auto c_bufferInstance = static_cast(bufferInstance); + + static_cast(goHttpFilter) + ->decoderCallbacks() + ->addDecodedData(*c_bufferInstance, streamingFilter); +} + +int GoHttpFilter_DecoderCallbacks_encodeHeaders(void* goHttpFilter, int responseCode, + GoBuf headersBuf, int endStream) { + auto headers = ego::http::ResponseHeaderMap{}; + if (!headers.ParseFromArray(headersBuf.data, headersBuf.len)) { + // non-zero returned value means errors. + return 1; + } + + auto response_headers{Envoy::Http::createHeaderMap( + {{Envoy::Http::Headers::get().Status, std::to_string(responseCode)}})}; + + for (const auto& h : headers.headers()) { + response_headers->addCopy(Envoy::Http::LowerCaseString(h.key()), h.value()); + } + + static_cast(goHttpFilter) + ->decoderCallbacks() + ->encodeHeaders(std::move(response_headers), endStream == 1 ? true : false); + return 0; +} + +const void* GoHttpFilter_EncoderCallbacks_encodingBuffer(void* goHttpFilter) { + return static_cast(goHttpFilter) + ->encoderCallbacks() + ->encodingBuffer(); +} + +void GoHttpFilter_EncoderCallbacks_addEncodedData(void* goHttpFilter, void* bufferInstance, + int streamingFilter) { + + auto c_bufferInstance = static_cast(bufferInstance); + + static_cast(goHttpFilter) + ->encoderCallbacks() + ->addEncodedData(*c_bufferInstance, streamingFilter); +} + +void GoHttpFilter_EncoderCallbacks_continueEncoding(void* goHttpFilter) { + static_cast(goHttpFilter)->encoderCallbacks()->continueEncoding(); +} + +void GoHttpFilter_GenericSecretConfigProvider_secret(void* goHttpFilter, GoStr* value) { + auto filter = static_cast(goHttpFilter); + auto secretProvider = filter->genericSecretConfigProvider(); + if (secretProvider == nullptr || secretProvider->secret() == nullptr) { + return; + } + // We need a variable on C-Side to hold the reference to not free after return + filter->secret_holder = + Envoy::Config::DataSource::read(secretProvider->secret()->secret(), true, filter->api()); + + value->len = filter->secret_holder.size(); + value->data = const_cast(filter->secret_holder.c_str()); +} + +int GoHttpFilter_Span_getContext(void *goHttpFilter, const intptr_t spanID, GoBuf buf){ + auto that = static_cast(goHttpFilter); + + Envoy::Http::RequestHeaderMapImpl headers; + that->getSpan(spanID).injectContext(headers); + auto result = ego::http::RequestHeaderMap{}; + + headers.iterate([](const Envoy::Http::HeaderEntry& header, void* context) -> Envoy::Http::HeaderMap::Iterate { + auto r = static_cast(context); + auto entry = r->mutable_headers()->Add(); + entry->set_key(header.key().getStringView().data(), header.key().getStringView().size()); + entry->set_value(header.value().getStringView().data(), header.value().getStringView().size()); + + return Envoy::Http::HeaderMap::Iterate::Continue; + }, &result); + + // no headers matches the prefix + if(0 == result.headers_size()) { + return 0; + } + + // the buffer is too small. Return required size. + const auto size = result.ByteSizeLong(); + if (size > buf.len) { + return size; + } + + // serialize data. + result.SerializePartialToArray(buf.data, buf.len); + return size; +} + +intptr_t GoHttpFilter_Span_spawnChild(void *goHttpFilter, intptr_t parentSpanID, GoStr name) { + auto that = static_cast(goHttpFilter); + auto c_name = std::string(name.data, name.len); + + return that->spawnChildSpan(parentSpanID, c_name); +} + +void GoHttpFilter_Span_finishSpan(void *goHttpFilter, intptr_t spanID) { + auto that = static_cast(goHttpFilter); + + that->finishSpan(spanID); +} + + +// Not use name GoStr for now, assumming that Go filters always get their own configurations. +uint64_t GoHttpFilter_ResolveMostSpecificPerGoFilterConfig(void* goHttpFilter, GoStr) { + return static_cast(goHttpFilter) + ->resolveMostSpecificPerGoFilterConfigTag(); +} \ No newline at end of file diff --git a/ego/src/cc/goc/log.cc b/ego/src/cc/goc/log.cc new file mode 100644 index 0000000..f1ca84c --- /dev/null +++ b/ego/src/cc/goc/log.cc @@ -0,0 +1,46 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include "common/common/logger.h" + +#include "envoy.h" + +// Macro ENVOY_LOG_MISC and logger are declare inside Envoy namespace +// So just wrapp it with a function extern "C" below +namespace Envoy { + +void Envoy_log_misc(uint32_t level, absl::string_view tag, absl::string_view message) { + switch (static_cast(level)) { + case spdlog::level::trace: + ENVOY_LOG_MISC(trace, "[ego_http][{}] {}", tag, message); + return; + case spdlog::level::debug: + ENVOY_LOG_MISC(debug, "[ego_http][{}] {}", tag, message); + return; + case spdlog::level::info: + ENVOY_LOG_MISC(info, "[ego_http][{}] {}", tag, message); + return; + case spdlog::level::warn: + ENVOY_LOG_MISC(warn, "[ego_http][{}] {}", tag, message); + return; + case spdlog::level::err: + ENVOY_LOG_MISC(error, "[ego_http][{}] {}", tag, message); + return; + case spdlog::level::critical: + ENVOY_LOG_MISC(critical, "[ego_http][{}] {}", tag, message); + return; + case spdlog::level::off: + return; + } + ENVOY_LOG_MISC(warn, "[ego_http][{}] UNDEFINED LOG LEVEL {}: {}", tag, level, message); +} + +} // namespace Envoy + +void Envoy_log_misc(uint32_t level, GoStr tag, GoStr message) { + auto c_message = absl::string_view(message.data, message.len); + auto c_tag = absl::string_view(tag.data, tag.len); + Envoy::Envoy_log_misc(level, c_tag, c_message); +} diff --git a/ego/src/cc/goc/proto/BUILD.bazel b/ego/src/cc/goc/proto/BUILD.bazel new file mode 100644 index 0000000..40bcc0d --- /dev/null +++ b/ego/src/cc/goc/proto/BUILD.bazel @@ -0,0 +1,45 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +# gazelle:ignore + +load("@io_bazel_rules_go//proto:compiler.bzl", "go_proto_compiler") +load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +# This is generate proto for C-Side +api_proto_package() + +# This is generate validation file +go_proto_compiler( + name = "pgv_plugin_go", + options = ["lang=go"], + plugin = "@com_envoyproxy_protoc_gen_validate//:protoc-gen-validate", + suffix = ".pb.validate.go", + valid_archive = False, +) + +# This is generate proto for G-Side usage +go_proto_library( + name = "go_default_library", + compilers = [ + "@io_bazel_rules_go//proto:go_proto", + "pgv_plugin_go", + ], + importpath = "github.com/grab/ego/ego/src/cc/goc/proto", + proto = ":pkg", # api_proto_package() generates this + visibility = ["//visibility:public"], + deps = [ + "@com_envoyproxy_protoc_gen_validate//validate:go_default_library", + "@com_github_golang_protobuf//ptypes:go_default_library", + "@com_github_golang_protobuf//ptypes/any:go_default_library", + "@com_github_golang_protobuf//ptypes/duration:go_default_library", + "@com_github_golang_protobuf//ptypes/struct:go_default_library", + "@com_github_golang_protobuf//ptypes/timestamp:go_default_library", + "@com_github_golang_protobuf//ptypes/wrappers:go_default_library", + "@com_google_googleapis//google/api:annotations_go_proto", + "@com_google_googleapis//google/rpc:status_go_proto", + ], +) diff --git a/ego/src/cc/goc/proto/dto.proto b/ego/src/cc/goc/proto/dto.proto new file mode 100644 index 0000000..5e1c78a --- /dev/null +++ b/ego/src/cc/goc/proto/dto.proto @@ -0,0 +1,24 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +syntax = "proto3"; + +package ego.http; + +import "validate/validate.proto"; + +// TODO: will check replace it by HeaderMap below +message RequestHeaderMap { + repeated HeaderEntry headers = 1; +} + +message ResponseHeaderMap { + repeated HeaderEntry headers = 1; +} + +message HeaderEntry { + string key = 1; + string value = 2; +} \ No newline at end of file diff --git a/ego/src/cc/goc/requestheadermap.cc b/ego/src/cc/goc/requestheadermap.cc new file mode 100644 index 0000000..8e1d995 --- /dev/null +++ b/ego/src/cc/goc/requestheadermap.cc @@ -0,0 +1,217 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include "envoy/http/header_map.h" + +#include "absl/strings/match.h" +#include "ego/src/cc/goc/proto/dto.pb.validate.h" +#include "envoy.h" + +void RequestHeaderMap_add(void* requestHeaderMap, GoStr name, GoStr value) { + auto that = static_cast(requestHeaderMap); + + // The std::string() constructor will create a copy of name. Unfortunately, + // there is no std::string_view, because... + auto c_name = std::string(name.data, name.len); + + // ...LowerCaseString creates a wrapped lowercase copy of c_name. + auto w_name = Envoy::Http::LowerCaseString(c_name); + + // we are wrapping the header value with a lightweight string_view, which + // is safe because... + auto w_value = absl::string_view(value.data, value.len); + + // ...addCopy will add another header `w_name` associated with a copy of + // `w_value` + that->addCopy(w_name, w_value); +} + +void RequestHeaderMap_set(void* requestHeaderMap, GoStr name, GoStr value) { + auto that = static_cast(requestHeaderMap); + + // The std::string() constructor will create a copy of name. Unfortunately, + // there is no std::string_view, because... + auto c_name = std::string(name.data, name.len); + + // ...LowerCaseString creates a wrapped lowercase copy of c_name. + auto w_name = Envoy::Http::LowerCaseString(c_name); + + // we are wrapping the header value with a lightweight string_view, which + // is safe because... + auto w_value = absl::string_view(value.data, value.len); + + // ...setCopy will add another header `w_name` associated with a copy of + // `w_value` + that->setCopy(w_name, w_value); +} + +void RequestHeaderMap_append(void* requestHeaderMap, GoStr name, GoStr value) { + auto that = static_cast(requestHeaderMap); + + // The std::string() constructor will create a copy of name. Unfortunately, + // there is no std::string_view, because... + auto c_name = std::string(name.data, name.len); + + // ...LowerCaseString creates a wrapped lowercase copy of c_name. + auto w_name = Envoy::Http::LowerCaseString(c_name); + + // we are wrapping the header value with a lightweight string_view, which + // is safe because... + auto w_value = absl::string_view(value.data, value.len); + + // ...appendCopy will add another header `w_name` associated with a copy of + // `w_value` + that->appendCopy(w_name, w_value); +} + +size_t RequestHeaderMap_getByPrefix(void* requestHeaderMap, GoStr prefix, GoBuf buf) { + auto that = static_cast(requestHeaderMap); + + auto c_prefix = std::string(prefix.data, prefix.len); + + auto result = ego::http::RequestHeaderMap{}; + auto args = std::make_pair(c_prefix, &result); + + that->iterate( + [](const Envoy::Http::HeaderEntry& header, void* context) -> Envoy::Http::HeaderMap::Iterate { + auto key_ret = static_cast*>(context); + const absl::string_view header_key_view = header.key().getStringView(); + if (absl::StartsWith(header_key_view, key_ret->first)) { + auto entry = std::make_unique(); + entry->set_key(header.key().getStringView().data(), header.key().getStringView().size()); + entry->set_value(header.value().getStringView().data(), + header.value().getStringView().size()); + key_ret->second->mutable_headers()->Add(std::move(*entry)); + } + return Envoy::Http::HeaderMap::Iterate::Continue; + }, + &args); + + // no headers matches the prefix + if (0 == result.headers_size()) { + return 0; + } + + // the buffer is too small. Return required size. + const auto size = result.ByteSizeLong(); + if (size > buf.len) { + return size; + } + + // serialize data. + result.SerializePartialToArray(buf.data, buf.len); + return size; +} + +void RequestHeaderMap_remove(void* requestHeaderMap, GoStr name) { + auto that = static_cast(requestHeaderMap); + + // The std::string() constructor will create a copy of name. Unfortunately, + // there is no std::string_view, because... + auto c_name = std::string(name.data, name.len); + + // ...LowerCaseString creates a wrapped lowercase copy of c_name. + auto w_name = Envoy::Http::LowerCaseString(c_name); + that->remove(w_name); +} + +void RequestHeaderMap_Path(void* requestHeaderMap, GoStr* value) { + ASSERT(nullptr != requestHeaderMap); + ASSERT(nullptr != value); + + auto that = static_cast(requestHeaderMap); + + ASSERT(nullptr != that->Path()); + auto path = that->Path()->value().getStringView(); + + // Path() returns a pointer, value() returns a reference, + // therefore getStringView().data() should be valid after return + value->len = path.size(); + value->data = const_cast(path.data()); +} + +// changing path might update result of resolveMostSpecificPerFilterConfig +// if route cache is cleared +void RequestHeaderMap_setPath(void* requestHeaderMap, GoStr path) { + ASSERT(nullptr != requestHeaderMap); + + auto that = static_cast(requestHeaderMap); + + // we are wrapping the header value with a lightweight string_view, which + // is safe because... + auto w_value = absl::string_view(path.data, path.len); + + that->setPath(w_value); +} + +void RequestHeaderMap_Method(void* requestHeaderMap, GoStr* value) { + ASSERT(nullptr != requestHeaderMap); + ASSERT(nullptr != value); + + auto that = static_cast(requestHeaderMap); + + ASSERT(nullptr != that->Method()); + auto method = that->Method()->value().getStringView(); + + // Method() returns a pointer, value() returns a reference, + // therefore getStringView().data() should be valid after return + value->len = method.size(); + value->data = const_cast(method.data()); +} + +void RequestHeaderMap_ContentType(void* requestHeaderMap, GoStr* value) { + ASSERT(nullptr != requestHeaderMap); + ASSERT(nullptr != value); + + auto that = static_cast(requestHeaderMap); + + if (nullptr == that->ContentType()) { + return; + } + auto contentType = that->ContentType()->value().getStringView(); + + // ContentType() returns a pointer, value() returns a reference, + // therefore getStringView().data() should be valid after return + value->len = contentType.size(); + value->data = const_cast(contentType.data()); +} + +void RequestHeaderMap_Authorization(void* requestHeaderMap, GoStr* value) { + ASSERT(nullptr != requestHeaderMap); + ASSERT(nullptr != value); + + auto that = static_cast(requestHeaderMap); + + if (that->Authorization() == nullptr) { + return; + } + auto authorization = that->Authorization()->value().getStringView(); + + // Authorization() returns a pointer, value() returns a reference, + // therefore getStringView().data() should be valid after return + value->len = authorization.size(); + value->data = const_cast(authorization.data()); +} + +void RequestHeaderMap_get(void* requestHeaderMap, GoStr key, GoStr* value) { + ASSERT(nullptr != requestHeaderMap); + ASSERT(nullptr != value); + + auto that = static_cast(requestHeaderMap); + + auto cName = std::string(key.data, key.len); + auto wName = Envoy::Http::LowerCaseString(cName); + + if (that->get(wName) == nullptr) { + return; + } + + auto valStringView = that->get(wName)->value().getStringView(); + + // get() returns a pointer, value() returns a reference, + // therefore getStringView().data() should be valid after return + value->len = valStringView.size(); + value->data = const_cast(valStringView.data()); +} diff --git a/ego/src/cc/goc/requesttrailermap.cc b/ego/src/cc/goc/requesttrailermap.cc new file mode 100644 index 0000000..0beccf7 --- /dev/null +++ b/ego/src/cc/goc/requesttrailermap.cc @@ -0,0 +1,28 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include "envoy/http/header_map.h" + +#include "envoy.h" + +void RequestTrailerMap_add(void* requestTrailerMap, GoStr name, GoStr value) { + + auto that = static_cast(requestTrailerMap); + + // The std::string() constructor will create a copy of name. Unfortunately, + // there is no std::string_view, because... + auto c_name = std::string(name.data, name.len); + + // ...LowerCaseString creates a wrapped lowercase copy of c_name. + auto w_name = Envoy::Http::LowerCaseString(c_name); + + // we are wrapping the header value with a lightweight string_view, which + // is safe because... + auto w_value = absl::string_view(value.data, value.len); + + // ...addCopy will add another header `w_name` associated with a copy of + // `w_value` + that->addCopy(w_name, w_value); +} \ No newline at end of file diff --git a/ego/src/cc/goc/responseheadermap.cc b/ego/src/cc/goc/responseheadermap.cc new file mode 100644 index 0000000..4bc02d2 --- /dev/null +++ b/ego/src/cc/goc/responseheadermap.cc @@ -0,0 +1,139 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include "envoy/http/header_map.h" + +#include "absl/strings/match.h" +#include "ego/src/cc/goc/proto/dto.pb.validate.h" +#include "envoy.h" + +void ResponseHeaderMap_add(void* responseHeaderMap, GoStr name, GoStr value) { + auto that = static_cast(responseHeaderMap); + + // The std::string() constructor will create a copy of name. Unfortunately, + // there is no std::string_view, because... + auto c_name = std::string(name.data, name.len); + + // ...LowerCaseString creates a wrapped lowercase copy of c_name. + auto w_name = Envoy::Http::LowerCaseString(c_name); + + // we are wrapping the header value with a lightweight string_view, which + // is safe because... + auto w_value = absl::string_view(value.data, value.len); + + // ...addCopy will add another header `w_name` associated with a copy of + // `w_value` + that->addCopy(w_name, w_value); +} + +void ResponseHeaderMap_set(void* responseHeaderMap, GoStr name, GoStr value) { + auto that = static_cast(responseHeaderMap); + + // The std::string() constructor will create a copy of name. Unfortunately, + // there is no std::string_view, because... + auto c_name = std::string(name.data, name.len); + + // ...LowerCaseString creates a wrapped lowercase copy of c_name. + auto w_name = Envoy::Http::LowerCaseString(c_name); + + // we are wrapping the header value with a lightweight string_view, which + // is safe because... + auto w_value = absl::string_view(value.data, value.len); + + // ...setCopy will add another header `w_name` associated with a copy of + // `w_value` + that->setCopy(w_name, w_value); +} + +void ResponseHeaderMap_append(void* responseHeaderMap, GoStr name, GoStr value) { + auto that = static_cast(responseHeaderMap); + + // The std::string() constructor will create a copy of name. Unfortunately, + // there is no std::string_view, because... + auto c_name = std::string(name.data, name.len); + + // ...LowerCaseString creates a wrapped lowercase copy of c_name. + auto w_name = Envoy::Http::LowerCaseString(c_name); + + // we are wrapping the header value with a lightweight string_view, which + // is safe because... + auto w_value = absl::string_view(value.data, value.len); + + // ...appendCopy will add another header `w_name` associated with a copy of + // `w_value` + that->appendCopy(w_name, w_value); +} + +void ResponseHeaderMap_remove(void* responseHeaderMap, GoStr name) { + auto that = static_cast(responseHeaderMap); + + // The std::string() constructor will create a copy of name. Unfortunately, + // there is no std::string_view, because... + auto c_name = std::string(name.data, name.len); + + // ...LowerCaseString creates a wrapped lowercase copy of c_name. + auto w_name = Envoy::Http::LowerCaseString(c_name); + that->remove(w_name); +} + +void ResponseHeaderMap_ContentType(void* responseHeaderMap, GoStr* value) { + ASSERT(nullptr != responseHeaderMap); + ASSERT(nullptr != value); + + auto that = static_cast(responseHeaderMap); + + if (nullptr == that->ContentType()) { + return; + } + auto contentType = that->ContentType()->value().getStringView(); + + // ContentType() returns a pointer, value() returns a reference, + // therefore getStringView().data() should be valid after return + value->len = contentType.size(); + value->data = const_cast(contentType.data()); +} + +void ResponseHeaderMap_get(void* responseHeaderMap, GoStr key, GoStr* value) { + ASSERT(nullptr != responseHeaderMap); + ASSERT(nullptr != value); + + auto that = static_cast(responseHeaderMap); + + auto c_name = std::string(key.data, key.len); + auto w_name = Envoy::Http::LowerCaseString(c_name); + + if (that->get(w_name) == nullptr) { + return; + } + + auto valStringView = that->get(w_name)->value().getStringView(); + + // get() returns a pointer, value() returns a reference, + // therefore getStringView().data() should be valid after return + value->len = valStringView.size(); + value->data = const_cast(valStringView.data()); +} + +void ResponseHeaderMap_Status(void* responseHeaderMap, GoStr* value) { + ASSERT(nullptr != responseHeaderMap); + ASSERT(nullptr != value); + + auto that = static_cast(responseHeaderMap); + + ASSERT(nullptr != that->Status()); + auto status = that->Status()->value().getStringView(); + + // Status() returns a pointer, value() returns a reference, + // therefore getStringView().data() should be valid after return + value->len = status.size(); + value->data = const_cast(status.data()); +} + +void ResponseHeaderMap_setStatus(void* responseHeaderMap, int status) { + ASSERT(nullptr != responseHeaderMap); + + auto that = static_cast(responseHeaderMap); + that->setStatus(status); +} diff --git a/ego/src/cc/goc/stats.cc b/ego/src/cc/goc/stats.cc new file mode 100644 index 0000000..4a8dbfb --- /dev/null +++ b/ego/src/cc/goc/stats.cc @@ -0,0 +1,114 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include "envoy/stats/scope.h" + +#include "common/stats/symbol_table_impl.h" + +#include "envoy.h" +#include "goc.h" + +// Stats::Scope +// +const void* Stats_Scope_counterFromStatName(void* scope, GoStr name) { + auto ptr = static_cast(scope); + auto w_name = absl::string_view(name.data, name.len); + + Envoy::Stats::StatNameManagedStorage storage(w_name, ptr->symbolTable()); + Envoy::Stats::StatName stat_name = storage.statName(); + + auto c = &ptr->counterFromStatName(stat_name); + return c; +} + +const void* Stats_Scope_gaugeFromStatName(void* scope, GoStr name, int importMode) { + auto ptr = static_cast(scope); + auto w_name = absl::string_view(name.data, name.len); + + Envoy::Stats::StatNameManagedStorage storage(w_name, ptr->symbolTable()); + Envoy::Stats::StatName stat_name = storage.statName(); + + auto c = &ptr->gaugeFromStatName(stat_name, Goc_Stats_ImportMode(importMode)); + return c; +} + +const void* Stats_Scope_histogramFromStatName(void* scope, GoStr name, int unit) { + auto ptr = static_cast(scope); + auto w_name = absl::string_view(name.data, name.len); + + Envoy::Stats::StatNameManagedStorage storage(w_name, ptr->symbolTable()); + Envoy::Stats::StatName stat_name = storage.statName(); + + auto c = &ptr->histogramFromStatName(stat_name, Goc_Stats_Unit(unit)); + return c; +} + +// Stats::Counter +// +void Stats_Counter_add(void* counter, uint64_t amount) { + auto ptr = static_cast(counter); + ptr->add(amount); +} + +void Stats_Counter_inc(void* counter) { + auto ptr = static_cast(counter); + ptr->inc(); +} + +uint64_t Stats_Counter_latch(void* counter) { + auto ptr = static_cast(counter); + return ptr->latch(); +} +void Stats_Counter_reset(void* counter) { + auto ptr = static_cast(counter); + ptr->reset(); +} +uint64_t Stats_Counter_value(void* counter) { + auto ptr = static_cast(counter); + return ptr->value(); +} + +// Stats::Gauge +// +void Stats_Gauge_add(void* gauge, uint64_t amount) { + auto ptr = static_cast(gauge); + ptr->add(amount); +} + +void Stats_Gauge_dec(void* gauge) { + auto ptr = static_cast(gauge); + ptr->dec(); +} + +void Stats_Gauge_inc(void* gauge) { + auto ptr = static_cast(gauge); + ptr->inc(); +} + +void Stats_Gauge_set(void* gauge, uint64_t value) { + auto ptr = static_cast(gauge); + ptr->set(value); +} + +void Stats_Gauge_sub(void* gauge, uint64_t amount) { + auto ptr = static_cast(gauge); + ptr->sub(amount); +} + +uint64_t Stats_Gauge_value(void* gauge) { + auto ptr = static_cast(gauge); + return ptr->value(); +} + +// Stats::Histogram +int Stats_Histogram_unit(void* histogram) { + auto ptr = static_cast(histogram); + return Goc_Stats_Unit_Value(ptr->unit()); +} + +void Stats_Histogram_recordValue(void* histogram, uint64_t value) { + auto ptr = static_cast(histogram); + ptr->recordValue(value); +} \ No newline at end of file diff --git a/ego/src/go/BUILD.bazel b/ego/src/go/BUILD.bazel new file mode 100644 index 0000000..cda315f --- /dev/null +++ b/ego/src/go/BUILD.bazel @@ -0,0 +1,25 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "httpfilter.go", + "registry.go", + ], + importpath = "github.com/grab/ego/ego/src/go", + visibility = ["//visibility:public"], + deps = [ + "//ego/src/go/envoy:go_default_library", + "//ego/src/go/envoy/datastatus:go_default_library", + "//ego/src/go/envoy/headersstatus:go_default_library", + "//ego/src/go/envoy/loglevel:go_default_library", + "//ego/src/go/envoy/trailersstatus:go_default_library", + "//ego/src/go/logger:go_default_library", + "//ego/src/go/volatile:go_default_library", + ], +) diff --git a/ego/src/go/README.md b/ego/src/go/README.md new file mode 100644 index 0000000..0226d2c --- /dev/null +++ b/ego/src/go/README.md @@ -0,0 +1,39 @@ +# Layout / Packages + +## src/cc/filter + +An envoy filter dispatching to the go runtime. + +## src/go + +A handful of Golang packages (`ego`) abstracting the interaction between Go code +and the Envoy runtime. + +### internal/cgo + +This package exists mostly because we have trouble linking CGO exports from +multiple packages (). For the similar +reasons, it contains the (unused) `main()` function. + +It contains all the CGO stubs for dispatching envoy filter callbacks to Go +handlers. Since we need to avoid circular imports, we wrap all call-backs to +the envoy runtime via interfaces declared in the [envoy](#envoy) package. + +This also simplifies development as all of these dependencies can be mocked. + +### envoy + +This package abstracts all interactions with the C runtime. The interfaces are +instantiated by the [cgo](#cgo) package, which can't be imported by the filter +packages due to language and linker constraints. + +### volatile + +This package exists mostly because `volatile.String` looks better than +`envoy.VolatileString`. + +### stub + +This package contains mocks for some of the interfaces defined in the +[envoy](#envoy) package. This is useful for running unit tests without having +to build envoy. diff --git a/ego/src/go/envoy/BUILD.bazel b/ego/src/go/envoy/BUILD.bazel new file mode 100644 index 0000000..80294fc --- /dev/null +++ b/ego/src/go/envoy/BUILD.bazel @@ -0,0 +1,21 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["envoy.go"], + importpath = "github.com/grab/ego/ego/src/go/envoy", + visibility = ["//visibility:public"], + deps = [ + "//ego/src/cc/goc/proto:go_default_library", + "//ego/src/go/envoy/lifespan:go_default_library", + "//ego/src/go/envoy/loglevel:go_default_library", + "//ego/src/go/envoy/statetype:go_default_library", + "//ego/src/go/envoy/stats:go_default_library", + "//ego/src/go/volatile:go_default_library", + ], +) diff --git a/ego/src/go/envoy/datastatus/BUILD.bazel b/ego/src/go/envoy/datastatus/BUILD.bazel new file mode 100644 index 0000000..12d2730 --- /dev/null +++ b/ego/src/go/envoy/datastatus/BUILD.bazel @@ -0,0 +1,13 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["const.go"], + importpath = "github.com/grab/ego/ego/src/go/envoy/datastatus", + visibility = ["//visibility:public"], +) diff --git a/ego/src/go/envoy/datastatus/const.go b/ego/src/go/envoy/datastatus/const.go new file mode 100644 index 0000000..83a0c0e --- /dev/null +++ b/ego/src/go/envoy/datastatus/const.go @@ -0,0 +1,20 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package datastatus + +// Envoy::Http::FilterDataStatus +// The constants have been chosen with arbtirary offsets to easier detect +// return value type mismatches. +// +// see //envoy/include/envoy/http/filter.h + +type Type int +const ( + Continue Type = 300 + StopIterationAndBuffer Type = 301 + StopIterationAndWatermark Type = 302 + StopIterationNoBuffer Type = 303 +) diff --git a/ego/src/go/envoy/envoy.go b/ego/src/go/envoy/envoy.go new file mode 100644 index 0000000..52990d3 --- /dev/null +++ b/ego/src/go/envoy/envoy.go @@ -0,0 +1,238 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package envoy + +import ( + "io" + + pb "github.com/grab/ego/ego/src/cc/goc/proto" + + "github.com/grab/ego/ego/src/go/envoy/lifespan" + "github.com/grab/ego/ego/src/go/envoy/loglevel" + "github.com/grab/ego/ego/src/go/envoy/statetype" + "github.com/grab/ego/ego/src/go/envoy/stats" + "github.com/grab/ego/ego/src/go/volatile" +) + +type GoHttpFilterConfig interface { + // Settings returns a pointer to the underlying envoy + // configuration data (a protobuf Any field). The data + // must not be modified nor must references be retained + // beyond the duration of the filter factory creation + // call. If in doubt, please use Copy() on the result. + Settings() volatile.Bytes + Scope() Scope +} + +type GoHttpFilter interface { + Post(uint64) + DecoderCallbacks() DecoderFilterCallbacks + EncoderCallbacks() EncoderFilterCallbacks + Pin() + Unpin() + Log(loglevel.Type, string) + ResolveMostSpecificPerGoFilterConfig(name string, route Route) interface{} + GenericSecretProvider() GenericSecretConfigProvider +} + +type StreamFilterCallbacks interface { + StreamInfo() StreamInfo + Route() Route + RouteExisting() bool + ActiveSpan() Span +} + +type DecoderFilterCallbacks interface { + StreamFilterCallbacks + ContinueDecoding() + SendLocalReply(responseCode int, body string, headers map[string]string, details string) + AddDecodedData(buffer BufferInstance, streamingFilter bool) + DecodingBuffer() BufferInstance + EncodeHeaders(responseCode int, headers *pb.ResponseHeaderMap, endStream bool) +} + +type EncoderFilterCallbacks interface { + StreamFilterCallbacks + EncodingBuffer() BufferInstance + AddEncodedData(buffer BufferInstance, streamingFilter bool) + ContinueEncoding() +} + +// BufferInstance is a proxy for Envoy::Buffer::Instance +// +// See //envoy/include/envoy/http/header_map.h +type BufferInstance interface { + // Copy up to len(p) bytes of data from the buffer to p and return the + // actual number of bytes retrieved. + CopyOut(start uint64, p []byte) int + + // Retrieves all raw slices + GetRawSlices() []volatile.Bytes + + // Retrieve the net total length of data stored in this buffer + Length() uint64 + + // NewReader is a Go convenience function + NewReader(start uint64) io.Reader +} + +// RequestHeaderMap is a proxy for Envoy::Http::RequestHeaderMap +// +// See //envoy/include/envoy/http/header_map.h +type HeaderMap interface { + HeaderMapReadOnly + headerMapUpdatable +} + +type HeaderMapReadOnly interface { + Get(name string) volatile.String +} + +type headerMapUpdatable interface { + AddCopy(name, value string) + SetCopy(name, value string) + AppendCopy(name, value string) + Remove(name string) +} + +type RequestOrResponseHeaderMap interface { + RequestOrResponseHeaderMapReadOnly + requestOrResponseHeaderMapUpdatable +} + +type RequestOrResponseHeaderMapReadOnly interface { + HeaderMapReadOnly + ContentType() volatile.String +} + +type requestOrResponseHeaderMapUpdatable interface { + headerMapUpdatable +} + +type RequestHeaderMap interface { + RequestHeaderMapReadOnly + requestHeaderMapUpdatable +} + +type RequestHeaderMapReadOnly interface { + RequestOrResponseHeaderMapReadOnly + Path() volatile.String + Method() volatile.String + Authorization() volatile.String + // There is no method with this name in Envoy. + // It's a utilitity for Go filters to query headers by prefix. + GetByPrefix(prefix string) map[string][]string +} + +type requestHeaderMapUpdatable interface { + requestOrResponseHeaderMapUpdatable + SetPath(path string) +} + +type RequestTrailerMap interface { + RequestTrailerMapReadOnly + requestTrailerMapUpdatable +} + +type requestTrailerMapUpdatable interface { + headerMapUpdatable +} + +type RequestTrailerMapReadOnly interface { + HeaderMapReadOnly +} + +type ResponseHeaderMap interface { + ResponseHeaderMapReadOnly + responseHeaderMapUpdatable +} + +type ResponseHeaderMapReadOnly interface { + RequestOrResponseHeaderMapReadOnly + Status() volatile.String +} + +type responseHeaderMapUpdatable interface { + requestOrResponseHeaderMapUpdatable + SetStatus(status int) +} + +type StreamInfo interface { + FilterState() FilterState + LastDownstreamTxByteSent() int64 + GetRequestHeaders() RequestHeaderMapReadOnly + ResponseCode() int + ResponseCodeDetails() volatile.String +} + +type FilterState interface { + SetData(name, value string, stateType statetype.Type, lifeSpan lifespan.Type) + GetDataReadOnly(name string) (volatile.String, bool) +} + +type Route interface { + RouteEntry() RouteEntry +} + +type RouteEntry interface { + PathMatchCriterion() PathMatchCriterion +} + +type PathMatchType int + +const ( + PathMatchNone PathMatchType = iota + PathMatchPrefix + PathMatchExact + PathMatchRegex +) + +type PathMatchCriterion interface { + MatchType() (PathMatchType, error) + Matcher() (volatile.String, error) +} + +type GenericSecretConfigProvider interface { + Secret() volatile.String +} + +// Scope envoy/include/envoy/stats/scope.h +type Scope interface { + CounterFromStatName(name string) Counter + GaugeFromStatName(name string, importMode stats.ImportMode) Gauge + HistogramFromStatName(name string, unit stats.Unit) Histogram +} + +// Counter envoy/include/envoy/stats/stats.h +type Counter interface { + Add(amount uint64) + Inc() + Latch() uint64 + Reset() + Value() uint64 +} + +// Gauge envoy/include/envoy/stats/stats.h +type Gauge interface { + Add(amount uint64) + Dec() + Inc() + Set(value uint64) + Sub(amount uint64) + Value() uint64 +} + +// Histogram envoy/include/envoy/stats/histogram.h +type Histogram interface { + Unit() stats.Unit + RecordValue(value uint64) +} + +type Span interface { + GetContext() map[string][]string + SpawnChild(name string) Span + FinishSpan() +} diff --git a/ego/src/go/envoy/headersstatus/BUILD.bazel b/ego/src/go/envoy/headersstatus/BUILD.bazel new file mode 100644 index 0000000..9688c33 --- /dev/null +++ b/ego/src/go/envoy/headersstatus/BUILD.bazel @@ -0,0 +1,13 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["const.go"], + importpath = "github.com/grab/ego/ego/src/go/envoy/headersstatus", + visibility = ["//visibility:public"], +) diff --git a/ego/src/go/envoy/headersstatus/const.go b/ego/src/go/envoy/headersstatus/const.go new file mode 100644 index 0000000..49589aa --- /dev/null +++ b/ego/src/go/envoy/headersstatus/const.go @@ -0,0 +1,20 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package headersstatus + +// Envoy::Http::FilterHeadersStatus +// The constants have been chosen with arbtirary offsets to easier detect +// return value type mismatches. +// +// see //envoy/include/envoy/http/filter.h +type Type int +const ( + Continue Type = 100 + StopIteration Type = 101 + ContinueAndEndStream Type = 102 + StopAllIterationAndBuffer Type = 103 + StopAllIterationAndWatermark Type = 104 +) diff --git a/ego/src/go/envoy/lifespan/BUILD.bazel b/ego/src/go/envoy/lifespan/BUILD.bazel new file mode 100644 index 0000000..bcef894 --- /dev/null +++ b/ego/src/go/envoy/lifespan/BUILD.bazel @@ -0,0 +1,13 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["const.go"], + importpath = "github.com/grab/ego/ego/src/go/envoy/lifespan", + visibility = ["//visibility:public"], +) diff --git a/ego/src/go/envoy/lifespan/const.go b/ego/src/go/envoy/lifespan/const.go new file mode 100644 index 0000000..c626bcc --- /dev/null +++ b/ego/src/go/envoy/lifespan/const.go @@ -0,0 +1,20 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package lifespan + +// Envoy::StreamInfo::FilterState::LifeSpan +// The constants have been chosen with arbtirary offsets to easier detect +// return value type mismatches. +// +// see //envoy/include/envoy/stream_info/filter_state.h +type Type int + +const ( + FilterChain Type = 1 + DownstreamRequest Type = 2 + DownstreamConnection Type = 3 + TopSpan Type = 4 +) diff --git a/ego/src/go/envoy/loglevel/BUILD.bazel b/ego/src/go/envoy/loglevel/BUILD.bazel new file mode 100644 index 0000000..45c1609 --- /dev/null +++ b/ego/src/go/envoy/loglevel/BUILD.bazel @@ -0,0 +1,13 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["const.go"], + importpath = "github.com/grab/ego/ego/src/go/envoy/loglevel", + visibility = ["//visibility:public"], +) diff --git a/ego/src/go/envoy/loglevel/const.go b/ego/src/go/envoy/loglevel/const.go new file mode 100644 index 0000000..7487ed5 --- /dev/null +++ b/ego/src/go/envoy/loglevel/const.go @@ -0,0 +1,20 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package loglevel + +// spdlog::level::level_enum +// +// see https://github.com/gabime/spdlog/blob/master/include/spdlog/common.h#L78 + +type Type int +const ( + Trace Type = 0 + Debug Type = 1 + Info Type = 2 + Warn Type = 3 + Error Type = 4 + Critical Type = 5 +) diff --git a/ego/src/go/envoy/statetype/BUILD.bazel b/ego/src/go/envoy/statetype/BUILD.bazel new file mode 100644 index 0000000..071bc60 --- /dev/null +++ b/ego/src/go/envoy/statetype/BUILD.bazel @@ -0,0 +1,13 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["const.go"], + importpath = "github.com/grab/ego/ego/src/go/envoy/statetype", + visibility = ["//visibility:public"], +) diff --git a/ego/src/go/envoy/statetype/const.go b/ego/src/go/envoy/statetype/const.go new file mode 100644 index 0000000..27cae19 --- /dev/null +++ b/ego/src/go/envoy/statetype/const.go @@ -0,0 +1,18 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package statetype + +// Envoy::StreamInfo::FilterState::StateType +// The constants have been chosen with arbtirary offsets to easier detect +// return value type mismatches. +// +// see //envoy/include/envoy/stream_info/filter_state.h +type Type int + +const ( + ReadOnly Type = 1 + Mutable Type = 2 +) diff --git a/ego/src/go/envoy/stats/BUILD.bazel b/ego/src/go/envoy/stats/BUILD.bazel new file mode 100644 index 0000000..93ef4bd --- /dev/null +++ b/ego/src/go/envoy/stats/BUILD.bazel @@ -0,0 +1,13 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["const.go"], + importpath = "github.com/grab/ego/ego/src/go/envoy/stats", + visibility = ["//visibility:public"], +) diff --git a/ego/src/go/envoy/stats/const.go b/ego/src/go/envoy/stats/const.go new file mode 100644 index 0000000..a0ccb91 --- /dev/null +++ b/ego/src/go/envoy/stats/const.go @@ -0,0 +1,39 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package stats + +// see envoy/include/envoy/stats/stats.h + +// ImportMode for Gauge Metric +type ImportMode int + +const ( + // Uninitialized means Gauge was discovered during hot-restart transfer. + Uninitialized ImportMode = 1 + // NeverImport means On hot-restart, each process starts with gauge at 0. + NeverImport ImportMode = 2 + // Accumulate means Transfers gauge state on hot-restart. + Accumulate ImportMode = 3 +) + +// see envoy/include/envoy/stats/histogram.h + +// Unit for Histogram Metric +type Unit int + +const ( + // Null means The histogram has been rejected, i.e. it's a null histogram + // and is not recording anything. + Null Unit = 1 + // Unspecified means Measured quantity does not require a unit, e.g. "items". + Unspecified Unit = 2 + // Bytes ... + Bytes Unit = 3 + // Microseconds ... + Microseconds Unit = 4 + // Milliseconds ... + Milliseconds Unit = 5 +) diff --git a/ego/src/go/envoy/trailersstatus/BUILD.bazel b/ego/src/go/envoy/trailersstatus/BUILD.bazel new file mode 100644 index 0000000..54af086 --- /dev/null +++ b/ego/src/go/envoy/trailersstatus/BUILD.bazel @@ -0,0 +1,13 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["const.go"], + importpath = "github.com/grab/ego/ego/src/go/envoy/trailersstatus", + visibility = ["//visibility:public"], +) diff --git a/ego/src/go/envoy/trailersstatus/const.go b/ego/src/go/envoy/trailersstatus/const.go new file mode 100644 index 0000000..8f14426 --- /dev/null +++ b/ego/src/go/envoy/trailersstatus/const.go @@ -0,0 +1,17 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package trailersstatus + +// Envoy::Http::FilterTrailerStatus +// The constants have been chosen with arbtirary offsets to easier detect +// return value type mismatches. +// +// see //envoy/include/envoy/http/filter.h +type Type int +const ( + Continue Type = 201 + StopIteration Type = 202 +) diff --git a/ego/src/go/httpfilter.go b/ego/src/go/httpfilter.go new file mode 100644 index 0000000..ae923c9 --- /dev/null +++ b/ego/src/go/httpfilter.go @@ -0,0 +1,127 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package ego + +import ( + "context" + + "github.com/grab/ego/ego/src/go/envoy" + "github.com/grab/ego/ego/src/go/envoy/datastatus" + "github.com/grab/ego/ego/src/go/envoy/headersstatus" + "github.com/grab/ego/ego/src/go/envoy/loglevel" + "github.com/grab/ego/ego/src/go/envoy/trailersstatus" + "github.com/grab/ego/ego/src/go/logger" +) + +type HttpFilterFactory func(native envoy.GoHttpFilter) HttpFilter + +type HttpFilter interface { + StreamDecoderFilter + StreamEncoderFilter + OnPost(uint64) + OnDestroy() + Logger() logger.Logger +} + +type StreamDecoderFilter interface { + DecodeHeaders(envoy.RequestHeaderMap, bool) headersstatus.Type + DecodeData(envoy.BufferInstance, bool) datastatus.Type + DecodeTrailers(envoy.RequestTrailerMap) trailersstatus.Type +} + +type StreamEncoderFilter interface { + EncodeHeaders(envoy.ResponseHeaderMap, bool) headersstatus.Type + EncodeData(envoy.BufferInstance, bool) datastatus.Type +} + +type HttpFilterBase struct { + Context context.Context + Cancel context.CancelFunc + Native envoy.GoHttpFilter +} + +func (f *HttpFilterBase) Init(native envoy.GoHttpFilter) { + f.Native = native + f.Context, f.Cancel = context.WithCancel(context.Background()) +} + +func (f *HttpFilterBase) Logger() logger.Logger { + return filterLogger{f.Native} +} + +func (f *HttpFilterBase) Pin() { + f.Native.Pin() +} + +func (f *HttpFilterBase) Recover() { + if err := recover(); err != nil { + // TODO log error + } +} + +func (f *HttpFilterBase) Unpin() { + f.Native.Unpin() + f.Recover() +} + +func (f *HttpFilterBase) OnDestroy() { + f.Cancel() +} + +func (f *HttpFilterBase) OnPost(tag uint64) { +} + +func (f *HttpFilterBase) DecodeData(data envoy.BufferInstance, endStream bool) datastatus.Type { + return datastatus.Continue +} + +func (f *HttpFilterBase) DecodeTrailers(trailes envoy.RequestTrailerMap) trailersstatus.Type { + return trailersstatus.Continue +} + +func (f *HttpFilterBase) DecodeHeaders(headers envoy.RequestHeaderMap, endStream bool) headersstatus.Type { + return headersstatus.Continue +} + +func (f *HttpFilterBase) EncodeHeaders(headers envoy.ResponseHeaderMap, endStream bool) headersstatus.Type { + return headersstatus.Continue +} + +func (f *HttpFilterBase) EncodeData(envoy.BufferInstance, bool) datastatus.Type { + return datastatus.Continue +} + +type filterLogger struct { + Native envoy.GoHttpFilter +} + +func (l filterLogger) Trace(message string, data ...interface{}) { + l.log(loglevel.Trace, message, data...) +} + +func (l filterLogger) Debug(message string, data ...interface{}) { + l.log(loglevel.Debug, message, data...) +} + +func (l filterLogger) Info(message string, data ...interface{}) { + l.log(loglevel.Info, message, data...) +} + +func (l filterLogger) Warn(message string, data ...interface{}) { + l.log(loglevel.Warn, message, data...) +} + +func (l filterLogger) Error(message string, data ...interface{}) { + l.log(loglevel.Error, message, data...) +} + +func (l filterLogger) Critical(message string, data ...interface{}) { + l.log(loglevel.Critical, message, data...) +} + +func (l filterLogger) log(level loglevel.Type, message string, data ...interface{}) { + l.Native.Log(level, logger.Render(message, data...)) +} diff --git a/ego/src/go/internal/cgo/BUILD.bazel b/ego/src/go/internal/cgo/BUILD.bazel new file mode 100644 index 0000000..70d950c --- /dev/null +++ b/ego/src/go/internal/cgo/BUILD.bazel @@ -0,0 +1,86 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") + +# Maintained by Gazelle. The only customisation is +# `cdeps = ["//ego/src/cc/cgo:native"]`, which gives us access to the downcall +# proxies and header file ("//ego/src/cc/cgo/envoy.h"). +go_library( + name = "go_default_library", + srcs = [ + "bufferinstance.go", + "clutch.go", + "cutils.go", + "decoder_callbacks.go", + "encoder_callbacks.go", + "filter_state.go", + "gohttpfilter.go", + "gohttpfilterconfig.go", + "logger.go", + "main.go", + "requestheadermap.go", + "requesttrailermap.go", + "responseheadermap.go", + "route.go", + "span.go", + "stats.go", + "stream_info.go", + ], + cdeps = ["//ego/src/cc/goc:goc"], + cgo = True, + importpath = "github.com/grab/ego/ego/src/go/internal/cgo", + visibility = ["//visibility:private"], + deps = [ + "//ego/src/cc/goc/proto:go_default_library", + "//ego/src/go:go_default_library", + "//ego/src/go/envoy:go_default_library", + "//ego/src/go/envoy/datastatus:go_default_library", + "//ego/src/go/envoy/headersstatus:go_default_library", + "//ego/src/go/envoy/lifespan:go_default_library", + "//ego/src/go/envoy/loglevel:go_default_library", + "//ego/src/go/envoy/statetype:go_default_library", + "//ego/src/go/envoy/stats:go_default_library", + "//ego/src/go/envoy/trailersstatus:go_default_library", + "//ego/src/go/logger:go_default_library", + "//ego/src/go/volatile:go_default_library", + "//egofilters:go_default_library", + "@com_github_golang_protobuf//proto:go_default_library", + ], +) + +# :cgo contains the Go portions of our envoy extensions. +go_binary( + name = "cgo", + cdeps = [ + "//ego/src/cc/goc:goc", + ], + embed = [":go_default_library"], + linkmode = "c-archive", + visibility = ["//visibility:public"], +) + +go_test( + name = "go_default_test", + srcs = ["clutch_test.go"], + embed = [":go_default_library"], + importpath = "github.com/grab/ego/ego/src/go/internal/cgo", + deps = [ + "@com_github_stretchr_testify//assert:go_default_library", + ], +) + +go_test( + name = "go_clutch_test", + srcs = [ + "clutch.go", + "clutch_test.go", + ], + cgo = True, + importpath = "github.com/grab/ego/ego/src/go/internal/cgo", + deps = [ + "@com_github_stretchr_testify//assert:go_default_library", + ], +) diff --git a/ego/src/go/internal/cgo/bufferinstance.go b/ego/src/go/internal/cgo/bufferinstance.go new file mode 100644 index 0000000..b6bdf08 --- /dev/null +++ b/ego/src/go/internal/cgo/bufferinstance.go @@ -0,0 +1,68 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package main + +// #include "ego/src/cc/goc/envoy.h" +import "C" +import ( + "io" + "unsafe" + + "github.com/grab/ego/ego/src/go/volatile" +) + +// bufferInstance implements envoy.BufferInstance +// +type bufferInstance struct { + ptr unsafe.Pointer +} + +func (b bufferInstance) CopyOut(start uint64, dest []byte) int { + return int(C.BufferInstance_copyOut(b.ptr, C.size_t(start), GoBuf(dest))) +} + +func (b bufferInstance) GetRawSlices() []volatile.Bytes { + + // FIXME: since this is not thread safe, we could just buffer the result + // for BufferInstance_getRawSlices to pick it up without having to + // call getRawSlices a second time... + max := int(C.BufferInstance_getRawSlicesCount(b.ptr)) + if 0 == max { + return nil + } + + temp := make([]C.GoBuf, max) + count := int(C.BufferInstance_getRawSlices(b.ptr, C.uint64_t(max), &temp[0])) + dest := make([]volatile.Bytes, count) + for i := 0; i < count; i++ { + dest[i] = CBytes(temp[i].data, temp[i].len, temp[i].cap) + } + return dest +} + +func (b bufferInstance) Length() uint64 { + return uint64(C.BufferInstance_length(b.ptr)) +} + +func (b bufferInstance) NewReader(start uint64) io.Reader { + return &bufferInstanceReader{bufferInstance: b, pos: start} +} + +type bufferInstanceReader struct { + bufferInstance + pos uint64 +} + +func (r *bufferInstanceReader) Read(p []byte) (n int, err error) { + if 0 < len(p) { + if n = r.CopyOut(r.pos, p); n <= 0 { + err = io.EOF + } else { + r.pos += uint64(n) + } + } + return +} diff --git a/ego/src/go/internal/cgo/clutch.go b/ego/src/go/internal/cgo/clutch.go new file mode 100644 index 0000000..970fb6a --- /dev/null +++ b/ego/src/go/internal/cgo/clutch.go @@ -0,0 +1,590 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package main + +import "C" +import ( + "container/heap" + "sync" + "sync/atomic" +) + +// TL;DR: clutch is mostly an optimised, thread safe map[uint64]interface{}. +// +// clutch is a self-compacting three-level registry with low-latency, lock-free +// O(1) lookup / insertion / removal, and BOUNDED CAPACITY of 16M entries per +// thread (adjustable at the expense of the minimum per-thread memory overhead) +// The name derives from the objective of coupling the envoy filter and the go +// filter with minimal friction. In fact, the problem it solves is very similar +// to a memory allocator that wants to return unused pages to the operating +// system. Since discovering that every C->Go call requires a mutex interaction +// anyway (see src/runtime/cgo/gcc_libinit.c:_cgo_wait_runtime_init_done), this +// may be considered a slight case of overengineering :) +// +// It is PURPOSE-BUILT as part of a C-to-Golang shim for envoy and maintains +// references to Go objects conceptually owned by envoy filter instances (C++ +// objects). It makes various assumptions that may not hold in a more general +// setting. For example, lookup is free from data races only if all Get(), +// Tag(), and Remove() calls are coming from the same thread as the associated +// call to AcquireSlot(). This is guaranteed by envoy's threading model (see +// https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310) where filter +// callbacks are always executed in the same thread in order to remove the need +// for locking). However, Get() is also thread safe as long as the registered +// item is guaranteed to exist, which is the case when accessing filter factory +// data upon filter creation. +// +// Registered items receive a tag (an "address") that is unique as long as the +// item is not removed from the registry. Tags are also unlikely to be reused +// with high probability, which provides a reasonable amount of protection +// against damage in the case of accidental double removals. +// +// Tags have an internal structure that helps with the retrieval of the item: +// +// tag +// +------+------+------+-------+ +// | mark | slot | hunk | entry | +// +------+------+------+-------+ +// 28bits 12bits 12bits 12bits +// +// These fields refer to these clutch data structures +// +// clutch (64KB) +// +-------+-------+-------+-------+ +// | *slot | *slot | ... | *slot | slots: 4K slot entries +// +-------+-------+-------+-------+ +// | int | int | | int | free-entry-index-stack +// +-------+-------+-------+-------+ +// +// slot (48KB) +// +-------+-------+-------+-------+ +// | *hunk | *hunk | ... | *hunk | hunks: ~4K hunk pointers +// +-------+-------+-------+-------+ +// | int | int | | int | hunk-usage-max-heap +// +-------+-------+-------+-------+ +// +// hunk (80KB) +// +------+------+-------+------+ +// | item | item | ... | item | items: ~4K items pairs +// +------+------+-------+------+ +// | int | int | | int | free-entry-index-stack / item marks +// +------+------+-------+------+ +// +// +// hence, the item for a given tag can be retrieved using three array lookups: +// +// entry = clutch.slots[tag.slot].hunks[tag.hunk].entries[tag.entry] +// +// As a safety, it is checked if entry.mark equals tag.mark. +// +// The first level of indirection ("slots") is used to shard the registry and +// avoid lock contention. +// +// The second level of indirection ("hunks") is used to ensure the management +// data structures ("hunk") are released quickly when they become sparsely +// populated. This is achieved by always preferring the fullest partially +// filled hunk for registering new items (this is what the hunk-index-heap is +// used for). Besides reducing memory overhead, this is also should improve +// cache locality. Each hunk requires approximately 80KB of RAM. +// +// The third level of indirection ("items") is a simple array with a free entry +// index. The LIFO nature of the free elements index is also hoped to improve +// cache locality. +// +// Since the first allocated hunk of any slot is never released, a realistic +// memory estimate is about 5MB of memory per instance when running 32 threads. +// +type clutch struct { + sync.RWMutex // lock for slot 0 + + slots [slotsLen]*slot + next [slotsLen]uint64 + head uint64 // first free thread slot minus 1 +} + +const ( + // changing these values to something other than (hunkBase: 4096, + // hunkCount: 4096, slotCount: 64) voids warranty. While there may be good + // reasons for this, please do think twice. + + // spare has two purposes: for one, it reduces the struct sizes a little to + // better fit traditional allocation boundaries, for another, it adds some + // redundancy into the address space which is needed to make the encoding + // more resilient. spare needs to be at least 1 and at least one of the + // slotsLen, itemsLen or hunksLen constants need to be adjusted. + spare = 10 + + itemsBase = 1 + maxItems = 4096 // must be < 2G + itemsLen = maxItems - spare + + hunkBase = maxItems * itemsBase + maxHunks = 4096 // must be < 2G + hunksLen = maxHunks - spare // ~16M filter instances / thread + + slotBase = maxHunks * hunkBase + maxSlots = 4096 // 4096 threads should be enough for everyone o_0 + slotsLen = maxSlots // no spares + + markBase = maxSlots * slotBase + + // special tag to indicate out-of-space + noAddr = ^uint32(0) +) + +// AcquireSlot implements a lock-free, concurrent version of the free list +// management employed by hunk. It returns a versioned slot reference to +// avoid accidental double releases. A return value of 0 is a valid slot +// identifier, but it also indicates a problem (e.g. out of slots, or an +// implementation bug). clutch operations on slot 0 may be slower as they +// use a mutex for coordination between threads. +func (c *clutch) AcquireSlot() uint64 { + + for { + // add 1 to reserve slot 0 for overflow and default handling + head := atomic.LoadUint64(&c.head) + 1 + + // head uses the high bits as version counter. in other words, + // head = version * slotsLen + index. This is motivated further + // down. + index := head % slotsLen + if index == 0 { + // we have allocated slotsLen-1 slots and need to use the + // reserved slot 0. We will fall back to Mutex locking for + // when calling Tag() with slot 0 + return 0 + } + + next := c.next[index] + if 0 == next { + // not initialised --> use implied default + next = index + 1 + } + + // c.next[index] this isn't ours, yet. Therefore, it may be modified + // between now and the CompareAndSwapUint32 call, causing the value + // held in next to become stale. For example: + // + // Thread 1 Thread 2 Thread 3 + // z := Acquire() + // Release(a) + // Release(b) + // + // x := Acquire: + // ...head -> b + // ...next -> a + // b := Acquire() + // ...head -> b + // ...next -> a + // ...head <- a + // Release(z) + // ...head -> a + // ...next <- a + // ...head <- z + // Release(b) + // ...head -> z + // ...next <- z + // ...head <- b + // + // head <- a *BANG* + // + // In this example, without further precautions, we would lose entry + // z forever, because the CompareAndSwapUint32 would see head unchanged + // and proceed. + // + // Therefore, we have embedded a version counter for the value of + // c.next[index] into the relevant reference to it (c.head, or other + // c.next[] entries). This version counter is updated in ReleaseSlot(). + // + // With this guard in place, it would need significant time of thread + // suspension until this counter completes a full cycle (likely longer + // than needed to read this comment), perfect timing, and the need for + // index to have arrive at the same value via a release sequence in + // order to provoke a harmful collision. + + if atomic.CompareAndSwapUint64(&c.head, head-1, next-1) { + + // allocate a new slot and remember its version in order to + // detect double free and possibly other problems. + c.slots[index], c.next[index] = &slot{}, head + + return head + } + } +} + +// ReleaseSlot() releases a thread slot given a versioned slot identifier. +func (c *clutch) ReleaseSlot(head uint64) { + + if 0 == head { + // don't release slot 0. + return + } + + index := head % slotsLen // strip version + if c.next[index] != head { + // FIXME: log corrupt / double free + return + } + + c.slots[index].release() + c.slots[index] = nil + + head = head + slotsLen // increment version count of future head + for { + next := atomic.LoadUint64(&c.head) + 1 // load future head.next + c.next[index] = next // we still own the slot, so this is safe + + // we use a simple mod count to avoid ABA issues. This is not safe in + // general, but we know that Release() is called only when a thread + // terminates. Since this is a rather heavy operation, we can assume + // that wrap-around won't occur while another thread is retrying its + // Acquire() operation. + if atomic.CompareAndSwapUint64(&c.head, next-1, head-1) { + return + } + } +} + +// TagItem stores a reference to item and returns a tag for it using which it +// can be retrieved. head must be a value returned by AcquireSlot(). +// A return value of 0 indicates the item could not be registered. +func (c *clutch) TagItem(head uint64, item interface{}) uint64 { + + if nil == item { + return 0 + } + + if head == 0 { + // something went wrong booking a slot --> use shared one + c.Lock() + defer c.Unlock() + if nil == c.slots[0] { + c.slots[0] = &slot{} + } + } + + index := head % slotsLen // strip version + slot := c.slots[index] + if c.next[index] != head || nil == slot { + // ouch. slot reference expired or invalid slot 0 reference, or wtf. + // TODO: log/stat + + // anyway, please don't crash if we can help it. + if 0 == head { + return 0 + } + return c.TagItem(0, item) + } + + // generate simplistic "checksum" that fits into the free tag bits + mark := uint32((slot.version*markBase + head*0x9e3779b9) / markBase) + + // allocate an address + addr := slot.add(item, mark) + if noAddr == addr { + // everything booked. not good. + // TODO: log/stat + + // try to buy some time... + if 0 == head { + return 0 + } + return c.TagItem(0, item) + } + + slot.version++ + + // assemble & return encode tag value. + // NOTE: if spare were zero, there is a corner case where we would return + // a tag value of 0. + return uint64(mark)*markBase + index*slotBase + uint64(addr) + spare/spare +} + +// GetItem returns a value previously registered with `tag`, or nil if no value +// was registered for `tag` or it was removed in the meantime (and no value has +// received the same tag -- which is possible but unlikely). +func (c *clutch) GetItem(tag uint64) interface{} { + return c.get(tag, false) +} + +// RemoveItem removes and returns a value previously registered with `tag`, or +// returns nil if no value was registered for `tag` or it was removed in the +// meantime (and no value has received the same tag -- which is possible but +// unlikely). +func (c *clutch) RemoveItem(tag uint64) interface{} { + return c.get(tag, true) +} + +// get() returns the item associated with tag, or nil if no such entry could +// be found. The entry will be removed if (and only if) `remove` is `true`. +func (c *clutch) get(tag uint64, remove bool) interface{} { + + if 0 == tag { + return nil + } + + tag -= spare / spare // sames as 1 but ensures 0 != spare + + mark := tag / markBase + tag -= mark * markBase + + slot := tag / slotBase + tag -= slot * slotBase + + if slotsLen <= slot { + // TODO: log/stat corruption + return nil + } + + // shared slot? + if 0 == slot { + if remove { + c.Lock() + defer c.Unlock() + } else { + c.RLock() + defer c.RUnlock() + } + } + + return c.slots[slot].get(uint32(tag), uint32(mark), remove) +} + +// slot implements heap.Interface for hunks.free to sort hunks by use count. +// This is done in order to expedite the release of hunks that are already +// less full than others (because they won't receive new items until they +// become the fullest hunk). +type slot struct { + hunks [hunksLen]*hunk + free [hunksLen]uint32 + version uint64 +} + +// Len implements heap.Interface.Len +func (s *slot) Len() int { + return hunksLen +} + +// Swap implements heap.Interface.Swap +func (s *slot) Swap(i, j int) { + + // s.free is a heap with indexes to free entries (increased by 1 + // to avoid the need for init). + if fi, fj := s.free[i], s.free[j]; fi != fj { + + if fi == 0 { + fi = uint32(i + 1) // uninitialised --> use default + } else if hi := s.hunks[fi-1]; hi != nil { + hi.rank = j + } + + if fj == 0 { + fj = uint32(j + 1) // uninitialised --> use default + } else if hj := s.hunks[fj-1]; hj != nil { + hj.rank = i + } + + s.free[i], s.free[j] = fj, fi + } +} + +// key defines our heap order +func (s *slot) key(i int) int { + + // construct a key such that the fullest hunks go to the beginning + // but completely filled hunks go to the end + + if f := s.free[i]; f != 0 { + // s.free is a heap with indexes to free entries (increased by 1 + // to avoid the need for init). + index := f - 1 + if hunk := s.hunks[index]; hunk != nil { + if used := hunk.used; used < itemsLen { + return used + 1 + } + // queue full hunks last + return -1 + } + } + + // queue absent hunks after empty hunks + return 0 +} + +// Less implements heap.Interface.Less +func (s *slot) Less(i, j int) bool { + return s.key(j) < s.key(i) // max heap! +} + +// Push implements heap.Interface.Push +func (s *slot) Push(x interface{}) { panic("don't push hunks") } + +// Pop implements heap.Interface.Pop +func (s *slot) Pop() interface{} { panic("don't pop hunks") } + +// add registers item, and associate it with the given mark. The returned value +// encodes the item's location in the slot. +func (s *slot) add(item interface{}, mark uint32) uint32 { + + // pick the available hunk with least free entries. + // s.free is a heap with indexes to free entries, but increased by 1 + // to avoid the need for init). + f := s.free[0] + + if 0 == f { + // The heap was not initialised. Swap will always initialise the hunk + // indexes it touches, so this can only happen if there are no elements + // on the heap, yet. + f = 1 + s.free[0] = f + s.hunks[0] = &hunk{} + + // Block some entries in the first hunk we allocated. This is + // intended to avoid a corner cases where after a spike in tags + // items are removed under an even distribution across all hunks. + // + // In this setting, we could end up with a sparsely populated set + // of hunks that won't go away. By blocking some of the first hunks + // entries, it is forced to the head of the list under this + // scenario. + // + // As a side effect, this also prevents the first hunk from ever being + // released, which is useful for low-concurrency high-frequency cases + s.hunks[0].used += itemsLen / 2 + } + + index := f - 1 + h := s.hunks[index] + if nil == h { + // no hunk allocated, yet (or freed). + h = &hunk{} + s.hunks[index] = h + } + + // try to place item in the hunk. This will fail if the hunk is full + addr := h.add(item, mark) + if noAddr == addr { + // based on our heap order (key()), if this hunk is full, all + // hunks must be full. + return noAddr + } + + if h.used == itemsLen { + // we only need to update the heap in case the hunk became unavailable + // because in all other cases, the hunk will remain a maximal element + heap.Fix(s, 0) + } + + // encode the item location and return the tag suffix + return index*hunkBase + addr +} + +// get returns the item addressed by tag if its mark matches, or nil if no +// such entry could be found. The entry will be removed if (and only if) +// `remove` is `true`. +func (s *slot) get(addr, mark uint32, remove bool) (result interface{}) { + + // decode the item's hunk index from the tag suffix + index := addr / hunkBase + addr -= index * hunkBase + + if hunksLen <= index { + // TODO: log/stat corruption + return nil + } + + if h := s.hunks[index]; nil != h { + // retrieve the item from its hunk based on the remaining addr bits + result = h.get(addr, mark, remove) + if result != nil && remove { + heap.Fix(s, int(h.rank)) // adjust free list position and h.rank + if 0 == h.used { + s.hunks[index] = nil // release hunk + } + } + } + return +} + +// release checks if the slot is clear and makes some noise, otherwise +func (s *slot) release() { + // TODO: double check everything is gone and log/stat otherwise +} + +// hunk is a simple item lookup with free entry management. Free entries are +// indexed via next, such that the first free element is at `head`, the next +// free element is at ^next[head], the next at ^next[^next[head]] etc., except +// when the value of next[index] is 0, in which case the next free entry is +// located at `next[index + 1]``. This means, recycled entries are always +// allocated in the reverse order of their release. +// +// While allocated, `next[index]` stores a marker that helps to detect the +// use of stale tags. When retrieving or deleting an item, this marker must +// match the value used when the item was added. +// +type hunk struct { + items [itemsLen]interface{} + next [itemsLen]uint32 // free list / mark storage + head uint32 // head of free list + rank int // position in free heap of owning slot + used int // number of used entries +} + +// find returns the item addressed by tag if its mark matches, or nil if no +// such entry could be found. The entry will be removed if (and only if) +// `remove` is `true`. +func (h *hunk) get(addr uint32, mark uint32, remove bool) (item interface{}) { + + index := addr / itemsBase + if itemsLen <= index { + // TODO: log/stat corruption + return nil + } + + if mark == h.next[index] { + + item = h.items[index] + if remove { + // release item reference + h.items[index] = nil + + // "push" free item index (flip bits to avoid the need for init) + h.next[index], h.head = ^h.head, index + h.used-- + } + } else { + // TODO: log/stat corruption / stale tag + } + return +} + +// tag registers item, and associate it with the given mark. The returned value +// encodes the item's location in the hunk. +func (h *hunk) add(item interface{}, mark uint32) uint32 { + + // don't proceed if we're out of free entries or if we would be wasting space + + if itemsLen <= h.used || nil == item { + return noAddr + } + + // "pop" next free item index + + index := h.head + next := ^h.next[index] + if 0 == ^next { + // not initialised --> use implied default + next = index + 1 + } + h.head = next + h.used++ + + // keep a reference to item and recycle the "next" entry for remembering + // the item mark in order to detect double free and possibly other issues. + h.items[index], h.next[index] = item, mark + + return index * itemsBase +} diff --git a/ego/src/go/internal/cgo/clutch_test.go b/ego/src/go/internal/cgo/clutch_test.go new file mode 100644 index 0000000..d431cd5 --- /dev/null +++ b/ego/src/go/internal/cgo/clutch_test.go @@ -0,0 +1,452 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type clutchTestItem struct { + tag, data uint64 +} + +type clutchSlotTester struct { + *clutch + slot uint64 + items map[uint64]clutchTestItem + t *testing.T + seq uint64 +} + +func newClutchSlotTester(t *testing.T, c *clutch) *clutchSlotTester { + s := clutchSlotTester{ + clutch: c, + slot: c.AcquireSlot(), + items: map[uint64]clutchTestItem{}, + t: t, + seq: 1867324, + } + return &s +} + +func (s *clutchSlotTester) release() { + s.clutch.ReleaseSlot(s.slot) +} + +// add checks tag release for possible problems +// (provided all releases were done via del()) +func (s *clutchSlotTester) add() uint64 { + + s.seq++ + tag := s.clutch.TagItem(s.slot, s.seq) + if !assert.NotEqual(s.t, 0, tag, "Could not add tag (seq:%v)", s.seq) { + return 0 + } + + index := tag & (markBase - 1) // insider knowledge: lower 24 bits must be unique + + old, found := s.items[index] + + if !assert.Equal(s.t, false, found, "Double tag for %v(%v): old:%v new:%v", index, index&(slotBase-1), old.tag, tag) { + return 0 + } + + s.items[index] = clutchTestItem{tag: tag, data: s.seq} + return tag +} + +// del checks tag release for possible problems +// (provided it was obtained via add()) +func (s *clutchSlotTester) del(tag uint64) { + + index := tag & (markBase - 1) + old, found := s.items[index] + + if !assert.Equal(s.t, true, found, "No tag for %v(%v): old:%v new:%v", index, index&(slotBase-1), old.tag, tag) { + return + } + + if !assert.Equal(s.t, old.tag, tag, "Bad tag for %v(%v): old:%v tag:%v", index, index&(slotBase-1), old.tag, tag) { + return + } + + item := s.clutch.RemoveItem(tag) + seq, ok := item.(uint64) + + if !assert.Equal(s.t, true, ok, "Tag not found for %v(%v): %v (item:%v)", index, index&(slotBase-1), tag, item) { + return + } + + if !assert.Equal(s.t, old.data, seq, "Bad item for %v(%v): expected:%v actual:%v", index, index&(slotBase-1), old.data, seq) { + return + } + + delete(s.items, index) +} + +// TestReleaseFirstTag is derived from a bug encountered in an early version +// of clutch: the sequence (+a,+b,+c,-a,-b,+a,+b) would lead to tag c being +// issued instead of b because releasing tag at index 0 a would break the +// internal free list and lead to reassignment of slot c. +func TestReleaseFirstTag(t *testing.T) { + s := newClutchSlotTester(t, &clutch{}) + defer s.release() + + // +1, +2, +3 (add three tags, indexes should be 1,2,3 based on inside know) + a := s.add() + if t.Failed() { + return + } + + b := s.add() + if t.Failed() { + return + } + + s.add() + if t.Failed() { + return + } + + // -1 (release first tag) + s.del(a) + if t.Failed() { + return + } + + // -2 (release second tag) + s.del(b) + if t.Failed() { + return + } + + // +2 (should be index 2 based on inside knowledge) + s.add() + if t.Failed() { + return + } + + // +1 (should be index 1 based on inside knowledge) + s.add() + if t.Failed() { + return + } +} + +// TestReleaseAll allocatate two hunks worth of tags (which will spread across +// three hunks, actually), and releases those using different patterns. +// 123456789ABCDEF, 2468ACE13579BDF, 369CF147AD258BE, 48C159D26AE37BF, +// 5AF16B26B38D49E, 6C17D28E39F4A5B, 7E18F293A4B5C6D, and the same in reverse. +func TestReleaseAll(t *testing.T) { + s := newClutchSlotTester(t, &clutch{}) + defer s.release() + + for direction := -1; direction < 2; direction += 2 { + for stride := 1; stride < 8; stride++ { + for n := 0; n < 2; n++ { + tags := [2 * itemsLen]uint64{} + for i := range tags { + tags[i] = s.add() + if t.Failed() { + return + } + } + + for m := 0; m < stride; m++ { + for i := range tags { + if m == i%stride { + if direction < 0 { + i = len(tags) - 1 - i + } + s.del(tags[i]) + if t.Failed() { + return + } + } + } + } + } + if !assert.Equal(s.t, 0, len(s.items), "Broken test case") { + return + } + } + } +} + +// TestAllocMax allocatate the maximum number of items for a slot and verifies +// that overflow to slot 0 is happening as planned +func TestAllocMax(t *testing.T) { + s := newClutchSlotTester(t, &clutch{}) + defer s.release() + + const maxSlotItems = itemsLen*hunksLen - itemsLen/2 // insider knowledge + + // fill up slot 1 + for i := 0; i < maxSlotItems; i++ { + // don't use s.add() to avoid bookkeeping overhead + tag := s.clutch.TagItem(s.slot, struct{}{}) + index := tag & (markBase - 1) + slot := index / slotBase + if !assert.Equal(s.t, s.slot, slot) { + return + } + } + + // this should go the shared slot + tag := s.clutch.TagItem(s.slot, s.seq) + if !assert.NotEqual(s.t, uint64(0), tag) { + return + } + + // validate slot is 0 + index := tag & (markBase - 1) + slot := index / slotBase + if !assert.Equal(s.t, uint64(0), slot) { + return + } +} + +func TestAcquireSlotSingleThread(t *testing.T) { + tcs := []struct { + name string + numberAcquire int + startExpected uint64 + endExpected uint64 + appendExpected []uint64 + }{ + { + name: "should start with one", + numberAcquire: 1, + startExpected: 1, + endExpected: 1, + }, + { + name: "should contains two values [1, 2]", + numberAcquire: 2, + startExpected: 1, + endExpected: 2, + }, + { + name: "should contains three values [1, 2, 3]", + numberAcquire: 3, + startExpected: 1, + endExpected: 3, + }, + { + name: "should contains 1000 values from [1 -> 1000]", + numberAcquire: 1000, + startExpected: 1, + endExpected: 1000, + }, + { + name: "should contains 4096 values from [1 -> 4095, 0]", + numberAcquire: 4096, + startExpected: 1, + endExpected: 4095, + appendExpected: []uint64{0}, + }, + { + name: "should contains 5000 values from [1 -> 4095, 0, 0, 0, 0, 0]", + numberAcquire: 5000, + startExpected: 1, + endExpected: 4095, + appendExpected: []uint64{0, 0, 0, 0, 0}, + }, + } + for _, v := range tcs { + tc := v + t.Run(tc.name, func(t *testing.T) { + clutch := &clutch{} + // Build expected + expected := make([]uint64, tc.numberAcquire) + expectedIndex := 0 + for i := tc.startExpected; i <= tc.endExpected; i, expectedIndex = i+1, expectedIndex+1 { + expected[expectedIndex] = i + } + for i := 0; i < len(tc.appendExpected); i, expectedIndex = i+1, expectedIndex+1 { + expected[expectedIndex] = tc.appendExpected[i] + } + // Build results + results := make([]uint64, tc.numberAcquire) + for i := 0; i < tc.numberAcquire; i++ { + results[i] = clutch.AcquireSlot() + } + // Check results + assert.Equal(t, expected, results) + }) + } +} + +func TestReleaseSlotSingleThread(t *testing.T) { + type Op struct { + accquire bool + releaseSlot uint64 + } + tcs := []struct { + name string + ops []Op + expected []uint64 + }{ + { + name: "should not increase version if release slot 0", + ops: []Op{ + Op{accquire: true}, + Op{accquire: true}, + Op{releaseSlot: 0}, + Op{accquire: true}, + }, + expected: []uint64{1, 2, 0, 3}, + }, + { + name: "should increase version after release slot 1", + ops: []Op{ + Op{accquire: true}, + Op{accquire: true}, + Op{releaseSlot: 1}, + Op{accquire: true}, + }, + expected: []uint64{1, 2, 0, 4097}, + }, + { + name: "should do not thing with double free slot-2", + ops: []Op{ + Op{accquire: true}, + Op{accquire: true}, + Op{accquire: true}, + Op{releaseSlot: 2}, + Op{releaseSlot: 2}, + Op{accquire: true}, + }, + expected: []uint64{1, 2, 3, 0, 0, 4098}, + }, + { + name: "should only increase version on slot-1 two times", + ops: []Op{ + Op{accquire: true}, + Op{accquire: true}, + Op{releaseSlot: 1}, + Op{accquire: true}, + Op{accquire: true}, + Op{releaseSlot: 4097}, + Op{accquire: true}, + Op{accquire: true}, + Op{accquire: true}, + }, + expected: []uint64{1, 2, 0, 4097, 3, 0, 8193, 4, 5}, + }, + { + name: "should only increase version of last slot", + ops: []Op{ + Op{accquire: true}, + Op{accquire: true}, + Op{accquire: true}, + Op{accquire: true}, + Op{accquire: true}, + Op{releaseSlot: 5}, + Op{accquire: true}, + }, + expected: []uint64{1, 2, 3, 4, 5, 0, 4101}, + }, + } + + for _, v := range tcs { + tc := v + t.Run(tc.name, func(t *testing.T) { + clutch := &clutch{} + results := make([]uint64, len(tc.ops)) + for i, op := range tc.ops { + slot := uint64(0) + if op.accquire { + slot = clutch.AcquireSlot() + } else { + clutch.ReleaseSlot(op.releaseSlot) + } + results[i] = slot + } + // Check results + assert.Equal(t, tc.expected, results) + }) + } +} + +func TestReleaseSlotSingleThreadFullRoundTrip(t *testing.T) { + clutch := &clutch{} + numberTimes := 5000 + maxSlot := 4096 + resultsRound1 := make([]uint64, numberTimes) + expectedRound1 := make([]uint64, numberTimes) + + resultsRound2 := make([]uint64, numberTimes) + expectedRound2 := make([]uint64, numberTimes) + + // Accquire round1 + for i := 0; i < numberTimes; i++ { + slot := clutch.AcquireSlot() + expectedSlot := uint64(0) + if i < maxSlot-1 { + expectedSlot = uint64(i + 1) + } + resultsRound1[i] = slot + expectedRound1[i] = expectedSlot + } + + // Release all slots + for i := numberTimes - 1; i >= 0; i-- { + clutch.ReleaseSlot(resultsRound1[i]) + } + + // Accquire round3 + for i := 0; i < numberTimes; i++ { + slot := clutch.AcquireSlot() + expectedSlot := uint64(0) + if i < maxSlot-1 { + expectedSlot = uint64(maxSlot + i + 1) + } + resultsRound2[i] = slot + expectedRound2[i] = expectedSlot + } + + // Check results + assert.Equal(t, expectedRound1, resultsRound1) + assert.Equal(t, expectedRound2, resultsRound2) +} + +func TestTagItemAndVerifyContent(t *testing.T) { + type item struct { + ID int + } + + maxItem := 16 * 1000 * 1000 + clutch := &clutch{} + slot := clutch.AcquireSlot() + results := map[uint64]*item{} + + for i := 0; i < maxItem; i++ { + item := &item{ + ID: i + 1, + } + tag := clutch.TagItem(slot, item) + if !assert.NotZero(t, tag) { + return + } + _, found := results[tag] + if !assert.False(t, found) { + return + } + results[tag] = item + } + if !assert.Equal(t, maxItem, len(results)) { + return + } + for k, v := range results { + item := clutch.GetItem(k) + if !assert.Equal(t, v, item) { + return + } + } +} diff --git a/ego/src/go/internal/cgo/cutils.go b/ego/src/go/internal/cgo/cutils.go new file mode 100644 index 0000000..a65311c --- /dev/null +++ b/ego/src/go/internal/cgo/cutils.go @@ -0,0 +1,115 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package main + +//#include +//#include "ego/src/cc/goc/envoy.h" +import "C" +import ( + "errors" + "math" + "reflect" + "unsafe" + + "github.com/grab/ego/ego/src/go/volatile" +) + +// GoStr create an unsafe reference to a Go string for passing it down to C++ +// +func GoStr(s string) C.GoStr { + h := (*reflect.StringHeader)(unsafe.Pointer(&s)) + return C.GoStr{ + len: C.ulong(h.Len), + data: (*C.char)(unsafe.Pointer(h.Data)), + } +} + +// GoBuf create an unsafe reference to a Go byte array for passing it down to C++ +// +func GoBuf(b []byte) C.GoBuf { + h := (*reflect.SliceHeader)(unsafe.Pointer(&b)) + return C.GoBuf{ + len: C.ulong(h.Len), + cap: C.ulong(h.Cap), + data: unsafe.Pointer(h.Data), + } +} + +// We have agreed on a sweet little convention for returning errors +// from C++ land: static, constant C strings. CErr wraps them into a +// Go error. +// +func CErr(ptr *C.char) error { + if nil == ptr { + return nil + } + return errors.New(string(CStr(ptr))) +} + +// CStr is very dangerous, because if the returned string slice escapes, +// its contents may be modified later. If you are unsure, do use C.GoString. +// +func CStr(ptr *C.char) volatile.String { + + // prevent null pointer access + if nil == ptr { + return "" + } + + return CStrN(ptr, C.strlen(ptr)) +} + +// CStrN is very dangerous, because if the returned string slice escapes, +// its contents may be modified later. If you are unsure, do use C.GoStringN. +// +func CStrN(ptr *C.char, n C.size_t) volatile.String { + + // prevent null pointer access + if nil == ptr { + if 0 != n { + panic("C string is null") + } + return "" + } + + // ensure no loss occurs during conversion. + if n != C.size_t(int(n)) || int(n) < 0 { + panic("C string too long") + } + h := reflect.StringHeader{uintptr(unsafe.Pointer(ptr)), int(n)} + return *(*volatile.String)(unsafe.Pointer(&h)) +} + +// CBytes is very dangerous, because if the returned byte slice escapes, +// its contents may be modified later. If you are unsure, do use C.GoBytes. +// +func CBytes(ptr unsafe.Pointer, size, cap C.size_t) volatile.Bytes { + + // The maximum address space can be larger than 4GB. Apparently, 50bits are + // supported for 64bit architectures, so we bump up the value in that case. + const maxCap = int(math.MaxInt32) | int((^uint(0))>>14) + + // ensure no loss occurs during conversion and capacity isn't too large + if C.size_t(int(cap)) != cap || int(cap) < 0 || maxCap < int(cap) { + panic("C buffer too large") + } + if C.size_t(int(size)) != size || int(size) < 0 || int(cap) < int(size) { + panic("Invalid C buffer") + } + // https://github.com/golang/go/wiki/cgo#turning-c-arrays-into-go-slices + return volatile.Bytes((*[maxCap]byte)(ptr)[:size:int(cap)]) +} + +func CLong(val C.int64_t) int64 { + return int64(val) +} + +func GoBool(val bool) C.int { + if val { + return C.int(1) + } + return C.int(0) +} diff --git a/ego/src/go/internal/cgo/decoder_callbacks.go b/ego/src/go/internal/cgo/decoder_callbacks.go new file mode 100644 index 0000000..bffd055 --- /dev/null +++ b/ego/src/go/internal/cgo/decoder_callbacks.go @@ -0,0 +1,111 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package main + +// #include "ego/src/cc/goc/envoy.h" +import "C" +import ( + "unsafe" + + pb "github.com/grab/ego/ego/src/cc/goc/proto" + "github.com/grab/ego/ego/src/go/envoy" + "github.com/grab/ego/ego/src/go/envoy/loglevel" + + "github.com/golang/protobuf/proto" +) + +type decoderCallbacks struct { + filter unsafe.Pointer +} + +// This section is implementation downcalls for StreamDecoderFilterCallbacks + +func (c decoderCallbacks) DecodingBuffer() envoy.BufferInstance { + ptr := C.GoHttpFilter_DecoderCallbacks_decodingBuffer(c.filter) + if ptr == nil { + return nil + } + + return bufferInstance{ptr} +} + +// See //envoy/include/envoy/http/filter.h +// Continue iterating through the filter chain with buffered headers and body data. This routine +// can only be called if the filter has previously returned StopIteration from decodeHeaders() +// AND one of StopIterationAndBuffer, StopIterationAndWatermark, or StopIterationNoBuffer +// from each previous call to decodeData(). +func (c decoderCallbacks) ContinueDecoding() { + C.GoHttpFilter_DecoderCallbacks_continueDecoding(c.filter) +} + +// See //envoy/include/envoy/http/filter.h +// sendLocalReply only available for StreamDecoderFilterCallbacks +// StreamEncoderFilterCallbacks doesn't have +func (c decoderCallbacks) SendLocalReply(responseCode int, body string, headers map[string]string, details string) { + headerMap := pb.RequestHeaderMap{} + var entries []*pb.HeaderEntry + for k, v := range headers { + entry := &pb.HeaderEntry{Key: k, Value: v} + entries = append(entries, entry) + } + headerMap.Headers = entries + headerBytes, err := proto.Marshal(&headerMap) + if err != nil { + Log(loglevel.Error, "decoderCallbacks", "can't marshall headers. "+err.Error()) + // Continue to send response with empty header + } + if 0 != C.GoHttpFilter_DecoderCallbacks_sendLocalReply(c.filter, C.int(responseCode), GoStr(body), GoBuf(headerBytes), GoStr(details)) { + Log(loglevel.Error, "decoderCallbacks", "can't sendLocalReply") + } +} + +func (c decoderCallbacks) AddDecodedData(buffer envoy.BufferInstance, streamingFilter bool) { + if buffer == nil { + return + } + + var streamingFilterInt int8 + if streamingFilter { + streamingFilterInt = 1 + } + + b := buffer.(bufferInstance) + C.GoHttpFilter_DecoderCallbacks_addDecodedData(c.filter, b.ptr, C.int(streamingFilterInt)) +} + +func (c decoderCallbacks) StreamInfo() envoy.StreamInfo { + return &streamInfo{c.filter, false} +} + +func (c decoderCallbacks) RouteExisting() bool { + existing := C.GoHttpFilter_StreamFilterCallbacks_routeExisting(c.filter, GoBool(false)) + return existing != 0 +} + +func (c decoderCallbacks) Route() envoy.Route { + return &route{c.filter, false} +} + +func (c decoderCallbacks) EncodeHeaders(responseCode int, headers *pb.ResponseHeaderMap, endStream bool) { + headerBytes, err := proto.Marshal(headers) + if err != nil { + Log(loglevel.Error, "decoderCallbacks", "can't marshall headers. "+err.Error()) + return + } + + endStreamVal := 0 + if endStream { + endStreamVal = 1 + } + + if 0 != C.GoHttpFilter_DecoderCallbacks_encodeHeaders(c.filter, C.int(responseCode), GoBuf(headerBytes), C.int(endStreamVal)) { + Log(loglevel.Error, "decoderCallbacks", "can't encodeHeaders") + } +} + +func (c decoderCallbacks) ActiveSpan() envoy.Span { + return span{filter: c.filter, spanID: -1} +} diff --git a/ego/src/go/internal/cgo/encoder_callbacks.go b/ego/src/go/internal/cgo/encoder_callbacks.go new file mode 100644 index 0000000..beecaaf --- /dev/null +++ b/ego/src/go/internal/cgo/encoder_callbacks.go @@ -0,0 +1,67 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package main + +// #include "ego/src/cc/goc/envoy.h" +import "C" +import ( + "unsafe" + + "github.com/grab/ego/ego/src/go/envoy" +) + +type encoderCallbacks struct { + filter unsafe.Pointer +} + +// This section is implementation downcalls for StreamEncoderFilterCallbacks +// +// See //envoy/include/envoy/http/filter.h +func (c encoderCallbacks) EncodingBuffer() envoy.BufferInstance { + ptr := C.GoHttpFilter_EncoderCallbacks_encodingBuffer(c.filter) + if ptr == nil { + return nil + } + + return bufferInstance{ptr} +} + +// See //envoy/include/envoy/http/filter.h +func (c encoderCallbacks) AddEncodedData(buffer envoy.BufferInstance, streamingFilter bool) { + if buffer == nil { + return + } + + var streamingFilterInt int8 + if streamingFilter { + streamingFilterInt = 1 + } + + b := buffer.(bufferInstance) + C.GoHttpFilter_EncoderCallbacks_addEncodedData(c.filter, b.ptr, C.int(streamingFilterInt)) +} + +// See //envoy/include/envoy/http/filter.h +func (c encoderCallbacks) ContinueEncoding() { + C.GoHttpFilter_EncoderCallbacks_continueEncoding(c.filter) +} + +func (c encoderCallbacks) StreamInfo() envoy.StreamInfo { + return &streamInfo{c.filter, true} +} + +func (c encoderCallbacks) RouteExisting() bool { + existing := C.GoHttpFilter_StreamFilterCallbacks_routeExisting(c.filter, GoBool(true)) + return existing != 0 +} + +func (c encoderCallbacks) Route() envoy.Route { + return &route{c.filter, true} +} + +func (c encoderCallbacks) ActiveSpan() envoy.Span { + return span{filter: c.filter, spanID: 0} +} diff --git a/ego/src/go/internal/cgo/filter_state.go b/ego/src/go/internal/cgo/filter_state.go new file mode 100644 index 0000000..21e9e0c --- /dev/null +++ b/ego/src/go/internal/cgo/filter_state.go @@ -0,0 +1,31 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package main + +// #include "ego/src/cc/goc/envoy.h" +import "C" +import ( + "unsafe" + + "github.com/grab/ego/ego/src/go/envoy/lifespan" + "github.com/grab/ego/ego/src/go/envoy/statetype" + "github.com/grab/ego/ego/src/go/volatile" +) + +type filterState struct { + filter unsafe.Pointer + encoder bool +} + +func (s filterState) SetData(name, value string, stateType statetype.Type, lifeSpan lifespan.Type) { + C.GoHttpFilter_DecoderCallbacks_StreamInfo_FilterState_setData(s.filter, GoStr(name), GoStr(value), C.int(stateType), C.int(lifeSpan)) +} + +func (s filterState) GetDataReadOnly(name string) (volatile.String, bool) { + var value C.GoStr + ok := C.GoHttpFilter_StreamFilterCallbacks_StreamInfo_FilterState_getDataReadOnly(s.filter, GoBool(s.encoder), GoStr(name), &value) + return CStrN(value.data, value.len), ok != 0 +} diff --git a/ego/src/go/internal/cgo/gohttpfilter.go b/ego/src/go/internal/cgo/gohttpfilter.go new file mode 100644 index 0000000..62ad7c1 --- /dev/null +++ b/ego/src/go/internal/cgo/gohttpfilter.go @@ -0,0 +1,312 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package main + +// #include "ego/src/cc/goc/envoy.h" +import "C" +import ( + "fmt" + "unsafe" + + ego "github.com/grab/ego/ego/src/go" + "github.com/grab/ego/ego/src/go/envoy" + "github.com/grab/ego/ego/src/go/envoy/datastatus" + "github.com/grab/ego/ego/src/go/envoy/headersstatus" + "github.com/grab/ego/ego/src/go/envoy/loglevel" + "github.com/grab/ego/ego/src/go/envoy/trailersstatus" + "github.com/grab/ego/ego/src/go/volatile" +) + +type goHttpFilter struct { + filter unsafe.Pointer +} + +func newGoHttpFilter(ptr unsafe.Pointer) envoy.GoHttpFilter { + return goHttpFilter{ptr} +} + +func (f goHttpFilter) DecoderCallbacks() envoy.DecoderFilterCallbacks { + return decoderCallbacks{f.filter} +} + +func (f goHttpFilter) EncoderCallbacks() envoy.EncoderFilterCallbacks { + return encoderCallbacks{f.filter} +} + +func (f goHttpFilter) ResolveMostSpecificPerGoFilterConfig(name string, route envoy.Route) interface{} { + cgoTag := C.GoHttpFilter_ResolveMostSpecificPerGoFilterConfig(f.filter, GoStr(name)) + return GetRouteSpecificFilterConfig(uint64(cgoTag)) +} + +func (f goHttpFilter) GenericSecretProvider() envoy.GenericSecretConfigProvider { + return genericSecretConfigProvider{f.filter} +} + +func (f goHttpFilter) Post(tag uint64) { + C.GoHttpFilter_post(f.filter, C.uint64_t(tag)) +} + +func (f goHttpFilter) Pin() { + C.GoHttpFilter_pin(f.filter) +} + +func (f goHttpFilter) Unpin() { + C.GoHttpFilter_unpin(f.filter) +} + +// Log with two simple paramters level & message, we can extend it with +// keyvals ...interface{} & logstring := l.getLogstring(keyvals) from structured log wrapper it +// It will not optimize for performance such as don't build the message if loglevel isn't match +func (f goHttpFilter) Log(logLevel loglevel.Type, message string) { + C.GoHttpFilter_log(f.filter, C.uint32_t(logLevel), GoStr(message)) +} + +type genericSecretConfigProvider struct { + filter unsafe.Pointer +} + +func (p genericSecretConfigProvider) Secret() volatile.String { + var value C.GoStr + C.GoHttpFilter_GenericSecretConfigProvider_secret(p.filter, &value) + return CStrN(value.data, value.len) +} + +// Cgo_GoHttpFilter_DecodeHeaders is the entry point for +// Envoy::Http::GoHttpFilter::decodeHeaders(). +// See //src/cc/filters/http/go/filter-cgo.cc +// +//export Cgo_GoHttpFilter_DecodeHeaders +func Cgo_GoHttpFilter_DecodeHeaders(filterTag uint64, headers unsafe.Pointer, end_stream C.int) int { + return int(cgo_GoHttpFilter_DecodeHeaders(filterTag, headers, end_stream)) +} + +func cgo_GoHttpFilter_DecodeHeaders(filterTag uint64, headers unsafe.Pointer, end_stream C.int) (result headersstatus.Type) { + const tag = "cgo_GoHttpFilter_DecodeHeaders" + defer func() { + if err := recover(); err != nil { + Log(loglevel.Error, tag, fmt.Sprintf("%v", err)) + // FIXME: emit 500 + // TODO(tien.nguyen): think about how can call to C side without filter point to call sendLocalReply + result = headersstatus.StopIteration + } + }() + filter := GetHttpFilter(filterTag) + if nil == filter { + Log(loglevel.Error, tag, "nil filter") + // FIXME: emit 500 + return headersstatus.StopIteration + } + return filter.DecodeHeaders(requestHeaderMap{headers}, end_stream != 0) +} + +// Cgo_GoHttpFilter_DecodeData is the entry point for +// Envoy::Http::GoHttpFilter::decodeData(). +// See //src/cc/filters/http/go/filter-cgo.cc +// +//export Cgo_GoHttpFilter_DecodeData +func Cgo_GoHttpFilter_DecodeData(filterTag uint64, buffer unsafe.Pointer, end_stream C.int) int { + return int(cgo_GoHttpFilter_DecodeData(filterTag, buffer, end_stream)) +} + +func cgo_GoHttpFilter_DecodeData(filterTag uint64, buffer unsafe.Pointer, end_stream C.int) (result datastatus.Type) { + const tag = "cgo_GoHttpFilter_DecodeData" + defer func() { + if err := recover(); err != nil { + Log(loglevel.Error, tag, fmt.Sprintf("%v", err)) + result = datastatus.StopIterationNoBuffer + } + }() + filter := GetHttpFilter(filterTag) + if nil == filter { + Log(loglevel.Error, tag, "nil filter") + // FIXME: emit 500 + return datastatus.StopIterationNoBuffer + } + return filter.DecodeData(bufferInstance{buffer}, end_stream != 0) +} + +// Cgo_GoHttpFilter_DecodeTrailers is the entry point for +// Envoy::Http::GoHttpFilter::decodeTrailers(). +// See //src/cc/filters/http/go/filter-cgo.cc +// +//export Cgo_GoHttpFilter_DecodeTrailers +func Cgo_GoHttpFilter_DecodeTrailers(filterTag uint64, trailers unsafe.Pointer) int { + return int(cgo_GoHttpFilter_DecodeTrailers(filterTag, trailers)) +} + +func cgo_GoHttpFilter_DecodeTrailers(filterTag uint64, trailers unsafe.Pointer) (result trailersstatus.Type) { + const tag = "cgo_GoHttpFilter_DecodeTrailers" + defer func() { + if err := recover(); err != nil { + Log(loglevel.Error, tag, fmt.Sprintf("%v", err)) + // FIXME: emit 500 + result = trailersstatus.StopIteration + } + }() + filter := GetHttpFilter(filterTag) + if nil == filter { + Log(loglevel.Error, tag, "nil filter") + // FIXME: emit 500 + return trailersstatus.StopIteration + } + return filter.DecodeTrailers(requestTrailerMap{trailers}) +} + +// Cgo_GoHttpFilter_OnPost is the entry point for +// Envoy::Http::GoHttpFilter::onPost(). +// See //src/cc/filters/http/go/filter-cgo.cc +// +//export Cgo_GoHttpFilter_OnPost +func Cgo_GoHttpFilter_OnPost(filterTag, postTag uint64) { + const tag = "Cgo_GoHttpFilter_OnPost" + defer func() { + if err := recover(); err != nil { + Log(loglevel.Error, tag, fmt.Sprintf("%v", err)) + } + }() + f := GetHttpFilter(filterTag) + if nil == f { + Log(loglevel.Error, tag, "nil filter") + return + } + f.OnPost(postTag) +} + +// Cgo_GoHttpFilter_OnDestroy is the entry point for +// Envoy::Http::GoHttpFilter::onDestroy(). +// See //src/cc/filters/http/go/filter-cgo.cc +// +//export Cgo_GoHttpFilter_OnDestroy +func Cgo_GoHttpFilter_OnDestroy(filterTag uint64) { + const tag = "Cgo_GoHttpFilter_OnDestroy" + defer func() { + if err := recover(); err != nil { + Log(loglevel.Error, tag, fmt.Sprintf("%v", err)) + } + }() + f := RemoveHttpFilter(filterTag) + if nil == f { + Log(loglevel.Error, tag, "nil filter") + return + } + f.OnDestroy() +} + +//export Cgo_GoHttpFilter_Create +func Cgo_GoHttpFilter_Create(native unsafe.Pointer, factoryTag uint64, filterSlot uint64) (result uint64) { + const tag = "Cgo_GoHttpFilter_Create" + defer func() { + if err := recover(); err != nil { + Log(loglevel.Error, tag, fmt.Sprintf("%v", err)) + result = 0 + } + }() + + // NOTE: we are not sure if we are running on the same thread as the + // filter factory creation. But we know the factory _is_ alive right + // now, so this is safe. + filterFactory := GetHttpFilterFactory(factoryTag) + if nil == filterFactory { + Log(loglevel.Error, tag, "nil filterFactory") + return 0 + } + + filter := filterFactory(newGoHttpFilter(native)) + if nil == filter { + Log(loglevel.Error, tag, "nil filter") + return 0 + } + + return TagHttpFilter(filterSlot, filter) +} + +//export Cgo_GoHttpFilter_EncodeHeaders +func Cgo_GoHttpFilter_EncodeHeaders(filterTag uint64, headers unsafe.Pointer, end_stream C.int) int { + return int(cgo_GoHttpFilter_EncodeHeaders(filterTag, headers, end_stream)) +} + +func cgo_GoHttpFilter_EncodeHeaders(filterTag uint64, headers unsafe.Pointer, end_stream C.int) (result headersstatus.Type) { + const tag = "cgo_GoHttpFilter_EncodeHeaders" + defer func() { + if err := recover(); err != nil { + Log(loglevel.Error, tag, fmt.Sprintf("%v", err)) + // FIXME: emit 500 + result = headersstatus.StopIteration + } + }() + filter := GetHttpFilter(filterTag) + if nil == filter { + Log(loglevel.Error, tag, "nil filter") + // FIXME: emit 500 + return headersstatus.StopIteration + } + + return filter.EncodeHeaders(responseHeaderMap{headers}, end_stream != 0) +} + +//export Cgo_GoHttpFilter_EncodeData +func Cgo_GoHttpFilter_EncodeData(filterTag uint64, buffer unsafe.Pointer, end_stream C.int) int { + return int(cgo_GoHttpFilter_EncodeData(filterTag, buffer, end_stream)) +} + +func cgo_GoHttpFilter_EncodeData(filterTag uint64, buffer unsafe.Pointer, end_stream C.int) (result datastatus.Type) { + const tag = "cgo_GoHttpFilter_EncodeData" + defer func() { + if err := recover(); err != nil { + Log(loglevel.Error, tag, fmt.Sprintf("%v", err)) + result = datastatus.StopIterationNoBuffer + } + }() + filter := GetHttpFilter(filterTag) + if nil == filter { + Log(loglevel.Error, tag, "nil filter") + // FIXME: emit 500 + return datastatus.StopIterationNoBuffer + } + + return filter.EncodeData(bufferInstance{buffer}, end_stream != 0) +} + +// httpFilters is a clutch to bridge the "air gap" between the C++ filter object +// and the go filter state. We share this among all filters, but in case the 16M +// clutch entries turn out to be insufficient, we can create one clutch per +// filter name (and stealing a few bits from the tag mark for the clutch ID). +// +var httpFilters = &clutch{} + +// Cgo_AcquireHttpFilterSlot is the public proxy for httpFilters.AcquireSlot +// +//export Cgo_AcquireHttpFilterSlot +func Cgo_AcquireHttpFilterSlot() uint64 { + return httpFilters.AcquireSlot() +} + +//Cgo_ReleaseHttpFilterSlot is the public proxy for httpFilters.ReleaseSlot +// +//export Cgo_ReleaseHttpFilterSlot +func Cgo_ReleaseHttpFilterSlot(id uint64) { + httpFilters.ReleaseSlot(id) +} + +// TagHttpFilter is the public proxy for httpFilters.TagItem +// +func TagHttpFilter(slot uint64, filter ego.HttpFilter) uint64 { + return httpFilters.TagItem(slot, filter) +} + +// GetHttpFilter is the public proxy for httpFilters.GetItem +// +func GetHttpFilter(tag uint64) ego.HttpFilter { + filter, _ := httpFilters.GetItem(tag).(ego.HttpFilter) + return filter +} + +// RemoveHttpFilter is the public proxy for httpFilters.RemoveItem +// +func RemoveHttpFilter(tag uint64) ego.HttpFilter { + filter, _ := httpFilters.RemoveItem(tag).(ego.HttpFilter) + return filter +} diff --git a/ego/src/go/internal/cgo/gohttpfilterconfig.go b/ego/src/go/internal/cgo/gohttpfilterconfig.go new file mode 100644 index 0000000..c9c5de3 --- /dev/null +++ b/ego/src/go/internal/cgo/gohttpfilterconfig.go @@ -0,0 +1,218 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package main + +//#include "ego/src/cc/goc/envoy.h" +import "C" +import ( + "fmt" + "unsafe" + + ego "github.com/grab/ego/ego/src/go" + "github.com/grab/ego/ego/src/go/envoy" + "github.com/grab/ego/ego/src/go/logger" + "github.com/grab/ego/ego/src/go/volatile" +) + +type goHttpFilterConfig struct { + settings volatile.Bytes + scope scope +} + +func (c goHttpFilterConfig) Settings() volatile.Bytes { + return c.settings +} + +func (c goHttpFilterConfig) Scope() envoy.Scope { + return c.scope +} + +//export Cgo_GoHttpFilterFactory_Create +func Cgo_GoHttpFilterFactory_Create(factorySlot uint64, name *C.char, nameLen C.size_t, + settings unsafe.Pointer, settingsLen C.size_t, scopePtr unsafe.Pointer) (result uint64) { + log := logger.NewLogger("Cgo_GoHttpFilterFactory_Create", nativeLogger{}) + + defer func() { + if err := recover(); err != nil { + log.Error(fmt.Sprintf("panic recover with error", err)) + result = 0 + } + }() + + factoryFactory := ego.GetHttpFilterFactoryFactory(CStrN(name, nameLen)) + if nil == factoryFactory { + log.Error("can not find factory by name") + return 0 + } + + cfg := goHttpFilterConfig{ + settings: CBytes(settings, settingsLen, settingsLen), + scope: scope{ + ptr: scopePtr, + }, + } + factory, err := factoryFactory.CreateFilterFactory(cfg) + if err != nil { + log.Error(fmt.Sprintf("invoke CreateFilterFactory failed with error", err)) + return 0 + } + if nil == factory { + log.Error("invoke CreateFilterFactory without error but return nil") + return 0 + } + + return TagHttpFilterFactory(factorySlot, factory) +} + +//export Cgo_GoHttpFilterFactory_OnDestroy +func Cgo_GoHttpFilterFactory_OnDestroy(factoryTag uint64) { + log := logger.NewLogger("Cgo_GoHttpFilterFactory_OnDestroy", nativeLogger{}) + defer func() { + if err := recover(); err != nil { + log.Error(fmt.Sprintf("panic recover with error", err)) + } + }() + factory := RemoveHttpFilterFactory(factoryTag) + if nil == factory { + log.Error("invoke remove factory return nil") + return + } +} + +// httpFilterFactories is a separate clutch for all our filter factories. This +// is a little bit overblown, indeed, but since factories are expected to live +// much longer than filters, this could result in severe hunk leakage. +// +var httpFilterFactories = &clutch{} + +// Cgo_AcquireHttpFilterFactorySlot is the public proxy for +// httpFilterFactories.AcquireSlot +// +//export Cgo_AcquireHttpFilterFactorySlot +func Cgo_AcquireHttpFilterFactorySlot() uint64 { + return httpFilterFactories.AcquireSlot() +} + +// Cgo_ReleaseHttpFilterFactorySlot is the public proxy for +// httpFilterFactories.ReleaseSlot +// +//export Cgo_ReleaseHttpFilterFactorySlot +func Cgo_ReleaseHttpFilterFactorySlot(id uint64) { + httpFilterFactories.ReleaseSlot(id) +} + +// TagHttpFilterFactory is the public proxy for +// httpFilterFactories.TagItem +// +func TagHttpFilterFactory(slot uint64, factory ego.HttpFilterFactory) uint64 { + return httpFilterFactories.TagItem(slot, factory) +} + +// GetHttpFilterFactory is the public proxy for +// httpFilterFactories.GetItem +// +func GetHttpFilterFactory(tag uint64) ego.HttpFilterFactory { + factory, _ := httpFilterFactories.GetItem(tag).(ego.HttpFilterFactory) + return factory +} + +// RemoveHttpFilterFactory is the public proxy for +// httpFilterFactories.RemoveItem +// +func RemoveHttpFilterFactory(tag uint64) ego.HttpFilterFactory { + factory, _ := httpFilterFactories.RemoveItem(tag).(ego.HttpFilterFactory) + return factory +} + +//export Cgo_RouteSpecificFilterConfig_Create +func Cgo_RouteSpecificFilterConfig_Create(configSlot uint64, name *C.char, nameLen C.size_t, settings unsafe.Pointer, settingsLen C.size_t) (result uint64) { + log := logger.NewLogger("Cgo_RouteSpecificFilterConfig_Create", nativeLogger{}) + + factoryFactory := ego.GetHttpFilterFactoryFactory(CStrN(name, nameLen)) + if factoryFactory == nil { + return 0 + } + + // TODO: Define new struct for route specific configuration? + cfg := goHttpFilterConfig{ + settings: CBytes(settings, settingsLen, settingsLen), + } + + config, err := factoryFactory.CreateRouteSpecificFilterConfig(cfg) + if err != nil { + log.Error(fmt.Sprintf("invoke CreateRouteSpecificFilterConfig failed with error", err)) + return 0 + } + if nil == config { + log.Error("invoke CreateRouteSpecificFilterConfig without error but return nil") + return 0 + } + + defer func() { + if err := recover(); err != nil { + log.Error(fmt.Sprintf("panic recover with error", err)) + result = 0 + } + }() + + return TagRouteSpecificFilterConfig(configSlot, config) +} + +//export Cgo_RouteSpecificFilterConfig_dtor +func Cgo_RouteSpecificFilterConfig_dtor(configTag uint64) { + log := logger.NewLogger("Cgo_RouteSpecificFilterConfig_dtor", nativeLogger{}) + defer func() { + if err := recover(); err != nil { + log.Error(fmt.Sprintf("panic recover with error", err)) + } + }() + factory := RemoveRouteSpecificFilterConfig(configTag) + if nil == factory { + log.Error("invoke remove factory return nil") + return + } +} + +var routeSpecificFilterConfigs = &clutch{} + +// Cgo_AcquireRouteSpecificFilterConfigSlot is the public proxy for +// routeSpecificFilterConfigs.AcquireSlot +// +//export Cgo_AcquireRouteSpecificFilterConfigSlot +func Cgo_AcquireRouteSpecificFilterConfigSlot() uint64 { + return routeSpecificFilterConfigs.AcquireSlot() +} + +// Cgo_ReleaseRouteSpecificFilterConfigSlot is the public proxy for +// routeSpecificFilterConfigs.ReleaseSlot +// +//export Cgo_ReleaseRouteSpecificFilterConfigSlot +func Cgo_ReleaseRouteSpecificFilterConfigSlot(id uint64) { + routeSpecificFilterConfigs.ReleaseSlot(id) +} + +// TagRouteSpecificFilterConfig is the public proxy for +// routeSpecificFilterConfigs.TagItem +// +func TagRouteSpecificFilterConfig(slot uint64, config interface{}) uint64 { + return routeSpecificFilterConfigs.TagItem(slot, config) +} + +// GetRouteSpecificFilterConfig is the public proxy for +// routeSpecificFilterConfigs.GetItem +// +func GetRouteSpecificFilterConfig(tag uint64) interface{} { + config := routeSpecificFilterConfigs.GetItem(tag) + return config +} + +// RemoveRouteSpecificFilterConfig is the public proxy for +// routeSpecificFilterConfigs.RemoveItem +// +func RemoveRouteSpecificFilterConfig(tag uint64) interface{} { + config := routeSpecificFilterConfigs.RemoveItem(tag) + return config +} diff --git a/ego/src/go/internal/cgo/logger.go b/ego/src/go/internal/cgo/logger.go new file mode 100644 index 0000000..dfd515b --- /dev/null +++ b/ego/src/go/internal/cgo/logger.go @@ -0,0 +1,25 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package main + +// #include "ego/src/cc/goc/envoy.h" +import "C" + +import ( + "github.com/grab/ego/ego/src/go/envoy/loglevel" +) + +// Log is a basic log function and base on that developers can extend or write their logger +func Log(level loglevel.Type, tag, message string) { + C.Envoy_log_misc(C.uint32_t(level), GoStr(tag), GoStr(message)) +} + +type nativeLogger struct { +} + +func (l nativeLogger) Log(level loglevel.Type, tag, message string) { + Log(level, tag, message) +} diff --git a/ego/src/go/internal/cgo/main.go b/ego/src/go/internal/cgo/main.go new file mode 100644 index 0000000..1de07c2 --- /dev/null +++ b/ego/src/go/internal/cgo/main.go @@ -0,0 +1,29 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package main + +import ( + logger "github.com/grab/ego/ego/src/go/logger" + + // This is a rather ugly. We can't use ego as a dependency of egofilters + // because main() needs to be located in the same package as the cgo code, + // and there can only be one package with cgo code (at least when building + // with bazel). + _ "github.com/grab/ego/egofilters" +) + +func init() { + + logger.Init(nativeLogger{}) + + // FIXME: To be super safe, we should probably say "hi" before accepting + // CGo calls: https://github.com/golang/go/issues/15943#issuecomment-713153486 +} + +// main() is not used -- this is a static library. +func main() { + +} diff --git a/ego/src/go/internal/cgo/requestheadermap.go b/ego/src/go/internal/cgo/requestheadermap.go new file mode 100644 index 0000000..20e7739 --- /dev/null +++ b/ego/src/go/internal/cgo/requestheadermap.go @@ -0,0 +1,115 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package main + +// #include "ego/src/cc/goc/envoy.h" +import "C" +import ( + "unsafe" + + "github.com/golang/protobuf/proto" + + pb "github.com/grab/ego/ego/src/cc/goc/proto" + "github.com/grab/ego/ego/src/go/volatile" +) + +// requestheadermap implements envoy.RequestHeaderMap +// +type requestHeaderMap struct{ ptr unsafe.Pointer } + +// AddCopy translates to +// Envoy::Http::RequestHeaderMap::addCopy(LowerCaseString, absl::string_view). +// +// See //envoy/include/envoy/http/header_map.h +func (h requestHeaderMap) AddCopy(name, value string) { + C.RequestHeaderMap_add(h.ptr, GoStr(name), GoStr(value)) +} + +// SetCopy translates to +// Envoy::Http::RequestHeaderMap::setCopy(LowerCaseString, absl::string_view). +// +// See //envoy/include/envoy/http/header_map.h +func (h requestHeaderMap) SetCopy(name, value string) { + C.RequestHeaderMap_set(h.ptr, GoStr(name), GoStr(value)) +} + +// AppendCopy translates to +// Envoy::Http::RequestHeaderMap::appendCopy(LowerCaseString, absl::string_view). +// +// See //envoy/include/envoy/http/header_map.h +func (h requestHeaderMap) AppendCopy(name, value string) { + C.RequestHeaderMap_append(h.ptr, GoStr(name), GoStr(value)) +} + +func (h requestHeaderMap) GetByPrefix(prefix string) map[string][]string { + defaultBufferSize := uint64(100) + data := make([]byte, defaultBufferSize) + size := uint64(C.RequestHeaderMap_getByPrefix(h.ptr, GoStr(prefix), GoBuf(data))) + if size > defaultBufferSize { + newBufferSize := size + data = make([]byte, newBufferSize) + size = uint64(C.RequestHeaderMap_getByPrefix(h.ptr, GoStr(prefix), GoBuf(data))) + if size > newBufferSize { + // it's not supposed to happen. + panic("Invalid logic") + } + } + headers := pb.RequestHeaderMap{} + if err := proto.Unmarshal(data[:size], &headers); err != nil { + // TODO: handle error + } + + result := make(map[string][]string) + for _, h := range headers.Headers { + if _, existing := result[h.Key]; !existing { + result[h.Key] = []string{h.Value} + } + } + + return result +} + +// Remove translates to +// Envoy::Http::RequestHeaderMap::remove(LowerCaseString). +// +// See //envoy/include/envoy/http/header_map.h +func (h requestHeaderMap) Remove(name string) { + C.RequestHeaderMap_remove(h.ptr, GoStr(name)) +} + +func (h requestHeaderMap) Path() volatile.String { + var value C.GoStr + C.RequestHeaderMap_Path(h.ptr, &value) + return CStrN(value.data, value.len) +} + +func (h requestHeaderMap) SetPath(path string) { + C.RequestHeaderMap_setPath(h.ptr, GoStr(path)) +} + +func (h requestHeaderMap) Method() volatile.String { + var value C.GoStr + C.RequestHeaderMap_Method(h.ptr, &value) + return CStrN(value.data, value.len) +} + +func (h requestHeaderMap) Authorization() volatile.String { + var value C.GoStr + C.RequestHeaderMap_Authorization(h.ptr, &value) + return CStrN(value.data, value.len) +} + +func (h requestHeaderMap) ContentType() volatile.String { + var value C.GoStr + C.RequestHeaderMap_ContentType(h.ptr, &value) + return CStrN(value.data, value.len) +} + +func (h requestHeaderMap) Get(name string) volatile.String { + var value C.GoStr + C.RequestHeaderMap_get(h.ptr, GoStr(name), &value) + return CStrN(value.data, value.len) +} diff --git a/ego/src/go/internal/cgo/requesttrailermap.go b/ego/src/go/internal/cgo/requesttrailermap.go new file mode 100644 index 0000000..77125cd --- /dev/null +++ b/ego/src/go/internal/cgo/requesttrailermap.go @@ -0,0 +1,48 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package main + +// #include "ego/src/cc/goc/envoy.h" +import "C" +import ( + "unsafe" + + "github.com/grab/ego/ego/src/go/volatile" +) + +// requestheadermap implements envoy.RequestHeaderMap +// +type requestTrailerMap struct{ ptr unsafe.Pointer } + +// AddCopy translates to +// Envoy::Http::RequestHeaderMap::addCopy(LowerCaseString, absl::string_view). +// +// See //envoy/include/envoy/http/header_map.h +func (h requestTrailerMap) AddCopy(name, value string) { + C.RequestTrailerMap_add(h.ptr, GoStr(name), GoStr(value)) +} + +func (h requestTrailerMap) SetCopy(name, value string) { + panic("Not implemented yet") +} + +func (h requestTrailerMap) AppendCopy(name, value string) { + panic("Not implemented yet") +} + +func (h requestTrailerMap) Remove(name string) { + panic("Not implemented yet") +} + +func (h requestTrailerMap) Get(name string) volatile.String { + panic("Not implemented yet") + return volatile.String("") +} + +func (h requestTrailerMap) GetByPrefix(prefix string) map[string][]string { + panic("Not implemented yet") + return map[string][]string{} +} diff --git a/ego/src/go/internal/cgo/responseheadermap.go b/ego/src/go/internal/cgo/responseheadermap.go new file mode 100644 index 0000000..421c8f1 --- /dev/null +++ b/ego/src/go/internal/cgo/responseheadermap.go @@ -0,0 +1,89 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package main + +// #include "ego/src/cc/goc/envoy.h" +import "C" +import ( + "unsafe" + + "github.com/grab/ego/ego/src/go/volatile" +) + +// responseHeaderMap implements envoy.ResponseHeaderMap +// +type responseHeaderMap struct{ ptr unsafe.Pointer } + +// AddCopy translates to +// Envoy::Http::ResponseHeaderMap::addCopy(LowerCaseString, absl::string_view). +// +// See //envoy/include/envoy/http/header_map.h +func (h responseHeaderMap) AddCopy(name, value string) { + C.ResponseHeaderMap_add(h.ptr, GoStr(name), GoStr(value)) +} + +// SetCopy translates to +// Envoy::Http::ResponseHeaderMap::setCopy(LowerCaseString, absl::string_view). +// +// See //envoy/include/envoy/http/header_map.h +func (h responseHeaderMap) SetCopy(name, value string) { + C.ResponseHeaderMap_set(h.ptr, GoStr(name), GoStr(value)) +} + +// AppendCopy translates to +// Envoy::Http::ResponseHeaderMap::appendCopy(LowerCaseString, absl::string_view). +// +// See //envoy/include/envoy/http/header_map.h +func (h responseHeaderMap) AppendCopy(name, value string) { + C.ResponseHeaderMap_append(h.ptr, GoStr(name), GoStr(value)) +} + +// Remove translates to +// Envoy::Http::ResponseHeaderMap::remove(LowerCaseString). +// +// See //envoy/include/envoy/http/header_map.h +func (h responseHeaderMap) Remove(name string) { + C.ResponseHeaderMap_remove(h.ptr, GoStr(name)) +} + +// ContentType translates to +// Envoy::Http::ResponseHeaderMap::ContentType(). +// +// See //envoy/include/envoy/http/header_map.h +func (h responseHeaderMap) ContentType() volatile.String { + var value C.GoStr + C.ResponseHeaderMap_ContentType(h.ptr, &value) + return CStrN(value.data, value.len) +} + +// Status translates to +// Envoy::Http::ResponseHeaderMap::Status(). +// +// See //envoy/include/envoy/http/header_map.h +func (h responseHeaderMap) Status() volatile.String { + var value C.GoStr + C.ResponseHeaderMap_Status(h.ptr, &value) + return CStrN(value.data, value.len) +} + +// SetStatus translates to +// Envoy::Http::ResponseHeaderMap::setStatus(uint64_t). +// +// See //envoy/include/envoy/http/header_map.h +func (h responseHeaderMap) SetStatus(status int) { + C.ResponseHeaderMap_setStatus(h.ptr, C.int(status)) + +} + +// Get translates to +// Envoy::Http::ResponseHeaderMap::Get(LowerCaseString). +// +// See //envoy/include/envoy/http/header_map.h +func (h responseHeaderMap) Get(name string) volatile.String { + var value C.GoStr + C.ResponseHeaderMap_get(h.ptr, GoStr(name), &value) + return CStrN(value.data, value.len) +} diff --git a/ego/src/go/internal/cgo/route.go b/ego/src/go/internal/cgo/route.go new file mode 100644 index 0000000..500fcbc --- /dev/null +++ b/ego/src/go/internal/cgo/route.go @@ -0,0 +1,64 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package main + +// #include "ego/src/cc/goc/envoy.h" +import "C" +import ( + "fmt" + "unsafe" + + "github.com/grab/ego/ego/src/go/envoy" + "github.com/grab/ego/ego/src/go/volatile" +) + +type route struct { + filter unsafe.Pointer + encoder bool +} + +func (r *route) RouteEntry() envoy.RouteEntry { + return &routeEntry{r.filter, r.encoder} +} + +type routeEntry struct { + filter unsafe.Pointer + encoder bool +} + +func (e *routeEntry) PathMatchCriterion() envoy.PathMatchCriterion { + return &pathMatchCriterion{e.filter, e.encoder} +} + +type pathMatchCriterion struct { + filter unsafe.Pointer + encoder bool +} + +func (c *pathMatchCriterion) MatchType() (envoy.PathMatchType, error) { + matchType := C.GoHttpFilter_StreamFilterCallbacks_route_routeEntry_pathMatchCriterion_matchType(c.filter, GoBool(c.encoder)) + switch matchType { + case 0: + return envoy.PathMatchNone, nil + case 1: + return envoy.PathMatchPrefix, nil + case 2: + return envoy.PathMatchExact, nil + case 3: + return envoy.PathMatchRegex, nil + } + return envoy.PathMatchNone, fmt.Errorf("invalid match type of %v", matchType) +} + +func (c *pathMatchCriterion) Matcher() (volatile.String, error) { + var value C.GoStr + errCode := C.GoHttpFilter_StreamFilterCallbacks_route_routeEntry_pathMatchCriterion_matcher(c.filter, GoBool(c.encoder), &value) + if errCode != 0 { + return volatile.String(""), fmt.Errorf("can't get matcher. error code %d", errCode) + } + + return CStrN(value.data, value.len), nil +} diff --git a/ego/src/go/internal/cgo/span.go b/ego/src/go/internal/cgo/span.go new file mode 100644 index 0000000..9356226 --- /dev/null +++ b/ego/src/go/internal/cgo/span.go @@ -0,0 +1,60 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package main + +// #include "ego/src/cc/goc/envoy.h" +import "C" + +import ( + "unsafe" + + "github.com/golang/protobuf/proto" + + pb "github.com/grab/ego/ego/src/cc/goc/proto" + "github.com/grab/ego/ego/src/go/envoy" +) + +type span struct { + filter unsafe.Pointer + spanID int64 +} + +func (s span) GetContext() map[string][]string { + defaultBufferSize := uint64(100) + data := make([]byte, defaultBufferSize) + size := uint64(C.GoHttpFilter_Span_getContext(s.filter, C.long(s.spanID), GoBuf(data))) + if size > defaultBufferSize { + newBufferSize := size + data = make([]byte, newBufferSize) + size = uint64(C.GoHttpFilter_Span_getContext(s.filter, C.long(s.spanID), GoBuf(data))) + if size > newBufferSize { + // it's not supposed to happen. + panic("Invalid logic") + } + } + headers := pb.RequestHeaderMap{} + if err := proto.Unmarshal(data[:size], &headers); err != nil { + // TODO: handle error + } + + result := make(map[string][]string) + for _, h := range headers.Headers { + if _, existing := result[h.Key]; !existing { + result[h.Key] = []string{h.Value} + } + } + + return result +} + +func (s span) SpawnChild(name string) envoy.Span { + spanID := C.GoHttpFilter_Span_spawnChild(s.filter, C.long(s.spanID), GoStr(name)) + return span{filter: s.filter, spanID: int64(spanID)} +} + +func (s span) FinishSpan() { + C.GoHttpFilter_Span_finishSpan(s.filter, C.long(s.spanID)) +} diff --git a/ego/src/go/internal/cgo/stats.go b/ego/src/go/internal/cgo/stats.go new file mode 100644 index 0000000..f2c6c6c --- /dev/null +++ b/ego/src/go/internal/cgo/stats.go @@ -0,0 +1,108 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package main + +// #include "ego/src/cc/goc/envoy.h" +import "C" +import ( + "unsafe" + + "github.com/grab/ego/ego/src/go/envoy" + "github.com/grab/ego/ego/src/go/envoy/stats" +) + +type scope struct { + ptr unsafe.Pointer +} + +func (s scope) CounterFromStatName(name string) envoy.Counter { + ptr := C.Stats_Scope_counterFromStatName(s.ptr, GoStr(name)) + if ptr == nil { + return nil + } + return counter{ptr} +} + +func (s scope) GaugeFromStatName(name string, importMode stats.ImportMode) envoy.Gauge { + ptr := C.Stats_Scope_gaugeFromStatName(s.ptr, GoStr(name), C.int(importMode)) + if ptr == nil { + return nil + } + return gauge{ptr} +} + +func (s scope) HistogramFromStatName(name string, unit stats.Unit) envoy.Histogram { + ptr := C.Stats_Scope_histogramFromStatName(s.ptr, GoStr(name), C.int(unit)) + if ptr == nil { + return nil + } + return histogram{ptr} +} + +type counter struct { + ptr unsafe.Pointer +} + +func (c counter) Add(amount uint64) { + C.Stats_Counter_add(c.ptr, C.uint64_t(amount)) +} + +func (c counter) Inc() { + C.Stats_Counter_inc(c.ptr) +} + +func (c counter) Latch() uint64 { + return uint64(C.Stats_Counter_latch(c.ptr)) +} + +func (c counter) Reset() { + C.Stats_Counter_reset(c.ptr) +} + +func (c counter) Value() uint64 { + return uint64(C.Stats_Counter_value(c.ptr)) + +} + +type gauge struct { + ptr unsafe.Pointer +} + +func (g gauge) Add(amount uint64) { + C.Stats_Gauge_add(g.ptr, C.uint64_t(amount)) +} + +func (g gauge) Dec() { + C.Stats_Gauge_dec(g.ptr) +} + +func (g gauge) Inc() { + C.Stats_Gauge_inc(g.ptr) +} + +func (g gauge) Set(value uint64) { + C.Stats_Gauge_set(g.ptr, C.uint64_t(value)) +} + +func (g gauge) Sub(amount uint64) { + C.Stats_Gauge_sub(g.ptr, C.uint64_t(amount)) +} + +func (g gauge) Value() uint64 { + return uint64(C.Stats_Gauge_value(g.ptr)) +} + +type histogram struct { + ptr unsafe.Pointer +} + +func (h histogram) Unit() stats.Unit { + return stats.Unit(C.Stats_Histogram_unit(h.ptr)) +} + +func (h histogram) RecordValue(value uint64) { + C.Stats_Histogram_recordValue(h.ptr, C.uint64_t(value)) +} diff --git a/ego/src/go/internal/cgo/stream_info.go b/ego/src/go/internal/cgo/stream_info.go new file mode 100644 index 0000000..324cb4e --- /dev/null +++ b/ego/src/go/internal/cgo/stream_info.go @@ -0,0 +1,46 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package main + +// #include "ego/src/cc/goc/envoy.h" +import "C" +import ( + "unsafe" + + "github.com/grab/ego/ego/src/go/envoy" + "github.com/grab/ego/ego/src/go/volatile" +) + +type streamInfo struct { + filter unsafe.Pointer + encoder bool +} + +func (i streamInfo) FilterState() envoy.FilterState { + return filterState{i.filter, i.encoder} +} + +func (i streamInfo) LastDownstreamTxByteSent() int64 { + return CLong(C.GoHttpFilter_StreamFilterCallbacks_StreamInfo_lastDownstreamTxByteSent(i.filter, GoBool(i.encoder))) +} + +func (i streamInfo) GetRequestHeaders() envoy.RequestHeaderMapReadOnly { + ptr := C.GoHttpFilter_StreamFilterCallbacks_StreamInfo_getRequestHeaders(i.filter, GoBool(i.encoder)) + if ptr == nil { + return nil + } + return &requestHeaderMap{ptr} +} + +func (i streamInfo) ResponseCode() int { + return int(C.GoHttpFilter_StreamFilterCallbacks_StreamInfo_responseCode(i.filter, GoBool(i.encoder))) +} + +func (i streamInfo) ResponseCodeDetails() volatile.String { + var value C.GoStr + C.GoHttpFilter_StreamFilterCallbacks_StreamInfo_responseCodeDetails(i.filter, GoBool(i.encoder), &value) + return CStrN(value.data, value.len) +} diff --git a/ego/src/go/logger/BUILD.bazel b/ego/src/go/logger/BUILD.bazel new file mode 100644 index 0000000..a49f4d4 --- /dev/null +++ b/ego/src/go/logger/BUILD.bazel @@ -0,0 +1,14 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["logger.go"], + importpath = "github.com/grab/ego/ego/src/go/logger", + visibility = ["//visibility:public"], + deps = ["//ego/src/go/envoy/loglevel:go_default_library"], +) diff --git a/ego/src/go/logger/logger.go b/ego/src/go/logger/logger.go new file mode 100644 index 0000000..94a8b76 --- /dev/null +++ b/ego/src/go/logger/logger.go @@ -0,0 +1,93 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package logger + +import ( + "fmt" + + "github.com/grab/ego/ego/src/go/envoy/loglevel" +) + +// Logger is an interface to encourage developers using this interface +type Logger interface { + Trace(message string, data ...interface{}) + Debug(message string, data ...interface{}) + Info(message string, data ...interface{}) + Warn(message string, data ...interface{}) + Error(message string, data ...interface{}) + Critical(message string, data ...interface{}) + // TODO: Fatal just like Critical but include throw exception on C, not implmeneted yet +} + +// Configure as neeeded for your log aggregator... +var Render func(string, ...interface{}) string = func(message string, data ...interface{}) string { + if 0 < len(data) { + message += fmt.Sprintf(" %+v", data) + } + return message +} + +// Data is an alias to make easier using Log function +type Data map[string]interface{} + +// NewLogger with utility wrapper log level & tag +func NewLogger(tag string, native NativeLogger) Logger { + finalNativeLogger := native + if finalNativeLogger == nil { + finalNativeLogger = defaultLogger + } + return envoyLogger{tag: tag, native: finalNativeLogger} +} + +// NewDefaultLogger with utility wrapper log level & tag +func NewDefaultLogger(tag string) Logger { + return envoyLogger{tag: tag, native: defaultLogger} +} + +var defaultLogger NativeLogger + +func Init(native NativeLogger) { + defaultLogger = native +} + +type envoyLogger struct { + tag string + native NativeLogger +} + +func (l envoyLogger) Trace(message string, data ...interface{}) { + l.log(loglevel.Trace, l.tag, message, data...) +} + +func (l envoyLogger) Debug(message string, data ...interface{}) { + l.log(loglevel.Debug, l.tag, message, data...) +} + +func (l envoyLogger) Info(message string, data ...interface{}) { + l.log(loglevel.Info, l.tag, message, data...) +} + +func (l envoyLogger) Warn(message string, data ...interface{}) { + l.log(loglevel.Warn, l.tag, message, data...) +} + +func (l envoyLogger) Error(message string, data ...interface{}) { + l.log(loglevel.Error, l.tag, message, data...) +} + +func (l envoyLogger) Critical(message string, data ...interface{}) { + l.log(loglevel.Critical, l.tag, message, data...) +} + +// Log is a wrapper for suggesting developers use it with log.Data +func (l envoyLogger) log(level loglevel.Type, tag, message string, data ...interface{}) { + l.native.Log(level, tag, Render(message, data...)) +} + +// NativeLogger is an interface to C Logger +type NativeLogger interface { + Log(level loglevel.Type, tag, message string) +} diff --git a/ego/src/go/registry.go b/ego/src/go/registry.go new file mode 100644 index 0000000..4265f9c --- /dev/null +++ b/ego/src/go/registry.go @@ -0,0 +1,30 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package ego + +import ( + "github.com/grab/ego/ego/src/go/envoy" + "github.com/grab/ego/ego/src/go/volatile" +) + +type HttpFilterFactoryFactory interface { + CreateFilterFactory(config envoy.GoHttpFilterConfig) (HttpFilterFactory, error) + CreateRouteSpecificFilterConfig(config envoy.GoHttpFilterConfig) (interface{}, error) +} + +var httpFilterFactoryFactories = map[string]HttpFilterFactoryFactory{} + +func RegisterHttpFilter(name string, factory HttpFilterFactoryFactory) HttpFilterFactoryFactory { + if _, found := httpFilterFactoryFactories[name]; found { + // TODO: log error, make some noise + } + httpFilterFactoryFactories[name] = factory + return factory +} + +func GetHttpFilterFactoryFactory(name volatile.String) HttpFilterFactoryFactory { + return httpFilterFactoryFactories[string(name)] +} diff --git a/ego/src/go/volatile/BUILD.bazel b/ego/src/go/volatile/BUILD.bazel new file mode 100644 index 0000000..05b6227 --- /dev/null +++ b/ego/src/go/volatile/BUILD.bazel @@ -0,0 +1,14 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["volatile.go"], + cgo = True, + importpath = "github.com/grab/ego/ego/src/go/volatile", + visibility = ["//visibility:public"], +) diff --git a/ego/src/go/volatile/volatile.go b/ego/src/go/volatile/volatile.go new file mode 100644 index 0000000..763caab --- /dev/null +++ b/ego/src/go/volatile/volatile.go @@ -0,0 +1,32 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package volatile + +import "C" +import ( + "reflect" + "unsafe" +) + +// String should not be kept anywhere beyond the life cycle of the C callback. +// In order to secure its contents, use String.Copy() +type String string + +// Bytes should not be kept anywhere beyond the life cycle of the C callback. +// In order to secure its contents, use Bytes.Copy() +type Bytes []byte + +// Copy creates a non-volatile, "safe" copy of the volatile string +func (s String) Copy() string { + h := (*reflect.StringHeader)(unsafe.Pointer(&s)) + return C.GoStringN((*C.char)(unsafe.Pointer(h.Data)), C.int(h.Len)) +} + +// Copy creates a non-volatile, "safe" copy of the volatile buffer +func (b Bytes) Copy() []byte { + h := (*reflect.SliceHeader)(unsafe.Pointer(&b)) + return C.GoBytes(unsafe.Pointer(h.Data), C.int(h.Cap))[:h.Len] +} diff --git a/ego/test/cc/filter/http/BUILD.bazel b/ego/test/cc/filter/http/BUILD.bazel new file mode 100644 index 0000000..2a535f4 --- /dev/null +++ b/ego/test/cc/filter/http/BUILD.bazel @@ -0,0 +1,40 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +package(default_visibility = ["//visibility:public"]) + +load( + "@envoy//bazel:envoy_build_system.bzl", + "envoy_cc_mock", +) + +load( + ":linkopts.bzl", + "ego_cc_test", +) + +ego_cc_test( + name = "http_filter_test", + srcs = [ + "filter_test.cc", + "span-group_test.cc", + ], + repository = "@envoy", + deps = [ + ":cgo_proxy_mocks", + "//ego/src/cc/filter/http:cgo", + "//ego/src/cc/filter/http:factory", + "//ego/src/cc/filter/http:native", + "//egofilters/http/getheader/proto:pkg_cc_proto", + "//egofilters/http/security/proto:pkg_cc_proto", + "@envoy//test/mocks/http:http_mocks", + ], +) + +envoy_cc_mock( + name = "cgo_proxy_mocks", + hdrs = ["mocks.h"], + repository = "@envoy", +) diff --git a/ego/test/cc/filter/http/filter_test.cc b/ego/test/cc/filter/http/filter_test.cc new file mode 100644 index 0000000..407c728 --- /dev/null +++ b/ego/test/cc/filter/http/filter_test.cc @@ -0,0 +1,311 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include "test/mocks/http/mocks.h" +#include "test/test_common/utility.h" + +#include "ego/src/cc/filter/http/filter.h" +#include "mocks.h" + +using testing::_; +using testing::NiceMock; +using testing::Return; + +namespace Envoy { +namespace Http { + +class GoHttpRouteSpecificFilterConfigTest : public testing::Test { +public: + void initializeConfig(const std::string& yaml) { + auto config = TestUtility::parseYaml(yaml); + config_ = std::make_shared(config); + } + + std::shared_ptr config_; +}; + +TEST_F(GoHttpRouteSpecificFilterConfigTest, MultipleFilterConfigurations) { + // NOTE: the getheader example is not accurate -- the filter does not support + // route specific configuration. But it should pass. + const std::string config_yaml = R"EOF( + filters: + getheader: + "@type": type.googleapis.com/egodemo.getheader.Settings + key: "x-123" + src: "http://example.com" + hdr: "x-yz" + security: + "@type": type.googleapis.com/ego.security.Requirement + provider_name: hmac + )EOF"; + + initializeConfig(config_yaml); + + ASSERT_NE(config_, nullptr); + + auto cgoTagGetHeader = config_->cgoTag("getheader"); + auto cgoTagSecurity = config_->cgoTag("security"); + EXPECT_NE(0, cgoTagGetHeader); + EXPECT_NE(0, cgoTagSecurity); + EXPECT_NE(cgoTagGetHeader, cgoTagSecurity); +} + +TEST_F(GoHttpRouteSpecificFilterConfigTest, EmptyConfiguration) { + const std::string config_yaml = R"EOF( + filters: + )EOF"; + + initializeConfig(config_yaml); + + ASSERT_NE(config_, nullptr); + EXPECT_EQ(0, config_->cgoTag("getheader")); +} + +class GoHttpFilterResolveMostSpecificPerGoFilterConfigTagTest : public testing::Test { +public: + void initializeFilter(std::string config_yaml) { + auto settings = TestUtility::parseYaml(config_yaml); + stats_scope_ = std::make_shared(); + auto config = std::make_shared(settings, stats_scope_); + + auto api = Envoy::Api::createApiForTest(); + auto cgo_proxy = std::make_shared(); + + filter_ = new GoHttpFilter(config, *api, nullptr, cgo_proxy, std::make_unique()); + stream_filter_ = filter_->ref(); + + filter_->setDecoderFilterCallbacks(decoder_callbacks_); + filter_->setEncoderFilterCallbacks(encoder_callbacks_); + } + + std::shared_ptr + createRouteSpecificFilterConfig(std::string config_yaml) { + auto route_config = TestUtility::parseYaml(config_yaml); + return std::make_shared(route_config); + } + + void cleanUp() { filter_->onDestroy(); } + + GoHttpFilter* filter_; + Envoy::Http::StreamFilterSharedPtr stream_filter_; + NiceMock decoder_callbacks_; + NiceMock encoder_callbacks_; + Envoy::Stats::ScopeSharedPtr stats_scope_; +}; + +TEST_F(GoHttpFilterResolveMostSpecificPerGoFilterConfigTagTest, ValidConfig) { + const std::string config_yaml = R"EOF( + filter: security + )EOF"; + initializeFilter(config_yaml); + + const std::string route_specific_config_yaml = R"EOF( + filters: + security: + "@type": type.googleapis.com/ego.security.Requirement + provider_name: hmac + )EOF"; + auto route_config = createRouteSpecificFilterConfig(route_specific_config_yaml); + EXPECT_CALL(*decoder_callbacks_.route_, perFilterConfig(GoHttpConstants::get().FilterName)) + .WillOnce(Return(route_config.get())); + + auto cgo_tag = filter_->resolveMostSpecificPerGoFilterConfigTag(); + EXPECT_NE(0, cgo_tag); + + cleanUp(); +} + +TEST_F(GoHttpFilterResolveMostSpecificPerGoFilterConfigTagTest, NotExistingConfig) { + const std::string config_yaml = R"EOF( + filter: security + )EOF"; + initializeFilter(config_yaml); + + const std::string route_specific_config_yaml = R"EOF( + filters: + getheader: + "@type": type.googleapis.com/egodemo.getheader.Settings + key: "x-123" + src: "http://example.com" + hdr: "x-yz" + )EOF"; + auto route_config = createRouteSpecificFilterConfig(route_specific_config_yaml); + + EXPECT_CALL(*decoder_callbacks_.route_, perFilterConfig(GoHttpConstants::get().FilterName)) + .WillOnce(Return(route_config.get())); + + auto cgo_tag = filter_->resolveMostSpecificPerGoFilterConfigTag(); + EXPECT_EQ(0, cgo_tag); + + cleanUp(); +} + +TEST_F(GoHttpFilterResolveMostSpecificPerGoFilterConfigTagTest, NullRouteSpecificConfig) { + const std::string config_yaml = R"EOF( + filter: security + )EOF"; + initializeFilter(config_yaml); + + EXPECT_CALL(*decoder_callbacks_.route_, perFilterConfig(GoHttpConstants::get().FilterName)) + .WillOnce(Return(nullptr)); + + auto cgo_tag = filter_->resolveMostSpecificPerGoFilterConfigTag(); + EXPECT_EQ(0, cgo_tag); + + cleanUp(); +} + +TEST_F(GoHttpFilterResolveMostSpecificPerGoFilterConfigTagTest, NullRoute) { + const std::string config_yaml = R"EOF( + filter: security + )EOF"; + initializeFilter(config_yaml); + + EXPECT_CALL(decoder_callbacks_, route).WillOnce(Return(nullptr)); + + auto cgo_tag = filter_->resolveMostSpecificPerGoFilterConfigTag(); + EXPECT_EQ(0, cgo_tag); + + cleanUp(); +} + +TEST_F(GoHttpFilterResolveMostSpecificPerGoFilterConfigTagTest, NullRouteEntry) { + const std::string config_yaml = R"EOF( + filter: security + )EOF"; + initializeFilter(config_yaml); + + EXPECT_CALL(*decoder_callbacks_.route_, routeEntry).WillOnce(Return(nullptr)); + + auto cgo_tag = filter_->resolveMostSpecificPerGoFilterConfigTag(); + EXPECT_EQ(0, cgo_tag); + + cleanUp(); +} + +class GoHttpFilterTest : public testing::Test { +public: + void initializeFilter() { + const std::string config_yaml = R"EOF( + filter: security + )EOF"; + auto settings = TestUtility::parseYaml(config_yaml); + stats_scope_ = std::make_shared(); + auto config = std::make_shared(settings, stats_scope_); + auto api = Envoy::Api::createApiForTest(); + + cgo_proxy_ = std::make_shared>(); + EXPECT_CALL(*cgo_proxy_, GoHttpFilterCreate).WillOnce(Return(100)); + + filter_ = new GoHttpFilter(config, *api, nullptr, cgo_proxy_, std::make_unique()); + stream_filter_ = filter_->ref(); + + filter_->setDecoderFilterCallbacks(decoder_callbacks_); + filter_->setEncoderFilterCallbacks(encoder_callbacks_); + } + + void cleanUp() { filter_->onDestroy(); } + + GoHttpFilter* filter_; + Envoy::Http::StreamFilterSharedPtr stream_filter_; + NiceMock decoder_callbacks_; + NiceMock encoder_callbacks_; + std::shared_ptr cgo_proxy_; + Envoy::Stats::ScopeSharedPtr stats_scope_; +}; + +TEST_F(GoHttpFilterTest, DecodeHeaders) { + initializeFilter(); + + Http::TestRequestHeaderMapImpl request_headers; + EXPECT_CALL(*cgo_proxy_, GoHttpFilterDecodeHeaders(_, &request_headers, true)); + + filter_->decodeHeaders(request_headers, true); + + cleanUp(); +} + +TEST_F(GoHttpFilterTest, DecodeData) { + initializeFilter(); + + Buffer::OwnedImpl data; + EXPECT_CALL(*cgo_proxy_, GoHttpFilterDecodeData(_, &data, true)); + + filter_->decodeData(data, true); + + cleanUp(); +} + +TEST_F(GoHttpFilterTest, DecodeTrailers) { + initializeFilter(); + + Http::TestRequestTrailerMapImpl request_trailers; + EXPECT_CALL(*cgo_proxy_, GoHttpFilterDecodeTrailers(_, &request_trailers)); + + filter_->decodeTrailers(request_trailers); + + cleanUp(); +} + +TEST_F(GoHttpFilterTest, EncodeHeaders) { + initializeFilter(); + + Http::TestResponseHeaderMapImpl response_headers; + EXPECT_CALL(*cgo_proxy_, GoHttpFilterEncodeHeaders(_, &response_headers, true)); + + filter_->encodeHeaders(response_headers, true); + + cleanUp(); +} + +TEST_F(GoHttpFilterTest, EncodeData) { + initializeFilter(); + + Buffer::OwnedImpl data; + EXPECT_CALL(*cgo_proxy_, GoHttpFilterEncodeData(_, &data, false)); + + filter_->encodeData(data, false); + + cleanUp(); +} + +TEST_F(GoHttpFilterTest, StreamFilterCallbacksWithFalseEncoder) { + initializeFilter(); + + auto *callbacks = filter_->streamFilterCallbacks(0); + + EXPECT_EQ(&decoder_callbacks_, callbacks); + + cleanUp(); +} + +TEST_F(GoHttpFilterTest, StreamFilterCallbacksWithTrueEncoder) { + initializeFilter(); + + auto *callbacks = filter_->streamFilterCallbacks(1); + + EXPECT_EQ(&encoder_callbacks_, callbacks); + + cleanUp(); +} + +TEST_F(GoHttpFilterTest, PostDownCallFlow) { + initializeFilter(); + + auto post_tag = 1; + EXPECT_CALL(decoder_callbacks_.dispatcher_, post(_)).WillOnce([](std::function callback) { + callback(); + }); + EXPECT_CALL(*cgo_proxy_, GoHttpFilterOnPost); + + filter_->pin(); + filter_->post(post_tag); + filter_->unpin(); + + cleanUp(); +} + +} // namespace Http +} // namespace Envoy \ No newline at end of file diff --git a/ego/test/cc/filter/http/linkopts.bzl b/ego/test/cc/filter/http/linkopts.bzl new file mode 100644 index 0000000..49a927f --- /dev/null +++ b/ego/test/cc/filter/http/linkopts.bzl @@ -0,0 +1,131 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +# Yes, it's all about "-framework Security". This is a bit tricky because +# `envoy_cc_test` sits on `linkopts` without recourse. + +#load("@rules_python//python:defs.bzl", "py_binary") +load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "cc_test") +#load("@rules_fuzzing//fuzzing:cc_defs.bzl", "fuzzing_decoration") +#load(":envoy_binary.bzl", "envoy_cc_binary") +load("@envoy//bazel:envoy_library.bzl", "tcmalloc_external_deps") +load( + "@envoy//bazel:envoy_internal.bzl", + "envoy_copts", + "envoy_external_dep_path", + "envoy_linkstatic", + "envoy_select_force_libcpp", + "envoy_stdlib_deps", + "tcmalloc_external_dep", +) + +# copy & paste from @envoy//bazel:envoy_test.bzl with small changes +def _envoy_cc_test_infrastructure_library( + name, + srcs = [], + hdrs = [], + data = [], + external_deps = [], + deps = [], + repository = "", + tags = [], + include_prefix = None, + copts = [], + **kargs): + # Add implicit tcmalloc external dependency(if available) in order to enable CPU and heap profiling in tests. + deps += tcmalloc_external_deps(repository) + + native.cc_library( + name = name, + srcs = srcs, + hdrs = hdrs, + data = data, + copts = envoy_copts(repository, test = True) + copts, + testonly = 1, + deps = deps + [envoy_external_dep_path(dep) for dep in external_deps] + [ + envoy_external_dep_path("googletest"), + ], + tags = tags, + include_prefix = include_prefix, + alwayslink = 1, + linkstatic = envoy_linkstatic(), + **kargs + ) + +# copy & paste from @envoy//bazel:envoy_test.bzl with small changes +def _envoy_test_linkopts(): + return select({ + "@envoy//bazel:apple": [ + # See note here: https://luajit.org/install.html + "-pagezero_size 10000", + "-image_base 100000000", + "-framework Security", + ], + "@envoy//bazel:windows_x86_64": [ + "-DEFAULTLIB:advapi32.lib", + "-DEFAULTLIB:ws2_32.lib", + "-WX", + ], + + # TODO(mattklein123): It's not great that we universally link against the following libs. + # In particular, -latomic and -lrt are not needed on all platforms. Make this more granular. + "//conditions:default": ["-pthread", "-lrt", "-ldl"], + }) + envoy_select_force_libcpp([], ["-lstdc++fs", "-latomic"]) + +# copy & paste from @envoy//bazel:envoy_test.bzl with small changes +def ego_cc_test( + name, + srcs = [], + data = [], + # List of pairs (Bazel shell script target, shell script args) + repository = "", + external_deps = [], + deps = [], + tags = [], + args = [], + copts = [], + shard_count = None, + coverage = True, + local = False, + size = "medium", + flaky = False): + if coverage: + coverage_tags = tags + ["coverage_test_lib"] + else: + coverage_tags = tags + + _envoy_cc_test_infrastructure_library( + name = name + "_lib_internal_only", + srcs = srcs, + data = data, + external_deps = external_deps, + deps = deps + ["@envoy//test/test_common:printers_includes"], + repository = repository, + tags = coverage_tags, + copts = copts, + # Allow public visibility so these can be consumed in coverage tests in external projects. + visibility = ["//visibility:public"], + ) + if coverage: + coverage_tags = tags + ["coverage_test"] + native.cc_test( + name = name, + copts = envoy_copts(repository, test = True) + copts, + linkopts = _envoy_test_linkopts(), + linkstatic = envoy_linkstatic(), + malloc = tcmalloc_external_dep(repository), + deps = envoy_stdlib_deps() + [ + ":" + name + "_lib_internal_only", + "@envoy//test:main", + ], + # from https://github.com/google/googletest/blob/6e1970e2376c14bf658eb88f655a054030353f9f/googlemock/src/gmock.cc#L51 + # 2 - by default, mocks act as StrictMocks. + args = args + ["--gmock_default_mock_behavior=2"], + tags = coverage_tags, + local = local, + shard_count = shard_count, + size = size, + flaky = flaky, + ) diff --git a/ego/test/cc/filter/http/mocks.h b/ego/test/cc/filter/http/mocks.h new file mode 100644 index 0000000..c47ae38 --- /dev/null +++ b/ego/test/cc/filter/http/mocks.h @@ -0,0 +1,35 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include "gmock/gmock.h" // Brings in gMock. + +namespace Envoy { +namespace Http { + +class MockCgoProxy : public CgoProxy { +public: + MockCgoProxy() = default; + ~MockCgoProxy() override = default; + + MOCK_METHOD(unsigned long long, GoHttpFilterCreate, + (void* native, unsigned long long factory_tag, unsigned long long filterSlot), + (override)); + MOCK_METHOD(void, GoHttpFilterOnDestroy, (unsigned long long filter_tag), (override)); + MOCK_METHOD(long long, GoHttpFilterDecodeHeaders, + (unsigned long long filter_tag, void* headers, int end_stream), (override)); + MOCK_METHOD(long long, GoHttpFilterDecodeData, + (unsigned long long filter_tag, void* buffer, int end_stream), (override)); + MOCK_METHOD(long long, GoHttpFilterDecodeTrailers, + (unsigned long long filter_tag, void* trailers), (override)); + MOCK_METHOD(long long, GoHttpFilterEncodeHeaders, + (unsigned long long filter_tag, void* headers, int end_stream), (override)); + MOCK_METHOD(long long, GoHttpFilterEncodeData, + (unsigned long long filter_tag, void* headers, int end_stream), (override)); + MOCK_METHOD(void, GoHttpFilterOnPost, + (unsigned long long filter_tag, unsigned long long post_tag), (override)); +}; + +} // namespace Http +} // namespace Envoy \ No newline at end of file diff --git a/ego/test/cc/filter/http/span-group_test.cc b/ego/test/cc/filter/http/span-group_test.cc new file mode 100644 index 0000000..c0f05d6 --- /dev/null +++ b/ego/test/cc/filter/http/span-group_test.cc @@ -0,0 +1,136 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include "test/mocks/http/mocks.h" +#include "test/mocks/tracing/mocks.h" +#include "test/test_common/utility.h" +#include "ego/src/cc/filter/http/span-group.h" +#include "common/tracing/http_tracer_impl.h" + + +using testing::_; +using testing::NiceMock; +using testing::Return; +using testing::ByMove; +using testing::AtLeast; + +namespace Envoy { +namespace Http { + +class SpanGroupTest : public testing::Test { + public: + void initialize(){ + span_group_.setDecoderFilterCallbacks(decoder_callbacks_); + span_group_.setEncoderFilterCallbacks(encoder_callbacks_); + EXPECT_CALL(decoder_callbacks_, activeSpan()).WillRepeatedly(ReturnRef(decoder_active_span_)); + EXPECT_CALL(encoder_callbacks_, activeSpan()).WillRepeatedly(ReturnRef(encoder_active_span_)); + }; + + SpanGroup span_group_; + NiceMock decoder_callbacks_; + NiceMock encoder_callbacks_; + NiceMock decoder_active_span_; + NiceMock encoder_active_span_; +}; + +TEST_F(SpanGroupTest, ReturnsActiveSpanFromDecoderCallbacksIfSpanIDIsMinusOne) { + initialize(); + + EXPECT_EQ(&decoder_callbacks_.activeSpan(), &span_group_.getSpan(SpanGroupConstants::get().DecoderActiveSpan)); +} + +TEST_F(SpanGroupTest, ReturnsActiveSpanFromEncoderCallbacksIfSpanIDIsZero) { + initialize(); + + EXPECT_EQ(&encoder_callbacks_.activeSpan(), &span_group_.getSpan(SpanGroupConstants::get().EncoderActiveSpan)); +} + +TEST_F(SpanGroupTest, ReturnsNullSpanIfNotFoundSpanID) { + initialize(); + + EXPECT_EQ(&Envoy::Tracing::NullSpan::instance(), &span_group_.getSpan(10)); +} + +TEST_F(SpanGroupTest, ReturnsNullSpanIfDecoderCallbacksIsNull) { + EXPECT_EQ(&Envoy::Tracing::NullSpan::instance(), &span_group_.getSpan(SpanGroupConstants::get().DecoderActiveSpan)); +} + +TEST_F(SpanGroupTest, ReturnsNullSpanIfEncoderCallbacksIsNull) { + EXPECT_EQ(&Envoy::Tracing::NullSpan::instance(), &span_group_.getSpan(SpanGroupConstants::get().EncoderActiveSpan)); +} + +TEST_F(SpanGroupTest, SpawnChildFromDecoderActiveSpan) { + initialize(); + + Tracing::MockSpan* child_span{new Tracing::MockSpan()}; + EXPECT_CALL(decoder_active_span_, spawnChild_).WillOnce(Return(child_span)); + + auto name = std::string("name"); + auto span_id = span_group_.spawnChildSpan(SpanGroupConstants::get().DecoderActiveSpan, name); + + EXPECT_CALL(*child_span, finishSpan).Times(AtLeast(1)); + span_group_.getSpan(span_id).finishSpan(); +} + +TEST_F(SpanGroupTest, SpawnChildFromEncoderActiveSpan) { + initialize(); + + Tracing::MockSpan* child_span{new Tracing::MockSpan()}; + EXPECT_CALL(encoder_active_span_, spawnChild_).WillOnce(Return(child_span)); + + auto name = std::string("name"); + auto span_id = span_group_.spawnChildSpan(SpanGroupConstants::get().EncoderActiveSpan, name); + + EXPECT_CALL(*child_span, finishSpan).Times(AtLeast(1)); + span_group_.getSpan(span_id).finishSpan(); +} + +TEST_F(SpanGroupTest, SpawnChildFromExistingSpan) { + initialize(); + + Tracing::MockSpan* child_span{new Tracing::MockSpan()}; + EXPECT_CALL(decoder_active_span_, spawnChild_).WillOnce(Return(child_span)); + + auto child_name = std::string("child_name"); + auto child_span_id = span_group_.spawnChildSpan(SpanGroupConstants::get().DecoderActiveSpan, child_name); + + Tracing::MockSpan* grand_child_span{new Tracing::MockSpan()}; + EXPECT_CALL(*child_span, spawnChild_).WillOnce(Return(grand_child_span)); + + auto grand_child_name = std::string("grand_child_name"); + auto grand_child_span_id = span_group_.spawnChildSpan(child_span_id, grand_child_name); + + EXPECT_CALL(*grand_child_span, finishSpan).Times(AtLeast(1)); + span_group_.getSpan(grand_child_span_id).finishSpan(); +} + +TEST_F(SpanGroupTest, SpawnChildFromNullSpan) { + initialize(); + + auto name = std::string("something"); + auto child_span_id = span_group_.spawnChildSpan(100, name); + + span_group_.getSpan(child_span_id).finishSpan(); +} + +TEST_F(SpanGroupTest, DeleteSpan) { + initialize(); + + Tracing::MockSpan* child_span{new Tracing::MockSpan()}; + EXPECT_CALL(encoder_active_span_, spawnChild_).WillOnce(Return(child_span)); + + auto name = std::string("something"); + auto child_span_id = span_group_.spawnChildSpan(SpanGroupConstants::get().EncoderActiveSpan, name); + span_group_.removeSpan(child_span_id); + EXPECT_EQ(&Envoy::Tracing::NullSpan::instance(), &span_group_.getSpan(child_span_id)); +} + +TEST_F(SpanGroupTest, DeleteNotExistingSpan) { + initialize(); + span_group_.removeSpan(123); +} + +} +} \ No newline at end of file diff --git a/ego/test/cc/goc/BUILD.bazel b/ego/test/cc/goc/BUILD.bazel new file mode 100644 index 0000000..4999151 --- /dev/null +++ b/ego/test/cc/goc/BUILD.bazel @@ -0,0 +1,23 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +package(default_visibility = ["//visibility:public"]) + +load( + "@envoy//bazel:envoy_build_system.bzl", + "envoy_cc_test", +) + +envoy_cc_test( + name = "goc_test", + srcs = [ + "requestheadermap_test.cc", + ], + repository = "@envoy", + deps = [ + "//ego/src/cc/goc:goc", + "@envoy//test/test_common:utility_lib", + ], +) diff --git a/ego/test/cc/goc/requestheadermap_test.cc b/ego/test/cc/goc/requestheadermap_test.cc new file mode 100644 index 0000000..d8ad0c3 --- /dev/null +++ b/ego/test/cc/goc/requestheadermap_test.cc @@ -0,0 +1,98 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +#include "test/test_common/utility.h" + +#include "ego/src/cc/filter/http/filter.h" +#include "ego/src/cc/goc/envoy.h" +#include "ego/src/cc/goc/proto/dto.pb.validate.h" + +class RequestHeaderMapGetByPrefixTest : public testing::Test { +public: + void init(char* prefix, char* buffer, size_t buffer_size) { + EXPECT_EQ(strlen(prefix), 5); + prefix_.len = strlen(prefix); + prefix_.data = prefix; + + buffer_.data = buffer; + buffer_.len = buffer_size; + buffer_.cap = buffer_size; + } + + GoBuf buffer_; + GoStr prefix_; +}; + +TEST_F(RequestHeaderMapGetByPrefixTest, FoundHeaders) { + Envoy::Http::TestRequestHeaderMapImpl request_headers{{"x-ego1", "val1"}, {"x-ego2", "val2"}}; + + char c_buffer[100]; + char c_prefix[] = "x-ego"; + init(c_prefix, c_buffer, 100); + + auto size = RequestHeaderMap_getByPrefix(&request_headers, prefix_, buffer_); + EXPECT_LT(0, size); + + auto result = ego::http::RequestHeaderMap{}; + auto ok = result.ParseFromArray(buffer_.data, size); + ASSERT_TRUE(ok); + + auto expected_result = ego::http::RequestHeaderMap{}; + + auto x_ego1_header = expected_result.add_headers(); + x_ego1_header->set_key("x-ego1"); + x_ego1_header->set_value("val1"); + + auto x_ego2_header = expected_result.add_headers(); + x_ego2_header->set_key("x-ego2"); + x_ego2_header->set_value("val2"); + + ASSERT_EQ(expected_result.SerializePartialAsString(), result.SerializePartialAsString()); +} + +TEST_F(RequestHeaderMapGetByPrefixTest, NotFoundHeaders) { + Envoy::Http::TestRequestHeaderMapImpl request_headers{}; + + char c_buffer[100]; + char c_prefix[] = "x-ego"; + init(c_prefix, c_buffer, 100); + + auto size = RequestHeaderMap_getByPrefix(&request_headers, prefix_, buffer_); + EXPECT_EQ(0, size); +} + +TEST_F(RequestHeaderMapGetByPrefixTest, ShouldNotWriteOverBufferSize) { + Envoy::Http::TestRequestHeaderMapImpl request_headers{{"x-ego1", "val1"}, {"x-ego2", "val2"}}; + + const auto buffer_size = 34; + char c_buffer[] = "0123456789012345678901234567890123should_not_be_overriden"; + + char c_prefix[] = "x-ego"; + init(c_prefix, c_buffer, buffer_size); + + auto size = RequestHeaderMap_getByPrefix(&request_headers, prefix_, buffer_); + EXPECT_EQ(32, size); + + // getByPrefix should not write over the buffer size + char expected_buffer[] = "0123456789012345678901234567890123should_not_be_overriden"; + ASSERT_STREQ(expected_buffer + buffer_size, c_buffer + buffer_size); +} + +TEST_F(RequestHeaderMapGetByPrefixTest, TooSmallBuffer) { + Envoy::Http::TestRequestHeaderMapImpl request_headers{{"x-ego1", "val1"}, {"x-ego2", "val2"}}; + + const auto buffer_size = 10; + char c_buffer[] = "this is a buffer with existing data"; + + char c_prefix[] = "x-ego"; + init(c_prefix, c_buffer, buffer_size); + + auto size = RequestHeaderMap_getByPrefix(&request_headers, prefix_, buffer_); + EXPECT_GT(size, buffer_size); + + // getByPrefix should not write to buffer size + char expected_buffer[] = "this is a buffer with existing data"; + ASSERT_STREQ(expected_buffer, c_buffer); +} \ No newline at end of file diff --git a/ego/test/go/mock/BUILD.bazel b/ego/test/go/mock/BUILD.bazel new file mode 100644 index 0000000..6f8854f --- /dev/null +++ b/ego/test/go/mock/BUILD.bazel @@ -0,0 +1,22 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "doc.go", + "envoy.go", + "logger.go", + ], + importpath = "github.com/grab/ego/ego/test/go/mock", + visibility = ["//visibility:public"], + deps = [ + "//ego/src/go/envoy:go_default_library", + "//ego/src/go/envoy/loglevel:go_default_library", + "//ego/src/go/volatile:go_default_library", + ], +) diff --git a/ego/test/go/mock/doc.go b/ego/test/go/mock/doc.go new file mode 100644 index 0000000..690064b --- /dev/null +++ b/ego/test/go/mock/doc.go @@ -0,0 +1,3 @@ +package mock + +//go:generate mockery --all --recursive=true --keeptree --case=underscore --output ./gen/envoy --dir ../../../src/go/envoy diff --git a/ego/test/go/mock/envoy.go b/ego/test/go/mock/envoy.go new file mode 100644 index 0000000..c7306b2 --- /dev/null +++ b/ego/test/go/mock/envoy.go @@ -0,0 +1,19 @@ +package mock + +import ( + "github.com/grab/ego/ego/src/go/envoy" + "github.com/grab/ego/ego/src/go/volatile" +) + +type GoHttpFilterConfig struct { + ConfigBytes []byte + EnvoyScope envoy.Scope +} + +func (conf *GoHttpFilterConfig) Settings() volatile.Bytes { + return volatile.Bytes(conf.ConfigBytes) +} + +func (conf *GoHttpFilterConfig) Scope() envoy.Scope { + return conf.EnvoyScope +} diff --git a/ego/test/go/mock/gen/envoy/BUILD.bazel b/ego/test/go/mock/gen/envoy/BUILD.bazel new file mode 100644 index 0000000..fcfec60 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/BUILD.bazel @@ -0,0 +1,51 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "buffer_instance.go", + "counter.go", + "decoder_filter_callbacks.go", + "encoder_filter_callbacks.go", + "filter_state.go", + "gauge.go", + "generic_secret_config_provider.go", + "go_http_filter.go", + "go_http_filter_config.go", + "header_map.go", + "header_map_read_only.go", + "header_map_updatable.go", + "histogram.go", + "path_match_criterion.go", + "request_header_map.go", + "request_header_map_read_only.go", + "request_header_map_updatable.go", + "request_or_response_header_map.go", + "request_or_response_header_map_read_only.go", + "request_or_response_header_map_updatable.go", + "request_trailer_map.go", + "request_trailer_map_read_only.go", + "request_trailer_map_updatable.go", + "response_header_map.go", + "response_header_map_read_only.go", + "response_header_map_updatable.go", + "route.go", + "route_entry.go", + "scope.go", + "span.go", + "stream_filter_callbacks.go", + "stream_info.go", + ], + importpath = "github.com/grab/ego/ego/test/go/mock/gen/envoy", + visibility = ["//visibility:public"], + deps = [ + "//ego/src/cc/goc/proto:go_default_library", + "//ego/src/go/envoy:go_default_library", + "//ego/src/go/envoy/lifespan:go_default_library", + "//ego/src/go/envoy/loglevel:go_default_library", + "//ego/src/go/envoy/statetype:go_default_library", + "//ego/src/go/envoy/stats:go_default_library", + "//ego/src/go/volatile:go_default_library", + "@com_github_stretchr_testify//mock:go_default_library", + ], +) diff --git a/ego/test/go/mock/gen/envoy/buffer_instance.go b/ego/test/go/mock/gen/envoy/buffer_instance.go new file mode 100644 index 0000000..4f11277 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/buffer_instance.go @@ -0,0 +1,75 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + io "io" + + volatile "github.com/grab/ego/ego/src/go/volatile" + mock "github.com/stretchr/testify/mock" +) + +// BufferInstance is an autogenerated mock type for the BufferInstance type +type BufferInstance struct { + mock.Mock +} + +// CopyOut provides a mock function with given fields: start, p +func (_m *BufferInstance) CopyOut(start uint64, p []byte) int { + ret := _m.Called(start, p) + + var r0 int + if rf, ok := ret.Get(0).(func(uint64, []byte) int); ok { + r0 = rf(start, p) + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// GetRawSlices provides a mock function with given fields: +func (_m *BufferInstance) GetRawSlices() []volatile.Bytes { + ret := _m.Called() + + var r0 []volatile.Bytes + if rf, ok := ret.Get(0).(func() []volatile.Bytes); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]volatile.Bytes) + } + } + + return r0 +} + +// Length provides a mock function with given fields: +func (_m *BufferInstance) Length() uint64 { + ret := _m.Called() + + var r0 uint64 + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + return r0 +} + +// NewReader provides a mock function with given fields: start +func (_m *BufferInstance) NewReader(start uint64) io.Reader { + ret := _m.Called(start) + + var r0 io.Reader + if rf, ok := ret.Get(0).(func(uint64) io.Reader); ok { + r0 = rf(start) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(io.Reader) + } + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/counter.go b/ego/test/go/mock/gen/envoy/counter.go new file mode 100644 index 0000000..3431196 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/counter.go @@ -0,0 +1,53 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// Counter is an autogenerated mock type for the Counter type +type Counter struct { + mock.Mock +} + +// Add provides a mock function with given fields: amount +func (_m *Counter) Add(amount uint64) { + _m.Called(amount) +} + +// Inc provides a mock function with given fields: +func (_m *Counter) Inc() { + _m.Called() +} + +// Latch provides a mock function with given fields: +func (_m *Counter) Latch() uint64 { + ret := _m.Called() + + var r0 uint64 + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + return r0 +} + +// Reset provides a mock function with given fields: +func (_m *Counter) Reset() { + _m.Called() +} + +// Value provides a mock function with given fields: +func (_m *Counter) Value() uint64 { + ret := _m.Called() + + var r0 uint64 + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/decoder_filter_callbacks.go b/ego/test/go/mock/gen/envoy/decoder_filter_callbacks.go new file mode 100644 index 0000000..57d89d1 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/decoder_filter_callbacks.go @@ -0,0 +1,113 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + ego_http "github.com/grab/ego/ego/src/cc/goc/proto" + envoy "github.com/grab/ego/ego/src/go/envoy" + + mock "github.com/stretchr/testify/mock" +) + +// DecoderFilterCallbacks is an autogenerated mock type for the DecoderFilterCallbacks type +type DecoderFilterCallbacks struct { + mock.Mock +} + +// ActiveSpan provides a mock function with given fields: +func (_m *DecoderFilterCallbacks) ActiveSpan() envoy.Span { + ret := _m.Called() + + var r0 envoy.Span + if rf, ok := ret.Get(0).(func() envoy.Span); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.Span) + } + } + + return r0 +} + +// AddDecodedData provides a mock function with given fields: buffer, streamingFilter +func (_m *DecoderFilterCallbacks) AddDecodedData(buffer envoy.BufferInstance, streamingFilter bool) { + _m.Called(buffer, streamingFilter) +} + +// ContinueDecoding provides a mock function with given fields: +func (_m *DecoderFilterCallbacks) ContinueDecoding() { + _m.Called() +} + +// DecodingBuffer provides a mock function with given fields: +func (_m *DecoderFilterCallbacks) DecodingBuffer() envoy.BufferInstance { + ret := _m.Called() + + var r0 envoy.BufferInstance + if rf, ok := ret.Get(0).(func() envoy.BufferInstance); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.BufferInstance) + } + } + + return r0 +} + +// EncodeHeaders provides a mock function with given fields: responseCode, headers, endStream +func (_m *DecoderFilterCallbacks) EncodeHeaders(responseCode int, headers *ego_http.ResponseHeaderMap, endStream bool) { + _m.Called(responseCode, headers, endStream) +} + +// Route provides a mock function with given fields: +func (_m *DecoderFilterCallbacks) Route() envoy.Route { + ret := _m.Called() + + var r0 envoy.Route + if rf, ok := ret.Get(0).(func() envoy.Route); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.Route) + } + } + + return r0 +} + +// RouteExisting provides a mock function with given fields: +func (_m *DecoderFilterCallbacks) RouteExisting() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// SendLocalReply provides a mock function with given fields: responseCode, body, headers, details +func (_m *DecoderFilterCallbacks) SendLocalReply(responseCode int, body string, headers map[string]string, details string) { + _m.Called(responseCode, body, headers, details) +} + +// StreamInfo provides a mock function with given fields: +func (_m *DecoderFilterCallbacks) StreamInfo() envoy.StreamInfo { + ret := _m.Called() + + var r0 envoy.StreamInfo + if rf, ok := ret.Get(0).(func() envoy.StreamInfo); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.StreamInfo) + } + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/encoder_filter_callbacks.go b/ego/test/go/mock/gen/envoy/encoder_filter_callbacks.go new file mode 100644 index 0000000..86a0c8e --- /dev/null +++ b/ego/test/go/mock/gen/envoy/encoder_filter_callbacks.go @@ -0,0 +1,101 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + envoy "github.com/grab/ego/ego/src/go/envoy" + mock "github.com/stretchr/testify/mock" +) + +// EncoderFilterCallbacks is an autogenerated mock type for the EncoderFilterCallbacks type +type EncoderFilterCallbacks struct { + mock.Mock +} + +// ActiveSpan provides a mock function with given fields: +func (_m *EncoderFilterCallbacks) ActiveSpan() envoy.Span { + ret := _m.Called() + + var r0 envoy.Span + if rf, ok := ret.Get(0).(func() envoy.Span); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.Span) + } + } + + return r0 +} + +// AddEncodedData provides a mock function with given fields: buffer, streamingFilter +func (_m *EncoderFilterCallbacks) AddEncodedData(buffer envoy.BufferInstance, streamingFilter bool) { + _m.Called(buffer, streamingFilter) +} + +// ContinueEncoding provides a mock function with given fields: +func (_m *EncoderFilterCallbacks) ContinueEncoding() { + _m.Called() +} + +// EncodingBuffer provides a mock function with given fields: +func (_m *EncoderFilterCallbacks) EncodingBuffer() envoy.BufferInstance { + ret := _m.Called() + + var r0 envoy.BufferInstance + if rf, ok := ret.Get(0).(func() envoy.BufferInstance); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.BufferInstance) + } + } + + return r0 +} + +// Route provides a mock function with given fields: +func (_m *EncoderFilterCallbacks) Route() envoy.Route { + ret := _m.Called() + + var r0 envoy.Route + if rf, ok := ret.Get(0).(func() envoy.Route); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.Route) + } + } + + return r0 +} + +// RouteExisting provides a mock function with given fields: +func (_m *EncoderFilterCallbacks) RouteExisting() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// StreamInfo provides a mock function with given fields: +func (_m *EncoderFilterCallbacks) StreamInfo() envoy.StreamInfo { + ret := _m.Called() + + var r0 envoy.StreamInfo + if rf, ok := ret.Get(0).(func() envoy.StreamInfo); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.StreamInfo) + } + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/filter_state.go b/ego/test/go/mock/gen/envoy/filter_state.go new file mode 100644 index 0000000..d84853c --- /dev/null +++ b/ego/test/go/mock/gen/envoy/filter_state.go @@ -0,0 +1,43 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + lifespan "github.com/grab/ego/ego/src/go/envoy/lifespan" + mock "github.com/stretchr/testify/mock" + + statetype "github.com/grab/ego/ego/src/go/envoy/statetype" + + volatile "github.com/grab/ego/ego/src/go/volatile" +) + +// FilterState is an autogenerated mock type for the FilterState type +type FilterState struct { + mock.Mock +} + +// GetDataReadOnly provides a mock function with given fields: name +func (_m *FilterState) GetDataReadOnly(name string) (volatile.String, bool) { + ret := _m.Called(name) + + var r0 volatile.String + if rf, ok := ret.Get(0).(func(string) volatile.String); ok { + r0 = rf(name) + } else { + r0 = ret.Get(0).(volatile.String) + } + + var r1 bool + if rf, ok := ret.Get(1).(func(string) bool); ok { + r1 = rf(name) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + +// SetData provides a mock function with given fields: name, value, stateType, lifeSpan +func (_m *FilterState) SetData(name string, value string, stateType statetype.Type, lifeSpan lifespan.Type) { + _m.Called(name, value, stateType, lifeSpan) +} diff --git a/ego/test/go/mock/gen/envoy/gauge.go b/ego/test/go/mock/gen/envoy/gauge.go new file mode 100644 index 0000000..971324b --- /dev/null +++ b/ego/test/go/mock/gen/envoy/gauge.go @@ -0,0 +1,49 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// Gauge is an autogenerated mock type for the Gauge type +type Gauge struct { + mock.Mock +} + +// Add provides a mock function with given fields: amount +func (_m *Gauge) Add(amount uint64) { + _m.Called(amount) +} + +// Dec provides a mock function with given fields: +func (_m *Gauge) Dec() { + _m.Called() +} + +// Inc provides a mock function with given fields: +func (_m *Gauge) Inc() { + _m.Called() +} + +// Set provides a mock function with given fields: value +func (_m *Gauge) Set(value uint64) { + _m.Called(value) +} + +// Sub provides a mock function with given fields: amount +func (_m *Gauge) Sub(amount uint64) { + _m.Called(amount) +} + +// Value provides a mock function with given fields: +func (_m *Gauge) Value() uint64 { + ret := _m.Called() + + var r0 uint64 + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/generic_secret_config_provider.go b/ego/test/go/mock/gen/envoy/generic_secret_config_provider.go new file mode 100644 index 0000000..d80535c --- /dev/null +++ b/ego/test/go/mock/gen/envoy/generic_secret_config_provider.go @@ -0,0 +1,27 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + volatile "github.com/grab/ego/ego/src/go/volatile" + mock "github.com/stretchr/testify/mock" +) + +// GenericSecretConfigProvider is an autogenerated mock type for the GenericSecretConfigProvider type +type GenericSecretConfigProvider struct { + mock.Mock +} + +// Secret provides a mock function with given fields: +func (_m *GenericSecretConfigProvider) Secret() volatile.String { + ret := _m.Called() + + var r0 volatile.String + if rf, ok := ret.Get(0).(func() volatile.String); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/go_http_filter.go b/ego/test/go/mock/gen/envoy/go_http_filter.go new file mode 100644 index 0000000..93f006d --- /dev/null +++ b/ego/test/go/mock/gen/envoy/go_http_filter.go @@ -0,0 +1,99 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + envoy "github.com/grab/ego/ego/src/go/envoy" + loglevel "github.com/grab/ego/ego/src/go/envoy/loglevel" + + mock "github.com/stretchr/testify/mock" +) + +// GoHttpFilter is an autogenerated mock type for the GoHttpFilter type +type GoHttpFilter struct { + mock.Mock +} + +// DecoderCallbacks provides a mock function with given fields: +func (_m *GoHttpFilter) DecoderCallbacks() envoy.DecoderFilterCallbacks { + ret := _m.Called() + + var r0 envoy.DecoderFilterCallbacks + if rf, ok := ret.Get(0).(func() envoy.DecoderFilterCallbacks); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.DecoderFilterCallbacks) + } + } + + return r0 +} + +// EncoderCallbacks provides a mock function with given fields: +func (_m *GoHttpFilter) EncoderCallbacks() envoy.EncoderFilterCallbacks { + ret := _m.Called() + + var r0 envoy.EncoderFilterCallbacks + if rf, ok := ret.Get(0).(func() envoy.EncoderFilterCallbacks); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.EncoderFilterCallbacks) + } + } + + return r0 +} + +// GenericSecretProvider provides a mock function with given fields: +func (_m *GoHttpFilter) GenericSecretProvider() envoy.GenericSecretConfigProvider { + ret := _m.Called() + + var r0 envoy.GenericSecretConfigProvider + if rf, ok := ret.Get(0).(func() envoy.GenericSecretConfigProvider); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.GenericSecretConfigProvider) + } + } + + return r0 +} + +// Log provides a mock function with given fields: _a0, _a1 +func (_m *GoHttpFilter) Log(_a0 loglevel.Type, _a1 string) { + _m.Called(_a0, _a1) +} + +// Pin provides a mock function with given fields: +func (_m *GoHttpFilter) Pin() { + _m.Called() +} + +// Post provides a mock function with given fields: _a0 +func (_m *GoHttpFilter) Post(_a0 uint64) { + _m.Called(_a0) +} + +// ResolveMostSpecificPerGoFilterConfig provides a mock function with given fields: name, route +func (_m *GoHttpFilter) ResolveMostSpecificPerGoFilterConfig(name string, route envoy.Route) interface{} { + ret := _m.Called(name, route) + + var r0 interface{} + if rf, ok := ret.Get(0).(func(string, envoy.Route) interface{}); ok { + r0 = rf(name, route) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + return r0 +} + +// Unpin provides a mock function with given fields: +func (_m *GoHttpFilter) Unpin() { + _m.Called() +} diff --git a/ego/test/go/mock/gen/envoy/go_http_filter_config.go b/ego/test/go/mock/gen/envoy/go_http_filter_config.go new file mode 100644 index 0000000..bef0206 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/go_http_filter_config.go @@ -0,0 +1,47 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + envoy "github.com/grab/ego/ego/src/go/envoy" + mock "github.com/stretchr/testify/mock" + + volatile "github.com/grab/ego/ego/src/go/volatile" +) + +// GoHttpFilterConfig is an autogenerated mock type for the GoHttpFilterConfig type +type GoHttpFilterConfig struct { + mock.Mock +} + +// Scope provides a mock function with given fields: +func (_m *GoHttpFilterConfig) Scope() envoy.Scope { + ret := _m.Called() + + var r0 envoy.Scope + if rf, ok := ret.Get(0).(func() envoy.Scope); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.Scope) + } + } + + return r0 +} + +// Settings provides a mock function with given fields: +func (_m *GoHttpFilterConfig) Settings() volatile.Bytes { + ret := _m.Called() + + var r0 volatile.Bytes + if rf, ok := ret.Get(0).(func() volatile.Bytes); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(volatile.Bytes) + } + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/header_map.go b/ego/test/go/mock/gen/envoy/header_map.go new file mode 100644 index 0000000..d4bd787 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/header_map.go @@ -0,0 +1,47 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + volatile "github.com/grab/ego/ego/src/go/volatile" + mock "github.com/stretchr/testify/mock" +) + +// HeaderMap is an autogenerated mock type for the HeaderMap type +type HeaderMap struct { + mock.Mock +} + +// AddCopy provides a mock function with given fields: name, value +func (_m *HeaderMap) AddCopy(name string, value string) { + _m.Called(name, value) +} + +// AppendCopy provides a mock function with given fields: name, value +func (_m *HeaderMap) AppendCopy(name string, value string) { + _m.Called(name, value) +} + +// Get provides a mock function with given fields: name +func (_m *HeaderMap) Get(name string) volatile.String { + ret := _m.Called(name) + + var r0 volatile.String + if rf, ok := ret.Get(0).(func(string) volatile.String); ok { + r0 = rf(name) + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} + +// Remove provides a mock function with given fields: name +func (_m *HeaderMap) Remove(name string) { + _m.Called(name) +} + +// SetCopy provides a mock function with given fields: name, value +func (_m *HeaderMap) SetCopy(name string, value string) { + _m.Called(name, value) +} diff --git a/ego/test/go/mock/gen/envoy/header_map_read_only.go b/ego/test/go/mock/gen/envoy/header_map_read_only.go new file mode 100644 index 0000000..923f391 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/header_map_read_only.go @@ -0,0 +1,27 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + volatile "github.com/grab/ego/ego/src/go/volatile" + mock "github.com/stretchr/testify/mock" +) + +// HeaderMapReadOnly is an autogenerated mock type for the HeaderMapReadOnly type +type HeaderMapReadOnly struct { + mock.Mock +} + +// Get provides a mock function with given fields: name +func (_m *HeaderMapReadOnly) Get(name string) volatile.String { + ret := _m.Called(name) + + var r0 volatile.String + if rf, ok := ret.Get(0).(func(string) volatile.String); ok { + r0 = rf(name) + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/header_map_updatable.go b/ego/test/go/mock/gen/envoy/header_map_updatable.go new file mode 100644 index 0000000..5549d52 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/header_map_updatable.go @@ -0,0 +1,30 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// headerMapUpdatable is an autogenerated mock type for the headerMapUpdatable type +type headerMapUpdatable struct { + mock.Mock +} + +// AddCopy provides a mock function with given fields: name, value +func (_m *headerMapUpdatable) AddCopy(name string, value string) { + _m.Called(name, value) +} + +// AppendCopy provides a mock function with given fields: name, value +func (_m *headerMapUpdatable) AppendCopy(name string, value string) { + _m.Called(name, value) +} + +// Remove provides a mock function with given fields: name +func (_m *headerMapUpdatable) Remove(name string) { + _m.Called(name) +} + +// SetCopy provides a mock function with given fields: name, value +func (_m *headerMapUpdatable) SetCopy(name string, value string) { + _m.Called(name, value) +} diff --git a/ego/test/go/mock/gen/envoy/histogram.go b/ego/test/go/mock/gen/envoy/histogram.go new file mode 100644 index 0000000..cdf0e1e --- /dev/null +++ b/ego/test/go/mock/gen/envoy/histogram.go @@ -0,0 +1,32 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + stats "github.com/grab/ego/ego/src/go/envoy/stats" + mock "github.com/stretchr/testify/mock" +) + +// Histogram is an autogenerated mock type for the Histogram type +type Histogram struct { + mock.Mock +} + +// RecordValue provides a mock function with given fields: value +func (_m *Histogram) RecordValue(value uint64) { + _m.Called(value) +} + +// Unit provides a mock function with given fields: +func (_m *Histogram) Unit() stats.Unit { + ret := _m.Called() + + var r0 stats.Unit + if rf, ok := ret.Get(0).(func() stats.Unit); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(stats.Unit) + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/path_match_criterion.go b/ego/test/go/mock/gen/envoy/path_match_criterion.go new file mode 100644 index 0000000..75baee5 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/path_match_criterion.go @@ -0,0 +1,57 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + envoy "github.com/grab/ego/ego/src/go/envoy" + mock "github.com/stretchr/testify/mock" + + volatile "github.com/grab/ego/ego/src/go/volatile" +) + +// PathMatchCriterion is an autogenerated mock type for the PathMatchCriterion type +type PathMatchCriterion struct { + mock.Mock +} + +// MatchType provides a mock function with given fields: +func (_m *PathMatchCriterion) MatchType() (envoy.PathMatchType, error) { + ret := _m.Called() + + var r0 envoy.PathMatchType + if rf, ok := ret.Get(0).(func() envoy.PathMatchType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(envoy.PathMatchType) + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Matcher provides a mock function with given fields: +func (_m *PathMatchCriterion) Matcher() (volatile.String, error) { + ret := _m.Called() + + var r0 volatile.String + if rf, ok := ret.Get(0).(func() volatile.String); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(volatile.String) + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/ego/test/go/mock/gen/envoy/request_header_map.go b/ego/test/go/mock/gen/envoy/request_header_map.go new file mode 100644 index 0000000..5c9fdbe --- /dev/null +++ b/ego/test/go/mock/gen/envoy/request_header_map.go @@ -0,0 +1,124 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + volatile "github.com/grab/ego/ego/src/go/volatile" + mock "github.com/stretchr/testify/mock" +) + +// RequestHeaderMap is an autogenerated mock type for the RequestHeaderMap type +type RequestHeaderMap struct { + mock.Mock +} + +// AddCopy provides a mock function with given fields: name, value +func (_m *RequestHeaderMap) AddCopy(name string, value string) { + _m.Called(name, value) +} + +// AppendCopy provides a mock function with given fields: name, value +func (_m *RequestHeaderMap) AppendCopy(name string, value string) { + _m.Called(name, value) +} + +// Authorization provides a mock function with given fields: +func (_m *RequestHeaderMap) Authorization() volatile.String { + ret := _m.Called() + + var r0 volatile.String + if rf, ok := ret.Get(0).(func() volatile.String); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} + +// ContentType provides a mock function with given fields: +func (_m *RequestHeaderMap) ContentType() volatile.String { + ret := _m.Called() + + var r0 volatile.String + if rf, ok := ret.Get(0).(func() volatile.String); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} + +// Get provides a mock function with given fields: name +func (_m *RequestHeaderMap) Get(name string) volatile.String { + ret := _m.Called(name) + + var r0 volatile.String + if rf, ok := ret.Get(0).(func(string) volatile.String); ok { + r0 = rf(name) + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} + +// GetByPrefix provides a mock function with given fields: prefix +func (_m *RequestHeaderMap) GetByPrefix(prefix string) map[string][]string { + ret := _m.Called(prefix) + + var r0 map[string][]string + if rf, ok := ret.Get(0).(func(string) map[string][]string); ok { + r0 = rf(prefix) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string][]string) + } + } + + return r0 +} + +// Method provides a mock function with given fields: +func (_m *RequestHeaderMap) Method() volatile.String { + ret := _m.Called() + + var r0 volatile.String + if rf, ok := ret.Get(0).(func() volatile.String); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} + +// Path provides a mock function with given fields: +func (_m *RequestHeaderMap) Path() volatile.String { + ret := _m.Called() + + var r0 volatile.String + if rf, ok := ret.Get(0).(func() volatile.String); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} + +// Remove provides a mock function with given fields: name +func (_m *RequestHeaderMap) Remove(name string) { + _m.Called(name) +} + +// SetCopy provides a mock function with given fields: name, value +func (_m *RequestHeaderMap) SetCopy(name string, value string) { + _m.Called(name, value) +} + +// SetPath provides a mock function with given fields: path +func (_m *RequestHeaderMap) SetPath(path string) { + _m.Called(path) +} diff --git a/ego/test/go/mock/gen/envoy/request_header_map_read_only.go b/ego/test/go/mock/gen/envoy/request_header_map_read_only.go new file mode 100644 index 0000000..c37bcb2 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/request_header_map_read_only.go @@ -0,0 +1,99 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + volatile "github.com/grab/ego/ego/src/go/volatile" + mock "github.com/stretchr/testify/mock" +) + +// RequestHeaderMapReadOnly is an autogenerated mock type for the RequestHeaderMapReadOnly type +type RequestHeaderMapReadOnly struct { + mock.Mock +} + +// Authorization provides a mock function with given fields: +func (_m *RequestHeaderMapReadOnly) Authorization() volatile.String { + ret := _m.Called() + + var r0 volatile.String + if rf, ok := ret.Get(0).(func() volatile.String); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} + +// ContentType provides a mock function with given fields: +func (_m *RequestHeaderMapReadOnly) ContentType() volatile.String { + ret := _m.Called() + + var r0 volatile.String + if rf, ok := ret.Get(0).(func() volatile.String); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} + +// Get provides a mock function with given fields: name +func (_m *RequestHeaderMapReadOnly) Get(name string) volatile.String { + ret := _m.Called(name) + + var r0 volatile.String + if rf, ok := ret.Get(0).(func(string) volatile.String); ok { + r0 = rf(name) + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} + +// GetByPrefix provides a mock function with given fields: prefix +func (_m *RequestHeaderMapReadOnly) GetByPrefix(prefix string) map[string][]string { + ret := _m.Called(prefix) + + var r0 map[string][]string + if rf, ok := ret.Get(0).(func(string) map[string][]string); ok { + r0 = rf(prefix) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string][]string) + } + } + + return r0 +} + +// Method provides a mock function with given fields: +func (_m *RequestHeaderMapReadOnly) Method() volatile.String { + ret := _m.Called() + + var r0 volatile.String + if rf, ok := ret.Get(0).(func() volatile.String); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} + +// Path provides a mock function with given fields: +func (_m *RequestHeaderMapReadOnly) Path() volatile.String { + ret := _m.Called() + + var r0 volatile.String + if rf, ok := ret.Get(0).(func() volatile.String); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/request_header_map_updatable.go b/ego/test/go/mock/gen/envoy/request_header_map_updatable.go new file mode 100644 index 0000000..dea6bd6 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/request_header_map_updatable.go @@ -0,0 +1,35 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// requestHeaderMapUpdatable is an autogenerated mock type for the requestHeaderMapUpdatable type +type requestHeaderMapUpdatable struct { + mock.Mock +} + +// AddCopy provides a mock function with given fields: name, value +func (_m *requestHeaderMapUpdatable) AddCopy(name string, value string) { + _m.Called(name, value) +} + +// AppendCopy provides a mock function with given fields: name, value +func (_m *requestHeaderMapUpdatable) AppendCopy(name string, value string) { + _m.Called(name, value) +} + +// Remove provides a mock function with given fields: name +func (_m *requestHeaderMapUpdatable) Remove(name string) { + _m.Called(name) +} + +// SetCopy provides a mock function with given fields: name, value +func (_m *requestHeaderMapUpdatable) SetCopy(name string, value string) { + _m.Called(name, value) +} + +// SetPath provides a mock function with given fields: path +func (_m *requestHeaderMapUpdatable) SetPath(path string) { + _m.Called(path) +} diff --git a/ego/test/go/mock/gen/envoy/request_or_response_header_map.go b/ego/test/go/mock/gen/envoy/request_or_response_header_map.go new file mode 100644 index 0000000..d58f570 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/request_or_response_header_map.go @@ -0,0 +1,61 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + volatile "github.com/grab/ego/ego/src/go/volatile" + mock "github.com/stretchr/testify/mock" +) + +// RequestOrResponseHeaderMap is an autogenerated mock type for the RequestOrResponseHeaderMap type +type RequestOrResponseHeaderMap struct { + mock.Mock +} + +// AddCopy provides a mock function with given fields: name, value +func (_m *RequestOrResponseHeaderMap) AddCopy(name string, value string) { + _m.Called(name, value) +} + +// AppendCopy provides a mock function with given fields: name, value +func (_m *RequestOrResponseHeaderMap) AppendCopy(name string, value string) { + _m.Called(name, value) +} + +// ContentType provides a mock function with given fields: +func (_m *RequestOrResponseHeaderMap) ContentType() volatile.String { + ret := _m.Called() + + var r0 volatile.String + if rf, ok := ret.Get(0).(func() volatile.String); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} + +// Get provides a mock function with given fields: name +func (_m *RequestOrResponseHeaderMap) Get(name string) volatile.String { + ret := _m.Called(name) + + var r0 volatile.String + if rf, ok := ret.Get(0).(func(string) volatile.String); ok { + r0 = rf(name) + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} + +// Remove provides a mock function with given fields: name +func (_m *RequestOrResponseHeaderMap) Remove(name string) { + _m.Called(name) +} + +// SetCopy provides a mock function with given fields: name, value +func (_m *RequestOrResponseHeaderMap) SetCopy(name string, value string) { + _m.Called(name, value) +} diff --git a/ego/test/go/mock/gen/envoy/request_or_response_header_map_read_only.go b/ego/test/go/mock/gen/envoy/request_or_response_header_map_read_only.go new file mode 100644 index 0000000..4e67b2a --- /dev/null +++ b/ego/test/go/mock/gen/envoy/request_or_response_header_map_read_only.go @@ -0,0 +1,41 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + volatile "github.com/grab/ego/ego/src/go/volatile" + mock "github.com/stretchr/testify/mock" +) + +// RequestOrResponseHeaderMapReadOnly is an autogenerated mock type for the RequestOrResponseHeaderMapReadOnly type +type RequestOrResponseHeaderMapReadOnly struct { + mock.Mock +} + +// ContentType provides a mock function with given fields: +func (_m *RequestOrResponseHeaderMapReadOnly) ContentType() volatile.String { + ret := _m.Called() + + var r0 volatile.String + if rf, ok := ret.Get(0).(func() volatile.String); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} + +// Get provides a mock function with given fields: name +func (_m *RequestOrResponseHeaderMapReadOnly) Get(name string) volatile.String { + ret := _m.Called(name) + + var r0 volatile.String + if rf, ok := ret.Get(0).(func(string) volatile.String); ok { + r0 = rf(name) + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/request_or_response_header_map_updatable.go b/ego/test/go/mock/gen/envoy/request_or_response_header_map_updatable.go new file mode 100644 index 0000000..99f0da5 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/request_or_response_header_map_updatable.go @@ -0,0 +1,30 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// requestOrResponseHeaderMapUpdatable is an autogenerated mock type for the requestOrResponseHeaderMapUpdatable type +type requestOrResponseHeaderMapUpdatable struct { + mock.Mock +} + +// AddCopy provides a mock function with given fields: name, value +func (_m *requestOrResponseHeaderMapUpdatable) AddCopy(name string, value string) { + _m.Called(name, value) +} + +// AppendCopy provides a mock function with given fields: name, value +func (_m *requestOrResponseHeaderMapUpdatable) AppendCopy(name string, value string) { + _m.Called(name, value) +} + +// Remove provides a mock function with given fields: name +func (_m *requestOrResponseHeaderMapUpdatable) Remove(name string) { + _m.Called(name) +} + +// SetCopy provides a mock function with given fields: name, value +func (_m *requestOrResponseHeaderMapUpdatable) SetCopy(name string, value string) { + _m.Called(name, value) +} diff --git a/ego/test/go/mock/gen/envoy/request_trailer_map.go b/ego/test/go/mock/gen/envoy/request_trailer_map.go new file mode 100644 index 0000000..e63ab22 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/request_trailer_map.go @@ -0,0 +1,47 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + volatile "github.com/grab/ego/ego/src/go/volatile" + mock "github.com/stretchr/testify/mock" +) + +// RequestTrailerMap is an autogenerated mock type for the RequestTrailerMap type +type RequestTrailerMap struct { + mock.Mock +} + +// AddCopy provides a mock function with given fields: name, value +func (_m *RequestTrailerMap) AddCopy(name string, value string) { + _m.Called(name, value) +} + +// AppendCopy provides a mock function with given fields: name, value +func (_m *RequestTrailerMap) AppendCopy(name string, value string) { + _m.Called(name, value) +} + +// Get provides a mock function with given fields: name +func (_m *RequestTrailerMap) Get(name string) volatile.String { + ret := _m.Called(name) + + var r0 volatile.String + if rf, ok := ret.Get(0).(func(string) volatile.String); ok { + r0 = rf(name) + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} + +// Remove provides a mock function with given fields: name +func (_m *RequestTrailerMap) Remove(name string) { + _m.Called(name) +} + +// SetCopy provides a mock function with given fields: name, value +func (_m *RequestTrailerMap) SetCopy(name string, value string) { + _m.Called(name, value) +} diff --git a/ego/test/go/mock/gen/envoy/request_trailer_map_read_only.go b/ego/test/go/mock/gen/envoy/request_trailer_map_read_only.go new file mode 100644 index 0000000..bbba658 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/request_trailer_map_read_only.go @@ -0,0 +1,27 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + volatile "github.com/grab/ego/ego/src/go/volatile" + mock "github.com/stretchr/testify/mock" +) + +// RequestTrailerMapReadOnly is an autogenerated mock type for the RequestTrailerMapReadOnly type +type RequestTrailerMapReadOnly struct { + mock.Mock +} + +// Get provides a mock function with given fields: name +func (_m *RequestTrailerMapReadOnly) Get(name string) volatile.String { + ret := _m.Called(name) + + var r0 volatile.String + if rf, ok := ret.Get(0).(func(string) volatile.String); ok { + r0 = rf(name) + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/request_trailer_map_updatable.go b/ego/test/go/mock/gen/envoy/request_trailer_map_updatable.go new file mode 100644 index 0000000..7253178 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/request_trailer_map_updatable.go @@ -0,0 +1,30 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// requestTrailerMapUpdatable is an autogenerated mock type for the requestTrailerMapUpdatable type +type requestTrailerMapUpdatable struct { + mock.Mock +} + +// AddCopy provides a mock function with given fields: name, value +func (_m *requestTrailerMapUpdatable) AddCopy(name string, value string) { + _m.Called(name, value) +} + +// AppendCopy provides a mock function with given fields: name, value +func (_m *requestTrailerMapUpdatable) AppendCopy(name string, value string) { + _m.Called(name, value) +} + +// Remove provides a mock function with given fields: name +func (_m *requestTrailerMapUpdatable) Remove(name string) { + _m.Called(name) +} + +// SetCopy provides a mock function with given fields: name, value +func (_m *requestTrailerMapUpdatable) SetCopy(name string, value string) { + _m.Called(name, value) +} diff --git a/ego/test/go/mock/gen/envoy/response_header_map.go b/ego/test/go/mock/gen/envoy/response_header_map.go new file mode 100644 index 0000000..bbb39e8 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/response_header_map.go @@ -0,0 +1,80 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + volatile "github.com/grab/ego/ego/src/go/volatile" + mock "github.com/stretchr/testify/mock" +) + +// ResponseHeaderMap is an autogenerated mock type for the ResponseHeaderMap type +type ResponseHeaderMap struct { + mock.Mock +} + +// AddCopy provides a mock function with given fields: name, value +func (_m *ResponseHeaderMap) AddCopy(name string, value string) { + _m.Called(name, value) +} + +// AppendCopy provides a mock function with given fields: name, value +func (_m *ResponseHeaderMap) AppendCopy(name string, value string) { + _m.Called(name, value) +} + +// ContentType provides a mock function with given fields: +func (_m *ResponseHeaderMap) ContentType() volatile.String { + ret := _m.Called() + + var r0 volatile.String + if rf, ok := ret.Get(0).(func() volatile.String); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} + +// Get provides a mock function with given fields: name +func (_m *ResponseHeaderMap) Get(name string) volatile.String { + ret := _m.Called(name) + + var r0 volatile.String + if rf, ok := ret.Get(0).(func(string) volatile.String); ok { + r0 = rf(name) + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} + +// Remove provides a mock function with given fields: name +func (_m *ResponseHeaderMap) Remove(name string) { + _m.Called(name) +} + +// SetCopy provides a mock function with given fields: name, value +func (_m *ResponseHeaderMap) SetCopy(name string, value string) { + _m.Called(name, value) +} + +// SetStatus provides a mock function with given fields: status +func (_m *ResponseHeaderMap) SetStatus(status int) { + _m.Called(status) +} + +// Status provides a mock function with given fields: +func (_m *ResponseHeaderMap) Status() volatile.String { + ret := _m.Called() + + var r0 volatile.String + if rf, ok := ret.Get(0).(func() volatile.String); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/response_header_map_read_only.go b/ego/test/go/mock/gen/envoy/response_header_map_read_only.go new file mode 100644 index 0000000..521eb5f --- /dev/null +++ b/ego/test/go/mock/gen/envoy/response_header_map_read_only.go @@ -0,0 +1,55 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + volatile "github.com/grab/ego/ego/src/go/volatile" + mock "github.com/stretchr/testify/mock" +) + +// ResponseHeaderMapReadOnly is an autogenerated mock type for the ResponseHeaderMapReadOnly type +type ResponseHeaderMapReadOnly struct { + mock.Mock +} + +// ContentType provides a mock function with given fields: +func (_m *ResponseHeaderMapReadOnly) ContentType() volatile.String { + ret := _m.Called() + + var r0 volatile.String + if rf, ok := ret.Get(0).(func() volatile.String); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} + +// Get provides a mock function with given fields: name +func (_m *ResponseHeaderMapReadOnly) Get(name string) volatile.String { + ret := _m.Called(name) + + var r0 volatile.String + if rf, ok := ret.Get(0).(func(string) volatile.String); ok { + r0 = rf(name) + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} + +// Status provides a mock function with given fields: +func (_m *ResponseHeaderMapReadOnly) Status() volatile.String { + ret := _m.Called() + + var r0 volatile.String + if rf, ok := ret.Get(0).(func() volatile.String); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/response_header_map_updatable.go b/ego/test/go/mock/gen/envoy/response_header_map_updatable.go new file mode 100644 index 0000000..2ffc1e4 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/response_header_map_updatable.go @@ -0,0 +1,35 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// responseHeaderMapUpdatable is an autogenerated mock type for the responseHeaderMapUpdatable type +type responseHeaderMapUpdatable struct { + mock.Mock +} + +// AddCopy provides a mock function with given fields: name, value +func (_m *responseHeaderMapUpdatable) AddCopy(name string, value string) { + _m.Called(name, value) +} + +// AppendCopy provides a mock function with given fields: name, value +func (_m *responseHeaderMapUpdatable) AppendCopy(name string, value string) { + _m.Called(name, value) +} + +// Remove provides a mock function with given fields: name +func (_m *responseHeaderMapUpdatable) Remove(name string) { + _m.Called(name) +} + +// SetCopy provides a mock function with given fields: name, value +func (_m *responseHeaderMapUpdatable) SetCopy(name string, value string) { + _m.Called(name, value) +} + +// SetStatus provides a mock function with given fields: status +func (_m *responseHeaderMapUpdatable) SetStatus(status int) { + _m.Called(status) +} diff --git a/ego/test/go/mock/gen/envoy/route.go b/ego/test/go/mock/gen/envoy/route.go new file mode 100644 index 0000000..fac508a --- /dev/null +++ b/ego/test/go/mock/gen/envoy/route.go @@ -0,0 +1,29 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + envoy "github.com/grab/ego/ego/src/go/envoy" + mock "github.com/stretchr/testify/mock" +) + +// Route is an autogenerated mock type for the Route type +type Route struct { + mock.Mock +} + +// RouteEntry provides a mock function with given fields: +func (_m *Route) RouteEntry() envoy.RouteEntry { + ret := _m.Called() + + var r0 envoy.RouteEntry + if rf, ok := ret.Get(0).(func() envoy.RouteEntry); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.RouteEntry) + } + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/route_entry.go b/ego/test/go/mock/gen/envoy/route_entry.go new file mode 100644 index 0000000..26a941b --- /dev/null +++ b/ego/test/go/mock/gen/envoy/route_entry.go @@ -0,0 +1,29 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + envoy "github.com/grab/ego/ego/src/go/envoy" + mock "github.com/stretchr/testify/mock" +) + +// RouteEntry is an autogenerated mock type for the RouteEntry type +type RouteEntry struct { + mock.Mock +} + +// PathMatchCriterion provides a mock function with given fields: +func (_m *RouteEntry) PathMatchCriterion() envoy.PathMatchCriterion { + ret := _m.Called() + + var r0 envoy.PathMatchCriterion + if rf, ok := ret.Get(0).(func() envoy.PathMatchCriterion); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.PathMatchCriterion) + } + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/scope.go b/ego/test/go/mock/gen/envoy/scope.go new file mode 100644 index 0000000..9d57692 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/scope.go @@ -0,0 +1,63 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + envoy "github.com/grab/ego/ego/src/go/envoy" + mock "github.com/stretchr/testify/mock" + + stats "github.com/grab/ego/ego/src/go/envoy/stats" +) + +// Scope is an autogenerated mock type for the Scope type +type Scope struct { + mock.Mock +} + +// CounterFromStatName provides a mock function with given fields: name +func (_m *Scope) CounterFromStatName(name string) envoy.Counter { + ret := _m.Called(name) + + var r0 envoy.Counter + if rf, ok := ret.Get(0).(func(string) envoy.Counter); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.Counter) + } + } + + return r0 +} + +// GaugeFromStatName provides a mock function with given fields: name, importMode +func (_m *Scope) GaugeFromStatName(name string, importMode stats.ImportMode) envoy.Gauge { + ret := _m.Called(name, importMode) + + var r0 envoy.Gauge + if rf, ok := ret.Get(0).(func(string, stats.ImportMode) envoy.Gauge); ok { + r0 = rf(name, importMode) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.Gauge) + } + } + + return r0 +} + +// HistogramFromStatName provides a mock function with given fields: name, unit +func (_m *Scope) HistogramFromStatName(name string, unit stats.Unit) envoy.Histogram { + ret := _m.Called(name, unit) + + var r0 envoy.Histogram + if rf, ok := ret.Get(0).(func(string, stats.Unit) envoy.Histogram); ok { + r0 = rf(name, unit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.Histogram) + } + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/span.go b/ego/test/go/mock/gen/envoy/span.go new file mode 100644 index 0000000..b8b8cb5 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/span.go @@ -0,0 +1,50 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + envoy "github.com/grab/ego/ego/src/go/envoy" + mock "github.com/stretchr/testify/mock" +) + +// Span is an autogenerated mock type for the Span type +type Span struct { + mock.Mock +} + +// FinishSpan provides a mock function with given fields: +func (_m *Span) FinishSpan() { + _m.Called() +} + +// GetContext provides a mock function with given fields: +func (_m *Span) GetContext() map[string][]string { + ret := _m.Called() + + var r0 map[string][]string + if rf, ok := ret.Get(0).(func() map[string][]string); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string][]string) + } + } + + return r0 +} + +// SpawnChild provides a mock function with given fields: name +func (_m *Span) SpawnChild(name string) envoy.Span { + ret := _m.Called(name) + + var r0 envoy.Span + if rf, ok := ret.Get(0).(func(string) envoy.Span); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.Span) + } + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/stream_filter_callbacks.go b/ego/test/go/mock/gen/envoy/stream_filter_callbacks.go new file mode 100644 index 0000000..f360172 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/stream_filter_callbacks.go @@ -0,0 +1,75 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + envoy "github.com/grab/ego/ego/src/go/envoy" + mock "github.com/stretchr/testify/mock" +) + +// StreamFilterCallbacks is an autogenerated mock type for the StreamFilterCallbacks type +type StreamFilterCallbacks struct { + mock.Mock +} + +// ActiveSpan provides a mock function with given fields: +func (_m *StreamFilterCallbacks) ActiveSpan() envoy.Span { + ret := _m.Called() + + var r0 envoy.Span + if rf, ok := ret.Get(0).(func() envoy.Span); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.Span) + } + } + + return r0 +} + +// Route provides a mock function with given fields: +func (_m *StreamFilterCallbacks) Route() envoy.Route { + ret := _m.Called() + + var r0 envoy.Route + if rf, ok := ret.Get(0).(func() envoy.Route); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.Route) + } + } + + return r0 +} + +// RouteExisting provides a mock function with given fields: +func (_m *StreamFilterCallbacks) RouteExisting() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// StreamInfo provides a mock function with given fields: +func (_m *StreamFilterCallbacks) StreamInfo() envoy.StreamInfo { + ret := _m.Called() + + var r0 envoy.StreamInfo + if rf, ok := ret.Get(0).(func() envoy.StreamInfo); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.StreamInfo) + } + } + + return r0 +} diff --git a/ego/test/go/mock/gen/envoy/stream_info.go b/ego/test/go/mock/gen/envoy/stream_info.go new file mode 100644 index 0000000..8bf7058 --- /dev/null +++ b/ego/test/go/mock/gen/envoy/stream_info.go @@ -0,0 +1,89 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + envoy "github.com/grab/ego/ego/src/go/envoy" + mock "github.com/stretchr/testify/mock" + + volatile "github.com/grab/ego/ego/src/go/volatile" +) + +// StreamInfo is an autogenerated mock type for the StreamInfo type +type StreamInfo struct { + mock.Mock +} + +// FilterState provides a mock function with given fields: +func (_m *StreamInfo) FilterState() envoy.FilterState { + ret := _m.Called() + + var r0 envoy.FilterState + if rf, ok := ret.Get(0).(func() envoy.FilterState); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.FilterState) + } + } + + return r0 +} + +// GetRequestHeaders provides a mock function with given fields: +func (_m *StreamInfo) GetRequestHeaders() envoy.RequestHeaderMapReadOnly { + ret := _m.Called() + + var r0 envoy.RequestHeaderMapReadOnly + if rf, ok := ret.Get(0).(func() envoy.RequestHeaderMapReadOnly); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.RequestHeaderMapReadOnly) + } + } + + return r0 +} + +// LastDownstreamTxByteSent provides a mock function with given fields: +func (_m *StreamInfo) LastDownstreamTxByteSent() int64 { + ret := _m.Called() + + var r0 int64 + if rf, ok := ret.Get(0).(func() int64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int64) + } + + return r0 +} + +// ResponseCode provides a mock function with given fields: +func (_m *StreamInfo) ResponseCode() int { + ret := _m.Called() + + var r0 int + if rf, ok := ret.Get(0).(func() int); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// ResponseCodeDetails provides a mock function with given fields: +func (_m *StreamInfo) ResponseCodeDetails() volatile.String { + ret := _m.Called() + + var r0 volatile.String + if rf, ok := ret.Get(0).(func() volatile.String); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(volatile.String) + } + + return r0 +} diff --git a/ego/test/go/mock/logger.go b/ego/test/go/mock/logger.go new file mode 100644 index 0000000..43084f9 --- /dev/null +++ b/ego/test/go/mock/logger.go @@ -0,0 +1,19 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package mock + +import ( + "fmt" + + "github.com/grab/ego/ego/src/go/envoy/loglevel" +) + +type NativeLogger struct{} + +// Log ... +func (l NativeLogger) Log(level loglevel.Type, tag, message string) { + fmt.Printf("[%v]: %v\n", tag, message) +} diff --git a/egofilters/BUILD.bazel b/egofilters/BUILD.bazel new file mode 100644 index 0000000..2ede1b9 --- /dev/null +++ b/egofilters/BUILD.bazel @@ -0,0 +1,34 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@envoy//bazel:envoy_build_system.bzl", "envoy_cc_library") + +package(default_visibility = ["//visibility:public"]) + +go_library( + name = "go_default_library", + srcs = ["filters.go"], + importpath = "github.com/grab/ego/egofilters", + visibility = ["//visibility:public"], + deps = [ + "//ego/src/go:go_default_library", + "//egofilters/http/getheader:go_default_library", + "//egofilters/http/security:go_default_library", + ], +) + +# This is our "Registry" equivalent for the proto schemas. By linking +# this with the envoy binary, envoy will be able to parse filter-specific +# configs. +# +envoy_cc_library( + name = "ego_filter_protos", + repository = "@envoy", + deps = [ + "//egofilters/http/getheader/proto:pkg_cc_proto", + "//egofilters/http/security/proto:pkg_cc_proto", + ], +) diff --git a/egofilters/filters.go b/egofilters/filters.go new file mode 100644 index 0000000..1fd2277 --- /dev/null +++ b/egofilters/filters.go @@ -0,0 +1,18 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package egofilters + +import ( + ego "github.com/grab/ego/ego/src/go" + + "github.com/grab/ego/egofilters/http/getheader" + "github.com/grab/ego/egofilters/http/security" +) + +func init() { + ego.RegisterHttpFilter("getheader", getheader.CreatFactoryFactory()) + ego.RegisterHttpFilter("security", security.CreateFactoryFactory()) +} diff --git a/egofilters/http/getheader/BUILD.bazel b/egofilters/http/getheader/BUILD.bazel new file mode 100644 index 0000000..afd5e8c --- /dev/null +++ b/egofilters/http/getheader/BUILD.bazel @@ -0,0 +1,24 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "factory.go", + "filter.go", + ], + importpath = "github.com/grab/ego/egofilters/http/getheader", + visibility = ["//visibility:public"], + deps = [ + "//ego/src/go:go_default_library", + "//ego/src/go/envoy:go_default_library", + "//ego/src/go/envoy/headersstatus:go_default_library", + "//ego/src/go/envoy/loglevel:go_default_library", + "//egofilters/http/getheader/proto:go_default_library", + "@com_github_golang_protobuf//proto:go_default_library", + ], +) diff --git a/egofilters/http/getheader/factory.go b/egofilters/http/getheader/factory.go new file mode 100644 index 0000000..a7e57cb --- /dev/null +++ b/egofilters/http/getheader/factory.go @@ -0,0 +1,42 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package getheader + +import ( + "github.com/golang/protobuf/proto" + + pb "github.com/grab/ego/egofilters/http/getheader/proto" + ego "github.com/grab/ego/ego/src/go" + "github.com/grab/ego/ego/src/go/envoy" +) + +type factory struct { +} + +func (f factory) CreateFilterFactory(native envoy.GoHttpFilterConfig) (ego.HttpFilterFactory, error) { + settings := pb.Settings{} + bytes := native.Settings() // Volatile! Handle with care! + if err := proto.Unmarshal([]byte(bytes), &settings); err != nil { + return nil, err + } + if err := settings.Validate(); err != nil { + return nil, err + } + + return func(native envoy.GoHttpFilter) ego.HttpFilter { + return newGetHeaderFilter(&settings, native) + }, nil +} + +// CreateRouteSpecificFilterConfig ... +func (f factory) CreateRouteSpecificFilterConfig(native envoy.GoHttpFilterConfig) (interface{}, error) { + return struct{}{}, nil +} + +// CreatFactoryFactory ... +func CreatFactoryFactory() ego.HttpFilterFactoryFactory { + return factory{} +} diff --git a/egofilters/http/getheader/filter.go b/egofilters/http/getheader/filter.go new file mode 100644 index 0000000..1b5a238 --- /dev/null +++ b/egofilters/http/getheader/filter.go @@ -0,0 +1,81 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package getheader + +import ( + "fmt" + "net/http" + "time" + + pb "github.com/grab/ego/egofilters/http/getheader/proto" + ego "github.com/grab/ego/ego/src/go" + "github.com/grab/ego/ego/src/go/envoy" + "github.com/grab/ego/ego/src/go/envoy/headersstatus" + "github.com/grab/ego/ego/src/go/envoy/loglevel" +) + +type getHeaderFilter struct { + ego.HttpFilterBase + settings *pb.Settings + // Just a simple keep requestHeaders & response of 3rdParty httpCall for OnPost callback + requestHeaders envoy.RequestHeaderMap + httpResponse *http.Response + httpErr error +} + +func newGetHeaderFilter(settings *pb.Settings, native envoy.GoHttpFilter) ego.HttpFilter { + f := &getHeaderFilter{settings: settings} + f.HttpFilterBase.Init(native) + return f +} + +func (f *getHeaderFilter) DecodeHeaders(headers envoy.RequestHeaderMap, endStream bool) headersstatus.Type { + // keep headers for use later in onPost + f.requestHeaders = headers + + f.Pin() // must do this for every go-routine + go func() { + defer f.Unpin() // must do this for every go-routine. Includes f.Recover() + + f.Context.Done() + request, err := http.NewRequestWithContext(f.Context, "GET", f.settings.Src, nil) + if err != nil { + // Send local repsonse + f.httpErr = err + f.Native.Post(0) + return + } + + client := http.Client{ + Timeout: 2 * time.Second, + } + f.httpResponse, f.httpErr = client.Do(request) + + // In this demo we only need one http-call at a time + // so tag = 0 because we don't need to manage multiple callback + f.Native.Post(0) + }() + // If we turn `headersstatus.Continue` on `DecodeHeaders` from Go side + // Although we call a http request with a goroutine and defer `defer f.Release()` so there is a chance + // for onDestroy happened before goroutine DONE and call a post to dispatcher. () + return headersstatus.StopAllIterationAndWatermark +} + +func (f *getHeaderFilter) OnPost(tag uint64) { + if f.httpErr != nil { + // Send local reply and not forward request to upstream + errMsg := fmt.Sprintf("can not connect to srcs header", f.httpErr) + f.Native.Log(loglevel.Error, errMsg) + f.Native.DecoderCallbacks().SendLocalReply(http.StatusFailedDependency, errMsg, nil, "") + + // Don't need to ContinueDecoding here, because we don't want to continue with forward to upstream + // If you try to do that OnDestoy may happen before and SendLocalReply will not have decodeCallback + } else { + respHeader := f.httpResponse.Header.Get(f.settings.Hdr) + f.requestHeaders.AddCopy(f.settings.Key, respHeader) + f.Native.DecoderCallbacks().ContinueDecoding() + } +} diff --git a/egofilters/http/getheader/proto/BUILD.bazel b/egofilters/http/getheader/proto/BUILD.bazel new file mode 100644 index 0000000..28beada --- /dev/null +++ b/egofilters/http/getheader/proto/BUILD.bazel @@ -0,0 +1,40 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +# gazelle:ignore + +load("@io_bazel_rules_go//proto:compiler.bzl", "go_proto_compiler") +load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +# This is generate proto for C-Side when we embeded in envoy config +api_proto_package() + +# This is generate validation file +go_proto_compiler( + name = "pgv_plugin_go", + options = ["lang=go"], + plugin = "@com_envoyproxy_protoc_gen_validate//:protoc-gen-validate", + suffix = ".pb.validate.go", + valid_archive = False, +) + +# This is generate proto for G-Side usage +go_proto_library( + name = "go_default_library", + compilers = [ + "@io_bazel_rules_go//proto:go_proto", + "pgv_plugin_go", + ], + importpath = "github.com/grab/ego/egofilters/http/getheader/proto", + proto = ":pkg", # api_proto_package() generates this + visibility = ["//visibility:public"], + deps = [ + "@com_envoyproxy_protoc_gen_validate//validate:go_default_library", + "@com_github_golang_protobuf//ptypes:go_default_library", + "@com_github_golang_protobuf//ptypes/any:go_default_library", + "@com_google_googleapis//google/api:annotations_go_proto", + ], +) diff --git a/egofilters/http/getheader/proto/getheader.proto b/egofilters/http/getheader/proto/getheader.proto new file mode 100644 index 0000000..539d788 --- /dev/null +++ b/egofilters/http/getheader/proto/getheader.proto @@ -0,0 +1,16 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +syntax = "proto3"; + +package egodemo.getheader; + +import "validate/validate.proto"; + +message Settings { + string key = 1 [ (validate.rules).string = {min_bytes : 5} ]; + string src = 2 [ (validate.rules).string = {min_bytes : 1} ]; + string hdr = 3 [ (validate.rules).string = {min_bytes : 1} ]; +} \ No newline at end of file diff --git a/egofilters/http/security/BUILD.bazel b/egofilters/http/security/BUILD.bazel new file mode 100644 index 0000000..7e351cd --- /dev/null +++ b/egofilters/http/security/BUILD.bazel @@ -0,0 +1,58 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "config.go", + "factory.go", + "filter.go", + ], + importpath = "github.com/grab/ego/egofilters/http/security", + visibility = ["//visibility:public"], + deps = [ + "//ego/src/go:go_default_library", + "//ego/src/go/envoy:go_default_library", + "//ego/src/go/envoy/datastatus:go_default_library", + "//ego/src/go/envoy/headersstatus:go_default_library", + "//ego/src/go/envoy/lifespan:go_default_library", + "//ego/src/go/envoy/statetype:go_default_library", + "//ego/src/go/envoy/trailersstatus:go_default_library", + "//ego/src/go/logger:go_default_library", + "//egofilters/http/security/context:go_default_library", + "//egofilters/http/security/proto:go_default_library", + "//egofilters/http/security/verifier:go_default_library", + "@com_github_golang_protobuf//proto:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "config_test.go", + "factory_test.go", + "filter_sign_test.go", + "filter_verify_test.go", + ], + embed = [":go_default_library"], + deps = [ + "//ego/src/go/envoy/datastatus:go_default_library", + "//ego/src/go/envoy/headersstatus:go_default_library", + "//ego/src/go/envoy/trailersstatus:go_default_library", + "//ego/src/go/volatile:go_default_library", + "//ego/test/go/mock:go_default_library", + "//ego/test/go/mock/gen/envoy:go_default_library", + "//egofilters/http/security/context:go_default_library", + "//egofilters/http/security/proto:go_default_library", + "//egofilters/http/security/verifier:go_default_library", + "//egofilters/mock/gen/http/security/verifier:go_default_library", + "@com_github_golang_protobuf//proto:go_default_library", + "@com_github_stretchr_testify//assert:go_default_library", + "@com_github_stretchr_testify//mock:go_default_library", + "@com_github_stretchr_testify//require:go_default_library", + ], +) diff --git a/egofilters/http/security/config.go b/egofilters/http/security/config.go new file mode 100644 index 0000000..967721d --- /dev/null +++ b/egofilters/http/security/config.go @@ -0,0 +1,96 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package security + +import ( + "errors" + + "github.com/golang/protobuf/proto" + + "github.com/grab/ego/ego/src/go/envoy" + + pb "github.com/grab/ego/egofilters/http/security/proto" + "github.com/grab/ego/egofilters/http/security/verifier" +) + +type securityStats struct { + // TODO: add more metrics if needed + authOK envoy.Counter + authDenied envoy.Counter + authError envoy.Counter +} + +type securityConfig struct { + verifiers map[string]verifier.Verifier + signers map[string]verifier.Signer + stats securityStats +} + +var ( + // ErrUnsupportedProvider ... + ErrUnsupportedProvider = errors.New("upsupported provider type") + // ErrCannotCreateStats ... + ErrCannotCreateStats = errors.New("can not create stats") +) + +// Initialize together with filter at initial stage +func createSecurityConfig(native envoy.GoHttpFilterConfig) (*securityConfig, error) { + settings := &pb.Settings{} + bytes := native.Settings() // Volatile! Handle with care! + if err := proto.Unmarshal([]byte(bytes), settings); err != nil { + return nil, err + } + if err := settings.Validate(); err != nil { + return nil, err + } + + verifiers := map[string]verifier.Verifier{} + signers := map[string]verifier.Signer{} + providers := settings.GetProviders() + // TODO: use statsScope to create stats detail for every provider + scope := native.Scope() + authOK := scope.CounterFromStatName("auth_ok") + authDenied := scope.CounterFromStatName("auth_denied") + authError := scope.CounterFromStatName("auth_error") + if authOK == nil || authDenied == nil || authError == nil { + return nil, ErrCannotCreateStats + } + secStats := securityStats{ + authOK: authOK, + authDenied: authDenied, + authError: authError, + } + + for k, v := range providers { + switch v.GetProviderType().(type) { + case *pb.Provider_CustomHmacProvider: + hmacProvider, _ := verifier.CreateCustomHMACProvider(v.GetCustomHmacProvider()) + verifiers[k] = hmacProvider + if hmacProvider != nil { + signers[k] = hmacProvider + } + default: + return nil, ErrUnsupportedProvider + } + + } + return &securityConfig{ + verifiers: verifiers, + signers: signers, + stats: secStats, + }, nil +} + +func (c *securityConfig) findProvider(requirement *pb.Requirement) (verifier.Verifier, verifier.Signer) { + // TODO: only take care of single provider for now. This needs to be + // extended to handle requires_all & require_any to combine multiple + // auth-types. + name := requirement.GetProviderName() + if name == "" { + return nil, nil + } + return c.verifiers[name], c.signers[name] +} diff --git a/egofilters/http/security/config_test.go b/egofilters/http/security/config_test.go new file mode 100644 index 0000000..bd806de --- /dev/null +++ b/egofilters/http/security/config_test.go @@ -0,0 +1,189 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package security + +import ( + "fmt" + "reflect" + "testing" + + "github.com/golang/protobuf/proto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grab/ego/ego/test/go/mock" + + pb "github.com/grab/ego/egofilters/http/security/proto" + + envoymocks "github.com/grab/ego/ego/test/go/mock/gen/envoy" +) + +func TestCreateSecurityConfig(t *testing.T) { + tcs := []struct { + name string + // set-up + pbConfig string + failedToCreateAuthOkCounter bool + failedToCreateAuthDeniedCounter bool + failedToCreateAuthErrorCounter bool + + // verify + hasError bool + verifiers map[string]string + signers map[string]string + }{ + { + name: "HMAC verifier", + pbConfig: ` + providers: < + key: "my_custom_hmac_provider" + value: < + custom_hmac_provider: < + request_validation_url: "https://custom-auth.example.com/v1/hmacverify" + service_key: "service_key" + service_token: "service_token" + > + > + > + `, + + verifiers: map[string]string{"my_custom_hmac_provider": "*verifier.customHMACProvider"}, + signers: map[string]string{"my_custom_hmac_provider": "*verifier.customHMACProvider"}, + }, + + { + name: "Can't create auth ok counter", + pbConfig: ` + providers: < + key: "my_custom_hmac_provider" + value: < + custom_hmac_provider: < + request_validation_url: "https://custom-auth.example.com/v1/hmacverify" + service_key: "service_key" + service_token: "service_token" + > + > + > + `, + failedToCreateAuthOkCounter: true, + + hasError: true, + }, + + { + name: "Can't create auth denied counter", + pbConfig: ` + providers: < + key: "my_custom_hmac_provider" + value: < + custom_hmac_provider: < + request_validation_url: "https://custom-auth.example.com/v1/hmacverify" + service_key: "service_key" + service_token: "service_token" + > + > + > + `, + failedToCreateAuthDeniedCounter: true, + + hasError: true, + }, + + { + name: "Can't create auth error counter", + pbConfig: ` + providers: < + key: "my_custom_hmac_provider" + value: < + custom_hmac_provider: < + request_validation_url: "https://custom-auth.example.com/v1/hmacverify" + service_key: "service_key" + service_token: "service_token" + > + > + > + `, + failedToCreateAuthErrorCounter: true, + + hasError: true, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + settings := &pb.Settings{} + err := proto.UnmarshalText(tc.pbConfig, settings) + require.Nil(t, err) + + configBytes, err := proto.Marshal(settings) + require.Nil(t, err) + + scope := &envoymocks.Scope{} + + authOkCounter := &envoymocks.Counter{} + authOkCounter.TestData().Set("name", "auth_ok") + + authDeniedCounter := &envoymocks.Counter{} + authOkCounter.TestData().Set("name", "auth_denied") + + authErrorCounter := &envoymocks.Counter{} + authErrorCounter.TestData().Set("name", "auth_error") + + stats := securityStats{ + authOK: authOkCounter, + authDenied: authDeniedCounter, + authError: authErrorCounter, + } + + if tc.failedToCreateAuthOkCounter { + scope.On("CounterFromStatName", "auth_ok").Return(nil) + } else { + scope.On("CounterFromStatName", "auth_ok").Return(authOkCounter) + } + if tc.failedToCreateAuthDeniedCounter { + scope.On("CounterFromStatName", "auth_denied").Return(nil) + } else { + scope.On("CounterFromStatName", "auth_denied").Return(authDeniedCounter) + } + + if tc.failedToCreateAuthErrorCounter { + scope.On("CounterFromStatName", "auth_error").Return(nil) + } else { + scope.On("CounterFromStatName", "auth_error").Return(authErrorCounter) + } + + gohttpConfig := &mock.GoHttpFilterConfig{ConfigBytes: configBytes, EnvoyScope: scope} + + securityConfig, err := createSecurityConfig(gohttpConfig) + + if tc.hasError { + assert.NotNil(t, err) + } else { + require.Nil(t, err) + + scope.AssertExpectations(t) + assert.Equal(t, stats, securityConfig.stats) + + for name, verifierType := range tc.verifiers { + assert.Equal(t, verifierType, fmt.Sprintf("%v", reflect.TypeOf(securityConfig.verifiers[name]))) + } + + for name, verifierType := range tc.signers { + assert.Equal(t, verifierType, fmt.Sprintf("%v", reflect.TypeOf(securityConfig.signers[name]))) + } + } + + }) + } +} + +func TestUnmashalError(t *testing.T) { + scope := &envoymocks.Scope{} + gohttpConfig := &mock.GoHttpFilterConfig{ConfigBytes: []byte("invalid_config"), EnvoyScope: scope} + + _, err := createSecurityConfig(gohttpConfig) + assert.NotNil(t, err) +} diff --git a/egofilters/http/security/context/BUILD.bazel b/egofilters/http/security/context/BUILD.bazel new file mode 100644 index 0000000..5c15aa3 --- /dev/null +++ b/egofilters/http/security/context/BUILD.bazel @@ -0,0 +1,33 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "context.go", + "request_context.go", + "response_context.go", + ], + importpath = "github.com/grab/ego/egofilters/http/security/context", + visibility = ["//visibility:public"], + deps = [ + "//ego/src/go/envoy:go_default_library", + "//ego/src/go/logger:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "request_context_test.go", + "response_context_test.go", + ], + embed = [":go_default_library"], + deps = [ + "@com_github_stretchr_testify//assert:go_default_library", + ], +) diff --git a/egofilters/http/security/context/context.go b/egofilters/http/security/context/context.go new file mode 100644 index 0000000..8c4634e --- /dev/null +++ b/egofilters/http/security/context/context.go @@ -0,0 +1,12 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package context + +import "github.com/grab/ego/ego/src/go/envoy" + +type Context interface { + ActiveSpan() envoy.Span +} diff --git a/egofilters/http/security/context/request_context.go b/egofilters/http/security/context/request_context.go new file mode 100644 index 0000000..8122adf --- /dev/null +++ b/egofilters/http/security/context/request_context.go @@ -0,0 +1,141 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package context + +import ( + gocontext "context" + "io" + "net/http" + + "github.com/grab/ego/ego/src/go/envoy" + "github.com/grab/ego/ego/src/go/logger" +) + +type AuthStatus int + +const ( + // TODO: Change AuthOK to another number as 0 is default value of int + // and we don't want AuthOk by default. + AuthOK AuthStatus = 0 + AuthError AuthStatus = 1 + AuthDenied AuthStatus = 3 +) + +const ( + FilterStatePrefix = "egodemo.security.ctx.session." +) + +// Authentication response object for a Callbacks. +type AuthResponse struct { + // Required call status. + Status AuthStatus + // Required http status used only on denied response. + StatusCode int + // Optional http body used only on denied response. + Body string + // Optional http headers used on either denied or ok responses. + HeadersToRemove map[string]struct{} + // Optional http headers used on either denied or ok responses. + HeadersToSet map[string]string + // Optional http headers used on either denied or ok responses. + HeadersToAppend map[string]string + // Filter State. FilterStatePrefix will be added to keys + // before storing in FilterState + FilterState map[string]string +} + +func AuthResponseOK() AuthResponse { + return AuthResponse{ + Status: AuthOK, + StatusCode: http.StatusOK, + } +} + +func AuthResponseUnauthorized() AuthResponse { + return AuthResponse{ + Status: AuthDenied, + StatusCode: http.StatusUnauthorized, + } +} + +func AuthResponseDenied(statusCode int) AuthResponse { + return AuthResponse{ + Status: AuthDenied, + StatusCode: statusCode, + } +} + +func AuthResponseError() AuthResponse { + return AuthResponse{ + Status: AuthError, + StatusCode: http.StatusInternalServerError, + } +} + +type Callbacks interface { + OnComplete(AuthResponse) +} + +type RequestContext interface { + Context + Callbacks() Callbacks + Headers() envoy.RequestHeaderMap + GoContext() gocontext.Context + BodyReader() io.Reader + GetSecret(string) string + Logger() logger.Logger +} + +type requestContextImpl struct { + callbacks Callbacks + goContext gocontext.Context + headers envoy.RequestHeaderMap + bodyReader io.Reader + secrets map[string]string + activeSpan envoy.Span + logger logger.Logger +} + +func (c *requestContextImpl) Callbacks() Callbacks { + return c.callbacks +} + +func (c *requestContextImpl) Headers() envoy.RequestHeaderMap { + return c.headers +} + +func (c *requestContextImpl) BodyReader() io.Reader { + return c.bodyReader +} + +func (c *requestContextImpl) GoContext() gocontext.Context { + return c.goContext +} + +func (c *requestContextImpl) GetSecret(key string) string { + return c.secrets[key] +} + +func (c *requestContextImpl) Logger() logger.Logger { + return c.logger +} + +func (c *requestContextImpl) ActiveSpan() envoy.Span { + return c.activeSpan +} + +func CreateRequestContext(callbacks Callbacks, goContext gocontext.Context, activeSpan envoy.Span, + headers envoy.RequestHeaderMap, secrets map[string]string, bodyReader io.Reader, logger logger.Logger) RequestContext { + return &requestContextImpl{ + callbacks: callbacks, + goContext: goContext, + headers: headers, + secrets: secrets, + logger: logger, + bodyReader: bodyReader, + activeSpan: activeSpan, + } +} diff --git a/egofilters/http/security/context/request_context_test.go b/egofilters/http/security/context/request_context_test.go new file mode 100644 index 0000000..b804187 --- /dev/null +++ b/egofilters/http/security/context/request_context_test.go @@ -0,0 +1,40 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package context + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_AuthResponseOK(t *testing.T) { + response := AuthResponseOK() + assert.NotNil(t, response) + assert.Equal(t, 200, response.StatusCode) + assert.Equal(t, AuthOK, response.Status) +} + +func Test_AuthResponseUnauthorized(t *testing.T) { + response := AuthResponseUnauthorized() + assert.NotNil(t, response) + assert.Equal(t, 401, response.StatusCode) + assert.Equal(t, AuthDenied, response.Status) +} + +func Test_AuthResponseDenied(t *testing.T) { + response := AuthResponseDenied(401) + assert.NotNil(t, response) + assert.Equal(t, 401, response.StatusCode) + assert.Equal(t, AuthDenied, response.Status) +} + +func Test_AuthResponseError(t *testing.T) { + response := AuthResponseError() + assert.NotNil(t, response) + assert.Equal(t, 500, response.StatusCode) + assert.Equal(t, AuthError, response.Status) +} diff --git a/egofilters/http/security/context/response_context.go b/egofilters/http/security/context/response_context.go new file mode 100644 index 0000000..ca60760 --- /dev/null +++ b/egofilters/http/security/context/response_context.go @@ -0,0 +1,100 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package context + +import ( + gocontext "context" + "io" + + "github.com/grab/ego/ego/src/go/envoy" + "github.com/grab/ego/ego/src/go/logger" +) + +// SignResponse Sign response object for a Callbacks. +type SignResponse struct { + StatusCode int + HeadersToSet map[string]string +} + +type ResponseCallbacks interface { + OnCompleteSigning(SignResponse) +} + +type ResponseContext interface { + Context + AuthResponse() AuthResponse + GoContext() gocontext.Context + BodyReader() io.Reader + GetSecret(string) string + Callbacks() ResponseCallbacks + Headers() envoy.ResponseHeaderMap + RequestHeaders() envoy.RequestHeaderMap + Logger() logger.Logger +} + +type responseContextImpl struct { + goContext gocontext.Context + authResponse AuthResponse + headers envoy.ResponseHeaderMap + requestHeaders envoy.RequestHeaderMap + bodyReader io.Reader + secrets map[string]string + callbacks ResponseCallbacks + logger logger.Logger + activeSpan envoy.Span +} + +func (c *responseContextImpl) AuthResponse() AuthResponse { + return c.authResponse +} + +func (c *responseContextImpl) BodyReader() io.Reader { + return c.bodyReader +} + +func (c *responseContextImpl) GoContext() gocontext.Context { + return c.goContext +} + +func (c *responseContextImpl) GetSecret(key string) string { + return c.secrets[key] +} + +func (c *responseContextImpl) Callbacks() ResponseCallbacks { + return c.callbacks +} + +func (c *responseContextImpl) Headers() envoy.ResponseHeaderMap { + return c.headers +} + +func (c *responseContextImpl) RequestHeaders() envoy.RequestHeaderMap { + return c.requestHeaders +} + +func (c *responseContextImpl) Logger() logger.Logger { + return c.logger +} + +func (c *responseContextImpl) ActiveSpan() envoy.Span { + return c.activeSpan +} + +func CreateResponseContext(callbacks ResponseCallbacks, goContext gocontext.Context, activeSpan envoy.Span, + secrets map[string]string, authResponse AuthResponse, requestHeaders envoy.RequestHeaderMap, headers envoy.ResponseHeaderMap, + bodyReader io.Reader, logger logger.Logger) ResponseContext { + return &responseContextImpl{ + callbacks: callbacks, + goContext: goContext, + secrets: secrets, + authResponse: authResponse, + requestHeaders: requestHeaders, + headers: headers, + bodyReader: bodyReader, + logger: logger, + activeSpan: activeSpan, + } +} diff --git a/egofilters/http/security/context/response_context_test.go b/egofilters/http/security/context/response_context_test.go new file mode 100644 index 0000000..05f0b02 --- /dev/null +++ b/egofilters/http/security/context/response_context_test.go @@ -0,0 +1,67 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package context + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_AuthResponse(t *testing.T) { + r := &responseContextImpl{} + r.authResponse = AuthResponseOK() + res := r.AuthResponse() + assert.Equal(t, r.authResponse, res) +} + +func Test_BodyReader(t *testing.T) { + r := &responseContextImpl{} + res := r.BodyReader() + assert.Nil(t, res) +} + +func Test_GoContext(t *testing.T) { + r := &responseContextImpl{} + res := r.GoContext() + assert.Nil(t, res) +} + +func Test_GetSecret(t *testing.T) { + r := &responseContextImpl{} + res := r.GetSecret("test") + assert.Equal(t, "", res) +} + +func Test_Callbacks(t *testing.T) { + r := &responseContextImpl{} + res := r.Callbacks() + assert.Nil(t, res) +} + +func Test_Headers(t *testing.T) { + r := &responseContextImpl{} + res := r.Headers() + assert.Nil(t, res) +} + +func Test_RequestHeaders(t *testing.T) { + r := &responseContextImpl{} + res := r.RequestHeaders() + assert.Nil(t, res) +} + +func Test_Logger(t *testing.T) { + r := &responseContextImpl{} + res := r.Logger() + assert.Nil(t, res) +} + +func Test_ActiveSpan(t *testing.T) { + r := &responseContextImpl{} + res := r.ActiveSpan() + assert.Nil(t, res) +} diff --git a/egofilters/http/security/factory.go b/egofilters/http/security/factory.go new file mode 100644 index 0000000..83ce18d --- /dev/null +++ b/egofilters/http/security/factory.go @@ -0,0 +1,47 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package security + +import ( + "github.com/golang/protobuf/proto" + + ego "github.com/grab/ego/ego/src/go" + "github.com/grab/ego/ego/src/go/envoy" + + pb "github.com/grab/ego/egofilters/http/security/proto" +) + +type factory struct{} + +// CreateFilterFactory ... +func (f factory) CreateFilterFactory(native envoy.GoHttpFilterConfig) (ego.HttpFilterFactory, error) { + config, err := createSecurityConfig(native) + if err != nil { + return nil, err + } + + return func(native envoy.GoHttpFilter) ego.HttpFilter { + return newSecurity(native, config) + }, nil +} + +// CreateRouteSpecificFilterConfig ... +func (f factory) CreateRouteSpecificFilterConfig(native envoy.GoHttpFilterConfig) (interface{}, error) { + settings := pb.Requirement{} + unsafeBytes := native.Settings() // Volatile! Handle with care! + if err := proto.Unmarshal([]byte(unsafeBytes), &settings); err != nil { + return nil, err + } + if err := settings.Validate(); err != nil { + return nil, err + } + return settings, nil +} + +// CreateFactoryFactory ... +func CreateFactoryFactory() ego.HttpFilterFactoryFactory { + return factory{} +} diff --git a/egofilters/http/security/factory_test.go b/egofilters/http/security/factory_test.go new file mode 100644 index 0000000..2200ae6 --- /dev/null +++ b/egofilters/http/security/factory_test.go @@ -0,0 +1,127 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package security + +import ( + "testing" + + "github.com/golang/protobuf/proto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/grab/ego/ego/src/go/volatile" + egomock "github.com/grab/ego/ego/test/go/mock" + + pb "github.com/grab/ego/egofilters/http/security/proto" + + envoymocks "github.com/grab/ego/ego/test/go/mock/gen/envoy" +) + +func TestCreateFilterFactoryReturnsError(t *testing.T) { + factoryFactory := CreateFactoryFactory() + + gohttpConfig := &egomock.GoHttpFilterConfig{ConfigBytes: []byte("invalid_config")} + _, err := factoryFactory.CreateFilterFactory(gohttpConfig) + + assert.NotNil(t, err) +} + +func TestCreateFilterSuccessfully(t *testing.T) { + pbConfig := ` + providers: < + key: "my_custom_hmac_provider" + value: < + custom_hmac_provider: < + request_validation_url: "https://hmac.example.com/validate" + service_key: "me" + service_token: "top-secret!" + > + > + > + ` + settings := &pb.Settings{} + err := proto.UnmarshalText(pbConfig, settings) + require.Nil(t, err) + + configBytes, err := proto.Marshal(settings) + require.Nil(t, err) + + scope := &envoymocks.Scope{} + scope.On("CounterFromStatName", "auth_ok").Return(&envoymocks.Counter{}) + scope.On("CounterFromStatName", "auth_denied").Return(&envoymocks.Counter{}) + scope.On("CounterFromStatName", "auth_error").Return(&envoymocks.Counter{}) + + factoryFactory := CreateFactoryFactory() + + gohttpConfig := &egomock.GoHttpFilterConfig{ConfigBytes: configBytes, EnvoyScope: scope} + factory, err := factoryFactory.CreateFilterFactory(gohttpConfig) + + require.Nil(t, err) + require.NotNil(t, factory) + + native := &envoymocks.GoHttpFilter{} + + secretProvider := &envoymocks.GenericSecretConfigProvider{} + native.On("GenericSecretProvider").Return((secretProvider)) + secretProvider.On("Secret").Return(volatile.String("secret")) + + native.On("Log", mock.Anything, mock.Anything) + + filter := factory(native) + assert.NotNil(t, filter) +} + +func TestCreateRouteSpecificConfigReturnsError(t *testing.T) { + factoryFactory := CreateFactoryFactory() + + gohttpConfig := &egomock.GoHttpFilterConfig{ConfigBytes: []byte("invalid_config")} + _, err := factoryFactory.CreateRouteSpecificFilterConfig(gohttpConfig) + + assert.NotNil(t, err) +} + +func TestCreateRouteSpecificSuccessfully(t *testing.T) { + pbConfig := ` + provider_name: "something" + ` + settings := &pb.Requirement{} + err := proto.UnmarshalText(pbConfig, settings) + require.Nil(t, err) + + configBytes, err := proto.Marshal(settings) + require.Nil(t, err) + + factoryFactory := CreateFactoryFactory() + + gohttpConfig := &egomock.GoHttpFilterConfig{ConfigBytes: configBytes} + config, err := factoryFactory.CreateRouteSpecificFilterConfig(gohttpConfig) + + require.Nil(t, err) + assert.NotNil(t, config) +} + +func TestCreateRouteSpecificReturnsValidationError(t *testing.T) { + pbConfig := ` + requires_any: < + requirements: < + > + > + ` + settings := &pb.Requirement{} + err := proto.UnmarshalText(pbConfig, settings) + require.Nil(t, err) + + configBytes, err := proto.Marshal(settings) + require.Nil(t, err) + + factoryFactory := CreateFactoryFactory() + + gohttpConfig := &egomock.GoHttpFilterConfig{ConfigBytes: configBytes} + _, err = factoryFactory.CreateRouteSpecificFilterConfig(gohttpConfig) + + require.NotNil(t, err) +} diff --git a/egofilters/http/security/filter.go b/egofilters/http/security/filter.go new file mode 100644 index 0000000..28b6b6b --- /dev/null +++ b/egofilters/http/security/filter.go @@ -0,0 +1,331 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package security + +import ( + "encoding/json" + "io" + + ego "github.com/grab/ego/ego/src/go" + "github.com/grab/ego/ego/src/go/envoy" + "github.com/grab/ego/ego/src/go/envoy/datastatus" + "github.com/grab/ego/ego/src/go/envoy/headersstatus" + "github.com/grab/ego/ego/src/go/envoy/lifespan" + "github.com/grab/ego/ego/src/go/envoy/statetype" + "github.com/grab/ego/ego/src/go/envoy/trailersstatus" + "github.com/grab/ego/ego/src/go/logger" + + "github.com/grab/ego/egofilters/http/security/context" + pb "github.com/grab/ego/egofilters/http/security/proto" + "github.com/grab/ego/egofilters/http/security/verifier" +) + +const ( + FilterID = "security" +) + +// State of this filter's communication with the verifier. +// The filter has either not started calling the verifier, in the middle of calling it +// or has completed. +type State int + +const ( + NotStarted State = iota + // when the filter is waiting for request body + WaitingForRequestBody + // When the filter has received a request & start verifying + Calling + // When the filter has sent a local reply to client + Responded + // When the filter allows a request coming to next filter + Complete + // when the filter is waiting for response body + WaitingForResponseBody + // when the filter is signing response + Signing +) + +const ( + authPost uint64 = 0 + signPost uint64 = 1 +) + +type security struct { + ego.HttpFilterBase + + config *securityConfig + state State + requestHeaders envoy.RequestHeaderMap + responseHeaders envoy.ResponseHeaderMap + + verifier verifier.Verifier + signer verifier.Signer + + // Used to caching response from OnComplete from a goroutine + authResponse context.AuthResponse + + // Used to cache sign response + signResponse context.SignResponse + + // Secret key/value pairs that we automagically get injected + secrets map[string]string +} + +func newSecurity(native envoy.GoHttpFilter, config *securityConfig) ego.HttpFilter { + f := &security{ + state: NotStarted, + config: config, + } + f.HttpFilterBase.Init(native) + + // Secrets already held by C, no need to copy + unsafeBytes := []byte(f.Native.GenericSecretProvider().Secret()) + if err := json.Unmarshal(unsafeBytes, &f.secrets); err != nil { + f.Logger().Error("[newSecurity] unmarshal secret error") + } + return f +} + +// Noted that we need to handle OnDestroy to stop whatever we're doing in securityFilter +func (f *security) OnDestroy() { + f.Cancel() +} + +func (f *security) DecodeHeaders(headers envoy.RequestHeaderMap, endStream bool) headersstatus.Type { + + f.Logger().Debug("[DecodeHeaders] called") + + config := f.Native.ResolveMostSpecificPerGoFilterConfig(FilterID, f.Native.DecoderCallbacks().Route()) + if config == nil { + return headersstatus.Continue + } + + requirement, ok := config.(pb.Requirement) + if !ok { + return headersstatus.Continue + } + + f.verifier, f.signer = f.config.findProvider(&requirement) + if f.verifier == nil { + // FIXME: shouldn't we rather block the request? + return headersstatus.Continue + } + + f.requestHeaders = headers + + // TODO: check logic for Http::Utility::isWebSocketUpgradeRequest(headers) + // and Http::Utility::isH2UpgradeRequest(headers) + if f.verifier.WithBody() && !endStream { + // Need to wait for body on DecodeData + f.state = WaitingForRequestBody + return headersstatus.StopIteration + } + + // Otherwise startVerify the request without body + f.Logger().Debug("[DecodeHeaders] start verify without body") + f.startVerify(nil) + + // Waiting for the authResp from the verifier + return headersstatus.StopAllIterationAndWatermark +} + +func (f *security) DecodeData(data envoy.BufferInstance, endStream bool) datastatus.Type { + + f.Logger().Debug("[DecodeData] called") + + if f.state != WaitingForRequestBody { + return datastatus.Continue + } + + // Only purpose of DecodeData is buffer data, if it doesn't need just + // continue. Note that will only get here if we have a verifier. + // TODO: check how we can conveniently limit the buffer size. + if !endStream { + // wait for all data + return datastatus.StopIterationAndBuffer + } + + // We're at the end of stream. Add the last piece of data to the buffer + dc := f.Native.DecoderCallbacks() + dc.AddDecodedData(data, true) + + // Start verifying with the body + f.Logger().Debug("[DecodeData] start verify with body") + f.startVerify(dc.DecodingBuffer().NewReader(0)) + + return datastatus.StopIterationAndWatermark +} + +func (f *security) DecodeTrailers(trailes envoy.RequestTrailerMap) trailersstatus.Type { + + f.Logger().Debug("[DecodeTrailers] called") + + if f.state == Calling { + return trailersstatus.StopIteration + } + return trailersstatus.Continue +} + +func (f *security) EncodeHeaders(headers envoy.ResponseHeaderMap, endStream bool) headersstatus.Type { + + f.Logger().Debug("[EncodeHeaders] called") + + if f.signer == nil || !f.signer.SigningRequired(headers, f.authResponse) { + return headersstatus.Continue + } + + f.responseHeaders = headers + if !endStream { + f.state = WaitingForResponseBody + return headersstatus.StopIteration + } + + f.startSigning(nil) + return headersstatus.StopAllIterationAndWatermark +} + +func (f *security) EncodeData(data envoy.BufferInstance, endStream bool) datastatus.Type { + f.Logger().Debug("[EncodeData] called") + + if f.state != WaitingForResponseBody { + return datastatus.Continue + } + + if endStream { + f.Native.EncoderCallbacks().AddEncodedData(data, true) + f.startSigning(f.Native.EncoderCallbacks().EncodingBuffer().NewReader(0)) + return datastatus.StopIterationAndWatermark + } + return datastatus.StopIterationAndBuffer +} + +// OnPost will be called from a filter from C side after we Post to get back to the "main-thread" +func (f *security) OnPost(tag uint64) { + f.Logger().Debug("[OnPost] called") + switch tag { + case authPost: + f.endVerify() + case signPost: + f.endSigning() + } +} + +// OnComplete implement for Callbacks interface, will be called by Verifier +func (f *security) OnComplete(response context.AuthResponse) { + f.Logger().Debug("[OnComplete] called") + f.authResponse = response + f.Native.Post(authPost) + f.Unpin() +} + +func (f *security) OnCompleteSigning(signResp context.SignResponse) { + f.Logger().Debug("[OnCompleteSigning] called") + + f.signResponse = signResp + f.Native.Post(signPost) +} + +func (f *security) startVerify(body io.Reader) { + f.Logger().Debug("[startVerify] called") + f.state = Calling + ctx := context.CreateRequestContext(f, f.Context, f.Native.DecoderCallbacks().ActiveSpan(), f.requestHeaders, f.secrets, body, f.Logger()) + f.Pin() + go func() { + f.verifier.Verify(ctx) + }() +} + +func (f *security) endVerify() { + + f.Logger().Debug("[endVerify] called") + + // This stream has been reset, abort the callback. + if f.state == Responded { + return + } + + dc := f.Native.DecoderCallbacks() + response := f.authResponse + + switch response.Status { + case context.AuthOK: + headers := f.requestHeaders + for k := range response.HeadersToRemove { + headers.Remove(k) + } + for k, v := range response.HeadersToSet { + headers.SetCopy(k, v) + } + for k, v := range response.HeadersToAppend { + headers.AppendCopy(k, v) + } + for k, v := range response.FilterState { + f.Native.DecoderCallbacks().StreamInfo().FilterState().SetData(context.FilterStatePrefix+k, v, statetype.ReadOnly, lifespan.DownstreamRequest) + } + f.config.stats.authOK.Inc() + f.state = Complete + + // This function has been called only from OnPost, result of OnComplete from a goroutine + // So we need to continue decoding because the case not allow already handled above + dc.ContinueDecoding() + + case context.AuthDenied: + // TODO: support modify headers of the local response to client + f.Logger().Warn("[endVerify] could not authenticate the request", logger.Data{ + "status_code": response.StatusCode, + }) + f.state = Responded + + // use only HeadersToSet for now. Consider adding HeaderToAppend and HeaderToRemove if any use cases. + dc.SendLocalReply(response.StatusCode, response.Body, response.HeadersToSet, "") + f.config.stats.authDenied.Inc() + + case context.AuthError: + // TODO: check possibility to support failureModeAllow + f.Logger().Warn("[endVerify] rejected the request with an error", logger.Data{ + "status_code": response.StatusCode, + }) + f.state = Responded + + // use only HeadersToSet for now. Consider adding HeaderToAppend and HeaderToRemove if any use cases. + dc.SendLocalReply(response.StatusCode, response.Body, response.HeadersToSet, "") + f.config.stats.authError.Inc() + + default: + f.Logger().Warn("[endVerify] unknown response status from the verifier", logger.Data{ + "status": response.Status, + }) + f.state = Responded + f.config.stats.authError.Inc() + } +} + +func (f *security) startSigning(body io.Reader) { + + f.Logger().Debug("[startSigning] called") + + f.state = Signing + var ctx = context.CreateResponseContext( + f, f.Context, f.Native.EncoderCallbacks().ActiveSpan(), f.secrets, f.authResponse, f.requestHeaders, f.responseHeaders, body, f.Logger()) + + f.Pin() + go func() { + defer f.Unpin() + f.signer.Sign(ctx) + }() +} + +func (f *security) endSigning() { + f.Logger().Debug("[endSigning] called") + if f.signResponse.StatusCode != 0 { + f.responseHeaders.SetStatus(f.signResponse.StatusCode) + } + for k, v := range f.signResponse.HeadersToSet { + f.responseHeaders.SetCopy(k, v) + } + + f.Native.EncoderCallbacks().ContinueEncoding() +} diff --git a/egofilters/http/security/filter_sign_test.go b/egofilters/http/security/filter_sign_test.go new file mode 100644 index 0000000..e9aca3d --- /dev/null +++ b/egofilters/http/security/filter_sign_test.go @@ -0,0 +1,390 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package security + +import ( + "strings" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/grab/ego/ego/src/go/envoy/datastatus" + "github.com/grab/ego/ego/src/go/envoy/headersstatus" + "github.com/grab/ego/ego/src/go/volatile" + + "github.com/grab/ego/egofilters/http/security/context" + pb "github.com/grab/ego/egofilters/http/security/proto" + "github.com/grab/ego/egofilters/http/security/verifier" + + verifiermocks "github.com/grab/ego/egofilters/mock/gen/http/security/verifier" + envoymocks "github.com/grab/ego/ego/test/go/mock/gen/envoy" +) + +func TestEncodeHeaders(t *testing.T) { + + tcs := []struct { + name string + // set-up + signRequired bool + endstream bool + routeSpecificFilterConfig interface{} + + // verify + signCalled bool + expectedEncodeHeadersResult headersstatus.Type + }{ + { + name: "should wait for body if endstream is false", + signRequired: true, + endstream: false, + routeSpecificFilterConfig: pb.Requirement{ + RequiresType: &pb.Requirement_ProviderName{ProviderName: "my_provider"}, + }, + + signCalled: false, + expectedEncodeHeadersResult: headersstatus.StopIteration, + }, + { + name: "should sign response if endstream is true", + signRequired: true, + endstream: true, + routeSpecificFilterConfig: pb.Requirement{ + RequiresType: &pb.Requirement_ProviderName{ProviderName: "my_provider"}, + }, + + signCalled: true, + expectedEncodeHeadersResult: headersstatus.StopAllIterationAndWatermark, + }, + { + name: "should continue if sign is not required", + signRequired: false, + endstream: true, + routeSpecificFilterConfig: pb.Requirement{ + RequiresType: &pb.Requirement_ProviderName{ProviderName: "my_provider"}, + }, + + signCalled: false, + expectedEncodeHeadersResult: headersstatus.Continue, + }, + { + name: "should continue if can't find signer", + signRequired: true, + endstream: true, + routeSpecificFilterConfig: pb.Requirement{ + RequiresType: &pb.Requirement_ProviderName{ProviderName: "my_unknown_provider"}, + }, + + signCalled: false, + expectedEncodeHeadersResult: headersstatus.Continue, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + native := &envoymocks.GoHttpFilter{} + + wg := sync.WaitGroup{} + native.On("Pin").Run(func(args mock.Arguments) { + wg.Add(1) + }) + native.On("Unpin").Run(func(args mock.Arguments) { + wg.Done() + }) + + secretProvider := &envoymocks.GenericSecretConfigProvider{} + native.On("GenericSecretProvider").Return((secretProvider)) + secretProvider.On("Secret").Return(volatile.String("zzz")) + + native.On("Log", mock.Anything, mock.Anything) + + decoderCallbacks := &envoymocks.DecoderFilterCallbacks{} + native.On("DecoderCallbacks").Return(decoderCallbacks) + + decoderCallbacks.On("ActiveSpan").Return(&envoymocks.Span{}) + + route := &envoymocks.Route{} + decoderCallbacks.On("Route").Return(route) + + encoderCallbacks := &envoymocks.EncoderFilterCallbacks{} + native.On("EncoderCallbacks").Return(encoderCallbacks) + + encoderSpan := &envoymocks.Span{} + encoderCallbacks.On("ActiveSpan").Return(encoderSpan) + + native.On("ResolveMostSpecificPerGoFilterConfig", FilterID, route).Return(tc.routeSpecificFilterConfig) + + signer := &verifiermocks.Signer{} + + config := &securityConfig{ + signers: map[string]verifier.Signer{ + "my_provider": signer, + }, + } + + filter := newSecurity(native, config) + assert.NotNil(t, filter) + + requestHeaders := &envoymocks.RequestHeaderMap{} + filter.DecodeHeaders(requestHeaders, false) + + responseHeaderMap := &envoymocks.ResponseHeaderMap{} + + signer.On("SigningRequired", responseHeaderMap, mock.AnythingOfType("AuthResponse")).Return(tc.signRequired) + var ctx context.ResponseContext + signer.On("Sign", mock.Anything).Run(func(args mock.Arguments) { + ctx = args[0].(context.ResponseContext) + }) + + result := filter.EncodeHeaders(responseHeaderMap, tc.endstream) + + assert.Equal(t, tc.expectedEncodeHeadersResult, result) + wg.Wait() + + if tc.signCalled { + signer.AssertCalled(t, "Sign", mock.Anything) + + // verify passing correct data to signer + assert.NotNil(t, ctx.Callbacks()) + assert.Equal(t, responseHeaderMap, ctx.Headers()) + assert.Equal(t, filter, ctx.Callbacks()) + assert.NotNil(t, ctx.Logger()) + assert.NotNil(t, ctx.GoContext()) + assert.Equal(t, encoderCallbacks.ActiveSpan(), ctx.ActiveSpan()) + assert.Nil(t, ctx.BodyReader()) + } + }) + } +} + +func TestEncodeData(t *testing.T) { + + tcs := []struct { + name string + endstream bool + signRequired bool + routeSpecificFilterConfig interface{} + + signCalled bool + expectedEncodeDataResult datastatus.Type + }{ + { + name: "should wait for full body if endstream is false", + endstream: false, + signRequired: true, + routeSpecificFilterConfig: pb.Requirement{ + RequiresType: &pb.Requirement_ProviderName{ProviderName: "my_provider"}, + }, + + signCalled: false, + expectedEncodeDataResult: datastatus.StopIterationAndBuffer, + }, + { + name: "should continue if sign is not required", + endstream: false, + signRequired: false, + routeSpecificFilterConfig: pb.Requirement{ + RequiresType: &pb.Requirement_ProviderName{ProviderName: "my_provider"}, + }, + + signCalled: false, + expectedEncodeDataResult: datastatus.Continue, + }, + { + name: "should sign response if endstream is true", + endstream: true, + signRequired: true, + routeSpecificFilterConfig: pb.Requirement{ + RequiresType: &pb.Requirement_ProviderName{ProviderName: "my_provider"}, + }, + + signCalled: true, + expectedEncodeDataResult: datastatus.StopIterationAndWatermark, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + native := &envoymocks.GoHttpFilter{} + + wg := sync.WaitGroup{} + native.On("Pin").Run(func(args mock.Arguments) { + wg.Add(1) + }) + native.On("Unpin").Run(func(args mock.Arguments) { + wg.Done() + }) + + secretProvider := &envoymocks.GenericSecretConfigProvider{} + native.On("GenericSecretProvider").Return((secretProvider)) + secretProvider.On("Secret").Return(volatile.String("zzz")) + + native.On("Log", mock.Anything, mock.Anything) + + decoderCallbacks := &envoymocks.DecoderFilterCallbacks{} + native.On("DecoderCallbacks").Return(decoderCallbacks) + + decoderCallbacks.On("ActiveSpan").Return(&envoymocks.Span{}) + + route := &envoymocks.Route{} + decoderCallbacks.On("Route").Return(route) + + encoderCallbacks := &envoymocks.EncoderFilterCallbacks{} + native.On("EncoderCallbacks").Return(encoderCallbacks) + + encoderSpan := &envoymocks.Span{} + encoderCallbacks.On("ActiveSpan").Return(encoderSpan) + + native.On("ResolveMostSpecificPerGoFilterConfig", FilterID, route).Return(tc.routeSpecificFilterConfig) + + signer := &verifiermocks.Signer{} + signer.On("SigningRequired", mock.Anything, mock.Anything).Return(tc.signRequired) + var ctx context.ResponseContext + signer.On("Sign", mock.Anything).Run(func(args mock.Arguments) { + ctx = args[0].(context.ResponseContext) + }) + config := &securityConfig{ + signers: map[string]verifier.Signer{ + "my_provider": signer, + }, + } + + filter := newSecurity(native, config) + assert.NotNil(t, filter) + + _ = filter.DecodeHeaders(&envoymocks.RequestHeaderMap{}, false) + + responseHeaderMap := &envoymocks.ResponseHeaderMap{} + filter.EncodeHeaders(responseHeaderMap, false) + + data := &envoymocks.BufferInstance{} + encoderCallbacks.On("AddEncodedData", data, true) + + fullBuffer := &envoymocks.BufferInstance{} + bodyReader := strings.NewReader("zzz") + fullBuffer.On("NewReader", uint64(0)).Return(bodyReader) + encoderCallbacks.On("EncodingBuffer").Return(fullBuffer) + + encodeDataResult := filter.EncodeData(data, tc.endstream) + + assert.Equal(t, tc.expectedEncodeDataResult, encodeDataResult) + wg.Wait() + + if tc.signCalled { + signer.AssertCalled(t, "Sign", mock.Anything) + encoderCallbacks.AssertExpectations(t) + + // verify passing correct data to signer + assert.NotNil(t, ctx.Callbacks()) + assert.Equal(t, responseHeaderMap, ctx.Headers()) + assert.Equal(t, filter, ctx.Callbacks()) + assert.NotNil(t, ctx.Logger()) + assert.NotNil(t, ctx.GoContext()) + assert.Equal(t, encoderCallbacks.ActiveSpan(), ctx.ActiveSpan()) + assert.Equal(t, bodyReader, ctx.BodyReader()) + } + }) + } +} + +func TestOnCompleteSigning(t *testing.T) { + + tcs := []struct { + name string + signResponse context.SignResponse + + statusCode int + responeHeaders map[string]string + }{ + { + name: "should continue encoding", + }, + { + name: "should set status code", + signResponse: context.SignResponse{ + StatusCode: 200, + }, + + statusCode: 200, + }, + { + name: "should set header", + signResponse: context.SignResponse{ + HeadersToSet: map[string]string{ + "x-header": "value", + }, + }, + + responeHeaders: map[string]string{ + "x-header": "value", + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + native := &envoymocks.GoHttpFilter{} + + secretProvider := &envoymocks.GenericSecretConfigProvider{} + native.On("GenericSecretProvider").Return((secretProvider)) + secretProvider.On("Secret").Return(volatile.String("zzz")) + + native.On("Log", mock.Anything, mock.Anything) + + decoderCallbacks := &envoymocks.DecoderFilterCallbacks{} + native.On("DecoderCallbacks").Return(decoderCallbacks) + + decoderCallbacks.On("ActiveSpan").Return(&envoymocks.Span{}) + + route := &envoymocks.Route{} + decoderCallbacks.On("Route").Return(route) + + encoderCallbacks := &envoymocks.EncoderFilterCallbacks{} + native.On("EncoderCallbacks").Return(encoderCallbacks) + + native.On("ResolveMostSpecificPerGoFilterConfig", FilterID, route).Return(pb.Requirement{ + RequiresType: &pb.Requirement_ProviderName{ProviderName: "my_provider"}, + }) + + signer := &verifiermocks.Signer{} + signer.On("SigningRequired", mock.Anything, mock.Anything).Return(true) + signer.On("Sign", mock.Anything) + config := &securityConfig{ + signers: map[string]verifier.Signer{ + "my_provider": signer, + }, + } + + filter := newSecurity(native, config) + assert.NotNil(t, filter) + + filter.DecodeHeaders(&envoymocks.RequestHeaderMap{}, false) + + responseHeaderMap := &envoymocks.ResponseHeaderMap{} + filter.EncodeHeaders(responseHeaderMap, false) + + callback := filter.(context.ResponseCallbacks) + + native.On("Post", signPost).Run(func(args mock.Arguments) { + filter.OnPost(signPost) + }) + + encoderCallbacks.On("ContinueEncoding") + if tc.statusCode > 0 { + responseHeaderMap.On("SetStatus", tc.statusCode) + } + for k, v := range tc.responeHeaders { + responseHeaderMap.On("SetCopy", k, v) + } + + callback.OnCompleteSigning(tc.signResponse) + + encoderCallbacks.AssertExpectations(t) + responseHeaderMap.AssertExpectations(t) + }) + } +} diff --git a/egofilters/http/security/filter_verify_test.go b/egofilters/http/security/filter_verify_test.go new file mode 100644 index 0000000..a7ac58c --- /dev/null +++ b/egofilters/http/security/filter_verify_test.go @@ -0,0 +1,495 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package security + +import ( + "strings" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/grab/ego/ego/src/go/envoy/datastatus" + "github.com/grab/ego/ego/src/go/envoy/headersstatus" + "github.com/grab/ego/ego/src/go/envoy/trailersstatus" + "github.com/grab/ego/ego/src/go/volatile" + + context "github.com/grab/ego/egofilters/http/security/context" + pb "github.com/grab/ego/egofilters/http/security/proto" + "github.com/grab/ego/egofilters/http/security/verifier" + + verifiermocks "github.com/grab/ego/egofilters/mock/gen/http/security/verifier" + envoymocks "github.com/grab/ego/ego/test/go/mock/gen/envoy" +) + +func TestDecodeHeaders(t *testing.T) { + + tcs := []struct { + // Set-up + name string + requestBodyRequired bool + endstream bool + routeSpecificFilterConfig interface{} + + // Verify + verifierCalled bool + expectedDecodeHeadersResult headersstatus.Type + expectedDecodeTrailersResult trailersstatus.Type + }{ + { + name: "should call verifier and wait for response if body isn't required", + requestBodyRequired: false, + endstream: true, + routeSpecificFilterConfig: pb.Requirement{ + RequiresType: &pb.Requirement_ProviderName{ProviderName: "my_verifier"}, + }, + + verifierCalled: true, + expectedDecodeHeadersResult: headersstatus.StopAllIterationAndWatermark, + expectedDecodeTrailersResult: trailersstatus.StopIteration, + }, + + { + name: "should wait for request body if required", + requestBodyRequired: true, + endstream: false, + routeSpecificFilterConfig: pb.Requirement{ + RequiresType: &pb.Requirement_ProviderName{ProviderName: "my_verifier"}, + }, + + verifierCalled: false, + expectedDecodeHeadersResult: headersstatus.StopIteration, + expectedDecodeTrailersResult: trailersstatus.Continue, + }, + + { + name: "shouldn't wait for request body if endstream is true", + requestBodyRequired: true, + endstream: true, + routeSpecificFilterConfig: pb.Requirement{ + RequiresType: &pb.Requirement_ProviderName{ProviderName: "my_verifier"}, + }, + + verifierCalled: true, + expectedDecodeHeadersResult: headersstatus.StopAllIterationAndWatermark, + expectedDecodeTrailersResult: trailersstatus.StopIteration, + }, + + { + name: "should continue if nil routeSpecificFilterConfig", + requestBodyRequired: true, + endstream: true, + routeSpecificFilterConfig: nil, + + verifierCalled: false, + expectedDecodeHeadersResult: headersstatus.Continue, + expectedDecodeTrailersResult: trailersstatus.Continue, + }, + + { + name: "should continue if invalid type of routeSpecificFilterConfig", + requestBodyRequired: true, + endstream: true, + routeSpecificFilterConfig: "string instead of requirement", + + verifierCalled: false, + expectedDecodeHeadersResult: headersstatus.Continue, + expectedDecodeTrailersResult: trailersstatus.Continue, + }, + + { + name: "should continue if unknown provider", + requestBodyRequired: true, + endstream: true, + routeSpecificFilterConfig: pb.Requirement{ + RequiresType: &pb.Requirement_ProviderName{ProviderName: "my_unknown_provider"}, + }, + + verifierCalled: false, + expectedDecodeHeadersResult: headersstatus.Continue, + expectedDecodeTrailersResult: trailersstatus.Continue, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + native := &envoymocks.GoHttpFilter{} + + wg := sync.WaitGroup{} + native.On("Pin").Run(func(args mock.Arguments) { + wg.Add(1) + }) + + secretProvider := &envoymocks.GenericSecretConfigProvider{} + native.On("GenericSecretProvider").Return((secretProvider)) + secretProvider.On("Secret").Return(volatile.String("zzz")) + + native.On("Log", mock.Anything, mock.Anything) + + decoderCallbacks := &envoymocks.DecoderFilterCallbacks{} + native.On("DecoderCallbacks").Return(decoderCallbacks) + + decoderCallbacks.On("ActiveSpan").Return(&envoymocks.Span{}) + + route := &envoymocks.Route{} + decoderCallbacks.On("Route").Return(route) + + native.On("ResolveMostSpecificPerGoFilterConfig", FilterID, route).Return(tc.routeSpecificFilterConfig) + + provider := &verifiermocks.Verifier{} + config := &securityConfig{ + verifiers: map[string]verifier.Verifier{ + "my_verifier": provider, + }, + } + provider.On("WithBody").Return(tc.requestBodyRequired) + + filter := newSecurity(native, config) + var ctx context.RequestContext + provider.On("Verify", mock.Anything).Run(func(args mock.Arguments) { + ctx = args[0].(context.RequestContext) + defer wg.Done() + }) + assert.NotNil(t, filter) + + headerMap := &envoymocks.RequestHeaderMap{} + decodeHeadersResult := filter.DecodeHeaders(headerMap, tc.endstream) + decodeTrailersResult := filter.DecodeTrailers(&envoymocks.RequestTrailerMap{}) + + wg.Wait() + + // Continue if not route specific route config + assert.Equal(t, tc.expectedDecodeHeadersResult, decodeHeadersResult) + assert.Equal(t, tc.expectedDecodeTrailersResult, decodeTrailersResult) + if tc.verifierCalled { + provider.AssertCalled(t, "Verify", mock.Anything) + // verify context passed to verifier + assert.Equal(t, filter, ctx.Callbacks()) + assert.NotNil(t, ctx.GoContext()) + assert.Equal(t, decoderCallbacks.ActiveSpan(), ctx.ActiveSpan()) + assert.Equal(t, headerMap, ctx.Headers()) + assert.Nil(t, ctx.BodyReader()) + assert.NotNil(t, ctx.Logger()) + } + }) + } +} + +func TestDecodeData(t *testing.T) { + tcs := []struct { + name string + // set-up + endstream bool + routeSpecificFilterConfig interface{} + + // verify + expectedDataStatus datastatus.Type + verifierCalled bool + }{ + { + name: "should call verifier if endstream is true", + endstream: true, + routeSpecificFilterConfig: pb.Requirement{ + RequiresType: &pb.Requirement_ProviderName{ProviderName: "my_verifier"}, + }, + + expectedDataStatus: datastatus.StopIterationAndWatermark, + verifierCalled: true, + }, + { + name: "should wait for more data if endstream is false", + endstream: false, + routeSpecificFilterConfig: pb.Requirement{ + RequiresType: &pb.Requirement_ProviderName{ProviderName: "my_verifier"}, + }, + + expectedDataStatus: datastatus.StopIterationAndBuffer, + verifierCalled: false, + }, + { + name: "should continue if not waiting for request body", + endstream: false, + routeSpecificFilterConfig: pb.Requirement{ + // Invalid provider name will cause filter ignoring current request + RequiresType: &pb.Requirement_ProviderName{ProviderName: "unknown"}, + }, + + expectedDataStatus: datastatus.Continue, + verifierCalled: false, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + native := &envoymocks.GoHttpFilter{} + + wg := sync.WaitGroup{} + native.On("Pin").Run(func(args mock.Arguments) { + wg.Add(1) + }) + + secretProvider := &envoymocks.GenericSecretConfigProvider{} + native.On("GenericSecretProvider").Return((secretProvider)) + secretProvider.On("Secret").Return(volatile.String("zzz")) + + native.On("Log", mock.Anything, mock.Anything) + + decoderCallbacks := &envoymocks.DecoderFilterCallbacks{} + native.On("DecoderCallbacks").Return(decoderCallbacks) + + decoderCallbacks.On("ActiveSpan").Return(&envoymocks.Span{}) + + route := &envoymocks.Route{} + decoderCallbacks.On("Route").Return(route) + + native.On("ResolveMostSpecificPerGoFilterConfig", FilterID, route).Return(tc.routeSpecificFilterConfig) + + provider := &verifiermocks.Verifier{} + config := &securityConfig{ + verifiers: map[string]verifier.Verifier{ + "my_verifier": provider, + }, + } + provider.On("WithBody").Return(true) + + filter := newSecurity(native, config) + var ctx context.RequestContext + provider.On("Verify", mock.Anything).Run(func(args mock.Arguments) { + ctx = args[0].(context.RequestContext) + defer wg.Done() + }) + assert.NotNil(t, filter) + + headerMap := &envoymocks.RequestHeaderMap{} + filter.DecodeHeaders(headerMap, false) + + bufferInstance := &envoymocks.BufferInstance{} + decoderCallbacks.On("AddDecodedData", bufferInstance, true) + + fullBuffer := &envoymocks.BufferInstance{} + + bodyReader := strings.NewReader("zzz") + fullBuffer.On("NewReader", uint64(0)).Return(bodyReader) + decoderCallbacks.On("DecodingBuffer").Return(fullBuffer) + + decodeDataResult := filter.DecodeData(bufferInstance, tc.endstream) + + wg.Wait() + + // Continue if not route specific route config + assert.Equal(t, tc.expectedDataStatus, decodeDataResult) + if tc.verifierCalled { + decoderCallbacks.AssertExpectations(t) + + provider.AssertCalled(t, "Verify", mock.Anything) + // verify context passed to verifier + assert.Equal(t, filter, ctx.Callbacks()) + assert.NotNil(t, ctx.GoContext()) + assert.Equal(t, decoderCallbacks.ActiveSpan(), ctx.ActiveSpan()) + assert.Equal(t, headerMap, ctx.Headers()) + assert.Equal(t, bodyReader, ctx.BodyReader()) + assert.NotNil(t, ctx.Logger()) + } + }) + } +} + +func TestOnComplete(t *testing.T) { + type localReplyData struct { + StatusCode int + Body string + Header map[string]string + } + + tcs := []struct { + name string + // set-up + authResp context.AuthResponse + + // verify + localReply *localReplyData + increaseOkCounter bool + increaseErrCounter bool + increaseDeniedCounter bool + filterState map[string]string + }{ + { + name: "verify successfully", + + authResp: context.AuthResponse{ + Status: context.AuthOK, + HeadersToRemove: map[string]struct{}{"header-to-remove": {}}, + HeadersToSet: map[string]string{"header-to-set": "val1"}, + HeadersToAppend: map[string]string{"header-to-append": "val2"}, + FilterState: map[string]string{"state1": "val1"}, + }, + + // HeadersToRemove, HeadersToSet and HeadersToAppend will be remove, set and append to headers to upstream respectly + increaseOkCounter: true, + + // hardcode keys to prevent regressions in target environment + filterState: map[string]string{"egodemo.security.ctx.session.state1": "val1"}, + }, + + { + name: "verify with error response", + + authResp: context.AuthResponse{ + StatusCode: 401, + Status: context.AuthError, + Body: "this is an error", + HeadersToRemove: map[string]struct{}{"header-to-remove": {}}, + HeadersToSet: map[string]string{"header-to-set": "val1"}, + HeadersToAppend: map[string]string{"header-to-append": "val2"}, + }, + + localReply: &localReplyData{ + StatusCode: 401, + Header: map[string]string{"header-to-set": "val1"}, + Body: "this is an error", + }, + increaseErrCounter: true, + }, + + { + name: "verify with denied response", + + authResp: context.AuthResponse{ + StatusCode: 500, + Body: "this is an denied error", + Status: context.AuthDenied, + HeadersToRemove: map[string]struct{}{"header-to-remove": {}}, + HeadersToSet: map[string]string{"header-to-set": "val1"}, + HeadersToAppend: map[string]string{"header-to-append": "val2"}, + }, + + localReply: &localReplyData{ + StatusCode: 500, + Header: map[string]string{"header-to-set": "val1"}, + Body: "this is an denied error", + }, + increaseDeniedCounter: true, + }, + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + native := &envoymocks.GoHttpFilter{} + + secretProvider := &envoymocks.GenericSecretConfigProvider{} + native.On("GenericSecretProvider").Return((secretProvider)) + secretProvider.On("Secret").Return(volatile.String("zzz")) + + native.On("Log", mock.Anything, mock.Anything) + + decoderCallbacks := &envoymocks.DecoderFilterCallbacks{} + native.On("DecoderCallbacks").Return(decoderCallbacks) + + decoderCallbacks.On("ContinueDecoding") + native.On("Unpin") + + provider := &verifiermocks.Verifier{} + + authOkStats := &envoymocks.Counter{} + authDeniedStats := &envoymocks.Counter{} + authErrorStats := &envoymocks.Counter{} + + config := &securityConfig{ + verifiers: map[string]verifier.Verifier{ + "my_verifier": provider, + }, + stats: securityStats{ + authOK: authOkStats, + authDenied: authDeniedStats, + authError: authErrorStats, + }, + } + + // set-up headermap + decoderCallbacks.On("ActiveSpan").Return(&envoymocks.Span{}) + + route := &envoymocks.Route{} + decoderCallbacks.On("Route").Return(route) + + native.On("ResolveMostSpecificPerGoFilterConfig", FilterID, route).Return(pb.Requirement{ + RequiresType: &pb.Requirement_ProviderName{ProviderName: "my_verifier"}, + }) + + wg := sync.WaitGroup{} + native.On("Pin").Run(func(args mock.Arguments) { + wg.Add(1) + }) + provider.On("Verify", mock.Anything).Run(func(args mock.Arguments) { + defer wg.Done() + }) + + headerMap := &envoymocks.RequestHeaderMap{} + // end set-up headermap + + if tc.increaseOkCounter { + authOkStats.On("Inc") + } + + if tc.increaseErrCounter { + authErrorStats.On("Inc") + } + + if tc.increaseDeniedCounter { + authDeniedStats.On("Inc") + } + + provider.On("WithBody").Return(true) + + filter := newSecurity(native, config) + assert.NotNil(t, filter) + + native.On("Post", authPost).Run(func(args mock.Arguments) { + filter.OnPost(authPost) + }) + + _ = filter.DecodeHeaders(headerMap, false) + + // set-up header map + filterState := &envoymocks.FilterState{} + if tc.localReply != nil { + decoderCallbacks.On("SendLocalReply", tc.localReply.StatusCode, tc.localReply.Body, tc.localReply.Header, mock.Anything) + } else { + headerMap.On("Remove", "header-to-remove") + headerMap.On("SetCopy", "header-to-set", "val1") + headerMap.On("AppendCopy", "header-to-append", "val2") + streamInfo := &envoymocks.StreamInfo{} + decoderCallbacks.On("StreamInfo").Return(streamInfo) + + streamInfo.On("FilterState").Return(filterState) + + for k, v := range tc.filterState { + filterState.On("SetData", k, v, mock.Anything, mock.Anything) + } + + } + + callback := filter.(context.Callbacks) + assert.NotNil(t, callback) + + callback.OnComplete(tc.authResp) + native.AssertCalled(t, "Post", authPost) + native.AssertCalled(t, "Unpin") + + // verify metrics + authErrorStats.AssertExpectations(t) + authOkStats.AssertExpectations(t) + authDeniedStats.AssertExpectations(t) + + headerMap.AssertExpectations(t) + filterState.AssertExpectations(t) + + if tc.localReply != nil { + decoderCallbacks.AssertCalled(t, "SendLocalReply", mock.Anything, mock.Anything, mock.Anything, mock.Anything) + } + }) + } + +} diff --git a/egofilters/http/security/http/BUILD.bazel b/egofilters/http/security/http/BUILD.bazel new file mode 100644 index 0000000..2c30337 --- /dev/null +++ b/egofilters/http/security/http/BUILD.bazel @@ -0,0 +1,27 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["http_client.go"], + importpath = "github.com/grab/ego/egofilters/http/security/http", + visibility = ["//visibility:public"], + deps = ["//egofilters/http/security/context:go_default_library"], +) + +go_test( + name = "go_default_test", + srcs = ["http_client_test.go"], + embed = [":go_default_library"], + deps = [ + "//ego/test/go/mock/gen/envoy:go_default_library", + "//egofilters/mock/gen/http/security/context:go_default_library", + "//egofilters/mock/gen/http/security/http:go_default_library", + "@com_github_stretchr_testify//assert:go_default_library", + "@com_github_stretchr_testify//mock:go_default_library", + ], +) diff --git a/egofilters/http/security/http/http_client.go b/egofilters/http/security/http/http_client.go new file mode 100644 index 0000000..b1eb2fb --- /dev/null +++ b/egofilters/http/security/http/http_client.go @@ -0,0 +1,44 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package http + +import ( + "net/http" + + "github.com/grab/ego/egofilters/http/security/context" +) + +type HttpClient interface { + Do(req *http.Request) (*http.Response, error) +} + +type HttpClientWithCtx interface { + HttpClient + DoWithTracing(ctx context.Context, req *http.Request, spanName string) (*http.Response, error) +} + +type httpClientImpl struct { + HttpClient +} + +func (client httpClientImpl) DoWithTracing(ctx context.Context, req *http.Request, spanName string) (*http.Response, error) { + if len(spanName) > 0 { + span := ctx.ActiveSpan().SpawnChild(spanName) + defer span.FinishSpan() + + spanHeaders := span.GetContext() + for k, vals := range spanHeaders { + if len(vals) > 0 { + req.Header.Add(k, vals[0]) + } + } + } + return client.Do(req) +} + +func NewHttpClientWithCtx(client HttpClient) HttpClientWithCtx { + return &httpClientImpl{client} +} diff --git a/egofilters/http/security/http/http_client_test.go b/egofilters/http/security/http/http_client_test.go new file mode 100644 index 0000000..3b3dff9 --- /dev/null +++ b/egofilters/http/security/http/http_client_test.go @@ -0,0 +1,110 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package http + +import ( + "errors" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + contextmocks "github.com/grab/ego/egofilters/mock/gen/http/security/context" + "github.com/grab/ego/egofilters/mock/gen/http/security/http" + egomocks "github.com/grab/ego/ego/test/go/mock/gen/envoy" +) + +func TestDoWithTracing(t *testing.T) { + tcs := []struct { + name string + spanName string + existingHeader http.Header + + spanContext map[string][]string + expectedHeader http.Header + }{ + { + name: "add headers to http request", + spanName: "span_name", + + spanContext: map[string][]string{"X-Tracing-Id": {"123"}}, + expectedHeader: http.Header{ + "X-Tracing-Id": []string{"123"}, + }, + }, + { + name: "add multiple headers to http request", + spanName: "span_name", + existingHeader: http.Header{"X-Existing": {"1"}}, + + spanContext: map[string][]string{"X-Tracing-Id": {"125"}, "X-Span-Id": {"124", "ignored"}}, + expectedHeader: http.Header{ + "X-Tracing-Id": []string{"125"}, + "X-Span-Id": []string{"124"}, + "X-Existing": []string{"1"}, + }, + }, + { + name: "should handle nil span context", + spanName: "span_name", + + spanContext: nil, + expectedHeader: http.Header{}, + }, + { + name: "should neither spawn child span nor add headers to http request if span name is empty", + spanName: "", + + spanContext: map[string][]string{"X-Tracing-Id": {"123"}}, + expectedHeader: http.Header{}, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + ctx := &contextmocks.Context{} + + childSpan := &egomocks.Span{} + if len(tc.spanName) > 0 { + activeSpan := &egomocks.Span{} + ctx.On("ActiveSpan").Return(activeSpan) + + childSpan := &egomocks.Span{} + activeSpan.On("SpawnChild", tc.spanName).Return(childSpan) + + childSpan.On("GetContext").Return(tc.spanContext) + childSpan.On("FinishSpan").Times(1) + } + + httpClient := &mocks.HttpClient{} + clientWithCtx := NewHttpClientWithCtx(httpClient) + + httpResponse := &http.Response{} + httpErr := errors.New("an error") + var actualReq *http.Request + httpClient.On("Do", mock.AnythingOfType("*http.Request")).Run(func(args mock.Arguments) { + actualReq = args[0].(*http.Request) + }).Return(httpResponse, httpErr) + + req, _ := http.NewRequest(http.MethodPost, "http://test.com", nil) + for k, v := range tc.existingHeader { + req.Header[k] = v + } + + actualResp, actualErr := clientWithCtx.DoWithTracing(ctx, req, tc.spanName) + + assert.Equal(t, httpResponse, actualResp) + assert.Equal(t, httpErr, actualErr) + + childSpan.AssertExpectations(t) + httpClient.AssertExpectations(t) + + assert.Equal(t, tc.expectedHeader, actualReq.Header) + }) + } + +} diff --git a/egofilters/http/security/proto/BUILD.bazel b/egofilters/http/security/proto/BUILD.bazel new file mode 100644 index 0000000..96c2d39 --- /dev/null +++ b/egofilters/http/security/proto/BUILD.bazel @@ -0,0 +1,40 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +# gazelle:ignore + +load("@io_bazel_rules_go//proto:compiler.bzl", "go_proto_compiler") +load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +# This is generate proto for C-Side when we embeded in envoy config +api_proto_package() + +# This is generate validation file +go_proto_compiler( + name = "pgv_plugin_go", + options = ["lang=go"], + plugin = "@com_envoyproxy_protoc_gen_validate//:protoc-gen-validate", + suffix = ".pb.validate.go", + valid_archive = False, +) + +# This is generate proto for G-Side usage +go_proto_library( + name = "go_default_library", + compilers = [ + "@io_bazel_rules_go//proto:go_proto", + "pgv_plugin_go", + ], + importpath = "github.com/grab/ego/egofilters/http/security/proto", + proto = ":pkg", # api_proto_package() generates this + visibility = ["//visibility:public"], + deps = [ + "@com_envoyproxy_protoc_gen_validate//validate:go_default_library", + "@com_github_golang_protobuf//ptypes:go_default_library", + "@com_github_golang_protobuf//ptypes/any:go_default_library", + "@com_google_googleapis//google/api:annotations_go_proto", + ], +) \ No newline at end of file diff --git a/egofilters/http/security/proto/security.proto b/egofilters/http/security/proto/security.proto new file mode 100644 index 0000000..efec662 --- /dev/null +++ b/egofilters/http/security/proto/security.proto @@ -0,0 +1,75 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +syntax = "proto3"; + +package ego.security; + +import "validate/validate.proto"; +import "google/protobuf/any.proto"; +import "google/protobuf/empty.proto"; + +message Settings { + map providers = 1 [ (validate.rules).map.min_pairs = 1 ]; +} + +// This message specifies a provider. +message Provider { + oneof provider_type { + CustomHMACProvider custom_hmac_provider = 1; + // add other providers + } +} + +// A CustomHMACProvider message specifies the information will use to verify +// HMAC signature +message CustomHMACProvider { + string request_validation_url = 1 [ (validate.rules).string = {min_bytes : 1} ]; + string response_signing_url = 2 [ (validate.rules).string = {} ]; + // A bit redundant here we will improve it with SecretManagement later + string service_key = 3 [ (validate.rules).string = {min_bytes : 1} ]; + string service_token = 4 [ (validate.rules).string = {min_bytes : 1} ]; + repeated string generated_upstream_headers = 5[ + (validate.rules).repeated.unique = true, + (validate.rules).repeated.items.string = {in: [ + "x-custom-userid" + ]}]; + bool sign_resp = 6; + bool tracing_enabled = 7; +} + +// This message specifies a requirement. An empty message means verification +// is not required. +message Requirement { + oneof requires_type { + // Specify a required provider name. + string provider_name = 1; + + // Specify list of Requirement. Their results are OR-ed. + // If any one of them passes, the result is passed. + RequirementOrList requires_any = 2; + + // Specify list of Requirement. Their results are AND-ed. + // All of them must pass, if one of them fails or missing, it fails. + RequirementAndList requires_all = 3; + } +} + +// This message specifies a list of RequiredProvider. +// Their results are OR-ed; if any one of them passes, the result is passed +message RequirementOrList { + // Specify a list of Requirement. + repeated Requirement requirements = 1 + [ (validate.rules).repeated = {min_items : 2} ]; +} + +// This message specifies a list of RequiredProvider. +// Their results are AND-ed; all of them must pass, if one of them fails or +// missing, it fails. +message RequirementAndList { + // Specify a list of Requirement. + repeated Requirement requirements = 1 + [ (validate.rules).repeated = {min_items : 2} ]; +} diff --git a/egofilters/http/security/verifier/BUILD.bazel b/egofilters/http/security/verifier/BUILD.bazel new file mode 100644 index 0000000..87db0e9 --- /dev/null +++ b/egofilters/http/security/verifier/BUILD.bazel @@ -0,0 +1,50 @@ +# Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +# +# Use of this source code is governed by the Apache License 2.0 that can be +# found in the LICENSE file + +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "base_provider.go", + "consts.go", + "custom_hmac_provider.go", + "custom_hmac_validator.go", + "verifier.go", + ], + importpath = "github.com/grab/ego/egofilters/http/security/verifier", + visibility = ["//visibility:public"], + deps = [ + "//ego/src/go/envoy:go_default_library", + "//egofilters/http/security/context:go_default_library", + "//egofilters/http/security/http:go_default_library", + "//egofilters/http/security/proto:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "custom_hmac_provider_factory_test.go", + "custom_hmac_provider_sign_required_test.go", + "custom_hmac_provider_sign_test.go", + "custom_hmac_provider_verify_test.go", + "custom_hmac_validator_test.go", + ], + embed = [":go_default_library"], + deps = [ + "//ego/src/go/logger:go_default_library", + "//ego/src/go/volatile:go_default_library", + "//ego/test/go/mock:go_default_library", + "//ego/test/go/mock/gen/envoy:go_default_library", + "//egofilters/http/security/context:go_default_library", + "//egofilters/http/security/proto:go_default_library", + "//egofilters/mock/gen/http/security/context:go_default_library", + "//egofilters/mock/gen/http/security/http:go_default_library", + "@com_github_stretchr_testify//assert:go_default_library", + "@com_github_stretchr_testify//mock:go_default_library", + "@com_github_stretchr_testify//require:go_default_library", + ], +) diff --git a/egofilters/http/security/verifier/base_provider.go b/egofilters/http/security/verifier/base_provider.go new file mode 100644 index 0000000..ca5dfdd --- /dev/null +++ b/egofilters/http/security/verifier/base_provider.go @@ -0,0 +1,19 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package verifier + +import ( + "github.com/grab/ego/egofilters/http/security/context" +) + +type baseProvider struct { +} + +func (v *baseProvider) Verify(ctx context.RequestContext) {} + +func (v *baseProvider) WithBody() bool { + return false +} diff --git a/egofilters/http/security/verifier/consts.go b/egofilters/http/security/verifier/consts.go new file mode 100644 index 0000000..0917139 --- /dev/null +++ b/egofilters/http/security/verifier/consts.go @@ -0,0 +1,16 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package verifier + +const ( + userIDHeader = "x-custom-userid" + + requestIDHeader = "X-Request-Id" + userAgentHeader = "User-Agent" + authorizationHeader = "Authorization" + contentTypeHeader = "Content-Type" + cacheControlHeader = "Cache-Control" +) diff --git a/egofilters/http/security/verifier/custom_hmac_provider.go b/egofilters/http/security/verifier/custom_hmac_provider.go new file mode 100644 index 0000000..85ee8ea --- /dev/null +++ b/egofilters/http/security/verifier/custom_hmac_provider.go @@ -0,0 +1,264 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package verifier + +import ( + "encoding/json" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/grab/ego/ego/src/go/envoy" + + "github.com/grab/ego/egofilters/http/security/context" + securityhttp "github.com/grab/ego/egofilters/http/security/http" + pb "github.com/grab/ego/egofilters/http/security/proto" +) + +const hmacUserIDSessionKey = "UserID" + +type getCurrentTimeOpt func() time.Time + +func getCurrentTime() time.Time { + return time.Now() +} + +// CreateCustomHMACProvider ... +func CreateCustomHMACProvider(provider *pb.CustomHMACProvider) (*customHMACProvider, error) { + client := &http.Client{ + Timeout: 10 * time.Second, + } + return createCustomHMACProvider(provider, securityhttp.NewHttpClientWithCtx(client), getCurrentTime, isValidSignature) +} + +func createCustomHMACProvider( + provider *pb.CustomHMACProvider, client securityhttp.HttpClientWithCtx, getCurrentTime getCurrentTimeOpt, isValidSignature isValidHMACSignatureOpt) (*customHMACProvider, error) { + return &customHMACProvider{ + provider: provider, + client: client, + getCurrentTime: getCurrentTime, + isValidSignature: isValidSignature, + }, nil +} + +type customHMACProvider struct { + baseProvider + provider *pb.CustomHMACProvider + client securityhttp.HttpClientWithCtx + getCurrentTime getCurrentTimeOpt + isValidSignature isValidHMACSignatureOpt +} + +func (v *customHMACProvider) Verify(ctx context.RequestContext) { + // validate headers. + parts := strings.SplitN(string(ctx.Headers().Authorization().Copy()), ":", 3) + if 2 != len(parts) { + // TODO: this should go to request log + ctx.Logger().Error("[Verify] invalid token length. Expected length of 2.", len(parts)) + v.reportUnauthorizedError(ctx) + return + } + + v.checkHMACSignature(ctx, parts) +} + +func (v *customHMACProvider) checkHMACSignature(ctx context.RequestContext, parts []string) { + + serviceKey := ctx.GetSecret(v.provider.ServiceKey) + serviceToken := ctx.GetSecret(v.provider.ServiceToken) + + url, err := url.Parse(v.provider.RequestValidationUrl) + if err != nil { + ctx.Logger().Error("[Verify] can't parse url.", url, err) + v.reportInternalError(ctx) + return + } + + request, err := http.NewRequestWithContext(ctx.GoContext(), http.MethodPost, url.String(), ctx.BodyReader()) + if err != nil { + ctx.Logger().Error("[Verify] can't new http request with context.", err) + v.reportInternalError(ctx) + return + } + + userID, signature := parts[0], parts[1] + + request.Header.Set("Authorization", "Token "+serviceKey+" "+serviceToken) + request.Header.Set("Cache-Control", "no-cache") + request.Header.Set("Content-Type", ctx.Headers().ContentType().Copy()) + + request.Header.Set("X-Custom-Auth-Date", ctx.Headers().Get("Date").Copy()) + request.Header.Set("X-Custom-Auth-Path", ctx.Headers().Path().Copy()) + request.Header.Set("X-Custom-Auth-Signature", signature) + request.Header.Set("X-Custom-Auth-Verb", ctx.Headers().Method().Copy()) + request.Header.Set(requestIDHeader, ctx.Headers().Get(requestIDHeader).Copy()) + + spanName := "" + if v.provider.TracingEnabled { + spanName = "custom_hmac_verify" + } + + httpResponse, httpErr := v.client.DoWithTracing(ctx, request, spanName) + v.handleHMACSignatureCheckResponse(ctx, userID, httpResponse, httpErr) +} + +func (v *customHMACProvider) handleHMACSignatureCheckResponse(ctx context.RequestContext, userID string, httpResponse *http.Response, httpErr error) { + if httpErr != nil { + ctx.Logger().Error("[Verify] error while calling custom auth provider.", httpErr) + v.reportInternalError(ctx) + return + } + + if httpResponse == nil { + ctx.Logger().Error("[Verify] nil response from custom auth provider.") + v.reportInternalError(ctx) + return + } + defer httpResponse.Body.Close() + + ctx.Logger().Debug("[Verify] status code from custom auth provider", httpResponse.StatusCode) + + valid, err := v.isValidSignature(httpResponse) + if err != nil { + ctx.Logger().Error("[Verify] can't validate signature check response.", err) + v.reportInternalError(ctx) + return + } + + if valid { + supportedHeaders := map[string]string{ + userIDHeader: userID, + } + + resp := context.AuthResponseOK() + resp.HeadersToSet = make(map[string]string) + resp.HeadersToRemove = make(map[string]struct{}) + + for _, h := range v.provider.GeneratedUpstreamHeaders { + if v, ok := supportedHeaders[h]; ok { + resp.HeadersToSet[h] = v + resp.HeadersToRemove[h] = struct{}{} + } + } + + resp.FilterState = map[string]string{ + hmacUserIDSessionKey: userID, + } + ctx.Callbacks().OnComplete(resp) + return + } + + v.reportUnauthorizedError(ctx) + return +} + +func (v *customHMACProvider) reportInternalError(ctx context.RequestContext) { + ctx.Callbacks().OnComplete(context.AuthResponseError()) +} + +func (v *customHMACProvider) reportUnauthorizedError(ctx context.RequestContext) { + ctx.Callbacks().OnComplete(context.AuthResponseUnauthorized()) +} + +func (v *customHMACProvider) WithBody() bool { + return true +} + +// CustomHMACSignResponse ... +type CustomHMACSignResponse struct { + Signature string `json:"signature"` +} + +func (v *customHMACProvider) Sign(ctx context.ResponseContext) { + serviceKey := ctx.GetSecret(v.provider.ServiceKey) + serviceToken := ctx.GetSecret(v.provider.ServiceToken) + + signURL, err := url.Parse(v.provider.ResponseSigningUrl) + if err != nil { + ctx.Logger().Error("[Sign] can't parse ResponseSignURL.", err) + ctx.Callbacks().OnCompleteSigning(context.SignResponse{ + StatusCode: http.StatusInternalServerError, + }) + return + } + signReq, err := http.NewRequestWithContext(ctx.GoContext(), http.MethodPost, signURL.String(), ctx.BodyReader()) + if err != nil { + ctx.Logger().Error("[Sign] can't create sign request.", err) + ctx.Callbacks().OnCompleteSigning(context.SignResponse{ + StatusCode: http.StatusInternalServerError, + }) + return + } + // golang will format to UTC by default so here need to force the timezone info to GMT + // RFC1123 is the preferred time format for RFC7231, + // eg. Sun, 06 Nov 1994 08:49:37 GMT + signedTime := v.getCurrentTime().In(time.FixedZone("GMT", 0)).Format(time.RFC1123) + signReq.Header.Set("X-Custom-Auth-Date", signedTime) + signReq.Header.Set("Authorization", "Token "+serviceKey+" "+serviceToken) + signReq.Header.Set("X-Custom-Auth-Status-Code", ctx.Headers().Status().Copy()) + + signReq.Header.Set(requestIDHeader, ctx.RequestHeaders().Get(requestIDHeader).Copy()) + + spanName := "" + if v.provider.TracingEnabled { + spanName = "custom_hmac_sign" + } + + resp, err := v.client.DoWithTracing(ctx, signReq, spanName) + + if err != nil { + ctx.Logger().Error("[Sign] can't send sign request.", err) + ctx.Callbacks().OnCompleteSigning(context.SignResponse{ + StatusCode: http.StatusInternalServerError, + }) + return + } + + if resp == nil { + ctx.Logger().Error("[Sign] empty sign response boy.", err) + ctx.Callbacks().OnCompleteSigning(context.SignResponse{ + StatusCode: http.StatusInternalServerError, + }) + return + } + defer resp.Body.Close() + + signResponse := &CustomHMACSignResponse{} + if resp.StatusCode != http.StatusOK { + ctx.Logger().Warn("[Sign] unknown status code", resp.StatusCode) + } + + err = json.NewDecoder(resp.Body).Decode(signResponse) + if err != nil { + ctx.Logger().Error("[Sign] can't unmarshal response.", err) + ctx.Callbacks().OnCompleteSigning(context.SignResponse{ + StatusCode: http.StatusInternalServerError, + }) + return + } + + ctx.Callbacks().OnCompleteSigning(context.SignResponse{ + HeadersToSet: map[string]string{ + "X-Custom-Auth-Signature-HMAC-SHA256": signResponse.Signature, + "Date": signedTime, + }, + }) +} + +func (v *customHMACProvider) SigningRequired(headers envoy.ResponseHeaderMap, authResp context.AuthResponse) bool { + if nil == headers || nil == authResp.FilterState || "" == authResp.FilterState[hmacUserIDSessionKey] { + return false + } + + status, err := strconv.ParseUint(string(headers.Status()), 10, 64) + if err != nil { + return false + } + + return v.provider.SignResp && status < http.StatusInternalServerError +} diff --git a/egofilters/http/security/verifier/custom_hmac_provider_factory_test.go b/egofilters/http/security/verifier/custom_hmac_provider_factory_test.go new file mode 100644 index 0000000..2e93702 --- /dev/null +++ b/egofilters/http/security/verifier/custom_hmac_provider_factory_test.go @@ -0,0 +1,23 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package verifier + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + pb "github.com/grab/ego/egofilters/http/security/proto" +) + +func TestCreateCustomHMACProvider(t *testing.T) { + provider, err := CreateCustomHMACProvider(&pb.CustomHMACProvider{}) + + require.Nil(t, err) + assert.NotNil(t, provider) + assert.NotNil(t, provider.getCurrentTime()) +} diff --git a/egofilters/http/security/verifier/custom_hmac_provider_sign_required_test.go b/egofilters/http/security/verifier/custom_hmac_provider_sign_required_test.go new file mode 100644 index 0000000..24b1991 --- /dev/null +++ b/egofilters/http/security/verifier/custom_hmac_provider_sign_required_test.go @@ -0,0 +1,158 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package verifier + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grab/ego/ego/src/go/volatile" + + "github.com/grab/ego/egofilters/http/security/context" + pb "github.com/grab/ego/egofilters/http/security/proto" + + httpmocks "github.com/grab/ego/egofilters/mock/gen/http/security/http" + envoymocks "github.com/grab/ego/ego/test/go/mock/gen/envoy" +) + +func TestSignRequired(t *testing.T) { + + tcs := []struct { + name string + statusCode string + config *pb.CustomHMACProvider + authResp context.AuthResponse + + signRequired bool + }{ + { + name: "should return true if everything is good", + statusCode: "200", + config: &pb.CustomHMACProvider{ + SignResp: true, + }, + authResp: context.AuthResponse{ + FilterState: map[string]string{hmacUserIDSessionKey: "123"}, + }, + + signRequired: true, + }, + { + name: "should return false if SignResp configuration is false", + statusCode: "200", + config: &pb.CustomHMACProvider{ + SignResp: false, + }, + authResp: context.AuthResponse{ + FilterState: map[string]string{hmacUserIDSessionKey: "123"}, + }, + + signRequired: false, + }, + { + name: "should return false if nil FilterState", + statusCode: "200", + config: &pb.CustomHMACProvider{ + SignResp: true, + }, + authResp: context.AuthResponse{}, + + signRequired: false, + }, + { + name: "should return false if missing hmacUserIDSessionKey", + statusCode: "200", + config: &pb.CustomHMACProvider{ + SignResp: true, + }, + authResp: context.AuthResponse{ + FilterState: map[string]string{}, + }, + + signRequired: false, + }, + { + name: "should return false if empty hmacUserIDSessionKey", + statusCode: "200", + config: &pb.CustomHMACProvider{ + SignResp: true, + }, + authResp: context.AuthResponse{ + FilterState: map[string]string{hmacUserIDSessionKey: ""}, + }, + + signRequired: false, + }, + { + name: "should return false if statusCode (500) >= 500", + statusCode: "500", + config: &pb.CustomHMACProvider{ + SignResp: true, + }, + authResp: context.AuthResponse{ + FilterState: map[string]string{hmacUserIDSessionKey: "123"}, + }, + + signRequired: false, + }, + + { + name: "should return false if statusCode (504) >= 500 ", + statusCode: "504", + config: &pb.CustomHMACProvider{ + SignResp: true, + }, + authResp: context.AuthResponse{ + FilterState: map[string]string{hmacUserIDSessionKey: "123"}, + }, + + signRequired: false, + }, + { + name: "should return false if invalid status code", + statusCode: "invalid", + config: &pb.CustomHMACProvider{ + SignResp: true, + }, + authResp: context.AuthResponse{ + FilterState: map[string]string{hmacUserIDSessionKey: "123"}, + }, + + signRequired: false, + }, + } + + for _, val := range tcs { + tc := val + t.Run(tc.name, func(t *testing.T) { + signer, err := createCustomHMACProvider(tc.config, &httpmocks.HttpClientWithCtx{}, nil, nil) + require.Nil(t, err) + + responseHeader := &envoymocks.ResponseHeaderMap{} + responseHeader.On("Status").Return(volatile.String(tc.statusCode)) + result := signer.SigningRequired(responseHeader, tc.authResp) + + assert.Equal(t, tc.signRequired, result) + }) + } +} + +func TestSigningRequiredReturnFalseIfNilResponseHeader(t *testing.T) { + config := &pb.CustomHMACProvider{ + SignResp: true, + } + signer, err := createCustomHMACProvider(config, &httpmocks.HttpClientWithCtx{}, nil, nil) + require.Nil(t, err) + + authResp := context.AuthResponse{ + FilterState: map[string]string{hmacUserIDSessionKey: "123"}, + } + result := signer.SigningRequired(nil, authResp) + + assert.Equal(t, false, result) +} diff --git a/egofilters/http/security/verifier/custom_hmac_provider_sign_test.go b/egofilters/http/security/verifier/custom_hmac_provider_sign_test.go new file mode 100644 index 0000000..8bd35ca --- /dev/null +++ b/egofilters/http/security/verifier/custom_hmac_provider_sign_test.go @@ -0,0 +1,328 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package verifier + +import ( + "bytes" + gocontext "context" + "errors" + "io/ioutil" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/grab/ego/ego/src/go/logger" + "github.com/grab/ego/ego/src/go/volatile" + egomocks "github.com/grab/ego/ego/test/go/mock" + + "github.com/grab/ego/egofilters/http/security/context" + pb "github.com/grab/ego/egofilters/http/security/proto" + + contextmocks "github.com/grab/ego/egofilters/mock/gen/http/security/context" + httpmocks "github.com/grab/ego/egofilters/mock/gen/http/security/http" + envoymocks "github.com/grab/ego/ego/test/go/mock/gen/envoy" +) + +func TestHMACSignResponse(t *testing.T) { + + tcs := []struct { + name string + config *pb.CustomHMACProvider + requestID string + responseStatusCode string + responseBody string + authResp context.AuthResponse + customAuthResponseStatusCode int + customAuthResponseBody string + httpErr error + currentTime time.Time + goCtx gocontext.Context + + headersToCustomAuth http.Header + bodyToCustomAuth string + spanName string + signResult context.SignResponse + }{ + { + name: "should call custom auth provider to sign response", + config: &pb.CustomHMACProvider{ + ResponseSigningUrl: "http://custom-auth.example.com", + ServiceToken: "service_token", + ServiceKey: "service_key", + TracingEnabled: true, + }, + requestID: "request1", + responseStatusCode: "200", + responseBody: "this is body from upstream", + + authResp: context.AuthResponse{ + FilterState: map[string]string{hmacUserIDSessionKey: "partner_id1"}, + }, + customAuthResponseBody: `{"signature": "hmac_signature"}`, + customAuthResponseStatusCode: http.StatusOK, + currentTime: time.Date(2000, time.January, 1, 2, 3, 4, 5, time.UTC), + goCtx: gocontext.WithValue(gocontext.Background(), "key", "val"), + + headersToCustomAuth: http.Header{ + "Authorization": []string{"Token decrypted_service_key decrypted_service_token"}, + "X-Custom-Auth-Date": []string{"Sat, 01 Jan 2000 02:03:04 GMT"}, + "X-Custom-Auth-Status-Code": []string{"200"}, + "X-Request-Id": []string{"request1"}, + }, + bodyToCustomAuth: "this is body from upstream", + spanName: "custom_hmac_sign", + signResult: context.SignResponse{ + HeadersToSet: map[string]string{"Date": "Sat, 01 Jan 2000 02:03:04 GMT", "X-Custom-Auth-Signature-HMAC-SHA256": "hmac_signature"}, + }, + }, + { + name: "should call custom auth provider to sign response with different configuration", + config: &pb.CustomHMACProvider{ + ResponseSigningUrl: "http://custom-auth-provider.example.com", + ServiceToken: "service_token2", + ServiceKey: "service_key2", + TracingEnabled: false, + }, + requestID: "request2", + responseStatusCode: "400", + responseBody: "this is body from upstream 2", + authResp: context.AuthResponse{ + FilterState: map[string]string{hmacUserIDSessionKey: "partner_id2"}, + }, + customAuthResponseBody: `{"signature": "hmac_signature2"}`, + customAuthResponseStatusCode: http.StatusOK, + currentTime: time.Date(2002, time.January, 2, 2, 3, 4, 5, time.UTC), + goCtx: gocontext.WithValue(gocontext.Background(), "key2", "val2"), + + headersToCustomAuth: http.Header{ + "Authorization": []string{"Token decrypted_service_key2 decrypted_service_token2"}, + "X-Custom-Auth-Date": []string{"Wed, 02 Jan 2002 02:03:04 GMT"}, + "X-Custom-Auth-Status-Code": []string{"400"}, + "X-Request-Id": []string{"request2"}, + }, + bodyToCustomAuth: "this is body from upstream 2", + spanName: "", + signResult: context.SignResponse{ + HeadersToSet: map[string]string{"Date": "Wed, 02 Jan 2002 02:03:04 GMT", "X-Custom-Auth-Signature-HMAC-SHA256": "hmac_signature2"}, + }, + }, + { + name: "should return 500 if can't parse ResponseSigningUrl", + config: &pb.CustomHMACProvider{ + ResponseSigningUrl: ":zzz", + ServiceToken: "service_token", + ServiceKey: "service_key", + }, + responseStatusCode: "200", + responseBody: "this is body from upstream", + authResp: context.AuthResponse{ + FilterState: map[string]string{hmacUserIDSessionKey: "partner_id1"}, + }, + customAuthResponseBody: `{"signature": "hmac_signature"}`, + customAuthResponseStatusCode: http.StatusOK, + currentTime: time.Date(2000, time.January, 1, 2, 3, 4, 5, time.UTC), + goCtx: gocontext.WithValue(gocontext.Background(), "key", "val"), + + signResult: context.SignResponse{ + StatusCode: http.StatusInternalServerError, + }, + }, + { + name: "should return 500 if can't call custom auth provider", + config: &pb.CustomHMACProvider{ + ResponseSigningUrl: "http://invalid.example.com", + ServiceToken: "service_token", + ServiceKey: "service_key", + }, + responseStatusCode: "200", + responseBody: "this is body from upstream", + authResp: context.AuthResponse{ + FilterState: map[string]string{hmacUserIDSessionKey: "partner_id1"}, + }, + httpErr: errors.New("can't call custom auth provider"), + currentTime: time.Date(2000, time.January, 1, 2, 3, 4, 5, time.UTC), + goCtx: gocontext.WithValue(gocontext.Background(), "key", "val"), + + signResult: context.SignResponse{ + StatusCode: http.StatusInternalServerError, + }, + }, + { + name: "should return 500 if nil Go context", + config: &pb.CustomHMACProvider{ + ResponseSigningUrl: "http://something.com", + ServiceToken: "service_token", + ServiceKey: "service_key", + }, + responseStatusCode: "200", + responseBody: "this is body from upstream", + authResp: context.AuthResponse{ + FilterState: map[string]string{hmacUserIDSessionKey: "partner_id1"}, + }, + currentTime: time.Date(2000, time.January, 2, 2, 3, 4, 5, time.UTC), + goCtx: nil, + + signResult: context.SignResponse{ + StatusCode: http.StatusInternalServerError, + }, + }, + { + name: "should return 500 if nil response from custom auth provider", + config: &pb.CustomHMACProvider{ + ResponseSigningUrl: "http://something.com", + ServiceToken: "service_token", + ServiceKey: "service_key", + }, + responseStatusCode: "200", + responseBody: "this is body from upstream", + authResp: context.AuthResponse{ + FilterState: map[string]string{hmacUserIDSessionKey: "partner_id1"}, + }, + currentTime: time.Date(2000, time.January, 1, 2, 3, 4, 5, time.UTC), + goCtx: gocontext.WithValue(gocontext.Background(), "key", "val"), + + signResult: context.SignResponse{ + StatusCode: http.StatusInternalServerError, + }, + }, + { + name: "should return 500 if can't parse response from custom auth provider", + config: &pb.CustomHMACProvider{ + ResponseSigningUrl: "http://custom-auth.example.com", + ServiceToken: "service_token", + ServiceKey: "service_key", + }, + responseStatusCode: "200", + responseBody: "this is body from upstream", + authResp: context.AuthResponse{ + FilterState: map[string]string{hmacUserIDSessionKey: "partner_id1"}, + }, + customAuthResponseBody: ``, + customAuthResponseStatusCode: http.StatusOK, + currentTime: time.Date(2000, time.January, 1, 2, 3, 4, 5, time.UTC), + goCtx: gocontext.WithValue(gocontext.Background(), "key", "val"), + + headersToCustomAuth: http.Header{ + "Authorization": []string{"Token decrypted_service_key decrypted_service_token"}, + "X-Custom-Auth-Date": []string{"Sat, 01 Jan 2000 02:03:04 GMT"}, + "X-Custom-Auth-Status-Code": []string{"200"}, + "X-Request-Id": []string{""}, + }, + bodyToCustomAuth: "this is body from upstream", + signResult: context.SignResponse{ + StatusCode: http.StatusInternalServerError, + }, + }, + { + name: "should not crash when custom auth provider return non-200 status code", + config: &pb.CustomHMACProvider{ + ResponseSigningUrl: "http://custom-auth.example.com", + ServiceToken: "service_token", + ServiceKey: "service_key", + }, + responseStatusCode: "200", + responseBody: "this is body from upstream", + authResp: context.AuthResponse{ + FilterState: map[string]string{hmacUserIDSessionKey: "partner_id1"}, + }, + customAuthResponseBody: ``, + customAuthResponseStatusCode: http.StatusBadRequest, + currentTime: time.Date(2000, time.January, 1, 2, 3, 4, 5, time.UTC), + goCtx: gocontext.WithValue(gocontext.Background(), "key", "val"), + + headersToCustomAuth: http.Header{ + "Authorization": []string{"Token decrypted_service_key decrypted_service_token"}, + "X-Custom-Auth-Date": []string{"Sat, 01 Jan 2000 02:03:04 GMT"}, + "X-Custom-Auth-Status-Code": []string{"200"}, + "X-Request-Id": []string{""}, + }, + bodyToCustomAuth: "this is body from upstream", + signResult: context.SignResponse{ + StatusCode: http.StatusInternalServerError, + }, + }, + } + + for _, val := range tcs { + tc := val + t.Run(tc.name, func(t *testing.T) { + responseContext := &contextmocks.ResponseContext{} + responseContext.On("GetSecret", tc.config.ServiceKey).Return("decrypted_" + tc.config.ServiceKey) + responseContext.On("GetSecret", tc.config.ServiceToken).Return("decrypted_" + tc.config.ServiceToken) + + requestHeader := &envoymocks.RequestHeaderMap{} + requestHeader.On("Get", requestIDHeader).Return(volatile.String(tc.requestID)) + responseContext.On("RequestHeaders").Return(requestHeader) + + responseHeader := &envoymocks.ResponseHeaderMap{} + responseHeader.On("Status").Return(volatile.String(tc.responseStatusCode)) + responseContext.On("Headers").Return(responseHeader) + + responseContext.On("GoContext").Return(tc.goCtx) + + bodyReader := bytes.NewReader([]byte(tc.responseBody)) + responseContext.On("BodyReader").Return(bodyReader) + + responseContext.On("AuthResponse").Return(tc.authResp) + + callbacks := &contextmocks.ResponseCallbacks{} + responseContext.On("Callbacks").Return(callbacks) + + // set-up Logger + responseContext.On("Logger").Return(logger.NewLogger("CustomHMACLogger", egomocks.NativeLogger{})) + + // Set-up custom auth provider response + var httpResponse *http.Response + if tc.customAuthResponseStatusCode > 0 || tc.customAuthResponseBody != "" { + httpResponse = &http.Response{StatusCode: tc.customAuthResponseStatusCode, Body: ioutil.NopCloser(strings.NewReader(tc.customAuthResponseBody))} + } + + // set-up HttpClient + httpClient := &httpmocks.HttpClientWithCtx{} + + var actualCustomAuthReq *http.Request + httpClient.On("DoWithTracing", responseContext, mock.AnythingOfType("*http.Request"), tc.spanName).Run(func(args mock.Arguments) { + actualCustomAuthReq = args[1].(*http.Request) + }).Return(httpResponse, tc.httpErr) + + mockGetCurrentTime := func() time.Time { + return tc.currentTime + } + signer, err := createCustomHMACProvider(tc.config, httpClient, mockGetCurrentTime, nil) + require.Nil(t, err) + + var signResp context.SignResponse + callbacks.On("OnCompleteSigning", mock.Anything).Run(func(args mock.Arguments) { + signResp = args[0].(context.SignResponse) + }) + + signer.Sign(responseContext) + + if tc.headersToCustomAuth != nil || tc.bodyToCustomAuth != "" { + require.NotNil(t, actualCustomAuthReq) + assert.Equal(t, http.MethodPost, actualCustomAuthReq.Method) + + // verify URL + assert.Equal(t, tc.config.ResponseSigningUrl, actualCustomAuthReq.URL.String()) + + // verify custom auth provider request headers + assert.Equal(t, tc.headersToCustomAuth, actualCustomAuthReq.Header) + + // verify custom auth provider request body + actualCustomAuthReqBytes, _ := ioutil.ReadAll(actualCustomAuthReq.Body) + assert.Equal(t, tc.bodyToCustomAuth, string(actualCustomAuthReqBytes)) + } + + assert.Equal(t, tc.signResult, signResp) + + }) + } +} diff --git a/egofilters/http/security/verifier/custom_hmac_provider_verify_test.go b/egofilters/http/security/verifier/custom_hmac_provider_verify_test.go new file mode 100644 index 0000000..611396a --- /dev/null +++ b/egofilters/http/security/verifier/custom_hmac_provider_verify_test.go @@ -0,0 +1,468 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package verifier + +import ( + "bytes" + gocontext "context" + "errors" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/grab/ego/ego/src/go/logger" + "github.com/grab/ego/ego/src/go/volatile" + egomocks "github.com/grab/ego/ego/test/go/mock" + + "github.com/grab/ego/egofilters/http/security/context" + pb "github.com/grab/ego/egofilters/http/security/proto" + + "github.com/grab/ego/egofilters/mock/gen/http/security/context" + contextmocks "github.com/grab/ego/egofilters/mock/gen/http/security/context" + httpmocks "github.com/grab/ego/egofilters/mock/gen/http/security/http" + envoymocks "github.com/grab/ego/ego/test/go/mock/gen/envoy" +) + +func TestHMACInvalidToken(t *testing.T) { + + tcs := []struct { + name string + authorizationHeader string + }{ + {name: "empty authorization header", authorizationHeader: ""}, + {name: "authorization header with only 1 part", authorizationHeader: "abc"}, + {name: "authorization header with more than 2 parts", authorizationHeader: "abc:def:ghc"}, + } + + for _, val := range tcs { + tc := val + t.Run(tc.name, func(t *testing.T) { + ctx := &contextmocks.RequestContext{} + + callbacks := &mocks.Callbacks{} + callbacks.On("OnComplete", context.AuthResponseUnauthorized()) + ctx.On("Callbacks").Return(callbacks) + + requetHeaderMap := &envoymocks.RequestHeaderMap{} + requetHeaderMap.On("Authorization").Return(volatile.String(tc.authorizationHeader)) + ctx.On("Headers").Return(requetHeaderMap) + + ctx.On("Logger").Return(logger.NewLogger("CustomHMACLogger", egomocks.NativeLogger{})) + + provider, _ := CreateCustomHMACProvider(&pb.CustomHMACProvider{}) + + provider.Verify(ctx) + + ctx.AssertExpectations(t) + callbacks.AssertExpectations(t) + requetHeaderMap.AssertExpectations(t) + }) + } +} + +func TestHMACVerify(t *testing.T) { + var tcs = []struct { + name string + settings pb.CustomHMACProvider + goCtx gocontext.Context + requestHeaders http.Header + requestBody string + customAuthResp *http.Response + httpErr error + validSignature bool + validationErr error + + headersToCustomAuth http.Header + bodyToCustomAuth string + spanName string + headersToSet map[string]string + headersToRemove map[string]struct{} + filterState map[string]string + authResponse context.AuthResponse + }{ + { + name: "should call custom auth provider with correct request headers and body", + settings: pb.CustomHMACProvider{ + RequestValidationUrl: "https://example.com/xyz", + ServiceKey: "service_key", + ServiceToken: "service_token", + GeneratedUpstreamHeaders: []string{"x-custom-userid"}, + TracingEnabled: true, + }, + goCtx: gocontext.WithValue(gocontext.Background(), "key", "val"), + requestHeaders: http.Header{ + "User-Agent": {""}, + "X-Request-Id": {"request-id1"}, + "Authorization": {"partner_id1:signature1"}, + ":path": {"/path1"}, + ":method": {"GET"}, + "Content-Type": {"application/json"}, + "Date": {"2015/1/1"}, + }, + requestBody: "request_body1", + customAuthResp: &http.Response{StatusCode: http.StatusOK, Body: ioutil.NopCloser(strings.NewReader(""))}, + validSignature: true, + + headersToCustomAuth: http.Header{ + "Cache-Control": {"no-cache"}, + "Content-Type": {"application/json"}, + "Authorization": {"Token decrypted_service_key decrypted_service_token"}, + "X-Request-Id": {"request-id1"}, + "X-Custom-Auth-Date": {"2015/1/1"}, + "X-Custom-Auth-Path": {"/path1"}, + "X-Custom-Auth-Signature": {"signature1"}, + "X-Custom-Auth-Verb": {"GET"}, + }, + bodyToCustomAuth: "request_body1", + spanName: "custom_hmac_verify", + authResponse: context.AuthResponseOK(), + headersToSet: map[string]string{ + "x-custom-userid": "partner_id1", + }, + headersToRemove: map[string]struct{}{ + "x-custom-userid": {}, + }, + filterState: map[string]string{ + "UserID": "partner_id1", + }, + }, + + { + name: "should only add supported generated_upstream_headers to requests to upstreams", + settings: pb.CustomHMACProvider{ + RequestValidationUrl: "https://custom-auth.example.com/xyz", + ServiceKey: "service_key", + ServiceToken: "service_token", + GeneratedUpstreamHeaders: []string{"x-custom-unknown"}, + TracingEnabled: false, + }, + goCtx: gocontext.WithValue(gocontext.Background(), "key", "val"), + requestHeaders: http.Header{ + "User-Agent": {""}, + "X-Request-Id": {"request-id1"}, + "Authorization": {"partner_id1:signature1"}, + ":path": {"/path1"}, + ":method": {"GET"}, + "Content-Type": {"application/json"}, + "Date": {"2015/1/1"}, + }, + requestBody: "request_body1", + spanName: "", + customAuthResp: &http.Response{StatusCode: http.StatusOK, Body: ioutil.NopCloser(strings.NewReader(""))}, + validSignature: true, + + headersToCustomAuth: http.Header{ + "Cache-Control": {"no-cache"}, + "Content-Type": {"application/json"}, + "Authorization": {"Token decrypted_service_key decrypted_service_token"}, + "X-Request-Id": {"request-id1"}, + "X-Custom-Auth-Date": {"2015/1/1"}, + "X-Custom-Auth-Path": {"/path1"}, + "X-Custom-Auth-Signature": {"signature1"}, + "X-Custom-Auth-Verb": {"GET"}, + }, + bodyToCustomAuth: "request_body1", + authResponse: context.AuthResponseOK(), + headersToSet: map[string]string{}, + headersToRemove: map[string]struct{}{}, + filterState: map[string]string{ + "UserID": "partner_id1", + }, + }, + + { + name: "should return 500 status code if can't parse URL", + settings: pb.CustomHMACProvider{ + RequestValidationUrl: ":zzz", + ServiceKey: "service_key2", + ServiceToken: "service_token2", + }, + goCtx: gocontext.WithValue(gocontext.Background(), "key", "val"), + requestHeaders: http.Header{ + "User-Agent": {""}, + "X-Request-Id": {"request-id1"}, + "Authorization": {"partner_id1:signature1"}, + ":path": {"/path1"}, + ":method": {"GET"}, + "Content-Type": {"application/json"}, + "Date": {"2015/1/1"}, + }, + + authResponse: context.AuthResponseError(), + }, + + { + name: "should return 500 status code if nill Go context", + settings: pb.CustomHMACProvider{ + RequestValidationUrl: "http://something.com", + ServiceKey: "service_key2", + ServiceToken: "service_token2", + }, + requestHeaders: http.Header{ + "User-Agent": {""}, + "X-Request-Id": {"request-id1"}, + "Authorization": {"partner_id1:signature1"}, + ":path": {"/path1"}, + ":method": {"GET"}, + "Content-Type": {"application/json"}, + "Date": {"2015/1/"}, + }, + + authResponse: context.AuthResponseError(), + }, + + { + name: "should return 500 status code if can't call custom auth provider", + settings: pb.CustomHMACProvider{ + RequestValidationUrl: "https://example.com/xyz", + ServiceKey: "service_key1", + ServiceToken: "service_token1", + }, + goCtx: gocontext.WithValue(gocontext.Background(), "key", "val"), + requestBody: "request_body1", + requestHeaders: http.Header{ + "User-Agent": {""}, + "X-Request-Id": {"request-id1"}, + "Authorization": {"partner_id1:signature1"}, + ":path": {"/path1"}, + ":method": {"GET"}, + "Content-Type": {"application/json"}, + "Date": {"2015/1/1"}, + }, + httpErr: errors.New("can't call custom auth provider"), + + headersToCustomAuth: http.Header{ + "Cache-Control": {"no-cache"}, + "Content-Type": {"application/json"}, + "Authorization": {"Token decrypted_service_key1 decrypted_service_token1"}, + "X-Request-Id": {"request-id1"}, + "X-Custom-Auth-Date": {"2015/1/1"}, + "X-Custom-Auth-Path": {"/path1"}, + "X-Custom-Auth-Signature": {"signature1"}, + "X-Custom-Auth-Verb": {"GET"}, + }, + bodyToCustomAuth: "request_body1", + + authResponse: context.AuthResponseError(), + }, + + { + name: "should return 500 status code if nil response from custom auth provider", + settings: pb.CustomHMACProvider{ + RequestValidationUrl: "https://example.com/xyz", + ServiceKey: "service_key1", + ServiceToken: "service_token1", + }, + goCtx: gocontext.WithValue(gocontext.Background(), "key", "val"), + requestBody: "request_body1", + requestHeaders: http.Header{ + "User-Agent": {""}, + "X-Request-Id": {"request-id1"}, + "Authorization": {"partner_id1:signature1"}, + ":path": {"/path1"}, + ":method": {"GET"}, + "Content-Type": {"application/json"}, + "Date": {"2015/1/1"}, + }, + + headersToCustomAuth: http.Header{ + "Cache-Control": {"no-cache"}, + "Content-Type": {"application/json"}, + "Authorization": {"Token decrypted_service_key1 decrypted_service_token1"}, + "X-Request-Id": {"request-id1"}, + "X-Custom-Auth-Date": {"2015/1/1"}, + "X-Custom-Auth-Path": {"/path1"}, + "X-Custom-Auth-Signature": {"signature1"}, + "X-Custom-Auth-Verb": {"GET"}, + }, + bodyToCustomAuth: "request_body1", + + authResponse: context.AuthResponseError(), + }, + + { + name: "should return 500 if validator returns error", + settings: pb.CustomHMACProvider{ + RequestValidationUrl: "https://example.com/xyz", + ServiceKey: "service_key", + ServiceToken: "service_token", + GeneratedUpstreamHeaders: []string{"x-custom-userid"}, + }, + goCtx: gocontext.WithValue(gocontext.Background(), "key", "val"), + requestHeaders: http.Header{ + "User-Agent": {""}, + "X-Request-Id": {"request-id1"}, + "Authorization": {"partner_id1:signature1"}, + ":path": {"/path1"}, + ":method": {"GET"}, + "Content-Type": {"application/json"}, + "Date": {"2015/1/1"}, + }, + requestBody: "request_body1", + customAuthResp: &http.Response{StatusCode: http.StatusOK, Body: ioutil.NopCloser(strings.NewReader(""))}, + validationErr: errors.New("unknown error"), + + headersToCustomAuth: http.Header{ + "Cache-Control": {"no-cache"}, + "Content-Type": {"application/json"}, + "Authorization": {"Token decrypted_service_key decrypted_service_token"}, + "X-Request-Id": {"request-id1"}, + "X-Custom-Auth-Date": {"2015/1/1"}, + "X-Custom-Auth-Path": {"/path1"}, + "X-Custom-Auth-Signature": {"signature1"}, + "X-Custom-Auth-Verb": {"GET"}, + }, + + bodyToCustomAuth: "request_body1", + authResponse: context.AuthResponseError(), + }, + + { + name: "should return 401 if validator return false", + settings: pb.CustomHMACProvider{ + RequestValidationUrl: "https://example.com/xyz", + ServiceKey: "service_key", + ServiceToken: "service_token", + GeneratedUpstreamHeaders: []string{"x-custom-userid"}, + }, + goCtx: gocontext.WithValue(gocontext.Background(), "key", "val"), + requestHeaders: http.Header{ + "User-Agent": {""}, + "X-Request-Id": {"request-id1"}, + "Authorization": {"partner_id1:signature1"}, + ":path": {"/path1"}, + ":method": {"GET"}, + "Content-Type": {"application/json"}, + "Date": {"2015/1/1"}, + }, + requestBody: "request_body1", + customAuthResp: &http.Response{StatusCode: http.StatusOK, Body: ioutil.NopCloser(strings.NewReader(""))}, + validSignature: false, + + headersToCustomAuth: http.Header{ + "Cache-Control": {"no-cache"}, + "Content-Type": {"application/json"}, + "Authorization": {"Token decrypted_service_key decrypted_service_token"}, + "X-Request-Id": {"request-id1"}, + "X-Custom-Auth-Date": {"2015/1/1"}, + "X-Custom-Auth-Path": {"/path1"}, + "X-Custom-Auth-Signature": {"signature1"}, + "X-Custom-Auth-Verb": {"GET"}, + }, + + bodyToCustomAuth: "request_body1", + authResponse: context.AuthResponseUnauthorized(), + }, + } + + for _, val := range tcs { + tc := val + t.Run(tc.name, func(t *testing.T) { + ctx := &contextmocks.RequestContext{} + ctx.On("GoContext").Return(tc.goCtx) + ctx.On("GetSecret", tc.settings.ServiceKey).Return("decrypted_" + tc.settings.ServiceKey) + ctx.On("GetSecret", tc.settings.ServiceToken).Return("decrypted_" + tc.settings.ServiceToken) + + callbacks := &mocks.Callbacks{} + ctx.On("Callbacks").Return(callbacks) + + bodyReader := bytes.NewReader([]byte(tc.requestBody)) + ctx.On("BodyReader").Return(bodyReader) + + // set-up HttpClient + httpClient := &httpmocks.HttpClientWithCtx{} + + var actualCustomAuthReq *http.Request + httpClient.On("DoWithTracing", ctx, mock.AnythingOfType("*http.Request"), tc.spanName).Run(func(args mock.Arguments) { + actualCustomAuthReq = args[1].(*http.Request) + }).Return(tc.customAuthResp, tc.httpErr) + + // set-up HeaderMap + headerMap := &envoymocks.RequestHeaderMap{} + ctx.On("Headers").Return(headerMap) + + if len(tc.requestHeaders.Get("Authorization")) > 0 { + headerMap.On("Authorization").Return(volatile.String(tc.requestHeaders.Get("Authorization"))) + } + + if len(tc.requestHeaders.Get(":path")) > 0 { + headerMap.On("Path").Return(volatile.String(tc.requestHeaders.Get(":path"))) + } + + if len(tc.requestHeaders.Get(":method")) > 0 { + headerMap.On("Method").Return(volatile.String(tc.requestHeaders.Get(":method"))) + } + + if len(tc.requestHeaders.Get("Content-Type")) > 0 { + headerMap.On("ContentType").Return(volatile.String(tc.requestHeaders.Get("Content-Type"))) + } + + if len(tc.requestHeaders.Get("Date")) > 0 { + headerMap.On("Date").Return(volatile.String(tc.requestHeaders.Get("Date"))) + } + + for k, vals := range tc.requestHeaders { + for _, val := range vals { + headerMap.On("Get", k).Return(volatile.String(val)) + } + } + // return emtpy if no x-request-id in headers + headerMap.On("Get", "X-Request-Id").Return(volatile.String("")) + + // set-up Logger + ctx.On("Logger").Return(logger.NewLogger("CustomHMACLogger", egomocks.NativeLogger{})) + + var validatorParam *http.Response + isValid := func(resp *http.Response) (bool, error) { + validatorParam = resp + return tc.validSignature, tc.validationErr + } + + provider, _ := createCustomHMACProvider(&tc.settings, httpClient, getCurrentTime, isValid) + + var authResp context.AuthResponse + callbacks.On("OnComplete", mock.Anything).Run(func(args mock.Arguments) { + authResp = args[0].(context.AuthResponse) + }) + + // Call verify function + provider.Verify(ctx) + + // verify WithBody is always true + assert.Equal(t, provider.WithBody(), true) + + ctx.AssertCalled(t, "GetSecret", tc.settings.ServiceKey) + ctx.AssertCalled(t, "GetSecret", tc.settings.ServiceToken) + if tc.headersToCustomAuth != nil || tc.bodyToCustomAuth != "" || tc.httpErr != nil { + require.NotNil(t, actualCustomAuthReq) + assert.Equal(t, http.MethodPost, actualCustomAuthReq.Method) + + // verify custom auth provider request headers + assert.Equal(t, tc.headersToCustomAuth, actualCustomAuthReq.Header) + + // verify custom auth provider request body + actualCustomAuthReqBytes, _ := ioutil.ReadAll(actualCustomAuthReq.Body) + + assert.Equal(t, tc.bodyToCustomAuth, string(actualCustomAuthReqBytes)) + } + + // Verify params to validator + assert.Same(t, tc.customAuthResp, validatorParam) + + // verify response + expectedResponse := tc.authResponse + expectedResponse.HeadersToSet = tc.headersToSet + expectedResponse.HeadersToRemove = tc.headersToRemove + expectedResponse.FilterState = tc.filterState + assert.Equal(t, expectedResponse, authResp) + }) + } +} diff --git a/egofilters/http/security/verifier/custom_hmac_validator.go b/egofilters/http/security/verifier/custom_hmac_validator.go new file mode 100644 index 0000000..e2d1dfb --- /dev/null +++ b/egofilters/http/security/verifier/custom_hmac_validator.go @@ -0,0 +1,36 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package verifier + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type hmacSignatureValidationResponse struct { + Valid bool `json:"valid"` +} + +type isValidHMACSignatureOpt func(resp *http.Response) (bool, error) + +func isValidSignature(resp *http.Response) (bool, error) { + if resp.StatusCode >= http.StatusInternalServerError { + return false, fmt.Errorf("5xx (%v) status code", resp.StatusCode) + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized { + return false, nil + } + + result := hmacSignatureValidationResponse{} + err := json.NewDecoder(resp.Body).Decode(&result) + if err != nil { + return false, fmt.Errorf("can't parse HMAC signature check response: %v", err) + } + + return result.Valid, nil +} diff --git a/egofilters/http/security/verifier/custom_hmac_validator_test.go b/egofilters/http/security/verifier/custom_hmac_validator_test.go new file mode 100644 index 0000000..006e71a --- /dev/null +++ b/egofilters/http/security/verifier/custom_hmac_validator_test.go @@ -0,0 +1,141 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package verifier + +import ( + "errors" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + pb "github.com/grab/ego/egofilters/http/security/proto" +) + +func TestIsValidSignature(t *testing.T) { + tcs := []struct { + name string + response *http.Response + + valid bool + err error + }{ + { + name: "should return valid if get 200 status code and valid response", + response: &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(strings.NewReader(`{"Valid": true}`)), + }, + + valid: true, + }, + { + name: "should return invalid if get 200 status code, but invalid response", + response: &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(strings.NewReader(`{"Valid": false}`)), + }, + + valid: false, + }, + { + name: "should return valid if get 401 status code, but valid response", + response: &http.Response{ + StatusCode: http.StatusUnauthorized, + Body: ioutil.NopCloser(strings.NewReader(`{"Valid": true}`)), + }, + + valid: true, + }, + { + name: "should return invalid if get 401 status code and invalid response", + response: &http.Response{ + StatusCode: http.StatusUnauthorized, + Body: ioutil.NopCloser(strings.NewReader(`{"Valid": false}`)), + }, + + valid: false, + }, + { + name: "should return invalid if get 401 status code and empty response", + response: &http.Response{ + StatusCode: http.StatusUnauthorized, + Body: ioutil.NopCloser(strings.NewReader(`{}`)), + }, + + valid: false, + }, + { + name: "should return error if status code >= 500", + response: &http.Response{ + StatusCode: http.StatusInternalServerError, + Body: ioutil.NopCloser(strings.NewReader("{}")), + }, + + err: errors.New("5xx (500) status code"), + }, + { + name: "should return error if status code >= 500", + response: &http.Response{ + StatusCode: http.StatusGatewayTimeout, + Body: ioutil.NopCloser(strings.NewReader("{}")), + }, + + err: errors.New("5xx (504) status code"), + }, + { + name: "should return error if can't parse body", + response: &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(strings.NewReader("")), + }, + + err: errors.New("can't parse HMAC signature check response: EOF"), + }, + { + name: "should return invalid if get 4xx status", + response: &http.Response{ + StatusCode: http.StatusBadRequest, + Body: ioutil.NopCloser(strings.NewReader(`{"Valid": true}`)), + }, + + valid: false, + }, + { + name: "should return invalid if get 3xx status", + response: &http.Response{ + StatusCode: http.StatusPermanentRedirect, + Body: ioutil.NopCloser(strings.NewReader(`{"Valid": true}`)), + }, + + valid: false, + }, + { + name: "should return invalid if get 2xx (exclude 200 and 204) status", + response: &http.Response{ + StatusCode: http.StatusAccepted, + Body: ioutil.NopCloser(strings.NewReader(`{"Valid": true}`)), + }, + + valid: false, + }, + } + + for _, tmp := range tcs { + tc := tmp + t.Run(tc.name, func(t *testing.T) { + provider, err := CreateCustomHMACProvider(&pb.CustomHMACProvider{}) + require.Nil(t, err) + + valid, err := provider.isValidSignature(tc.response) + assert.Equal(t, tc.err, err) + assert.Equal(t, tc.valid, valid) + }) + } +} diff --git a/egofilters/http/security/verifier/verifier.go b/egofilters/http/security/verifier/verifier.go new file mode 100644 index 0000000..34eb9b2 --- /dev/null +++ b/egofilters/http/security/verifier/verifier.go @@ -0,0 +1,25 @@ +// Copyright 2020-2021 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// +// Use of this source code is governed by the Apache License 2.0 that can be +// found in the LICENSE file + +package verifier + +import ( + "github.com/grab/ego/ego/src/go/envoy" + + "github.com/grab/ego/egofilters/http/security/context" +) + +// Verifier ... +type Verifier interface { + Verify(context.RequestContext) + WithBody() bool +} + +// Signer ... +type Signer interface { + // Clients have to check if SigningRequired before calling Sign. + Sign(context.ResponseContext) + SigningRequired(headers envoy.ResponseHeaderMap, authResp context.AuthResponse) bool +} diff --git a/egofilters/mock/BUILD.bazel b/egofilters/mock/BUILD.bazel new file mode 100644 index 0000000..6924eaf --- /dev/null +++ b/egofilters/mock/BUILD.bazel @@ -0,0 +1,8 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["doc.go"], + importpath = "github.com/grab/ego/egofilters/mock", + visibility = ["//visibility:public"], +) diff --git a/egofilters/mock/doc.go b/egofilters/mock/doc.go new file mode 100644 index 0000000..92bd9bc --- /dev/null +++ b/egofilters/mock/doc.go @@ -0,0 +1,3 @@ +package mock + +//go:generate mockery --all --recursive=true --keeptree --case=underscore --output ./gen --dir ../ diff --git a/egofilters/mock/gen/http/security/context/BUILD.bazel b/egofilters/mock/gen/http/security/context/BUILD.bazel new file mode 100644 index 0000000..0c14dc1 --- /dev/null +++ b/egofilters/mock/gen/http/security/context/BUILD.bazel @@ -0,0 +1,20 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "callbacks.go", + "context.go", + "request_context.go", + "response_callbacks.go", + "response_context.go", + ], + importpath = "github.com/grab/ego/egofilters/mock/gen/http/security/context", + visibility = ["//visibility:public"], + deps = [ + "//ego/src/go/envoy:go_default_library", + "//ego/src/go/logger:go_default_library", + "//egofilters/http/security/context:go_default_library", + "@com_github_stretchr_testify//mock:go_default_library", + ], +) diff --git a/egofilters/mock/gen/http/security/context/callbacks.go b/egofilters/mock/gen/http/security/context/callbacks.go new file mode 100644 index 0000000..d01c535 --- /dev/null +++ b/egofilters/mock/gen/http/security/context/callbacks.go @@ -0,0 +1,18 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + context "github.com/grab/ego/egofilters/http/security/context" + mock "github.com/stretchr/testify/mock" +) + +// Callbacks is an autogenerated mock type for the Callbacks type +type Callbacks struct { + mock.Mock +} + +// OnComplete provides a mock function with given fields: _a0 +func (_m *Callbacks) OnComplete(_a0 context.AuthResponse) { + _m.Called(_a0) +} diff --git a/egofilters/mock/gen/http/security/context/context.go b/egofilters/mock/gen/http/security/context/context.go new file mode 100644 index 0000000..c6ea708 --- /dev/null +++ b/egofilters/mock/gen/http/security/context/context.go @@ -0,0 +1,29 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + envoy "github.com/grab/ego/ego/src/go/envoy" + mock "github.com/stretchr/testify/mock" +) + +// Context is an autogenerated mock type for the Context type +type Context struct { + mock.Mock +} + +// ActiveSpan provides a mock function with given fields: +func (_m *Context) ActiveSpan() envoy.Span { + ret := _m.Called() + + var r0 envoy.Span + if rf, ok := ret.Get(0).(func() envoy.Span); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.Span) + } + } + + return r0 +} diff --git a/egofilters/mock/gen/http/security/context/request_context.go b/egofilters/mock/gen/http/security/context/request_context.go new file mode 100644 index 0000000..0e73483 --- /dev/null +++ b/egofilters/mock/gen/http/security/context/request_context.go @@ -0,0 +1,132 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + context2 "context" + + context "github.com/grab/ego/egofilters/http/security/context" + + envoy "github.com/grab/ego/ego/src/go/envoy" + + io "io" + + logger "github.com/grab/ego/ego/src/go/logger" + + mock "github.com/stretchr/testify/mock" +) + +// RequestContext is an autogenerated mock type for the RequestContext type +type RequestContext struct { + mock.Mock +} + +// ActiveSpan provides a mock function with given fields: +func (_m *RequestContext) ActiveSpan() envoy.Span { + ret := _m.Called() + + var r0 envoy.Span + if rf, ok := ret.Get(0).(func() envoy.Span); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.Span) + } + } + + return r0 +} + +// BodyReader provides a mock function with given fields: +func (_m *RequestContext) BodyReader() io.Reader { + ret := _m.Called() + + var r0 io.Reader + if rf, ok := ret.Get(0).(func() io.Reader); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(io.Reader) + } + } + + return r0 +} + +// Callbacks provides a mock function with given fields: +func (_m *RequestContext) Callbacks() context.Callbacks { + ret := _m.Called() + + var r0 context.Callbacks + if rf, ok := ret.Get(0).(func() context.Callbacks); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(context.Callbacks) + } + } + + return r0 +} + +// GetSecret provides a mock function with given fields: _a0 +func (_m *RequestContext) GetSecret(_a0 string) string { + ret := _m.Called(_a0) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// GoContext provides a mock function with given fields: +func (_m *RequestContext) GoContext() context2.Context { + ret := _m.Called() + + var r0 context2.Context + if rf, ok := ret.Get(0).(func() context2.Context); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(context2.Context) + } + } + + return r0 +} + +// Headers provides a mock function with given fields: +func (_m *RequestContext) Headers() envoy.RequestHeaderMap { + ret := _m.Called() + + var r0 envoy.RequestHeaderMap + if rf, ok := ret.Get(0).(func() envoy.RequestHeaderMap); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.RequestHeaderMap) + } + } + + return r0 +} + +// Logger provides a mock function with given fields: +func (_m *RequestContext) Logger() logger.Logger { + ret := _m.Called() + + var r0 logger.Logger + if rf, ok := ret.Get(0).(func() logger.Logger); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(logger.Logger) + } + } + + return r0 +} diff --git a/egofilters/mock/gen/http/security/context/response_callbacks.go b/egofilters/mock/gen/http/security/context/response_callbacks.go new file mode 100644 index 0000000..f45f359 --- /dev/null +++ b/egofilters/mock/gen/http/security/context/response_callbacks.go @@ -0,0 +1,18 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + context "github.com/grab/ego/egofilters/http/security/context" + mock "github.com/stretchr/testify/mock" +) + +// ResponseCallbacks is an autogenerated mock type for the ResponseCallbacks type +type ResponseCallbacks struct { + mock.Mock +} + +// OnCompleteSigning provides a mock function with given fields: _a0 +func (_m *ResponseCallbacks) OnCompleteSigning(_a0 context.SignResponse) { + _m.Called(_a0) +} diff --git a/egofilters/mock/gen/http/security/context/response_context.go b/egofilters/mock/gen/http/security/context/response_context.go new file mode 100644 index 0000000..e9e4c31 --- /dev/null +++ b/egofilters/mock/gen/http/security/context/response_context.go @@ -0,0 +1,162 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + context2 "context" + + context "github.com/grab/ego/egofilters/http/security/context" + + envoy "github.com/grab/ego/ego/src/go/envoy" + + io "io" + + logger "github.com/grab/ego/ego/src/go/logger" + + mock "github.com/stretchr/testify/mock" +) + +// ResponseContext is an autogenerated mock type for the ResponseContext type +type ResponseContext struct { + mock.Mock +} + +// ActiveSpan provides a mock function with given fields: +func (_m *ResponseContext) ActiveSpan() envoy.Span { + ret := _m.Called() + + var r0 envoy.Span + if rf, ok := ret.Get(0).(func() envoy.Span); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.Span) + } + } + + return r0 +} + +// AuthResponse provides a mock function with given fields: +func (_m *ResponseContext) AuthResponse() context.AuthResponse { + ret := _m.Called() + + var r0 context.AuthResponse + if rf, ok := ret.Get(0).(func() context.AuthResponse); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(context.AuthResponse) + } + + return r0 +} + +// BodyReader provides a mock function with given fields: +func (_m *ResponseContext) BodyReader() io.Reader { + ret := _m.Called() + + var r0 io.Reader + if rf, ok := ret.Get(0).(func() io.Reader); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(io.Reader) + } + } + + return r0 +} + +// Callbacks provides a mock function with given fields: +func (_m *ResponseContext) Callbacks() context.ResponseCallbacks { + ret := _m.Called() + + var r0 context.ResponseCallbacks + if rf, ok := ret.Get(0).(func() context.ResponseCallbacks); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(context.ResponseCallbacks) + } + } + + return r0 +} + +// GetSecret provides a mock function with given fields: _a0 +func (_m *ResponseContext) GetSecret(_a0 string) string { + ret := _m.Called(_a0) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// GoContext provides a mock function with given fields: +func (_m *ResponseContext) GoContext() context2.Context { + ret := _m.Called() + + var r0 context2.Context + if rf, ok := ret.Get(0).(func() context2.Context); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(context2.Context) + } + } + + return r0 +} + +// Headers provides a mock function with given fields: +func (_m *ResponseContext) Headers() envoy.ResponseHeaderMap { + ret := _m.Called() + + var r0 envoy.ResponseHeaderMap + if rf, ok := ret.Get(0).(func() envoy.ResponseHeaderMap); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.ResponseHeaderMap) + } + } + + return r0 +} + +// Logger provides a mock function with given fields: +func (_m *ResponseContext) Logger() logger.Logger { + ret := _m.Called() + + var r0 logger.Logger + if rf, ok := ret.Get(0).(func() logger.Logger); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(logger.Logger) + } + } + + return r0 +} + +// RequestHeaders provides a mock function with given fields: +func (_m *ResponseContext) RequestHeaders() envoy.RequestHeaderMap { + ret := _m.Called() + + var r0 envoy.RequestHeaderMap + if rf, ok := ret.Get(0).(func() envoy.RequestHeaderMap); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(envoy.RequestHeaderMap) + } + } + + return r0 +} diff --git a/egofilters/mock/gen/http/security/http/BUILD.bazel b/egofilters/mock/gen/http/security/http/BUILD.bazel new file mode 100644 index 0000000..1699fb5 --- /dev/null +++ b/egofilters/mock/gen/http/security/http/BUILD.bazel @@ -0,0 +1,15 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "http_client.go", + "http_client_with_ctx.go", + ], + importpath = "github.com/grab/ego/egofilters/mock/gen/http/security/http", + visibility = ["//visibility:public"], + deps = [ + "//egofilters/http/security/context:go_default_library", + "@com_github_stretchr_testify//mock:go_default_library", + ], +) diff --git a/egofilters/mock/gen/http/security/http/http_client.go b/egofilters/mock/gen/http/security/http/http_client.go new file mode 100644 index 0000000..fe00fb5 --- /dev/null +++ b/egofilters/mock/gen/http/security/http/http_client.go @@ -0,0 +1,37 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + http "net/http" + + mock "github.com/stretchr/testify/mock" +) + +// HttpClient is an autogenerated mock type for the HttpClient type +type HttpClient struct { + mock.Mock +} + +// Do provides a mock function with given fields: req +func (_m *HttpClient) Do(req *http.Request) (*http.Response, error) { + ret := _m.Called(req) + + var r0 *http.Response + if rf, ok := ret.Get(0).(func(*http.Request) *http.Response); ok { + r0 = rf(req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*http.Response) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*http.Request) error); ok { + r1 = rf(req) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/egofilters/mock/gen/http/security/http/http_client_with_ctx.go b/egofilters/mock/gen/http/security/http/http_client_with_ctx.go new file mode 100644 index 0000000..4225f04 --- /dev/null +++ b/egofilters/mock/gen/http/security/http/http_client_with_ctx.go @@ -0,0 +1,62 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + http "net/http" + + context "github.com/grab/ego/egofilters/http/security/context" + + mock "github.com/stretchr/testify/mock" +) + +// HttpClientWithCtx is an autogenerated mock type for the HttpClientWithCtx type +type HttpClientWithCtx struct { + mock.Mock +} + +// Do provides a mock function with given fields: req +func (_m *HttpClientWithCtx) Do(req *http.Request) (*http.Response, error) { + ret := _m.Called(req) + + var r0 *http.Response + if rf, ok := ret.Get(0).(func(*http.Request) *http.Response); ok { + r0 = rf(req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*http.Response) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*http.Request) error); ok { + r1 = rf(req) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DoWithTracing provides a mock function with given fields: ctx, req, spanName +func (_m *HttpClientWithCtx) DoWithTracing(ctx context.Context, req *http.Request, spanName string) (*http.Response, error) { + ret := _m.Called(ctx, req, spanName) + + var r0 *http.Response + if rf, ok := ret.Get(0).(func(context.Context, *http.Request, string) *http.Response); ok { + r0 = rf(ctx, req, spanName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*http.Response) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *http.Request, string) error); ok { + r1 = rf(ctx, req, spanName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/egofilters/mock/gen/http/security/proto/BUILD.bazel b/egofilters/mock/gen/http/security/proto/BUILD.bazel new file mode 100644 index 0000000..4807692 --- /dev/null +++ b/egofilters/mock/gen/http/security/proto/BUILD.bazel @@ -0,0 +1,12 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "is_provider__provider_type.go", + "is_requirement__requires_type.go", + ], + importpath = "github.com/grab/ego/egofilters/mock/gen/http/security/proto", + visibility = ["//visibility:public"], + deps = ["@com_github_stretchr_testify//mock:go_default_library"], +) diff --git a/egofilters/mock/gen/http/security/proto/is_provider__provider_type.go b/egofilters/mock/gen/http/security/proto/is_provider__provider_type.go new file mode 100644 index 0000000..15dd2b8 --- /dev/null +++ b/egofilters/mock/gen/http/security/proto/is_provider__provider_type.go @@ -0,0 +1,15 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// isProvider_ProviderType is an autogenerated mock type for the isProvider_ProviderType type +type isProvider_ProviderType struct { + mock.Mock +} + +// isProvider_ProviderType provides a mock function with given fields: +func (_m *isProvider_ProviderType) isProvider_ProviderType() { + _m.Called() +} diff --git a/egofilters/mock/gen/http/security/proto/is_requirement__requires_type.go b/egofilters/mock/gen/http/security/proto/is_requirement__requires_type.go new file mode 100644 index 0000000..ba5d965 --- /dev/null +++ b/egofilters/mock/gen/http/security/proto/is_requirement__requires_type.go @@ -0,0 +1,15 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// isRequirement_RequiresType is an autogenerated mock type for the isRequirement_RequiresType type +type isRequirement_RequiresType struct { + mock.Mock +} + +// isRequirement_RequiresType provides a mock function with given fields: +func (_m *isRequirement_RequiresType) isRequirement_RequiresType() { + _m.Called() +} diff --git a/egofilters/mock/gen/http/security/verifier/BUILD.bazel b/egofilters/mock/gen/http/security/verifier/BUILD.bazel new file mode 100644 index 0000000..bbadfd5 --- /dev/null +++ b/egofilters/mock/gen/http/security/verifier/BUILD.bazel @@ -0,0 +1,18 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "get_current_time_opt.go", + "is_valid_hmac_signature_opt.go", + "signer.go", + "verifier.go", + ], + importpath = "github.com/grab/ego/egofilters/mock/gen/http/security/verifier", + visibility = ["//visibility:public"], + deps = [ + "//ego/src/go/envoy:go_default_library", + "//egofilters/http/security/context:go_default_library", + "@com_github_stretchr_testify//mock:go_default_library", + ], +) diff --git a/egofilters/mock/gen/http/security/verifier/get_current_time_opt.go b/egofilters/mock/gen/http/security/verifier/get_current_time_opt.go new file mode 100644 index 0000000..9774ccf --- /dev/null +++ b/egofilters/mock/gen/http/security/verifier/get_current_time_opt.go @@ -0,0 +1,28 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + time "time" + + mock "github.com/stretchr/testify/mock" +) + +// getCurrentTimeOpt is an autogenerated mock type for the getCurrentTimeOpt type +type getCurrentTimeOpt struct { + mock.Mock +} + +// Execute provides a mock function with given fields: +func (_m *getCurrentTimeOpt) Execute() time.Time { + ret := _m.Called() + + var r0 time.Time + if rf, ok := ret.Get(0).(func() time.Time); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Time) + } + + return r0 +} diff --git a/egofilters/mock/gen/http/security/verifier/is_valid_hmac_signature_opt.go b/egofilters/mock/gen/http/security/verifier/is_valid_hmac_signature_opt.go new file mode 100644 index 0000000..f4b17de --- /dev/null +++ b/egofilters/mock/gen/http/security/verifier/is_valid_hmac_signature_opt.go @@ -0,0 +1,35 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + http "net/http" + + mock "github.com/stretchr/testify/mock" +) + +// isValidHMACSignatureOpt is an autogenerated mock type for the isValidHMACSignatureOpt type +type isValidHMACSignatureOpt struct { + mock.Mock +} + +// Execute provides a mock function with given fields: resp +func (_m *isValidHMACSignatureOpt) Execute(resp *http.Response) (bool, error) { + ret := _m.Called(resp) + + var r0 bool + if rf, ok := ret.Get(0).(func(*http.Response) bool); ok { + r0 = rf(resp) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 error + if rf, ok := ret.Get(1).(func(*http.Response) error); ok { + r1 = rf(resp) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/egofilters/mock/gen/http/security/verifier/signer.go b/egofilters/mock/gen/http/security/verifier/signer.go new file mode 100644 index 0000000..741e392 --- /dev/null +++ b/egofilters/mock/gen/http/security/verifier/signer.go @@ -0,0 +1,34 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + envoy "github.com/grab/ego/ego/src/go/envoy" + context "github.com/grab/ego/egofilters/http/security/context" + + mock "github.com/stretchr/testify/mock" +) + +// Signer is an autogenerated mock type for the Signer type +type Signer struct { + mock.Mock +} + +// Sign provides a mock function with given fields: _a0 +func (_m *Signer) Sign(_a0 context.ResponseContext) { + _m.Called(_a0) +} + +// SigningRequired provides a mock function with given fields: headers, authResp +func (_m *Signer) SigningRequired(headers envoy.ResponseHeaderMap, authResp context.AuthResponse) bool { + ret := _m.Called(headers, authResp) + + var r0 bool + if rf, ok := ret.Get(0).(func(envoy.ResponseHeaderMap, context.AuthResponse) bool); ok { + r0 = rf(headers, authResp) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} diff --git a/egofilters/mock/gen/http/security/verifier/verifier.go b/egofilters/mock/gen/http/security/verifier/verifier.go new file mode 100644 index 0000000..d64d689 --- /dev/null +++ b/egofilters/mock/gen/http/security/verifier/verifier.go @@ -0,0 +1,32 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import ( + context "github.com/grab/ego/egofilters/http/security/context" + mock "github.com/stretchr/testify/mock" +) + +// Verifier is an autogenerated mock type for the Verifier type +type Verifier struct { + mock.Mock +} + +// Verify provides a mock function with given fields: _a0 +func (_m *Verifier) Verify(_a0 context.RequestContext) { + _m.Called(_a0) +} + +// WithBody provides a mock function with given fields: +func (_m *Verifier) WithBody() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} diff --git a/envoy b/envoy new file mode 160000 index 0000000..11afcf9 --- /dev/null +++ b/envoy @@ -0,0 +1 @@ +Subproject commit 11afcf987c511371464b4e5f00b1500d79421791 diff --git a/envoy.yaml b/envoy.yaml new file mode 100644 index 0000000..139b506 --- /dev/null +++ b/envoy.yaml @@ -0,0 +1,122 @@ +admin: + access_log_path: ./admin_access.log + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 8081 +node: + # See https://github.com/envoyproxy/envoy/blob/master/api/envoy/config/core/v3/base.proto#L136 + # It is needed when we use xDS as it allows an SDS server to identify a + # specific Envoy instance and conditionally serve data. + id: egodemo + cluster: egodemo +static_resources: + secrets: + - name: /ego-demo/secrets + generic_secret: + secret: + inline_string: | + { + "egodemo-hmac-api-key":"API_KEY_SECRET", + "egodemo-hmac-api-secret":"API_TOKEN_SECRET" + } + listeners: + - name: api + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 8080 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + codec_type: auto + stat_prefix: ingress_http + access_log: + - name: envoy.access_loggers.file + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: "/dev/stdout" + route_config: + name: local_route + virtual_hosts: + - name: www + domains: + - "*" + routes: + - match: + prefix: /hmac/unsigned + route: + cluster: echo + typed_per_filter_config: + ego_http: + "@type": type.googleapis.com/ego.http.SettingsPerRoute + filters: + security: + "@type": type.googleapis.com/ego.security.Requirement + provider_name: simple_hmac_example + - match: + path: "/hmac/signed" + route: + cluster: echo + typed_per_filter_config: + ego_http: + "@type": type.googleapis.com/ego.http.SettingsPerRoute + filters: + security: + "@type": type.googleapis.com/ego.security.Requirement + provider_name: signed_hmac_example + - match: + # catch-all + prefix: / + route: + cluster: echo + http_filters: + - name: ego-http + typed_config: + "@type": type.googleapis.com/ego.http.Settings + filter: getheader + settings: + "@type": type.googleapis.com/egodemo.getheader.Settings + key: x-getheader-result + src: http://127.0.0.1:8888/echo + hdr: x-powered-by + - name: ego_http + typed_config: + "@type": type.googleapis.com/ego.http.Settings + filter: security + sds_secret_config: + name: /ego-demo/secrets + settings: + "@type": type.googleapis.com/ego.security.Settings + providers: + simple_hmac_example: + custom_hmac_provider: + request_validation_url: "http://127.0.0.1:8889/verify" + service_key: "egodemo-hmac-api-key" + service_token: "egodemo-hmac-api-secret" + signed_hmac_example: + custom_hmac_provider: + request_validation_url: "http://127.0.0.1:8889/verify" + response_signing_url: "http://127.0.0.1:8889/sign" + service_key: "egodemo-hmac-api-key" + service_token: "egodemo-hmac-api-secret" + sign_resp: true + - name: envoy.filters.http.router + clusters: + - name: echo + connect_timeout: 0.25s + type: STATIC + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: echo + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 8888 diff --git a/external/gomockery.patch b/external/gomockery.patch new file mode 100644 index 0000000..a6f9be9 --- /dev/null +++ b/external/gomockery.patch @@ -0,0 +1,25 @@ +diff --git a/gomockery.bzl b/gomockery.bzl +--- a/gomockery.bzl ++++ b/gomockery.bzl +@@ -83,7 +83,7 @@ def _go_mockery_impl(ctx): + ), + ] + +-_go_mockery = go_rule( ++_go_mockery = rule( + _go_mockery_impl, + attrs = { + "src": attr.label( +@@ -127,7 +127,11 @@ _go_mockery = go_rule( + cfg = "host", + mandatory = False, + ), +- } ++ "_go_context_data": attr.label( ++ default = "@io_bazel_rules_go//:go_context_data", ++ ), ++ }, ++ toolchains = ["@io_bazel_rules_go//go:toolchain"], + ) + + def _go_tool_run_shell_stdout(ctx, cmd, args, extra_inputs, outputs): diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5ed50d3 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/grab/ego + +go 1.14 + +replace github.com/grab/ego => ./ + +require ( + github.com/envoyproxy/protoc-gen-validate v0.4.1 + github.com/golang/protobuf v1.4.3 + github.com/stretchr/testify v1.7.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..914dfcb --- /dev/null +++ b/go.sum @@ -0,0 +1,107 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v0.4.1 h1:7dLaJvASGRD7X49jSCSXXHwKPm0ZN9r9kJD+p+vS7dM= +github.com/envoyproxy/protoc-gen-validate v0.4.1/go.mod h1:E+IEazqdaWv3FrnGtZIu3b9fPFMK8AzeTTrk9SfVwWs= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/iancoleman/strcase v0.0.0-20180726023541-3605ed457bf7/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/lyft/protoc-gen-star v0.5.1/go.mod h1:9toiA3cC7z5uVbODF7kEQ91Xn7XNFkVUl+SrEe+ZORU= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.3.4/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/services/echo/BUILD.bazel b/services/echo/BUILD.bazel new file mode 100644 index 0000000..43436ad --- /dev/null +++ b/services/echo/BUILD.bazel @@ -0,0 +1,15 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "go_default_library", + srcs = ["main.go"], + importpath = "github.com/grab/ego/services/echo", + visibility = ["//visibility:private"], +) + +go_binary( + name = "echo", + out = "echo", + embed = [":go_default_library"], + visibility = ["//visibility:public"], +) diff --git a/services/echo/main.go b/services/echo/main.go new file mode 100644 index 0000000..126f9e4 --- /dev/null +++ b/services/echo/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "runtime" +) + +// A simple echo service that dumps request headers and body into the +// response body and adds a custom header needed by one of the examples +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + + w.Header().Set("x-powered-by", runtime.Version()) + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, "%v %v%v\n", r.Method, r.Host, r.URL) + + for name, headers := range r.Header { + for _, h := range headers { + fmt.Fprintf(w, "%v: %v\n", name, h) + } + } + fmt.Fprint(w, "\n") + + if body, err := ioutil.ReadAll(r.Body); err != nil { + fmt.Fprint(w, err.Error()) + } else { + _, _ = w.Write(body) + } + }) + http.ListenAndServe(":8888", nil) +} diff --git a/services/hmac/BUILD.bazel b/services/hmac/BUILD.bazel new file mode 100644 index 0000000..1237128 --- /dev/null +++ b/services/hmac/BUILD.bazel @@ -0,0 +1,15 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "go_default_library", + srcs = ["main.go"], + importpath = "github.com/grab/ego/services/hmac", + visibility = ["//visibility:private"], +) + +go_binary( + name = "hmac", + out = "hmac", + embed = [":go_default_library"], + visibility = ["//visibility:public"], +) diff --git a/services/hmac/main.go b/services/hmac/main.go new file mode 100644 index 0000000..ae9b677 --- /dev/null +++ b/services/hmac/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + "io/ioutil" + "net/http" +) + +func main() { + http.HandleFunc("/verify", func(w http.ResponseWriter, r *http.Request) { + // small pseudo-hmac based on sum length + // signature = len(METHOD) + len(PATH) + len(BODY) + reqSignature := r.Header.Get("X-Custom-Auth-Signature") + reqVerb := r.Header.Get("X-Custom-Auth-Verb") + reqPath := r.Header.Get("X-Custom-Auth-Path") + verifedSignature := len(reqVerb) + len(reqPath) + if body, err := ioutil.ReadAll(r.Body); err == nil { + verifedSignature = verifedSignature + len(body) + } + if reqSignature == fmt.Sprintf("%v", verifedSignature) { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{"valid": true}`) + } else { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprint(w, `{"valid": false}`) + } + }) + + http.HandleFunc("/sign", func(w http.ResponseWriter, r *http.Request) { + // resp signature = str(status_code) + len(BODY) + respStatusCode := r.Header.Get("X-Custom-Auth-Status-Code") + bodyLen := 0 + if body, err := ioutil.ReadAll(r.Body); err == nil { + bodyLen = len(body) + } + signedSignature := fmt.Sprintf("%s_%d", respStatusCode, bodyLen) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{"signature": "%s"}`, signedSignature) + }) + http.ListenAndServe(":8889", nil) +} diff --git a/tools/copy_pb_go.py b/tools/copy_pb_go.py new file mode 100755 index 0000000..0a44a63 --- /dev/null +++ b/tools/copy_pb_go.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +import os +import os.path +from shutil import copyfile + +print("Current Working Directory " , os.getcwd()) +for dirpath, dirnames, filenames in os.walk("./bazel-out"): + for filename in [f for f in filenames if f.endswith(".pb.go") or f.endswith(".pb.validate.go")]: + name = os.path.join(dirpath, filename) + parts = name.split("%/github.com/grab/ego/") + if len(parts) == 1: + # print("skipping " + name) + continue + dest = parts[1] + print("copying", name, "-->", dest) + os.makedirs(os.path.dirname(dest), exist_ok=True) + copyfile(name, dest) diff --git a/tools/gen_compilation_database.py b/tools/gen_compilation_database.py new file mode 100755 index 0000000..1227c3c --- /dev/null +++ b/tools/gen_compilation_database.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 + +import argparse +import glob +import json +import os +import shlex +import subprocess +from pathlib import Path +import copy + + +def runBazelBuildForCompilationDatabase(bazel_options, bazel_targets): + query_targets = ' union '.join(bazel_targets) + query = ' union '.join( + q.format(query_targets) for q in [ + 'attr(include_prefix, ".+", kind(cc_library, deps({})))', + 'attr(strip_include_prefix, ".+", kind(cc_library, deps({})))', + 'attr(generator_function, ".*proto_library", kind(cc_.*, deps({})))', + ]) + build_targets = subprocess.check_output(["bazel", "query", "--notool_deps", + query]).decode().splitlines() + subprocess.check_call(["bazel", "build"] + bazel_options + build_targets) + + +# This method is equivalent to https://github.com/grailbio/bazel-compilation-database/blob/master/generate.sh +def generateCompilationDatabase(args, prefix): + # We need to download all remote outputs for generated source code. This option lives here to override those + # specified in bazelrc. + bazel_options = shlex.split(os.environ.get("BAZEL_BUILD_OPTIONS", "")) + [ + "--config=compdb", + "--remote_download_outputs=all", + ] + if args.run_bazel_build: + runBazelBuildForCompilationDatabase(bazel_options, args.bazel_targets) + + subprocess.check_call(["bazel", "build"] + bazel_options + [ + "--aspects=@bazel_compdb//:aspects.bzl%compilation_database_aspect", + "--output_groups=compdb_files" + ] + args.bazel_targets) + + execroot = subprocess.check_output(["bazel", "info", "execution_root"] + + bazel_options).decode().strip() + + compdb = [] + for compdb_file in Path(execroot).glob("**/*.compile_commands.json"): + comp = json.loads("[" + compdb_file.read_text().replace("__EXEC_ROOT__", execroot) + + "]") + + for a in comp : + a["file"] = prefix + a["file"] + compdb.extend(comp) + return compdb + + +def isHeader(filename): + for ext in (".h", ".hh", ".hpp", ".hxx"): + if filename.endswith(ext): + return True + return False + + +def isCompileTarget(target, args): + filename = target["file"] + if not args.include_headers and isHeader(filename): + return False + + if not args.include_genfiles: + if filename.startswith("bazel-out/"): + return False + + if not args.include_external: + if filename.startswith("external/"): + return False + + return True + + +def modifyCompileCommand(target, args): + cc, options = target["command"].split(" ", 1) + + # Workaround for bazel added C++11 options, those doesn't affect build itself but + # clang-tidy will misinterpret them. + options = options.replace("-std=c++0x ", "") + options = options.replace("-std=c++11 ", "") + + if args.vscode: + # Visual Studio Code doesn't seem to like "-iquote". Replace it with + # old-style "-I". + options = options.replace("-iquote ", "-I ") + + if isHeader(target["file"]): + options += " -Wno-pragma-once-outside-header -Wno-unused-const-variable" + options += " -Wno-unused-function" + + target["command"] = " ".join([cc, options]) + return target + + +def fixCompilationDatabase(args, db): + db = [modifyCompileCommand(target, args) for target in db if isCompileTarget(target, args)] + + with open("compile_commands.json", "w") as db_file: + json.dump(db, db_file, indent=2) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Generate JSON compilation database') + parser.add_argument('--run_bazel_build', action='store_true') + parser.add_argument('--include_external', action='store_true') + parser.add_argument('--include_genfiles', action='store_true') + parser.add_argument('--include_headers', action='store_true') + parser.add_argument('--vscode', action='store_true') + parser.add_argument('bazel_targets', + nargs='*', + default=["//source/...", "//test/...", "//tools/..."]) + args = parser.parse_args() + + + os.chdir("./envoy") + print("Current Working Directory " , os.getcwd()) + compdb=generateCompilationDatabase(args, "envoy/") + + os.chdir("..") + args.bazel_targets=["//ego/src/..."] + print("Current Working Directory " , os.getcwd()) + + cgo_args = copy.copy(args) + cgo_args.bazel_targets=["//ego/src/..."] + + compdb.extend( generateCompilationDatabase(cgo_args, "")) + fixCompilationDatabase(args, compdb) \ No newline at end of file diff --git a/tools/generate_test_coverage_report.sh b/tools/generate_test_coverage_report.sh new file mode 100755 index 0000000..135e6e4 --- /dev/null +++ b/tools/generate_test_coverage_report.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Clean up test logs for next run as caches are shared. +rm -rf bazel-out/k8-fastbuild/testlogs + +bazel coverage //egofilters/... + +COVERAGE_DIR=./generated +mkdir -p "${COVERAGE_DIR}" + +COVERAGE_DATA="${COVERAGE_DIR}/coverage.dat" + +echo "Merging coverage data..." +# Merge script supports only set. +echo "mode: set" > ${COVERAGE_DATA} && cat $(find -L bazel-out/k8-fastbuild/testlogs/ -name coverage.dat) | grep -v mode: | sort -r | \ +awk '{if($1 != last) {print $0;last=$1}}' >> ${COVERAGE_DATA} + +echo "Code coverage report..." +GOPATH=$(pwd)/GOPATH GO111MODULE=off go tool cover -html=generated/coverage.dat -o generated/coverage.html + +echo "Code coverage summary..." +go tool cover -func=generated/coverage.dat + +echo "Coverage report is at $(pwd)/${COVERAGE_DIR}/converage.html" + diff --git a/tools/sync.sh b/tools/sync.sh new file mode 100755 index 0000000..c1e5868 --- /dev/null +++ b/tools/sync.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +case $1 in +"up") + rsync -rlptzv --progress --exclude bazel --exclude user.bazelrc --exclude /envoy --exclude .git --exclude compile_commands.json . "$DEV_BOX_USER@$DEV_BOX_IP:~/$DEV_BOX_FOLDER_NAME" + ;; +"down") + rsync -rlptzv --progress --delete --exclude=.git --exclude bazel --exclude user.bazelrc --exclude /envoy "$DEV_BOX_USER@$DEV_BOX_IP:~/$DEV_BOX_FOLDER_NAME/*" ./ + ;; +*) + echo "Unknown command. Should be 'sync up' or 'sync down'" +esac \ No newline at end of file