mirror of
https://github.com/alibaba/higress.git
synced 2026-02-06 23:21:08 +08:00
Add plugins (#27)
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,5 +1,9 @@
|
||||
external
|
||||
out
|
||||
*.tgz
|
||||
|
||||
*.wasm
|
||||
.idea/
|
||||
bazel-bin
|
||||
bazel-out
|
||||
bazel-testlogs
|
||||
bazel-wasm-cpp
|
||||
@@ -19,6 +19,7 @@ header:
|
||||
- 'Makefile*'
|
||||
- 'script/**'
|
||||
- '.gitmodules'
|
||||
- 'plugins/**'
|
||||
|
||||
comment: on-failure
|
||||
dependency:
|
||||
|
||||
21
Makefile
21
Makefile
@@ -1,3 +1,24 @@
|
||||
# WARNING: DO NOT EDIT, THIS FILE IS PROBABLY A COPY
|
||||
#
|
||||
# The original version of this file is located in the https://github.com/istio/common-files repo.
|
||||
# If you're looking at this file in a different repo and want to make a change, please go to the
|
||||
# common-files repo, make the change there and check it in. Then come back to this repo and run
|
||||
# "make update-common".
|
||||
|
||||
# Copyright Istio Authors
|
||||
#
|
||||
# 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.
|
||||
|
||||
SHELL := /bin/bash
|
||||
|
||||
# allow optional per-repo overrides
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
# Copyright 2019 Istio Authors
|
||||
#
|
||||
# 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.
|
||||
|
||||
.DEFAULT_GOAL := default
|
||||
|
||||
# This repository has been enabled for BUILD_WITH_CONTAINER=1. Some
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright 2019 Istio Authors
|
||||
#
|
||||
# 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.
|
||||
|
||||
INPUTS=("${@}")
|
||||
TARGET_ARCH=${TARGET_ARCH:-amd64}
|
||||
DOCKER_WORKING_DIR=${INPUTS[${#INPUTS[@]}-1]}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
## Copyright 2018 Istio Authors
|
||||
##
|
||||
## 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.
|
||||
|
||||
docker.higress: BUILD_ARGS=--build-arg BASE_VERSION=${BASE_VERSION} --build-arg HUB=${HUB}
|
||||
docker.higress: $(OUT_LINUX)/higress
|
||||
docker.higress: docker/Dockerfile.higress
|
||||
|
||||
0
plugins/wasm-cpp/BUILD
Normal file
0
plugins/wasm-cpp/BUILD
Normal file
55
plugins/wasm-cpp/WORKSPACE
Normal file
55
plugins/wasm-cpp/WORKSPACE
Normal file
@@ -0,0 +1,55 @@
|
||||
workspace(name = "istio_ecosystem_wasm_extensions")
|
||||
|
||||
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
load("//bazel:third_party.bzl", "wasm_extension_dependency")
|
||||
|
||||
wasm_extension_dependency()
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_docker//repositories:repositories.bzl",
|
||||
container_repositories = "repositories",
|
||||
)
|
||||
|
||||
container_repositories()
|
||||
|
||||
load("@io_bazel_rules_docker//repositories:deps.bzl", container_deps = "deps")
|
||||
|
||||
container_deps()
|
||||
|
||||
PROXY_WASM_CPP_SDK_SHA = "fd0be8405db25de0264bdb78fae3a82668c03782"
|
||||
|
||||
PROXY_WASM_CPP_SDK_SHA256 = "c57de2425b5c61d7f630c5061e319b4557ae1f1c7526e5a51c33dc1299471b08"
|
||||
|
||||
http_archive(
|
||||
name = "proxy_wasm_cpp_sdk",
|
||||
sha256 = PROXY_WASM_CPP_SDK_SHA256,
|
||||
strip_prefix = "proxy-wasm-cpp-sdk-" + PROXY_WASM_CPP_SDK_SHA,
|
||||
url = "https://github.com/proxy-wasm/proxy-wasm-cpp-sdk/archive/" + PROXY_WASM_CPP_SDK_SHA + ".tar.gz",
|
||||
)
|
||||
|
||||
load("@proxy_wasm_cpp_sdk//bazel/dep:deps.bzl", "wasm_dependencies")
|
||||
|
||||
wasm_dependencies()
|
||||
|
||||
load("@proxy_wasm_cpp_sdk//bazel/dep:deps_extra.bzl", "wasm_dependencies_extra")
|
||||
|
||||
wasm_dependencies_extra()
|
||||
|
||||
load("@istio_ecosystem_wasm_extensions//bazel:wasm.bzl", "wasm_libraries")
|
||||
|
||||
wasm_libraries()
|
||||
|
||||
# To import proxy wasm cpp host, which will be used in unit testing.
|
||||
load("@proxy_wasm_cpp_host//bazel:repositories.bzl", "proxy_wasm_cpp_host_repositories")
|
||||
|
||||
proxy_wasm_cpp_host_repositories()
|
||||
|
||||
load("@proxy_wasm_cpp_host//bazel:dependencies.bzl", "proxy_wasm_cpp_host_dependencies")
|
||||
|
||||
proxy_wasm_cpp_host_dependencies()
|
||||
|
||||
http_archive(
|
||||
name = "bazel_compdb",
|
||||
strip_prefix = "bazel-compilation-database-0.5.2",
|
||||
urls = ["https://github.com/grailbio/bazel-compilation-database/archive/0.5.2.tar.gz"],
|
||||
)
|
||||
0
plugins/wasm-cpp/bazel/BUILD
Normal file
0
plugins/wasm-cpp/bazel/BUILD
Normal file
31
plugins/wasm-cpp/bazel/absl.patch
Normal file
31
plugins/wasm-cpp/bazel/absl.patch
Normal file
@@ -0,0 +1,31 @@
|
||||
diff --git a/absl/time/internal/cctz/src/time_zone_format.cc b/absl/time/internal/cctz/src/time_zone_format.cc
|
||||
index d8cb047..0c5f182 100644
|
||||
--- a/absl/time/internal/cctz/src/time_zone_format.cc
|
||||
+++ b/absl/time/internal/cctz/src/time_zone_format.cc
|
||||
@@ -18,6 +18,8 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
+#define HAS_STRPTIME 0
|
||||
+
|
||||
#if defined(HAS_STRPTIME) && HAS_STRPTIME
|
||||
#if !defined(_XOPEN_SOURCE)
|
||||
#define _XOPEN_SOURCE // Definedness suffices for strptime.
|
||||
@@ -58,7 +60,7 @@ namespace {
|
||||
|
||||
#if !HAS_STRPTIME
|
||||
// Build a strptime() using C++11's std::get_time().
|
||||
-char* strptime(const char* s, const char* fmt, std::tm* tm) {
|
||||
+char* strptime_local(const char* s, const char* fmt, std::tm* tm) {
|
||||
std::istringstream input(s);
|
||||
input >> std::get_time(tm, fmt);
|
||||
if (input.fail()) return nullptr;
|
||||
@@ -648,7 +650,7 @@ const char* ParseSubSeconds(const char* dp, detail::femtoseconds* subseconds) {
|
||||
// Parses a string into a std::tm using strptime(3).
|
||||
const char* ParseTM(const char* dp, const char* fmt, std::tm* tm) {
|
||||
if (dp != nullptr) {
|
||||
- dp = strptime(dp, fmt, tm);
|
||||
+ dp = strptime_local(dp, fmt, tm);
|
||||
}
|
||||
return dp;
|
||||
}
|
||||
50
plugins/wasm-cpp/bazel/boringssl.patch
Normal file
50
plugins/wasm-cpp/bazel/boringssl.patch
Normal file
@@ -0,0 +1,50 @@
|
||||
diff --git a/src/crypto/fipsmodule/rand/internal.h b/src/crypto/fipsmodule/rand/internal.h
|
||||
index 127e5d1..87fc6f0 100644
|
||||
--- a/src/crypto/fipsmodule/rand/internal.h
|
||||
+++ b/src/crypto/fipsmodule/rand/internal.h
|
||||
@@ -27,7 +27,7 @@ extern "C" {
|
||||
|
||||
|
||||
#if !defined(OPENSSL_WINDOWS) && !defined(OPENSSL_FUCHSIA) && \
|
||||
- !defined(BORINGSSL_UNSAFE_DETERMINISTIC_MODE) && !defined(OPENSSL_TRUSTY)
|
||||
+ !defined(BORINGSSL_UNSAFE_DETERMINISTIC_MODE) && !defined(OPENSSL_TRUSTY) && !defined(__EMSCRIPTEN__)
|
||||
#define OPENSSL_URANDOM
|
||||
#endif
|
||||
|
||||
diff --git a/src/crypto/internal.h b/src/crypto/internal.h
|
||||
index b288583..b2e9321 100644
|
||||
--- a/src/crypto/internal.h
|
||||
+++ b/src/crypto/internal.h
|
||||
@@ -130,6 +130,10 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
+#if defined(__EMSCRIPTEN__)
|
||||
+#undef OPENSSL_THREADS
|
||||
+#endif
|
||||
+
|
||||
#if defined(OPENSSL_THREADS) && \
|
||||
(!defined(OPENSSL_WINDOWS) || defined(__MINGW32__))
|
||||
#include <pthread.h>
|
||||
@@ -493,7 +497,7 @@ OPENSSL_EXPORT void CRYPTO_once(CRYPTO_once_t *once, void (*init)(void));
|
||||
|
||||
// Automatically enable C11 atomics if implemented.
|
||||
#if !defined(OPENSSL_C11_ATOMIC) && !defined(__STDC_NO_ATOMICS__) && \
|
||||
- defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
|
||||
+ defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L && !defined(__EMSCRIPTEN__)
|
||||
#define OPENSSL_C11_ATOMIC
|
||||
#endif
|
||||
|
||||
diff --git a/src/crypto/rand_extra/deterministic.c b/src/crypto/rand_extra/deterministic.c
|
||||
index 435f063..13a77db 100644
|
||||
--- a/src/crypto/rand_extra/deterministic.c
|
||||
+++ b/src/crypto/rand_extra/deterministic.c
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
#include <openssl/rand.h>
|
||||
|
||||
-#if defined(BORINGSSL_UNSAFE_DETERMINISTIC_MODE)
|
||||
+#if defined(BORINGSSL_UNSAFE_DETERMINISTIC_MODE) || defined(__EMSCRIPTEN__)
|
||||
|
||||
#include <string.h>
|
||||
|
||||
53
plugins/wasm-cpp/bazel/re2.patch
Normal file
53
plugins/wasm-cpp/bazel/re2.patch
Normal file
@@ -0,0 +1,53 @@
|
||||
diff --git a/util/mutex.h b/util/mutex.h
|
||||
index e2a8715..4031804 100644
|
||||
--- a/util/mutex.h
|
||||
+++ b/util/mutex.h
|
||||
@@ -28,10 +28,10 @@
|
||||
#if defined(MUTEX_IS_WIN32_SRWLOCK)
|
||||
#include <windows.h>
|
||||
typedef SRWLOCK MutexType;
|
||||
-#elif defined(MUTEX_IS_PTHREAD_RWLOCK)
|
||||
-#include <pthread.h>
|
||||
-#include <stdlib.h>
|
||||
-typedef pthread_rwlock_t MutexType;
|
||||
+// #elif defined(MUTEX_IS_PTHREAD_RWLOCK)
|
||||
+// #include <pthread.h>
|
||||
+// #include <stdlib.h>
|
||||
+// typedef pthread_rwlock_t MutexType;
|
||||
#else
|
||||
#include <mutex>
|
||||
typedef std::mutex MutexType;
|
||||
@@ -73,21 +73,21 @@ void Mutex::Unlock() { ReleaseSRWLockExclusive(&mutex_); }
|
||||
void Mutex::ReaderLock() { AcquireSRWLockShared(&mutex_); }
|
||||
void Mutex::ReaderUnlock() { ReleaseSRWLockShared(&mutex_); }
|
||||
|
||||
-#elif defined(MUTEX_IS_PTHREAD_RWLOCK)
|
||||
+// #elif defined(MUTEX_IS_PTHREAD_RWLOCK)
|
||||
|
||||
-#define SAFE_PTHREAD(fncall) \
|
||||
- do { \
|
||||
- if ((fncall) != 0) abort(); \
|
||||
- } while (0)
|
||||
+// #define SAFE_PTHREAD(fncall) \
|
||||
+// do { \
|
||||
+// if ((fncall) != 0) abort(); \
|
||||
+// } while (0)
|
||||
|
||||
-Mutex::Mutex() { SAFE_PTHREAD(pthread_rwlock_init(&mutex_, NULL)); }
|
||||
-Mutex::~Mutex() { SAFE_PTHREAD(pthread_rwlock_destroy(&mutex_)); }
|
||||
-void Mutex::Lock() { SAFE_PTHREAD(pthread_rwlock_wrlock(&mutex_)); }
|
||||
-void Mutex::Unlock() { SAFE_PTHREAD(pthread_rwlock_unlock(&mutex_)); }
|
||||
-void Mutex::ReaderLock() { SAFE_PTHREAD(pthread_rwlock_rdlock(&mutex_)); }
|
||||
-void Mutex::ReaderUnlock() { SAFE_PTHREAD(pthread_rwlock_unlock(&mutex_)); }
|
||||
+// Mutex::Mutex() { SAFE_PTHREAD(pthread_rwlock_init(&mutex_, NULL)); }
|
||||
+// Mutex::~Mutex() { SAFE_PTHREAD(pthread_rwlock_destroy(&mutex_)); }
|
||||
+// void Mutex::Lock() { SAFE_PTHREAD(pthread_rwlock_wrlock(&mutex_)); }
|
||||
+// void Mutex::Unlock() { SAFE_PTHREAD(pthread_rwlock_unlock(&mutex_)); }
|
||||
+// void Mutex::ReaderLock() { SAFE_PTHREAD(pthread_rwlock_rdlock(&mutex_)); }
|
||||
+// void Mutex::ReaderUnlock() { SAFE_PTHREAD(pthread_rwlock_unlock(&mutex_)); }
|
||||
|
||||
-#undef SAFE_PTHREAD
|
||||
+// #undef SAFE_PTHREAD
|
||||
|
||||
#else
|
||||
|
||||
10
plugins/wasm-cpp/bazel/third_party.bzl
Normal file
10
plugins/wasm-cpp/bazel/third_party.bzl
Normal file
@@ -0,0 +1,10 @@
|
||||
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
|
||||
def wasm_extension_dependency():
|
||||
# Need to push Wasm OCI images
|
||||
http_archive(
|
||||
name = "io_bazel_rules_docker",
|
||||
sha256 = "92779d3445e7bdc79b961030b996cb0c91820ade7ffa7edca69273f404b085d5",
|
||||
strip_prefix = "rules_docker-0.20.0",
|
||||
urls = ["https://github.com/bazelbuild/rules_docker/releases/download/v0.20.0/rules_docker-v0.20.0.tar.gz"],
|
||||
)
|
||||
119
plugins/wasm-cpp/bazel/wasm.bzl
Normal file
119
plugins/wasm-cpp/bazel/wasm.bzl
Normal file
@@ -0,0 +1,119 @@
|
||||
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
|
||||
load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
|
||||
load(
|
||||
"@io_bazel_rules_docker//container:container.bzl",
|
||||
"container_image",
|
||||
"container_push",
|
||||
)
|
||||
|
||||
def wasm_libraries():
|
||||
http_archive(
|
||||
name = "com_google_absl",
|
||||
sha256 = "ec8ef47335310cc3382bdc0d0cc1097a001e67dc83fcba807845aa5696e7e1e4",
|
||||
strip_prefix = "abseil-cpp-302b250e1d917ede77b5ff00a6fd9f28430f1563",
|
||||
url = "https://github.com/abseil/abseil-cpp/archive/302b250e1d917ede77b5ff00a6fd9f28430f1563.tar.gz",
|
||||
patch_args = ["-p1"],
|
||||
patches = ["//bazel:absl.patch"],
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "com_github_nlohmann_json_single_header",
|
||||
sha256 = "3b5d2b8f8282b80557091514d8ab97e27f9574336c804ee666fda673a9b59926",
|
||||
urls = [
|
||||
"https://github.com/nlohmann/json/releases/download/v3.7.3/json.hpp",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# import google test and cpp host for unit testing
|
||||
http_archive(
|
||||
name = "com_google_googletest",
|
||||
sha256 = "9dc9157a9a1551ec7a7e43daea9a694a0bb5fb8bec81235d8a1e6ef64c716dcb",
|
||||
strip_prefix = "googletest-release-1.10.0",
|
||||
urls = ["https://github.com/google/googletest/archive/release-1.10.0.tar.gz"],
|
||||
)
|
||||
|
||||
PROXY_WASM_CPP_HOST_SHA = "f38347360feaaf5b2a733f219c4d8c9660d626f0"
|
||||
PROXY_WASM_CPP_HOST_SHA256 = "bf10de946eb5785813895c2bf16504afc0cd590b9655d9ee52fb1074d0825ea3"
|
||||
|
||||
http_archive(
|
||||
name = "proxy_wasm_cpp_host",
|
||||
sha256 = PROXY_WASM_CPP_HOST_SHA256,
|
||||
strip_prefix = "proxy-wasm-cpp-host-" + PROXY_WASM_CPP_HOST_SHA,
|
||||
url = "https://github.com/proxy-wasm/proxy-wasm-cpp-host/archive/" + PROXY_WASM_CPP_HOST_SHA +".tar.gz",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "boringssl",
|
||||
urls = ["https://github.com/google/boringssl/archive/648cbaf033401b7fe7acdce02f275b06a88aab5c.tar.gz"],
|
||||
strip_prefix = "boringssl-648cbaf033401b7fe7acdce02f275b06a88aab5c",
|
||||
patch_args = ["-p1"],
|
||||
patches = ["//bazel:boringssl.patch"],
|
||||
)
|
||||
|
||||
native.bind(
|
||||
name = "ssl",
|
||||
actual = "@boringssl//:ssl",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "com_google_protobuf",
|
||||
urls = ["https://github.com/protocolbuffers/protobuf/releases/download/v{version}/protobuf-all-3.18.0.tar.gz"],
|
||||
strip_prefix = "protobuf-3.18.0",
|
||||
)
|
||||
|
||||
native.bind(
|
||||
name = "protobuf",
|
||||
actual = "@com_google_protobuf//:protobuf",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "com_googlesource_code_re2",
|
||||
urls = ["https://github.com/google/re2/archive/2020-07-06.tar.gz"],
|
||||
strip_prefix = "re2-2020-07-06",
|
||||
patch_args = ["-p1"],
|
||||
patches = ["//bazel:re2.patch"],
|
||||
)
|
||||
|
||||
native.bind(
|
||||
name = "abseil_flat_hash_set",
|
||||
actual = "@com_google_absl//absl/container:flat_hash_set",
|
||||
)
|
||||
|
||||
native.bind(
|
||||
name = "abseil_strings",
|
||||
actual = "@com_google_absl//absl/strings:strings",
|
||||
)
|
||||
|
||||
native.bind(
|
||||
name = "abseil_time",
|
||||
actual = "@com_google_absl//absl/time:time",
|
||||
)
|
||||
|
||||
native.bind(
|
||||
name = "protobuf",
|
||||
actual = "@com_google_protobuf//:protobuf",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "com_github_google_jwt_verify",
|
||||
urls = ["https://github.com/google/jwt_verify_lib/archive/26c22c0ce1bc607eec8fa5dd26b707378adc7a88.tar.gz"],
|
||||
strip_prefix = "jwt_verify_lib-26c22c0ce1bc607eec8fa5dd26b707378adc7a88"
|
||||
)
|
||||
|
||||
|
||||
def declare_wasm_image_targets(name, wasm_file):
|
||||
# Rename to the spec compatible name.
|
||||
copy_file("copy_original_file", wasm_file, "plugin.wasm")
|
||||
container_image(
|
||||
name = "wasm_image",
|
||||
files = [":plugin.wasm"],
|
||||
)
|
||||
container_push(
|
||||
name = "push_wasm_image",
|
||||
format = "OCI",
|
||||
image = ":wasm_image",
|
||||
registry = "ghcr.io",
|
||||
repository = "istio-ecosystem/wasm-extensions/"+name,
|
||||
tag = "$(WASM_IMAGE_TAG)",
|
||||
)
|
||||
131
plugins/wasm-cpp/common/BUILD
Normal file
131
plugins/wasm-cpp/common/BUILD
Normal file
@@ -0,0 +1,131 @@
|
||||
cc_library(
|
||||
name = "common_util",
|
||||
hdrs = [
|
||||
"common_util.h",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"@com_google_absl//absl/strings",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "http_util",
|
||||
srcs = ["http_util.cc"],
|
||||
hdrs = [
|
||||
"http_util.h",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":common_util",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/time",
|
||||
"@com_google_absl//absl/strings:str_format",
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "http_util_nullvm",
|
||||
srcs = ["http_util.cc"],
|
||||
hdrs = [
|
||||
"http_util.h",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
":common_util",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/time",
|
||||
"@com_google_absl//absl/strings:str_format",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
cc_library(
|
||||
name = "crypto_util",
|
||||
srcs = [
|
||||
"crypto_util.cc",
|
||||
"crypt_blowfish.c",
|
||||
"base64.h",
|
||||
],
|
||||
hdrs = [
|
||||
"crypto_util.h",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":common_util",
|
||||
":json_util",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@boringssl//:ssl",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "rule_util",
|
||||
hdrs = [
|
||||
"route_rule_matcher.h",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":common_util",
|
||||
":http_util",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "rule_util_nullvm",
|
||||
hdrs = [
|
||||
"route_rule_matcher.h",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
":common_util",
|
||||
":http_util_nullvm",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
cc_library(
|
||||
name = "regex_util",
|
||||
hdrs = [
|
||||
"regex.h",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":common_util",
|
||||
"@com_googlesource_code_re2//:re2",
|
||||
],
|
||||
)
|
||||
|
||||
# genrule(
|
||||
# name = "nlohmann_json_hpp",
|
||||
# srcs = ["@com_github_nlohmann_json_single_header//file"],
|
||||
# outs = ["nlohmann_json.hpp"],
|
||||
# cmd = "cp $< $@",
|
||||
# visibility = ["//visibility:public"],
|
||||
# )
|
||||
|
||||
cc_library(
|
||||
name = "json_util",
|
||||
srcs = ["json_util.cc"],
|
||||
hdrs = [
|
||||
"json_util.h",
|
||||
"nlohmann_json.hpp",
|
||||
],
|
||||
copts = ["-UNULL_PLUGIN"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":common_util",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/types:optional",
|
||||
],
|
||||
)
|
||||
|
||||
exports_files([
|
||||
"base64.h",
|
||||
"json_util.cc",
|
||||
"json_util.h",
|
||||
])
|
||||
200
plugins/wasm-cpp/common/base64.h
Normal file
200
plugins/wasm-cpp/common/base64.h
Normal file
@@ -0,0 +1,200 @@
|
||||
/* Copyright 2019 Istio Authors. All Rights Reserved.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* From
|
||||
* https://github.com/envoyproxy/envoy/blob/master/source/common/common/base64.{h,cc}
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
static const std::string EMPTY_STRING;
|
||||
|
||||
class Base64 {
|
||||
public:
|
||||
static std::string encode(const char* input, uint64_t length,
|
||||
bool add_padding);
|
||||
static std::string encode(const char* input, uint64_t length) {
|
||||
return encode(input, length, true);
|
||||
}
|
||||
static std::string decodeWithoutPadding(std::string_view input);
|
||||
};
|
||||
|
||||
// clang-format off
|
||||
inline constexpr char CHAR_TABLE[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
inline constexpr unsigned char REVERSE_LOOKUP_TABLE[256] = {
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 64, 63,
|
||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 64, 64, 64, 64, 64, 0, 1, 2, 3, 4, 5, 6,
|
||||
7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, 64,
|
||||
64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
|
||||
49, 50, 51, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64};
|
||||
// clang-format on
|
||||
|
||||
inline bool decodeBase(const uint8_t cur_char, uint64_t pos, std::string& ret,
|
||||
const unsigned char* const reverse_lookup_table) {
|
||||
const unsigned char c = reverse_lookup_table[static_cast<uint32_t>(cur_char)];
|
||||
if (c == 64) {
|
||||
// Invalid character
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (pos % 4) {
|
||||
case 0:
|
||||
ret.push_back(c << 2);
|
||||
break;
|
||||
case 1:
|
||||
ret.back() |= c >> 4;
|
||||
ret.push_back(c << 4);
|
||||
break;
|
||||
case 2:
|
||||
ret.back() |= c >> 2;
|
||||
ret.push_back(c << 6);
|
||||
break;
|
||||
case 3:
|
||||
ret.back() |= c;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool decodeLast(const uint8_t cur_char, uint64_t pos, std::string& ret,
|
||||
const unsigned char* const reverse_lookup_table) {
|
||||
const unsigned char c = reverse_lookup_table[static_cast<uint32_t>(cur_char)];
|
||||
if (c == 64) {
|
||||
// Invalid character
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (pos % 4) {
|
||||
case 0:
|
||||
return false;
|
||||
case 1:
|
||||
ret.back() |= c >> 4;
|
||||
return (c & 0b1111) == 0;
|
||||
case 2:
|
||||
ret.back() |= c >> 2;
|
||||
return (c & 0b11) == 0;
|
||||
case 3:
|
||||
ret.back() |= c;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void encodeBase(const uint8_t cur_char, uint64_t pos, uint8_t& next_c,
|
||||
std::string& ret, const char* const char_table) {
|
||||
switch (pos % 3) {
|
||||
case 0:
|
||||
ret.push_back(char_table[cur_char >> 2]);
|
||||
next_c = (cur_char & 0x03) << 4;
|
||||
break;
|
||||
case 1:
|
||||
ret.push_back(char_table[next_c | (cur_char >> 4)]);
|
||||
next_c = (cur_char & 0x0f) << 2;
|
||||
break;
|
||||
case 2:
|
||||
ret.push_back(char_table[next_c | (cur_char >> 6)]);
|
||||
ret.push_back(char_table[cur_char & 0x3f]);
|
||||
next_c = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
inline void encodeLast(uint64_t pos, uint8_t last_char, std::string& ret,
|
||||
const char* const char_table, bool add_padding) {
|
||||
switch (pos % 3) {
|
||||
case 1:
|
||||
ret.push_back(char_table[last_char]);
|
||||
if (add_padding) {
|
||||
ret.push_back('=');
|
||||
ret.push_back('=');
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
ret.push_back(char_table[last_char]);
|
||||
if (add_padding) {
|
||||
ret.push_back('=');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string Base64::encode(const char* input, uint64_t length,
|
||||
bool add_padding) {
|
||||
uint64_t output_length = (length + 2) / 3 * 4;
|
||||
std::string ret;
|
||||
ret.reserve(output_length);
|
||||
|
||||
uint64_t pos = 0;
|
||||
uint8_t next_c = 0;
|
||||
|
||||
for (uint64_t i = 0; i < length; ++i) {
|
||||
encodeBase(input[i], pos++, next_c, ret, CHAR_TABLE);
|
||||
}
|
||||
|
||||
encodeLast(pos, next_c, ret, CHAR_TABLE, add_padding);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline std::string Base64::decodeWithoutPadding(std::string_view input) {
|
||||
if (input.empty()) {
|
||||
return EMPTY_STRING;
|
||||
}
|
||||
|
||||
// At most last two chars can be '='.
|
||||
size_t n = input.length();
|
||||
if (input[n - 1] == '=') {
|
||||
n--;
|
||||
if (n > 0 && input[n - 1] == '=') {
|
||||
n--;
|
||||
}
|
||||
}
|
||||
// Last position before "valid" padding character.
|
||||
uint64_t last = n - 1;
|
||||
// Determine output length.
|
||||
size_t max_length = (n + 3) / 4 * 3;
|
||||
if (n % 4 == 3) {
|
||||
max_length -= 1;
|
||||
}
|
||||
if (n % 4 == 2) {
|
||||
max_length -= 2;
|
||||
}
|
||||
|
||||
std::string ret;
|
||||
ret.reserve(max_length);
|
||||
for (uint64_t i = 0; i < last; ++i) {
|
||||
if (!decodeBase(input[i], i, ret, REVERSE_LOOKUP_TABLE)) {
|
||||
return EMPTY_STRING;
|
||||
}
|
||||
}
|
||||
|
||||
if (!decodeLast(input[last], last, ret, REVERSE_LOOKUP_TABLE)) {
|
||||
return EMPTY_STRING;
|
||||
}
|
||||
|
||||
ASSERT(ret.size() == max_length);
|
||||
return ret;
|
||||
}
|
||||
59
plugins/wasm-cpp/common/common_util.h
Normal file
59
plugins/wasm-cpp/common/common_util.h
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#include "absl/strings/string_view.h"
|
||||
|
||||
namespace Wasm::Common {
|
||||
inline absl::string_view stdToAbsl(const std::string_view& str) {
|
||||
return {str.data(), str.size()};
|
||||
}
|
||||
inline std::string_view abslToStd(const absl::string_view& str) {
|
||||
return {str.data(), str.size()};
|
||||
}
|
||||
|
||||
const char WhitespaceChars[] = " \t\f\v\n\r";
|
||||
|
||||
inline std::string_view ltrim(std::string_view source) {
|
||||
const std::string_view::size_type pos =
|
||||
source.find_first_not_of(WhitespaceChars);
|
||||
if (pos != std::string_view::npos) {
|
||||
source.remove_prefix(pos);
|
||||
} else {
|
||||
source.remove_prefix(source.size());
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
inline std::string_view rtrim(std::string_view source) {
|
||||
const std::string_view::size_type pos =
|
||||
source.find_last_not_of(WhitespaceChars);
|
||||
if (pos != std::string_view::npos) {
|
||||
source.remove_suffix(source.size() - pos - 1);
|
||||
} else {
|
||||
source.remove_suffix(source.size());
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
inline std::string_view trim(std::string_view source) {
|
||||
return ltrim(rtrim(source));
|
||||
}
|
||||
|
||||
} // namespace Wasm::Common
|
||||
806
plugins/wasm-cpp/common/crypt_blowfish.c
Normal file
806
plugins/wasm-cpp/common/crypt_blowfish.c
Normal file
@@ -0,0 +1,806 @@
|
||||
/* Modified by Rich Felker in for inclusion in musl libc, based on
|
||||
* Solar Designer's second size-optimized version sent to the musl
|
||||
* mailing list. */
|
||||
|
||||
/*
|
||||
* The crypt_blowfish homepage is:
|
||||
*
|
||||
* http://www.openwall.com/crypt/
|
||||
*
|
||||
* This code comes from John the Ripper password cracker, with reentrant
|
||||
* and crypt(3) interfaces added, but optimizations specific to password
|
||||
* cracking removed.
|
||||
*
|
||||
* Written by Solar Designer <solar at openwall.com> in 1998-2012.
|
||||
* No copyright is claimed, and the software is hereby placed in the public
|
||||
* domain. In case this attempt to disclaim copyright and place the software
|
||||
* in the public domain is deemed null and void, then the software is
|
||||
* Copyright (c) 1998-2014 Solar Designer and it is hereby released to the
|
||||
* general public under the following terms:
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted.
|
||||
*
|
||||
* There's ABSOLUTELY NO WARRANTY, express or implied.
|
||||
*
|
||||
* It is my intent that you should be able to use this on your system,
|
||||
* as part of a software package, or anywhere else to improve security,
|
||||
* ensure compatibility, or for any other purpose. I would appreciate
|
||||
* it if you give credit where it is due and keep your modifications in
|
||||
* the public domain as well, but I don't require that in order to let
|
||||
* you place this code and any modifications you make under a license
|
||||
* of your choice.
|
||||
*
|
||||
* This implementation is fully compatible with OpenBSD's bcrypt.c for prefix
|
||||
* "$2b$", originally by Niels Provos <provos at citi.umich.edu>, and it uses
|
||||
* some of his ideas. The password hashing algorithm was designed by David
|
||||
* Mazieres <dm at lcs.mit.edu>. For information on the level of
|
||||
* compatibility for bcrypt hash prefixes other than "$2b$", please refer to
|
||||
* the comments in BF_set_key() below and to the included crypt(3) man page.
|
||||
*
|
||||
* There's a paper on the algorithm that explains its design decisions:
|
||||
*
|
||||
* http://www.usenix.org/events/usenix99/provos.html
|
||||
*
|
||||
* Some of the tricks in BF_ROUND might be inspired by Eric Young's
|
||||
* Blowfish library (I can't be sure if I would think of something if I
|
||||
* hadn't seen his code).
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef uint32_t BF_word;
|
||||
typedef int32_t BF_word_signed;
|
||||
|
||||
/* Number of Blowfish rounds, this is also hardcoded into a few places */
|
||||
#define BF_N 16
|
||||
|
||||
typedef BF_word BF_key[BF_N + 2];
|
||||
|
||||
typedef union {
|
||||
struct {
|
||||
BF_key P;
|
||||
BF_word S[4][0x100];
|
||||
} s;
|
||||
BF_word PS[BF_N + 2 + 4 * 0x100];
|
||||
} BF_ctx;
|
||||
|
||||
/*
|
||||
* Magic IV for 64 Blowfish encryptions that we do at the end.
|
||||
* The string is "OrpheanBeholderScryDoubt" on big-endian.
|
||||
*/
|
||||
static const BF_word BF_magic_w[6] = {
|
||||
0x4F727068, 0x65616E42, 0x65686F6C,
|
||||
0x64657253, 0x63727944, 0x6F756274
|
||||
};
|
||||
|
||||
/*
|
||||
* P-box and S-box tables initialized with digits of Pi.
|
||||
*/
|
||||
static const BF_ctx BF_init_state = {{
|
||||
{
|
||||
0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344,
|
||||
0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89,
|
||||
0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c,
|
||||
0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917,
|
||||
0x9216d5d9, 0x8979fb1b
|
||||
}, {
|
||||
{
|
||||
0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7,
|
||||
0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99,
|
||||
0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16,
|
||||
0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e,
|
||||
0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee,
|
||||
0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013,
|
||||
0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef,
|
||||
0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e,
|
||||
0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60,
|
||||
0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440,
|
||||
0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce,
|
||||
0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a,
|
||||
0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e,
|
||||
0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677,
|
||||
0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193,
|
||||
0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032,
|
||||
0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88,
|
||||
0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239,
|
||||
0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e,
|
||||
0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0,
|
||||
0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3,
|
||||
0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98,
|
||||
0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88,
|
||||
0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe,
|
||||
0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6,
|
||||
0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d,
|
||||
0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b,
|
||||
0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7,
|
||||
0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba,
|
||||
0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463,
|
||||
0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f,
|
||||
0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09,
|
||||
0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3,
|
||||
0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb,
|
||||
0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279,
|
||||
0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8,
|
||||
0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab,
|
||||
0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82,
|
||||
0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db,
|
||||
0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573,
|
||||
0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0,
|
||||
0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b,
|
||||
0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790,
|
||||
0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8,
|
||||
0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4,
|
||||
0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0,
|
||||
0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7,
|
||||
0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c,
|
||||
0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad,
|
||||
0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1,
|
||||
0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299,
|
||||
0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9,
|
||||
0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477,
|
||||
0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf,
|
||||
0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49,
|
||||
0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af,
|
||||
0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa,
|
||||
0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5,
|
||||
0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41,
|
||||
0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915,
|
||||
0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400,
|
||||
0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915,
|
||||
0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664,
|
||||
0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a
|
||||
}, {
|
||||
0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623,
|
||||
0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266,
|
||||
0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1,
|
||||
0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e,
|
||||
0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6,
|
||||
0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1,
|
||||
0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e,
|
||||
0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1,
|
||||
0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737,
|
||||
0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8,
|
||||
0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff,
|
||||
0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd,
|
||||
0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701,
|
||||
0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7,
|
||||
0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41,
|
||||
0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331,
|
||||
0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf,
|
||||
0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af,
|
||||
0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e,
|
||||
0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87,
|
||||
0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c,
|
||||
0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2,
|
||||
0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16,
|
||||
0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd,
|
||||
0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b,
|
||||
0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509,
|
||||
0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e,
|
||||
0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3,
|
||||
0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f,
|
||||
0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a,
|
||||
0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4,
|
||||
0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960,
|
||||
0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66,
|
||||
0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28,
|
||||
0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802,
|
||||
0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84,
|
||||
0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510,
|
||||
0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf,
|
||||
0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14,
|
||||
0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e,
|
||||
0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50,
|
||||
0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7,
|
||||
0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8,
|
||||
0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281,
|
||||
0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99,
|
||||
0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696,
|
||||
0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128,
|
||||
0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73,
|
||||
0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0,
|
||||
0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0,
|
||||
0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105,
|
||||
0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250,
|
||||
0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3,
|
||||
0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285,
|
||||
0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00,
|
||||
0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061,
|
||||
0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb,
|
||||
0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e,
|
||||
0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735,
|
||||
0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc,
|
||||
0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9,
|
||||
0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340,
|
||||
0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20,
|
||||
0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7
|
||||
}, {
|
||||
0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934,
|
||||
0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068,
|
||||
0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af,
|
||||
0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840,
|
||||
0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45,
|
||||
0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504,
|
||||
0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a,
|
||||
0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb,
|
||||
0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee,
|
||||
0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6,
|
||||
0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42,
|
||||
0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b,
|
||||
0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2,
|
||||
0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb,
|
||||
0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527,
|
||||
0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b,
|
||||
0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33,
|
||||
0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c,
|
||||
0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3,
|
||||
0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc,
|
||||
0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17,
|
||||
0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564,
|
||||
0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b,
|
||||
0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115,
|
||||
0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922,
|
||||
0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728,
|
||||
0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0,
|
||||
0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e,
|
||||
0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37,
|
||||
0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d,
|
||||
0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804,
|
||||
0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b,
|
||||
0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3,
|
||||
0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb,
|
||||
0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d,
|
||||
0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c,
|
||||
0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350,
|
||||
0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9,
|
||||
0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a,
|
||||
0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe,
|
||||
0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d,
|
||||
0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc,
|
||||
0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f,
|
||||
0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61,
|
||||
0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2,
|
||||
0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9,
|
||||
0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2,
|
||||
0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c,
|
||||
0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e,
|
||||
0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633,
|
||||
0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10,
|
||||
0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169,
|
||||
0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52,
|
||||
0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027,
|
||||
0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5,
|
||||
0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62,
|
||||
0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634,
|
||||
0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76,
|
||||
0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24,
|
||||
0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc,
|
||||
0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4,
|
||||
0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c,
|
||||
0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837,
|
||||
0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0
|
||||
}, {
|
||||
0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b,
|
||||
0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe,
|
||||
0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b,
|
||||
0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4,
|
||||
0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8,
|
||||
0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6,
|
||||
0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304,
|
||||
0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22,
|
||||
0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4,
|
||||
0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6,
|
||||
0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9,
|
||||
0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59,
|
||||
0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593,
|
||||
0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51,
|
||||
0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28,
|
||||
0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c,
|
||||
0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b,
|
||||
0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28,
|
||||
0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c,
|
||||
0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd,
|
||||
0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a,
|
||||
0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319,
|
||||
0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb,
|
||||
0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f,
|
||||
0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991,
|
||||
0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32,
|
||||
0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680,
|
||||
0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166,
|
||||
0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae,
|
||||
0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb,
|
||||
0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5,
|
||||
0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47,
|
||||
0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370,
|
||||
0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d,
|
||||
0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84,
|
||||
0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048,
|
||||
0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8,
|
||||
0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd,
|
||||
0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9,
|
||||
0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7,
|
||||
0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38,
|
||||
0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f,
|
||||
0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c,
|
||||
0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525,
|
||||
0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1,
|
||||
0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442,
|
||||
0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964,
|
||||
0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e,
|
||||
0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8,
|
||||
0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d,
|
||||
0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f,
|
||||
0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299,
|
||||
0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02,
|
||||
0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc,
|
||||
0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614,
|
||||
0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a,
|
||||
0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6,
|
||||
0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b,
|
||||
0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0,
|
||||
0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060,
|
||||
0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e,
|
||||
0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9,
|
||||
0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f,
|
||||
0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6
|
||||
}
|
||||
}
|
||||
}};
|
||||
|
||||
static const unsigned char BF_itoa64[64 + 1] =
|
||||
"./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
static const unsigned char BF_atoi64[0x60] = {
|
||||
64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 0, 1,
|
||||
54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 64, 64, 64, 64, 64,
|
||||
64, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
||||
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 64, 64, 64, 64, 64,
|
||||
64, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
|
||||
43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 64, 64, 64, 64, 64
|
||||
};
|
||||
|
||||
#define BF_safe_atoi64(dst, src) \
|
||||
{ \
|
||||
tmp = (unsigned char)(src); \
|
||||
if ((unsigned int)(tmp -= 0x20) >= 0x60) return -1; \
|
||||
tmp = BF_atoi64[tmp]; \
|
||||
if (tmp > 63) return -1; \
|
||||
(dst) = tmp; \
|
||||
}
|
||||
|
||||
static int BF_decode(BF_word *dst, const char *src, int size)
|
||||
{
|
||||
unsigned char *dptr = (unsigned char *)dst;
|
||||
unsigned char *end = dptr + size;
|
||||
const unsigned char *sptr = (const unsigned char *)src;
|
||||
unsigned int tmp, c1, c2, c3, c4;
|
||||
|
||||
do {
|
||||
BF_safe_atoi64(c1, *sptr++);
|
||||
BF_safe_atoi64(c2, *sptr++);
|
||||
*dptr++ = (c1 << 2) | ((c2 & 0x30) >> 4);
|
||||
if (dptr >= end) break;
|
||||
|
||||
BF_safe_atoi64(c3, *sptr++);
|
||||
*dptr++ = ((c2 & 0x0F) << 4) | ((c3 & 0x3C) >> 2);
|
||||
if (dptr >= end) break;
|
||||
|
||||
BF_safe_atoi64(c4, *sptr++);
|
||||
*dptr++ = ((c3 & 0x03) << 6) | c4;
|
||||
} while (dptr < end);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void BF_encode(char *dst, const BF_word *src, int size)
|
||||
{
|
||||
const unsigned char *sptr = (const unsigned char *)src;
|
||||
const unsigned char *end = sptr + size;
|
||||
unsigned char *dptr = (unsigned char *)dst;
|
||||
unsigned int c1, c2;
|
||||
|
||||
do {
|
||||
c1 = *sptr++;
|
||||
*dptr++ = BF_itoa64[c1 >> 2];
|
||||
c1 = (c1 & 0x03) << 4;
|
||||
if (sptr >= end) {
|
||||
*dptr++ = BF_itoa64[c1];
|
||||
break;
|
||||
}
|
||||
|
||||
c2 = *sptr++;
|
||||
c1 |= c2 >> 4;
|
||||
*dptr++ = BF_itoa64[c1];
|
||||
c1 = (c2 & 0x0f) << 2;
|
||||
if (sptr >= end) {
|
||||
*dptr++ = BF_itoa64[c1];
|
||||
break;
|
||||
}
|
||||
|
||||
c2 = *sptr++;
|
||||
c1 |= c2 >> 6;
|
||||
*dptr++ = BF_itoa64[c1];
|
||||
*dptr++ = BF_itoa64[c2 & 0x3f];
|
||||
} while (sptr < end);
|
||||
}
|
||||
|
||||
static void BF_swap(BF_word *x, int count)
|
||||
{
|
||||
if ((union { int i; char c; }){1}.c)
|
||||
do {
|
||||
BF_word tmp = *x;
|
||||
tmp = (tmp << 16) | (tmp >> 16);
|
||||
*x++ = ((tmp & 0x00FF00FF) << 8) | ((tmp >> 8) & 0x00FF00FF);
|
||||
} while (--count);
|
||||
}
|
||||
|
||||
#define BF_ROUND(L, R, N) \
|
||||
tmp1 = L & 0xFF; \
|
||||
tmp2 = L >> 8; \
|
||||
tmp2 &= 0xFF; \
|
||||
tmp3 = L >> 16; \
|
||||
tmp3 &= 0xFF; \
|
||||
tmp4 = L >> 24; \
|
||||
tmp1 = ctx->s.S[3][tmp1]; \
|
||||
tmp2 = ctx->s.S[2][tmp2]; \
|
||||
tmp3 = ctx->s.S[1][tmp3]; \
|
||||
tmp3 += ctx->s.S[0][tmp4]; \
|
||||
tmp3 ^= tmp2; \
|
||||
R ^= ctx->s.P[N + 1]; \
|
||||
tmp3 += tmp1; \
|
||||
R ^= tmp3;
|
||||
|
||||
static BF_word BF_encrypt(BF_ctx *ctx,
|
||||
BF_word L, BF_word R,
|
||||
BF_word *start, BF_word *end)
|
||||
{
|
||||
BF_word tmp1, tmp2, tmp3, tmp4;
|
||||
BF_word *ptr = start;
|
||||
|
||||
do {
|
||||
L ^= ctx->s.P[0];
|
||||
#if 0
|
||||
BF_ROUND(L, R, 0);
|
||||
BF_ROUND(R, L, 1);
|
||||
BF_ROUND(L, R, 2);
|
||||
BF_ROUND(R, L, 3);
|
||||
BF_ROUND(L, R, 4);
|
||||
BF_ROUND(R, L, 5);
|
||||
BF_ROUND(L, R, 6);
|
||||
BF_ROUND(R, L, 7);
|
||||
BF_ROUND(L, R, 8);
|
||||
BF_ROUND(R, L, 9);
|
||||
BF_ROUND(L, R, 10);
|
||||
BF_ROUND(R, L, 11);
|
||||
BF_ROUND(L, R, 12);
|
||||
BF_ROUND(R, L, 13);
|
||||
BF_ROUND(L, R, 14);
|
||||
BF_ROUND(R, L, 15);
|
||||
#else
|
||||
for (int i=0; i<16; i+=2) {
|
||||
BF_ROUND(L, R, i);
|
||||
BF_ROUND(R, L, i+1);
|
||||
}
|
||||
#endif
|
||||
tmp4 = R;
|
||||
R = L;
|
||||
L = tmp4 ^ ctx->s.P[BF_N + 1];
|
||||
*ptr++ = L;
|
||||
*ptr++ = R;
|
||||
} while (ptr < end);
|
||||
|
||||
return L;
|
||||
}
|
||||
|
||||
static void BF_set_key(const char *key, BF_key expanded, BF_key initial,
|
||||
unsigned char flags)
|
||||
{
|
||||
const char *ptr = key;
|
||||
unsigned int bug, i, j;
|
||||
BF_word safety, sign, diff, tmp[2];
|
||||
|
||||
/*
|
||||
* There was a sign extension bug in older revisions of this function. While
|
||||
* we would have liked to simply fix the bug and move on, we have to provide
|
||||
* a backwards compatibility feature (essentially the bug) for some systems and
|
||||
* a safety measure for some others. The latter is needed because for certain
|
||||
* multiple inputs to the buggy algorithm there exist easily found inputs to
|
||||
* the correct algorithm that produce the same hash. Thus, we optionally
|
||||
* deviate from the correct algorithm just enough to avoid such collisions.
|
||||
* While the bug itself affected the majority of passwords containing
|
||||
* characters with the 8th bit set (although only a percentage of those in a
|
||||
* collision-producing way), the anti-collision safety measure affects
|
||||
* only a subset of passwords containing the '\xff' character (not even all of
|
||||
* those passwords, just some of them). This character is not found in valid
|
||||
* UTF-8 sequences and is rarely used in popular 8-bit character encodings.
|
||||
* Thus, the safety measure is unlikely to cause much annoyance, and is a
|
||||
* reasonable tradeoff to use when authenticating against existing hashes that
|
||||
* are not reliably known to have been computed with the correct algorithm.
|
||||
*
|
||||
* We use an approach that tries to minimize side-channel leaks of password
|
||||
* information - that is, we mostly use fixed-cost bitwise operations instead
|
||||
* of branches or table lookups. (One conditional branch based on password
|
||||
* length remains. It is not part of the bug aftermath, though, and is
|
||||
* difficult and possibly unreasonable to avoid given the use of C strings by
|
||||
* the caller, which results in similar timing leaks anyway.)
|
||||
*
|
||||
* For actual implementation, we set an array index in the variable "bug"
|
||||
* (0 means no bug, 1 means sign extension bug emulation) and a flag in the
|
||||
* variable "safety" (bit 16 is set when the safety measure is requested).
|
||||
* Valid combinations of settings are:
|
||||
*
|
||||
* Prefix "$2a$": bug = 0, safety = 0x10000
|
||||
* Prefix "$2b$": bug = 0, safety = 0
|
||||
* Prefix "$2x$": bug = 1, safety = 0
|
||||
* Prefix "$2y$": bug = 0, safety = 0
|
||||
*/
|
||||
bug = flags & 1;
|
||||
safety = ((BF_word)flags & 2) << 15;
|
||||
|
||||
sign = diff = 0;
|
||||
|
||||
for (i = 0; i < BF_N + 2; i++) {
|
||||
tmp[0] = tmp[1] = 0;
|
||||
for (j = 0; j < 4; j++) {
|
||||
tmp[0] <<= 8;
|
||||
tmp[0] |= (unsigned char)*ptr; /* correct */
|
||||
tmp[1] <<= 8;
|
||||
tmp[1] |= (signed char)*ptr; /* bug */
|
||||
/*
|
||||
* Sign extension in the first char has no effect - nothing to overwrite yet,
|
||||
* and those extra 24 bits will be fully shifted out of the 32-bit word. For
|
||||
* chars 2, 3, 4 in each four-char block, we set bit 7 of "sign" if sign
|
||||
* extension in tmp[1] occurs. Once this flag is set, it remains set.
|
||||
*/
|
||||
if (j)
|
||||
sign |= tmp[1] & 0x80;
|
||||
if (!*ptr)
|
||||
ptr = key;
|
||||
else
|
||||
ptr++;
|
||||
}
|
||||
diff |= tmp[0] ^ tmp[1]; /* Non-zero on any differences */
|
||||
|
||||
expanded[i] = tmp[bug];
|
||||
initial[i] = BF_init_state.s.P[i] ^ tmp[bug];
|
||||
}
|
||||
|
||||
/*
|
||||
* At this point, "diff" is zero iff the correct and buggy algorithms produced
|
||||
* exactly the same result. If so and if "sign" is non-zero, which indicates
|
||||
* that there was a non-benign sign extension, this means that we have a
|
||||
* collision between the correctly computed hash for this password and a set of
|
||||
* passwords that could be supplied to the buggy algorithm. Our safety measure
|
||||
* is meant to protect from such many-buggy to one-correct collisions, by
|
||||
* deviating from the correct algorithm in such cases. Let's check for this.
|
||||
*/
|
||||
diff |= diff >> 16; /* still zero iff exact match */
|
||||
diff &= 0xffff; /* ditto */
|
||||
diff += 0xffff; /* bit 16 set iff "diff" was non-zero (on non-match) */
|
||||
sign <<= 9; /* move the non-benign sign extension flag to bit 16 */
|
||||
sign &= ~diff & safety; /* action needed? */
|
||||
|
||||
/*
|
||||
* If we have determined that we need to deviate from the correct algorithm,
|
||||
* flip bit 16 in initial expanded key. (The choice of 16 is arbitrary, but
|
||||
* let's stick to it now. It came out of the approach we used above, and it's
|
||||
* not any worse than any other choice we could make.)
|
||||
*
|
||||
* It is crucial that we don't do the same to the expanded key used in the main
|
||||
* Eksblowfish loop. By doing it to only one of these two, we deviate from a
|
||||
* state that could be directly specified by a password to the buggy algorithm
|
||||
* (and to the fully correct one as well, but that's a side-effect).
|
||||
*/
|
||||
initial[0] ^= sign;
|
||||
}
|
||||
|
||||
static const unsigned char flags_by_subtype[26] = {
|
||||
2, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 4, 0
|
||||
};
|
||||
|
||||
static char *BF_crypt(const char *key, const char *setting,
|
||||
char *output, BF_word min)
|
||||
{
|
||||
struct {
|
||||
BF_ctx ctx;
|
||||
BF_key expanded_key;
|
||||
union {
|
||||
BF_word salt[4];
|
||||
BF_word output[6];
|
||||
} binary;
|
||||
} data;
|
||||
BF_word count;
|
||||
int i;
|
||||
|
||||
if (setting[0] != '$' ||
|
||||
setting[1] != '2' ||
|
||||
setting[2] - 'a' > 25U ||
|
||||
!flags_by_subtype[setting[2] - 'a'] ||
|
||||
setting[3] != '$' ||
|
||||
setting[4] - '0' > 1U ||
|
||||
setting[5] - '0' > 9U ||
|
||||
setting[6] != '$') {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
count = (BF_word)1 << ((setting[4] - '0') * 10 + (setting[5] - '0'));
|
||||
if (count < min || BF_decode(data.binary.salt, &setting[7], 16)) {
|
||||
return NULL;
|
||||
}
|
||||
BF_swap(data.binary.salt, 4);
|
||||
|
||||
BF_set_key(key, data.expanded_key, data.ctx.s.P,
|
||||
flags_by_subtype[setting[2] - 'a']);
|
||||
|
||||
memcpy(data.ctx.s.S, BF_init_state.s.S, sizeof(data.ctx.s.S));
|
||||
|
||||
{
|
||||
BF_word L = 0, R = 0;
|
||||
BF_word *ptr = &data.ctx.PS[0];
|
||||
do {
|
||||
L = BF_encrypt(&data.ctx,
|
||||
L ^ data.binary.salt[0], R ^ data.binary.salt[1],
|
||||
ptr, ptr);
|
||||
R = *(ptr + 1);
|
||||
ptr += 2;
|
||||
|
||||
if (ptr >= &data.ctx.PS[BF_N + 2 + 4 * 0x100])
|
||||
break;
|
||||
|
||||
L = BF_encrypt(&data.ctx,
|
||||
L ^ data.binary.salt[2], R ^ data.binary.salt[3],
|
||||
ptr, ptr);
|
||||
R = *(ptr + 1);
|
||||
ptr += 2;
|
||||
} while (1);
|
||||
}
|
||||
|
||||
do {
|
||||
int done;
|
||||
|
||||
for (i = 0; i < BF_N + 2; i += 2) {
|
||||
data.ctx.s.P[i] ^= data.expanded_key[i];
|
||||
data.ctx.s.P[i + 1] ^= data.expanded_key[i + 1];
|
||||
}
|
||||
|
||||
done = 0;
|
||||
do {
|
||||
BF_encrypt(&data.ctx, 0, 0,
|
||||
&data.ctx.PS[0],
|
||||
&data.ctx.PS[BF_N + 2 + 4 * 0x100]);
|
||||
|
||||
if (done)
|
||||
break;
|
||||
done = 1;
|
||||
|
||||
{
|
||||
BF_word tmp1, tmp2, tmp3, tmp4;
|
||||
|
||||
tmp1 = data.binary.salt[0];
|
||||
tmp2 = data.binary.salt[1];
|
||||
tmp3 = data.binary.salt[2];
|
||||
tmp4 = data.binary.salt[3];
|
||||
for (i = 0; i < BF_N; i += 4) {
|
||||
data.ctx.s.P[i] ^= tmp1;
|
||||
data.ctx.s.P[i + 1] ^= tmp2;
|
||||
data.ctx.s.P[i + 2] ^= tmp3;
|
||||
data.ctx.s.P[i + 3] ^= tmp4;
|
||||
}
|
||||
data.ctx.s.P[16] ^= tmp1;
|
||||
data.ctx.s.P[17] ^= tmp2;
|
||||
}
|
||||
} while (1);
|
||||
} while (--count);
|
||||
|
||||
for (i = 0; i < 6; i += 2) {
|
||||
BF_word L, LR[2];
|
||||
|
||||
L = BF_magic_w[i];
|
||||
LR[1] = BF_magic_w[i + 1];
|
||||
|
||||
count = 64;
|
||||
do {
|
||||
L = BF_encrypt(&data.ctx, L, LR[1],
|
||||
&LR[0], &LR[0]);
|
||||
} while (--count);
|
||||
|
||||
data.binary.output[i] = L;
|
||||
data.binary.output[i + 1] = LR[1];
|
||||
}
|
||||
|
||||
memcpy(output, setting, 7 + 22 - 1);
|
||||
output[7 + 22 - 1] = BF_itoa64[
|
||||
BF_atoi64[setting[7 + 22 - 1] - 0x20] & 0x30];
|
||||
|
||||
/* This has to be bug-compatible with the original implementation, so
|
||||
* only encode 23 of the 24 bytes. :-) */
|
||||
BF_swap(data.binary.output, 6);
|
||||
BF_encode(&output[7 + 22], data.binary.output, 23);
|
||||
output[7 + 22 + 31] = '\0';
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/*
|
||||
* Please preserve the runtime self-test. It serves two purposes at once:
|
||||
*
|
||||
* 1. We really can't afford the risk of producing incompatible hashes e.g.
|
||||
* when there's something like gcc bug 26587 again, whereas an application or
|
||||
* library integrating this code might not also integrate our external tests or
|
||||
* it might not run them after every build. Even if it does, the miscompile
|
||||
* might only occur on the production build, but not on a testing build (such
|
||||
* as because of different optimization settings). It is painful to recover
|
||||
* from incorrectly-computed hashes - merely fixing whatever broke is not
|
||||
* enough. Thus, a proactive measure like this self-test is needed.
|
||||
*
|
||||
* 2. We don't want to leave sensitive data from our actual password hash
|
||||
* computation on the stack or in registers. Previous revisions of the code
|
||||
* would do explicit cleanups, but simply running the self-test after hash
|
||||
* computation is more reliable.
|
||||
*
|
||||
* The performance cost of this quick self-test is around 0.6% at the "$2a$08"
|
||||
* setting.
|
||||
*/
|
||||
char *__crypt_blowfish(const char *key, const char *setting, char *output)
|
||||
{
|
||||
const char *test_key = "8b \xd0\xc1\xd2\xcf\xcc\xd8";
|
||||
const char *test_setting = "$2a$00$abcdefghijklmnopqrstuu";
|
||||
static const char test_hashes[2][34] = {
|
||||
"i1D709vfamulimlGcq0qq3UvuUasvEa\0\x55", /* 'a', 'b', 'y' */
|
||||
"VUrPmXD6q/nVSSp7pNDhCR9071IfIRe\0\x55", /* 'x' */
|
||||
};
|
||||
const char *test_hash = test_hashes[0];
|
||||
char *retval;
|
||||
const char *p;
|
||||
int ok;
|
||||
struct {
|
||||
char s[7 + 22 + 1];
|
||||
char o[7 + 22 + 31 + 1 + 1 + 1];
|
||||
} buf;
|
||||
|
||||
/* Hash the supplied password */
|
||||
retval = BF_crypt(key, setting, output, 16);
|
||||
|
||||
/*
|
||||
* Do a quick self-test. It is important that we make both calls to BF_crypt()
|
||||
* from the same scope such that they likely use the same stack locations,
|
||||
* which makes the second call overwrite the first call's sensitive data on the
|
||||
* stack and makes it more likely that any alignment related issues would be
|
||||
* detected by the self-test.
|
||||
*/
|
||||
memcpy(buf.s, test_setting, sizeof(buf.s));
|
||||
if (retval) {
|
||||
unsigned int flags = flags_by_subtype[setting[2] - 'a'];
|
||||
test_hash = test_hashes[flags & 1];
|
||||
buf.s[2] = setting[2];
|
||||
}
|
||||
memset(buf.o, 0x55, sizeof(buf.o));
|
||||
buf.o[sizeof(buf.o) - 1] = 0;
|
||||
p = BF_crypt(test_key, buf.s, buf.o, 1);
|
||||
|
||||
ok = (p == buf.o &&
|
||||
!memcmp(p, buf.s, 7 + 22) &&
|
||||
!memcmp(p + (7 + 22),
|
||||
test_hash,
|
||||
31 + 1 + 1 + 1));
|
||||
|
||||
{
|
||||
const char *k = "\xff\xa3" "34" "\xff\xff\xff\xa3" "345";
|
||||
BF_key ae, ai, ye, yi;
|
||||
BF_set_key(k, ae, ai, 2); /* $2a$ */
|
||||
BF_set_key(k, ye, yi, 4); /* $2y$ */
|
||||
ai[0] ^= 0x10000; /* undo the safety (for comparison) */
|
||||
ok = ok && ai[0] == 0xdb9c59bc && ye[17] == 0x33343500 &&
|
||||
!memcmp(ae, ye, sizeof(ae)) &&
|
||||
!memcmp(ai, yi, sizeof(ai));
|
||||
}
|
||||
|
||||
if (ok && retval)
|
||||
return retval;
|
||||
|
||||
return "*";
|
||||
}
|
||||
283
plugins/wasm-cpp/common/crypto_util.cc
Normal file
283
plugins/wasm-cpp/common/crypto_util.cc
Normal file
@@ -0,0 +1,283 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "crypto_util.h"
|
||||
|
||||
#include <crypt.h>
|
||||
#include <openssl/sha.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string_view>
|
||||
|
||||
#include "absl/strings/ascii.h"
|
||||
#include "absl/strings/match.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "base64.h"
|
||||
|
||||
extern "C" {
|
||||
char* __crypt_blowfish(const char* key, const char* setting, char* output);
|
||||
}
|
||||
|
||||
namespace Wasm::Common::Crypto {
|
||||
|
||||
namespace {
|
||||
size_t getDigestLength(std::string_view name) {
|
||||
if (name == "sha1") {
|
||||
return SHA_DIGEST_LENGTH;
|
||||
}
|
||||
if (name == "sha224") {
|
||||
return SHA224_DIGEST_LENGTH;
|
||||
}
|
||||
if (name == "sha256") {
|
||||
return SHA256_DIGEST_LENGTH;
|
||||
}
|
||||
if (name == "sha384") {
|
||||
return SHA384_DIGEST_LENGTH;
|
||||
}
|
||||
if (name == "sha512") {
|
||||
return SHA512_DIGEST_LENGTH;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
const EVP_MD* getHashFunction(std::string_view name) {
|
||||
// Hash algorithms set refers
|
||||
// https://github.com/google/boringssl/blob/master/include/openssl/digest.h
|
||||
if (name == "sha1") {
|
||||
return EVP_sha1();
|
||||
}
|
||||
if (name == "sha224") {
|
||||
return EVP_sha224();
|
||||
}
|
||||
if (name == "sha256") {
|
||||
return EVP_sha256();
|
||||
}
|
||||
if (name == "sha384") {
|
||||
return EVP_sha384();
|
||||
}
|
||||
if (name == "sha512") {
|
||||
return EVP_sha512();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::vector<uint8_t> getShaHmac(std::string_view hash_type,
|
||||
std::string_view key,
|
||||
std::string_view message) {
|
||||
auto length = getDigestLength(hash_type);
|
||||
if (length == 0) {
|
||||
return {};
|
||||
}
|
||||
const auto* hashFunc = getHashFunction(hash_type);
|
||||
if (hashFunc == nullptr) {
|
||||
return {};
|
||||
}
|
||||
std::vector<uint8_t> hmac(length);
|
||||
HMAC(hashFunc, key.data(), key.size(),
|
||||
reinterpret_cast<const uint8_t*>(message.data()), message.size(),
|
||||
hmac.data(), nullptr);
|
||||
return hmac;
|
||||
}
|
||||
|
||||
std::string getShaHmacBase64(std::string_view hash_type, std::string_view key,
|
||||
std::string_view message) {
|
||||
auto hmac = getShaHmac(hash_type, key, message);
|
||||
return Base64::encode(reinterpret_cast<const char*>(hmac.data()),
|
||||
hmac.size());
|
||||
}
|
||||
|
||||
std::vector<uint8_t> getMD5(std::string_view message) {
|
||||
std::vector<uint8_t> md5(MD5_DIGEST_LENGTH);
|
||||
MD5(reinterpret_cast<const uint8_t*>(message.data()), message.size(),
|
||||
md5.data());
|
||||
return md5;
|
||||
}
|
||||
|
||||
std::string getMD5Base64(std::string_view message) {
|
||||
auto md5 = getMD5(message);
|
||||
return Base64::encode(reinterpret_cast<const char*>(md5.data()), md5.size());
|
||||
}
|
||||
|
||||
bool libc_crypt(const std::string& key, const std::string& salt,
|
||||
std::string& encrypted) {
|
||||
char* value;
|
||||
struct crypt_data cd;
|
||||
cd.initialized = 0;
|
||||
value = crypt_r(key.data(), salt.data(), &cd);
|
||||
if (value != nullptr) {
|
||||
encrypted = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void crypt_to64(std::string& encrypted, uint32_t v, size_t n) {
|
||||
static u_char itoa64[] =
|
||||
"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
while (n-- > 0) {
|
||||
encrypted.push_back(itoa64[v & 0x3f]);
|
||||
v >>= 6;
|
||||
}
|
||||
}
|
||||
|
||||
bool crypt_apr1(const std::string& key, const std::string& salt,
|
||||
std::string& encrypted) {
|
||||
const char* salt_data;
|
||||
const char* last;
|
||||
std::array<u_char, 16> final_data;
|
||||
salt_data = salt.data();
|
||||
salt_data += sizeof("$apr1$") - 1;
|
||||
// true salt: no magic, max 8 chars, stop at first $
|
||||
last = salt_data + 8;
|
||||
const char* p;
|
||||
for (p = salt_data; *p != 0 && *p != '$' && p < last; p++) {
|
||||
/* void */
|
||||
}
|
||||
size_t salt_len = p - salt_data;
|
||||
MD5_CTX md5;
|
||||
MD5_Init(&md5);
|
||||
MD5_Update(&md5, key.data(), key.size());
|
||||
MD5_Update(&md5, "$apr1$", sizeof("$apr1$") - 1);
|
||||
MD5_Update(&md5, salt_data, salt_len);
|
||||
|
||||
MD5_CTX ctx1;
|
||||
MD5_Init(&ctx1);
|
||||
MD5_Update(&ctx1, key.data(), key.size());
|
||||
MD5_Update(&ctx1, salt_data, salt_len);
|
||||
MD5_Update(&ctx1, key.data(), key.size());
|
||||
MD5_Final(final_data.data(), &ctx1);
|
||||
|
||||
for (int i = key.size(); i > 0; i -= 16) {
|
||||
MD5_Update(&md5, final_data.data(), i > 16 ? 16 : i);
|
||||
}
|
||||
final_data.fill(0);
|
||||
for (auto i = key.size(); i != 0; i >>= 1) {
|
||||
if ((i & 1) != 0) {
|
||||
MD5_Update(&md5, final_data.data(), 1);
|
||||
} else {
|
||||
MD5_Update(&md5, key.data(), 1);
|
||||
}
|
||||
}
|
||||
MD5_Final(final_data.data(), &md5);
|
||||
for (auto i = 0; i < 1000; i++) {
|
||||
MD5_Init(&ctx1);
|
||||
if ((i & 1) != 0) {
|
||||
MD5_Update(&ctx1, key.data(), key.size());
|
||||
} else {
|
||||
MD5_Update(&ctx1, final_data.data(), 16);
|
||||
}
|
||||
if (i % 3 != 0) {
|
||||
MD5_Update(&ctx1, salt_data, salt_len);
|
||||
}
|
||||
if (i % 7 != 0) {
|
||||
MD5_Update(&ctx1, key.data(), key.size());
|
||||
}
|
||||
if ((i & 1) != 0) {
|
||||
MD5_Update(&ctx1, final_data.data(), 16);
|
||||
} else {
|
||||
MD5_Update(&ctx1, key.data(), key.size());
|
||||
}
|
||||
MD5_Final(final_data.data(), &ctx1);
|
||||
}
|
||||
encrypted =
|
||||
absl::StrCat("$apr1$", absl::string_view{salt_data, salt_len}, "$");
|
||||
crypt_to64(encrypted,
|
||||
(final_data[0] << 16) | (final_data[6] << 8) | final_data[12], 4);
|
||||
crypt_to64(encrypted,
|
||||
(final_data[1] << 16) | (final_data[7] << 8) | final_data[13], 4);
|
||||
crypt_to64(encrypted,
|
||||
(final_data[2] << 16) | (final_data[8] << 8) | final_data[14], 4);
|
||||
crypt_to64(encrypted,
|
||||
(final_data[3] << 16) | (final_data[9] << 8) | final_data[15], 4);
|
||||
crypt_to64(encrypted,
|
||||
(final_data[4] << 16) | (final_data[10] << 8) | final_data[5], 4);
|
||||
crypt_to64(encrypted, final_data[11], 2);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool crypt_plain(const std::string& key, const std::string& salt,
|
||||
std::string& encrypted) {
|
||||
encrypted = absl::StrCat("{PLAIN}", key);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool crypt_ssha(const std::string& key, const std::string& salt,
|
||||
std::string& encrypted) {
|
||||
auto decoded = Base64::decodeWithoutPadding(
|
||||
{salt.data() + sizeof("{SSHA}") - 1, salt.size() - sizeof("{SSHA}") + 1});
|
||||
if (decoded.empty()) {
|
||||
return false;
|
||||
}
|
||||
if (decoded.size() < 20) {
|
||||
decoded.resize(20);
|
||||
}
|
||||
SHA_CTX sha1;
|
||||
SHA1_Init(&sha1);
|
||||
SHA1_Update(&sha1, key.data(), key.size());
|
||||
SHA1_Update(&sha1, decoded.data() + 20, decoded.size() - 20);
|
||||
SHA1_Final((u_char*)decoded.data(), &sha1);
|
||||
|
||||
encrypted =
|
||||
absl::StrCat("{SSHA}", Base64::encode(decoded.data(), decoded.size()));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool crypt_sha(const std::string& key, const std::string& salt,
|
||||
std::string& encrypted) {
|
||||
SHA_CTX sha1;
|
||||
std::array<u_char, 20> digest;
|
||||
SHA1_Init(&sha1);
|
||||
SHA1_Update(&sha1, key.data(), key.size());
|
||||
SHA1_Final(digest.data(), &sha1);
|
||||
encrypted = absl::StrCat("{SHA}",
|
||||
Base64::encode((char*)digest.data(), digest.size()));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool bcrypt(const std::string& key, const std::string& salt,
|
||||
std::string& encrypted) {
|
||||
struct crypt_data cd;
|
||||
cd.initialized = 0;
|
||||
char* value = __crypt_blowfish(key.data(), salt.data(), (char*)&cd);
|
||||
if (value != nullptr) {
|
||||
encrypted = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool crypt(const std::string& key, const std::string& salt,
|
||||
std::string& encrypted) {
|
||||
if (absl::StartsWith(salt, "$apr1$")) {
|
||||
return crypt_apr1(key, salt, encrypted);
|
||||
}
|
||||
if (absl::StartsWith(salt, "{SHA}")) {
|
||||
return crypt_sha(key, salt, encrypted);
|
||||
}
|
||||
if (absl::StartsWith(salt, "{SSHA}")) {
|
||||
return crypt_ssha(key, salt, encrypted);
|
||||
}
|
||||
if (absl::StartsWith(salt, "{PLAIN}")) {
|
||||
return crypt_plain(key, salt, encrypted);
|
||||
}
|
||||
if (salt.size() > 3 && salt[1] == '2' && salt[3] == '$') {
|
||||
return bcrypt(key, salt, encrypted);
|
||||
}
|
||||
// fallback to libc crypt()
|
||||
return libc_crypt(key, salt, encrypted);
|
||||
}
|
||||
} // namespace Wasm::Common::Crypto
|
||||
44
plugins/wasm-cpp/common/crypto_util.h
Normal file
44
plugins/wasm-cpp/common/crypto_util.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "openssl/hmac.h"
|
||||
#include "openssl/md5.h"
|
||||
#include "openssl/sha.h"
|
||||
|
||||
#define ASSERT(_X) assert(_X)
|
||||
|
||||
namespace Wasm::Common::Crypto {
|
||||
|
||||
std::vector<uint8_t> getShaHmac(std::string_view hash_type,
|
||||
std::string_view key, std::string_view message);
|
||||
|
||||
std::string getShaHmacBase64(std::string_view hash_type, std::string_view key,
|
||||
std::string_view message);
|
||||
|
||||
std::vector<uint8_t> getMD5(std::string_view message);
|
||||
|
||||
std::string getMD5Base64(std::string_view message);
|
||||
|
||||
bool crypt(const std::string& key, const std::string& salt,
|
||||
std::string& encrypted);
|
||||
|
||||
} // namespace Wasm::Common::Crypto
|
||||
269
plugins/wasm-cpp/common/http_util.cc
Normal file
269
plugins/wasm-cpp/common/http_util.cc
Normal file
@@ -0,0 +1,269 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "http_util.h"
|
||||
|
||||
#include "absl/strings/ascii.h"
|
||||
#include "absl/strings/match.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
|
||||
namespace Wasm::Common::Http {
|
||||
|
||||
std::string_view stripPortFromHost(std::string_view request_host) {
|
||||
// Remove port, if there is any. At Istio 1.10, port will be stripped
|
||||
// by default https://github.com/istio/istio/issues/25350.
|
||||
// Port removing code is inspired by
|
||||
// https://github.com/envoyproxy/envoy/blob/v1.17.0/source/common/http/header_utility.cc#L219
|
||||
const std::string_view::size_type port_start = request_host.rfind(':');
|
||||
if (port_start != std::string_view::npos) {
|
||||
// According to RFC3986 v6 address is always enclosed in "[]".
|
||||
// section 3.2.2.
|
||||
const auto v6_end_index = request_host.rfind("]");
|
||||
if (v6_end_index == std::string_view::npos || v6_end_index < port_start) {
|
||||
if ((port_start + 1) <= request_host.size()) {
|
||||
return request_host.substr(0, port_start);
|
||||
}
|
||||
}
|
||||
}
|
||||
return request_host;
|
||||
}
|
||||
|
||||
std::string PercentEncoding::encode(absl::string_view value,
|
||||
absl::string_view reserved_chars) {
|
||||
std::unordered_set<char> reserved_char_set{reserved_chars.begin(),
|
||||
reserved_chars.end()};
|
||||
for (size_t i = 0; i < value.size(); ++i) {
|
||||
const char& ch = value[i];
|
||||
// The escaping characters are defined in
|
||||
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses.
|
||||
//
|
||||
// We do checking for each char in the string. If the current char is
|
||||
// included in the defined escaping characters, we jump to "the slow path"
|
||||
// (append the char [encoded or not encoded] to the returned string one by
|
||||
// one) started from the current index.
|
||||
if (ch < ' ' || ch >= '~' ||
|
||||
reserved_char_set.find(ch) != reserved_char_set.end()) {
|
||||
return PercentEncoding::encode(value, i, reserved_char_set);
|
||||
}
|
||||
}
|
||||
return std::string(value);
|
||||
}
|
||||
|
||||
std::string PercentEncoding::encode(
|
||||
absl::string_view value, size_t index,
|
||||
const std::unordered_set<char>& reserved_char_set) {
|
||||
std::string encoded;
|
||||
if (index > 0) {
|
||||
absl::StrAppend(&encoded, value.substr(0, index));
|
||||
}
|
||||
|
||||
for (size_t i = index; i < value.size(); ++i) {
|
||||
const char& ch = value[i];
|
||||
if (ch < ' ' || ch >= '~' ||
|
||||
reserved_char_set.find(ch) != reserved_char_set.end()) {
|
||||
// For consistency, URI producers should use uppercase hexadecimal digits
|
||||
// for all percent-encodings.
|
||||
// https://tools.ietf.org/html/rfc3986#section-2.1.
|
||||
absl::StrAppend(&encoded, absl::StrFormat("%02X", ch));
|
||||
} else {
|
||||
encoded.push_back(ch);
|
||||
}
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
std::string PercentEncoding::decode(absl::string_view encoded) {
|
||||
std::string decoded;
|
||||
decoded.reserve(encoded.size());
|
||||
for (size_t i = 0; i < encoded.size(); ++i) {
|
||||
char ch = encoded[i];
|
||||
if (ch == '%' && i + 2 < encoded.size()) {
|
||||
const char& hi = encoded[i + 1];
|
||||
const char& lo = encoded[i + 2];
|
||||
if (absl::ascii_isdigit(hi)) {
|
||||
ch = hi - '0';
|
||||
} else {
|
||||
ch = absl::ascii_toupper(hi) - 'A' + 10;
|
||||
}
|
||||
|
||||
ch *= 16;
|
||||
if (absl::ascii_isdigit(lo)) {
|
||||
ch += lo - '0';
|
||||
} else {
|
||||
ch += absl::ascii_toupper(lo) - 'A' + 10;
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
decoded.push_back(ch);
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
||||
SystemTime httpTime(std::string_view date) {
|
||||
absl::Time time;
|
||||
static constexpr std::array<absl::string_view, 4> rfc7231_date_formats = {
|
||||
"%a, %d %b %Y %H:%M:%S GMT", "%a, %d %b %Y %H:%M:%S GMT+00:00",
|
||||
"%A, %d-%b-%y %H:%M:%S GMT", "%a %b %e %H:%M:%S %Y"};
|
||||
for (auto format : rfc7231_date_formats) {
|
||||
if (absl::ParseTime(format, absl::string_view(date.data(), date.size()),
|
||||
&time, nullptr)) {
|
||||
return absl::ToChronoTime(time);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QueryParams parseQueryString(absl::string_view url) {
|
||||
size_t start = url.find('?');
|
||||
if (start == std::string::npos) {
|
||||
QueryParams params;
|
||||
return params;
|
||||
}
|
||||
|
||||
start++;
|
||||
return parseParameters(url, start, /*decode_params=*/false);
|
||||
}
|
||||
|
||||
QueryParams parseAndDecodeQueryString(absl::string_view url) {
|
||||
size_t start = url.find('?');
|
||||
if (start == std::string::npos) {
|
||||
QueryParams params;
|
||||
return params;
|
||||
}
|
||||
|
||||
start++;
|
||||
return parseParameters(url, start, /*decode_params=*/true);
|
||||
}
|
||||
|
||||
QueryParams parseFromBody(absl::string_view body) {
|
||||
return parseParameters(body, 0, /*decode_params=*/true);
|
||||
}
|
||||
|
||||
inline std::string subspan(absl::string_view source, size_t start, size_t end) {
|
||||
return {source.data() + start, end - start};
|
||||
}
|
||||
|
||||
QueryParams parseParameters(absl::string_view data, size_t start,
|
||||
bool decode_params) {
|
||||
QueryParams params;
|
||||
|
||||
while (start < data.size()) {
|
||||
size_t end = data.find('&', start);
|
||||
if (end == std::string::npos) {
|
||||
end = data.size();
|
||||
}
|
||||
absl::string_view param(data.data() + start, end - start);
|
||||
|
||||
const size_t equal = param.find('=');
|
||||
if (equal != std::string::npos) {
|
||||
const auto param_name = subspan(data, start, start + equal);
|
||||
const auto param_value = subspan(data, start + equal + 1, end);
|
||||
params.emplace(
|
||||
decode_params ? PercentEncoding::decode(param_name) : param_name,
|
||||
decode_params ? PercentEncoding::decode(param_value) : param_value);
|
||||
} else {
|
||||
params.emplace(subspan(data, start, end), "");
|
||||
}
|
||||
|
||||
start = end + 1;
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
std::vector<std::string> getAllOfHeader(std::string_view key) {
|
||||
std::vector<std::string> result;
|
||||
auto headers = getRequestHeaderPairs()->pairs();
|
||||
for (auto& header : headers) {
|
||||
if (absl::EqualsIgnoreCase(header.first, key)) {
|
||||
result.push_back(std::string(header.second));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void forEachCookie(
|
||||
std::string_view cookie_header,
|
||||
const std::function<bool(absl::string_view, absl::string_view)>&
|
||||
cookie_consumer) {
|
||||
auto cookie_headers = getAllOfHeader(cookie_header);
|
||||
|
||||
for (auto& cookie_header_value : cookie_headers) {
|
||||
// Split the cookie header into individual cookies.
|
||||
for (const auto& s :
|
||||
absl::StrSplit(cookie_header_value, ";", absl::SkipEmpty())) {
|
||||
// Find the key part of the cookie (i.e. the name of the cookie).
|
||||
size_t first_non_space = s.find_first_not_of(' ');
|
||||
size_t equals_index = s.find('=');
|
||||
if (equals_index == absl::string_view::npos) {
|
||||
// The cookie is malformed if it does not have an `=`. Continue
|
||||
// checking other cookies in this header.
|
||||
continue;
|
||||
}
|
||||
absl::string_view k =
|
||||
s.substr(first_non_space, equals_index - first_non_space);
|
||||
absl::string_view v = s.substr(equals_index + 1, s.size() - 1);
|
||||
|
||||
// Cookie values may be wrapped in double quotes.
|
||||
// https://tools.ietf.org/html/rfc6265#section-4.1.1
|
||||
if (v.size() >= 2 && v.back() == '"' && v[0] == '"') {
|
||||
v = v.substr(1, v.size() - 2);
|
||||
}
|
||||
|
||||
if (!cookie_consumer(k, v)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::string> parseCookies(
|
||||
const std::function<bool(std::string_view)>& key_filter) {
|
||||
std::unordered_map<std::string, std::string> cookies;
|
||||
forEachCookie(
|
||||
Header::Cookie,
|
||||
[&cookies, &key_filter](std::string_view k, std::string_view v) -> bool {
|
||||
if (key_filter(k)) {
|
||||
cookies.emplace(k, v);
|
||||
}
|
||||
|
||||
// continue iterating until all cookies are processed.
|
||||
return true;
|
||||
});
|
||||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
std::string buildOriginalUri(std::optional<uint32_t> max_path_length) {
|
||||
auto path_ptr = getRequestHeader(Header::Path);
|
||||
auto path = path_ptr->view();
|
||||
if (path.empty()) {
|
||||
return "";
|
||||
}
|
||||
auto envoy_path_ptr = getRequestHeader(Header::EnvoyOriginalPath);
|
||||
auto envoy_path = envoy_path_ptr->view();
|
||||
std::string_view final_path(envoy_path.empty() ? path : envoy_path);
|
||||
if (max_path_length && final_path.length() > max_path_length) {
|
||||
final_path = final_path.substr(0, max_path_length.value());
|
||||
}
|
||||
auto scheme_ptr = getRequestHeader(Header::Scheme);
|
||||
auto scheme = scheme_ptr->view();
|
||||
auto host_ptr = getRequestHeader(Header::Host);
|
||||
auto host = host_ptr->view();
|
||||
return absl::StrCat(scheme, "://", host, final_path);
|
||||
}
|
||||
|
||||
} // namespace Wasm::Common::Http
|
||||
145
plugins/wasm-cpp/common/http_util.h
Normal file
145
plugins/wasm-cpp/common/http_util.h
Normal file
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/time/time.h"
|
||||
|
||||
#ifndef NULL_PLUGIN
|
||||
|
||||
#include "proxy_wasm_intrinsics.h"
|
||||
|
||||
#else
|
||||
|
||||
#include "include/proxy-wasm/null_plugin.h"
|
||||
using namespace proxy_wasm::null_plugin;
|
||||
using proxy_wasm::FilterDataStatus;
|
||||
using proxy_wasm::FilterHeadersStatus;
|
||||
|
||||
#endif
|
||||
|
||||
namespace Wasm::Common::Http {
|
||||
|
||||
using QueryParams = std::map<std::string, std::string>;
|
||||
using SystemTime = std::chrono::time_point<std::chrono::system_clock>;
|
||||
|
||||
namespace Header {
|
||||
constexpr std::string_view Scheme(":scheme");
|
||||
constexpr std::string_view Method(":method");
|
||||
constexpr std::string_view Host(":authority");
|
||||
constexpr std::string_view Path(":path");
|
||||
constexpr std::string_view EnvoyOriginalPath("x-envoy-original-path");
|
||||
constexpr std::string_view Accept("accept");
|
||||
constexpr std::string_view ContentMD5("content-md5");
|
||||
constexpr std::string_view ContentType("content-type");
|
||||
constexpr std::string_view ContentLength("content-length");
|
||||
constexpr std::string_view UserAgent("user-agent");
|
||||
constexpr std::string_view Date("date");
|
||||
constexpr std::string_view Cookie("cookie");
|
||||
} // namespace Header
|
||||
|
||||
namespace ContentTypeValues {
|
||||
constexpr std::string_view Grpc{"application/grpc"};
|
||||
}
|
||||
|
||||
class PercentEncoding {
|
||||
public:
|
||||
/**
|
||||
* Encodes string view to its percent encoded representation. Non-visible
|
||||
* ASCII is always escaped, in addition to a given list of reserved chars.
|
||||
*
|
||||
* @param value supplies string to be encoded.
|
||||
* @param reserved_chars list of reserved chars to escape. By default the
|
||||
* escaped chars in
|
||||
* https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses
|
||||
* are used.
|
||||
* @return std::string percent-encoded string.
|
||||
*/
|
||||
static std::string encode(absl::string_view value,
|
||||
absl::string_view reserved_chars = "%");
|
||||
|
||||
/**
|
||||
* Decodes string view from its percent encoded representation.
|
||||
* @param encoded supplies string to be decoded.
|
||||
* @return std::string decoded string
|
||||
* https://tools.ietf.org/html/rfc3986#section-2.1.
|
||||
*/
|
||||
static std::string decode(absl::string_view value);
|
||||
|
||||
private:
|
||||
// Encodes string view to its percent encoded representation, with start
|
||||
// index.
|
||||
static std::string encode(absl::string_view value, size_t index,
|
||||
const std::unordered_set<char>& reserved_char_set);
|
||||
};
|
||||
|
||||
SystemTime httpTime(std::string_view date);
|
||||
|
||||
inline bool timePointValid(SystemTime time_point) {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
time_point.time_since_epoch())
|
||||
.count() != 0;
|
||||
}
|
||||
|
||||
std::string_view stripPortFromHost(std::string_view request_host);
|
||||
|
||||
/**
|
||||
* Parse a URL into query parameters.
|
||||
* @param url supplies the url to parse.
|
||||
* @return QueryParams the parsed parameters, if any.
|
||||
*/
|
||||
QueryParams parseQueryString(absl::string_view url);
|
||||
|
||||
/**
|
||||
* Parse a URL into query parameters.
|
||||
* @param url supplies the url to parse.
|
||||
* @return QueryParams the parsed and percent-decoded parameters, if any.
|
||||
*/
|
||||
QueryParams parseAndDecodeQueryString(absl::string_view url);
|
||||
|
||||
/**
|
||||
* Parse a a request body into query parameters.
|
||||
* @param body supplies the body to parse.
|
||||
* @return QueryParams the parsed parameters, if any.
|
||||
*/
|
||||
QueryParams parseFromBody(absl::string_view body);
|
||||
|
||||
/**
|
||||
* Parse query parameters from a URL or body.
|
||||
* @param data supplies the data to parse.
|
||||
* @param start supplies the offset within the data.
|
||||
* @param decode_params supplies the flag whether to percent-decode the parsed
|
||||
* parameters (both name and value). Set to false to keep the parameters
|
||||
* encoded.
|
||||
* @return QueryParams the parsed parameters, if any.
|
||||
*/
|
||||
QueryParams parseParameters(absl::string_view data, size_t start,
|
||||
bool decode_params);
|
||||
|
||||
std::vector<std::string> getAllOfHeader(std::string_view key);
|
||||
|
||||
std::unordered_map<std::string, std::string> parseCookies(
|
||||
const std::function<bool(std::string_view)>& key_filter);
|
||||
|
||||
std::string buildOriginalUri(std::optional<uint32_t> max_path_length);
|
||||
|
||||
} // namespace Wasm::Common::Http
|
||||
199
plugins/wasm-cpp/common/json_util.cc
Normal file
199
plugins/wasm-cpp/common/json_util.cc
Normal file
@@ -0,0 +1,199 @@
|
||||
/* Copyright 2020 Istio Authors. All Rights Reserved.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "json_util.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "absl/strings/numbers.h"
|
||||
|
||||
namespace Wasm {
|
||||
namespace Common {
|
||||
|
||||
std::optional<JsonObject> JsonParse(std::string_view str) {
|
||||
const auto result = JsonObject::parse(str, nullptr, false);
|
||||
if (result.is_discarded() || !result.is_object()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template <>
|
||||
std::pair<std::optional<int64_t>, JsonParserResultDetail> JsonValueAs<int64_t>(
|
||||
const JsonObject& j) {
|
||||
if (j.is_number()) {
|
||||
return std::make_pair(j.get<int64_t>(), JsonParserResultDetail::OK);
|
||||
} else if (j.is_string()) {
|
||||
int64_t result = 0;
|
||||
if (absl::SimpleAtoi(j.get_ref<std::string const&>(), &result)) {
|
||||
return std::make_pair(result, JsonParserResultDetail::OK);
|
||||
} else {
|
||||
return std::make_pair(std::nullopt,
|
||||
JsonParserResultDetail::INVALID_VALUE);
|
||||
}
|
||||
}
|
||||
return std::make_pair(std::nullopt, JsonParserResultDetail::TYPE_ERROR);
|
||||
}
|
||||
|
||||
template <>
|
||||
std::pair<std::optional<uint64_t>, JsonParserResultDetail>
|
||||
JsonValueAs<uint64_t>(const JsonObject& j) {
|
||||
if (j.is_number()) {
|
||||
return std::make_pair(j.get<uint64_t>(), JsonParserResultDetail::OK);
|
||||
} else if (j.is_string()) {
|
||||
uint64_t result = 0;
|
||||
if (absl::SimpleAtoi(j.get_ref<std::string const&>(), &result)) {
|
||||
return std::make_pair(result, JsonParserResultDetail::OK);
|
||||
} else {
|
||||
return std::make_pair(std::nullopt,
|
||||
JsonParserResultDetail::INVALID_VALUE);
|
||||
}
|
||||
}
|
||||
return std::make_pair(std::nullopt, JsonParserResultDetail::TYPE_ERROR);
|
||||
}
|
||||
|
||||
template <>
|
||||
std::pair<std::optional<std::string_view>, JsonParserResultDetail>
|
||||
JsonValueAs<std::string_view>(const JsonObject& j) {
|
||||
if (j.is_string()) {
|
||||
return std::make_pair(std::string_view(j.get_ref<std::string const&>()),
|
||||
JsonParserResultDetail::OK);
|
||||
}
|
||||
return std::make_pair(std::nullopt, JsonParserResultDetail::TYPE_ERROR);
|
||||
}
|
||||
|
||||
template <>
|
||||
std::pair<std::optional<std::string>, JsonParserResultDetail>
|
||||
JsonValueAs<std::string>(const JsonObject& j) {
|
||||
if (j.is_string()) {
|
||||
return std::make_pair(j.get_ref<std::string const&>(),
|
||||
JsonParserResultDetail::OK);
|
||||
}
|
||||
if (j.is_number_unsigned()) {
|
||||
return std::make_pair(
|
||||
std::to_string((unsigned long long)(j.get<uint64_t>())),
|
||||
JsonParserResultDetail::OK);
|
||||
}
|
||||
return std::make_pair(std::nullopt, JsonParserResultDetail::TYPE_ERROR);
|
||||
}
|
||||
|
||||
template <>
|
||||
std::pair<std::optional<bool>, JsonParserResultDetail> JsonValueAs<bool>(
|
||||
const JsonObject& j) {
|
||||
if (j.is_boolean()) {
|
||||
return std::make_pair(j.get<bool>(), JsonParserResultDetail::OK);
|
||||
}
|
||||
if (j.is_string()) {
|
||||
const std::string& v = j.get_ref<std::string const&>();
|
||||
if (v == "true") {
|
||||
return std::make_pair(true, JsonParserResultDetail::OK);
|
||||
} else if (v == "false") {
|
||||
return std::make_pair(false, JsonParserResultDetail::OK);
|
||||
} else {
|
||||
return std::make_pair(std::nullopt,
|
||||
JsonParserResultDetail::INVALID_VALUE);
|
||||
}
|
||||
}
|
||||
return std::make_pair(std::nullopt, JsonParserResultDetail::TYPE_ERROR);
|
||||
}
|
||||
|
||||
template <>
|
||||
std::pair<std::optional<std::vector<std::string_view>>, JsonParserResultDetail>
|
||||
JsonValueAs<std::vector<std::string_view>>(const JsonObject& j) {
|
||||
std::pair<std::optional<std::vector<std::string_view>>,
|
||||
JsonParserResultDetail>
|
||||
values = std::make_pair(std::nullopt, JsonParserResultDetail::OK);
|
||||
if (j.is_array()) {
|
||||
for (const auto& elt : j) {
|
||||
if (!elt.is_string()) {
|
||||
values.first = std::nullopt;
|
||||
values.second = JsonParserResultDetail::TYPE_ERROR;
|
||||
return values;
|
||||
}
|
||||
if (!values.first.has_value()) {
|
||||
values.first = std::vector<std::string_view>();
|
||||
}
|
||||
values.first->emplace_back(elt.get_ref<std::string const&>());
|
||||
}
|
||||
return values;
|
||||
}
|
||||
values.second = JsonParserResultDetail::TYPE_ERROR;
|
||||
return values;
|
||||
}
|
||||
|
||||
template <>
|
||||
std::pair<std::optional<JsonObject>, JsonParserResultDetail>
|
||||
JsonValueAs<JsonObject>(const JsonObject& j) {
|
||||
if (j.is_object()) {
|
||||
return std::make_pair(j.get<JsonObject>(), JsonParserResultDetail::OK);
|
||||
}
|
||||
return std::make_pair(std::nullopt, JsonParserResultDetail::TYPE_ERROR);
|
||||
}
|
||||
|
||||
bool JsonArrayIterate(
|
||||
const JsonObject& j, std::string_view field,
|
||||
const std::function<bool(const JsonObject& elt)>& visitor) {
|
||||
auto it = j.find(field);
|
||||
if (it == j.end()) {
|
||||
return true;
|
||||
}
|
||||
if (!it.value().is_array()) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& elt : it.value().items()) {
|
||||
if (!visitor(elt.value())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool JsonObjectIterate(const JsonObject& j, std::string_view field,
|
||||
const std::function<bool(std::string key)>& visitor) {
|
||||
auto it = j.find(field);
|
||||
if (it == j.end()) {
|
||||
return true;
|
||||
}
|
||||
if (!it.value().is_object()) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& elt : it.value().items()) {
|
||||
auto json_value = JsonValueAs<std::string>(elt.key());
|
||||
if (json_value.second != JsonParserResultDetail::OK) {
|
||||
return false;
|
||||
}
|
||||
if (!visitor(json_value.first.value())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool JsonObjectIterate(const JsonObject& j,
|
||||
const std::function<bool(std::string key)>& visitor) {
|
||||
for (const auto& elt : j.items()) {
|
||||
auto json_value = JsonValueAs<std::string>(elt.key());
|
||||
if (json_value.second != JsonParserResultDetail::OK) {
|
||||
return false;
|
||||
}
|
||||
if (!visitor(json_value.first.value())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
} // namespace Wasm
|
||||
120
plugins/wasm-cpp/common/json_util.h
Normal file
120
plugins/wasm-cpp/common/json_util.h
Normal file
@@ -0,0 +1,120 @@
|
||||
/* Copyright 2020 Istio Authors. All Rights Reserved.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/nlohmann_json.hpp"
|
||||
|
||||
/**
|
||||
* Utilities for working with JSON without exceptions.
|
||||
*/
|
||||
namespace Wasm {
|
||||
namespace Common {
|
||||
|
||||
using JsonObject = ::nlohmann::json;
|
||||
|
||||
enum JsonParserResultDetail {
|
||||
UNKNOWN,
|
||||
OK,
|
||||
OUT_OF_RANGE,
|
||||
TYPE_ERROR,
|
||||
INVALID_VALUE,
|
||||
};
|
||||
|
||||
std::optional<JsonObject> JsonParse(std::string_view str);
|
||||
|
||||
template <typename T>
|
||||
std::pair<std::optional<T>, JsonParserResultDetail> JsonValueAs(
|
||||
const JsonObject&) {
|
||||
static_assert(true, "Unsupported Type");
|
||||
}
|
||||
|
||||
template <>
|
||||
std::pair<std::optional<std::string_view>, JsonParserResultDetail>
|
||||
JsonValueAs<std::string_view>(const JsonObject& j);
|
||||
|
||||
template <>
|
||||
std::pair<std::optional<std::string>, JsonParserResultDetail>
|
||||
JsonValueAs<std::string>(const JsonObject& j);
|
||||
|
||||
template <>
|
||||
std::pair<std::optional<int64_t>, JsonParserResultDetail> JsonValueAs<int64_t>(
|
||||
const JsonObject& j);
|
||||
|
||||
template <>
|
||||
std::pair<std::optional<uint64_t>, JsonParserResultDetail>
|
||||
JsonValueAs<uint64_t>(const JsonObject& j);
|
||||
|
||||
template <>
|
||||
std::pair<std::optional<bool>, JsonParserResultDetail> JsonValueAs<bool>(
|
||||
const JsonObject& j);
|
||||
|
||||
template <>
|
||||
std::pair<std::optional<JsonObject>, JsonParserResultDetail>
|
||||
JsonValueAs<JsonObject>(const JsonObject& j);
|
||||
|
||||
template <>
|
||||
std::pair<std::optional<std::vector<std::string_view>>, JsonParserResultDetail>
|
||||
JsonValueAs<std::vector<std::string_view>>(const JsonObject& j);
|
||||
|
||||
template <class T>
|
||||
class JsonGetField {
|
||||
public:
|
||||
JsonGetField(const JsonObject& j, std::string_view field);
|
||||
const JsonParserResultDetail& detail() { return detail_; }
|
||||
T value() { return object_; }
|
||||
T value_or(T v) {
|
||||
if (detail_ != JsonParserResultDetail::OK)
|
||||
return v;
|
||||
else
|
||||
return object_;
|
||||
};
|
||||
|
||||
private:
|
||||
JsonParserResultDetail detail_;
|
||||
T object_;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
JsonGetField<T>::JsonGetField(const JsonObject& j, std::string_view field) {
|
||||
auto it = j.find(field);
|
||||
if (it == j.end()) {
|
||||
detail_ = JsonParserResultDetail::OUT_OF_RANGE;
|
||||
return;
|
||||
}
|
||||
auto value = JsonValueAs<T>(it.value());
|
||||
detail_ = value.second;
|
||||
if (value.first.has_value()) {
|
||||
object_ = value.first.value();
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over an optional array field.
|
||||
// Returns false if set and not an array, or any of the visitor calls returns
|
||||
// false.
|
||||
bool JsonArrayIterate(
|
||||
const JsonObject& j, std::string_view field,
|
||||
const std::function<bool(const JsonObject& elt)>& visitor);
|
||||
|
||||
// Iterate over an optional object field key set.
|
||||
// Returns false if set and not an object, or any of the visitor calls returns
|
||||
// false.
|
||||
bool JsonObjectIterate(const JsonObject& j, std::string_view field,
|
||||
const std::function<bool(std::string key)>& visitor);
|
||||
bool JsonObjectIterate(const JsonObject& j,
|
||||
const std::function<bool(std::string key)>& visitor);
|
||||
|
||||
} // namespace Common
|
||||
} // namespace Wasm
|
||||
22875
plugins/wasm-cpp/common/nlohmann_json.hpp
Normal file
22875
plugins/wasm-cpp/common/nlohmann_json.hpp
Normal file
File diff suppressed because it is too large
Load Diff
61
plugins/wasm-cpp/common/regex.h
Normal file
61
plugins/wasm-cpp/common/regex.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "re2/re2.h"
|
||||
|
||||
namespace Wasm::Common::Regex {
|
||||
|
||||
class CompiledGoogleReMatcher {
|
||||
public:
|
||||
CompiledGoogleReMatcher(const std::string& regex,
|
||||
bool do_program_size_check = true)
|
||||
: regex_(regex, re2::RE2::Quiet) {
|
||||
if (!regex_.ok()) {
|
||||
throw std::runtime_error(regex_.error());
|
||||
}
|
||||
if (do_program_size_check) {
|
||||
const auto regex_program_size =
|
||||
static_cast<uint32_t>(regex_.ProgramSize());
|
||||
if (regex_program_size > 100) {
|
||||
throw std::runtime_error("too complex regex: " + regex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool match(std::string_view value) const {
|
||||
return re2::RE2::FullMatch(re2::StringPiece(value.data(), value.size()),
|
||||
regex_);
|
||||
}
|
||||
|
||||
std::string replaceAll(std::string_view value,
|
||||
std::string_view substitution) const {
|
||||
std::string result = std::string(value);
|
||||
re2::RE2::GlobalReplace(
|
||||
&result, regex_,
|
||||
re2::StringPiece(substitution.data(), substitution.size()));
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
const re2::RE2 regex_;
|
||||
};
|
||||
|
||||
} // namespace Wasm::Common::Regex
|
||||
461
plugins/wasm-cpp/common/route_rule_matcher.h
Normal file
461
plugins/wasm-cpp/common/route_rule_matcher.h
Normal file
@@ -0,0 +1,461 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/strings/match.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "common/json_util.h"
|
||||
#include "http_util.h"
|
||||
|
||||
#ifndef NULL_PLUGIN
|
||||
|
||||
#include "proxy_wasm_intrinsics.h"
|
||||
|
||||
#else
|
||||
|
||||
#include "include/proxy-wasm/null_plugin.h"
|
||||
using namespace proxy_wasm::null_plugin;
|
||||
using proxy_wasm::FilterDataStatus;
|
||||
using proxy_wasm::FilterHeadersStatus;
|
||||
|
||||
#endif
|
||||
|
||||
#define GET_HEADER_VIEW(key, name) \
|
||||
auto name##_ptr = getRequestHeader(key); \
|
||||
auto name = name##_ptr->view();
|
||||
|
||||
#define GET_RESPONSE_HEADER_VIEW(key, name) \
|
||||
auto name##_ptr = getResponseHeader(key); \
|
||||
auto name = name##_ptr->view();
|
||||
|
||||
using ::nlohmann::json;
|
||||
using ::Wasm::Common::JsonArrayIterate;
|
||||
using ::Wasm::Common::JsonGetField;
|
||||
using ::Wasm::Common::JsonObjectIterate;
|
||||
using ::Wasm::Common::JsonValueAs;
|
||||
|
||||
template <typename PluginConfig>
|
||||
class RouteRuleMatcher {
|
||||
public:
|
||||
enum CATEGORY { Route, Host };
|
||||
enum MATCH_TYPE { Prefix, Exact, Suffix };
|
||||
struct RuleConfig {
|
||||
CATEGORY category;
|
||||
std::unordered_set<std::string> routes;
|
||||
std::vector<std::pair<MATCH_TYPE, std::string>> hosts;
|
||||
PluginConfig config;
|
||||
};
|
||||
struct AuthRuleConfig {
|
||||
RuleConfig rule_config;
|
||||
std::unordered_set<std::string> allow_set;
|
||||
bool has_local_config = false;
|
||||
};
|
||||
RouteRuleMatcher() = default;
|
||||
virtual ~RouteRuleMatcher() = default;
|
||||
|
||||
void setInvalidConfig() { invalid_config_ = true; }
|
||||
|
||||
std::vector<std::pair<int, std::reference_wrapper<PluginConfig>>> getRules() {
|
||||
std::vector<std::pair<int, std::reference_wrapper<PluginConfig>>> rules;
|
||||
rules.reserve(rule_config_.size() + 1);
|
||||
if (global_config_) {
|
||||
rules.emplace_back(0, global_config_.value());
|
||||
}
|
||||
for (int i = 0; i < rule_config_.size(); i++) {
|
||||
rules.emplace_back(i + 1, rule_config_[i].config);
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
||||
FilterHeadersStatus onHeaders(
|
||||
const std::function<FilterHeadersStatus(const PluginConfig&)> process) {
|
||||
if (invalid_config_) {
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
auto config = getMatchConfig();
|
||||
if (!config.second) {
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
return process(config.second.value());
|
||||
}
|
||||
|
||||
bool checkRule(const std::function<bool(const PluginConfig&)> checkPlugin) {
|
||||
if (invalid_config_) {
|
||||
return true;
|
||||
}
|
||||
auto config = getMatchConfig();
|
||||
if (!config.second) {
|
||||
// No config need to check
|
||||
return true;
|
||||
}
|
||||
return checkPlugin(config.second.value());
|
||||
}
|
||||
|
||||
bool checkAuthRule(
|
||||
const std::function<
|
||||
bool(const PluginConfig&,
|
||||
const std::optional<std::unordered_set<std::string>>& allow_set)>
|
||||
checkPlugin) {
|
||||
if (invalid_config_) {
|
||||
return true;
|
||||
}
|
||||
auto config = getMatchAuthConfig();
|
||||
if (!config.first) {
|
||||
// No config need to check
|
||||
LOG_DEBUG("no match config");
|
||||
return true;
|
||||
}
|
||||
return checkPlugin(config.first.value(), config.second);
|
||||
}
|
||||
|
||||
bool checkRuleWithId(
|
||||
const std::function<bool(int, const PluginConfig&)> checkPlugin) {
|
||||
if (invalid_config_) {
|
||||
return true;
|
||||
}
|
||||
auto config = getMatchConfig();
|
||||
if (!config.second) {
|
||||
// No config need to check
|
||||
return true;
|
||||
}
|
||||
return checkPlugin(config.first, config.second.value());
|
||||
}
|
||||
|
||||
std::pair<int, std::optional<std::reference_wrapper<PluginConfig>>>
|
||||
getMatchConfig() {
|
||||
auto request_host_header = getRequestHeader(":authority");
|
||||
auto request_host = request_host_header->view();
|
||||
std::string route_name;
|
||||
getValue({"route_name"}, &route_name);
|
||||
std::optional<std::reference_wrapper<PluginConfig>> match_config;
|
||||
int rule_id;
|
||||
if (global_config_) {
|
||||
rule_id = 0;
|
||||
match_config = global_config_.value();
|
||||
}
|
||||
for (int i = 0; i < rule_config_.size(); ++i) {
|
||||
auto& rule = rule_config_[i];
|
||||
if (rule.category == CATEGORY::Host) {
|
||||
if (hostMatch(rule, request_host)) {
|
||||
rule_id = i + 1;
|
||||
match_config = rule.config;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// category == Route
|
||||
if (rule.routes.find(route_name) != rule.routes.end()) {
|
||||
rule_id = i + 1;
|
||||
match_config = rule.config;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match_config) {
|
||||
return std::make_pair(rule_id, match_config);
|
||||
}
|
||||
return std::make_pair(-1, match_config);
|
||||
}
|
||||
|
||||
std::pair<
|
||||
std::optional<std::reference_wrapper<PluginConfig>>,
|
||||
std::optional<std::reference_wrapper<std::unordered_set<std::string>>>>
|
||||
getMatchAuthConfig() {
|
||||
auto request_host_header = getRequestHeader(":authority");
|
||||
auto request_host = request_host_header->view();
|
||||
std::string route_name;
|
||||
getValue({"route_name"}, &route_name);
|
||||
std::optional<std::reference_wrapper<PluginConfig>> match_config;
|
||||
std::optional<std::reference_wrapper<std::unordered_set<std::string>>>
|
||||
allow_set;
|
||||
if (global_config_) {
|
||||
match_config = global_config_.value();
|
||||
}
|
||||
if (auth_rule_config_.empty()) {
|
||||
return std::make_pair(match_config, std::nullopt);
|
||||
}
|
||||
bool is_matched = false;
|
||||
for (auto& auth_rule : auth_rule_config_) {
|
||||
if (auth_rule.rule_config.category == CATEGORY::Host) {
|
||||
if (hostMatch(auth_rule.rule_config, request_host)) {
|
||||
is_matched = true;
|
||||
if (auth_rule.has_local_config) {
|
||||
LOG_DEBUG("has local config");
|
||||
match_config = auth_rule.rule_config.config;
|
||||
} else {
|
||||
LOG_DEBUG("has not local config");
|
||||
allow_set = auth_rule.allow_set;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// category == Route
|
||||
if (auth_rule.rule_config.routes.find(route_name) !=
|
||||
auth_rule.rule_config.routes.end()) {
|
||||
is_matched = true;
|
||||
if (auth_rule.has_local_config) {
|
||||
match_config = auth_rule.rule_config.config;
|
||||
} else {
|
||||
allow_set = auth_rule.allow_set;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return is_matched ? std::make_pair(match_config, allow_set)
|
||||
: std::make_pair(std::nullopt, std::nullopt);
|
||||
}
|
||||
|
||||
void setEmptyGlobalConfig() { global_config_ = PluginConfig{}; }
|
||||
|
||||
bool parseRuleConfig(const json& config) {
|
||||
bool has_rules = false;
|
||||
int32_t key_count = config.size();
|
||||
auto it = config.find("_rules_");
|
||||
if (it != config.end()) {
|
||||
has_rules = true;
|
||||
key_count--;
|
||||
}
|
||||
PluginConfig plugin_config;
|
||||
// has other config fields
|
||||
if (key_count > 0 && parsePluginConfig(config, plugin_config)) {
|
||||
global_config_ = std::move(plugin_config);
|
||||
}
|
||||
if (!has_rules) {
|
||||
return global_config_ ? true : false;
|
||||
}
|
||||
auto rules = it.value();
|
||||
if (!rules.is_array()) {
|
||||
LOG_WARN("'_rules_' field is not an array");
|
||||
return false;
|
||||
}
|
||||
for (const auto& item : rules.items()) {
|
||||
RuleConfig rule;
|
||||
auto config = item.value();
|
||||
if (!parsePluginConfig(config, rule.config)) {
|
||||
LOG_WARN("parse rule's config failed");
|
||||
return false;
|
||||
}
|
||||
if (!parseRouteMatchConfig(config, rule.routes)) {
|
||||
LOG_WARN("failed to parse configuration for _match_route_");
|
||||
return false;
|
||||
}
|
||||
if (!parseDomainMatchConfig(config, rule.hosts)) {
|
||||
LOG_WARN("failed to parse configuration for _match_domain_");
|
||||
return false;
|
||||
}
|
||||
auto no_route = rule.routes.empty();
|
||||
auto no_host = rule.hosts.empty();
|
||||
if ((no_route && no_host) || (!no_route && !no_host)) {
|
||||
LOG_WARN(
|
||||
"there is only one of '_match_route_' and '_match_domain_' can "
|
||||
"present in configuration.");
|
||||
return false;
|
||||
}
|
||||
if (!no_route) {
|
||||
rule.category = CATEGORY::Route;
|
||||
} else {
|
||||
rule.category = CATEGORY::Host;
|
||||
}
|
||||
rule_config_.push_back(std::move(rule));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseAuthRuleConfig(const json& config) {
|
||||
bool has_rules = false;
|
||||
int32_t key_count = config.size();
|
||||
auto it = config.find("_rules_");
|
||||
if (it != config.end()) {
|
||||
has_rules = true;
|
||||
key_count--;
|
||||
}
|
||||
PluginConfig plugin_config;
|
||||
// has other config fields
|
||||
if (key_count > 0 && parsePluginConfig(config, plugin_config)) {
|
||||
global_config_ = std::move(plugin_config);
|
||||
}
|
||||
if (!has_rules) {
|
||||
return global_config_ ? true : false;
|
||||
}
|
||||
auto rules = it.value();
|
||||
if (!rules.is_array()) {
|
||||
LOG_WARN("'_rules_' field is not an array");
|
||||
return false;
|
||||
}
|
||||
for (const auto& item : rules.items()) {
|
||||
AuthRuleConfig auth_rule;
|
||||
auto config = item.value();
|
||||
auto has_allow = config.find("allow");
|
||||
if (has_allow != config.end()) {
|
||||
LOG_DEBUG("has allow filed");
|
||||
if (!JsonArrayIterate(config, "allow", [&](const json& allow) -> bool {
|
||||
auto parse_result = JsonValueAs<std::string>(allow);
|
||||
if (parse_result.second !=
|
||||
Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!parse_result.first) {
|
||||
LOG_WARN(
|
||||
"failed to parse 'allow' field in filter configuration.");
|
||||
return false;
|
||||
}
|
||||
LOG_DEBUG(parse_result.first.value());
|
||||
auth_rule.allow_set.insert(parse_result.first.value());
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for allow");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!parsePluginConfig(config, auth_rule.rule_config.config)) {
|
||||
if (has_allow == config.end()) {
|
||||
LOG_WARN("parse rule's config failed");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
auth_rule.has_local_config = true;
|
||||
}
|
||||
if (!parseRouteMatchConfig(config, auth_rule.rule_config.routes)) {
|
||||
LOG_WARN("failed to parse configuration for _match_route_");
|
||||
return false;
|
||||
}
|
||||
if (!parseDomainMatchConfig(config, auth_rule.rule_config.hosts)) {
|
||||
LOG_WARN("failed to parse configuration for _match_domain_");
|
||||
return false;
|
||||
}
|
||||
auto no_route = auth_rule.rule_config.routes.empty();
|
||||
auto no_host = auth_rule.rule_config.hosts.empty();
|
||||
if ((no_route && no_host) || (!no_route && !no_host)) {
|
||||
LOG_WARN(
|
||||
"there is only one of '_match_route_' and '_match_domain_' can "
|
||||
"present in configuration.");
|
||||
return false;
|
||||
}
|
||||
if (!no_route) {
|
||||
auth_rule.rule_config.category = CATEGORY::Route;
|
||||
} else {
|
||||
auth_rule.rule_config.category = CATEGORY::Host;
|
||||
}
|
||||
auth_rule_config_.push_back(std::move(auth_rule));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual bool parsePluginConfig(const json&, PluginConfig&) = 0;
|
||||
|
||||
private:
|
||||
bool hostMatch(const RuleConfig& rule, std::string_view request_host) {
|
||||
if (rule.hosts.empty()) {
|
||||
// If no host specified, consider this rule applies to all host.
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const auto& host_match : rule.hosts) {
|
||||
const auto& host = host_match.second;
|
||||
switch (host_match.first) {
|
||||
case MATCH_TYPE::Suffix:
|
||||
if (absl::EndsWith(
|
||||
absl::string_view(request_host.data(), request_host.size()),
|
||||
absl::string_view(host.data(), host.size()))) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case MATCH_TYPE::Prefix:
|
||||
if (absl::StartsWith(
|
||||
absl::string_view(request_host.data(), request_host.size()),
|
||||
absl::string_view(host.data(), host.size()))) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case MATCH_TYPE::Exact:
|
||||
if (request_host == host_match.second) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
LOG_WARN(absl::StrCat("unexpected host match pattern"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool parseRouteMatchConfig(const json& config,
|
||||
std::unordered_set<std::string>& routes) {
|
||||
return JsonArrayIterate(
|
||||
config, "_match_route_", [&](const json& route) -> bool {
|
||||
auto parse_result = JsonValueAs<std::string>(route);
|
||||
if (parse_result.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!parse_result.first) {
|
||||
LOG_WARN(
|
||||
"failed to parse '_match_route_' field in filter "
|
||||
"configuration.");
|
||||
return false;
|
||||
}
|
||||
routes.insert(parse_result.first.value());
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool parseDomainMatchConfig(
|
||||
const json& config,
|
||||
std::vector<std::pair<MATCH_TYPE, std::string>>& hosts) {
|
||||
return JsonArrayIterate(
|
||||
config, "_match_domain_", [&](const json& host) -> bool {
|
||||
auto parse_result = JsonValueAs<std::string>(host);
|
||||
if (parse_result.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!parse_result.first) {
|
||||
LOG_WARN(
|
||||
"failed to parse '_match_domain_' field in filter "
|
||||
"configuration.");
|
||||
return false;
|
||||
}
|
||||
auto& host_str = parse_result.first.value();
|
||||
std::pair<MATCH_TYPE, std::string> host_match;
|
||||
if (absl::StartsWith(host_str, "*")) {
|
||||
// suffix match
|
||||
host_match.first = MATCH_TYPE::Suffix;
|
||||
host_match.second = host_str.substr(1);
|
||||
// if (absl::StartsWith(host_match.second, ".")) {
|
||||
// host_match.second = host_match.second.substr(1);
|
||||
// }
|
||||
} else if (absl::EndsWith(host_str, "*")) {
|
||||
// prefix match
|
||||
host_match.first = MATCH_TYPE::Prefix;
|
||||
host_match.second = host_str.substr(0, host_str.size() - 1);
|
||||
// if (absl::EndsWith(host_match.second, ".")) {
|
||||
// host_match.second = host_match.second.substr(
|
||||
// 0, host_match.second.size() - 1);
|
||||
// }
|
||||
} else {
|
||||
host_match.first = MATCH_TYPE::Exact;
|
||||
host_match.second = host_str;
|
||||
}
|
||||
hosts.push_back(host_match);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool invalid_config_ = false;
|
||||
std::vector<RuleConfig> rule_config_;
|
||||
std::vector<AuthRuleConfig> auth_rule_config_;
|
||||
std::optional<PluginConfig> global_config_ = std::nullopt;
|
||||
};
|
||||
61
plugins/wasm-cpp/extensions/basic_auth/BUILD
Normal file
61
plugins/wasm-cpp/extensions/basic_auth/BUILD
Normal file
@@ -0,0 +1,61 @@
|
||||
load("@proxy_wasm_cpp_sdk//bazel/wasm:wasm.bzl", "wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
wasm_cc_binary(
|
||||
name = "basic_auth.wasm",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
"plugin.h",
|
||||
"//common:base64.h",
|
||||
],
|
||||
deps = [
|
||||
"//common:rule_util",
|
||||
"//common:json_util",
|
||||
"//common:crypto_util",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/time",
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "basic_auth_lib",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
"//common:base64.h",
|
||||
],
|
||||
hdrs = [
|
||||
"plugin.h",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
visibility = ["//visibility:public"],
|
||||
alwayslink = 1,
|
||||
deps = [
|
||||
"//common:rule_util",
|
||||
"//common:json_util",
|
||||
"//common:crypto_util",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/time",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "basic_auth_test",
|
||||
srcs = [
|
||||
"plugin_test.cc",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
":basic_auth_lib",
|
||||
"@com_google_googletest//:gtest",
|
||||
"@com_google_googletest//:gtest_main",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
],
|
||||
linkopts = ["-lcrypt"],
|
||||
)
|
||||
|
||||
declare_wasm_image_targets(
|
||||
name = "basic_auth",
|
||||
wasm_file = ":basic_auth.wasm",
|
||||
)
|
||||
351
plugins/wasm-cpp/extensions/basic_auth/plugin.cc
Normal file
351
plugins/wasm-cpp/extensions/basic_auth/plugin.cc
Normal file
@@ -0,0 +1,351 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/basic_auth/plugin.h"
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "common/base64.h"
|
||||
#include "common/common_util.h"
|
||||
#include "common/crypto_util.h"
|
||||
#include "common/json_util.h"
|
||||
|
||||
using ::nlohmann::json;
|
||||
using ::Wasm::Common::JsonArrayIterate;
|
||||
using ::Wasm::Common::JsonGetField;
|
||||
using ::Wasm::Common::JsonObjectIterate;
|
||||
using ::Wasm::Common::JsonValueAs;
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace basic_auth {
|
||||
|
||||
PROXY_WASM_NULL_PLUGIN_REGISTRY
|
||||
|
||||
NullPluginRegistry* context_registry_;
|
||||
RegisterNullVmPluginFactory register_basic_auth_plugin(
|
||||
"envoy.wasm.basic_auth", []() {
|
||||
return std::make_unique<NullPlugin>(basic_auth::context_registry_);
|
||||
});
|
||||
|
||||
#endif
|
||||
|
||||
static RegisterContextFactory register_BasicAuth(
|
||||
CONTEXT_FACTORY(PluginContext), ROOT_FACTORY(PluginRootContext));
|
||||
|
||||
namespace {
|
||||
|
||||
void deniedNoBasicAuthData(const std::string& realm) {
|
||||
sendLocalResponse(
|
||||
401,
|
||||
"Request denied by Basic Auth check. No Basic "
|
||||
"Authentication information found.",
|
||||
"", {{"WWW-Authenticate", absl::StrCat("Basic realm=", realm)}});
|
||||
}
|
||||
|
||||
void deniedInvalidCredentials(const std::string& realm) {
|
||||
sendLocalResponse(
|
||||
401,
|
||||
"Request denied by Basic Auth check. Invalid "
|
||||
"username and/or password",
|
||||
"", {{"WWW-Authenticate", absl::StrCat("Basic realm=", realm)}});
|
||||
}
|
||||
|
||||
void deniedUnauthorizedConsumer(const std::string& realm) {
|
||||
sendLocalResponse(
|
||||
403, "Request denied by Basic Auth check. Unauthorized consumer", "",
|
||||
{{"WWW-Authenticate", absl::StrCat("Basic realm=", realm)}});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
BasicAuthConfigRule& rule) {
|
||||
if ((configuration.find("consumers") != configuration.end()) &&
|
||||
(configuration.find("credentials") != configuration.end())) {
|
||||
LOG_WARN(
|
||||
"The consumers field and the credentials field cannot appear at the "
|
||||
"same level");
|
||||
return false;
|
||||
}
|
||||
auto it = configuration.find("encrypted");
|
||||
if (it != configuration.end()) {
|
||||
auto passwd_encrypted = JsonValueAs<bool>(it.value());
|
||||
if (passwd_encrypted.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse passwd_encrypted");
|
||||
return false;
|
||||
}
|
||||
rule.passwd_encrypted = passwd_encrypted.first.value();
|
||||
}
|
||||
// no consumer name
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "credentials", [&](const json& credentials) -> bool {
|
||||
auto credential = JsonValueAs<std::string>(credentials);
|
||||
if (credential.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("credential cannot be parsed");
|
||||
return false;
|
||||
}
|
||||
// Check if credential has `:` in it. If it has, it needs to be
|
||||
// base64 encoded.
|
||||
if (absl::StrContains(credential.first.value(), ":")) {
|
||||
return addBasicAuthConfigRule(rule, credential.first.value(),
|
||||
std::nullopt, false);
|
||||
}
|
||||
if (rule.passwd_encrypted) {
|
||||
LOG_WARN("colon not found in encrypted credential");
|
||||
return false;
|
||||
}
|
||||
// Otherwise, try base64 decode and insert into credential list if
|
||||
// it can be decoded.
|
||||
if (!Base64::decodeWithoutPadding(credential.first.value())
|
||||
.empty()) {
|
||||
return addBasicAuthConfigRule(rule, credential.first.value(),
|
||||
std::nullopt, true);
|
||||
}
|
||||
return false;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for credentials.");
|
||||
return false;
|
||||
}
|
||||
// with consumer name
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "consumers", [&](const json& consumer) -> bool {
|
||||
auto item = consumer.find("name");
|
||||
if (item == consumer.end()) {
|
||||
LOG_WARN("can't find 'name' field in consumer.");
|
||||
return false;
|
||||
}
|
||||
auto name = JsonValueAs<std::string>(item.value());
|
||||
if (name.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!name.first) {
|
||||
LOG_WARN("'name' cannot be parsed");
|
||||
return false;
|
||||
}
|
||||
item = consumer.find("credential");
|
||||
if (item == consumer.end()) {
|
||||
LOG_WARN("can't find 'credential' field in consumer.");
|
||||
return false;
|
||||
}
|
||||
auto credential = JsonValueAs<std::string>(item.value());
|
||||
if (credential.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!credential.first) {
|
||||
LOG_WARN("field 'credential' cannot be parsed");
|
||||
return false;
|
||||
}
|
||||
// Check if credential has `:` in it. If it has, it needs to be
|
||||
// base64 encoded.
|
||||
if (absl::StrContains(credential.first.value(), ":")) {
|
||||
return addBasicAuthConfigRule(rule, credential.first.value(),
|
||||
name.first, false);
|
||||
}
|
||||
if (rule.passwd_encrypted) {
|
||||
LOG_WARN("colon not found in encrypted credential");
|
||||
return false;
|
||||
}
|
||||
// Otherwise, try base64 decode and insert into credential list if
|
||||
// it can be decoded.
|
||||
if (!Base64::decodeWithoutPadding(credential.first.value())
|
||||
.empty()) {
|
||||
return addBasicAuthConfigRule(rule, credential.first.value(),
|
||||
name.first, true);
|
||||
}
|
||||
return false;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for credentials.");
|
||||
return false;
|
||||
}
|
||||
if (rule.encoded_credentials.empty() && rule.encrypted_credentials.empty()) {
|
||||
LOG_INFO("at least one credential has to be configured for a rule.");
|
||||
return false;
|
||||
}
|
||||
it = configuration.find("realm");
|
||||
if (it != configuration.end()) {
|
||||
auto realm_string = JsonValueAs<std::string>(it.value());
|
||||
if (realm_string.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse realm");
|
||||
return false;
|
||||
}
|
||||
rule.realm = realm_string.first.value();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::addBasicAuthConfigRule(
|
||||
BasicAuthConfigRule& rule, const std::string& credential,
|
||||
const std::optional<std::string>& name, bool base64_encoded) {
|
||||
std::string stored_str;
|
||||
const std::string* stored_ptr = nullptr;
|
||||
if (!base64_encoded && !rule.passwd_encrypted) {
|
||||
stored_str = Base64::encode(credential.data(), credential.size());
|
||||
stored_ptr = &stored_str;
|
||||
} else {
|
||||
stored_ptr = &credential;
|
||||
}
|
||||
if (!rule.passwd_encrypted) {
|
||||
rule.encoded_credentials.insert(*stored_ptr);
|
||||
} else {
|
||||
std::vector<std::string> pair =
|
||||
absl::StrSplit(*stored_ptr, absl::MaxSplits(":", 2));
|
||||
if (pair.size() != 2) {
|
||||
LOG_WARN(absl::StrCat("invalid encrypted credential: ", *stored_ptr));
|
||||
return false;
|
||||
}
|
||||
rule.encrypted_credentials.emplace(
|
||||
std::make_pair(std::move(pair[0]), std::move(pair[1])));
|
||||
}
|
||||
if (name) {
|
||||
if (rule.credential_to_name.find(*stored_ptr) !=
|
||||
rule.credential_to_name.end()) {
|
||||
LOG_WARN(absl::StrCat("duplicate consumer credential: ", *stored_ptr));
|
||||
return false;
|
||||
}
|
||||
rule.credential_to_name.emplace(std::make_pair(*stored_ptr, name.value()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::checkPlugin(
|
||||
const BasicAuthConfigRule& rule,
|
||||
const std::optional<std::unordered_set<std::string>>& allow_set) {
|
||||
auto authorization_header = getRequestHeader("authorization");
|
||||
auto authorization = authorization_header->view();
|
||||
// Check if the Basic auth header starts with "Basic "
|
||||
if (!absl::StartsWith(Wasm::Common::stdToAbsl(authorization), "Basic ")) {
|
||||
deniedNoBasicAuthData(rule.realm);
|
||||
return false;
|
||||
}
|
||||
auto authorization_strip =
|
||||
absl::StripPrefix(Wasm::Common::stdToAbsl(authorization), "Basic ");
|
||||
|
||||
std::string to_find_name;
|
||||
if (!rule.passwd_encrypted) {
|
||||
auto auth_credential_iter =
|
||||
rule.encoded_credentials.find(std::string(authorization_strip));
|
||||
// Check if encoded credential is part of the credential_to_name
|
||||
// map from our container to grant or deny access.
|
||||
if (auth_credential_iter == rule.encoded_credentials.end()) {
|
||||
deniedInvalidCredentials(rule.realm);
|
||||
return false;
|
||||
}
|
||||
to_find_name = std::string(authorization_strip);
|
||||
} else {
|
||||
auto user_and_passwd = Base64::decodeWithoutPadding(
|
||||
Wasm::Common::abslToStd(authorization_strip));
|
||||
if (user_and_passwd.empty()) {
|
||||
LOG_WARN(
|
||||
absl::StrCat("invalid base64 authorization: ", authorization_strip));
|
||||
deniedInvalidCredentials(rule.realm);
|
||||
return false;
|
||||
}
|
||||
std::vector<std::string> pair =
|
||||
absl::StrSplit(user_and_passwd, absl::MaxSplits(":", 2));
|
||||
if (pair.size() != 2) {
|
||||
LOG_WARN(
|
||||
absl::StrCat("invalid decoded authorization: ", user_and_passwd));
|
||||
deniedInvalidCredentials(rule.realm);
|
||||
return false;
|
||||
}
|
||||
auto encrypted_iter = rule.encrypted_credentials.find(pair[0]);
|
||||
if (encrypted_iter == rule.encrypted_credentials.end()) {
|
||||
LOG_DEBUG(absl::StrCat("username not found: ", pair[0]));
|
||||
deniedInvalidCredentials(rule.realm);
|
||||
return false;
|
||||
}
|
||||
auto expect_encrypted = encrypted_iter->second;
|
||||
std::string actual_encrypted;
|
||||
if (!Wasm::Common::Crypto::crypt(pair[1], expect_encrypted,
|
||||
actual_encrypted)) {
|
||||
LOG_DEBUG(absl::StrCat("crypt failed, expect: ", pair[1]));
|
||||
deniedInvalidCredentials(rule.realm);
|
||||
return false;
|
||||
}
|
||||
LOG_DEBUG(absl::StrCat("expect_encrypted: ", expect_encrypted,
|
||||
", actual_encrypted: ", actual_encrypted));
|
||||
if (expect_encrypted != actual_encrypted) {
|
||||
LOG_DEBUG(absl::StrCat("invalid encrypted: ", actual_encrypted,
|
||||
", expect: ", expect_encrypted));
|
||||
deniedInvalidCredentials(rule.realm);
|
||||
return false;
|
||||
}
|
||||
to_find_name = absl::StrCat(pair[0], ":", expect_encrypted);
|
||||
}
|
||||
|
||||
// Check if this credential has a consumer name. If so, check if this
|
||||
// consumer is allowed to access. If allow_set is empty, allow all consumers.
|
||||
auto credential_to_name_iter = rule.credential_to_name.find(to_find_name);
|
||||
if (credential_to_name_iter != rule.credential_to_name.end()) {
|
||||
if (allow_set && !allow_set.value().empty()) {
|
||||
if (allow_set.value().find(credential_to_name_iter->second) ==
|
||||
allow_set.value().end()) {
|
||||
deniedUnauthorizedConsumer(rule.realm);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
addRequestHeader("X-Mse-Consumer", credential_to_name_iter->second);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::onConfigure(size_t size) {
|
||||
// Parse configuration JSON string.
|
||||
if (size > 0 && !configure(size)) {
|
||||
LOG_WARN("configuration has errors initialization will not continue.");
|
||||
setInvalidConfig();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::configure(size_t configuration_size) {
|
||||
auto configuration_data = getBufferBytes(WasmBufferType::PluginConfiguration,
|
||||
0, configuration_size);
|
||||
// Parse configuration JSON string.
|
||||
auto result = ::Wasm::Common::JsonParse(configuration_data->view());
|
||||
if (!result) {
|
||||
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
Wasm::Common::stdToAbsl(configuration_data->view())));
|
||||
return false;
|
||||
}
|
||||
if (!parseAuthRuleConfig(result.value())) {
|
||||
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
Wasm::Common::stdToAbsl(configuration_data->view())));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) {
|
||||
auto* rootCtx = rootContext();
|
||||
return rootCtx->checkAuthRule(
|
||||
[rootCtx](const auto& config, const auto& allow_set) {
|
||||
return rootCtx->checkPlugin(config, allow_set);
|
||||
})
|
||||
? FilterHeadersStatus::Continue
|
||||
: FilterHeadersStatus::StopIteration;
|
||||
}
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace basic_auth
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
87
plugins/wasm-cpp/extensions/basic_auth/plugin.h
Normal file
87
plugins/wasm-cpp/extensions/basic_auth/plugin.h
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "common/route_rule_matcher.h"
|
||||
#define ASSERT(_X) assert(_X)
|
||||
|
||||
#ifndef NULL_PLUGIN
|
||||
|
||||
#include "proxy_wasm_intrinsics.h"
|
||||
|
||||
#else
|
||||
|
||||
#include "include/proxy-wasm/null_plugin.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace basic_auth {
|
||||
|
||||
#endif
|
||||
|
||||
struct BasicAuthConfigRule {
|
||||
std::unordered_map<std::string, std::string> encrypted_credentials;
|
||||
std::unordered_set<std::string> encoded_credentials;
|
||||
std::unordered_map<std::string, std::string> credential_to_name;
|
||||
std::string realm = "MSE Gateway";
|
||||
bool passwd_encrypted = false;
|
||||
};
|
||||
|
||||
// PluginRootContext is the root context for all streams processed by the
|
||||
// thread. It has the same lifetime as the worker thread and acts as target for
|
||||
// interactions that outlives individual stream, e.g. timer, async calls.
|
||||
class PluginRootContext : public RootContext,
|
||||
public RouteRuleMatcher<BasicAuthConfigRule> {
|
||||
public:
|
||||
PluginRootContext(uint32_t id, std::string_view root_id)
|
||||
: RootContext(id, root_id) {}
|
||||
~PluginRootContext() {}
|
||||
bool onConfigure(size_t) override;
|
||||
bool checkPlugin(const BasicAuthConfigRule&,
|
||||
const std::optional<std::unordered_set<std::string>>&);
|
||||
bool configure(size_t);
|
||||
|
||||
private:
|
||||
bool parsePluginConfig(const json&, BasicAuthConfigRule&) override;
|
||||
bool addBasicAuthConfigRule(BasicAuthConfigRule& rule,
|
||||
const std::string& credential,
|
||||
const std::optional<std::string>& name,
|
||||
bool base64_encoded);
|
||||
};
|
||||
|
||||
// Per-stream context.
|
||||
class PluginContext : public Context {
|
||||
public:
|
||||
explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {}
|
||||
FilterHeadersStatus onRequestHeaders(uint32_t, bool) override;
|
||||
|
||||
private:
|
||||
inline PluginRootContext* rootContext() {
|
||||
return dynamic_cast<PluginRootContext*>(this->root());
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace basic_auth
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
969
plugins/wasm-cpp/extensions/basic_auth/plugin_test.cc
Normal file
969
plugins/wasm-cpp/extensions/basic_auth/plugin_test.cc
Normal file
@@ -0,0 +1,969 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/basic_auth/plugin.h"
|
||||
|
||||
#include "common/base64.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "include/proxy-wasm/context.h"
|
||||
#include "include/proxy-wasm/null.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace basic_auth {
|
||||
|
||||
NullPluginRegistry* context_registry_;
|
||||
RegisterNullVmPluginFactory register_basic_auth_plugin("basic_auth", []() {
|
||||
return std::make_unique<NullPlugin>(basic_auth::context_registry_);
|
||||
});
|
||||
|
||||
class MockContext : public proxy_wasm::ContextBase {
|
||||
public:
|
||||
MockContext(WasmBase* wasm) : ContextBase(wasm) {}
|
||||
|
||||
MOCK_METHOD(BufferInterface*, getBuffer, (WasmBufferType));
|
||||
MOCK_METHOD(WasmResult, log, (uint32_t, std::string_view));
|
||||
MOCK_METHOD(WasmResult, getHeaderMapValue,
|
||||
(WasmHeaderMapType /* type */, std::string_view /* key */,
|
||||
std::string_view* /*result */));
|
||||
MOCK_METHOD(WasmResult, addHeaderMapValue,
|
||||
(WasmHeaderMapType /* type */, std::string_view /* key */,
|
||||
std::string_view /* value */));
|
||||
MOCK_METHOD(WasmResult, sendLocalResponse,
|
||||
(uint32_t /* response_code */, std::string_view /* body */,
|
||||
Pairs /* additional_headers */, uint32_t /* grpc_status */,
|
||||
std::string_view /* details */));
|
||||
MOCK_METHOD(WasmResult, getProperty, (std::string_view, std::string*));
|
||||
};
|
||||
|
||||
class BasicAuthTest : public ::testing::Test {
|
||||
protected:
|
||||
BasicAuthTest() {
|
||||
// Initialize test VM
|
||||
test_vm_ = createNullVm();
|
||||
wasm_base_ = std::make_unique<WasmBase>(
|
||||
std::move(test_vm_), "test-vm", "", "",
|
||||
std::unordered_map<std::string, std::string>{},
|
||||
AllowedCapabilitiesMap{});
|
||||
wasm_base_->load("basic_auth");
|
||||
wasm_base_->initialize();
|
||||
|
||||
// Initialize host side context
|
||||
mock_context_ = std::make_unique<MockContext>(wasm_base_.get());
|
||||
current_context_ = mock_context_.get();
|
||||
|
||||
ON_CALL(*mock_context_, log(testing::_, testing::_))
|
||||
.WillByDefault([](uint32_t, std::string_view m) {
|
||||
std::cerr << m << "\n";
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getHeaderMapValue(WasmHeaderMapType::RequestHeaders,
|
||||
testing::_, testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view header,
|
||||
std::string_view* result) {
|
||||
if (header == ":authority") {
|
||||
*result = authority_;
|
||||
}
|
||||
if (header == "authorization") {
|
||||
if (authorization_header_.empty()) {
|
||||
authorization_header_ =
|
||||
"Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
}
|
||||
*result = authorization_header_;
|
||||
}
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
ON_CALL(*mock_context_, addHeaderMapValue(WasmHeaderMapType::RequestHeaders,
|
||||
testing::_, testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view key,
|
||||
std::string_view value) { return WasmResult::Ok; });
|
||||
|
||||
ON_CALL(*mock_context_, getProperty(testing::_, testing::_))
|
||||
.WillByDefault([&](std::string_view path, std::string* result) {
|
||||
*result = route_name_;
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
// Initialize Wasm sandbox context
|
||||
root_context_ = std::make_unique<PluginRootContext>(0, "");
|
||||
context_ = std::make_unique<PluginContext>(1, root_context_.get());
|
||||
}
|
||||
~BasicAuthTest() override {}
|
||||
|
||||
std::unique_ptr<WasmBase> wasm_base_;
|
||||
std::unique_ptr<WasmVm> test_vm_;
|
||||
std::unique_ptr<MockContext> mock_context_;
|
||||
|
||||
std::unique_ptr<PluginRootContext> root_context_;
|
||||
std::unique_ptr<PluginContext> context_;
|
||||
|
||||
std::string authority_;
|
||||
std::string cred_;
|
||||
std::string route_name_;
|
||||
std::string authorization_header_;
|
||||
};
|
||||
|
||||
TEST_F(BasicAuthTest, OnConfigureSuccess) {
|
||||
// without consumer
|
||||
{
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"credentials":[ "ok:test", "admin:admin", "admin2:admin2",
|
||||
"YWRtaW4zOmFkbWluMw==" ],
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_":[ "abc", "test" ],
|
||||
"credentials":[ "ok:test", "admin:admin", "admin2:admin2",
|
||||
"YWRtaW4zOmFkbWluMw==" ]
|
||||
},
|
||||
{
|
||||
"_match_domain_":[ "test.com", "*.example.com" ],
|
||||
"credentials":[ "admin:admin", "admin2:admin2", "ok:test",
|
||||
"YWRtaW4zOmFkbWluMw==" ]
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
|
||||
// with consumer
|
||||
{
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers" : [
|
||||
{"credential" : "getuser1:123456", "name" : "consumer1"},
|
||||
{"credential" : "getuser2:123456", "name" : "consumer2"},
|
||||
{"credential" : "postuser1:123456", "name" : "consumer3"},
|
||||
{"credential" : "postuser2:123456", "name" : "consumer4"}
|
||||
],
|
||||
"_rules_" : [
|
||||
{
|
||||
"_match_route_" : ["route-1"],
|
||||
"allow" : [ "consumer1", "consumer2" ]
|
||||
},
|
||||
{
|
||||
"_match_domain_" : ["*.example.com"],
|
||||
"allow" : [ "consumer3", "consumer4" ]
|
||||
}
|
||||
]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BasicAuthTest, OnConfigureNoRules) {
|
||||
// without consumer
|
||||
{
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"credentials":[ "ok:test", "admin:admin", "admin2:admin2" ]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
|
||||
// with consumer
|
||||
{
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers" : [
|
||||
{"credential" : "getuser1:123456", "name" : "consumer1"},
|
||||
{"credential" : "getuser2:123456", "name" : "consumer2"},
|
||||
{"credential" : "postuser1:123456", "name" : "consumer3"},
|
||||
{"credential" : "postuser2:123456", "name" : "consumer4"}
|
||||
]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BasicAuthTest, OnConfigureOnlyRules) {
|
||||
// without consumer
|
||||
{
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_domain_":[ "test.com.*"],
|
||||
"credentials":[ "ok:test", "admin:admin", "admin2:admin2" ]
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
|
||||
// with consumer
|
||||
{
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_" : [
|
||||
{
|
||||
"_match_route_" : ["route-1"],
|
||||
"consumers" : [
|
||||
{"credential" : "getuser1:123456", "name" : "consumer1"},
|
||||
{"credential" : "getuser2:123456", "name" : "consumer2"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_match_domain_" : ["*.example.com"],
|
||||
"consumers" : [
|
||||
{"credential" : "postuser1:123456", "name" : "consumer3"},
|
||||
{"credential" : "postuser2:123456", "name" : "consumer4"}
|
||||
]
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BasicAuthTest, OnConfigureEmptyRules) {
|
||||
// without consumer
|
||||
{
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_": [
|
||||
{
|
||||
"credentials":[ "ok:test", "admin:admin", "admin2:admin2" ]
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_FALSE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
|
||||
// with consumer
|
||||
{
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_" : [
|
||||
{
|
||||
"consumers" : [
|
||||
{"credential" : "getuser1:123456", "name" : "consumer1"},
|
||||
{"credential" : "getuser2:123456", "name" : "consumer2"}
|
||||
]
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_FALSE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BasicAuthTest, OnConfigureDuplicateRules) {
|
||||
// without consumer
|
||||
{
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_domain_": ["abc.com"],
|
||||
"_match_route_": ["abc"],
|
||||
"credentials":[ "ok:test", "admin:admin", "admin2:admin2" ]
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_FALSE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
|
||||
// with consumer
|
||||
{
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_domain_": ["abc.com"],
|
||||
"_match_route_": ["abc"],
|
||||
"consumers" : [
|
||||
{"credential" : "getuser1:123456", "name" : "consumer1"},
|
||||
{"credential" : "getuser2:123456", "name" : "consumer2"}
|
||||
]
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_FALSE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BasicAuthTest, OnConfigureNoCredentials) {
|
||||
// without consumer
|
||||
{
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_":[ "abc", "test" ],
|
||||
"credentials":[ ]
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_FALSE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
|
||||
// with consumer
|
||||
{
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_":[ "abc", "test" ],
|
||||
"consumers":[ ]
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_FALSE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BasicAuthTest, OnConfigureEmptyConfig) {
|
||||
std::string configuration = "{}";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_FALSE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
|
||||
TEST_F(BasicAuthTest, OnConfigureDuplicateCredential) {
|
||||
// without consumer
|
||||
// "admin:admin" base64 encoded is "YWRtaW46YWRtaW4="
|
||||
{
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"credentials":[ "admin:admin", "YWRtaW46YWRtaW4=" ]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
|
||||
// with consumer
|
||||
// a consumer credential cannot be mapped to two name
|
||||
{
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers" : [
|
||||
{"credential" : "admin:admin", "name" : "consumer1"},
|
||||
{"credential" : "YWRtaW46YWRtaW4=", "name" : "consumer2"},
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_FALSE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
|
||||
// with consumer
|
||||
// two consumer credentials can be mapped to the same name
|
||||
{
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers" : [
|
||||
{"credential" : "admin:admin", "name" : "consumer"},
|
||||
{"credential" : "admin2:admin2", "name" : "consumer"}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BasicAuthTest, OnConfigureCredentialsWithConsumers) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_" : [
|
||||
{
|
||||
"_match_route_" : ["route-1"],
|
||||
"consumers" : [
|
||||
{"credential" : "getuser1:123456", "name" : "consumer1"}
|
||||
],
|
||||
"credentials" : ["ok:test", "admin:admin", "admin2:admin2"]
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_FALSE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
|
||||
TEST_F(BasicAuthTest, RuleAllow) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_":[ "test", "config" ],
|
||||
"credentials":[ "ok:test", "admin2:admin2", "YWRtaW4zOmFkbWluMw==" ]
|
||||
},
|
||||
{
|
||||
"_match_domain_":[ "test.com", "*.example.com" ],
|
||||
"credentials":[ "admin:admin"]
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
route_name_ = "test";
|
||||
cred_ = "ok:test";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
route_name_ = "config";
|
||||
cred_ = "admin2:admin2";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
cred_ = "admin3:admin3";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
route_name_ = "nope";
|
||||
authority_ = "www.example.com:8080";
|
||||
cred_ = "admin:admin";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(BasicAuthTest, RuleWithConsumerAllow) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers" : [
|
||||
{"credential" : "ok:test", "name" : "consumer_ok"},
|
||||
{"credential" : "admin2:admin2", "name" : "consumer2"},
|
||||
{"credential" : "YWRtaW4zOmFkbWluMw==", "name" : "consumer3"},
|
||||
{"credential" : "admin:admin", "name" : "consumer"}
|
||||
],
|
||||
"_rules_" : [
|
||||
{
|
||||
"_match_route_" : ["test", "config"],
|
||||
"allow" : [ "consumer_ok", "consumer2", "consumer3"]
|
||||
},
|
||||
{
|
||||
"_match_domain_" : ["test.com", "*.example.com"],
|
||||
"allow" : [ "consumer" ]
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
route_name_ = "test";
|
||||
cred_ = "ok:test";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
route_name_ = "config";
|
||||
cred_ = "admin2:admin2";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
cred_ = "admin3:admin3";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
route_name_ = "nope";
|
||||
authority_ = "www.example.com:8080";
|
||||
cred_ = "admin:admin";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(BasicAuthTest, RuleWithEncryptedConsumerAllow) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"encrypted": true,
|
||||
"consumers" : [
|
||||
{"credential" : "myName:$2y$05$c4WoMPo3SXsafkva.HHa6uXQZWr7oboPiC2bT/r7q1BB8I2s0BRqC", "name": "consumer"}
|
||||
],
|
||||
"_rules_" : [
|
||||
{
|
||||
"_match_route_" : ["test_allow"],
|
||||
"allow" : [ "consumer"]
|
||||
},
|
||||
{
|
||||
"_match_route_" : ["test_deny"],
|
||||
"allow" : []
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
route_name_ = "test_allow";
|
||||
cred_ = "myName:myPassword";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
route_name_ = "test_deny";
|
||||
cred_ = "abc:123";
|
||||
authorization_header_ = "";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(401, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
|
||||
TEST_F(BasicAuthTest, RuleDeny) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_domain_":[ "test.com", "example.*" ],
|
||||
"credentials":[ "ok:test", "admin:admin", "admin2:admin2" ]
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
authority_ = "example.com";
|
||||
cred_ = "wrong-cred";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(401, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
authority_ = "example.com";
|
||||
cred_ = "admin2:admin2";
|
||||
authorization_header_ = Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(401, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
|
||||
TEST_F(BasicAuthTest, RuleWithConsumerDeny) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers" : [
|
||||
{"credential" : "ok:test", "name" : "consumer_ok"},
|
||||
{"credential" : "admin:admin", "name" : "consumer"}
|
||||
],
|
||||
"_rules_" : [
|
||||
{
|
||||
"_match_domain_" : ["test.com", "*.example.com"],
|
||||
"allow" : [ "consumer" ]
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
authority_ = "www.example.com";
|
||||
cred_ = "ok:test";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(403, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
authority_ = "www.example.com";
|
||||
cred_ = "admin:admin";
|
||||
authorization_header_ = Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(401, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
|
||||
TEST_F(BasicAuthTest, GlobalAllow) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"credentials":[ "ok:test", "admin:admin", "admin2:admin2" ],
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_":[ "test", "config" ],
|
||||
"credentials":[ "admin3:admin3", "YWRtaW4zOmFkbWluMw==" ]
|
||||
},
|
||||
{
|
||||
"_match_domain_":[ "test.com", "*.example.com" ],
|
||||
"credentials":[ "admin4:admin4"]
|
||||
},
|
||||
{
|
||||
"_match_route_":["crypt"],
|
||||
"credentials": ["myName:rqXexS6ZhobKA"],
|
||||
"encrypted": true
|
||||
},
|
||||
{
|
||||
"_match_route_":["bcrypt"],
|
||||
"credentials": ["myName:$2y$05$c4WoMPo3SXsafkva.HHa6uXQZWr7oboPiC2bT/r7q1BB8I2s0BRqC"],
|
||||
"encrypted": true
|
||||
},
|
||||
{
|
||||
"_match_route_":["apr1"],
|
||||
"credentials": ["myName:$apr1$EXfBN1bF$nuywSFTnPTcqbH5z4x6IG/"],
|
||||
"encrypted": true
|
||||
},
|
||||
{
|
||||
"_match_route_":["plain"],
|
||||
"credentials": ["myName:{PLAIN}myPassword"],
|
||||
"encrypted": true
|
||||
},
|
||||
{
|
||||
"_match_route_":["sha"],
|
||||
"credentials": ["myName:{SHA}VBPuJHI7uixaa6LQGWx4s+5GKNE="],
|
||||
"encrypted": true
|
||||
},
|
||||
{
|
||||
"_match_route_":["ssha"],
|
||||
"credentials": ["myName:{SSHA}98JUfJee5Wb13m5683sLku40P3Y2VjNX"],
|
||||
"encrypted": true
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
cred_ = "ok:test";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
authority_ = "test.com";
|
||||
cred_ = "admin4:admin4";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
route_name_ = "test";
|
||||
cred_ = "admin3:admin3";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
authority_ = "";
|
||||
authorization_header_ = "";
|
||||
cred_ = "myName:myPassword";
|
||||
|
||||
route_name_ = "crypt";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
route_name_ = "bcrypt";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
route_name_ = "apr1";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
route_name_ = "plain";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
route_name_ = "sha";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
route_name_ = "ssha";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(BasicAuthTest, GlobalWithConsumerAllow) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers" : [
|
||||
{"credential" : "ok:test", "name" : "consumer_ok"},
|
||||
{"credential" : "admin2:admin2", "name" : "consumer2"},
|
||||
{"credential" : "admin:admin", "name" : "consumer"}
|
||||
],
|
||||
"_rules_" : [
|
||||
{
|
||||
"_match_route_" : ["test", "config"],
|
||||
"consumers" : [
|
||||
{"credential" : "admin3:admin3", "name" : "consumer3"},
|
||||
{"credential" : "YWRtaW41OmFkbWluNQ==", "name" : "consumer5"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_match_domain_" : ["test.com", "*.example.com"],
|
||||
"consumers" : [
|
||||
{"credential" : "admin4:admin4", "name" : "consumer4"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_match_route_" : ["crypt"],
|
||||
"encrypted" : true,
|
||||
"consumers" : [
|
||||
{"credential" : "myName:$2y$05$c4WoMPo3SXsafkva.HHa6uXQZWr7oboPiC2bT/r7q1BB8I2s0BRqC", "name": "consumer crypt"}
|
||||
]
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
cred_ = "ok:test";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
authority_ = "test.com";
|
||||
cred_ = "admin4:admin4";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
route_name_ = "test";
|
||||
cred_ = "admin3:admin3";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
authorization_header_ = "";
|
||||
authority_ = "";
|
||||
route_name_ = "crypt";
|
||||
cred_ = "myName:myPassword";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(BasicAuthTest, GlobalDeny) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"credentials":[ "ok:test", "admin:admin", "admin2:admin2" ],
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_":[ "test", "config" ],
|
||||
"credentials":[ "admin3:admin3", "YWRtaW4zOmFkbWluMw==" ]
|
||||
},
|
||||
{
|
||||
"_match_domain_":[ "test.com", "*.example.com" ],
|
||||
"credentials":[ "admin4:admin4"]
|
||||
}
|
||||
]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
cred_ = "wrong-cred";
|
||||
route_name_ = "config";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(401, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
authority_ = "www.example.com";
|
||||
cred_ = "admin2:admin2";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(401, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
route_name_ = "config";
|
||||
cred_ = "admin4:admin4";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(401, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
|
||||
TEST_F(BasicAuthTest, GlobalWithConsumerDeny) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers" : [
|
||||
{"credential" : "ok:test", "name" : "consumer_ok"},
|
||||
{"credential" : "admin2:admin2", "name" : "consumer2"},
|
||||
{"credential" : "admin:admin", "name" : "consumer"}
|
||||
],
|
||||
"_rules_" : [
|
||||
{
|
||||
"_match_route_" : ["test", "config"],
|
||||
"consumers" : [
|
||||
{"credential" : "admin3:admin3", "name" : "consumer3"},
|
||||
{"credential" : "YWRtaW41OmFkbWluNQ==", "name" : "consumer5"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_match_domain_" : ["test.com", "*.example.com"],
|
||||
"consumers" : [
|
||||
{"credential" : "admin4:admin4", "name" : "consumer4"}
|
||||
]
|
||||
}
|
||||
]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
cred_ = "wrong-cred";
|
||||
route_name_ = "config";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(401, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
authority_ = "www.example.com";
|
||||
cred_ = "admin2:admin2";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(401, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
route_name_ = "config";
|
||||
cred_ = "admin4:admin4";
|
||||
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(401, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
|
||||
} // namespace basic_auth
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
57
plugins/wasm-cpp/extensions/bot_detect/BUILD
Normal file
57
plugins/wasm-cpp/extensions/bot_detect/BUILD
Normal file
@@ -0,0 +1,57 @@
|
||||
load("@proxy_wasm_cpp_sdk//bazel/wasm:wasm.bzl", "wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
wasm_cc_binary(
|
||||
name = "bot_detect.wasm",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
"plugin.h",
|
||||
],
|
||||
deps = [
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/time",
|
||||
"//common:json_util",
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics",
|
||||
"//common:http_util",
|
||||
"//common:regex_util",
|
||||
"//common:rule_util",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "bot_detect_lib",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
],
|
||||
hdrs = [
|
||||
"plugin.h",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
"@com_google_absl//absl/strings",
|
||||
"//common:json_util",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
"//common:http_util",
|
||||
"//common:regex_util",
|
||||
"//common:rule_util",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "bot_detect_test",
|
||||
srcs = [
|
||||
"plugin_test.cc",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
":bot_detect_lib",
|
||||
"@com_google_googletest//:gtest",
|
||||
"@com_google_googletest//:gtest_main",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
],
|
||||
)
|
||||
|
||||
declare_wasm_image_targets(
|
||||
name = "bot_detect",
|
||||
wasm_file = ":bot_detect.wasm",
|
||||
)
|
||||
188
plugins/wasm-cpp/extensions/bot_detect/plugin.cc
Normal file
188
plugins/wasm-cpp/extensions/bot_detect/plugin.cc
Normal file
@@ -0,0 +1,188 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/bot_detect/plugin.h"
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "common/json_util.h"
|
||||
|
||||
using ::nlohmann::json;
|
||||
using ::Wasm::Common::JsonArrayIterate;
|
||||
using ::Wasm::Common::JsonGetField;
|
||||
using ::Wasm::Common::JsonObjectIterate;
|
||||
using ::Wasm::Common::JsonValueAs;
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace bot_detect {
|
||||
|
||||
PROXY_WASM_NULL_PLUGIN_REGISTRY
|
||||
|
||||
#endif
|
||||
|
||||
static RegisterContextFactory register_BotDetect(
|
||||
CONTEXT_FACTORY(PluginContext), ROOT_FACTORY(PluginRootContext));
|
||||
|
||||
static std::array<std::string, 6> default_bot_regex = {
|
||||
R"(/((?:Ant-)?Nutch|[A-z]+[Bb]ot|[A-z]+[Ss]pider|Axtaris|fetchurl|Isara|ShopSalad|Tailsweep)[ \-](\d+)(?:\.(\d+)(?:\.(\d+))?)?)",
|
||||
R"((?:\/[A-Za-z0-9\.]+|) {0,5}([A-Za-z0-9 \-_\!\[\]:]{0,50}(?:[Aa]rchiver|[Ii]ndexer|[Ss]craper|[Bb]ot|[Ss]pider|[Cc]rawl[a-z]{0,50}))[/ ](\d+)(?:\.(\d+)(?:\.(\d+)|)|))",
|
||||
R"((?:\/[A-Za-z0-9\.]+|) {0,5}([A-Za-z0-9 \-_\!\[\]:]{0,50}(?:[Aa]rchiver|[Ii]ndexer|[Ss]craper|[Bb]ot|[Ss]pider|[Cc]rawl[a-z]{0,50})) (\d+)(?:\.(\d+)(?:\.(\d+)|)|))",
|
||||
R"(((?:[A-z0-9]{1,50}|[A-z\-]{1,50} ?|)(?: the |)(?:[Ss][Pp][Ii][Dd][Ee][Rr]|[Ss]crape|[Cc][Rr][Aa][Ww][Ll])[A-z0-9]{0,50})(?:(?:[ /]| v)(\d+)(?:\.(\d+)|)(?:\.(\d+)|)|))",
|
||||
R"(\b(008|Altresium|Argus|BaiduMobaider|BoardReader|DNSGroup|DataparkSearch|EDI|Goodzer|Grub|INGRID|Infohelfer|LinkedInBot|LOOQ|Nutch|OgScrper|PathDefender|Peew|PostPost|Steeler|Twitterbot|VSE|WebCrunch|WebZIP|Y!J-BR[A-Z]|YahooSeeker|envolk|sproose|wminer)/(\d+)(?:\.(\d+)|)(?:\.(\d+)|))",
|
||||
R"((CSimpleSpider|Cityreview Robot|CrawlDaddy|CrawlFire|Finderbots|Index crawler|Job Roboter|KiwiStatus Spider|Lijit Crawler|QuerySeekerSpider|ScollSpider|Trends Crawler|USyd-NLP-Spider|SiteCat Webbot|BotName\/\$BotVersion|123metaspider-Bot|1470\.net crawler|50\.nu|8bo Crawler Bot|Aboundex|Accoona-[A-z]{1,30}-Agent|AdsBot-Google(?:-[a-z]{1,30}|)|altavista|AppEngine-Google|archive.{0,30}\.org_bot|archiver|Ask Jeeves|[Bb]ai[Dd]u[Ss]pider(?:-[A-Za-z]{1,30})(?:-[A-Za-z]{1,30}|)|bingbot|BingPreview|blitzbot|BlogBridge|Bloglovin|BoardReader Blog Indexer|BoardReader Favicon Fetcher|boitho.com-dc|BotSeer|BUbiNG|\b\w{0,30}favicon\w{0,30}\b|\bYeti(?:-[a-z]{1,30}|)|Catchpoint(?: bot|)|[Cc]harlotte|Checklinks|clumboot|Comodo HTTP\(S\) Crawler|Comodo-Webinspector-Crawler|ConveraCrawler|CRAWL-E|CrawlConvera|Daumoa(?:-feedfetcher|)|Feed Seeker Bot|Feedbin|findlinks|Flamingo_SearchEngine|FollowSite Bot|furlbot|Genieo|gigabot|GomezAgent|gonzo1|(?:[a-zA-Z]{1,30}-|)Googlebot(?:-[a-zA-Z]{1,30}|)|Google SketchUp|grub-client|gsa-crawler|heritrix|HiddenMarket|holmes|HooWWWer|htdig|ia_archiver|ICC-Crawler|Icarus6j|ichiro(?:/mobile|)|IconSurf|IlTrovatore(?:-Setaccio|)|InfuzApp|Innovazion Crawler|InternetArchive|IP2[a-z]{1,30}Bot|jbot\b|KaloogaBot|Kraken|Kurzor|larbin|LEIA|LesnikBot|Linguee Bot|LinkAider|LinkedInBot|Lite Bot|Llaut|lycos|Mail\.RU_Bot|masscan|masidani_bot|Mediapartners-Google|Microsoft .{0,30} Bot|mogimogi|mozDex|MJ12bot|msnbot(?:-media {0,2}|)|msrbot|Mtps Feed Aggregation System|netresearch|Netvibes|NewsGator[^/]{0,30}|^NING|Nutch[^/]{0,30}|Nymesis|ObjectsSearch|OgScrper|Orbiter|OOZBOT|PagePeeker|PagesInventory|PaxleFramework|Peeplo Screenshot Bot|PlantyNet_WebRobot|Pompos|Qwantify|Read%20Later|Reaper|RedCarpet|Retreiver|Riddler|Rival IQ|scooter|Scrapy|Scrubby|searchsight|seekbot|semanticdiscovery|SemrushBot|Simpy|SimplePie|SEOstats|SimpleRSS|SiteCon|Slackbot-LinkExpanding|Slack-ImgProxy|Slurp|snappy|Speedy Spider|Squrl Java|Stringer|TheUsefulbot|ThumbShotsBot|Thumbshots\.ru|Tiny Tiny RSS|Twitterbot|WhatsApp|URL2PNG|Vagabondo|VoilaBot|^vortex|Votay bot|^voyager|WASALive.Bot|Web-sniffer|WebThumb|WeSEE:[A-z]{1,30}|WhatWeb|WIRE|WordPress|Wotbox|www\.almaden\.ibm\.com|Xenu(?:.s|) Link Sleuth|Xerka [A-z]{1,30}Bot|yacy(?:bot|)|YahooSeeker|Yahoo! Slurp|Yandex\w{1,30}|YodaoBot(?:-[A-z]{1,30}|)|YottaaMonitor|Yowedo|^Zao|^Zao-Crawler|ZeBot_www\.ze\.bz|ZooShot|ZyBorg)(?:[ /]v?(\d+)(?:\.(\d+)(?:\.(\d+)|)|)|))",
|
||||
};
|
||||
|
||||
bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
BotDetectConfigRule& rule) {
|
||||
auto it = configuration.find("blocked_code");
|
||||
if (it != configuration.end()) {
|
||||
auto blocked_code = JsonValueAs<int64_t>(it.value());
|
||||
if (blocked_code.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse status code");
|
||||
return false;
|
||||
}
|
||||
rule.blocked_code = blocked_code.first.value();
|
||||
}
|
||||
it = configuration.find("blocked_message");
|
||||
if (it != configuration.end()) {
|
||||
auto blocked_message = JsonValueAs<std::string>(it.value());
|
||||
if (blocked_message.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse blocked_message");
|
||||
return false;
|
||||
}
|
||||
rule.blocked_message = blocked_message.first.value();
|
||||
}
|
||||
if (!JsonArrayIterate(configuration, "allow", [&](const json& item) -> bool {
|
||||
auto regex = JsonValueAs<std::string>(item);
|
||||
if (regex.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse allow");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
rule.allow.push_back(
|
||||
std::make_unique<ReMatcher>(regex.first.value()));
|
||||
} catch (const std::runtime_error& e) {
|
||||
LOG_WARN(e.what());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for allow.");
|
||||
return false;
|
||||
}
|
||||
if (!JsonArrayIterate(configuration, "deny", [&](const json& item) -> bool {
|
||||
auto regex = JsonValueAs<std::string>(item);
|
||||
if (regex.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse deny");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
rule.deny.push_back(std::make_unique<ReMatcher>(regex.first.value()));
|
||||
} catch (const std::runtime_error& e) {
|
||||
LOG_WARN(e.what());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for deny.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::onConfigure(size_t size) {
|
||||
// Parse configuration JSON string.
|
||||
if (size > 0 && !configure(size)) {
|
||||
LOG_WARN("configuration has errors initialization will not continue.");
|
||||
setInvalidConfig();
|
||||
return true;
|
||||
}
|
||||
if (size == 0) {
|
||||
// support empty config
|
||||
setEmptyGlobalConfig();
|
||||
}
|
||||
for (auto& regex : default_bot_regex) {
|
||||
default_matchers_.push_back(std::make_unique<ReMatcher>(regex, false));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::configure(size_t configuration_size) {
|
||||
auto configuration_data = getBufferBytes(WasmBufferType::PluginConfiguration,
|
||||
0, configuration_size);
|
||||
// Parse configuration JSON string.
|
||||
auto result = ::Wasm::Common::JsonParse(configuration_data->view());
|
||||
if (!result.has_value()) {
|
||||
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
configuration_data->view()));
|
||||
return false;
|
||||
}
|
||||
if (!parseRuleConfig(result.value())) {
|
||||
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
configuration_data->view()));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::checkHeader(const BotDetectConfigRule& rule) {
|
||||
GET_HEADER_VIEW(Wasm::Common::Http::Header::UserAgent, user_agent);
|
||||
for (const auto& matcher : rule.allow) {
|
||||
if (matcher->match(user_agent)) {
|
||||
LOG_DEBUG("bot detected by allow rule");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const auto& matcher : rule.deny) {
|
||||
if (matcher->match(user_agent)) {
|
||||
LOG_DEBUG("bot detected by deny rule");
|
||||
sendLocalResponse(rule.blocked_code, "", rule.blocked_message, {});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const auto& matcher : default_matchers_) {
|
||||
if (matcher->match(user_agent)) {
|
||||
LOG_DEBUG("bot detected by default rule");
|
||||
sendLocalResponse(rule.blocked_code, "", rule.blocked_message, {});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) {
|
||||
auto* rootCtx = rootContext();
|
||||
return rootCtx->checkRule([rootCtx](const auto& config) {
|
||||
return rootCtx->checkHeader(config);
|
||||
})
|
||||
? FilterHeadersStatus::Continue
|
||||
: FilterHeadersStatus::StopIteration;
|
||||
}
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace bot_detect
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
88
plugins/wasm-cpp/extensions/bot_detect/plugin.h
Normal file
88
plugins/wasm-cpp/extensions/bot_detect/plugin.h
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "common/http_util.h"
|
||||
#include "common/regex.h"
|
||||
#include "common/route_rule_matcher.h"
|
||||
#define ASSERT(_X) assert(_X)
|
||||
|
||||
#ifndef NULL_PLUGIN
|
||||
|
||||
#include "proxy_wasm_intrinsics.h"
|
||||
|
||||
#else
|
||||
|
||||
#include "include/proxy-wasm/null_plugin.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace bot_detect {
|
||||
|
||||
#endif
|
||||
|
||||
using ReMatcher = Wasm::Common::Regex::CompiledGoogleReMatcher;
|
||||
using ReMatcherPtr = std::unique_ptr<ReMatcher>;
|
||||
|
||||
struct BotDetectConfigRule {
|
||||
int blocked_code = 403;
|
||||
std::string blocked_message;
|
||||
std::vector<ReMatcherPtr> allow;
|
||||
std::vector<ReMatcherPtr> deny;
|
||||
};
|
||||
|
||||
// PluginRootContext is the root context for all streams processed by the
|
||||
// thread. It has the same lifetime as the worker thread and acts as target for
|
||||
// interactions that outlives individual stream, e.g. timer, async calls.
|
||||
class PluginRootContext : public RootContext,
|
||||
public RouteRuleMatcher<BotDetectConfigRule> {
|
||||
public:
|
||||
PluginRootContext(uint32_t id, std::string_view root_id)
|
||||
: RootContext(id, root_id) {}
|
||||
~PluginRootContext() {}
|
||||
bool onConfigure(size_t) override;
|
||||
bool checkHeader(const BotDetectConfigRule&);
|
||||
bool configure(size_t);
|
||||
|
||||
private:
|
||||
bool parsePluginConfig(const json&, BotDetectConfigRule&) override;
|
||||
|
||||
std::vector<ReMatcherPtr> default_matchers_;
|
||||
};
|
||||
|
||||
// Per-stream context.
|
||||
class PluginContext : public Context {
|
||||
public:
|
||||
explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {}
|
||||
FilterHeadersStatus onRequestHeaders(uint32_t, bool) override;
|
||||
|
||||
private:
|
||||
inline PluginRootContext* rootContext() {
|
||||
return dynamic_cast<PluginRootContext*>(this->root());
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace bot_detect
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
178
plugins/wasm-cpp/extensions/bot_detect/plugin_test.cc
Normal file
178
plugins/wasm-cpp/extensions/bot_detect/plugin_test.cc
Normal file
@@ -0,0 +1,178 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/bot_detect/plugin.h"
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "include/proxy-wasm/context.h"
|
||||
#include "include/proxy-wasm/null.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace bot_detect {
|
||||
|
||||
NullPluginRegistry* context_registry_;
|
||||
RegisterNullVmPluginFactory register_bot_detect_plugin("bot_detect", []() {
|
||||
return std::make_unique<NullPlugin>(bot_detect::context_registry_);
|
||||
});
|
||||
|
||||
class MockContext : public proxy_wasm::ContextBase {
|
||||
public:
|
||||
MockContext(WasmBase* wasm) : ContextBase(wasm) {}
|
||||
|
||||
MOCK_METHOD(BufferInterface*, getBuffer, (WasmBufferType));
|
||||
MOCK_METHOD(WasmResult, log, (uint32_t, std::string_view));
|
||||
MOCK_METHOD(WasmResult, getHeaderMapValue,
|
||||
(WasmHeaderMapType /* type */, std::string_view /* key */,
|
||||
std::string_view* /*result */));
|
||||
MOCK_METHOD(WasmResult, getHeaderMapPairs, (WasmHeaderMapType, Pairs*));
|
||||
MOCK_METHOD(WasmResult, sendLocalResponse,
|
||||
(uint32_t /* response_code */, std::string_view /* body */,
|
||||
Pairs /* additional_headers */, uint32_t /* grpc_status */,
|
||||
std::string_view /* details */));
|
||||
MOCK_METHOD(WasmResult, getProperty, (std::string_view, std::string*));
|
||||
};
|
||||
|
||||
class BotDetectTest : public ::testing::Test {
|
||||
protected:
|
||||
BotDetectTest() {
|
||||
// Initialize test VM
|
||||
test_vm_ = createNullVm();
|
||||
wasm_base_ = std::make_unique<WasmBase>(
|
||||
std::move(test_vm_), "test-vm", "", "",
|
||||
std::unordered_map<std::string, std::string>{},
|
||||
AllowedCapabilitiesMap{});
|
||||
wasm_base_->load("bot_detect");
|
||||
wasm_base_->initialize();
|
||||
|
||||
// Initialize host side context
|
||||
mock_context_ = std::make_unique<MockContext>(wasm_base_.get());
|
||||
current_context_ = mock_context_.get();
|
||||
|
||||
ON_CALL(*mock_context_, log(testing::_, testing::_))
|
||||
.WillByDefault([](uint32_t, std::string_view m) {
|
||||
std::cerr << m << "\n";
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getHeaderMapValue(WasmHeaderMapType::RequestHeaders,
|
||||
testing::_, testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view header,
|
||||
std::string_view* result) {
|
||||
if (header == ":authority") {
|
||||
*result = authority_;
|
||||
}
|
||||
if (header == "user-agent") {
|
||||
*result = user_agent_;
|
||||
}
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getProperty(testing::_, testing::_))
|
||||
.WillByDefault([&](std::string_view path, std::string* result) {
|
||||
*result = route_name_;
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
// Initialize Wasm sandbox context
|
||||
root_context_ = std::make_unique<PluginRootContext>(0, "");
|
||||
context_ = std::make_unique<PluginContext>(1, root_context_.get());
|
||||
}
|
||||
~BotDetectTest() override {}
|
||||
|
||||
std::unique_ptr<WasmBase> wasm_base_;
|
||||
std::unique_ptr<WasmVm> test_vm_;
|
||||
std::unique_ptr<MockContext> mock_context_;
|
||||
|
||||
std::unique_ptr<PluginRootContext> root_context_;
|
||||
std::unique_ptr<PluginContext> context_;
|
||||
|
||||
std::string authority_;
|
||||
std::string route_name_;
|
||||
std::string user_agent_;
|
||||
};
|
||||
|
||||
TEST_F(BotDetectTest, UseDefault) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"blocked_code": 404
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->onConfigure(configuration.size()));
|
||||
|
||||
user_agent_ = "BaiduMobaider/1.1.0";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(404, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
user_agent_ = "Go-client";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(BotDetectTest, UseAllowAndDeny) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"blocked_code": 404,
|
||||
"allow": ["BaiduMobaider.*"],
|
||||
"deny": ["Go-client"]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->onConfigure(configuration.size()));
|
||||
|
||||
user_agent_ = "BaiduMobaider/1.1.0";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
user_agent_ = "Go-client";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(404, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
|
||||
TEST_F(BotDetectTest, UseEmptyConfig) {
|
||||
std::string configuration = R"()";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_TRUE(root_context_->onConfigure(configuration.size()));
|
||||
|
||||
user_agent_ = "BaiduMobaider/1.1.0";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(403, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
user_agent_ = "Go-client";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
} // namespace bot_detect
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
55
plugins/wasm-cpp/extensions/custom_response/BUILD
Normal file
55
plugins/wasm-cpp/extensions/custom_response/BUILD
Normal file
@@ -0,0 +1,55 @@
|
||||
load("@proxy_wasm_cpp_sdk//bazel/wasm:wasm.bzl", "wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
wasm_cc_binary(
|
||||
name = "custom_response.wasm",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
"plugin.h",
|
||||
],
|
||||
deps = [
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/time",
|
||||
"//common:json_util",
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics",
|
||||
"//common:http_util",
|
||||
"//common:rule_util",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "custom_response_lib",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
],
|
||||
hdrs = [
|
||||
"plugin.h",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
"@com_google_absl//absl/strings",
|
||||
"//common:json_util",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
"//common:http_util_nullvm",
|
||||
"//common:rule_util_nullvm",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "custom_response_test",
|
||||
srcs = [
|
||||
"plugin_test.cc",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
":custom_response_lib",
|
||||
"@com_google_googletest//:gtest",
|
||||
"@com_google_googletest//:gtest_main",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
],
|
||||
)
|
||||
|
||||
declare_wasm_image_targets(
|
||||
name = "custom_response",
|
||||
wasm_file = ":custom_response.wasm",
|
||||
)
|
||||
194
plugins/wasm-cpp/extensions/custom_response/plugin.cc
Normal file
194
plugins/wasm-cpp/extensions/custom_response/plugin.cc
Normal file
@@ -0,0 +1,194 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/custom_response/plugin.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "common/json_util.h"
|
||||
|
||||
using ::nlohmann::json;
|
||||
using ::Wasm::Common::JsonArrayIterate;
|
||||
using ::Wasm::Common::JsonGetField;
|
||||
using ::Wasm::Common::JsonObjectIterate;
|
||||
using ::Wasm::Common::JsonValueAs;
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace custom_response {
|
||||
|
||||
PROXY_WASM_NULL_PLUGIN_REGISTRY
|
||||
|
||||
#endif
|
||||
|
||||
static RegisterContextFactory register_CustomResponse(
|
||||
CONTEXT_FACTORY(PluginContext), ROOT_FACTORY(PluginRootContext));
|
||||
|
||||
bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
CustomResponseConfigRule& rule) {
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "enable_on_status", [&](const json& item) -> bool {
|
||||
auto status = JsonValueAs<int64_t>(item);
|
||||
if (status.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse enable_on_status");
|
||||
return false;
|
||||
}
|
||||
rule.enable_on_status.push_back(
|
||||
std::to_string(status.first.value()));
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for enable_on_status.");
|
||||
return false;
|
||||
}
|
||||
bool has_content_type = false;
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "headers", [&](const json& item) -> bool {
|
||||
auto header = JsonValueAs<std::string>(item);
|
||||
if (header.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse header");
|
||||
return false;
|
||||
}
|
||||
std::vector<std::string> pair =
|
||||
absl::StrSplit(header.first.value(), absl::MaxSplits("=", 2));
|
||||
if (pair.size() != 2) {
|
||||
LOG_WARN("invalid header pair format");
|
||||
}
|
||||
if (absl::AsciiStrToLower(pair[0]) ==
|
||||
Wasm::Common::Http::Header::ContentLength) {
|
||||
return true;
|
||||
}
|
||||
if (absl::AsciiStrToLower(pair[0]) ==
|
||||
Wasm::Common::Http::Header::ContentType) {
|
||||
rule.content_type = pair[1];
|
||||
has_content_type = true;
|
||||
return true;
|
||||
}
|
||||
rule.headers.emplace_back(pair[0], pair[1]);
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for headers.");
|
||||
return false;
|
||||
}
|
||||
auto it = configuration.find("status_code");
|
||||
if (it != configuration.end()) {
|
||||
auto status_code = JsonValueAs<int64_t>(it.value());
|
||||
if (status_code.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse status code");
|
||||
return false;
|
||||
}
|
||||
rule.status_code = status_code.first.value();
|
||||
}
|
||||
it = configuration.find("body");
|
||||
if (it != configuration.end()) {
|
||||
auto body_string = JsonValueAs<std::string>(it.value());
|
||||
if (body_string.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse body");
|
||||
return false;
|
||||
}
|
||||
rule.body = body_string.first.value();
|
||||
}
|
||||
if (!rule.body.empty() && !has_content_type) {
|
||||
auto try_decode_json = Wasm::Common::JsonParse(rule.body);
|
||||
if (try_decode_json.has_value()) {
|
||||
rule.content_type = "application/json; charset=utf-8";
|
||||
// rule.headers.emplace_back(Wasm::Common::Http::Header::ContentType,
|
||||
// "application/json; charset=utf-8");
|
||||
} else {
|
||||
rule.content_type = "text/plain; charset=utf-8";
|
||||
// rule.headers.emplace_back(Wasm::Common::Http::Header::ContentType,
|
||||
// "text/plain; charset=utf-8");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
FilterHeadersStatus PluginRootContext::onRequest(
|
||||
const CustomResponseConfigRule& rule) {
|
||||
if (!rule.enable_on_status.empty()) {
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
sendLocalResponse(rule.status_code, "", rule.body, rule.headers);
|
||||
return FilterHeadersStatus::StopIteration;
|
||||
}
|
||||
|
||||
FilterHeadersStatus PluginRootContext::onResponse(
|
||||
const CustomResponseConfigRule& rule) {
|
||||
GET_RESPONSE_HEADER_VIEW(":status", status_code);
|
||||
bool hit = false;
|
||||
for (const auto& status : rule.enable_on_status) {
|
||||
if (status_code == status) {
|
||||
hit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hit) {
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
replaceResponseHeader(Wasm::Common::Http::Header::ContentType,
|
||||
rule.content_type);
|
||||
sendLocalResponse(rule.status_code, "", rule.body, rule.headers);
|
||||
return FilterHeadersStatus::StopIteration;
|
||||
}
|
||||
|
||||
bool PluginRootContext::onConfigure(size_t size) {
|
||||
// Parse configuration JSON string.
|
||||
if (size > 0 && !configure(size)) {
|
||||
LOG_WARN("configuration has errors initialization will not continue.");
|
||||
setInvalidConfig();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::configure(size_t configuration_size) {
|
||||
auto configuration_data = getBufferBytes(WasmBufferType::PluginConfiguration,
|
||||
0, configuration_size);
|
||||
// Parse configuration JSON string.
|
||||
auto result = ::Wasm::Common::JsonParse(configuration_data->view());
|
||||
if (!result.has_value()) {
|
||||
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
configuration_data->view()));
|
||||
return false;
|
||||
}
|
||||
if (!parseRuleConfig(result.value())) {
|
||||
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
configuration_data->view()));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) {
|
||||
auto* rootCtx = rootContext();
|
||||
return rootCtx->onHeaders(
|
||||
[rootCtx](const auto& config) { return rootCtx->onRequest(config); });
|
||||
}
|
||||
|
||||
FilterHeadersStatus PluginContext::onResponseHeaders(uint32_t, bool) {
|
||||
auto* rootCtx = rootContext();
|
||||
return rootCtx->onHeaders(
|
||||
[rootCtx](const auto& config) { return rootCtx->onResponse(config); });
|
||||
}
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace custom_response
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
85
plugins/wasm-cpp/extensions/custom_response/plugin.h
Normal file
85
plugins/wasm-cpp/extensions/custom_response/plugin.h
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "common/http_util.h"
|
||||
#include "common/route_rule_matcher.h"
|
||||
#define ASSERT(_X) assert(_X)
|
||||
|
||||
#ifndef NULL_PLUGIN
|
||||
|
||||
#include "proxy_wasm_intrinsics.h"
|
||||
|
||||
#else
|
||||
|
||||
#include "include/proxy-wasm/null_plugin.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace custom_response {
|
||||
|
||||
#endif
|
||||
|
||||
struct CustomResponseConfigRule {
|
||||
std::vector<std::string> enable_on_status;
|
||||
std::vector<std::pair<std::string, std::string>> headers;
|
||||
std::string content_type;
|
||||
int32_t status_code = 200;
|
||||
std::string body;
|
||||
};
|
||||
|
||||
// PluginRootContext is the root context for all streams processed by the
|
||||
// thread. It has the same lifetime as the worker thread and acts as target for
|
||||
// interactions that outlives individual stream, e.g. timer, async calls.
|
||||
class PluginRootContext : public RootContext,
|
||||
public RouteRuleMatcher<CustomResponseConfigRule> {
|
||||
public:
|
||||
PluginRootContext(uint32_t id, std::string_view root_id)
|
||||
: RootContext(id, root_id) {}
|
||||
~PluginRootContext() {}
|
||||
bool onConfigure(size_t) override;
|
||||
FilterHeadersStatus onRequest(const CustomResponseConfigRule&);
|
||||
FilterHeadersStatus onResponse(const CustomResponseConfigRule&);
|
||||
bool configure(size_t);
|
||||
|
||||
private:
|
||||
bool parsePluginConfig(const json&, CustomResponseConfigRule&) override;
|
||||
};
|
||||
|
||||
// Per-stream context.
|
||||
class PluginContext : public Context {
|
||||
public:
|
||||
explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {}
|
||||
FilterHeadersStatus onRequestHeaders(uint32_t, bool) override;
|
||||
FilterHeadersStatus onResponseHeaders(uint32_t, bool) override;
|
||||
|
||||
private:
|
||||
inline PluginRootContext* rootContext() {
|
||||
return dynamic_cast<PluginRootContext*>(this->root());
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace custom_response
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
186
plugins/wasm-cpp/extensions/custom_response/plugin_test.cc
Normal file
186
plugins/wasm-cpp/extensions/custom_response/plugin_test.cc
Normal file
@@ -0,0 +1,186 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/custom_response/plugin.h"
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "include/proxy-wasm/context.h"
|
||||
#include "include/proxy-wasm/null.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace custom_response {
|
||||
|
||||
NullPluginRegistry* context_registry_;
|
||||
RegisterNullVmPluginFactory register_custom_response_plugin(
|
||||
"custom_response", []() {
|
||||
return std::make_unique<NullPlugin>(custom_response::context_registry_);
|
||||
});
|
||||
|
||||
class MockContext : public proxy_wasm::ContextBase {
|
||||
public:
|
||||
MockContext(WasmBase* wasm) : ContextBase(wasm) {}
|
||||
|
||||
MOCK_METHOD(BufferInterface*, getBuffer, (WasmBufferType));
|
||||
MOCK_METHOD(WasmResult, log, (uint32_t, std::string_view));
|
||||
MOCK_METHOD(WasmResult, getHeaderMapValue,
|
||||
(WasmHeaderMapType /* type */, std::string_view /* key */,
|
||||
std::string_view* /*result */));
|
||||
MOCK_METHOD(WasmResult, replaceHeaderMapValue,
|
||||
(WasmHeaderMapType /* type */, std::string_view /* key */,
|
||||
std::string_view /* value */));
|
||||
MOCK_METHOD(WasmResult, sendLocalResponse,
|
||||
(uint32_t /* response_code */, std::string_view /* body */,
|
||||
Pairs /* additional_headers */, uint32_t /* grpc_status */,
|
||||
std::string_view /* details */));
|
||||
MOCK_METHOD(WasmResult, getProperty, (std::string_view, std::string*));
|
||||
};
|
||||
|
||||
class CustomResponseTest : public ::testing::Test {
|
||||
protected:
|
||||
CustomResponseTest() {
|
||||
// Initialize test VM
|
||||
test_vm_ = createNullVm();
|
||||
wasm_base_ = std::make_unique<WasmBase>(
|
||||
std::move(test_vm_), "test-vm", "", "",
|
||||
std::unordered_map<std::string, std::string>{},
|
||||
AllowedCapabilitiesMap{});
|
||||
wasm_base_->load("custom_response");
|
||||
wasm_base_->initialize();
|
||||
|
||||
// Initialize host side context
|
||||
mock_context_ = std::make_unique<MockContext>(wasm_base_.get());
|
||||
current_context_ = mock_context_.get();
|
||||
|
||||
ON_CALL(*mock_context_, log(testing::_, testing::_))
|
||||
.WillByDefault([](uint32_t, std::string_view m) {
|
||||
std::cerr << m << "\n";
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getHeaderMapValue(WasmHeaderMapType::RequestHeaders,
|
||||
testing::_, testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view header,
|
||||
std::string_view* result) {
|
||||
if (header == ":authority") {
|
||||
*result = authority_;
|
||||
}
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_,
|
||||
replaceHeaderMapValue(WasmHeaderMapType::RequestHeaders, testing::_,
|
||||
testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view key,
|
||||
std::string_view value) { return WasmResult::Ok; });
|
||||
|
||||
ON_CALL(*mock_context_,
|
||||
getHeaderMapValue(WasmHeaderMapType::ResponseHeaders, testing::_,
|
||||
testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view header,
|
||||
std::string_view* result) {
|
||||
if (header == ":status") {
|
||||
*result = status_code_;
|
||||
}
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getProperty(testing::_, testing::_))
|
||||
.WillByDefault([&](std::string_view path, std::string* result) {
|
||||
*result = route_name_;
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
// Initialize Wasm sandbox context
|
||||
root_context_ = std::make_unique<PluginRootContext>(0, "");
|
||||
context_ = std::make_unique<PluginContext>(1, root_context_.get());
|
||||
}
|
||||
~CustomResponseTest() override {}
|
||||
|
||||
std::unique_ptr<WasmBase> wasm_base_;
|
||||
std::unique_ptr<WasmVm> test_vm_;
|
||||
std::unique_ptr<MockContext> mock_context_;
|
||||
|
||||
std::unique_ptr<PluginRootContext> root_context_;
|
||||
std::unique_ptr<PluginContext> context_;
|
||||
|
||||
std::string authority_;
|
||||
std::string route_name_;
|
||||
std::string status_code_;
|
||||
};
|
||||
|
||||
TEST_F(CustomResponseTest, EnableOnStatus) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"enable_on_status": [429],
|
||||
"headers": ["abc=123","zty=test"],
|
||||
"status_code": 233,
|
||||
"body": "{\"abc\":123}"
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
status_code_ = "200";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
EXPECT_EQ(context_->onResponseHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
status_code_ = "429";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(233, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onResponseHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
|
||||
TEST_F(CustomResponseTest, NoGlobalRule) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_": [{
|
||||
"_match_route_": ["test"],
|
||||
"headers": ["abc=123","zty=test"],
|
||||
"status_code": 233,
|
||||
"body": "{\"abc\":123}"
|
||||
}]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
EXPECT_EQ(context_->onResponseHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
route_name_ = "test";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(233, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
} // namespace custom_response
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
63
plugins/wasm-cpp/extensions/hmac_auth/BUILD
Normal file
63
plugins/wasm-cpp/extensions/hmac_auth/BUILD
Normal file
@@ -0,0 +1,63 @@
|
||||
load("@proxy_wasm_cpp_sdk//bazel/wasm:wasm.bzl", "wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
wasm_cc_binary(
|
||||
name = "hmac_auth.wasm",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
"plugin.h",
|
||||
"//common:base64.h",
|
||||
],
|
||||
deps = [
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/strings:str_format",
|
||||
"@com_google_absl//absl/time",
|
||||
"//common:json_util",
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics",
|
||||
"//common:crypto_util",
|
||||
"//common:http_util",
|
||||
"//common:rule_util",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "hmac_auth_lib",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
"//common:base64.h",
|
||||
],
|
||||
hdrs = [
|
||||
"plugin.h",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/strings:str_format",
|
||||
"@com_google_absl//absl/time",
|
||||
"//common:json_util",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
"//common:crypto_util",
|
||||
"//common:http_util",
|
||||
"//common:rule_util",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "hmac_auth_test",
|
||||
srcs = [
|
||||
"plugin_test.cc",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
":hmac_auth_lib",
|
||||
"@com_google_googletest//:gtest",
|
||||
"@com_google_googletest//:gtest_main",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
],
|
||||
linkopts = ["-lcrypt"],
|
||||
)
|
||||
|
||||
declare_wasm_image_targets(
|
||||
name = "hmac_auth",
|
||||
wasm_file = ":hmac_auth.wasm",
|
||||
)
|
||||
509
plugins/wasm-cpp/extensions/hmac_auth/plugin.cc
Normal file
509
plugins/wasm-cpp/extensions/hmac_auth/plugin.cc
Normal file
@@ -0,0 +1,509 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/hmac_auth/plugin.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <valarray>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_replace.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "common/base64.h"
|
||||
#include "common/crypto_util.h"
|
||||
#include "common/http_util.h"
|
||||
#include "common/json_util.h"
|
||||
|
||||
using ::nlohmann::json;
|
||||
using ::Wasm::Common::JsonArrayIterate;
|
||||
using ::Wasm::Common::JsonGetField;
|
||||
using ::Wasm::Common::JsonObjectIterate;
|
||||
using ::Wasm::Common::JsonValueAs;
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace hmac_auth {
|
||||
|
||||
PROXY_WASM_NULL_PLUGIN_REGISTRY
|
||||
|
||||
#endif
|
||||
|
||||
static RegisterContextFactory register_HmacAuth(
|
||||
CONTEXT_FACTORY(PluginContext), ROOT_FACTORY(PluginRootContext));
|
||||
|
||||
static constexpr std::string_view CA_KEY = "x-ca-key";
|
||||
static constexpr std::string_view CA_SIGNATURE_METHOD = "x-ca-signature-method";
|
||||
static constexpr std::string_view CA_SIGNATURE_HEADERS =
|
||||
"x-ca-signature-headers";
|
||||
static constexpr std::string_view CA_SIGNATURE = "x-ca-signature";
|
||||
static constexpr std::string_view CA_ERRMSG = "x-ca-error-message";
|
||||
static constexpr std::string_view CA_TIMESTAMP = "x-ca-timestamp";
|
||||
|
||||
static constexpr size_t MILLISEC_MIN_LENGTH = 13;
|
||||
|
||||
static constexpr std::array<std::string_view, 5> CHECK_HEADERS{
|
||||
Wasm::Common::Http::Header::Method,
|
||||
Wasm::Common::Http::Header::Accept,
|
||||
Wasm::Common::Http::Header::ContentMD5,
|
||||
Wasm::Common::Http::Header::ContentType,
|
||||
Wasm::Common::Http::Header::Date,
|
||||
};
|
||||
|
||||
static constexpr size_t MAX_BODY_SIZE = 32 * 1024 * 1024;
|
||||
|
||||
static constexpr int64_t NANO_SECONDS = 1000 * 1000 * 1000;
|
||||
|
||||
namespace {
|
||||
|
||||
void deniedInvalidCaKey() {
|
||||
sendLocalResponse(401, "Invalid Key", "Invalid Key", {});
|
||||
}
|
||||
|
||||
void deniedNoSignature() {
|
||||
sendLocalResponse(401, "Empty Signature", "Empty Signature", {});
|
||||
}
|
||||
|
||||
void deniedUnauthorizedConsumer() {
|
||||
sendLocalResponse(403, "Unauthorized Consumer", "Unauthorized Consumer", {});
|
||||
}
|
||||
|
||||
void deniedInvalidCredentials(const std::string& errmsg) {
|
||||
sendLocalResponse(400, "Invalid Signature", "Invalid Signature",
|
||||
{{std::string(CA_ERRMSG), errmsg}});
|
||||
}
|
||||
|
||||
void deniedInvalidContentMD5() {
|
||||
sendLocalResponse(400, "Invalid Content-MD5", "Invalid Content-MD5", {});
|
||||
}
|
||||
|
||||
void deniedInvalidDate() {
|
||||
sendLocalResponse(400, "Invalid Date", "Invalid Date", {});
|
||||
}
|
||||
|
||||
void deniedBodyTooLarge() {
|
||||
sendLocalResponse(413, "Request Body Too Large", "Request Body Too Large",
|
||||
{});
|
||||
}
|
||||
|
||||
std::string getStringToSign() {
|
||||
std::string message;
|
||||
for (const auto& header : CHECK_HEADERS) {
|
||||
auto header_value = getRequestHeader(header)->toString();
|
||||
absl::StrAppendFormat(&message, "%s\n", header_value);
|
||||
}
|
||||
|
||||
auto dynamic_check_headers =
|
||||
getRequestHeader(CA_SIGNATURE_HEADERS)->toString();
|
||||
std::vector<std::string> header_arr;
|
||||
for (const auto& header : absl::StrSplit(dynamic_check_headers, ",")) {
|
||||
auto lower_header = absl::AsciiStrToLower(header);
|
||||
if (lower_header == CA_SIGNATURE || lower_header == CA_SIGNATURE_HEADERS) {
|
||||
continue;
|
||||
}
|
||||
bool is_static = false;
|
||||
for (const auto& h : CHECK_HEADERS) {
|
||||
if (h == lower_header) {
|
||||
is_static = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!is_static) {
|
||||
header_arr.push_back(std::move(lower_header));
|
||||
}
|
||||
}
|
||||
std::sort(header_arr.begin(), header_arr.end());
|
||||
for (const auto& header : header_arr) {
|
||||
auto header_value = getRequestHeader(header)->toString();
|
||||
absl::StrAppendFormat(&message, "%s:%s\n", header, header_value);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
void getStringToSignWithParam(
|
||||
std::string* str_to_sign, const std::string& path,
|
||||
std::optional<std::reference_wrapper<Wasm::Common::Http::QueryParams>>
|
||||
body_params) {
|
||||
// need alphabetical order
|
||||
auto params =
|
||||
Wasm::Common::Http::parseAndDecodeQueryString(std::string(path));
|
||||
if (body_params) {
|
||||
for (auto&& param : body_params.value().get()) {
|
||||
params.emplace(param);
|
||||
}
|
||||
}
|
||||
auto url_path = path.substr(0, path.find('?'));
|
||||
absl::StrAppend(str_to_sign, url_path);
|
||||
if (params.empty()) {
|
||||
return;
|
||||
}
|
||||
str_to_sign->append("?");
|
||||
auto it = params.begin();
|
||||
for (; it != std::prev(params.end()); it++) {
|
||||
absl::StrAppendFormat(str_to_sign, "%s=%s&", it->first, it->second);
|
||||
}
|
||||
absl::StrAppendFormat(str_to_sign, "%s=%s", it->first, it->second);
|
||||
return;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
HmacAuthConfigRule& rule) {
|
||||
if ((configuration.find("consumers") != configuration.end()) &&
|
||||
(configuration.find("credentials") != configuration.end())) {
|
||||
LOG_WARN(
|
||||
"The consumers field and the credentials field cannot appear at the "
|
||||
"same level");
|
||||
return false;
|
||||
}
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "credentials", [&](const json& credential) -> bool {
|
||||
auto item = credential.find("key");
|
||||
if (item == credential.end()) {
|
||||
LOG_WARN("can't find 'key' field in credential.");
|
||||
return false;
|
||||
}
|
||||
auto key = JsonValueAs<std::string>(item.value());
|
||||
if (key.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!key.first) {
|
||||
return false;
|
||||
}
|
||||
item = credential.find("secret");
|
||||
if (item == credential.end()) {
|
||||
LOG_WARN("can't find 'secret' field in credential.");
|
||||
return false;
|
||||
}
|
||||
auto secret = JsonValueAs<std::string>(item.value());
|
||||
if (secret.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!secret.first) {
|
||||
return false;
|
||||
}
|
||||
auto result = rule.credentials.emplace(
|
||||
std::make_pair(key.first.value(), secret.first.value()));
|
||||
if (!result.second) {
|
||||
LOG_WARN(absl::StrCat("duplicate credential key: ",
|
||||
key.first.value()));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for credentials.");
|
||||
return false;
|
||||
}
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "consumers", [&](const json& consumer) -> bool {
|
||||
auto item = consumer.find("key");
|
||||
if (item == consumer.end()) {
|
||||
LOG_WARN("can't find 'key' field in consumer.");
|
||||
return false;
|
||||
}
|
||||
auto key = JsonValueAs<std::string>(item.value());
|
||||
if (key.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!key.first) {
|
||||
return false;
|
||||
}
|
||||
item = consumer.find("secret");
|
||||
if (item == consumer.end()) {
|
||||
LOG_WARN("can't find 'secret' field in consumer.");
|
||||
return false;
|
||||
}
|
||||
auto secret = JsonValueAs<std::string>(item.value());
|
||||
if (secret.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!secret.first) {
|
||||
return false;
|
||||
}
|
||||
item = consumer.find("name");
|
||||
if (item == consumer.end()) {
|
||||
LOG_WARN("can't find 'name' field in consumer.");
|
||||
return false;
|
||||
}
|
||||
auto name = JsonValueAs<std::string>(item.value());
|
||||
if (name.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!name.first) {
|
||||
return false;
|
||||
}
|
||||
if (rule.credentials.find(key.first.value()) !=
|
||||
rule.credentials.end()) {
|
||||
LOG_WARN(
|
||||
absl::StrCat("duplicate consumer key: ", key.first.value()));
|
||||
return false;
|
||||
}
|
||||
rule.credentials.emplace(
|
||||
std::make_pair(key.first.value(), secret.first.value()));
|
||||
rule.key_to_name.emplace(
|
||||
std::make_pair(key.first.value(), name.first.value()));
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for credentials.");
|
||||
return false;
|
||||
}
|
||||
if (rule.credentials.empty()) {
|
||||
LOG_INFO("at least one credential has to be configured for a rule.");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto it = configuration.find("date_offset");
|
||||
if (it != configuration.end()) {
|
||||
auto date_offset = JsonValueAs<int64_t>(it.value());
|
||||
if (date_offset.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!date_offset.first) {
|
||||
LOG_WARN("failed to parse 'date_offset' field in configuration.");
|
||||
return false;
|
||||
}
|
||||
rule.date_nano_offset = date_offset.first.value() * NANO_SECONDS;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::checkConsumer(
|
||||
const std::string& ca_key, const HmacAuthConfigRule& rule,
|
||||
const std::optional<std::unordered_set<std::string>>& allow_set) {
|
||||
if (ca_key.empty()) {
|
||||
LOG_DEBUG("empty key");
|
||||
deniedInvalidCaKey();
|
||||
return false;
|
||||
}
|
||||
auto credentials_iter = rule.credentials.find(std::string(ca_key));
|
||||
if (credentials_iter == rule.credentials.end()) {
|
||||
LOG_DEBUG(absl::StrCat("can't find secret through key: ", ca_key));
|
||||
deniedInvalidCaKey();
|
||||
return false;
|
||||
}
|
||||
auto key_to_name_iter = rule.key_to_name.find(std::string(ca_key));
|
||||
if (key_to_name_iter != rule.key_to_name.end()) {
|
||||
if (allow_set && !allow_set.value().empty()) {
|
||||
if (allow_set.value().find(key_to_name_iter->second) ==
|
||||
allow_set.value().end()) {
|
||||
LOG_DEBUG(absl::StrCat("consumer is not allowed: ",
|
||||
key_to_name_iter->second));
|
||||
deniedUnauthorizedConsumer();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
addRequestHeader("X-Mse-Consumer", key_to_name_iter->second);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::checkPlugin(
|
||||
const std::string& ca_key, const std::string& signature,
|
||||
const std::string& signature_method, const std::string& path,
|
||||
const std::string& date, bool is_timetamp, std::string* sts,
|
||||
const HmacAuthConfigRule& rule,
|
||||
std::optional<std::reference_wrapper<Wasm::Common::Http::QueryParams>>
|
||||
body_params) {
|
||||
if (ca_key.empty()) {
|
||||
LOG_DEBUG("empty key");
|
||||
deniedInvalidCaKey();
|
||||
return false;
|
||||
}
|
||||
if (signature.empty()) {
|
||||
LOG_DEBUG("empty signature");
|
||||
deniedNoSignature();
|
||||
return false;
|
||||
}
|
||||
int64_t time_offset = 0;
|
||||
if (rule.date_nano_offset > 0) {
|
||||
auto current_time = getCurrentTimeNanoseconds();
|
||||
if (!is_timetamp) {
|
||||
auto time_from_date = Wasm::Common::Http::httpTime(date);
|
||||
if (!Wasm::Common::Http::timePointValid(time_from_date)) {
|
||||
LOG_DEBUG(absl::StrFormat("invalid date format: %s", date));
|
||||
deniedInvalidDate();
|
||||
return false;
|
||||
}
|
||||
time_offset = std::abs(
|
||||
(long long)(std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
time_from_date.time_since_epoch())
|
||||
.count() -
|
||||
current_time));
|
||||
} else {
|
||||
int64_t timestamp;
|
||||
if (!absl::SimpleAtoi(date, ×tamp)) {
|
||||
LOG_DEBUG(absl::StrFormat("invalid timestamp format: %s", date));
|
||||
deniedInvalidDate();
|
||||
return false;
|
||||
}
|
||||
time_offset = std::abs((long long)(timestamp - current_time));
|
||||
// milliseconds to nanoseconds
|
||||
time_offset *= 1e6;
|
||||
// seconds
|
||||
if (date.size() < MILLISEC_MIN_LENGTH) {
|
||||
time_offset *= 1e3;
|
||||
}
|
||||
}
|
||||
if (time_offset > rule.date_nano_offset) {
|
||||
LOG_DEBUG(absl::StrFormat("date expired, offset is: %u",
|
||||
time_offset / NANO_SECONDS));
|
||||
deniedInvalidDate();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
std::string hash_type{"sha256"};
|
||||
if (signature_method == "HmacSHA1") {
|
||||
hash_type = "sha1";
|
||||
}
|
||||
auto credentials_iter = rule.credentials.find(std::string(ca_key));
|
||||
if (credentials_iter == rule.credentials.end()) {
|
||||
LOG_DEBUG(absl::StrCat("can't find secret through key: ", ca_key));
|
||||
deniedInvalidCaKey();
|
||||
return false;
|
||||
}
|
||||
const auto& secret = credentials_iter->second;
|
||||
getStringToSignWithParam(sts, path, body_params);
|
||||
const auto& str_to_sign = *sts;
|
||||
auto hmac =
|
||||
Wasm::Common::Crypto::getShaHmacBase64(hash_type, secret, str_to_sign);
|
||||
if (hmac != signature) {
|
||||
auto tip = absl::StrReplaceAll(str_to_sign, {{"\n", "#"}});
|
||||
LOG_DEBUG(absl::StrCat("invalid signature, stringToSign: ", tip,
|
||||
" signature: ", hmac));
|
||||
deniedInvalidCredentials(absl::StrFormat("Server StringToSign:`%s`", tip));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::onConfigure(size_t size) {
|
||||
// Parse configuration JSON string.
|
||||
if (size > 0 && !configure(size)) {
|
||||
LOG_WARN("configuration has errors initialization will not continue.");
|
||||
setInvalidConfig();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::configure(size_t configuration_size) {
|
||||
auto configuration_data = getBufferBytes(WasmBufferType::PluginConfiguration,
|
||||
0, configuration_size);
|
||||
// Parse configuration JSON string.
|
||||
auto result = ::Wasm::Common::JsonParse(configuration_data->view());
|
||||
if (!result) {
|
||||
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
configuration_data->view()));
|
||||
return false;
|
||||
}
|
||||
if (!parseAuthRuleConfig(result.value())) {
|
||||
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
configuration_data->view()));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) {
|
||||
ca_key_ = getRequestHeader(CA_KEY)->toString();
|
||||
signature_ = getRequestHeader(CA_SIGNATURE)->toString();
|
||||
signature_method_ = getRequestHeader(CA_SIGNATURE_METHOD)->toString();
|
||||
path_ = getRequestHeader(Wasm::Common::Http::Header::Path)->toString();
|
||||
date_ = getRequestHeader(Wasm::Common::Http::Header::Date)->toString();
|
||||
str_to_sign_ = getStringToSign();
|
||||
body_md5_ =
|
||||
getRequestHeader(Wasm::Common::Http::Header::ContentMD5)->toString();
|
||||
GET_HEADER_VIEW(Wasm::Common::Http::Header::ContentType, content_type);
|
||||
|
||||
if (date_.empty()) {
|
||||
date_ = getRequestHeader(CA_TIMESTAMP)->toString();
|
||||
is_timestamp_ = true;
|
||||
}
|
||||
auto* rootCtx = rootContext();
|
||||
|
||||
auto config = rootCtx->getMatchAuthConfig();
|
||||
config_ = config.first;
|
||||
if (!config_) {
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
allow_set_ = config.second;
|
||||
// check if ca_key present in config and it's consumer_name is allowed
|
||||
if (!rootCtx->checkConsumer(ca_key_, config_.value(), allow_set_)) {
|
||||
return FilterHeadersStatus::StopIteration;
|
||||
}
|
||||
|
||||
if (absl::StrContains(absl::AsciiStrToLower(content_type),
|
||||
"application/x-www-form-urlencoded")) {
|
||||
check_body_params_ = true;
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
|
||||
return rootCtx->checkPlugin(ca_key_, signature_, signature_method_, path_,
|
||||
date_, is_timestamp_, &str_to_sign_,
|
||||
config_.value(), std::nullopt)
|
||||
? FilterHeadersStatus::Continue
|
||||
: FilterHeadersStatus::StopIteration;
|
||||
}
|
||||
|
||||
FilterDataStatus PluginContext::onRequestBody(size_t body_size,
|
||||
bool end_stream) {
|
||||
if (!config_) {
|
||||
return FilterDataStatus::Continue;
|
||||
}
|
||||
if (body_md5_.empty() && !check_body_params_) {
|
||||
return FilterDataStatus::Continue;
|
||||
}
|
||||
body_total_size_ += body_size;
|
||||
if (body_total_size_ > MAX_BODY_SIZE) {
|
||||
LOG_DEBUG("body_size is too large");
|
||||
deniedBodyTooLarge();
|
||||
return FilterDataStatus::StopIterationNoBuffer;
|
||||
}
|
||||
if (!end_stream) {
|
||||
return FilterDataStatus::StopIterationAndBuffer;
|
||||
}
|
||||
auto body =
|
||||
getBufferBytes(WasmBufferType::HttpRequestBody, 0, body_total_size_);
|
||||
LOG_DEBUG("body: " + body->toString());
|
||||
if (!body_md5_.empty()) {
|
||||
if (body->size() == 0) {
|
||||
LOG_DEBUG("got empty body");
|
||||
deniedInvalidContentMD5();
|
||||
return FilterDataStatus::StopIterationNoBuffer;
|
||||
}
|
||||
auto md5 = Wasm::Common::Crypto::getMD5Base64(body->view());
|
||||
if (md5 != body_md5_) {
|
||||
LOG_DEBUG(
|
||||
absl::StrFormat("body md5 expect: %s, actual: %s", body_md5_, md5));
|
||||
deniedInvalidContentMD5();
|
||||
return FilterDataStatus::StopIterationNoBuffer;
|
||||
}
|
||||
}
|
||||
if (check_body_params_) {
|
||||
auto body_params = Wasm::Common::Http::parseFromBody(body->view());
|
||||
auto* rootCtx = rootContext();
|
||||
return rootCtx->checkPlugin(ca_key_, signature_, signature_method_, path_,
|
||||
date_, is_timestamp_, &str_to_sign_,
|
||||
config_.value(), body_params)
|
||||
? FilterDataStatus::Continue
|
||||
: FilterDataStatus::StopIterationNoBuffer;
|
||||
}
|
||||
return FilterDataStatus::Continue;
|
||||
}
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace hmac_auth
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
105
plugins/wasm-cpp/extensions/hmac_auth/plugin.h
Normal file
105
plugins/wasm-cpp/extensions/hmac_auth/plugin.h
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "common/http_util.h"
|
||||
#include "common/route_rule_matcher.h"
|
||||
#define ASSERT(_X) assert(_X)
|
||||
|
||||
#ifndef NULL_PLUGIN
|
||||
|
||||
#include "proxy_wasm_intrinsics.h"
|
||||
|
||||
#else
|
||||
|
||||
#include "include/proxy-wasm/null_plugin.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace hmac_auth {
|
||||
|
||||
#endif
|
||||
|
||||
struct HmacAuthConfigRule {
|
||||
std::unordered_map<std::string, std::string> credentials;
|
||||
std::unordered_map<std::string, std::string> key_to_name;
|
||||
int64_t date_nano_offset = -1;
|
||||
};
|
||||
|
||||
// PluginRootContext is the root context for all streams processed by the
|
||||
// thread. It has the same lifetime as the worker thread and acts as target for
|
||||
// interactions that outlives individual stream, e.g. timer, async calls.
|
||||
class PluginRootContext : public RootContext,
|
||||
public RouteRuleMatcher<HmacAuthConfigRule> {
|
||||
public:
|
||||
PluginRootContext(uint32_t id, std::string_view root_id)
|
||||
: RootContext(id, root_id) {}
|
||||
~PluginRootContext() {}
|
||||
bool onConfigure(size_t) override;
|
||||
bool checkPlugin(
|
||||
const std::string& ca_key, const std::string& signature,
|
||||
const std::string& signature_method, const std::string& path,
|
||||
const std::string& date, bool is_timestamp, std::string* sts,
|
||||
const HmacAuthConfigRule&,
|
||||
std::optional<std::reference_wrapper<Wasm::Common::Http::QueryParams>>);
|
||||
bool checkConsumer(const std::string&, const HmacAuthConfigRule&,
|
||||
const std::optional<std::unordered_set<std::string>>&);
|
||||
bool configure(size_t);
|
||||
|
||||
private:
|
||||
bool parsePluginConfig(const json&, HmacAuthConfigRule&) override;
|
||||
};
|
||||
|
||||
// Per-stream context.
|
||||
class PluginContext : public Context {
|
||||
public:
|
||||
explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {}
|
||||
FilterHeadersStatus onRequestHeaders(uint32_t, bool) override;
|
||||
FilterDataStatus onRequestBody(size_t, bool) override;
|
||||
|
||||
private:
|
||||
inline PluginRootContext* rootContext() {
|
||||
return dynamic_cast<PluginRootContext*>(this->root());
|
||||
}
|
||||
|
||||
std::string ca_key_;
|
||||
std::string signature_;
|
||||
std::string signature_method_;
|
||||
std::string path_;
|
||||
std::string date_;
|
||||
std::string str_to_sign_;
|
||||
std::string body_md5_;
|
||||
bool is_timestamp_ = false;
|
||||
std::optional<std::reference_wrapper<HmacAuthConfigRule>> config_;
|
||||
std::optional<std::unordered_set<std::string>> allow_set_;
|
||||
bool check_body_params_ = false;
|
||||
size_t body_total_size_ = 0;
|
||||
};
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace hmac_auth
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
599
plugins/wasm-cpp/extensions/hmac_auth/plugin_test.cc
Normal file
599
plugins/wasm-cpp/extensions/hmac_auth/plugin_test.cc
Normal file
@@ -0,0 +1,599 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/hmac_auth/plugin.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
#include "common/base64.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "include/proxy-wasm/context.h"
|
||||
#include "include/proxy-wasm/null.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace hmac_auth {
|
||||
|
||||
NullPluginRegistry* context_registry_;
|
||||
RegisterNullVmPluginFactory register_hmac_auth_plugin("hmac_auth", []() {
|
||||
return std::make_unique<NullPlugin>(hmac_auth::context_registry_);
|
||||
});
|
||||
|
||||
class MockContext : public proxy_wasm::ContextBase {
|
||||
public:
|
||||
MockContext(WasmBase* wasm) : ContextBase(wasm) {}
|
||||
|
||||
MOCK_METHOD(BufferInterface*, getBuffer, (WasmBufferType));
|
||||
MOCK_METHOD(WasmResult, log, (uint32_t, std::string_view));
|
||||
MOCK_METHOD(WasmResult, getHeaderMapValue,
|
||||
(WasmHeaderMapType /* type */, std::string_view /* key */,
|
||||
std::string_view* /*result */));
|
||||
MOCK_METHOD(WasmResult, addHeaderMapValue,
|
||||
(WasmHeaderMapType /* type */, std::string_view /* key */,
|
||||
std::string_view /* value */));
|
||||
MOCK_METHOD(WasmResult, sendLocalResponse,
|
||||
(uint32_t /* response_code */, std::string_view /* body */,
|
||||
Pairs /* additional_headers */, uint32_t /* grpc_status */,
|
||||
std::string_view /* details */));
|
||||
MOCK_METHOD(uint64_t, getCurrentTimeNanoseconds, ());
|
||||
MOCK_METHOD(WasmResult, getProperty, (std::string_view, std::string*));
|
||||
};
|
||||
|
||||
class HmacAuthTest : public ::testing::Test {
|
||||
protected:
|
||||
HmacAuthTest() {
|
||||
// Initialize test VM
|
||||
test_vm_ = createNullVm();
|
||||
wasm_base_ = std::make_unique<WasmBase>(
|
||||
std::move(test_vm_), "test-vm", "", "",
|
||||
std::unordered_map<std::string, std::string>{},
|
||||
AllowedCapabilitiesMap{});
|
||||
wasm_base_->load("hmac_auth");
|
||||
wasm_base_->initialize();
|
||||
|
||||
// Initialize host side context
|
||||
mock_context_ = std::make_unique<MockContext>(wasm_base_.get());
|
||||
current_context_ = mock_context_.get();
|
||||
|
||||
ON_CALL(*mock_context_, log(testing::_, testing::_))
|
||||
.WillByDefault([](uint32_t, std::string_view m) {
|
||||
std::cerr << m << "\n";
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getHeaderMapValue(WasmHeaderMapType::RequestHeaders,
|
||||
testing::_, testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view header,
|
||||
std::string_view* result) {
|
||||
auto it = headers_.find(std::string(header));
|
||||
if (it == headers_.end()) {
|
||||
std::cerr << header << " not found.\n";
|
||||
return WasmResult::NotFound;
|
||||
}
|
||||
*result = it->second;
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, addHeaderMapValue(WasmHeaderMapType::RequestHeaders,
|
||||
testing::_, testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view key,
|
||||
std::string_view value) { return WasmResult::Ok; });
|
||||
|
||||
ON_CALL(*mock_context_, getBuffer(testing::_))
|
||||
.WillByDefault([&](WasmBufferType type) {
|
||||
if (type == WasmBufferType::HttpRequestBody) {
|
||||
return &body_;
|
||||
}
|
||||
return &config_;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getCurrentTimeNanoseconds()).WillByDefault([&]() {
|
||||
return current_time_;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getProperty(testing::_, testing::_))
|
||||
.WillByDefault([&](std::string_view path, std::string* result) {
|
||||
*result = route_name_;
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
// Initialize Wasm sandbox context
|
||||
root_context_ = std::make_unique<PluginRootContext>(0, "");
|
||||
context_ = std::make_unique<PluginContext>(1, root_context_.get());
|
||||
}
|
||||
~HmacAuthTest() override {}
|
||||
|
||||
std::unique_ptr<WasmBase> wasm_base_;
|
||||
std::unique_ptr<WasmVm> test_vm_;
|
||||
std::unique_ptr<MockContext> mock_context_;
|
||||
|
||||
std::unique_ptr<PluginRootContext> root_context_;
|
||||
std::unique_ptr<PluginContext> context_;
|
||||
|
||||
std::map<std::string, std::string> headers_;
|
||||
std::string route_name_;
|
||||
BufferBase body_;
|
||||
BufferBase config_;
|
||||
uint64_t current_time_;
|
||||
};
|
||||
|
||||
TEST_F(HmacAuthTest, Sign) {
|
||||
headers_ = {
|
||||
{":path",
|
||||
"/http2test/test?param1=test&username=xiaoming&password=123456789"},
|
||||
{":method", "POST"},
|
||||
{"accept", "application/json; charset=utf-8"},
|
||||
{"ca_version", "1"},
|
||||
{"content-type", "application/x-www-form-urlencoded; charset=utf-8"},
|
||||
{"x-ca-timestamp", "1525872629832"},
|
||||
{"date", "Wed, 09 May 2018 13:30:29 GMT+00:00"},
|
||||
{"user-agent", "ALIYUN-ANDROID-DEMO"},
|
||||
{"x-ca-nonce", "c9f15cbf-f4ac-4a6c-b54d-f51abf4b5b44"},
|
||||
{"content-length", "33"},
|
||||
{"username", "xiaoming&password=123456789"},
|
||||
{"x-ca-key", "203753385"},
|
||||
{"x-ca-signature-method", "HmacSHA256"},
|
||||
{"x-ca-signature", "xfX+bZxY2yl7EB/qdoDy9v/uscw3Nnj1pgoU+Bm6xdM="},
|
||||
{"x-ca-signature-headers",
|
||||
"x-ca-timestamp,x-ca-key,x-ca-nonce,x-ca-signature-method"},
|
||||
};
|
||||
// auto actual = root_context_->getStringToSign(
|
||||
// "/http2test/test?param1=test&username=xiaoming&password=123456789",
|
||||
// std::nullopt);
|
||||
// EXPECT_EQ(actual, R"(POST
|
||||
// application/json; charset=utf-8
|
||||
|
||||
// application/x-www-form-urlencoded; charset=utf-8
|
||||
// Wed, 09 May 2018 13:30:29 GMT+00:00
|
||||
// x-ca-key:203753385
|
||||
// x-ca-nonce:c9f15cbf-f4ac-4a6c-b54d-f51abf4b5b44
|
||||
// x-ca-signature-method:HmacSHA256
|
||||
// x-ca-timestamp:1525872629832
|
||||
// /http2test/test?param1=test&password=123456789&username=xiaoming)");
|
||||
|
||||
headers_ = {
|
||||
{":path", "/Third/Tools/checkSign"},
|
||||
{":method", "GET"},
|
||||
{"accept", "application/json"},
|
||||
{"content-type", "application/json"},
|
||||
{"x-ca-timestamp", "1646365291734"},
|
||||
{"x-ca-nonce", "787dd0c2-7bd8-41cd-9c19-62c05ea524a2"},
|
||||
{"x-ca-key", "appKey"},
|
||||
{"x-ca-signature-headers", "x-ca-key,x-ca-nonce,x-ca-timestamp"},
|
||||
{"x-ca-signature", "EdJSFAMOWyXZOpXhevZnjuS0ZafnwnCqaSk5hz+tXo8="},
|
||||
};
|
||||
HmacAuthConfigRule rule;
|
||||
rule.credentials = {{"appKey", "appSecret"}};
|
||||
// EXPECT_EQ(root_context_->checkPlugin(rule, std::nullopt), true);
|
||||
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_":["test"],
|
||||
"credentials":[
|
||||
{"key": "appKey", "secret": "appSecret"}
|
||||
]
|
||||
}
|
||||
]
|
||||
})";
|
||||
route_name_ = "test";
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(HmacAuthTest, SignWithConsumer) {
|
||||
headers_ = {
|
||||
{":path", "/Third/Tools/checkSign"},
|
||||
{":method", "GET"},
|
||||
{"accept", "application/json"},
|
||||
{"content-type", "application/json"},
|
||||
{"x-ca-timestamp", "1646365291734"},
|
||||
{"x-ca-nonce", "787dd0c2-7bd8-41cd-9c19-62c05ea524a2"},
|
||||
{"x-ca-key", "appKey"},
|
||||
{"x-ca-signature-headers", "x-ca-key,x-ca-nonce,x-ca-timestamp"},
|
||||
{"x-ca-signature", "EdJSFAMOWyXZOpXhevZnjuS0ZafnwnCqaSk5hz+tXo8="},
|
||||
};
|
||||
HmacAuthConfigRule rule;
|
||||
rule.credentials = {{"appKey", "appSecret"}};
|
||||
// EXPECT_EQ(root_context_->checkPlugin(rule, std::nullopt), true);
|
||||
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers": [{"key": "appKey", "secret": "appSecret", "name": "consumer"}],
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_":["test"],
|
||||
"allow":["consumer"]
|
||||
}
|
||||
]
|
||||
})";
|
||||
route_name_ = "test";
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(HmacAuthTest, ParamInBody) {
|
||||
headers_ = {
|
||||
{":path", "/http2test/test?param1=test"},
|
||||
{":method", "POST"},
|
||||
{"accept", "application/json; charset=utf-8"},
|
||||
{"ca_version", "1"},
|
||||
{"content-type", "application/x-www-form-urlencoded; charset=utf-8"},
|
||||
{"x-ca-timestamp", "1525872629832"},
|
||||
{"date", "Wed, 09 May 2018 13:30:29 GMT+00:00"},
|
||||
{"user-agent", "ALIYUN-ANDROID-DEMO"},
|
||||
{"x-ca-nonce", "c9f15cbf-f4ac-4a6c-b54d-f51abf4b5b44"},
|
||||
{"content-length", "33"},
|
||||
{"username", "xiaoming&password=123456789"},
|
||||
{"x-ca-key", "203753385"},
|
||||
{"x-ca-signature-method", "HmacSHA256"},
|
||||
{"x-ca-signature", "xfX+bZxY2yl7EB/qdoDy9v/uscw3Nnj1pgoU+Bm6xdM="},
|
||||
{"x-ca-signature-headers",
|
||||
"x-ca-timestamp,x-ca-key,x-ca-nonce,x-ca-signature-method"},
|
||||
};
|
||||
Wasm::Common::Http::QueryParams body_params = {{"username", "xiaoming"},
|
||||
{"password", "123456789"}};
|
||||
// auto actual =
|
||||
// root_context_->getStringToSign("/http2test/test?param1=test",
|
||||
// body_params);
|
||||
// EXPECT_EQ(actual, R"(POST
|
||||
// application/json; charset=utf-8
|
||||
|
||||
// application/x-www-form-urlencoded; charset=utf-8
|
||||
// Wed, 09 May 2018 13:30:29 GMT+00:00
|
||||
// x-ca-key:203753385
|
||||
// x-ca-nonce:c9f15cbf-f4ac-4a6c-b54d-f51abf4b5b44
|
||||
// x-ca-signature-method:HmacSHA256
|
||||
// x-ca-timestamp:1525872629832
|
||||
// /http2test/test?param1=test&password=123456789&username=xiaoming)");
|
||||
|
||||
headers_ = {
|
||||
{":path", "/Third/User/getNyAccessToken"},
|
||||
{":method", "POST"},
|
||||
{"accept", "application/json"},
|
||||
{"content-type", "application/x-www-form-urlencoded"},
|
||||
{"x-ca-timestamp", "1646646682418"},
|
||||
{"x-ca-nonce", "ca5a6753-b76c-4fff-a9d9-e5bb643e8cdf"},
|
||||
{"x-ca-key", "appKey"},
|
||||
{"x-ca-signature-headers", "x-ca-key,x-ca-nonce,x-ca-timestamp"},
|
||||
{"x-ca-signature", "gmf9xq0hc95Hmt+7G+OocS009ka3v1v0rvfshKzYc3w="},
|
||||
};
|
||||
HmacAuthConfigRule rule;
|
||||
rule.credentials = {{"appKey", "appSecret"}};
|
||||
body_params = {{"nickname", "nickname"},
|
||||
{"room_id", "6893"},
|
||||
{"uuid", "uuid"},
|
||||
{"photo", "photo"}};
|
||||
// EXPECT_EQ(root_context_->checkPlugin(rule, body_params), true);
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_":["test"],
|
||||
"credentials":[
|
||||
{"key": "appKey", "secret": "appSecret"}
|
||||
]
|
||||
}
|
||||
]
|
||||
})";
|
||||
route_name_ = "test";
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
std::string body("nickname=nickname&room_id=6893&uuid=uuid&photo=photo");
|
||||
body_.set(body);
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
EXPECT_EQ(context_->onRequestBody(body.size(), true),
|
||||
FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(HmacAuthTest, ParamInBodyWithConsumer) {
|
||||
headers_ = {
|
||||
{":path", "/Third/User/getNyAccessToken"},
|
||||
{":method", "POST"},
|
||||
{"accept", "application/json"},
|
||||
{"content-type", "application/x-www-form-urlencoded"},
|
||||
{"x-ca-timestamp", "1646646682418"},
|
||||
{"x-ca-nonce", "ca5a6753-b76c-4fff-a9d9-e5bb643e8cdf"},
|
||||
{"x-ca-key", "appKey"},
|
||||
{"x-ca-signature-headers", "x-ca-key,x-ca-nonce,x-ca-timestamp"},
|
||||
{"x-ca-signature", "gmf9xq0hc95Hmt+7G+OocS009ka3v1v0rvfshKzYc3w="},
|
||||
};
|
||||
HmacAuthConfigRule rule;
|
||||
rule.credentials = {{"appKey", "appSecret"}};
|
||||
Wasm::Common::Http::QueryParams body_params = {{"nickname", "nickname"},
|
||||
{"room_id", "6893"},
|
||||
{"uuid", "uuid"},
|
||||
{"photo", "photo"}};
|
||||
// EXPECT_EQ(root_context_->checkPlugin(rule, body_params), true);
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers": [{"key": "appKey", "secret": "appSecret", "name": "consumer"}],
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_":["test"],
|
||||
"allow":["consumer"]
|
||||
}
|
||||
]
|
||||
})";
|
||||
route_name_ = "test";
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
std::string body("nickname=nickname&room_id=6893&uuid=uuid&photo=photo");
|
||||
body_.set(body);
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
EXPECT_EQ(context_->onRequestBody(body.size(), true),
|
||||
FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(HmacAuthTest, ParamInBodyWrongSignature) {
|
||||
headers_ = {
|
||||
{":path", "/Third/User/getNyAccessToken"},
|
||||
{":method", "POST"},
|
||||
{"accept", "application/json"},
|
||||
{"content-type", "application/x-www-form-urlencoded"},
|
||||
{"x-ca-timestamp", "1646646682418"},
|
||||
{"x-ca-nonce", "ca5a6753-b76c-4fff-a9d9-e5bb643e8cdf"},
|
||||
{"x-ca-key", "appKey"},
|
||||
{"x-ca-signature-headers", "x-ca-key,x-ca-nonce,x-ca-timestamp"},
|
||||
{"x-ca-signature", "wrong"},
|
||||
};
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_":["test"],
|
||||
"credentials":[
|
||||
{"key": "appKey", "secret": "appSecret"}
|
||||
]
|
||||
}
|
||||
]
|
||||
})";
|
||||
route_name_ = "test";
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
std::string body("nickname=nickname&room_id=6893&uuid=uuid&photo=photo");
|
||||
body_.set(body);
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(400, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestBody(body.size(), true),
|
||||
FilterDataStatus::StopIterationNoBuffer);
|
||||
}
|
||||
|
||||
TEST_F(HmacAuthTest, InvalidSecret) {
|
||||
{
|
||||
headers_ = {
|
||||
{":path", "/Third/Tools/checkSign"},
|
||||
{":method", "GET"},
|
||||
{"accept", "application/json"},
|
||||
{"content-type", "application/json"},
|
||||
{"x-ca-timestamp", "1646365291734"},
|
||||
{"x-ca-nonce", "787dd0c2-7bd8-41cd-9c19-62c05ea524a2"},
|
||||
{"x-ca-key", "appKey"},
|
||||
{"x-ca-signature-headers", "x-ca-key,x-ca-nonce,x-ca-timestamp"},
|
||||
{"x-ca-signature", "EdJSFAMOWyXZOpXhevZnjuS0ZafnwnCqaSk5hz+tXo8="},
|
||||
};
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"credentials":[
|
||||
{"key": "appKey", "secret": ""}
|
||||
]
|
||||
})";
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
|
||||
{
|
||||
headers_ = {
|
||||
{":path", "/Third/Tools/checkSign"},
|
||||
{":method", "GET"},
|
||||
{"accept", "application/json"},
|
||||
{"content-type", "application/json"},
|
||||
{"x-ca-timestamp", "1646365291734"},
|
||||
{"x-ca-nonce", "787dd0c2-7bd8-41cd-9c19-62c05ea524a2"},
|
||||
{"x-ca-key", "appKey"},
|
||||
{"x-ca-signature-headers", "x-ca-key,x-ca-nonce,x-ca-timestamp"},
|
||||
{"x-ca-signature", "EdJSFAMOWyXZOpXhevZnjuS0ZafnwnCqaSk5hz+tXo8="},
|
||||
};
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers":[
|
||||
{"key": "appKey", "secret": "", "name": "consumer1"}
|
||||
]
|
||||
})";
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(HmacAuthTest, DuplicateKey) {
|
||||
{
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"credentials":[
|
||||
{"key": "appKey", "secret": ""},
|
||||
{"key": "appKey", "secret": "123"}
|
||||
]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
config_.set(configuration);
|
||||
EXPECT_FALSE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
|
||||
{
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers":[
|
||||
{"key": "appKey", "secret": "", "name": "consumer1"},
|
||||
{"key": "appKey", "secret": "123", "name": "consumer2"}
|
||||
]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
config_.set(configuration);
|
||||
EXPECT_FALSE(root_context_->configure(configuration.size()));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(HmacAuthTest, BodyMD5) {
|
||||
body_.set("abc");
|
||||
headers_ = {{"content-md5", "kAFQmDzST7DWlj99KOF/cg=="}};
|
||||
context_->onRequestHeaders(0, false);
|
||||
EXPECT_EQ(context_->onRequestBody(3, true), FilterDataStatus::Continue);
|
||||
|
||||
headers_ = {};
|
||||
context_->onRequestHeaders(0, false);
|
||||
EXPECT_EQ(context_->onRequestBody(0, false), FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(HmacAuthTest, DateCheck) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"credentials":[
|
||||
{"key": "203753385", "secret": "123456"}
|
||||
],
|
||||
"date_offset": 3600
|
||||
})";
|
||||
BufferBase buffer;
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
headers_ = {
|
||||
{":path",
|
||||
"/http2test/test?param1=test&username=xiaoming&password=123456789"},
|
||||
{":method", "POST"},
|
||||
{"accept", "application/json; charset=utf-8"},
|
||||
{"ca_version", "1"},
|
||||
{"content-type", "application/x-www-form-urlencoded; charset=utf-8"},
|
||||
{"x-ca-timestamp", "1525872629832"},
|
||||
{"date", "Wed, 09 May 2018 13:30:29 GMT+00:00"},
|
||||
{"user-agent", "ALIYUN-ANDROID-DEMO"},
|
||||
{"x-ca-nonce", "c9f15cbf-f4ac-4a6c-b54d-f51abf4b5b44"},
|
||||
{"content-length", "33"},
|
||||
{"username", "xiaoming&password=123456789"},
|
||||
{"x-ca-key", "203753385"},
|
||||
{"x-ca-signature-method", "HmacSHA256"},
|
||||
{"x-ca-signature", "FJbhmAFYz9zfl1FrThxzxBt79BvaHQIzy8Wpctn+xXE="},
|
||||
{"x-ca-signature-headers",
|
||||
"x-ca-timestamp,x-ca-key,x-ca-nonce,x-ca-signature-method"},
|
||||
};
|
||||
current_time_ = (uint64_t)1525876230 * 1000 * 1000 * 1000;
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
EXPECT_EQ(context_->onRequestBody(0, true),
|
||||
FilterDataStatus::StopIterationNoBuffer);
|
||||
current_time_ = (uint64_t)1525869027 * 1000 * 1000 * 1000;
|
||||
EXPECT_EQ(context_->onRequestBody(0, true),
|
||||
FilterDataStatus::StopIterationNoBuffer);
|
||||
current_time_ = (uint64_t)1525869029 * 1000 * 1000 * 1000;
|
||||
EXPECT_EQ(context_->onRequestBody(0, true), FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(HmacAuthTest, TimestampCheck) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"credentials":[
|
||||
{"key": "203753385", "secret": "123456"}
|
||||
],
|
||||
"date_offset": 3600
|
||||
})";
|
||||
BufferBase buffer;
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
headers_ = {
|
||||
{":path",
|
||||
"/http2test/test?param1=test&username=xiaoming&password=123456789"},
|
||||
{":method", "POST"},
|
||||
{"accept", "application/json; charset=utf-8"},
|
||||
{"ca_version", "1"},
|
||||
{"content-type", "application/x-www-form-urlencoded; charset=utf-8"},
|
||||
{"x-ca-timestamp", "1525872629832"},
|
||||
{"user-agent", "ALIYUN-ANDROID-DEMO"},
|
||||
{"x-ca-nonce", "c9f15cbf-f4ac-4a6c-b54d-f51abf4b5b44"},
|
||||
{"content-length", "33"},
|
||||
{"username", "xiaoming&password=123456789"},
|
||||
{"x-ca-key", "203753385"},
|
||||
{"x-ca-signature-method", "HmacSHA256"},
|
||||
{"x-ca-signature", "wcQC8014+HW0TumVfXy8+UXI4JDvkhjPlqp6rTE7cZo="},
|
||||
{"x-ca-signature-headers",
|
||||
"x-ca-timestamp,x-ca-key,x-ca-nonce,x-ca-signature-method"},
|
||||
};
|
||||
current_time_ = (uint64_t)1525876230 * 1000 * 1000 * 1000;
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
EXPECT_EQ(context_->onRequestBody(0, true),
|
||||
FilterDataStatus::StopIterationNoBuffer);
|
||||
current_time_ = (uint64_t)1525869027 * 1000 * 1000 * 1000;
|
||||
EXPECT_EQ(context_->onRequestBody(0, true),
|
||||
FilterDataStatus::StopIterationNoBuffer);
|
||||
current_time_ = (uint64_t)1525869029 * 1000 * 1000 * 1000;
|
||||
EXPECT_EQ(context_->onRequestBody(0, true), FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(HmacAuthTest, TimestampSecCheck) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"credentials":[
|
||||
{"key": "203753385", "secret": "123456"}
|
||||
],
|
||||
"date_offset": 3600
|
||||
})";
|
||||
BufferBase buffer;
|
||||
config_.set(configuration);
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
headers_ = {
|
||||
{":path",
|
||||
"/http2test/test?param1=test&username=xiaoming&password=123456789"},
|
||||
{":method", "POST"},
|
||||
{"accept", "application/json; charset=utf-8"},
|
||||
{"ca_version", "1"},
|
||||
{"content-type", "application/x-www-form-urlencoded; charset=utf-8"},
|
||||
{"x-ca-timestamp", "1525872629"},
|
||||
{"user-agent", "ALIYUN-ANDROID-DEMO"},
|
||||
{"x-ca-nonce", "c9f15cbf-f4ac-4a6c-b54d-f51abf4b5b44"},
|
||||
{"content-length", "33"},
|
||||
{"username", "xiaoming&password=123456789"},
|
||||
{"x-ca-key", "203753385"},
|
||||
{"x-ca-signature-method", "HmacSHA256"},
|
||||
{"x-ca-signature", "7yl5Rba+3pnp9weLP3af1Hejz4K3RFp+BHL7N2w98/U="},
|
||||
{"x-ca-signature-headers",
|
||||
"x-ca-timestamp,x-ca-key,x-ca-nonce,x-ca-signature-method"},
|
||||
};
|
||||
current_time_ = (uint64_t)1525876230 * 1000 * 1000 * 1000;
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
EXPECT_EQ(context_->onRequestBody(0, true),
|
||||
FilterDataStatus::StopIterationNoBuffer);
|
||||
current_time_ = (uint64_t)1525869027 * 1000 * 1000 * 1000;
|
||||
EXPECT_EQ(context_->onRequestBody(0, true),
|
||||
FilterDataStatus::StopIterationNoBuffer);
|
||||
current_time_ = (uint64_t)1525869029 * 1000 * 1000 * 1000;
|
||||
EXPECT_EQ(context_->onRequestBody(0, true), FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
} // namespace hmac_auth
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
68
plugins/wasm-cpp/extensions/jwt_auth/BUILD
Normal file
68
plugins/wasm-cpp/extensions/jwt_auth/BUILD
Normal file
@@ -0,0 +1,68 @@
|
||||
load("@proxy_wasm_cpp_sdk//bazel/wasm:wasm.bzl", "wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
wasm_cc_binary(
|
||||
name = "jwt_auth.wasm",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
"plugin.h",
|
||||
"extractor.cc",
|
||||
"extractor.h",
|
||||
"//common:base64.h",
|
||||
],
|
||||
deps = [
|
||||
"@com_github_google_jwt_verify//:jwt_verify_lib",
|
||||
"@com_google_absl//absl/container:btree",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/strings:str_format",
|
||||
"@com_google_absl//absl/time",
|
||||
"//common:json_util",
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics",
|
||||
"//common:http_util",
|
||||
"//common:rule_util",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "jwt_auth_lib",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
"extractor.cc",
|
||||
"//common:base64.h",
|
||||
],
|
||||
hdrs = [
|
||||
"plugin.h",
|
||||
"extractor.h",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
"@com_github_google_jwt_verify//:jwt_verify_lib",
|
||||
"@com_google_absl//absl/container:btree",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/strings:str_format",
|
||||
"@com_google_absl//absl/time",
|
||||
"//common:json_util",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
"//common:http_util_nullvm",
|
||||
"//common:rule_util_nullvm",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "jwt_auth_test",
|
||||
srcs = [
|
||||
"plugin_test.cc",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
":jwt_auth_lib",
|
||||
"@com_google_googletest//:gtest",
|
||||
"@com_google_googletest//:gtest_main",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
],
|
||||
)
|
||||
|
||||
declare_wasm_image_targets(
|
||||
name = "jwt_auth",
|
||||
wasm_file = ":jwt_auth.wasm",
|
||||
)
|
||||
306
plugins/wasm-cpp/extensions/jwt_auth/extractor.cc
Normal file
306
plugins/wasm-cpp/extensions/jwt_auth/extractor.cc
Normal file
@@ -0,0 +1,306 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
// modified base on envoy/source/extensions/filters/http/jwt_authn/extractor.cc
|
||||
#include "extensions/jwt_auth/extractor.h"
|
||||
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "absl/container/btree_map.h"
|
||||
#include "common/http_util.h"
|
||||
#include "extensions/jwt_auth/plugin.h"
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace jwt_auth {
|
||||
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
/**
|
||||
* Check Claims specified in Provider
|
||||
*/
|
||||
class JwtClaimChecker {
|
||||
public:
|
||||
JwtClaimChecker(const ClaimsMap& claims) : allowed_claims_(claims) {}
|
||||
|
||||
// check if a jwt issuer is allowed
|
||||
bool check(const std::string& key, const std::string& value) const {
|
||||
if (allowed_claims_.empty()) {
|
||||
return true;
|
||||
}
|
||||
auto it = allowed_claims_.find(key);
|
||||
return it != allowed_claims_.end() && it->second == value;
|
||||
}
|
||||
|
||||
private:
|
||||
// Only these specified claims are allowed.
|
||||
const ClaimsMap& allowed_claims_;
|
||||
};
|
||||
|
||||
using JwtClaimCheckerPtr = std::unique_ptr<JwtClaimChecker>;
|
||||
|
||||
// A base JwtLocation object to store token and claim_checker.
|
||||
class JwtLocationBase : public JwtLocation {
|
||||
public:
|
||||
JwtLocationBase(const std::string& token,
|
||||
const JwtClaimChecker& claim_checker)
|
||||
: token_(token), claim_checker_(claim_checker) {}
|
||||
|
||||
// Get the token string
|
||||
const std::string& token() const override { return token_; }
|
||||
|
||||
// Check if an claim has specified the location.
|
||||
bool isClaimAllowed(const std::string& key,
|
||||
const std::string& value) const override {
|
||||
return claim_checker_.check(key, value);
|
||||
}
|
||||
|
||||
void addClaimToHeader(const std::string& header, const std::string& value,
|
||||
bool override) const override {
|
||||
claims_to_headers_.emplace_back(header, value, override);
|
||||
}
|
||||
|
||||
void claimsToHeaders() const override {
|
||||
for (const auto& claim_to_header : claims_to_headers_) {
|
||||
const auto& header_key = std::get<0>(claim_to_header);
|
||||
const auto& header_value = std::get<1>(claim_to_header);
|
||||
if (std::get<2>(claim_to_header)) {
|
||||
auto header_ptr = getRequestHeader(header_key);
|
||||
if (!header_ptr->view().empty()) {
|
||||
replaceRequestHeader(header_key, header_value);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
addRequestHeader(header_key, header_value);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::vector<std::tuple<std::string, std::string, bool>>
|
||||
claims_to_headers_;
|
||||
// Extracted token.
|
||||
const std::string token_;
|
||||
// Claim checker
|
||||
const JwtClaimChecker& claim_checker_;
|
||||
};
|
||||
|
||||
// The JwtLocation for header extraction.
|
||||
class JwtHeaderLocation : public JwtLocationBase {
|
||||
public:
|
||||
JwtHeaderLocation(const std::string& token,
|
||||
const JwtClaimChecker& claim_checker,
|
||||
const std::string& header)
|
||||
: JwtLocationBase(token, claim_checker), header_(header) {}
|
||||
|
||||
void removeJwt() const override { removeRequestHeader(header_); }
|
||||
|
||||
private:
|
||||
// the header name the JWT is extracted from.
|
||||
const std::string& header_;
|
||||
};
|
||||
|
||||
// The JwtLocation for param extraction.
|
||||
class JwtParamLocation : public JwtLocationBase {
|
||||
public:
|
||||
JwtParamLocation(const std::string& token,
|
||||
const JwtClaimChecker& claim_checker, const std::string&)
|
||||
: JwtLocationBase(token, claim_checker) {}
|
||||
|
||||
void removeJwt() const override {
|
||||
// TODO(qiwzhang): remove JWT from parameter.
|
||||
}
|
||||
};
|
||||
|
||||
// The JwtLocation for cookie extraction.
|
||||
class JwtCookieLocation : public JwtLocationBase {
|
||||
public:
|
||||
JwtCookieLocation(const std::string& token,
|
||||
const JwtClaimChecker& claim_checker)
|
||||
: JwtLocationBase(token, claim_checker) {}
|
||||
|
||||
void removeJwt() const override {
|
||||
// TODO(theshubhamp): remove JWT from cookies.
|
||||
}
|
||||
};
|
||||
|
||||
class ExtractorImpl : public Extractor {
|
||||
public:
|
||||
ExtractorImpl(const Consumer& provider);
|
||||
|
||||
std::vector<JwtLocationConstPtr> extract() const override;
|
||||
|
||||
private:
|
||||
// add a header config
|
||||
void addHeaderConfig(const ClaimsMap& claims, const std::string& header_name,
|
||||
const std::string& value_prefix);
|
||||
// add a query param config
|
||||
void addQueryParamConfig(const ClaimsMap& claims, const std::string& param);
|
||||
// add a query param config
|
||||
void addCookieConfig(const ClaimsMap& claims, const std::string& cookie);
|
||||
// ctor helper for a jwt provider config
|
||||
void addProvider(const Consumer& provider);
|
||||
|
||||
// HeaderMap value type to store prefix and issuers that specified this
|
||||
// header.
|
||||
struct HeaderLocationSpec {
|
||||
HeaderLocationSpec(const std::string& header,
|
||||
const std::string& value_prefix)
|
||||
: header_(header), value_prefix_(value_prefix) {}
|
||||
// The header name.
|
||||
std::string header_;
|
||||
// The value prefix. e.g. for "Bearer <token>", the value_prefix is "Bearer
|
||||
// ".
|
||||
std::string value_prefix_;
|
||||
// Issuers that specified this header.
|
||||
JwtClaimCheckerPtr claim_checker_;
|
||||
};
|
||||
using HeaderLocationSpecPtr = std::unique_ptr<HeaderLocationSpec>;
|
||||
// The map of (header + value_prefix) to HeaderLocationSpecPtr
|
||||
std::map<std::string, HeaderLocationSpecPtr> header_locations_;
|
||||
|
||||
// ParamMap value type to store issuers that specified this header.
|
||||
struct ParamLocationSpec {
|
||||
// Issuers that specified this param.
|
||||
JwtClaimCheckerPtr claim_checker_;
|
||||
};
|
||||
// The map of a parameter key to set of issuers specified the parameter
|
||||
std::map<std::string, ParamLocationSpec> param_locations_;
|
||||
|
||||
// CookieMap value type to store issuers that specified this cookie.
|
||||
struct CookieLocationSpec {
|
||||
// Issuers that specified this param.
|
||||
JwtClaimCheckerPtr claim_checker_;
|
||||
};
|
||||
// The map of a cookie key to set of issuers specified the cookie.
|
||||
absl::btree_map<std::string, CookieLocationSpec> cookie_locations_;
|
||||
};
|
||||
|
||||
ExtractorImpl::ExtractorImpl(const Consumer& provider) {
|
||||
addProvider(provider);
|
||||
}
|
||||
|
||||
void ExtractorImpl::addProvider(const Consumer& provider) {
|
||||
for (const auto& header : provider.from_headers) {
|
||||
addHeaderConfig(provider.allowd_claims, header.header, header.value_prefix);
|
||||
}
|
||||
for (const std::string& param : provider.from_params) {
|
||||
addQueryParamConfig(provider.allowd_claims, param);
|
||||
}
|
||||
for (const std::string& cookie : provider.from_cookies) {
|
||||
addCookieConfig(provider.allowd_claims, cookie);
|
||||
}
|
||||
}
|
||||
|
||||
void ExtractorImpl::addHeaderConfig(const ClaimsMap& claims,
|
||||
const std::string& header_name,
|
||||
const std::string& value_prefix) {
|
||||
const std::string map_key = header_name + value_prefix;
|
||||
auto& header_location_spec = header_locations_[map_key];
|
||||
if (!header_location_spec) {
|
||||
header_location_spec =
|
||||
std::make_unique<HeaderLocationSpec>(header_name, value_prefix);
|
||||
}
|
||||
header_location_spec->claim_checker_ =
|
||||
std::make_unique<JwtClaimChecker>(claims);
|
||||
}
|
||||
|
||||
void ExtractorImpl::addQueryParamConfig(const ClaimsMap& claims,
|
||||
const std::string& param) {
|
||||
auto& param_location_spec = param_locations_[param];
|
||||
param_location_spec.claim_checker_ =
|
||||
std::make_unique<JwtClaimChecker>(claims);
|
||||
}
|
||||
|
||||
void ExtractorImpl::addCookieConfig(const ClaimsMap& claims,
|
||||
const std::string& cookie) {
|
||||
auto& cookie_location_spec = cookie_locations_[cookie];
|
||||
cookie_location_spec.claim_checker_ =
|
||||
std::make_unique<JwtClaimChecker>(claims);
|
||||
}
|
||||
|
||||
std::vector<JwtLocationConstPtr> ExtractorImpl::extract() const {
|
||||
std::vector<JwtLocationConstPtr> tokens;
|
||||
|
||||
// Check header locations first
|
||||
for (const auto& location_it : header_locations_) {
|
||||
const auto& location_spec = location_it.second;
|
||||
|
||||
auto header = getRequestHeader(location_spec->header_)->toString();
|
||||
if (!header.empty()) {
|
||||
const auto pos = header.find(location_spec->value_prefix_);
|
||||
if (pos == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
auto header_strip =
|
||||
header.substr(pos + location_spec->value_prefix_.length());
|
||||
tokens.push_back(std::make_unique<const JwtHeaderLocation>(
|
||||
header_strip, *location_spec->claim_checker_,
|
||||
location_spec->header_));
|
||||
}
|
||||
}
|
||||
|
||||
// Check query parameter locations only if query parameter locations specified
|
||||
// and Path() is not null
|
||||
auto path = getRequestHeader(Wasm::Common::Http::Header::Path)->toString();
|
||||
if (!param_locations_.empty() && !path.empty()) {
|
||||
const auto& params = Wasm::Common::Http::parseAndDecodeQueryString(path);
|
||||
for (const auto& location_it : param_locations_) {
|
||||
const auto& param_key = location_it.first;
|
||||
const auto& location_spec = location_it.second;
|
||||
const auto& it = params.find(param_key);
|
||||
if (it != params.end()) {
|
||||
tokens.push_back(std::make_unique<const JwtParamLocation>(
|
||||
it->second, *location_spec.claim_checker_, param_key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check cookie locations.
|
||||
if (!cookie_locations_.empty()) {
|
||||
const auto& cookies =
|
||||
Wasm::Common::Http::parseCookies([&](absl::string_view k) -> bool {
|
||||
return cookie_locations_.contains(k);
|
||||
});
|
||||
|
||||
for (const auto& location_it : cookie_locations_) {
|
||||
const auto& cookie_key = location_it.first;
|
||||
const auto& location_spec = location_it.second;
|
||||
const auto& it = cookies.find(cookie_key);
|
||||
if (it != cookies.end()) {
|
||||
tokens.push_back(std::make_unique<const JwtCookieLocation>(
|
||||
it->second, *location_spec.claim_checker_));
|
||||
}
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ExtractorConstPtr Extractor::create(const Consumer& provider) {
|
||||
return std::make_unique<ExtractorImpl>(provider);
|
||||
}
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace jwt_auth
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
125
plugins/wasm-cpp/extensions/jwt_auth/extractor.h
Normal file
125
plugins/wasm-cpp/extensions/jwt_auth/extractor.h
Normal file
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// modified base on envoy/source/extensions/filters/http/jwt_authn/extractor.h
|
||||
#pragma once
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifndef NULL_PLUGIN
|
||||
|
||||
#include "proxy_wasm_intrinsics.h"
|
||||
#else
|
||||
|
||||
#include "include/proxy-wasm/null_plugin.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace jwt_auth {
|
||||
|
||||
#endif
|
||||
|
||||
#define PURE = 0
|
||||
|
||||
/**
|
||||
* JwtLocation stores following token information:
|
||||
*
|
||||
* * extracted token string,
|
||||
* * the location where the JWT is extracted from,
|
||||
* * list of issuers specified the location.
|
||||
*
|
||||
*/
|
||||
class JwtLocation {
|
||||
public:
|
||||
virtual ~JwtLocation() = default;
|
||||
|
||||
// Get the token string
|
||||
virtual const std::string& token() const PURE;
|
||||
|
||||
// Check if claim has specified the location.
|
||||
virtual bool isClaimAllowed(const std::string& key,
|
||||
const std::string& value) const PURE;
|
||||
|
||||
// Remove the token from the headers
|
||||
virtual void removeJwt() const PURE;
|
||||
|
||||
// Store the claim to header
|
||||
virtual void addClaimToHeader(const std::string& header,
|
||||
const std::string& value,
|
||||
bool override) const PURE;
|
||||
|
||||
// Set claim to request header
|
||||
virtual void claimsToHeaders() const PURE;
|
||||
};
|
||||
|
||||
using JwtLocationConstPtr = std::unique_ptr<const JwtLocation>;
|
||||
|
||||
class Extractor;
|
||||
using ExtractorConstPtr = std::unique_ptr<const Extractor>;
|
||||
|
||||
struct Consumer;
|
||||
/**
|
||||
* Extracts JWT from locations specified in the config.
|
||||
*
|
||||
* Usage example:
|
||||
*
|
||||
* auto extractor = Extractor::create(config);
|
||||
* auto tokens = extractor->extract(headers);
|
||||
* for (token : tokens) {
|
||||
* Jwt jwt;
|
||||
* if (jwt.parseFromString(token->token()) != Status::Ok) {
|
||||
* // Handle JWT parsing failure.
|
||||
* }
|
||||
*
|
||||
* if (need_to_remove) {
|
||||
* // remove the JWT
|
||||
* token->removeJwt(headers);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
class Extractor {
|
||||
public:
|
||||
virtual ~Extractor() = default;
|
||||
|
||||
/**
|
||||
* Extract all JWT tokens from the headers. If set of header_keys or
|
||||
* param_keys is not empty only those in the matching locations will be
|
||||
* returned.
|
||||
*
|
||||
* @param headers is the HTTP request headers.
|
||||
* @return list of extracted Jwt location info.
|
||||
*/
|
||||
virtual std::vector<JwtLocationConstPtr> extract() const PURE;
|
||||
|
||||
/**
|
||||
* Create an instance of Extractor for a given config.
|
||||
* @param from_headers header location config.
|
||||
* @param from_params query param location config.
|
||||
* @return the extractor object.
|
||||
*/
|
||||
static ExtractorConstPtr create(const Consumer& provider);
|
||||
};
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace jwt_auth
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
397
plugins/wasm-cpp/extensions/jwt_auth/plugin.cc
Normal file
397
plugins/wasm-cpp/extensions/jwt_auth/plugin.cc
Normal file
@@ -0,0 +1,397 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/jwt_auth/plugin.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "common/common_util.h"
|
||||
#include "common/http_util.h"
|
||||
#include "common/json_util.h"
|
||||
|
||||
using ::nlohmann::json;
|
||||
using ::Wasm::Common::JsonArrayIterate;
|
||||
using ::Wasm::Common::JsonGetField;
|
||||
using ::Wasm::Common::JsonObjectIterate;
|
||||
using ::Wasm::Common::JsonValueAs;
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace jwt_auth {
|
||||
|
||||
PROXY_WASM_NULL_PLUGIN_REGISTRY
|
||||
|
||||
#endif
|
||||
namespace {
|
||||
constexpr absl::string_view InvalidTokenErrorString =
|
||||
", error=\"invalid_token\"";
|
||||
constexpr uint32_t MaximumUriLength = 256;
|
||||
constexpr std::string_view kRcDetailJwtAuthnPrefix = "jwt_authn_access_denied";
|
||||
std::string generateRcDetails(std::string_view error_msg) {
|
||||
// Replace space with underscore since RCDetails may be written to access log.
|
||||
// Some log processors assume each log segment is separated by whitespace.
|
||||
return absl::StrCat(kRcDetailJwtAuthnPrefix, "{",
|
||||
absl::StrJoin(absl::StrSplit(error_msg, ' '), "_"), "}");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
static RegisterContextFactory register_JwtAuth(CONTEXT_FACTORY(PluginContext),
|
||||
ROOT_FACTORY(PluginRootContext));
|
||||
|
||||
#define JSON_FIND_FIELD(dict, field) \
|
||||
auto dict##_##field##_json = dict.find(#field); \
|
||||
if (dict##_##field##_json == dict.end()) { \
|
||||
LOG_WARN("can't find '" #field "' in " #dict); \
|
||||
return false; \
|
||||
}
|
||||
|
||||
#define JSON_VALUE_AS(type, src, dst, err_msg) \
|
||||
auto dst##_v = JsonValueAs<type>(src); \
|
||||
if (dst##_v.second != Wasm::Common::JsonParserResultDetail::OK || \
|
||||
!dst##_v.first) { \
|
||||
LOG_WARN(#err_msg); \
|
||||
return false; \
|
||||
} \
|
||||
auto& dst = dst##_v.first.value();
|
||||
|
||||
#define JSON_FIELD_VALUE_AS(type, dict, field) \
|
||||
JSON_VALUE_AS(type, dict##_##field##_json.value(), dict##_##field, \
|
||||
"'" #field "' field in " #dict "convert to " #type " failed")
|
||||
|
||||
bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
JwtAuthConfigRule& rule) {
|
||||
std::unordered_set<std::string> name_set;
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "consumers", [&](const json& consumer) -> bool {
|
||||
Consumer c;
|
||||
JSON_FIND_FIELD(consumer, name);
|
||||
JSON_FIELD_VALUE_AS(std::string, consumer, name);
|
||||
if (name_set.count(consumer_name) != 0) {
|
||||
LOG_WARN("consumer already exists: " + consumer_name);
|
||||
return false;
|
||||
}
|
||||
c.name = consumer_name;
|
||||
JSON_FIND_FIELD(consumer, jwks);
|
||||
JSON_FIELD_VALUE_AS(std::string, consumer, jwks);
|
||||
c.jwks = google::jwt_verify::Jwks::createFrom(
|
||||
consumer_jwks, google::jwt_verify::Jwks::JWKS);
|
||||
if (c.jwks->getStatus() != Status::Ok) {
|
||||
LOG_WARN(absl::StrFormat(
|
||||
"jwks is invalid, consumer:%s, status:%s, jwks:%s",
|
||||
consumer_name,
|
||||
google::jwt_verify::getStatusString(c.jwks->getStatus()),
|
||||
consumer_jwks));
|
||||
return false;
|
||||
}
|
||||
std::unordered_map<std::string, std::string> claims;
|
||||
auto consumer_claims_json = consumer.find("claims");
|
||||
if (consumer_claims_json != consumer.end()) {
|
||||
JSON_FIELD_VALUE_AS(Wasm::Common::JsonObject, consumer, claims);
|
||||
if (!JsonObjectIterate(
|
||||
consumer_claims, [&](std::string key) -> bool {
|
||||
auto claims_claim_json = consumer_claims.find(key);
|
||||
JSON_FIELD_VALUE_AS(std::string, claims, claim);
|
||||
claims.emplace(std::make_pair(
|
||||
key, Wasm::Common::trim(claims_claim)));
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse 'claims' in consumer: " +
|
||||
consumer_name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
auto consumer_issuer_json = consumer.find("issuer");
|
||||
if (consumer_issuer_json != consumer.end()) {
|
||||
JSON_FIELD_VALUE_AS(std::string, consumer, issuer);
|
||||
claims.emplace(
|
||||
std::make_pair("iss", Wasm::Common::trim(consumer_issuer)));
|
||||
}
|
||||
c.allowd_claims = std::move(claims);
|
||||
std::vector<FromHeader> from_headers;
|
||||
if (!JsonArrayIterate(
|
||||
consumer, "from_headers",
|
||||
[&](const json& from_header) -> bool {
|
||||
JSON_FIND_FIELD(from_header, name);
|
||||
JSON_FIELD_VALUE_AS(std::string, from_header, name);
|
||||
JSON_FIND_FIELD(from_header, value_prefix);
|
||||
JSON_FIELD_VALUE_AS(std::string, from_header,
|
||||
value_prefix);
|
||||
from_headers.push_back(FromHeader{
|
||||
from_header_name, from_header_value_prefix});
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse 'from_headers' in consumer: " +
|
||||
consumer_name);
|
||||
return false;
|
||||
}
|
||||
std::vector<std::string> from_params;
|
||||
if (!JsonArrayIterate(consumer, "from_params",
|
||||
[&](const json& from_param_json) -> bool {
|
||||
JSON_VALUE_AS(std::string, from_param_json,
|
||||
from_param, "invalid item");
|
||||
from_params.push_back(from_param);
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse 'from_params' in consumer: " +
|
||||
consumer_name);
|
||||
return false;
|
||||
}
|
||||
std::vector<std::string> from_cookies;
|
||||
if (!JsonArrayIterate(consumer, "from_cookies",
|
||||
[&](const json& from_cookie_json) -> bool {
|
||||
JSON_VALUE_AS(std::string, from_cookie_json,
|
||||
from_cookie, "invalid item");
|
||||
from_cookies.push_back(from_cookie);
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse 'from_cookies' in consumer: " +
|
||||
consumer_name);
|
||||
return false;
|
||||
}
|
||||
if (!from_headers.empty() || !from_params.empty() ||
|
||||
!from_cookies.empty()) {
|
||||
c.from_headers = std::move(from_headers);
|
||||
c.from_params = std::move(from_params);
|
||||
c.from_cookies = std::move(from_cookies);
|
||||
}
|
||||
std::unordered_map<std::string, ClaimToHeader> claims_to_headers;
|
||||
if (!JsonArrayIterate(
|
||||
consumer, "claims_to_headers",
|
||||
[&](const json& item_json) -> bool {
|
||||
JSON_VALUE_AS(Wasm::Common::JsonObject, item_json, item,
|
||||
"invalid item");
|
||||
JSON_FIND_FIELD(item, claim);
|
||||
JSON_FIELD_VALUE_AS(std::string, item, claim);
|
||||
auto c2h_it = claims_to_headers.find(item_claim);
|
||||
if (c2h_it != claims_to_headers.end()) {
|
||||
LOG_WARN("claim to header already exists: " +
|
||||
item_claim);
|
||||
return false;
|
||||
}
|
||||
auto& c2h = claims_to_headers[item_claim];
|
||||
JSON_FIND_FIELD(item, header);
|
||||
JSON_FIELD_VALUE_AS(std::string, item, header);
|
||||
c2h.header = std::move(item_header);
|
||||
auto item_override_json = item.find("override");
|
||||
if (item_override_json != item.end()) {
|
||||
JSON_FIELD_VALUE_AS(bool, item, override);
|
||||
c2h.override = item_override;
|
||||
}
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse 'claims_to_headers' in consumer: " +
|
||||
consumer_name);
|
||||
return false;
|
||||
}
|
||||
c.claims_to_headers = std::move(claims_to_headers);
|
||||
auto consumer_clock_skew_seconds_json =
|
||||
consumer.find("clock_skew_seconds");
|
||||
if (consumer_clock_skew_seconds_json != consumer.end()) {
|
||||
JSON_FIELD_VALUE_AS(uint64_t, consumer, clock_skew_seconds);
|
||||
c.clock_skew = consumer_clock_skew_seconds;
|
||||
}
|
||||
auto consumer_keep_token_json = consumer.find("keep_token");
|
||||
if (consumer_keep_token_json != consumer.end()) {
|
||||
JSON_FIELD_VALUE_AS(bool, consumer, keep_token);
|
||||
c.keep_token = consumer_keep_token;
|
||||
}
|
||||
c.extractor = Extractor::create(c);
|
||||
rule.consumers.push_back(std::move(c));
|
||||
name_set.insert(consumer_name);
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for consumers.");
|
||||
return false;
|
||||
}
|
||||
if (rule.consumers.empty()) {
|
||||
LOG_INFO("at least one consumer has to be configured for a rule.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Status PluginRootContext::consumerVerify(
|
||||
const Consumer& consumer, uint64_t now,
|
||||
std::vector<JwtLocationConstPtr>& jwt_tokens) {
|
||||
auto tokens = consumer.extractor->extract();
|
||||
if (tokens.empty()) {
|
||||
return Status::JwtMissed;
|
||||
}
|
||||
for (auto& token : tokens) {
|
||||
google::jwt_verify::Jwt jwt;
|
||||
Status status = jwt.parseFromString(token->token());
|
||||
if (status != Status::Ok) {
|
||||
LOG_INFO(absl::StrFormat(
|
||||
"jwt parse failed, consumer:%s, token:%s, status:%s", consumer.name,
|
||||
token->token(), google::jwt_verify::getStatusString(status)));
|
||||
return status;
|
||||
}
|
||||
StructUtils payload_getter(jwt.payload_pb_);
|
||||
if (!consumer.allowd_claims.empty()) {
|
||||
for (const auto& claim : consumer.allowd_claims) {
|
||||
std::string value;
|
||||
if (payload_getter.GetString(claim.first, &value) ==
|
||||
StructUtils::WRONG_TYPE) {
|
||||
LOG_INFO(absl::StrFormat(
|
||||
"jwt payload invalid, consumer:%s, token:%s, claim:%s",
|
||||
consumer.name, jwt.payload_str_, claim.first));
|
||||
return Status::JwtVerificationFail;
|
||||
}
|
||||
if (value != claim.second) {
|
||||
LOG_INFO(absl::StrFormat(
|
||||
"jwt payload invalid, consumer:%s, claim:%s, value:%s, expect:%s",
|
||||
consumer.name, claim.first, value, claim.second));
|
||||
return Status::JwtVerificationFail;
|
||||
}
|
||||
}
|
||||
}
|
||||
status = jwt.verifyTimeConstraint(now, consumer.clock_skew);
|
||||
if (status != Status::Ok) {
|
||||
LOG_DEBUG(absl::StrFormat(
|
||||
"jwt verify time failed, consumer:%s, token:%s, status:%s",
|
||||
consumer.name, token->token(),
|
||||
google::jwt_verify::getStatusString(status)));
|
||||
return status;
|
||||
}
|
||||
status =
|
||||
google::jwt_verify::verifyJwtWithoutTimeChecking(jwt, *consumer.jwks);
|
||||
if (status != Status::Ok) {
|
||||
LOG_DEBUG(absl::StrFormat(
|
||||
"jwt verify failed, consumer:%s, token:%s, status:%s", consumer.name,
|
||||
token->token(), google::jwt_verify::getStatusString(status)));
|
||||
return status;
|
||||
}
|
||||
for (const auto& claim_to_header : consumer.claims_to_headers) {
|
||||
std::string value;
|
||||
if (payload_getter.GetString(claim_to_header.first, &value) !=
|
||||
StructUtils::WRONG_TYPE) {
|
||||
token->addClaimToHeader(claim_to_header.second.header, value,
|
||||
claim_to_header.second.override);
|
||||
} else {
|
||||
uint64_t num_value;
|
||||
if (payload_getter.GetUInt64(claim_to_header.first, &num_value) !=
|
||||
StructUtils::WRONG_TYPE) {
|
||||
token->addClaimToHeader(claim_to_header.second.header,
|
||||
std::to_string((unsigned long long)num_value),
|
||||
claim_to_header.second.override);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
jwt_tokens = std::move(tokens);
|
||||
return Status::Ok;
|
||||
}
|
||||
|
||||
bool PluginRootContext::checkPlugin(
|
||||
const JwtAuthConfigRule& rule,
|
||||
const std::optional<std::unordered_set<std::string>>& allow_set) {
|
||||
std::optional<Status> err_status;
|
||||
bool verified = false;
|
||||
uint64_t now = getCurrentTimeNanoseconds() / 1e9;
|
||||
for (const auto& consumer : rule.consumers) {
|
||||
std::vector<JwtLocationConstPtr> tokens;
|
||||
auto status = consumerVerify(consumer, now, tokens);
|
||||
if (status == Status::Ok) {
|
||||
verified = true;
|
||||
// global config without allow_set field allows any consumers
|
||||
if (!allow_set ||
|
||||
allow_set.value().find(consumer.name) != allow_set.value().end()) {
|
||||
addRequestHeader("X-Mse-Consumer", consumer.name);
|
||||
for (auto& token : tokens) {
|
||||
if (!consumer.keep_token) {
|
||||
token->removeJwt();
|
||||
}
|
||||
token->claimsToHeaders();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// use the first status
|
||||
if (!err_status) {
|
||||
err_status = status;
|
||||
}
|
||||
}
|
||||
if (!verified) {
|
||||
auto status = err_status ? err_status.value() : Status::JwtMissed;
|
||||
auto err_str = google::jwt_verify::getStatusString(status);
|
||||
auto authn_value = absl::StrCat(
|
||||
"Bearer realm=\"",
|
||||
Wasm::Common::Http::buildOriginalUri(MaximumUriLength), "\"");
|
||||
if (status != Status::JwtMissed) {
|
||||
absl::StrAppend(&authn_value, InvalidTokenErrorString);
|
||||
}
|
||||
sendLocalResponse(401, generateRcDetails(err_str), err_str,
|
||||
{{"WWW-Authenticate", authn_value}});
|
||||
} else {
|
||||
sendLocalResponse(403, kRcDetailJwtAuthnPrefix, "Access Denied", {});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PluginRootContext::onConfigure(size_t size) {
|
||||
// Parse configuration JSON string.
|
||||
if (size > 0 && !configure(size)) {
|
||||
LOG_WARN("configuration has errors initialization will not continue.");
|
||||
setInvalidConfig();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::configure(size_t configuration_size) {
|
||||
auto configuration_data = getBufferBytes(WasmBufferType::PluginConfiguration,
|
||||
0, configuration_size);
|
||||
// Parse configuration JSON string.
|
||||
auto result = ::Wasm::Common::JsonParse(configuration_data->view());
|
||||
if (!result) {
|
||||
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
configuration_data->view()));
|
||||
return false;
|
||||
}
|
||||
if (!parseAuthRuleConfig(result.value())) {
|
||||
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
configuration_data->view()));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) {
|
||||
auto* rootCtx = rootContext();
|
||||
return rootCtx->checkAuthRule(
|
||||
[rootCtx](const auto& config, const auto& allow_set) {
|
||||
return rootCtx->checkPlugin(config, allow_set);
|
||||
})
|
||||
? FilterHeadersStatus::Continue
|
||||
: FilterHeadersStatus::StopIteration;
|
||||
}
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace jwt_auth
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
117
plugins/wasm-cpp/extensions/jwt_auth/plugin.h
Normal file
117
plugins/wasm-cpp/extensions/jwt_auth/plugin.h
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "common/route_rule_matcher.h"
|
||||
#include "extensions/jwt_auth/extractor.h"
|
||||
#include "jwt_verify_lib/check_audience.h"
|
||||
#include "jwt_verify_lib/jwt.h"
|
||||
#include "jwt_verify_lib/status.h"
|
||||
#include "jwt_verify_lib/struct_utils.h"
|
||||
#include "jwt_verify_lib/verify.h"
|
||||
#define ASSERT(_X) assert(_X)
|
||||
|
||||
#ifndef NULL_PLUGIN
|
||||
|
||||
#include "proxy_wasm_intrinsics.h"
|
||||
|
||||
#else
|
||||
|
||||
#include "include/proxy-wasm/null_plugin.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace jwt_auth {
|
||||
|
||||
#endif
|
||||
|
||||
using ::google::jwt_verify::Status;
|
||||
using ::google::jwt_verify::StructUtils;
|
||||
struct FromHeader {
|
||||
std::string header;
|
||||
std::string value_prefix;
|
||||
};
|
||||
|
||||
struct ClaimToHeader {
|
||||
std::string header;
|
||||
bool override = true;
|
||||
};
|
||||
|
||||
using ClaimsMap =
|
||||
std::unordered_map<std::string /*claim*/, std::string /*claim value*/>;
|
||||
|
||||
struct Consumer {
|
||||
std::string name;
|
||||
google::jwt_verify::JwksPtr jwks;
|
||||
ClaimsMap allowd_claims;
|
||||
std::vector<FromHeader> from_headers = {{"Authorization", "Bearer "}};
|
||||
std::vector<std::string> from_params = {"access_token"};
|
||||
std::vector<std::string> from_cookies;
|
||||
uint64_t clock_skew = 60;
|
||||
bool keep_token = true;
|
||||
std::unordered_map<std::string /*claim*/, ClaimToHeader> claims_to_headers;
|
||||
ExtractorConstPtr extractor;
|
||||
};
|
||||
|
||||
struct JwtAuthConfigRule {
|
||||
std::vector<Consumer> consumers;
|
||||
};
|
||||
|
||||
// PluginRootContext is the root context for all streams processed by the
|
||||
// thread. It has the same lifetime as the worker thread and acts as target for
|
||||
// interactions that outlives individual stream, e.g. timer, async calls.
|
||||
class PluginRootContext : public RootContext,
|
||||
public RouteRuleMatcher<JwtAuthConfigRule> {
|
||||
public:
|
||||
PluginRootContext(uint32_t id, std::string_view root_id)
|
||||
: RootContext(id, root_id) {}
|
||||
~PluginRootContext() {}
|
||||
bool onConfigure(size_t) override;
|
||||
bool checkPlugin(const JwtAuthConfigRule&,
|
||||
const std::optional<std::unordered_set<std::string>>&);
|
||||
bool configure(size_t);
|
||||
|
||||
private:
|
||||
bool parsePluginConfig(const json&, JwtAuthConfigRule&) override;
|
||||
Status consumerVerify(const Consumer&, uint64_t,
|
||||
std::vector<JwtLocationConstPtr>&);
|
||||
std::string extractCredential(const JwtAuthConfigRule&);
|
||||
};
|
||||
|
||||
// Per-stream context.
|
||||
class PluginContext : public Context {
|
||||
public:
|
||||
explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {}
|
||||
FilterHeadersStatus onRequestHeaders(uint32_t, bool) override;
|
||||
|
||||
private:
|
||||
inline PluginRootContext* rootContext() {
|
||||
return dynamic_cast<PluginRootContext*>(this->root());
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace jwt_auth
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
269
plugins/wasm-cpp/extensions/jwt_auth/plugin_test.cc
Normal file
269
plugins/wasm-cpp/extensions/jwt_auth/plugin_test.cc
Normal file
@@ -0,0 +1,269 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/jwt_auth/plugin.h"
|
||||
|
||||
#include "common/base64.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "include/proxy-wasm/context.h"
|
||||
#include "include/proxy-wasm/null.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace jwt_auth {
|
||||
|
||||
NullPluginRegistry* context_registry_;
|
||||
RegisterNullVmPluginFactory register_jwt_auth_plugin("jwt_auth", []() {
|
||||
return std::make_unique<NullPlugin>(jwt_auth::context_registry_);
|
||||
});
|
||||
|
||||
class MockContext : public proxy_wasm::ContextBase {
|
||||
public:
|
||||
MockContext(WasmBase* wasm) : ContextBase(wasm) {}
|
||||
|
||||
MOCK_METHOD(BufferInterface*, getBuffer, (WasmBufferType));
|
||||
MOCK_METHOD(WasmResult, log, (uint32_t, std::string_view));
|
||||
MOCK_METHOD(WasmResult, getHeaderMapValue,
|
||||
(WasmHeaderMapType /* type */, std::string_view /* jwt */,
|
||||
std::string_view* /*result */));
|
||||
MOCK_METHOD(WasmResult, addHeaderMapValue,
|
||||
(WasmHeaderMapType /* type */, std::string_view /* jwt */,
|
||||
std::string_view /* value */));
|
||||
MOCK_METHOD(WasmResult, sendLocalResponse,
|
||||
(uint32_t /* response_code */, std::string_view /* body */,
|
||||
Pairs /* additional_headers */, uint32_t /* grpc_status */,
|
||||
std::string_view /* details */));
|
||||
MOCK_METHOD(uint64_t, getCurrentTimeNanoseconds, ());
|
||||
MOCK_METHOD(WasmResult, getProperty, (std::string_view, std::string*));
|
||||
};
|
||||
|
||||
class JwtAuthTest : public ::testing::Test {
|
||||
protected:
|
||||
JwtAuthTest() {
|
||||
// Initialize test VM
|
||||
test_vm_ = createNullVm();
|
||||
wasm_base_ = std::make_unique<WasmBase>(
|
||||
std::move(test_vm_), "test-vm", "", "",
|
||||
std::unordered_map<std::string, std::string>{},
|
||||
AllowedCapabilitiesMap{});
|
||||
wasm_base_->load("jwt_auth");
|
||||
wasm_base_->initialize();
|
||||
|
||||
// Initialize host side context
|
||||
mock_context_ = std::make_unique<MockContext>(wasm_base_.get());
|
||||
current_context_ = mock_context_.get();
|
||||
|
||||
ON_CALL(*mock_context_, log(testing::_, testing::_))
|
||||
.WillByDefault([](uint32_t, std::string_view m) {
|
||||
std::cerr << m << "\n";
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getHeaderMapValue(WasmHeaderMapType::RequestHeaders,
|
||||
testing::_, testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view header,
|
||||
std::string_view* result) {
|
||||
if (header == ":authority") {
|
||||
*result = authority_;
|
||||
}
|
||||
if (header == ":path") {
|
||||
*result = path_;
|
||||
}
|
||||
if (header == "Authorization") {
|
||||
*result = jwt_header_;
|
||||
}
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
ON_CALL(*mock_context_, addHeaderMapValue(WasmHeaderMapType::RequestHeaders,
|
||||
testing::_, testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view jwt,
|
||||
std::string_view value) { return WasmResult::Ok; });
|
||||
|
||||
ON_CALL(*mock_context_, getCurrentTimeNanoseconds()).WillByDefault([&]() {
|
||||
return current_time_;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getProperty(testing::_, testing::_))
|
||||
.WillByDefault([&](std::string_view path, std::string* result) {
|
||||
*result = route_name_;
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
// Initialize Wasm sandbox context
|
||||
root_context_ = std::make_unique<PluginRootContext>(0, "");
|
||||
context_ = std::make_unique<PluginContext>(1, root_context_.get());
|
||||
}
|
||||
~JwtAuthTest() override {}
|
||||
|
||||
std::unique_ptr<WasmBase> wasm_base_;
|
||||
std::unique_ptr<WasmVm> test_vm_;
|
||||
std::unique_ptr<MockContext> mock_context_;
|
||||
|
||||
std::unique_ptr<PluginRootContext> root_context_;
|
||||
std::unique_ptr<PluginContext> context_;
|
||||
|
||||
std::string path_;
|
||||
std::string authority_;
|
||||
std::string route_name_;
|
||||
std::string jwt_header_;
|
||||
uint64_t current_time_;
|
||||
};
|
||||
|
||||
TEST_F(JwtAuthTest, RSA) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers": [
|
||||
{
|
||||
"name": "consumer-1",
|
||||
"issuer": "abc",
|
||||
"jwks": "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"123\",\"alg\":\"RS256\",\"n\":\"i0B67f1jggT9QJlZ_8QL9QQ56LfurrqDhpuu8BxtVcfxrYmaXaCtqTn7OfCuca7cGHdrJIjq99rz890NmYFZuvhaZ-LMt2iyiSb9LZJAeJmHf7ecguXS_-4x3hvbsrgUDi9tlg7xxbqGYcrco3anmalAFxsbswtu2PAXLtTnUo6aYwZsWA6ksq4FL3-anPNL5oZUgIp3HGyhhLTLdlQcC83jzxbguOim-0OEz-N4fniTYRivK7MlibHKrJfO3xa_6whBS07HW4Ydc37ZN3Rx9Ov3ZyV0idFblU519nUdqp_inXj1eEpynlxH60Ys_aTU2POGZh_25KXGdF_ZC_MSRw\"}]}"
|
||||
}
|
||||
]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
current_time_ = 1665673819 * 1e9;
|
||||
jwt_header_ =
|
||||
R"(Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJhYmMiLCJzdWIiOiJ0ZXN0IiwiaWF0IjoxNjY1NjYwNTI3LCJleHAiOjE2NjU2NzM4MTl9.FwSnlW9NjZ_5w6cm-YqteUy4LjKCXfQCWVCGcM3RsaqBhcHTz_IFOFMLnjI9QAG_IhxPP4s0ln7-duESns4YogkmqWV0ckMKZo9OEYOLpD6kXaA6H6g9RaLedogReKk1bDauFWFBrqMwvnxIqOIPj2ZOEQcKDVxO08mPSXb5-cxbvCA2rcmBk8_JHD8DBW990IfUCrsUFP4w4Zy3HlU__ZZhaCqzukI1ZOOgwu2_wMifvdv2n2PvqRNcmpjuGJ-FUXhAduCTPO9ZLGBOZcxkPl4U28Frfb1hSEV83NfK3iPBoLjC3u-M7kc1FJHcUORy_Bof6mzBX7npYckbsb-SJA)";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(JwtAuthTest, OCT) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers": [
|
||||
{
|
||||
"name": "consumer-2",
|
||||
"issuer": "abcd",
|
||||
"jwks": "{\"keys\":[{\"kty\":\"oct\",\"kid\":\"123\",\"k\":\"hM0k3AbXBPpKOGg__Ql2Obcq7s60myWDpbHXzgKUQdYo7YCRp0gUqkCnbGSvZ2rGEl4YFkKqIqW7mTHdj-bcqXpNr-NOznEyMpVPOIlqG_NWVC3dydBgcsIZIdD-MR2AQceEaxriPA_VmiUCwfwL2Bhs6_i7eolXoY11EapLQtutz0BV6ZxQQ4dYUmct--7PLNb4BWJyQeWu0QfbIthnvhYllyl2dgeLTEJT58wzFz5HeNMNz8ohY5K0XaKAe5cepryqoXLhA-V-O1OjSG8lCNdKS09OY6O0fkyweKEtuDfien5tHHSsHXoAxYEHPFcSRL4bFPLZ0orTt1_4zpyfew\",\"alg\":\"HS256\"}]}"
|
||||
}
|
||||
]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
current_time_ = 1665673819 * 1e9;
|
||||
jwt_header_ =
|
||||
R"(Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJhYmNkIiwic3ViIjoidGVzdCIsImlhdCI6MTY2NTY2MDUyNywiZXhwIjoxNjY1NjczODE5fQ.7BVJOAobz_xYjsenu_CsYhYbgF1gMcqZSpaeQ8HwKmc)";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(JwtAuthTest, AuthZ) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers": [
|
||||
{
|
||||
"name": "consumer-1",
|
||||
"issuer": "abc",
|
||||
"jwks": "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"123\",\"alg\":\"RS256\",\"n\":\"i0B67f1jggT9QJlZ_8QL9QQ56LfurrqDhpuu8BxtVcfxrYmaXaCtqTn7OfCuca7cGHdrJIjq99rz890NmYFZuvhaZ-LMt2iyiSb9LZJAeJmHf7ecguXS_-4x3hvbsrgUDi9tlg7xxbqGYcrco3anmalAFxsbswtu2PAXLtTnUo6aYwZsWA6ksq4FL3-anPNL5oZUgIp3HGyhhLTLdlQcC83jzxbguOim-0OEz-N4fniTYRivK7MlibHKrJfO3xa_6whBS07HW4Ydc37ZN3Rx9Ov3ZyV0idFblU519nUdqp_inXj1eEpynlxH60Ys_aTU2POGZh_25KXGdF_ZC_MSRw\"}]}"
|
||||
},
|
||||
{
|
||||
"name": "consumer-2",
|
||||
"issuer": "abcd",
|
||||
"jwks": "{\"keys\":[{\"kty\":\"oct\",\"kid\":\"123\",\"k\":\"hM0k3AbXBPpKOGg__Ql2Obcq7s60myWDpbHXzgKUQdYo7YCRp0gUqkCnbGSvZ2rGEl4YFkKqIqW7mTHdj-bcqXpNr-NOznEyMpVPOIlqG_NWVC3dydBgcsIZIdD-MR2AQceEaxriPA_VmiUCwfwL2Bhs6_i7eolXoY11EapLQtutz0BV6ZxQQ4dYUmct--7PLNb4BWJyQeWu0QfbIthnvhYllyl2dgeLTEJT58wzFz5HeNMNz8ohY5K0XaKAe5cepryqoXLhA-V-O1OjSG8lCNdKS09OY6O0fkyweKEtuDfien5tHHSsHXoAxYEHPFcSRL4bFPLZ0orTt1_4zpyfew\",\"alg\":\"HS256\"}]}"
|
||||
}
|
||||
],
|
||||
"_rules_": [{
|
||||
"_match_route_": [
|
||||
"test1"
|
||||
],
|
||||
"allow": [
|
||||
"consumer-1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"_match_route_": [
|
||||
"test2"
|
||||
],
|
||||
"allow": [
|
||||
"consumer-2"
|
||||
]
|
||||
}
|
||||
]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
current_time_ = 1665673819 * 1e9;
|
||||
jwt_header_ =
|
||||
R"(Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJhYmNkIiwic3ViIjoidGVzdCIsImlhdCI6MTY2NTY2MDUyNywiZXhwIjoxNjY1NjczODE5fQ.7BVJOAobz_xYjsenu_CsYhYbgF1gMcqZSpaeQ8HwKmc)";
|
||||
route_name_ = "test1";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(403, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
route_name_ = "test2";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(JwtAuthTest, ClaimToHeader) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers": [
|
||||
{
|
||||
"name": "consumer-2",
|
||||
"issuer": "abcd",
|
||||
"claims_to_headers": [
|
||||
{
|
||||
"claim": "sub",
|
||||
"header": "x-sub"
|
||||
},
|
||||
{
|
||||
"claim": "exp",
|
||||
"header": "x-exp"
|
||||
}
|
||||
],
|
||||
"jwks": "{\"keys\":[{\"kty\":\"oct\",\"kid\":\"123\",\"k\":\"hM0k3AbXBPpKOGg__Ql2Obcq7s60myWDpbHXzgKUQdYo7YCRp0gUqkCnbGSvZ2rGEl4YFkKqIqW7mTHdj-bcqXpNr-NOznEyMpVPOIlqG_NWVC3dydBgcsIZIdD-MR2AQceEaxriPA_VmiUCwfwL2Bhs6_i7eolXoY11EapLQtutz0BV6ZxQQ4dYUmct--7PLNb4BWJyQeWu0QfbIthnvhYllyl2dgeLTEJT58wzFz5HeNMNz8ohY5K0XaKAe5cepryqoXLhA-V-O1OjSG8lCNdKS09OY6O0fkyweKEtuDfien5tHHSsHXoAxYEHPFcSRL4bFPLZ0orTt1_4zpyfew\",\"alg\":\"HS256\"}]}"
|
||||
}
|
||||
]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
current_time_ = 1665673819 * 1e9;
|
||||
jwt_header_ =
|
||||
R"(Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJhYmNkIiwic3ViIjoidGVzdCIsImlhdCI6MTY2NTY2MDUyNywiZXhwIjoxNjY1NjczODE5fQ.7BVJOAobz_xYjsenu_CsYhYbgF1gMcqZSpaeQ8HwKmc)";
|
||||
EXPECT_CALL(*mock_context_,
|
||||
addHeaderMapValue(testing::_, std::string_view("x-sub"),
|
||||
std::string_view("test")));
|
||||
EXPECT_CALL(*mock_context_,
|
||||
addHeaderMapValue(testing::_, std::string_view("x-exp"),
|
||||
std::string_view("1665673819")));
|
||||
EXPECT_CALL(*mock_context_,
|
||||
addHeaderMapValue(testing::_, std::string_view("X-Mse-Consumer"),
|
||||
std::string_view("consumer-2")));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
} // namespace jwt_auth
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
58
plugins/wasm-cpp/extensions/key_auth/BUILD
Normal file
58
plugins/wasm-cpp/extensions/key_auth/BUILD
Normal file
@@ -0,0 +1,58 @@
|
||||
load("@proxy_wasm_cpp_sdk//bazel/wasm:wasm.bzl", "wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
wasm_cc_binary(
|
||||
name = "key_auth.wasm",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
"plugin.h",
|
||||
"//common:base64.h",
|
||||
],
|
||||
deps = [
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/time",
|
||||
"//common:json_util",
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics",
|
||||
"//common:http_util",
|
||||
"//common:rule_util",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "key_auth_lib",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
"//common:base64.h",
|
||||
],
|
||||
hdrs = [
|
||||
"plugin.h",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/time",
|
||||
"//common:json_util",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
"//common:http_util",
|
||||
"//common:rule_util",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "key_auth_test",
|
||||
srcs = [
|
||||
"plugin_test.cc",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
":key_auth_lib",
|
||||
"@com_google_googletest//:gtest",
|
||||
"@com_google_googletest//:gtest_main",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
],
|
||||
)
|
||||
|
||||
declare_wasm_image_targets(
|
||||
name = "key_auth",
|
||||
wasm_file = ":key_auth.wasm",
|
||||
)
|
||||
279
plugins/wasm-cpp/extensions/key_auth/plugin.cc
Normal file
279
plugins/wasm-cpp/extensions/key_auth/plugin.cc
Normal file
@@ -0,0 +1,279 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/key_auth/plugin.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "common/http_util.h"
|
||||
#include "common/json_util.h"
|
||||
|
||||
using ::nlohmann::json;
|
||||
using ::Wasm::Common::JsonArrayIterate;
|
||||
using ::Wasm::Common::JsonGetField;
|
||||
using ::Wasm::Common::JsonObjectIterate;
|
||||
using ::Wasm::Common::JsonValueAs;
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace key_auth {
|
||||
|
||||
PROXY_WASM_NULL_PLUGIN_REGISTRY
|
||||
|
||||
#endif
|
||||
|
||||
static RegisterContextFactory register_KeyAuth(CONTEXT_FACTORY(PluginContext),
|
||||
ROOT_FACTORY(PluginRootContext));
|
||||
|
||||
namespace {
|
||||
|
||||
void deniedNoKeyAuthData(const std::string& realm) {
|
||||
sendLocalResponse(401, "No API key found in request", "",
|
||||
{{"WWW-Authenticate", absl::StrCat("Key realm=", realm)}});
|
||||
}
|
||||
|
||||
void deniedInvalidCredentials(const std::string& realm) {
|
||||
sendLocalResponse(401, "Request denied by Key Auth check. Invalid API key",
|
||||
"",
|
||||
{{"WWW-Authenticate", absl::StrCat("Key realm=", realm)}});
|
||||
}
|
||||
|
||||
void deniedUnauthorizedConsumer(const std::string& realm) {
|
||||
sendLocalResponse(
|
||||
403, "Request denied by Key Auth check. Unauthorized consumer", "",
|
||||
{{"WWW-Authenticate", absl::StrCat("Basic realm=", realm)}});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
KeyAuthConfigRule& rule) {
|
||||
if ((configuration.find("consumers") != configuration.end()) &&
|
||||
(configuration.find("credentials") != configuration.end())) {
|
||||
LOG_WARN(
|
||||
"The consumers field and the credentials field cannot appear at the "
|
||||
"same level");
|
||||
return false;
|
||||
}
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "credentials", [&](const json& credentials) -> bool {
|
||||
auto credential = JsonValueAs<std::string>(credentials);
|
||||
if (credential.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
return false;
|
||||
}
|
||||
rule.credentials.insert(credential.first.value());
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for credentials.");
|
||||
return false;
|
||||
}
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "consumers", [&](const json& consumer) -> bool {
|
||||
auto item = consumer.find("name");
|
||||
if (item == consumer.end()) {
|
||||
LOG_WARN("can't find 'name' field in consumer.");
|
||||
return false;
|
||||
}
|
||||
auto name = JsonValueAs<std::string>(item.value());
|
||||
if (name.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!name.first) {
|
||||
return false;
|
||||
}
|
||||
item = consumer.find("credential");
|
||||
if (item == consumer.end()) {
|
||||
LOG_WARN("can't find 'credential' field in consumer.");
|
||||
return false;
|
||||
}
|
||||
auto credential = JsonValueAs<std::string>(item.value());
|
||||
if (credential.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!credential.first) {
|
||||
return false;
|
||||
}
|
||||
if (rule.credential_to_name.find(credential.first.value()) !=
|
||||
rule.credential_to_name.end()) {
|
||||
LOG_WARN(absl::StrCat("duplicate consumer credential: ",
|
||||
credential.first.value()));
|
||||
return false;
|
||||
}
|
||||
rule.credentials.insert(credential.first.value());
|
||||
rule.credential_to_name.emplace(
|
||||
std::make_pair(credential.first.value(), name.first.value()));
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for credentials.");
|
||||
return false;
|
||||
}
|
||||
if (rule.credentials.empty()) {
|
||||
LOG_INFO("at least one credential has to be configured for a rule.");
|
||||
return false;
|
||||
}
|
||||
if (!JsonArrayIterate(configuration, "keys", [&](const json& item) -> bool {
|
||||
auto key = JsonValueAs<std::string>(item);
|
||||
if (key.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
return false;
|
||||
}
|
||||
rule.keys.push_back(key.first.value());
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for keys.");
|
||||
return false;
|
||||
}
|
||||
if (rule.keys.empty()) {
|
||||
LOG_WARN("at least one key has to be configured for a rule.");
|
||||
return false;
|
||||
}
|
||||
auto it = configuration.find("realm");
|
||||
if (it != configuration.end()) {
|
||||
auto realm_string = JsonValueAs<std::string>(it.value());
|
||||
if (realm_string.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
return false;
|
||||
}
|
||||
rule.realm = realm_string.first.value();
|
||||
}
|
||||
it = configuration.find("in_query");
|
||||
if (it != configuration.end()) {
|
||||
auto in_query = JsonValueAs<bool>(it.value());
|
||||
if (in_query.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!in_query.first) {
|
||||
LOG_WARN("failed to parse 'in_query' field in filter configuration.");
|
||||
return false;
|
||||
}
|
||||
rule.in_query = in_query.first.value();
|
||||
}
|
||||
it = configuration.find("in_header");
|
||||
if (it != configuration.end()) {
|
||||
auto in_header = JsonValueAs<bool>(it.value());
|
||||
if (in_header.second != Wasm::Common::JsonParserResultDetail::OK ||
|
||||
!in_header.first) {
|
||||
LOG_WARN("failed to parse 'in_header' field in filter configuration.");
|
||||
return false;
|
||||
}
|
||||
rule.in_header = in_header.first.value();
|
||||
}
|
||||
if (!rule.in_query && !rule.in_header) {
|
||||
LOG_WARN("at least one of 'in_query' and 'in_header' must set to true");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::checkPlugin(
|
||||
const KeyAuthConfigRule& rule,
|
||||
const std::optional<std::unordered_set<std::string>>& allow_set) {
|
||||
auto credential = extractCredential(rule);
|
||||
if (credential.empty()) {
|
||||
LOG_DEBUG("empty credential");
|
||||
deniedNoKeyAuthData(rule.realm);
|
||||
return false;
|
||||
}
|
||||
auto auth_credential_iter = rule.credentials.find(std::string(credential));
|
||||
// Check if the credential is part of the credentials
|
||||
// set from our container to grant or deny access.
|
||||
if (auth_credential_iter == rule.credentials.end()) {
|
||||
LOG_DEBUG(absl::StrCat("api key not found: ", credential));
|
||||
deniedInvalidCredentials(rule.realm);
|
||||
return false;
|
||||
}
|
||||
// Check if this credential has a consumer name. If so, check if this
|
||||
// consumer is allowed to access. If allow_set is empty, allow all consumers.
|
||||
auto credential_to_name_iter =
|
||||
rule.credential_to_name.find(std::string(std::string(credential)));
|
||||
if (credential_to_name_iter != rule.credential_to_name.end()) {
|
||||
if (allow_set && !allow_set.value().empty()) {
|
||||
if (allow_set.value().find(credential_to_name_iter->second) ==
|
||||
allow_set.value().end()) {
|
||||
deniedUnauthorizedConsumer(rule.realm);
|
||||
LOG_DEBUG(credential_to_name_iter->second);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
addRequestHeader("X-Mse-Consumer", credential_to_name_iter->second);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::onConfigure(size_t size) {
|
||||
// Parse configuration JSON string.
|
||||
if (size > 0 && !configure(size)) {
|
||||
LOG_WARN("configuration has errors initialization will not continue.");
|
||||
setInvalidConfig();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::configure(size_t configuration_size) {
|
||||
auto configuration_data = getBufferBytes(WasmBufferType::PluginConfiguration,
|
||||
0, configuration_size);
|
||||
// Parse configuration JSON string.
|
||||
auto result = ::Wasm::Common::JsonParse(configuration_data->view());
|
||||
if (!result) {
|
||||
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
configuration_data->view()));
|
||||
return false;
|
||||
}
|
||||
if (!parseAuthRuleConfig(result.value())) {
|
||||
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
configuration_data->view()));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string PluginRootContext::extractCredential(
|
||||
const KeyAuthConfigRule& rule) {
|
||||
auto request_path_header = getRequestHeader(":path");
|
||||
auto path = request_path_header->view();
|
||||
LOG_DEBUG(std::string(path));
|
||||
if (rule.in_query) {
|
||||
auto params = Wasm::Common::Http::parseAndDecodeQueryString(path);
|
||||
for (const auto& key : rule.keys) {
|
||||
auto it = params.find(key);
|
||||
if (it != params.end()) {
|
||||
return it->second;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rule.in_header) {
|
||||
for (const auto& key : rule.keys) {
|
||||
auto header = getRequestHeader(key);
|
||||
if (header->size() != 0) {
|
||||
return header->toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) {
|
||||
auto* rootCtx = rootContext();
|
||||
return rootCtx->checkAuthRule(
|
||||
[rootCtx](const auto& config, const auto& allow_set) {
|
||||
return rootCtx->checkPlugin(config, allow_set);
|
||||
})
|
||||
? FilterHeadersStatus::Continue
|
||||
: FilterHeadersStatus::StopIteration;
|
||||
}
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace key_auth
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
85
plugins/wasm-cpp/extensions/key_auth/plugin.h
Normal file
85
plugins/wasm-cpp/extensions/key_auth/plugin.h
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "common/route_rule_matcher.h"
|
||||
#define ASSERT(_X) assert(_X)
|
||||
|
||||
#ifndef NULL_PLUGIN
|
||||
|
||||
#include "proxy_wasm_intrinsics.h"
|
||||
|
||||
#else
|
||||
|
||||
#include "include/proxy-wasm/null_plugin.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace key_auth {
|
||||
|
||||
#endif
|
||||
|
||||
struct KeyAuthConfigRule {
|
||||
std::unordered_set<std::string> credentials;
|
||||
std::unordered_map<std::string, std::string> credential_to_name;
|
||||
std::string realm = "MSE Gateway";
|
||||
std::vector<std::string> keys;
|
||||
bool in_query = true;
|
||||
bool in_header = true;
|
||||
};
|
||||
|
||||
// PluginRootContext is the root context for all streams processed by the
|
||||
// thread. It has the same lifetime as the worker thread and acts as target for
|
||||
// interactions that outlives individual stream, e.g. timer, async calls.
|
||||
class PluginRootContext : public RootContext,
|
||||
public RouteRuleMatcher<KeyAuthConfigRule> {
|
||||
public:
|
||||
PluginRootContext(uint32_t id, std::string_view root_id)
|
||||
: RootContext(id, root_id) {}
|
||||
~PluginRootContext() {}
|
||||
bool onConfigure(size_t) override;
|
||||
bool checkPlugin(const KeyAuthConfigRule&,
|
||||
const std::optional<std::unordered_set<std::string>>&);
|
||||
bool configure(size_t);
|
||||
|
||||
private:
|
||||
bool parsePluginConfig(const json&, KeyAuthConfigRule&) override;
|
||||
std::string extractCredential(const KeyAuthConfigRule&);
|
||||
};
|
||||
|
||||
// Per-stream context.
|
||||
class PluginContext : public Context {
|
||||
public:
|
||||
explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {}
|
||||
FilterHeadersStatus onRequestHeaders(uint32_t, bool) override;
|
||||
|
||||
private:
|
||||
inline PluginRootContext* rootContext() {
|
||||
return dynamic_cast<PluginRootContext*>(this->root());
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace key_auth
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
245
plugins/wasm-cpp/extensions/key_auth/plugin_test.cc
Normal file
245
plugins/wasm-cpp/extensions/key_auth/plugin_test.cc
Normal file
@@ -0,0 +1,245 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/key_auth/plugin.h"
|
||||
|
||||
#include "common/base64.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "include/proxy-wasm/context.h"
|
||||
#include "include/proxy-wasm/null.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace key_auth {
|
||||
|
||||
NullPluginRegistry* context_registry_;
|
||||
RegisterNullVmPluginFactory register_key_auth_plugin("key_auth", []() {
|
||||
return std::make_unique<NullPlugin>(key_auth::context_registry_);
|
||||
});
|
||||
|
||||
class MockContext : public proxy_wasm::ContextBase {
|
||||
public:
|
||||
MockContext(WasmBase* wasm) : ContextBase(wasm) {}
|
||||
|
||||
MOCK_METHOD(BufferInterface*, getBuffer, (WasmBufferType));
|
||||
MOCK_METHOD(WasmResult, log, (uint32_t, std::string_view));
|
||||
MOCK_METHOD(WasmResult, getHeaderMapValue,
|
||||
(WasmHeaderMapType /* type */, std::string_view /* key */,
|
||||
std::string_view* /*result */));
|
||||
MOCK_METHOD(WasmResult, addHeaderMapValue,
|
||||
(WasmHeaderMapType /* type */, std::string_view /* key */,
|
||||
std::string_view /* value */));
|
||||
MOCK_METHOD(WasmResult, sendLocalResponse,
|
||||
(uint32_t /* response_code */, std::string_view /* body */,
|
||||
Pairs /* additional_headers */, uint32_t /* grpc_status */,
|
||||
std::string_view /* details */));
|
||||
MOCK_METHOD(WasmResult, getProperty, (std::string_view, std::string*));
|
||||
};
|
||||
|
||||
class KeyAuthTest : public ::testing::Test {
|
||||
protected:
|
||||
KeyAuthTest() {
|
||||
// Initialize test VM
|
||||
test_vm_ = createNullVm();
|
||||
wasm_base_ = std::make_unique<WasmBase>(
|
||||
std::move(test_vm_), "test-vm", "", "",
|
||||
std::unordered_map<std::string, std::string>{},
|
||||
AllowedCapabilitiesMap{});
|
||||
wasm_base_->load("key_auth");
|
||||
wasm_base_->initialize();
|
||||
|
||||
// Initialize host side context
|
||||
mock_context_ = std::make_unique<MockContext>(wasm_base_.get());
|
||||
current_context_ = mock_context_.get();
|
||||
|
||||
ON_CALL(*mock_context_, log(testing::_, testing::_))
|
||||
.WillByDefault([](uint32_t, std::string_view m) {
|
||||
std::cerr << m << "\n";
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getHeaderMapValue(WasmHeaderMapType::RequestHeaders,
|
||||
testing::_, testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view header,
|
||||
std::string_view* result) {
|
||||
if (header == ":authority") {
|
||||
*result = authority_;
|
||||
}
|
||||
if (header == ":path") {
|
||||
*result = path_;
|
||||
}
|
||||
if (header == "x-api-key") {
|
||||
*result = key_header_;
|
||||
}
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
ON_CALL(*mock_context_, addHeaderMapValue(WasmHeaderMapType::RequestHeaders,
|
||||
testing::_, testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view key,
|
||||
std::string_view value) { return WasmResult::Ok; });
|
||||
|
||||
ON_CALL(*mock_context_, getProperty(testing::_, testing::_))
|
||||
.WillByDefault([&](std::string_view path, std::string* result) {
|
||||
*result = route_name_;
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
// Initialize Wasm sandbox context
|
||||
root_context_ = std::make_unique<PluginRootContext>(0, "");
|
||||
context_ = std::make_unique<PluginContext>(1, root_context_.get());
|
||||
}
|
||||
~KeyAuthTest() override {}
|
||||
|
||||
std::unique_ptr<WasmBase> wasm_base_;
|
||||
std::unique_ptr<WasmVm> test_vm_;
|
||||
std::unique_ptr<MockContext> mock_context_;
|
||||
|
||||
std::unique_ptr<PluginRootContext> root_context_;
|
||||
std::unique_ptr<PluginContext> context_;
|
||||
|
||||
std::string path_;
|
||||
std::string authority_;
|
||||
std::string route_name_;
|
||||
std::string key_header_;
|
||||
};
|
||||
|
||||
TEST_F(KeyAuthTest, InQuery) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_": [
|
||||
{
|
||||
"_match_route_": ["test"],
|
||||
"credentials":["abc"],
|
||||
"keys": ["apiKey", "x-api-key"]
|
||||
}
|
||||
]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
buffer.set(configuration);
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
route_name_ = "test";
|
||||
path_ = "/test?hello=123&apiKey=abc";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
path_ = "/test?hello=123";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/test?hello=123&apiKey=123";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
|
||||
TEST_F(KeyAuthTest, InQueryWithConsumer) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers" : [ {"credential" : "abc", "name" : "consumer1"} ],
|
||||
"keys" : [ "apiKey", "x-api-key" ],
|
||||
"_rules_" : [ {"_match_route_" : ["test"], "allow" : ["consumer1"]} ]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
buffer.set(configuration);
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
route_name_ = "test";
|
||||
path_ = "/test?hello=1&apiKey=abc";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
path_ = "/test?hello=123";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/test?hello=123&apiKey=123";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
|
||||
TEST_F(KeyAuthTest, InHeader) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"credentials":["abc", "xyz"],
|
||||
"keys": ["x-api-key"]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
buffer.set(configuration);
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
path_ = "/test?hello=123";
|
||||
key_header_ = "abc";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
path_ = "/test?hello=123";
|
||||
key_header_ = "xyz";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
path_ = "/test?hello=123";
|
||||
key_header_ = "";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/test?hello=123";
|
||||
key_header_ = "123";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
|
||||
TEST_F(KeyAuthTest, InHeaderWithConsumer) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"consumers" : [ {"credential" : "abc", "name" : "consumer1"},
|
||||
{"credential" : "xyz", "name" : "consumer1"} ],
|
||||
"keys": ["x-api-key"]
|
||||
})";
|
||||
BufferBase buffer;
|
||||
buffer.set(configuration);
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
path_ = "/test?hello=123";
|
||||
key_header_ = "abc";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
path_ = "/test?hello=123";
|
||||
key_header_ = "xyz";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
path_ = "/test?hello=123";
|
||||
key_header_ = "";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/test?hello=123";
|
||||
key_header_ = "123";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
}
|
||||
|
||||
} // namespace key_auth
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
59
plugins/wasm-cpp/extensions/key_rate_limit/BUILD
Normal file
59
plugins/wasm-cpp/extensions/key_rate_limit/BUILD
Normal file
@@ -0,0 +1,59 @@
|
||||
load("@proxy_wasm_cpp_sdk//bazel/wasm:wasm.bzl", "wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
wasm_cc_binary(
|
||||
name = "key_rate_limit.wasm",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
"plugin.h",
|
||||
"bucket.h",
|
||||
"bucket.cc",
|
||||
],
|
||||
deps = [
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/time",
|
||||
"//common:json_util",
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics",
|
||||
"//common:http_util",
|
||||
"//common:rule_util",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "key_rate_limit_lib",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
"bucket.cc",
|
||||
],
|
||||
hdrs = [
|
||||
"plugin.h",
|
||||
"bucket.h",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
"@com_google_absl//absl/strings",
|
||||
"//common:json_util",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
"//common:http_util",
|
||||
"//common:rule_util",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "key_rate_limit_test",
|
||||
srcs = [
|
||||
"plugin_test.cc",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
":key_rate_limit_lib",
|
||||
"@com_google_googletest//:gtest",
|
||||
"@com_google_googletest//:gtest_main",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
],
|
||||
)
|
||||
|
||||
declare_wasm_image_targets(
|
||||
name = "key_rate_limit",
|
||||
wasm_file = ":key_rate_limit.wasm",
|
||||
)
|
||||
177
plugins/wasm-cpp/extensions/key_rate_limit/bucket.cc
Normal file
177
plugins/wasm-cpp/extensions/key_rate_limit/bucket.cc
Normal file
@@ -0,0 +1,177 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/key_rate_limit/bucket.h"
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
|
||||
namespace {
|
||||
|
||||
const int maxGetTokenRetry = 20;
|
||||
|
||||
// Key-prefix for token bucket shared data.
|
||||
std::string tokenBucketPrefix = "mse.token_bucket";
|
||||
|
||||
// Key-prefix for token bucket last updated time.
|
||||
std::string lastRefilledPrefix = "mse.last_refilled";
|
||||
|
||||
} // namespace
|
||||
|
||||
bool getToken(int rule_id, const std::string &key) {
|
||||
WasmDataPtr token_bucket_data;
|
||||
uint32_t cas;
|
||||
std::string tokenBucketKey =
|
||||
std::to_string(rule_id) + tokenBucketPrefix + key;
|
||||
for (int i = 0; i < maxGetTokenRetry; i++) {
|
||||
if (WasmResult::Ok !=
|
||||
getSharedData(tokenBucketKey, &token_bucket_data, &cas)) {
|
||||
return false;
|
||||
}
|
||||
uint64_t token_left =
|
||||
*reinterpret_cast<const uint64_t *>(token_bucket_data->data());
|
||||
if (token_left == 0) {
|
||||
return false;
|
||||
}
|
||||
token_left -= 1;
|
||||
auto res = setSharedData(
|
||||
tokenBucketKey,
|
||||
{reinterpret_cast<const char *>(&token_left), sizeof(token_left)}, cas);
|
||||
if (res == WasmResult::Ok) {
|
||||
return true;
|
||||
}
|
||||
if (res == WasmResult::CasMismatch) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_WARN("get token failed with cas mismatch");
|
||||
return true;
|
||||
}
|
||||
|
||||
void refillToken(const std::vector<std::pair<int, LimitItem>> &rules) {
|
||||
uint32_t last_update_cas;
|
||||
WasmDataPtr last_update_data;
|
||||
for (const auto &rule : rules) {
|
||||
auto id = std::to_string(rule.first);
|
||||
std::string lastRefilledKey = id + lastRefilledPrefix + rule.second.key;
|
||||
std::string tokenBucketKey = id + tokenBucketPrefix + rule.second.key;
|
||||
auto result =
|
||||
getSharedData(lastRefilledKey, &last_update_data, &last_update_cas);
|
||||
if (result != WasmResult::Ok) {
|
||||
LOG_WARN(
|
||||
absl::StrCat("failed to get last update time of the local rate limit "
|
||||
"token bucket ",
|
||||
toString(result)));
|
||||
continue;
|
||||
}
|
||||
uint64_t last_update =
|
||||
*reinterpret_cast<const uint64_t *>(last_update_data->data());
|
||||
uint64_t now = getCurrentTimeNanoseconds();
|
||||
if (now - last_update < rule.second.refill_interval_nanosec) {
|
||||
continue;
|
||||
}
|
||||
// Otherwise, try set last updated time. If updated failed because of cas
|
||||
// mismatch, the bucket is going to be refilled by other VMs.
|
||||
auto res = setSharedData(
|
||||
lastRefilledKey, {reinterpret_cast<const char *>(&now), sizeof(now)},
|
||||
last_update_cas);
|
||||
if (res == WasmResult::CasMismatch) {
|
||||
continue;
|
||||
}
|
||||
do {
|
||||
if (WasmResult::Ok !=
|
||||
getSharedData(tokenBucketKey, &last_update_data, &last_update_cas)) {
|
||||
LOG_WARN("failed to get current local rate limit token bucket");
|
||||
break;
|
||||
}
|
||||
uint64_t token_left =
|
||||
*reinterpret_cast<const uint64_t *>(last_update_data->data());
|
||||
// Refill tokens, and update bucket with cas. If update failed because of
|
||||
// cas mismatch, retry refilling.
|
||||
token_left += rule.second.tokens_per_refill;
|
||||
if (token_left > rule.second.max_tokens) {
|
||||
token_left = rule.second.max_tokens;
|
||||
}
|
||||
if (WasmResult::CasMismatch ==
|
||||
setSharedData(
|
||||
tokenBucketKey,
|
||||
{reinterpret_cast<const char *>(&token_left), sizeof(token_left)},
|
||||
last_update_cas)) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
} while (true);
|
||||
}
|
||||
}
|
||||
|
||||
bool initializeTokenBucket(
|
||||
const std::vector<std::pair<int, LimitItem>> &rules) {
|
||||
uint32_t last_update_cas;
|
||||
WasmDataPtr last_update_data;
|
||||
uint64_t initial_value = 0;
|
||||
for (const auto &rule : rules) {
|
||||
auto id = std::to_string(rule.first);
|
||||
std::string lastRefilledKey = id + lastRefilledPrefix + rule.second.key;
|
||||
std::string tokenBucketKey = id + tokenBucketPrefix + rule.second.key;
|
||||
auto res =
|
||||
getSharedData(lastRefilledKey, &last_update_data, &last_update_cas);
|
||||
if (res == WasmResult::NotFound) {
|
||||
setSharedData(lastRefilledKey,
|
||||
{reinterpret_cast<const char *>(&initial_value),
|
||||
sizeof(initial_value)});
|
||||
setSharedData(tokenBucketKey,
|
||||
{reinterpret_cast<const char *>(&rule.second.max_tokens),
|
||||
sizeof(uint64_t)});
|
||||
continue;
|
||||
}
|
||||
// reconfigure
|
||||
do {
|
||||
if (WasmResult::Ok !=
|
||||
getSharedData(lastRefilledKey, &last_update_data, &last_update_cas)) {
|
||||
LOG_WARN("failed to get lastRefilled");
|
||||
return false;
|
||||
}
|
||||
if (WasmResult::CasMismatch ==
|
||||
setSharedData(lastRefilledKey,
|
||||
{reinterpret_cast<const char *>(&initial_value),
|
||||
sizeof(initial_value)},
|
||||
last_update_cas)) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
} while (true);
|
||||
do {
|
||||
if (WasmResult::Ok !=
|
||||
getSharedData(tokenBucketKey, &last_update_data, &last_update_cas)) {
|
||||
LOG_WARN("failed to get tokenBucket");
|
||||
return false;
|
||||
}
|
||||
if (WasmResult::CasMismatch ==
|
||||
setSharedData(
|
||||
tokenBucketKey,
|
||||
{reinterpret_cast<const char *>(&rule.second.max_tokens),
|
||||
sizeof(uint64_t)},
|
||||
last_update_cas)) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
} while (true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
40
plugins/wasm-cpp/extensions/key_rate_limit/bucket.h
Normal file
40
plugins/wasm-cpp/extensions/key_rate_limit/bucket.h
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef NULL_PLUGIN
|
||||
|
||||
#include "proxy_wasm_intrinsics.h"
|
||||
|
||||
#else
|
||||
|
||||
#include "include/proxy-wasm/null_plugin.h"
|
||||
using namespace proxy_wasm::null_plugin;
|
||||
using proxy_wasm::WasmResult;
|
||||
|
||||
#endif
|
||||
|
||||
struct LimitItem {
|
||||
std::string key;
|
||||
uint64_t tokens_per_refill;
|
||||
uint64_t refill_interval_nanosec;
|
||||
uint64_t max_tokens;
|
||||
};
|
||||
|
||||
bool getToken(int rule_id, const std::string& key);
|
||||
void refillToken(const std::vector<std::pair<int, LimitItem>>& rules);
|
||||
bool initializeTokenBucket(const std::vector<std::pair<int, LimitItem>>& rules);
|
||||
231
plugins/wasm-cpp/extensions/key_rate_limit/plugin.cc
Normal file
231
plugins/wasm-cpp/extensions/key_rate_limit/plugin.cc
Normal file
@@ -0,0 +1,231 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/key_rate_limit/plugin.h"
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "common/json_util.h"
|
||||
|
||||
using ::nlohmann::json;
|
||||
using ::Wasm::Common::JsonArrayIterate;
|
||||
using ::Wasm::Common::JsonGetField;
|
||||
using ::Wasm::Common::JsonObjectIterate;
|
||||
using ::Wasm::Common::JsonValueAs;
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace key_rate_limit {
|
||||
|
||||
PROXY_WASM_NULL_PLUGIN_REGISTRY
|
||||
|
||||
#endif
|
||||
|
||||
static RegisterContextFactory register_KeyRateLimit(
|
||||
CONTEXT_FACTORY(PluginContext), ROOT_FACTORY(PluginRootContext));
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint64_t second_nano = 1000 * 1000 * 1000;
|
||||
constexpr uint64_t minute_nano = 60 * second_nano;
|
||||
constexpr uint64_t hour_nano = 60 * minute_nano;
|
||||
constexpr uint64_t day_nano = 24 * hour_nano;
|
||||
|
||||
// tooManyRequest returns a 429 response code.
|
||||
void tooManyRequest() {
|
||||
sendLocalResponse(429, "Too many requests", "rate_limited", {});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
KeyRateLimitConfigRule& rule) {
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "limit_keys", [&](const json& item) -> bool {
|
||||
std::string key =
|
||||
Wasm::Common::JsonGetField<std::string>(item, "key").value();
|
||||
uint64_t qps =
|
||||
Wasm::Common::JsonGetField<uint64_t>(item, "query_per_second")
|
||||
.value_or(0);
|
||||
if (qps > 0) {
|
||||
rule.limit_keys.emplace(key, LimitItem{
|
||||
key,
|
||||
qps,
|
||||
second_nano,
|
||||
qps,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
uint64_t qpm =
|
||||
Wasm::Common::JsonGetField<uint64_t>(item, "query_per_minute")
|
||||
.value_or(0);
|
||||
if (qpm > 0) {
|
||||
rule.limit_keys.emplace(key, LimitItem{
|
||||
key,
|
||||
qpm,
|
||||
minute_nano,
|
||||
qpm,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
uint64_t qph =
|
||||
Wasm::Common::JsonGetField<uint64_t>(item, "query_per_hour")
|
||||
.value_or(0);
|
||||
if (qph > 0) {
|
||||
rule.limit_keys.emplace(key, LimitItem{
|
||||
key,
|
||||
qph,
|
||||
hour_nano,
|
||||
qph,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
uint64_t qpd =
|
||||
Wasm::Common::JsonGetField<uint64_t>(item, "query_per_day")
|
||||
.value_or(0);
|
||||
if (qpd > 0) {
|
||||
rule.limit_keys.emplace(key, LimitItem{
|
||||
key,
|
||||
qpd,
|
||||
day_nano,
|
||||
qpd,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
LOG_WARN(
|
||||
"one of 'query_per_second', 'query_per_minute', "
|
||||
"'query_per_hour' or 'query_per_day' must be set");
|
||||
return false;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for limit_keys.");
|
||||
return false;
|
||||
}
|
||||
if (rule.limit_keys.empty()) {
|
||||
LOG_WARN("no limit keys found in configuration");
|
||||
return false;
|
||||
}
|
||||
auto it = configuration.find("limit_by_header");
|
||||
if (it != configuration.end()) {
|
||||
auto limit_by_header = JsonValueAs<std::string>(it.value());
|
||||
if (limit_by_header.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse limit_by_header");
|
||||
return false;
|
||||
}
|
||||
rule.limit_by_header = limit_by_header.first.value();
|
||||
}
|
||||
it = configuration.find("limit_by_param");
|
||||
if (it != configuration.end()) {
|
||||
auto limit_by_param = JsonValueAs<std::string>(it.value());
|
||||
if (limit_by_param.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse limit_by_param");
|
||||
return false;
|
||||
}
|
||||
rule.limit_by_param = limit_by_param.first.value();
|
||||
}
|
||||
auto emptyHeader = rule.limit_by_header.empty();
|
||||
auto emptyParam = rule.limit_by_param.empty();
|
||||
if ((emptyHeader && emptyParam) || (!emptyHeader && !emptyParam)) {
|
||||
LOG_WARN("only one of 'limit_by_param' and 'limit_by_header' can be set");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::checkPlugin(int rule_id,
|
||||
const KeyRateLimitConfigRule& config) {
|
||||
const auto& headerKey = config.limit_by_header;
|
||||
const auto& paramKey = config.limit_by_param;
|
||||
std::string key;
|
||||
if (!headerKey.empty()) {
|
||||
GET_HEADER_VIEW(headerKey, header);
|
||||
key = header;
|
||||
} else {
|
||||
// use paramKey which must not be empty
|
||||
GET_HEADER_VIEW(":path", path);
|
||||
const auto& params = Wasm::Common::Http::parseQueryString(path);
|
||||
auto it = params.find(paramKey);
|
||||
if (it != params.end()) {
|
||||
key = it->second;
|
||||
}
|
||||
}
|
||||
const auto& limit_keys = config.limit_keys;
|
||||
if (limit_keys.find(key) == limit_keys.end()) {
|
||||
return true;
|
||||
}
|
||||
if (!getToken(rule_id, key)) {
|
||||
tooManyRequest();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void PluginRootContext::onTick() { refillToken(limits_); }
|
||||
|
||||
bool PluginRootContext::onConfigure(size_t size) {
|
||||
// Parse configuration JSON string.
|
||||
if (size > 0 && !configure(size)) {
|
||||
LOG_WARN("configuration has errors initialization will not continue.");
|
||||
setInvalidConfig();
|
||||
return true;
|
||||
}
|
||||
const auto& rules = getRules();
|
||||
for (const auto& rule : rules) {
|
||||
for (auto& keyItem : rule.second.get().limit_keys) {
|
||||
limits_.emplace_back(rule.first, keyItem.second);
|
||||
}
|
||||
}
|
||||
initializeTokenBucket(limits_);
|
||||
proxy_set_tick_period_milliseconds(1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::configure(size_t configuration_size) {
|
||||
auto configuration_data = getBufferBytes(WasmBufferType::PluginConfiguration,
|
||||
0, configuration_size);
|
||||
// Parse configuration JSON string.
|
||||
auto result = ::Wasm::Common::JsonParse(configuration_data->view());
|
||||
if (!result.has_value()) {
|
||||
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
configuration_data->view()));
|
||||
return false;
|
||||
}
|
||||
if (!parseRuleConfig(result.value())) {
|
||||
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
configuration_data->view()));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) {
|
||||
auto* rootCtx = rootContext();
|
||||
return rootCtx->checkRuleWithId([rootCtx](auto rule_id, const auto& config) {
|
||||
return rootCtx->checkPlugin(rule_id, config);
|
||||
})
|
||||
? FilterHeadersStatus::Continue
|
||||
: FilterHeadersStatus::StopIteration;
|
||||
}
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace key_rate_limit
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
89
plugins/wasm-cpp/extensions/key_rate_limit/plugin.h
Normal file
89
plugins/wasm-cpp/extensions/key_rate_limit/plugin.h
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "bucket.h"
|
||||
#include "common/http_util.h"
|
||||
#include "common/route_rule_matcher.h"
|
||||
#define ASSERT(_X) assert(_X)
|
||||
|
||||
#ifndef NULL_PLUGIN
|
||||
|
||||
#include "proxy_wasm_intrinsics.h"
|
||||
|
||||
#else
|
||||
|
||||
#include "include/proxy-wasm/null_plugin.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace key_rate_limit {
|
||||
|
||||
#endif
|
||||
|
||||
struct KeyRateLimitConfigRule {
|
||||
std::unordered_map<std::string, LimitItem> limit_keys;
|
||||
std::string limit_by_header;
|
||||
std::string limit_by_param;
|
||||
};
|
||||
|
||||
// PluginRootContext is the root context for all streams processed by the
|
||||
// thread. It has the same lifetime as the worker thread and acts as target for
|
||||
// interactions that outlives individual stream, e.g. timer, async calls.
|
||||
class PluginRootContext : public RootContext,
|
||||
public RouteRuleMatcher<KeyRateLimitConfigRule> {
|
||||
public:
|
||||
PluginRootContext(uint32_t id, std::string_view root_id)
|
||||
: RootContext(id, root_id) {}
|
||||
~PluginRootContext() {}
|
||||
bool onConfigure(size_t) override;
|
||||
void onTick() override;
|
||||
bool checkPlugin(int, const KeyRateLimitConfigRule&);
|
||||
bool configure(size_t);
|
||||
|
||||
private:
|
||||
bool parsePluginConfig(const json&, KeyRateLimitConfigRule&) override;
|
||||
|
||||
std::vector<std::pair<int, LimitItem>> limits_;
|
||||
|
||||
friend class KeyRateLimitTest_Config_Test;
|
||||
friend class KeyRateLimitTest_RuleConfig_Test;
|
||||
};
|
||||
|
||||
// Per-stream context.
|
||||
class PluginContext : public Context {
|
||||
public:
|
||||
explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {}
|
||||
FilterHeadersStatus onRequestHeaders(uint32_t, bool) override;
|
||||
|
||||
private:
|
||||
inline PluginRootContext* rootContext() {
|
||||
return dynamic_cast<PluginRootContext*>(this->root());
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace key_rate_limit
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
206
plugins/wasm-cpp/extensions/key_rate_limit/plugin_test.cc
Normal file
206
plugins/wasm-cpp/extensions/key_rate_limit/plugin_test.cc
Normal file
@@ -0,0 +1,206 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/key_rate_limit/plugin.h"
|
||||
|
||||
#include "absl/strings/str_join.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "include/proxy-wasm/context.h"
|
||||
#include "include/proxy-wasm/null.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace key_rate_limit {
|
||||
|
||||
NullPluginRegistry* context_registry_;
|
||||
RegisterNullVmPluginFactory register_key_rate_limit_plugin(
|
||||
"key_rate_limit", []() {
|
||||
return std::make_unique<NullPlugin>(key_rate_limit::context_registry_);
|
||||
});
|
||||
|
||||
class MockContext : public proxy_wasm::ContextBase {
|
||||
public:
|
||||
MockContext(WasmBase* wasm) : ContextBase(wasm) {}
|
||||
|
||||
MOCK_METHOD(BufferInterface*, getBuffer, (WasmBufferType));
|
||||
MOCK_METHOD(WasmResult, log, (uint32_t, std::string_view));
|
||||
MOCK_METHOD(WasmResult, getHeaderMapValue,
|
||||
(WasmHeaderMapType /* type */, std::string_view /* key */,
|
||||
std::string_view* /*result */));
|
||||
MOCK_METHOD(WasmResult, sendLocalResponse,
|
||||
(uint32_t /* response_code */, std::string_view /* body */,
|
||||
Pairs /* additional_headers */, uint32_t /* grpc_status */,
|
||||
std::string_view /* details */));
|
||||
MOCK_METHOD(WasmResult, getProperty, (std::string_view, std::string*));
|
||||
};
|
||||
|
||||
class KeyRateLimitTest : public ::testing::Test {
|
||||
protected:
|
||||
KeyRateLimitTest() {
|
||||
// Initialize test VM
|
||||
test_vm_ = createNullVm();
|
||||
wasm_base_ = std::make_unique<WasmBase>(
|
||||
std::move(test_vm_), "test-vm", "", "",
|
||||
std::unordered_map<std::string, std::string>{},
|
||||
AllowedCapabilitiesMap{});
|
||||
wasm_base_->load("key_rate_limit");
|
||||
wasm_base_->initialize();
|
||||
|
||||
// Initialize host side context
|
||||
mock_context_ = std::make_unique<MockContext>(wasm_base_.get());
|
||||
current_context_ = mock_context_.get();
|
||||
|
||||
ON_CALL(*mock_context_, log(testing::_, testing::_))
|
||||
.WillByDefault([](uint32_t, std::string_view m) {
|
||||
std::cerr << m << "\n";
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getHeaderMapValue(WasmHeaderMapType::RequestHeaders,
|
||||
testing::_, testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view header,
|
||||
std::string_view* result) {
|
||||
if (header == ":authority") {
|
||||
*result = authority_;
|
||||
}
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_,
|
||||
getHeaderMapValue(WasmHeaderMapType::ResponseHeaders, testing::_,
|
||||
testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view header,
|
||||
std::string_view* result) {
|
||||
if (header == ":status") {
|
||||
*result = status_code_;
|
||||
}
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getProperty(testing::_, testing::_))
|
||||
.WillByDefault([&](std::string_view path, std::string* result) {
|
||||
*result = route_name_;
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
// Initialize Wasm sandbox context
|
||||
root_context_ = std::make_unique<PluginRootContext>(0, "");
|
||||
context_ = std::make_unique<PluginContext>(1, root_context_.get());
|
||||
}
|
||||
~KeyRateLimitTest() override {}
|
||||
|
||||
std::unique_ptr<WasmBase> wasm_base_;
|
||||
std::unique_ptr<WasmVm> test_vm_;
|
||||
std::unique_ptr<MockContext> mock_context_;
|
||||
|
||||
std::unique_ptr<PluginRootContext> root_context_;
|
||||
std::unique_ptr<PluginContext> context_;
|
||||
|
||||
std::string authority_;
|
||||
std::string route_name_;
|
||||
std::string status_code_;
|
||||
};
|
||||
|
||||
TEST_F(KeyRateLimitTest, Config) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"limit_by_header": "x-api-key",
|
||||
"limit_keys": [
|
||||
{
|
||||
"key": "a",
|
||||
"query_per_second": 1
|
||||
},
|
||||
{
|
||||
"key": "b",
|
||||
"query_per_minute": 1
|
||||
},
|
||||
{
|
||||
"key": "c",
|
||||
"query_per_hour": 1
|
||||
},
|
||||
{
|
||||
"key": "d",
|
||||
"query_per_day": 1
|
||||
}
|
||||
],
|
||||
"_rules_" : [
|
||||
{
|
||||
"_match_route_":["test"],
|
||||
"limit_by_param": "apikey",
|
||||
"limit_keys": [
|
||||
{
|
||||
"key": "a",
|
||||
"query_per_second": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->onConfigure(configuration.size()));
|
||||
EXPECT_EQ(root_context_->limits_.size(), 5);
|
||||
EXPECT_EQ(root_context_->limits_[0].first, 0);
|
||||
EXPECT_EQ(root_context_->limits_[1].first, 0);
|
||||
EXPECT_EQ(root_context_->limits_[2].first, 0);
|
||||
EXPECT_EQ(root_context_->limits_[3].first, 0);
|
||||
EXPECT_EQ(root_context_->limits_[4].first, 1);
|
||||
}
|
||||
|
||||
TEST_F(KeyRateLimitTest, RuleConfig) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"_rules_" : [
|
||||
{
|
||||
"_match_route_":["test"],
|
||||
"limit_by_param": "apikey",
|
||||
"limit_keys": [
|
||||
{
|
||||
"key": "a",
|
||||
"query_per_second": 10
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_match_route_":["abc"],
|
||||
"limit_by_param": "apikey",
|
||||
"limit_keys": [
|
||||
{
|
||||
"key": "a",
|
||||
"query_per_second": 100
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
BufferBase buffer;
|
||||
buffer.set({configuration.data(), configuration.size()});
|
||||
|
||||
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
|
||||
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
|
||||
EXPECT_TRUE(root_context_->onConfigure(configuration.size()));
|
||||
EXPECT_EQ(root_context_->limits_.size(), 2);
|
||||
EXPECT_EQ(root_context_->limits_[0].first, 1);
|
||||
EXPECT_EQ(root_context_->limits_[1].first, 2);
|
||||
}
|
||||
|
||||
} // namespace key_rate_limit
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
55
plugins/wasm-cpp/extensions/request_block/BUILD
Normal file
55
plugins/wasm-cpp/extensions/request_block/BUILD
Normal file
@@ -0,0 +1,55 @@
|
||||
load("@proxy_wasm_cpp_sdk//bazel/wasm:wasm.bzl", "wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
wasm_cc_binary(
|
||||
name = "request_block.wasm",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
"plugin.h",
|
||||
],
|
||||
deps = [
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/time",
|
||||
"//common:json_util",
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics",
|
||||
"//common:http_util",
|
||||
"//common:rule_util",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "request_block_lib",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
],
|
||||
hdrs = [
|
||||
"plugin.h",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
"@com_google_absl//absl/strings",
|
||||
"//common:json_util",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
"//common:http_util",
|
||||
"//common:rule_util",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "request_block_test",
|
||||
srcs = [
|
||||
"plugin_test.cc",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
":request_block_lib",
|
||||
"@com_google_googletest//:gtest",
|
||||
"@com_google_googletest//:gtest_main",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
],
|
||||
)
|
||||
|
||||
declare_wasm_image_targets(
|
||||
name = "request_block",
|
||||
wasm_file = ":request_block.wasm",
|
||||
)
|
||||
266
plugins/wasm-cpp/extensions/request_block/plugin.cc
Normal file
266
plugins/wasm-cpp/extensions/request_block/plugin.cc
Normal file
@@ -0,0 +1,266 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/request_block/plugin.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "common/json_util.h"
|
||||
|
||||
using ::nlohmann::json;
|
||||
using ::Wasm::Common::JsonArrayIterate;
|
||||
using ::Wasm::Common::JsonGetField;
|
||||
using ::Wasm::Common::JsonObjectIterate;
|
||||
using ::Wasm::Common::JsonValueAs;
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace request_block {
|
||||
|
||||
PROXY_WASM_NULL_PLUGIN_REGISTRY
|
||||
|
||||
#endif
|
||||
|
||||
static RegisterContextFactory register_RequestBlock(
|
||||
CONTEXT_FACTORY(PluginContext), ROOT_FACTORY(PluginRootContext));
|
||||
|
||||
static constexpr size_t MAX_BODY_SIZE = 32 * 1024 * 1024;
|
||||
|
||||
bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
RequestBlockConfigRule& rule) {
|
||||
auto it = configuration.find("blocked_code");
|
||||
if (it != configuration.end()) {
|
||||
auto blocked_code = JsonValueAs<int64_t>(it.value());
|
||||
if (blocked_code.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse status code");
|
||||
return false;
|
||||
}
|
||||
rule.blocked_code = blocked_code.first.value();
|
||||
}
|
||||
it = configuration.find("blocked_message");
|
||||
if (it != configuration.end()) {
|
||||
auto blocked_message = JsonValueAs<std::string>(it.value());
|
||||
if (blocked_message.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse blocked_message");
|
||||
return false;
|
||||
}
|
||||
rule.blocked_message = blocked_message.first.value();
|
||||
}
|
||||
it = configuration.find("case_sensitive");
|
||||
if (it != configuration.end()) {
|
||||
auto case_sensitive = JsonValueAs<bool>(it.value());
|
||||
if (case_sensitive.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse case_sensitive");
|
||||
return false;
|
||||
}
|
||||
rule.case_sensitive = case_sensitive.first.value();
|
||||
}
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "block_urls", [&](const json& item) -> bool {
|
||||
auto url = JsonValueAs<std::string>(item);
|
||||
if (url.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse block_urls");
|
||||
return false;
|
||||
}
|
||||
if (rule.case_sensitive) {
|
||||
rule.block_urls.push_back(std::move(url.first.value()));
|
||||
} else {
|
||||
rule.block_urls.push_back(
|
||||
absl::AsciiStrToLower(url.first.value()));
|
||||
}
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for block_urls.");
|
||||
return false;
|
||||
}
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "block_headers", [&](const json& item) -> bool {
|
||||
auto header = JsonValueAs<std::string>(item);
|
||||
if (header.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse block_headers");
|
||||
return false;
|
||||
}
|
||||
if (rule.case_sensitive) {
|
||||
rule.block_headers.push_back(std::move(header.first.value()));
|
||||
} else {
|
||||
rule.block_headers.push_back(
|
||||
absl::AsciiStrToLower(header.first.value()));
|
||||
}
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for block_headers.");
|
||||
return false;
|
||||
}
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "block_bodys", [&](const json& item) -> bool {
|
||||
auto body = JsonValueAs<std::string>(item);
|
||||
if (body.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse block_bodys");
|
||||
return false;
|
||||
}
|
||||
if (rule.case_sensitive) {
|
||||
rule.block_bodys.push_back(std::move(body.first.value()));
|
||||
} else {
|
||||
rule.block_bodys.push_back(
|
||||
absl::AsciiStrToLower(body.first.value()));
|
||||
}
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for block_bodys.");
|
||||
return false;
|
||||
}
|
||||
if (rule.block_bodys.empty() && rule.block_headers.empty() &&
|
||||
rule.block_urls.empty()) {
|
||||
LOG_WARN("there is no block rules");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::onConfigure(size_t size) {
|
||||
// Parse configuration JSON string.
|
||||
if (size > 0 && !configure(size)) {
|
||||
LOG_WARN("configuration has errors initialization will not continue.");
|
||||
setInvalidConfig();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::configure(size_t configuration_size) {
|
||||
auto configuration_data = getBufferBytes(WasmBufferType::PluginConfiguration,
|
||||
0, configuration_size);
|
||||
// Parse configuration JSON string.
|
||||
auto result = ::Wasm::Common::JsonParse(configuration_data->view());
|
||||
if (!result.has_value()) {
|
||||
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
configuration_data->view()));
|
||||
return false;
|
||||
}
|
||||
if (!parseRuleConfig(result.value())) {
|
||||
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
|
||||
configuration_data->view()));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PluginRootContext::checkHeader(const RequestBlockConfigRule& rule,
|
||||
bool& check_body) {
|
||||
if (!rule.block_urls.empty()) {
|
||||
std::string urlstr;
|
||||
std::string_view url;
|
||||
GET_HEADER_VIEW(":path", request_url);
|
||||
if (rule.case_sensitive) {
|
||||
url = request_url;
|
||||
} else {
|
||||
urlstr = absl::AsciiStrToLower(request_url);
|
||||
url = urlstr;
|
||||
}
|
||||
for (const auto& block_url : rule.block_urls) {
|
||||
if (absl::StrContains(url, block_url)) {
|
||||
sendLocalResponse(rule.blocked_code, "", rule.blocked_message, {});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!rule.block_headers.empty()) {
|
||||
auto headersPtr = getRequestHeaderPairs();
|
||||
std::string headerstr;
|
||||
std::string_view headers;
|
||||
if (rule.case_sensitive) {
|
||||
headers = headersPtr->view();
|
||||
} else {
|
||||
headerstr = absl::AsciiStrToLower(headersPtr->view());
|
||||
headers = headerstr;
|
||||
}
|
||||
for (const auto& block_header : rule.block_headers) {
|
||||
if (absl::StrContains(headers, block_header)) {
|
||||
sendLocalResponse(rule.blocked_code, "", rule.blocked_message, {});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!rule.block_bodys.empty()) {
|
||||
check_body = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool PluginRootContext::checkBody(const RequestBlockConfigRule& rule,
|
||||
std::string_view request_body) {
|
||||
std::string bodystr;
|
||||
std::string_view body;
|
||||
if (rule.case_sensitive) {
|
||||
body = request_body;
|
||||
} else {
|
||||
bodystr = absl::AsciiStrToLower(request_body);
|
||||
body = bodystr;
|
||||
}
|
||||
for (const auto& block_body : rule.block_bodys) {
|
||||
if (absl::StrContains(body, block_body)) {
|
||||
sendLocalResponse(rule.blocked_code, "", rule.blocked_message, {});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) {
|
||||
auto* rootCtx = rootContext();
|
||||
auto config = rootCtx->getMatchConfig();
|
||||
config_ = config.second;
|
||||
if (!config_) {
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
return rootCtx->checkHeader(config_.value(), check_body_)
|
||||
? FilterHeadersStatus::Continue
|
||||
: FilterHeadersStatus::StopIteration;
|
||||
}
|
||||
|
||||
FilterDataStatus PluginContext::onRequestBody(size_t body_size,
|
||||
bool end_stream) {
|
||||
if (!config_) {
|
||||
return FilterDataStatus::Continue;
|
||||
}
|
||||
if (!check_body_) {
|
||||
return FilterDataStatus::Continue;
|
||||
}
|
||||
body_total_size_ += body_size;
|
||||
if (body_total_size_ > MAX_BODY_SIZE) {
|
||||
LOG_DEBUG("body_size is too large");
|
||||
return FilterDataStatus::Continue;
|
||||
}
|
||||
if (!end_stream) {
|
||||
return FilterDataStatus::StopIterationAndBuffer;
|
||||
}
|
||||
auto body =
|
||||
getBufferBytes(WasmBufferType::HttpRequestBody, 0, body_total_size_);
|
||||
auto* rootCtx = rootContext();
|
||||
return rootCtx->checkBody(config_.value(), body->view())
|
||||
? FilterDataStatus::Continue
|
||||
: FilterDataStatus::StopIterationNoBuffer;
|
||||
}
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace request_block
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
92
plugins/wasm-cpp/extensions/request_block/plugin.h
Normal file
92
plugins/wasm-cpp/extensions/request_block/plugin.h
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "common/http_util.h"
|
||||
#include "common/route_rule_matcher.h"
|
||||
#define ASSERT(_X) assert(_X)
|
||||
|
||||
#ifndef NULL_PLUGIN
|
||||
|
||||
#include "proxy_wasm_intrinsics.h"
|
||||
|
||||
#else
|
||||
|
||||
#include "include/proxy-wasm/null_plugin.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace request_block {
|
||||
|
||||
#endif
|
||||
|
||||
struct RequestBlockConfigRule {
|
||||
int blocked_code = 403;
|
||||
std::string blocked_message;
|
||||
bool case_sensitive = true;
|
||||
std::vector<std::string> block_urls;
|
||||
std::vector<std::string> block_headers;
|
||||
std::vector<std::string> block_bodys;
|
||||
};
|
||||
|
||||
// PluginRootContext is the root context for all streams processed by the
|
||||
// thread. It has the same lifetime as the worker thread and acts as target for
|
||||
// interactions that outlives individual stream, e.g. timer, async calls.
|
||||
class PluginRootContext : public RootContext,
|
||||
public RouteRuleMatcher<RequestBlockConfigRule> {
|
||||
public:
|
||||
PluginRootContext(uint32_t id, std::string_view root_id)
|
||||
: RootContext(id, root_id) {}
|
||||
~PluginRootContext() {}
|
||||
bool onConfigure(size_t) override;
|
||||
bool checkHeader(const RequestBlockConfigRule&, bool&);
|
||||
bool checkBody(const RequestBlockConfigRule&, std::string_view);
|
||||
bool configure(size_t);
|
||||
|
||||
private:
|
||||
bool parsePluginConfig(const json&, RequestBlockConfigRule&) override;
|
||||
};
|
||||
|
||||
// Per-stream context.
|
||||
class PluginContext : public Context {
|
||||
public:
|
||||
explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {}
|
||||
FilterHeadersStatus onRequestHeaders(uint32_t, bool) override;
|
||||
FilterDataStatus onRequestBody(size_t, bool) override;
|
||||
|
||||
private:
|
||||
inline PluginRootContext* rootContext() {
|
||||
return dynamic_cast<PluginRootContext*>(this->root());
|
||||
}
|
||||
|
||||
size_t body_total_size_ = 0;
|
||||
bool check_body_ = false;
|
||||
std::optional<std::reference_wrapper<RequestBlockConfigRule>> config_;
|
||||
};
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace request_block
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
237
plugins/wasm-cpp/extensions/request_block/plugin_test.cc
Normal file
237
plugins/wasm-cpp/extensions/request_block/plugin_test.cc
Normal file
@@ -0,0 +1,237 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/request_block/plugin.h"
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "include/proxy-wasm/context.h"
|
||||
#include "include/proxy-wasm/null.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace request_block {
|
||||
|
||||
NullPluginRegistry* context_registry_;
|
||||
RegisterNullVmPluginFactory register_request_block_plugin(
|
||||
"request_block", []() {
|
||||
return std::make_unique<NullPlugin>(request_block::context_registry_);
|
||||
});
|
||||
|
||||
class MockContext : public proxy_wasm::ContextBase {
|
||||
public:
|
||||
MockContext(WasmBase* wasm) : ContextBase(wasm) {}
|
||||
|
||||
MOCK_METHOD(BufferInterface*, getBuffer, (WasmBufferType));
|
||||
MOCK_METHOD(WasmResult, log, (uint32_t, std::string_view));
|
||||
MOCK_METHOD(WasmResult, getHeaderMapValue,
|
||||
(WasmHeaderMapType /* type */, std::string_view /* key */,
|
||||
std::string_view* /*result */));
|
||||
MOCK_METHOD(WasmResult, getHeaderMapPairs, (WasmHeaderMapType, Pairs*));
|
||||
MOCK_METHOD(WasmResult, sendLocalResponse,
|
||||
(uint32_t /* response_code */, std::string_view /* body */,
|
||||
Pairs /* additional_headers */, uint32_t /* grpc_status */,
|
||||
std::string_view /* details */));
|
||||
MOCK_METHOD(WasmResult, getProperty, (std::string_view, std::string*));
|
||||
};
|
||||
|
||||
class RequestBlockTest : public ::testing::Test {
|
||||
protected:
|
||||
RequestBlockTest() {
|
||||
// Initialize test VM
|
||||
test_vm_ = createNullVm();
|
||||
wasm_base_ = std::make_unique<WasmBase>(
|
||||
std::move(test_vm_), "test-vm", "", "",
|
||||
std::unordered_map<std::string, std::string>{},
|
||||
AllowedCapabilitiesMap{});
|
||||
wasm_base_->load("request_block");
|
||||
wasm_base_->initialize();
|
||||
|
||||
// Initialize host side context
|
||||
mock_context_ = std::make_unique<MockContext>(wasm_base_.get());
|
||||
current_context_ = mock_context_.get();
|
||||
|
||||
ON_CALL(*mock_context_, log(testing::_, testing::_))
|
||||
.WillByDefault([](uint32_t, std::string_view m) {
|
||||
std::cerr << m << "\n";
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getHeaderMapValue(WasmHeaderMapType::RequestHeaders,
|
||||
testing::_, testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view header,
|
||||
std::string_view* result) {
|
||||
if (header == ":authority") {
|
||||
*result = authority_;
|
||||
}
|
||||
if (header == ":path") {
|
||||
*result = path_;
|
||||
}
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getProperty(testing::_, testing::_))
|
||||
.WillByDefault([&](std::string_view path, std::string* result) {
|
||||
*result = route_name_;
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_,
|
||||
getHeaderMapPairs(WasmHeaderMapType::RequestHeaders, testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, Pairs* result) {
|
||||
*result = headers_;
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getBuffer(testing::_))
|
||||
.WillByDefault([&](WasmBufferType type) {
|
||||
if (type == WasmBufferType::HttpRequestBody) {
|
||||
return &body_;
|
||||
}
|
||||
return &config_;
|
||||
});
|
||||
|
||||
// Initialize Wasm sandbox context
|
||||
root_context_ = std::make_unique<PluginRootContext>(0, "");
|
||||
context_ = std::make_unique<PluginContext>(1, root_context_.get());
|
||||
}
|
||||
~RequestBlockTest() override {}
|
||||
|
||||
std::unique_ptr<WasmBase> wasm_base_;
|
||||
std::unique_ptr<WasmVm> test_vm_;
|
||||
std::unique_ptr<MockContext> mock_context_;
|
||||
|
||||
std::unique_ptr<PluginRootContext> root_context_;
|
||||
std::unique_ptr<PluginContext> context_;
|
||||
|
||||
std::string authority_;
|
||||
std::string route_name_;
|
||||
std::string path_;
|
||||
Pairs headers_;
|
||||
BufferBase body_;
|
||||
BufferBase config_;
|
||||
};
|
||||
|
||||
TEST_F(RequestBlockTest, CaseSensitive) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"block_urls": ["?foo=bar", "swagger.html"],
|
||||
"block_headers": ["headerKey", "headerValue"],
|
||||
"block_bodys": ["Hello World"]
|
||||
})";
|
||||
|
||||
config_.set({configuration.data(), configuration.size()});
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
path_ = "/?foo=BAR";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
path_ = "/?foo=bar";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(403, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/swagger.html?foo=BAR";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(403, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "";
|
||||
headers_ = {{"headerKey", "123"}};
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(403, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
headers_ = {{"abc", "headerValue"}};
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(403, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
headers_ = {{"abc", "123"}};
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
body_.set("Hello World");
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(403, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestBody(11, true),
|
||||
FilterDataStatus::StopIterationNoBuffer);
|
||||
|
||||
body_.set("hello world");
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
EXPECT_EQ(context_->onRequestBody(11, true), FilterDataStatus::Continue);
|
||||
}
|
||||
|
||||
TEST_F(RequestBlockTest, CaseInsensitive) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"case_sensitive": false,
|
||||
"blocked_code": 404,
|
||||
"block_urls": ["?foo=bar", "swagger.html"],
|
||||
"block_headers": ["headerKey", "headerValue"],
|
||||
"block_bodys": ["Hello World"]
|
||||
})";
|
||||
|
||||
config_.set({configuration.data(), configuration.size()});
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
path_ = "/?foo=BAR";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(404, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/swagger.html?foo=bar";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(404, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "";
|
||||
headers_ = {{"headerkey", "123"}};
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(404, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
headers_ = {{"abc", "headervalue"}};
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(404, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
headers_ = {{"abc", "123"}};
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
body_.set("hello world");
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(404, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestBody(11, true),
|
||||
FilterDataStatus::StopIterationNoBuffer);
|
||||
}
|
||||
|
||||
} // namespace request_block
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
55
plugins/wasm-cpp/extensions/sni_misdirect/BUILD
Normal file
55
plugins/wasm-cpp/extensions/sni_misdirect/BUILD
Normal file
@@ -0,0 +1,55 @@
|
||||
load("@proxy_wasm_cpp_sdk//bazel/wasm:wasm.bzl", "wasm_cc_binary")
|
||||
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
|
||||
|
||||
wasm_cc_binary(
|
||||
name = "sni_misdirect.wasm",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
"plugin.h",
|
||||
],
|
||||
deps = [
|
||||
"@com_google_absl//absl/strings:str_format",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics",
|
||||
"//common:http_util",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "sni_misdirect_lib",
|
||||
srcs = [
|
||||
"plugin.cc",
|
||||
],
|
||||
hdrs = [
|
||||
"plugin.h",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
visibility = ["//visibility:public"],
|
||||
alwayslink = 1,
|
||||
deps = [
|
||||
"@com_google_absl//absl/strings:str_format",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
"//common:http_util",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "sni_misdirect_test",
|
||||
srcs = [
|
||||
"plugin_test.cc",
|
||||
],
|
||||
copts = ["-DNULL_PLUGIN"],
|
||||
deps = [
|
||||
":sni_misdirect_lib",
|
||||
"@com_google_absl//absl/strings:str_format",
|
||||
"@com_google_googletest//:gtest",
|
||||
"@com_google_googletest//:gtest_main",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
],
|
||||
)
|
||||
|
||||
declare_wasm_image_targets(
|
||||
name = "sni_misdirect",
|
||||
wasm_file = ":sni_misdirect.wasm",
|
||||
)
|
||||
111
plugins/wasm-cpp/extensions/sni_misdirect/plugin.cc
Normal file
111
plugins/wasm-cpp/extensions/sni_misdirect/plugin.cc
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/sni_misdirect/plugin.h"
|
||||
|
||||
#include "absl/strings/match.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "common/http_util.h"
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace sni_misdirect {
|
||||
|
||||
PROXY_WASM_NULL_PLUGIN_REGISTRY
|
||||
|
||||
NullPluginRegistry* context_registry_;
|
||||
RegisterNullVmPluginFactory register_sni_misdirect_plugin(
|
||||
"envoy.wasm.sni_misdirect", []() {
|
||||
return std::make_unique<NullPlugin>(sni_misdirect::context_registry_);
|
||||
});
|
||||
|
||||
#endif
|
||||
|
||||
static RegisterContextFactory register_SNIMisdirect(
|
||||
CONTEXT_FACTORY(PluginContext), ROOT_FACTORY(PluginRootContext));
|
||||
|
||||
namespace {
|
||||
|
||||
void misdirectedRequest() {
|
||||
sendLocalResponse(421, "Misdirected Request", "", {});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) {
|
||||
std::string protocol;
|
||||
// no need to check HTTP/1.0 and HTTP/1.1
|
||||
if (getValue({"request", "protocol"}, &protocol) &&
|
||||
absl::StartsWith(protocol, "HTTP/1")) {
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
// no need to check http scheme
|
||||
std::string scheme;
|
||||
if (getValue({"request", "scheme"}, &scheme) && scheme != "https") {
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
// no need to check grpc
|
||||
auto content_type_header =
|
||||
getRequestHeader(Wasm::Common::Http::Header::ContentType);
|
||||
auto content_type = content_type_header->view();
|
||||
auto grpc_value =
|
||||
absl::string_view(Wasm::Common::Http::ContentTypeValues::Grpc.data(),
|
||||
Wasm::Common::Http::ContentTypeValues::Grpc.size());
|
||||
if (absl::StartsWith(
|
||||
absl::string_view(content_type.data(), content_type.size()),
|
||||
grpc_value) &&
|
||||
(content_type.size() == grpc_value.size() ||
|
||||
content_type.at(grpc_value.size()) == '+')) {
|
||||
LOG_DEBUG("ignore grpc");
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
std::string sni;
|
||||
if (!getValue({"connection", "requested_server_name"}, &sni) || sni.empty()) {
|
||||
LOG_DEBUG("failed to get sni");
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
|
||||
auto host_header = getRequestHeader(":authority");
|
||||
auto host = host_header->view();
|
||||
if (host.empty()) {
|
||||
LOG_CRITICAL("failed to get authority");
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
host = Wasm::Common::Http::stripPortFromHost(host);
|
||||
LOG_DEBUG(absl::StrFormat("sni:%s authority:%s", sni, host));
|
||||
if (sni == host) {
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
auto isWildcardSNI = absl::StartsWith(sni, "*.");
|
||||
if (!isWildcardSNI) {
|
||||
misdirectedRequest();
|
||||
return FilterHeadersStatus::StopIteration;
|
||||
}
|
||||
if (!absl::StrContains(absl::string_view(host.data(), host.size()),
|
||||
sni.substr(1))) {
|
||||
misdirectedRequest();
|
||||
return FilterHeadersStatus::StopIteration;
|
||||
}
|
||||
return FilterHeadersStatus::Continue;
|
||||
}
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace sni_misdirect
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
60
plugins/wasm-cpp/extensions/sni_misdirect/plugin.h
Normal file
60
plugins/wasm-cpp/extensions/sni_misdirect/plugin.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#define ASSERT(_X) assert(_X)
|
||||
|
||||
#ifndef NULL_PLUGIN
|
||||
|
||||
#include "proxy_wasm_intrinsics.h"
|
||||
|
||||
#else
|
||||
|
||||
#include "include/proxy-wasm/null_plugin.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace sni_misdirect {
|
||||
|
||||
#endif
|
||||
|
||||
// PluginRootContext is the root context for all streams processed by the
|
||||
// thread. It has the same lifetime as the worker thread and acts as target for
|
||||
// interactions that outlives individual stream, e.g. timer, async calls.
|
||||
class PluginRootContext : public RootContext {
|
||||
public:
|
||||
PluginRootContext(uint32_t id, std::string_view root_id)
|
||||
: RootContext(id, root_id) {}
|
||||
~PluginRootContext() {}
|
||||
};
|
||||
|
||||
// Per-stream context.
|
||||
class PluginContext : public Context {
|
||||
public:
|
||||
explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {}
|
||||
FilterHeadersStatus onRequestHeaders(uint32_t, bool) override;
|
||||
};
|
||||
|
||||
#ifdef NULL_PLUGIN
|
||||
|
||||
} // namespace sni_misdirect
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
#endif
|
||||
197
plugins/wasm-cpp/extensions/sni_misdirect/plugin_test.cc
Normal file
197
plugins/wasm-cpp/extensions/sni_misdirect/plugin_test.cc
Normal file
@@ -0,0 +1,197 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include "extensions/sni_misdirect/plugin.h"
|
||||
|
||||
#include <initializer_list>
|
||||
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "include/proxy-wasm/context.h"
|
||||
#include "include/proxy-wasm/null.h"
|
||||
|
||||
namespace proxy_wasm {
|
||||
namespace null_plugin {
|
||||
namespace sni_misdirect {
|
||||
|
||||
NullPluginRegistry* context_registry_;
|
||||
RegisterNullVmPluginFactory register_sni_misdirect_plugin(
|
||||
"sni_misdirect", []() {
|
||||
return std::make_unique<NullPlugin>(sni_misdirect::context_registry_);
|
||||
});
|
||||
|
||||
class MockContext : public proxy_wasm::ContextBase {
|
||||
public:
|
||||
MockContext(WasmBase* wasm) : ContextBase(wasm) {}
|
||||
|
||||
MOCK_METHOD(BufferInterface*, getBuffer, (WasmBufferType));
|
||||
MOCK_METHOD(WasmResult, log, (uint32_t, std::string_view));
|
||||
MOCK_METHOD(WasmResult, getHeaderMapValue,
|
||||
(WasmHeaderMapType /* type */, std::string_view /* key */,
|
||||
std::string_view* /*result */));
|
||||
MOCK_METHOD(WasmResult, sendLocalResponse,
|
||||
(uint32_t /* response_code */, std::string_view /* body */,
|
||||
Pairs /* additional_headers */, uint32_t /* grpc_status */,
|
||||
std::string_view /* details */));
|
||||
MOCK_METHOD(WasmResult, getProperty, (std::string_view, std::string*));
|
||||
};
|
||||
|
||||
class SNIMisdirectTest : public ::testing::Test {
|
||||
protected:
|
||||
SNIMisdirectTest() {
|
||||
// Initialize test VM
|
||||
test_vm_ = createNullVm();
|
||||
wasm_base_ = std::make_unique<WasmBase>(
|
||||
std::move(test_vm_), "test-vm", "", "",
|
||||
std::unordered_map<std::string, std::string>{},
|
||||
AllowedCapabilitiesMap{});
|
||||
wasm_base_->load("sni_misdirect");
|
||||
wasm_base_->initialize();
|
||||
|
||||
// Initialize host side context
|
||||
mock_context_ = std::make_unique<MockContext>(wasm_base_.get());
|
||||
current_context_ = mock_context_.get();
|
||||
|
||||
ON_CALL(*mock_context_, log(testing::_, testing::_))
|
||||
.WillByDefault([](uint32_t, std::string_view m) {
|
||||
std::cerr << m << "\n";
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getHeaderMapValue(WasmHeaderMapType::RequestHeaders,
|
||||
testing::_, testing::_))
|
||||
.WillByDefault([&](WasmHeaderMapType, std::string_view header,
|
||||
std::string_view* result) {
|
||||
if (header == ":authority") {
|
||||
*result = authority_;
|
||||
}
|
||||
if (header == "content-type") {
|
||||
*result = content_type_;
|
||||
}
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
ON_CALL(*mock_context_, getProperty(testing::_, testing::_))
|
||||
.WillByDefault([&](std::string_view path, std::string* result) {
|
||||
if (path == absl::StrFormat("%s%c%s%c", "connection", 0,
|
||||
"requested_server_name", 0)) {
|
||||
*result = sni_;
|
||||
}
|
||||
if (path ==
|
||||
absl::StrFormat("%s%c%s%c", "request", 0, "protocol", 0)) {
|
||||
*result = protocol_;
|
||||
}
|
||||
if (path == absl::StrFormat("%s%c%s%c", "request", 0, "scheme", 0)) {
|
||||
*result = scheme_;
|
||||
}
|
||||
return WasmResult::Ok;
|
||||
});
|
||||
|
||||
// Initialize Wasm sandbox context
|
||||
root_context_ = std::make_unique<PluginRootContext>(0, "");
|
||||
context_ = std::make_unique<PluginContext>(1, root_context_.get());
|
||||
}
|
||||
~SNIMisdirectTest() override {}
|
||||
|
||||
std::unique_ptr<WasmBase> wasm_base_;
|
||||
std::unique_ptr<WasmVm> test_vm_;
|
||||
std::unique_ptr<MockContext> mock_context_;
|
||||
|
||||
std::unique_ptr<PluginRootContext> root_context_;
|
||||
std::unique_ptr<PluginContext> context_;
|
||||
|
||||
std::string protocol_ = "HTTP/2";
|
||||
std::string authority_;
|
||||
std::string sni_;
|
||||
std::string content_type_;
|
||||
std::string scheme_ = "https";
|
||||
};
|
||||
|
||||
TEST_F(SNIMisdirectTest, OnMatch) {
|
||||
authority_ = "a.example.com";
|
||||
sni_ = "b.example.com";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(421, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
authority_ = "a.example.com";
|
||||
sni_ = "a.example.com";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
authority_ = "a.example.com:80";
|
||||
sni_ = "a.example.com";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
authority_ = "a.test.com";
|
||||
sni_ = "*.example.com";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(421, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
authority_ = "a.example.com";
|
||||
sni_ = "*.example.com";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
authority_ = "a.example.com";
|
||||
sni_ = "b.example.com";
|
||||
protocol_ = "HTTP/1.1";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
authority_ = "a.example.com";
|
||||
sni_ = "";
|
||||
protocol_ = "HTTP/2";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
authority_ = "a.example.com";
|
||||
sni_ = "b.example.com";
|
||||
protocol_ = "HTTP/2";
|
||||
content_type_ = "application/grpc";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
authority_ = "a.example.com";
|
||||
sni_ = "b.example.com";
|
||||
protocol_ = "HTTP/2";
|
||||
content_type_ = "application/grpc+";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
authority_ = "a.example.com";
|
||||
sni_ = "b.example.com";
|
||||
protocol_ = "HTTP/2";
|
||||
content_type_ = "application/grpc-web";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(421, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
authority_ = "a.example.com";
|
||||
sni_ = "b.example.com";
|
||||
protocol_ = "HTTP/2";
|
||||
scheme_ = "http";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
}
|
||||
|
||||
} // namespace sni_misdirect
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
33
plugins/wasm-cpp/scripts/build_and_push.sh
Executable file
33
plugins/wasm-cpp/scripts/build_and_push.sh
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
|
||||
read -p "please enter the env(prod,pre): " env
|
||||
|
||||
repo=""
|
||||
|
||||
case $env in
|
||||
prod)
|
||||
repo="platform_wasm"
|
||||
echo "注意!正在操作生产环境"
|
||||
;;
|
||||
pre)
|
||||
repo="platform_wasm_pre"
|
||||
;;
|
||||
*)
|
||||
echo "unknown env: "$env
|
||||
exit
|
||||
esac
|
||||
|
||||
read -p "please enter the registry addr: " registry_addr
|
||||
read -p "please enter username: " username
|
||||
read -p "please enter password: " -s password
|
||||
|
||||
|
||||
plugins=("basic-auth" "bot-detect" "custom-response" "hmac-auth" "key-auth" "key-rate-limit" "request-block" "sni-misdirect" "jwt-auth")
|
||||
|
||||
for plugin in ${plugins[@]}; do
|
||||
dir_name=`echo $plugin | tr '-' '_'`
|
||||
bazel build //extensions/$dir_name:$dir_name.wasm
|
||||
oras push -u $username -p $password $registry_addr/$repo/$plugin:1.0.0 \
|
||||
config.json:application/vnd.module.wasm.config.v1+json \
|
||||
bazel-bin/extensions/$dir_name/$dir_name.wasm:application/vnd.module.wasm.content.layer.v1+wasm
|
||||
done
|
||||
49
plugins/wasm-go/README.md
Normal file
49
plugins/wasm-go/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
## Intro
|
||||
|
||||
This SDK is used to develop the WASM Plugins of MSE Gateway (powered by envoy).
|
||||
|
||||
## Requirements
|
||||
|
||||
(need support Go's type parameters)
|
||||
|
||||
Go version: >= 1.18
|
||||
|
||||
TinyGo version: >= 0.25.0
|
||||
|
||||
## Quick Examples
|
||||
|
||||
### wasm plugin config
|
||||
|
||||
```yaml
|
||||
# this config will take effect globally (all incoming requests are affected)
|
||||
block_urls:
|
||||
- "test"
|
||||
_rules_:
|
||||
# matching by route name takes effect
|
||||
- _match_route_:
|
||||
- route-a
|
||||
- route-b
|
||||
block_bodys:
|
||||
- "hello world"
|
||||
# matching by domain takes effect
|
||||
- _match_domain_:
|
||||
- "*.example.com"
|
||||
- test.com
|
||||
block_urls:
|
||||
- "swagger.html"
|
||||
block_bodys:
|
||||
- "hello world"
|
||||
```
|
||||
|
||||
### code
|
||||
|
||||
[request-block](example/request-block)
|
||||
|
||||
|
||||
### compile to wasm
|
||||
|
||||
```bash
|
||||
tinygo build -o main.wasm -scheduler=none -target=wasi ./main.go
|
||||
```
|
||||
|
||||
|
||||
17
plugins/wasm-go/example/hello-world/go.mod
Normal file
17
plugins/wasm-go/example/hello-world/go.mod
Normal file
@@ -0,0 +1,17 @@
|
||||
module github.com/mse-group/wasm-extensions-go/example/hello-world
|
||||
|
||||
go 1.18
|
||||
|
||||
replace github.com/mse-group/wasm-extensions-go => ../..
|
||||
|
||||
require (
|
||||
github.com/mse-group/wasm-extensions-go v0.0.0
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/tidwall/gjson v1.14.3 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
)
|
||||
14
plugins/wasm-go/example/hello-world/go.sum
Normal file
14
plugins/wasm-go/example/hello-world/go.sum
Normal file
@@ -0,0 +1,14 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c h1:OCUFXVGixHLfNjg6/QYEhv+jHJ5mRGhpEUVFv9eWPJE=
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c/go.mod h1:5t/pWFNJ9eMyu/K/Z+OeGhDJ9sN9eCo8fc2pyM/Qjg4=
|
||||
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
41
plugins/wasm-go/example/hello-world/main.go
Normal file
41
plugins/wasm-go/example/hello-world/main.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
|
||||
|
||||
"github.com/mse-group/wasm-extensions-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
wrapper.SetCtx(
|
||||
"hello-world",
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
type HelloWorldConfig struct {
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(contextID uint32, config HelloWorldConfig, needBody *bool, log wrapper.LogWrapper) types.Action {
|
||||
err := proxywasm.AddHttpRequestHeader("hello", "world")
|
||||
if err != nil {
|
||||
log.Critical("failed to set request header")
|
||||
}
|
||||
proxywasm.SendHttpResponse(200, nil, []byte("hello world"), -1)
|
||||
return types.ActionContinue
|
||||
}
|
||||
17
plugins/wasm-go/example/http-call/go.mod
Normal file
17
plugins/wasm-go/example/http-call/go.mod
Normal file
@@ -0,0 +1,17 @@
|
||||
module github.com/mse-group/wasm-extensions-go/example/http-call
|
||||
|
||||
go 1.18
|
||||
|
||||
replace github.com/mse-group/wasm-extensions-go => ../..
|
||||
|
||||
require (
|
||||
github.com/mse-group/wasm-extensions-go v0.0.0
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c
|
||||
github.com/tidwall/gjson v1.14.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
)
|
||||
14
plugins/wasm-go/example/http-call/go.sum
Normal file
14
plugins/wasm-go/example/http-call/go.sum
Normal file
@@ -0,0 +1,14 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c h1:OCUFXVGixHLfNjg6/QYEhv+jHJ5mRGhpEUVFv9eWPJE=
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c/go.mod h1:5t/pWFNJ9eMyu/K/Z+OeGhDJ9sN9eCo8fc2pyM/Qjg4=
|
||||
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
120
plugins/wasm-go/example/http-call/main.go
Normal file
120
plugins/wasm-go/example/http-call/main.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/mse-group/wasm-extensions-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
wrapper.SetCtx(
|
||||
"http-call",
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
type HttpCallConfig struct {
|
||||
client wrapper.HttpClient
|
||||
requestPath string
|
||||
bodyHeader string
|
||||
tokenHeader string
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *HttpCallConfig, log wrapper.LogWrapper) error {
|
||||
config.bodyHeader = json.Get("bodyHeader").String()
|
||||
if config.bodyHeader == "" {
|
||||
return errors.New("missing bodyHeader in config")
|
||||
}
|
||||
config.tokenHeader = json.Get("tokenHeader").String()
|
||||
if config.tokenHeader == "" {
|
||||
return errors.New("missing tokenHeader in config")
|
||||
}
|
||||
config.requestPath = json.Get("requestPath").String()
|
||||
if config.requestPath == "" {
|
||||
return errors.New("missing requestPath in config")
|
||||
}
|
||||
serviceSource := json.Get("serviceSource").String()
|
||||
serviceName := json.Get("serviceName").String()
|
||||
servicePort := json.Get("servicePort").Int()
|
||||
if serviceName == "" || servicePort == 0 {
|
||||
return errors.New("invalid service config")
|
||||
}
|
||||
switch serviceSource {
|
||||
case "k8s":
|
||||
namespace := json.Get("namespace").String()
|
||||
config.client = wrapper.NewClusterClient(wrapper.K8sCluster{
|
||||
ServiceName: serviceName,
|
||||
Namespace: namespace,
|
||||
Port: servicePort,
|
||||
})
|
||||
return nil
|
||||
case "nacos":
|
||||
namespace := json.Get("namespace").String()
|
||||
config.client = wrapper.NewClusterClient(wrapper.NacosCluster{
|
||||
ServiceName: serviceName,
|
||||
NamespaceID: namespace,
|
||||
Port: servicePort,
|
||||
})
|
||||
return nil
|
||||
case "ip":
|
||||
config.client = wrapper.NewClusterClient(wrapper.StaticIpCluster{
|
||||
ServiceName: serviceName,
|
||||
Port: servicePort,
|
||||
})
|
||||
return nil
|
||||
case "dns":
|
||||
domain := json.Get("domain").String()
|
||||
config.client = wrapper.NewClusterClient(wrapper.DnsCluster{
|
||||
ServiceName: serviceName,
|
||||
Port: servicePort,
|
||||
Domain: domain,
|
||||
})
|
||||
return nil
|
||||
default:
|
||||
return errors.New("unknown service source: " + serviceSource)
|
||||
}
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(contextID uint32, config HttpCallConfig, needBody *bool, log wrapper.LogWrapper) types.Action {
|
||||
config.client.Get(config.requestPath, nil,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
defer proxywasm.ResumeHttpRequest()
|
||||
if statusCode != http.StatusOK {
|
||||
log.Errorf("http call failed, status: %d", statusCode)
|
||||
proxywasm.SendHttpResponse(http.StatusInternalServerError, nil,
|
||||
[]byte("http call failed"), -1)
|
||||
return
|
||||
}
|
||||
// avoid protocol error
|
||||
body := strings.ReplaceAll(string(responseBody), "\n", "#")
|
||||
// set body to the original request header
|
||||
proxywasm.AddHttpRequestHeader(config.bodyHeader, body)
|
||||
// set service response token header to the original request header
|
||||
token := responseHeaders.Get(config.tokenHeader)
|
||||
if token != "" {
|
||||
proxywasm.AddHttpRequestHeader(config.tokenHeader, token)
|
||||
}
|
||||
})
|
||||
return types.ActionPause
|
||||
}
|
||||
17
plugins/wasm-go/example/request-block/go.mod
Normal file
17
plugins/wasm-go/example/request-block/go.mod
Normal file
@@ -0,0 +1,17 @@
|
||||
module github.com/mse-group/wasm-extensions-go/example/request-block
|
||||
|
||||
go 1.18
|
||||
|
||||
replace github.com/mse-group/wasm-extensions-go => ../..
|
||||
|
||||
require (
|
||||
github.com/mse-group/wasm-extensions-go v0.0.0
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c
|
||||
github.com/tidwall/gjson v1.14.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
)
|
||||
14
plugins/wasm-go/example/request-block/go.sum
Normal file
14
plugins/wasm-go/example/request-block/go.sum
Normal file
@@ -0,0 +1,14 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c h1:OCUFXVGixHLfNjg6/QYEhv+jHJ5mRGhpEUVFv9eWPJE=
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c/go.mod h1:5t/pWFNJ9eMyu/K/Z+OeGhDJ9sN9eCo8fc2pyM/Qjg4=
|
||||
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
153
plugins/wasm-go/example/request-block/main.go
Normal file
153
plugins/wasm-go/example/request-block/main.go
Normal file
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/mse-group/wasm-extensions-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
wrapper.SetCtx(
|
||||
"request-block",
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
|
||||
)
|
||||
}
|
||||
|
||||
type RequestBlockConfig struct {
|
||||
blockedCode uint32
|
||||
blockedMessage string
|
||||
caseSensitive bool
|
||||
blockUrls []string
|
||||
blockHeaders []string
|
||||
blockBodys []string
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *RequestBlockConfig, log wrapper.LogWrapper) error {
|
||||
code := json.Get("blocked_code").Int()
|
||||
if code != 0 && code > 100 && code < 600 {
|
||||
config.blockedCode = uint32(code)
|
||||
} else {
|
||||
config.blockedCode = 403
|
||||
}
|
||||
config.blockedMessage = json.Get("blocked_message").String()
|
||||
config.caseSensitive = json.Get("case_sensitive").Bool()
|
||||
for _, item := range json.Get("block_urls").Array() {
|
||||
url := item.String()
|
||||
if url == "" {
|
||||
continue
|
||||
}
|
||||
if config.caseSensitive {
|
||||
config.blockUrls = append(config.blockUrls, url)
|
||||
} else {
|
||||
config.blockUrls = append(config.blockUrls, strings.ToLower(url))
|
||||
}
|
||||
}
|
||||
for _, item := range json.Get("block_headers").Array() {
|
||||
header := item.String()
|
||||
if header == "" {
|
||||
continue
|
||||
}
|
||||
if config.caseSensitive {
|
||||
config.blockHeaders = append(config.blockHeaders, header)
|
||||
} else {
|
||||
config.blockHeaders = append(config.blockHeaders, strings.ToLower(header))
|
||||
}
|
||||
}
|
||||
for _, item := range json.Get("block_bodys").Array() {
|
||||
body := item.String()
|
||||
if body == "" {
|
||||
continue
|
||||
}
|
||||
if config.caseSensitive {
|
||||
config.blockBodys = append(config.blockBodys, body)
|
||||
} else {
|
||||
config.blockBodys = append(config.blockBodys, strings.ToLower(body))
|
||||
}
|
||||
}
|
||||
if len(config.blockUrls) == 0 && len(config.blockHeaders) == 0 &&
|
||||
len(config.blockBodys) == 0 {
|
||||
return errors.New("there is no block rules")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(contextID uint32, config RequestBlockConfig, needBody *bool, log wrapper.LogWrapper) types.Action {
|
||||
if len(config.blockUrls) > 0 {
|
||||
requestUrl, err := proxywasm.GetHttpRequestHeader(":path")
|
||||
if err != nil {
|
||||
log.Warnf("get path failed: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
if !config.caseSensitive {
|
||||
requestUrl = strings.ToLower(requestUrl)
|
||||
}
|
||||
for _, blockUrl := range config.blockUrls {
|
||||
if strings.Contains(requestUrl, blockUrl) {
|
||||
proxywasm.SendHttpResponse(config.blockedCode, nil, []byte(config.blockedMessage), -1)
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(config.blockHeaders) > 0 {
|
||||
headers, err := proxywasm.GetHttpRequestHeaders()
|
||||
if err != nil {
|
||||
log.Warnf("get headers failed: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
var headerPairs []string
|
||||
for _, kv := range headers {
|
||||
headerPairs = append(headerPairs, fmt.Sprintf("%s\n%s", kv[0], kv[1]))
|
||||
}
|
||||
headerStr := strings.Join(headerPairs, "\n")
|
||||
if !config.caseSensitive {
|
||||
headerStr = strings.ToLower(headerStr)
|
||||
}
|
||||
for _, blockHeader := range config.blockHeaders {
|
||||
if strings.Contains(headerStr, blockHeader) {
|
||||
proxywasm.SendHttpResponse(config.blockedCode, nil, []byte(config.blockedMessage), -1)
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(config.blockBodys) == 0 {
|
||||
*needBody = false
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func onHttpRequestBody(contextID uint32, config RequestBlockConfig, body []byte, log wrapper.LogWrapper) types.Action {
|
||||
bodyStr := string(body)
|
||||
if !config.caseSensitive {
|
||||
bodyStr = strings.ToLower(bodyStr)
|
||||
}
|
||||
for _, blockBody := range config.blockBodys {
|
||||
if strings.Contains(bodyStr, blockBody) {
|
||||
proxywasm.SendHttpResponse(config.blockedCode, nil, []byte(config.blockedMessage), -1)
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
return types.ActionContinue
|
||||
|
||||
}
|
||||
18
plugins/wasm-go/go.mod
Normal file
18
plugins/wasm-go/go.mod
Normal file
@@ -0,0 +1,18 @@
|
||||
module github.com/mse-group/wasm-extensions-go
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c
|
||||
github.com/tidwall/gjson v1.14.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
25
plugins/wasm-go/go.sum
Normal file
25
plugins/wasm-go/go.sum
Normal file
@@ -0,0 +1,25 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c h1:OCUFXVGixHLfNjg6/QYEhv+jHJ5mRGhpEUVFv9eWPJE=
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c/go.mod h1:5t/pWFNJ9eMyu/K/Z+OeGhDJ9sN9eCo8fc2pyM/Qjg4=
|
||||
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
196
plugins/wasm-go/pkg/matcher/rule_matcher.go
Normal file
196
plugins/wasm-go/pkg/matcher/rule_matcher.go
Normal file
@@ -0,0 +1,196 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
package matcher
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type Category int
|
||||
|
||||
const (
|
||||
Route Category = iota
|
||||
Host
|
||||
)
|
||||
|
||||
type MatchType int
|
||||
|
||||
const (
|
||||
Prefix MatchType = iota
|
||||
Exact
|
||||
Suffix
|
||||
)
|
||||
|
||||
const (
|
||||
RULES_KEY = "_rules_"
|
||||
MATCH_ROUTE_KEY = "_match_route_"
|
||||
MATCH_DOMAIN_KEY = "_match_domain_"
|
||||
)
|
||||
|
||||
type HostMatcher struct {
|
||||
matchType MatchType
|
||||
host string
|
||||
}
|
||||
|
||||
type RuleConfig[PluginConfig any] struct {
|
||||
category Category
|
||||
routes map[string]struct{}
|
||||
hosts []HostMatcher
|
||||
config PluginConfig
|
||||
}
|
||||
|
||||
type RuleMatcher[PluginConfig any] struct {
|
||||
ruleConfig []RuleConfig[PluginConfig]
|
||||
globalConfig PluginConfig
|
||||
hasGlobalConfig bool
|
||||
}
|
||||
|
||||
func (m RuleMatcher[PluginConfig]) GetMatchConfig() (*PluginConfig, error) {
|
||||
host, err := proxywasm.GetHttpRequestHeader(":authority")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
routeName, err := proxywasm.GetProperty([]string{"route_name"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, rule := range m.ruleConfig {
|
||||
if rule.category == Host {
|
||||
if m.hostMatch(rule, host) {
|
||||
return &rule.config, nil
|
||||
}
|
||||
}
|
||||
// category == Route
|
||||
if _, ok := rule.routes[string(routeName)]; ok {
|
||||
return &rule.config, nil
|
||||
}
|
||||
}
|
||||
if m.hasGlobalConfig {
|
||||
return &m.globalConfig, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *RuleMatcher[PluginConfig]) ParseRuleConfig(config gjson.Result,
|
||||
parsePluginConfig func(gjson.Result, *PluginConfig) error) error {
|
||||
var rules []gjson.Result
|
||||
obj := config.Map()
|
||||
keyCount := len(obj)
|
||||
if keyCount == 0 {
|
||||
// enable globally for empty config
|
||||
m.hasGlobalConfig = true
|
||||
return nil
|
||||
}
|
||||
if rulesJson, ok := obj[RULES_KEY]; ok {
|
||||
rules = rulesJson.Array()
|
||||
keyCount--
|
||||
}
|
||||
var pluginConfig PluginConfig
|
||||
if keyCount > 0 {
|
||||
err := parsePluginConfig(config, &pluginConfig)
|
||||
if err != nil {
|
||||
proxywasm.LogInfof("parse global config failed, err:%v", err)
|
||||
} else {
|
||||
m.globalConfig = pluginConfig
|
||||
m.hasGlobalConfig = true
|
||||
}
|
||||
}
|
||||
if len(rules) == 0 {
|
||||
if m.hasGlobalConfig {
|
||||
return nil
|
||||
}
|
||||
return errors.New("parse config failed, no valid rules")
|
||||
}
|
||||
for _, ruleJson := range rules {
|
||||
var rule RuleConfig[PluginConfig]
|
||||
err := parsePluginConfig(ruleJson, &rule.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rule.routes = m.parseRouteMatchConfig(ruleJson)
|
||||
rule.hosts = m.parseHostMatchConfig(ruleJson)
|
||||
noRoute := len(rule.routes) == 0
|
||||
noHosts := len(rule.hosts) == 0
|
||||
if (noRoute && noHosts) || (!noRoute && !noHosts) {
|
||||
return errors.New("there is only one of '_match_route_' and '_match_domain_' can present in configuration.")
|
||||
}
|
||||
if !noRoute {
|
||||
rule.category = Route
|
||||
} else {
|
||||
rule.category = Host
|
||||
}
|
||||
m.ruleConfig = append(m.ruleConfig, rule)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m RuleMatcher[PluginConfig]) parseRouteMatchConfig(config gjson.Result) map[string]struct{} {
|
||||
keys := config.Get(MATCH_ROUTE_KEY).Array()
|
||||
routes := make(map[string]struct{})
|
||||
for _, item := range keys {
|
||||
routeName := item.String()
|
||||
if routeName != "" {
|
||||
routes[routeName] = struct{}{}
|
||||
}
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
func (m RuleMatcher[PluginConfig]) parseHostMatchConfig(config gjson.Result) []HostMatcher {
|
||||
keys := config.Get(MATCH_DOMAIN_KEY).Array()
|
||||
var hostMatchers []HostMatcher
|
||||
for _, item := range keys {
|
||||
host := item.String()
|
||||
var hostMatcher HostMatcher
|
||||
if strings.HasPrefix(host, "*") {
|
||||
hostMatcher.matchType = Suffix
|
||||
hostMatcher.host = host[1:]
|
||||
} else if strings.HasSuffix(host, "*") {
|
||||
hostMatcher.matchType = Prefix
|
||||
hostMatcher.host = host[:len(host)-1]
|
||||
} else {
|
||||
hostMatcher.matchType = Exact
|
||||
hostMatcher.host = host
|
||||
}
|
||||
hostMatchers = append(hostMatchers, hostMatcher)
|
||||
}
|
||||
return hostMatchers
|
||||
}
|
||||
|
||||
func (m RuleMatcher[PluginConfig]) hostMatch(rule RuleConfig[PluginConfig], reqHost string) bool {
|
||||
for _, hostMatch := range rule.hosts {
|
||||
switch hostMatch.matchType {
|
||||
case Suffix:
|
||||
if strings.HasSuffix(reqHost, hostMatch.host) {
|
||||
return true
|
||||
}
|
||||
case Prefix:
|
||||
if strings.HasPrefix(reqHost, hostMatch.host) {
|
||||
return true
|
||||
}
|
||||
case Exact:
|
||||
if reqHost == hostMatch.host {
|
||||
return true
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
238
plugins/wasm-go/pkg/matcher/rule_matcher_test.go
Normal file
238
plugins/wasm-go/pkg/matcher/rule_matcher_test.go
Normal file
@@ -0,0 +1,238 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
package matcher
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type customConfig struct {
|
||||
name string
|
||||
age int64
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *customConfig) error {
|
||||
config.name = json.Get("name").String()
|
||||
config.age = json.Get("age").Int()
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestHostMatch(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
config RuleConfig[customConfig]
|
||||
host string
|
||||
result bool
|
||||
}{
|
||||
{
|
||||
name: "prefix",
|
||||
config: RuleConfig[customConfig]{
|
||||
hosts: []HostMatcher{
|
||||
{
|
||||
matchType: Prefix,
|
||||
host: "www.",
|
||||
},
|
||||
},
|
||||
},
|
||||
host: "www.test.com",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
name: "prefix failed",
|
||||
config: RuleConfig[customConfig]{
|
||||
hosts: []HostMatcher{
|
||||
{
|
||||
matchType: Prefix,
|
||||
host: "www.",
|
||||
},
|
||||
},
|
||||
},
|
||||
host: "test.com",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
name: "suffix",
|
||||
config: RuleConfig[customConfig]{
|
||||
hosts: []HostMatcher{
|
||||
{
|
||||
matchType: Suffix,
|
||||
host: ".example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
host: "www.example.com",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
name: "suffix failed",
|
||||
config: RuleConfig[customConfig]{
|
||||
hosts: []HostMatcher{
|
||||
{
|
||||
matchType: Suffix,
|
||||
host: ".example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
host: "example.com",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
name: "exact",
|
||||
config: RuleConfig[customConfig]{
|
||||
hosts: []HostMatcher{
|
||||
{
|
||||
matchType: Exact,
|
||||
host: "www.example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
host: "www.example.com",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
name: "exact failed",
|
||||
config: RuleConfig[customConfig]{
|
||||
hosts: []HostMatcher{
|
||||
{
|
||||
matchType: Exact,
|
||||
host: "www.example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
host: "example.com",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
name: "any",
|
||||
config: RuleConfig[customConfig]{
|
||||
hosts: []HostMatcher{
|
||||
{
|
||||
matchType: Suffix,
|
||||
host: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
host: "www.example.com",
|
||||
result: true,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
var m RuleMatcher[customConfig]
|
||||
assert.Equal(t, c.result, m.hostMatch(c.config, c.host))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRuleConfig(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
config string
|
||||
errMsg string
|
||||
expected RuleMatcher[customConfig]
|
||||
}{
|
||||
{
|
||||
name: "global config",
|
||||
config: `{"name":"john", "age":18}`,
|
||||
expected: RuleMatcher[customConfig]{
|
||||
globalConfig: customConfig{
|
||||
name: "john",
|
||||
age: 18,
|
||||
},
|
||||
hasGlobalConfig: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rules config",
|
||||
config: `{"_rules_":[{"_match_domain_":["*.example.com","www.*","*","www.abc.com"],"name":"john", "age":18},{"_match_route_":["test1","test2"],"name":"ann", "age":16}]}`,
|
||||
expected: RuleMatcher[customConfig]{
|
||||
ruleConfig: []RuleConfig[customConfig]{
|
||||
{
|
||||
category: Host,
|
||||
hosts: []HostMatcher{
|
||||
{
|
||||
matchType: Suffix,
|
||||
host: ".example.com",
|
||||
},
|
||||
{
|
||||
matchType: Prefix,
|
||||
host: "www.",
|
||||
},
|
||||
{
|
||||
matchType: Suffix,
|
||||
host: "",
|
||||
},
|
||||
{
|
||||
matchType: Exact,
|
||||
host: "www.abc.com",
|
||||
},
|
||||
},
|
||||
routes: map[string]struct{}{},
|
||||
config: customConfig{
|
||||
name: "john",
|
||||
age: 18,
|
||||
},
|
||||
},
|
||||
{
|
||||
category: Route,
|
||||
routes: map[string]struct{}{
|
||||
"test1": {},
|
||||
"test2": {},
|
||||
},
|
||||
config: customConfig{
|
||||
name: "ann",
|
||||
age: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no rule",
|
||||
config: `{"_rules_":[]}`,
|
||||
errMsg: "parse config failed, no valid rules",
|
||||
},
|
||||
{
|
||||
name: "invalid rule",
|
||||
config: `{"_rules_":[{"_match_domain_":["*"],"_match_route_":["test"]}]}`,
|
||||
errMsg: "there is only one of '_match_route_' and '_match_domain_' can present in configuration.",
|
||||
},
|
||||
{
|
||||
name: "invalid rule",
|
||||
config: `{"_rules_":[{"age":16}]}`,
|
||||
errMsg: "there is only one of '_match_route_' and '_match_domain_' can present in configuration.",
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
var actual RuleMatcher[customConfig]
|
||||
err := actual.ParseRuleConfig(gjson.Parse(c.config), parseConfig)
|
||||
if err != nil {
|
||||
if c.errMsg == "" {
|
||||
t.Errorf("parse failed: %v", err)
|
||||
}
|
||||
if err.Error() != c.errMsg {
|
||||
t.Errorf("expect err: %s, actual err: %s", c.errMsg,
|
||||
err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
assert.Equal(t, c.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
112
plugins/wasm-go/pkg/wrapper/cluster_wrapper.go
Normal file
112
plugins/wasm-go/pkg/wrapper/cluster_wrapper.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Cluster interface {
|
||||
ClusterName() string
|
||||
HostName() string
|
||||
}
|
||||
|
||||
type K8sCluster struct {
|
||||
ServiceName string
|
||||
Namespace string
|
||||
Port int64
|
||||
Version string
|
||||
Host string
|
||||
}
|
||||
|
||||
func (c K8sCluster) ClusterName() string {
|
||||
namespace := "default"
|
||||
if c.Namespace != "" {
|
||||
namespace = c.Namespace
|
||||
}
|
||||
return fmt.Sprintf("outbound|%d|%s|%s.%s.svc.cluster.local",
|
||||
c.Port, c.Version, c.ServiceName, namespace)
|
||||
}
|
||||
|
||||
func (c K8sCluster) HostName() string {
|
||||
if c.Host != "" {
|
||||
return c.Host
|
||||
}
|
||||
return fmt.Sprintf("%s.%s.svc.cluster.local", c.ServiceName, c.Namespace)
|
||||
}
|
||||
|
||||
type NacosCluster struct {
|
||||
ServiceName string
|
||||
// use DEFAULT-GROUP by default
|
||||
Group string
|
||||
NamespaceID string
|
||||
Port int64
|
||||
// set true if use edas/sae registry
|
||||
IsExtRegistry bool
|
||||
Version string
|
||||
Host string
|
||||
}
|
||||
|
||||
func (c NacosCluster) ClusterName() string {
|
||||
group := "DEFAULT-GROUP"
|
||||
if c.Group != "" {
|
||||
group = strings.ReplaceAll(c.Group, "_", "-")
|
||||
}
|
||||
tail := "nacos"
|
||||
if c.IsExtRegistry {
|
||||
tail += "-ext"
|
||||
}
|
||||
return fmt.Sprintf("outbound|%d|%s|%s.%s.%s.%s",
|
||||
c.Port, c.Version, c.ServiceName, group, c.NamespaceID, tail)
|
||||
}
|
||||
|
||||
func (c NacosCluster) HostName() string {
|
||||
if c.Host != "" {
|
||||
return c.Host
|
||||
}
|
||||
return c.ServiceName
|
||||
}
|
||||
|
||||
type StaticIpCluster struct {
|
||||
ServiceName string
|
||||
Port int64
|
||||
Host string
|
||||
}
|
||||
|
||||
func (c StaticIpCluster) ClusterName() string {
|
||||
return fmt.Sprintf("outbound|%d||%s.static", c.Port, c.ServiceName)
|
||||
}
|
||||
|
||||
func (c StaticIpCluster) HostName() string {
|
||||
if c.Host != "" {
|
||||
return c.Host
|
||||
}
|
||||
return c.ServiceName
|
||||
}
|
||||
|
||||
type DnsCluster struct {
|
||||
ServiceName string
|
||||
Domain string
|
||||
Port int64
|
||||
}
|
||||
|
||||
func (c DnsCluster) ClusterName() string {
|
||||
return fmt.Sprintf("outbound|%d||%s.dns", c.Port, c.ServiceName)
|
||||
}
|
||||
|
||||
func (c DnsCluster) HostName() string {
|
||||
return c.Domain
|
||||
}
|
||||
102
plugins/wasm-go/pkg/wrapper/cluster_wrapper_test.go
Normal file
102
plugins/wasm-go/pkg/wrapper/cluster_wrapper_test.go
Normal file
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestClusterAndHost(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
cluster Cluster
|
||||
expectCluster string
|
||||
expectHost string
|
||||
}{
|
||||
{
|
||||
name: "k8s",
|
||||
cluster: K8sCluster{
|
||||
ServiceName: "foo",
|
||||
Namespace: "bar",
|
||||
Port: 8080,
|
||||
Version: "1.0",
|
||||
},
|
||||
expectCluster: "outbound|8080|1.0|foo.bar.svc.cluster.local",
|
||||
expectHost: "foo.bar.svc.cluster.local",
|
||||
},
|
||||
{
|
||||
name: "k8s default",
|
||||
cluster: K8sCluster{
|
||||
ServiceName: "foo",
|
||||
Port: 8080,
|
||||
Host: "www.example.com",
|
||||
},
|
||||
expectCluster: "outbound|8080||foo.default.svc.cluster.local",
|
||||
expectHost: "www.example.com",
|
||||
},
|
||||
{
|
||||
name: "nacos",
|
||||
cluster: NacosCluster{
|
||||
ServiceName: "foo",
|
||||
Group: "DEFAULT_GROUP",
|
||||
NamespaceID: "xxxx",
|
||||
Port: 8080,
|
||||
Version: "1.0",
|
||||
},
|
||||
expectCluster: "outbound|8080|1.0|foo.DEFAULT-GROUP.xxxx.nacos",
|
||||
expectHost: "foo",
|
||||
},
|
||||
{
|
||||
name: "nacos ext",
|
||||
cluster: NacosCluster{
|
||||
ServiceName: "foo",
|
||||
NamespaceID: "xxxx",
|
||||
Port: 8080,
|
||||
IsExtRegistry: true,
|
||||
Host: "www.test.com",
|
||||
},
|
||||
expectCluster: "outbound|8080||foo.DEFAULT-GROUP.xxxx.nacos-ext",
|
||||
expectHost: "www.test.com",
|
||||
},
|
||||
{
|
||||
name: "static",
|
||||
cluster: StaticIpCluster{
|
||||
ServiceName: "foo",
|
||||
Port: 8080,
|
||||
Host: "www.test.com",
|
||||
},
|
||||
expectCluster: "outbound|8080||foo.static",
|
||||
expectHost: "www.test.com",
|
||||
},
|
||||
{
|
||||
name: "dns",
|
||||
cluster: DnsCluster{
|
||||
ServiceName: "foo",
|
||||
Port: 8080,
|
||||
Domain: "www.test.com",
|
||||
},
|
||||
expectCluster: "outbound|8080||foo.dns",
|
||||
expectHost: "www.test.com",
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
assert.Equal(t, c.expectCluster, c.cluster.ClusterName())
|
||||
assert.Equal(t, c.expectHost, c.cluster.HostName())
|
||||
})
|
||||
}
|
||||
}
|
||||
121
plugins/wasm-go/pkg/wrapper/http_wrapper.go
Normal file
121
plugins/wasm-go/pkg/wrapper/http_wrapper.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
|
||||
)
|
||||
|
||||
type ResponseCallback func(statusCode int, responseHeaders http.Header, responseBody []byte)
|
||||
|
||||
type HttpClient interface {
|
||||
Get(path string, headers [][2]string, cb ResponseCallback, timeoutMillisecond ...uint32) error
|
||||
Head(path string, headers [][2]string, cb ResponseCallback, timeoutMillisecond ...uint32) error
|
||||
Options(path string, headers [][2]string, cb ResponseCallback, timeoutMillisecond ...uint32) error
|
||||
Post(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
|
||||
Put(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
|
||||
Patch(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
|
||||
Delete(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
|
||||
Connect(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
|
||||
Trace(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
|
||||
}
|
||||
|
||||
type ClusterClient[C Cluster] struct {
|
||||
cluster C
|
||||
}
|
||||
|
||||
func NewClusterClient[C Cluster](cluster C) *ClusterClient[C] {
|
||||
return &ClusterClient[C]{cluster: cluster}
|
||||
}
|
||||
|
||||
func (c ClusterClient[C]) Get(path string, headers [][2]string, cb ResponseCallback, timeoutMillisecond ...uint32) error {
|
||||
return HttpCall(c.cluster, http.MethodGet, path, headers, nil, cb, timeoutMillisecond...)
|
||||
}
|
||||
func (c ClusterClient[C]) Head(path string, headers [][2]string, cb ResponseCallback, timeoutMillisecond ...uint32) error {
|
||||
return HttpCall(c.cluster, http.MethodHead, path, headers, nil, cb, timeoutMillisecond...)
|
||||
}
|
||||
func (c ClusterClient[C]) Options(path string, headers [][2]string, cb ResponseCallback, timeoutMillisecond ...uint32) error {
|
||||
return HttpCall(c.cluster, http.MethodOptions, path, headers, nil, cb, timeoutMillisecond...)
|
||||
}
|
||||
func (c ClusterClient[C]) Post(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error {
|
||||
return HttpCall(c.cluster, http.MethodPost, path, headers, body, cb, timeoutMillisecond...)
|
||||
}
|
||||
func (c ClusterClient[C]) Put(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error {
|
||||
return HttpCall(c.cluster, http.MethodPut, path, headers, body, cb, timeoutMillisecond...)
|
||||
}
|
||||
func (c ClusterClient[C]) Patch(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error {
|
||||
return HttpCall(c.cluster, http.MethodPatch, path, headers, body, cb, timeoutMillisecond...)
|
||||
}
|
||||
func (c ClusterClient[C]) Delete(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error {
|
||||
return HttpCall(c.cluster, http.MethodDelete, path, headers, body, cb, timeoutMillisecond...)
|
||||
}
|
||||
func (c ClusterClient[C]) Connect(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error {
|
||||
return HttpCall(c.cluster, http.MethodConnect, path, headers, body, cb, timeoutMillisecond...)
|
||||
}
|
||||
func (c ClusterClient[C]) Trace(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error {
|
||||
return HttpCall(c.cluster, http.MethodTrace, path, headers, body, cb, timeoutMillisecond...)
|
||||
}
|
||||
|
||||
func HttpCall(cluster Cluster, method, path string, headers [][2]string, body []byte,
|
||||
callback ResponseCallback, timeoutMillisecond ...uint32) error {
|
||||
for i := len(headers) - 1; i >= 0; i-- {
|
||||
key := headers[i][0]
|
||||
if key == ":method" || key == ":path" || key == ":authority" {
|
||||
headers = append(headers[:i], headers[i+1:]...)
|
||||
}
|
||||
}
|
||||
// default timeout is 500ms
|
||||
var timeout uint32 = 500
|
||||
if len(timeoutMillisecond) > 0 {
|
||||
timeout = timeoutMillisecond[0]
|
||||
}
|
||||
headers = append(headers, [2]string{":method", method}, [2]string{":path", path}, [2]string{":authority", cluster.HostName()})
|
||||
requestID := uuid.New().String()
|
||||
_, err := proxywasm.DispatchHttpCall(cluster.ClusterName(), headers, body, nil, timeout, func(numHeaders, bodySize, numTrailers int) {
|
||||
respBody, err := proxywasm.GetHttpCallResponseBody(0, bodySize)
|
||||
if err != nil {
|
||||
proxywasm.LogCriticalf("failed to get response body: %v", err)
|
||||
}
|
||||
respHeaders, err := proxywasm.GetHttpCallResponseHeaders()
|
||||
if err != nil {
|
||||
proxywasm.LogCriticalf("failed to get response headers: %v", err)
|
||||
}
|
||||
code := http.StatusBadGateway
|
||||
var normalResponse bool
|
||||
headers := make(http.Header)
|
||||
for _, h := range respHeaders {
|
||||
if h[0] == ":status" {
|
||||
code, err = strconv.Atoi(h[1])
|
||||
if err != nil {
|
||||
proxywasm.LogErrorf("failed to parse status: %v", err)
|
||||
code = http.StatusInternalServerError
|
||||
} else {
|
||||
normalResponse = true
|
||||
}
|
||||
}
|
||||
headers.Add(h[0], h[1])
|
||||
}
|
||||
proxywasm.LogDebugf("http call end, id: %s, code: %d, normal: %t, body: %s",
|
||||
requestID, code, normalResponse, respBody)
|
||||
callback(code, headers, respBody)
|
||||
})
|
||||
proxywasm.LogDebugf("http call start, id: %s, cluster: %+v, method: %s, path: %s, body: %s, timeout: %d",
|
||||
requestID, cluster, method, path, body, timeout)
|
||||
return err
|
||||
}
|
||||
120
plugins/wasm-go/pkg/wrapper/log_wrapper.go
Normal file
120
plugins/wasm-go/pkg/wrapper/log_wrapper.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
|
||||
)
|
||||
|
||||
type LogLevel uint32
|
||||
|
||||
const (
|
||||
LogLevelTrace LogLevel = iota
|
||||
LogLevelDebug
|
||||
LogLevelInfo
|
||||
LogLevelWarn
|
||||
LogLevelError
|
||||
LogLevelCritical
|
||||
)
|
||||
|
||||
type LogWrapper struct {
|
||||
pluginName string
|
||||
}
|
||||
|
||||
func (l LogWrapper) log(level LogLevel, msg string) {
|
||||
msg = fmt.Sprintf("[%s] %s", l.pluginName, msg)
|
||||
switch level {
|
||||
case LogLevelTrace:
|
||||
proxywasm.LogTrace(msg)
|
||||
case LogLevelDebug:
|
||||
proxywasm.LogDebug(msg)
|
||||
case LogLevelInfo:
|
||||
proxywasm.LogInfo(msg)
|
||||
case LogLevelWarn:
|
||||
proxywasm.LogWarn(msg)
|
||||
case LogLevelError:
|
||||
proxywasm.LogError(msg)
|
||||
case LogLevelCritical:
|
||||
proxywasm.LogCritical(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (l LogWrapper) logFormat(level LogLevel, format string, args ...interface{}) {
|
||||
format = fmt.Sprintf("[%s] %s", l.pluginName, format)
|
||||
switch level {
|
||||
case LogLevelTrace:
|
||||
proxywasm.LogTracef(format, args...)
|
||||
case LogLevelDebug:
|
||||
proxywasm.LogDebugf(format, args...)
|
||||
case LogLevelInfo:
|
||||
proxywasm.LogInfof(format, args...)
|
||||
case LogLevelWarn:
|
||||
proxywasm.LogWarnf(format, args...)
|
||||
case LogLevelError:
|
||||
proxywasm.LogErrorf(format, args...)
|
||||
case LogLevelCritical:
|
||||
proxywasm.LogCriticalf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l LogWrapper) Trace(msg string) {
|
||||
l.log(LogLevelTrace, msg)
|
||||
}
|
||||
|
||||
func (l LogWrapper) Tracef(format string, args ...interface{}) {
|
||||
l.logFormat(LogLevelTrace, format, args...)
|
||||
}
|
||||
|
||||
func (l LogWrapper) Debug(msg string) {
|
||||
l.log(LogLevelDebug, msg)
|
||||
}
|
||||
|
||||
func (l LogWrapper) Debugf(format string, args ...interface{}) {
|
||||
l.logFormat(LogLevelDebug, format, args...)
|
||||
}
|
||||
|
||||
func (l LogWrapper) Info(msg string) {
|
||||
l.log(LogLevelInfo, msg)
|
||||
}
|
||||
|
||||
func (l LogWrapper) Infof(format string, args ...interface{}) {
|
||||
l.logFormat(LogLevelInfo, format, args...)
|
||||
}
|
||||
|
||||
func (l LogWrapper) Warn(msg string) {
|
||||
l.log(LogLevelWarn, msg)
|
||||
}
|
||||
|
||||
func (l LogWrapper) Warnf(format string, args ...interface{}) {
|
||||
l.logFormat(LogLevelWarn, format, args...)
|
||||
}
|
||||
|
||||
func (l LogWrapper) Error(msg string) {
|
||||
l.log(LogLevelError, msg)
|
||||
}
|
||||
|
||||
func (l LogWrapper) Errorf(format string, args ...interface{}) {
|
||||
l.logFormat(LogLevelError, format, args...)
|
||||
}
|
||||
|
||||
func (l LogWrapper) Critical(msg string) {
|
||||
l.log(LogLevelCritical, msg)
|
||||
}
|
||||
|
||||
func (l LogWrapper) Criticalf(format string, args ...interface{}) {
|
||||
l.logFormat(LogLevelCritical, format, args...)
|
||||
}
|
||||
235
plugins/wasm-go/pkg/wrapper/plugin_wrapper.go
Normal file
235
plugins/wasm-go/pkg/wrapper/plugin_wrapper.go
Normal file
@@ -0,0 +1,235 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/mse-group/wasm-extensions-go/pkg/matcher"
|
||||
)
|
||||
|
||||
type ParseConfigFunc[PluginConfig any] func(json gjson.Result, config *PluginConfig, log LogWrapper) error
|
||||
type onHttpHeadersFunc[PluginConfig any] func(contextID uint32, config PluginConfig, needBody *bool, log LogWrapper) types.Action
|
||||
type onHttpBodyFunc[PluginConfig any] func(contextID uint32, config PluginConfig, body []byte, log LogWrapper) types.Action
|
||||
|
||||
type CommonVmCtx[PluginConfig any] struct {
|
||||
types.DefaultVMContext
|
||||
pluginName string
|
||||
log LogWrapper
|
||||
parseConfig ParseConfigFunc[PluginConfig]
|
||||
onHttpRequestHeaders onHttpHeadersFunc[PluginConfig]
|
||||
onHttpRequestBody onHttpBodyFunc[PluginConfig]
|
||||
onHttpResponseHeaders onHttpHeadersFunc[PluginConfig]
|
||||
onHttpResponseBody onHttpBodyFunc[PluginConfig]
|
||||
}
|
||||
|
||||
func SetCtx[PluginConfig any](pluginName string, setFuncs ...SetPluginFunc[PluginConfig]) {
|
||||
proxywasm.SetVMContext(NewCommonVmCtx(pluginName, setFuncs...))
|
||||
}
|
||||
|
||||
type SetPluginFunc[PluginConfig any] func(*CommonVmCtx[PluginConfig])
|
||||
|
||||
func ParseConfigBy[PluginConfig any](f ParseConfigFunc[PluginConfig]) SetPluginFunc[PluginConfig] {
|
||||
return func(ctx *CommonVmCtx[PluginConfig]) {
|
||||
ctx.parseConfig = f
|
||||
}
|
||||
}
|
||||
|
||||
func ProcessRequestHeadersBy[PluginConfig any](f onHttpHeadersFunc[PluginConfig]) SetPluginFunc[PluginConfig] {
|
||||
return func(ctx *CommonVmCtx[PluginConfig]) {
|
||||
ctx.onHttpRequestHeaders = f
|
||||
}
|
||||
}
|
||||
|
||||
func ProcessRequestBodyBy[PluginConfig any](f onHttpBodyFunc[PluginConfig]) SetPluginFunc[PluginConfig] {
|
||||
return func(ctx *CommonVmCtx[PluginConfig]) {
|
||||
ctx.onHttpRequestBody = f
|
||||
}
|
||||
}
|
||||
|
||||
func ProcessResponseHeadersBy[PluginConfig any](f onHttpHeadersFunc[PluginConfig]) SetPluginFunc[PluginConfig] {
|
||||
return func(ctx *CommonVmCtx[PluginConfig]) {
|
||||
ctx.onHttpResponseHeaders = f
|
||||
}
|
||||
}
|
||||
|
||||
func ProcessResponseBodyBy[PluginConfig any](f onHttpBodyFunc[PluginConfig]) SetPluginFunc[PluginConfig] {
|
||||
return func(ctx *CommonVmCtx[PluginConfig]) {
|
||||
ctx.onHttpResponseBody = f
|
||||
}
|
||||
}
|
||||
|
||||
func parseEmptyPluginConfig[PluginConfig any](gjson.Result, *PluginConfig, LogWrapper) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewCommonVmCtx[PluginConfig any](pluginName string, setFuncs ...SetPluginFunc[PluginConfig]) *CommonVmCtx[PluginConfig] {
|
||||
ctx := &CommonVmCtx[PluginConfig]{
|
||||
pluginName: pluginName,
|
||||
log: LogWrapper{pluginName},
|
||||
}
|
||||
for _, set := range setFuncs {
|
||||
set(ctx)
|
||||
}
|
||||
if ctx.parseConfig == nil {
|
||||
var config PluginConfig
|
||||
if unsafe.Sizeof(config) != 0 {
|
||||
msg := "the `parseConfig` is missing in NewCommonVmCtx's arguments"
|
||||
ctx.log.Critical(msg)
|
||||
panic(msg)
|
||||
}
|
||||
ctx.parseConfig = parseEmptyPluginConfig[PluginConfig]
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (ctx *CommonVmCtx[PluginConfig]) NewPluginContext(uint32) types.PluginContext {
|
||||
return &CommonPluginCtx[PluginConfig]{
|
||||
vm: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
type CommonPluginCtx[PluginConfig any] struct {
|
||||
types.DefaultPluginContext
|
||||
matcher.RuleMatcher[PluginConfig]
|
||||
vm *CommonVmCtx[PluginConfig]
|
||||
}
|
||||
|
||||
func (ctx *CommonPluginCtx[PluginConfig]) OnPluginStart(int) types.OnPluginStartStatus {
|
||||
data, err := proxywasm.GetPluginConfiguration()
|
||||
if err != nil && err != types.ErrorStatusNotFound {
|
||||
ctx.vm.log.Criticalf("error reading plugin configuration: %v", err)
|
||||
return types.OnPluginStartStatusFailed
|
||||
}
|
||||
if len(data) == 0 {
|
||||
ctx.vm.log.Warn("need config")
|
||||
return types.OnPluginStartStatusFailed
|
||||
}
|
||||
if !gjson.ValidBytes(data) {
|
||||
ctx.vm.log.Warnf("the plugin configuration is not a valid json: %s", string(data))
|
||||
return types.OnPluginStartStatusFailed
|
||||
|
||||
}
|
||||
jsonData := gjson.ParseBytes(data)
|
||||
err = ctx.ParseRuleConfig(jsonData, func(js gjson.Result, cfg *PluginConfig) error {
|
||||
return ctx.vm.parseConfig(js, cfg, ctx.vm.log)
|
||||
})
|
||||
if err != nil {
|
||||
ctx.vm.log.Warnf("parse rule config failed: %v", err)
|
||||
return types.OnPluginStartStatusFailed
|
||||
}
|
||||
return types.OnPluginStartStatusOK
|
||||
}
|
||||
|
||||
func (ctx *CommonPluginCtx[PluginConfig]) NewHttpContext(contextID uint32) types.HttpContext {
|
||||
httpCtx := &CommonHttpCtx[PluginConfig]{
|
||||
plugin: ctx,
|
||||
contextID: contextID,
|
||||
}
|
||||
if ctx.vm.onHttpRequestBody != nil {
|
||||
httpCtx.needRequestBody = true
|
||||
}
|
||||
if ctx.vm.onHttpResponseBody != nil {
|
||||
httpCtx.needResponseBody = true
|
||||
}
|
||||
return httpCtx
|
||||
}
|
||||
|
||||
type CommonHttpCtx[PluginConfig any] struct {
|
||||
types.DefaultHttpContext
|
||||
plugin *CommonPluginCtx[PluginConfig]
|
||||
config *PluginConfig
|
||||
needRequestBody bool
|
||||
needResponseBody bool
|
||||
requestBodySize int
|
||||
responseBodySize int
|
||||
contextID uint32
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
|
||||
config, err := ctx.plugin.GetMatchConfig()
|
||||
if err != nil {
|
||||
ctx.plugin.vm.log.Errorf("get match config failed, err:%v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
if config == nil {
|
||||
return types.ActionContinue
|
||||
}
|
||||
ctx.config = config
|
||||
if ctx.plugin.vm.onHttpRequestHeaders == nil {
|
||||
return types.ActionContinue
|
||||
}
|
||||
return ctx.plugin.vm.onHttpRequestHeaders(ctx.contextID, *config,
|
||||
&ctx.needRequestBody, ctx.plugin.vm.log)
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestBody(bodySize int, endOfStream bool) types.Action {
|
||||
if ctx.config == nil {
|
||||
return types.ActionContinue
|
||||
}
|
||||
if ctx.plugin.vm.onHttpRequestBody == nil {
|
||||
return types.ActionContinue
|
||||
}
|
||||
if !ctx.needRequestBody {
|
||||
return types.ActionContinue
|
||||
}
|
||||
ctx.requestBodySize += bodySize
|
||||
if !endOfStream {
|
||||
return types.ActionPause
|
||||
}
|
||||
body, err := proxywasm.GetHttpRequestBody(0, ctx.requestBodySize)
|
||||
if err != nil {
|
||||
ctx.plugin.vm.log.Warnf("get request body failed: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
return ctx.plugin.vm.onHttpRequestBody(ctx.contextID, *ctx.config, body, ctx.plugin.vm.log)
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
|
||||
if ctx.config == nil {
|
||||
return types.ActionContinue
|
||||
}
|
||||
if ctx.plugin.vm.onHttpResponseHeaders == nil {
|
||||
return types.ActionContinue
|
||||
}
|
||||
return ctx.plugin.vm.onHttpResponseHeaders(ctx.contextID, *ctx.config,
|
||||
&ctx.needResponseBody, ctx.plugin.vm.log)
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) OnHttpResponseBody(bodySize int, endOfStream bool) types.Action {
|
||||
if ctx.config == nil {
|
||||
return types.ActionContinue
|
||||
}
|
||||
if ctx.plugin.vm.onHttpResponseBody == nil {
|
||||
return types.ActionContinue
|
||||
}
|
||||
if !ctx.needResponseBody {
|
||||
return types.ActionContinue
|
||||
}
|
||||
ctx.responseBodySize += bodySize
|
||||
if !endOfStream {
|
||||
return types.ActionPause
|
||||
}
|
||||
body, err := proxywasm.GetHttpResponseBody(0, ctx.responseBodySize)
|
||||
if err != nil {
|
||||
ctx.plugin.vm.log.Warnf("get response body failed: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
return ctx.plugin.vm.onHttpResponseBody(ctx.contextID, *ctx.config, body, ctx.plugin.vm.log)
|
||||
}
|
||||
53
plugins/wasm-go/pkg/wrapper/request_wrapper.go
Normal file
53
plugins/wasm-go/pkg/wrapper/request_wrapper.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// 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.
|
||||
|
||||
package wrapper
|
||||
|
||||
import "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
|
||||
|
||||
func GetRequestScheme() string {
|
||||
scheme, err := proxywasm.GetHttpRequestHeader(":scheme")
|
||||
if err != nil {
|
||||
proxywasm.LogError("parse request scheme failed")
|
||||
return ""
|
||||
}
|
||||
return scheme
|
||||
}
|
||||
|
||||
func GetRequestHost() string {
|
||||
host, err := proxywasm.GetHttpRequestHeader(":authority")
|
||||
if err != nil {
|
||||
proxywasm.LogError("parse request host failed")
|
||||
return ""
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
func GetRequestPath() string {
|
||||
path, err := proxywasm.GetHttpRequestHeader(":path")
|
||||
if err != nil {
|
||||
proxywasm.LogError("parse request path failed")
|
||||
return ""
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func GetRequestMethod() string {
|
||||
method, err := proxywasm.GetHttpRequestHeader(":method")
|
||||
if err != nil {
|
||||
proxywasm.LogError("parse request path failed")
|
||||
return ""
|
||||
}
|
||||
return method
|
||||
}
|
||||
@@ -1,5 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
# WARNING: DO NOT EDIT, THIS FILE IS PROBABLY A COPY
|
||||
#
|
||||
# The original version of this file is located in the https://github.com/istio/common-files repo.
|
||||
# If you're looking at this file in a different repo and want to make a change, please go to the
|
||||
# common-files repo, make the change there and check it in. Then come back to this repo and run
|
||||
# "make update-common".
|
||||
|
||||
# Copyright Istio Authors. All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
# This script builds and version stamps the output
|
||||
|
||||
export GOPROXY="https://proxy.golang.com.cn,direct"
|
||||
|
||||
VERBOSE=${VERBOSE:-"0"}
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
# WARNING: DO NOT EDIT, THIS FILE IS PROBABLY A COPY
|
||||
#
|
||||
# The original version of this file is located in the https://github.com/istio/common-files repo.
|
||||
# If you're looking at this file in a different repo and want to make a change, please go to the
|
||||
# common-files repo, make the change there and check it in. Then come back to this repo and run
|
||||
# "make update-common".
|
||||
|
||||
# Copyright Istio Authors
|
||||
#
|
||||
# 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.
|
||||
|
||||
if BUILD_GIT_REVISION=$(git rev-parse HEAD 2> /dev/null); then
|
||||
if [[ -z "${IGNORE_DIRTY_TREE}" ]] && [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then
|
||||
BUILD_GIT_REVISION=${BUILD_GIT_REVISION}"-dirty"
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
# WARNING: DO NOT EDIT, THIS FILE IS PROBABLY A COPY
|
||||
#
|
||||
# The original version of this file is located in the https://github.com/istio/common-files repo.
|
||||
# If you're looking at this file in a different repo and want to make a change, please go to the
|
||||
# common-files repo, make the change there and check it in. Then come back to this repo and run
|
||||
# "make update-common".
|
||||
|
||||
# Copyright Istio Authors
|
||||
#
|
||||
# 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.
|
||||
|
||||
set -e
|
||||
|
||||
WD=$(dirname "$0")
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
#!/bin/bash
|
||||
# shellcheck disable=SC2034
|
||||
|
||||
# WARNING: DO NOT EDIT, THIS FILE IS PROBABLY A COPY
|
||||
#
|
||||
# The original version of this file is located in the https://github.com/istio/common-files repo.
|
||||
# If you're looking at this file in a different repo and want to make a change, please go to the
|
||||
# common-files repo, make the change there and check it in. Then come back to this repo and run
|
||||
# "make update-common".
|
||||
|
||||
# Copyright Istio Authors
|
||||
#
|
||||
# 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.
|
||||
|
||||
set -e
|
||||
|
||||
|
||||
Reference in New Issue
Block a user