mirror of
https://github.com/alibaba/higress.git
synced 2026-02-28 14:40:50 +08:00
Compare commits
22 Commits
v2.0.6-rc.
...
plugins/wa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce94c6e62d | ||
|
|
05f251e627 | ||
|
|
0259eaddbb | ||
|
|
cfa3baddf8 | ||
|
|
b1f625a652 | ||
|
|
fd1eb54f25 | ||
|
|
c7550e2d49 | ||
|
|
ba74f4bbb9 | ||
|
|
9e418dafd9 | ||
|
|
95523a1bc7 | ||
|
|
dcd8466127 | ||
|
|
cceae6ad2a | ||
|
|
32f9a5ff32 | ||
|
|
6f95297b80 | ||
|
|
95426d5ccf | ||
|
|
a05b6b1e9d | ||
|
|
d0628344da | ||
|
|
a1bf315b13 | ||
|
|
b3d9123d59 | ||
|
|
817061c6cc | ||
|
|
ea0d5e7564 | ||
|
|
2a89c3bb70 |
4
.github/workflows/build-and-test-plugin.yaml
vendored
4
.github/workflows/build-and-test-plugin.yaml
vendored
@@ -6,11 +6,15 @@ on:
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
- 'test/**'
|
||||
- 'helm/**'
|
||||
- 'Makefile.core.mk'
|
||||
pull_request:
|
||||
branches: [ "*" ]
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
- 'test/**'
|
||||
- 'helm/**'
|
||||
- 'Makefile.core.mk'
|
||||
workflow_dispatch: ~
|
||||
|
||||
jobs:
|
||||
|
||||
2
.github/workflows/deploy-to-oss.yaml
vendored
2
.github/workflows/deploy-to-oss.yaml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Download Helm Charts Index
|
||||
uses: doggycool/ossutil-github-action@master
|
||||
with:
|
||||
ossArgs: 'cp -r -u oss://higress-website-cn-hongkong/helm-charts/index.yaml ./artifact/'
|
||||
ossArgs: 'cp oss://higress-website-cn-hongkong/helm-charts/index.yaml ./artifact/'
|
||||
accessKey: ${{ secrets.ACCESS_KEYID }}
|
||||
accessSecret: ${{ secrets.ACCESS_KEYSECRET }}
|
||||
endpoint: oss-cn-hongkong.aliyuncs.com
|
||||
|
||||
@@ -12,6 +12,7 @@ header:
|
||||
- 'LICENSE'
|
||||
- 'api/**'
|
||||
- 'samples/**'
|
||||
- 'docs/**'
|
||||
- '.github/**'
|
||||
- '.licenserc.yaml'
|
||||
- 'helm/**'
|
||||
|
||||
@@ -188,7 +188,7 @@ install: pre-install
|
||||
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
|
||||
|
||||
ENVOY_LATEST_IMAGE_TAG ?= 958467a353d411ae3f06e03b096bfd342cddb2c6
|
||||
ISTIO_LATEST_IMAGE_TAG ?= 958467a353d411ae3f06e03b096bfd342cddb2c6
|
||||
ISTIO_LATEST_IMAGE_TAG ?= d9c728d3b01f64855e012b08d136e306f1160397
|
||||
|
||||
install-dev: pre-install
|
||||
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true'
|
||||
|
||||
@@ -341,7 +341,7 @@ type WasmPlugin struct {
|
||||
// Extended by Higress, matching rules take effect
|
||||
MatchRules []*MatchRule `protobuf:"bytes,102,rep,name=match_rules,json=matchRules,proto3" json:"match_rules,omitempty"`
|
||||
// disable the default config
|
||||
DefaultConfigDisable bool `protobuf:"varint,103,opt,name=default_config_disable,json=defaultConfigDisable,proto3" json:"default_config_disable,omitempty"`
|
||||
DefaultConfigDisable *wrappers.BoolValue `protobuf:"bytes,103,opt,name=default_config_disable,json=defaultConfigDisable,proto3" json:"default_config_disable,omitempty"`
|
||||
}
|
||||
|
||||
func (x *WasmPlugin) Reset() {
|
||||
@@ -467,11 +467,11 @@ func (x *WasmPlugin) GetMatchRules() []*MatchRule {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *WasmPlugin) GetDefaultConfigDisable() bool {
|
||||
func (x *WasmPlugin) GetDefaultConfigDisable() *wrappers.BoolValue {
|
||||
if x != nil {
|
||||
return x.DefaultConfigDisable
|
||||
}
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extended by Higress
|
||||
@@ -480,11 +480,11 @@ type MatchRule struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Ingress []string `protobuf:"bytes,1,rep,name=ingress,proto3" json:"ingress,omitempty"`
|
||||
Domain []string `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"`
|
||||
Config *_struct.Struct `protobuf:"bytes,3,opt,name=config,proto3" json:"config,omitempty"`
|
||||
ConfigDisable bool `protobuf:"varint,4,opt,name=config_disable,json=configDisable,proto3" json:"config_disable,omitempty"`
|
||||
Service []string `protobuf:"bytes,5,rep,name=service,proto3" json:"service,omitempty"`
|
||||
Ingress []string `protobuf:"bytes,1,rep,name=ingress,proto3" json:"ingress,omitempty"`
|
||||
Domain []string `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"`
|
||||
Config *_struct.Struct `protobuf:"bytes,3,opt,name=config,proto3" json:"config,omitempty"`
|
||||
ConfigDisable *wrappers.BoolValue `protobuf:"bytes,4,opt,name=config_disable,json=configDisable,proto3" json:"config_disable,omitempty"`
|
||||
Service []string `protobuf:"bytes,5,rep,name=service,proto3" json:"service,omitempty"`
|
||||
}
|
||||
|
||||
func (x *MatchRule) Reset() {
|
||||
@@ -540,11 +540,11 @@ func (x *MatchRule) GetConfig() *_struct.Struct {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MatchRule) GetConfigDisable() bool {
|
||||
func (x *MatchRule) GetConfigDisable() *wrappers.BoolValue {
|
||||
if x != nil {
|
||||
return x.ConfigDisable
|
||||
}
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MatchRule) GetService() []string {
|
||||
@@ -686,7 +686,7 @@ var file_extensions_v1alpha1_wasmplugin_proto_rawDesc = []byte{
|
||||
0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x22, 0x8d, 0x06, 0x0a, 0x0a, 0x57, 0x61, 0x73, 0x6d, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e,
|
||||
0x6f, 0x22, 0xa9, 0x06, 0x0a, 0x0a, 0x57, 0x61, 0x73, 0x6d, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e,
|
||||
0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75,
|
||||
0x72, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x03, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x53, 0x0a, 0x11, 0x69, 0x6d,
|
||||
@@ -731,52 +731,55 @@ var file_extensions_v1alpha1_wasmplugin_proto_rawDesc = []byte{
|
||||
0x73, 0x18, 0x66, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73,
|
||||
0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x61,
|
||||
0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x75, 0x6c, 0x65, 0x52,
|
||||
0x0a, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x64,
|
||||
0x0a, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x50, 0x0a, 0x16, 0x64,
|
||||
0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x64, 0x69,
|
||||
0x73, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x67, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x65, 0x66,
|
||||
0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c,
|
||||
0x65, 0x22, 0xaf, 0x01, 0x0a, 0x09, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x75, 0x6c, 0x65, 0x12,
|
||||
0x18, 0x0a, 0x07, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09,
|
||||
0x52, 0x07, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d,
|
||||
0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69,
|
||||
0x6e, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66,
|
||||
0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x64, 0x69, 0x73,
|
||||
0x61, 0x62, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66,
|
||||
0x69, 0x67, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72,
|
||||
0x76, 0x69, 0x63, 0x65, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76,
|
||||
0x69, 0x63, 0x65, 0x22, 0x41, 0x0a, 0x08, 0x56, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
|
||||
0x35, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x68,
|
||||
0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e,
|
||||
0x73, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x45, 0x6e, 0x76, 0x56, 0x61,
|
||||
0x72, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x22, 0x7e, 0x0a, 0x06, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x72,
|
||||
0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
||||
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x4a, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x72,
|
||||
0x6f, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65,
|
||||
0x73, 0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31,
|
||||
0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x53,
|
||||
0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x46, 0x72, 0x6f, 0x6d,
|
||||
0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2a, 0x45, 0x0a, 0x0b, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e,
|
||||
0x50, 0x68, 0x61, 0x73, 0x65, 0x12, 0x15, 0x0a, 0x11, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49,
|
||||
0x46, 0x49, 0x45, 0x44, 0x5f, 0x50, 0x48, 0x41, 0x53, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05,
|
||||
0x41, 0x55, 0x54, 0x48, 0x4e, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x55, 0x54, 0x48, 0x5a,
|
||||
0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x54, 0x53, 0x10, 0x03, 0x2a, 0x42, 0x0a,
|
||||
0x0a, 0x50, 0x75, 0x6c, 0x6c, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x16, 0x0a, 0x12, 0x55,
|
||||
0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43,
|
||||
0x59, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x66, 0x4e, 0x6f, 0x74, 0x50, 0x72, 0x65, 0x73,
|
||||
0x65, 0x6e, 0x74, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x10,
|
||||
0x02, 0x2a, 0x26, 0x0a, 0x0e, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x53, 0x6f, 0x75,
|
||||
0x72, 0x63, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x49, 0x4e, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x00, 0x12,
|
||||
0x08, 0x0a, 0x04, 0x48, 0x4f, 0x53, 0x54, 0x10, 0x01, 0x2a, 0x2d, 0x0a, 0x0c, 0x46, 0x61, 0x69,
|
||||
0x6c, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x0e, 0x0a, 0x0a, 0x46, 0x41, 0x49,
|
||||
0x4c, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x41, 0x49,
|
||||
0x4c, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x01, 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68,
|
||||
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x2f, 0x68,
|
||||
0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x65, 0x78, 0x74, 0x65, 0x6e,
|
||||
0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x73, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f,
|
||||
0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x14, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74,
|
||||
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x22, 0xcb, 0x01,
|
||||
0x0a, 0x09, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x69,
|
||||
0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x69, 0x6e,
|
||||
0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18,
|
||||
0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x2f, 0x0a,
|
||||
0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e,
|
||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
|
||||
0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x41,
|
||||
0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
|
||||
0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c,
|
||||
0x75, 0x65, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c,
|
||||
0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x05, 0x20, 0x03,
|
||||
0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0x41, 0x0a, 0x08, 0x56,
|
||||
0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x35, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x01,
|
||||
0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x65,
|
||||
0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68,
|
||||
0x61, 0x31, 0x2e, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x72, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x22, 0x7e,
|
||||
0x0a, 0x06, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x4a, 0x0a, 0x0a,
|
||||
0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e,
|
||||
0x32, 0x2b, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e,
|
||||
0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x45,
|
||||
0x6e, 0x76, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x76,
|
||||
0x61, 0x6c, 0x75, 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2a, 0x45,
|
||||
0x0a, 0x0b, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x50, 0x68, 0x61, 0x73, 0x65, 0x12, 0x15, 0x0a,
|
||||
0x11, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x5f, 0x50, 0x48, 0x41,
|
||||
0x53, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x55, 0x54, 0x48, 0x4e, 0x10, 0x01, 0x12,
|
||||
0x09, 0x0a, 0x05, 0x41, 0x55, 0x54, 0x48, 0x5a, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54,
|
||||
0x41, 0x54, 0x53, 0x10, 0x03, 0x2a, 0x42, 0x0a, 0x0a, 0x50, 0x75, 0x6c, 0x6c, 0x50, 0x6f, 0x6c,
|
||||
0x69, 0x63, 0x79, 0x12, 0x16, 0x0a, 0x12, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49,
|
||||
0x45, 0x44, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x49,
|
||||
0x66, 0x4e, 0x6f, 0x74, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x10, 0x01, 0x12, 0x0a, 0x0a,
|
||||
0x06, 0x41, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x10, 0x02, 0x2a, 0x26, 0x0a, 0x0e, 0x45, 0x6e, 0x76,
|
||||
0x56, 0x61, 0x6c, 0x75, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x49,
|
||||
0x4e, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x4f, 0x53, 0x54, 0x10,
|
||||
0x01, 0x2a, 0x2d, 0x0a, 0x0c, 0x46, 0x61, 0x69, 0x6c, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67,
|
||||
0x79, 0x12, 0x0e, 0x0a, 0x0a, 0x46, 0x41, 0x49, 0x4c, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10,
|
||||
0x00, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x41, 0x49, 0x4c, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x01,
|
||||
0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61,
|
||||
0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x2f, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2f, 0x61,
|
||||
0x70, 0x69, 0x2f, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31,
|
||||
0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -804,6 +807,7 @@ var file_extensions_v1alpha1_wasmplugin_proto_goTypes = []interface{}{
|
||||
(*EnvVar)(nil), // 7: higress.extensions.v1alpha1.EnvVar
|
||||
(*_struct.Struct)(nil), // 8: google.protobuf.Struct
|
||||
(*wrappers.Int32Value)(nil), // 9: google.protobuf.Int32Value
|
||||
(*wrappers.BoolValue)(nil), // 10: google.protobuf.BoolValue
|
||||
}
|
||||
var file_extensions_v1alpha1_wasmplugin_proto_depIdxs = []int32{
|
||||
1, // 0: higress.extensions.v1alpha1.WasmPlugin.image_pull_policy:type_name -> higress.extensions.v1alpha1.PullPolicy
|
||||
@@ -814,14 +818,16 @@ var file_extensions_v1alpha1_wasmplugin_proto_depIdxs = []int32{
|
||||
6, // 5: higress.extensions.v1alpha1.WasmPlugin.vm_config:type_name -> higress.extensions.v1alpha1.VmConfig
|
||||
8, // 6: higress.extensions.v1alpha1.WasmPlugin.default_config:type_name -> google.protobuf.Struct
|
||||
5, // 7: higress.extensions.v1alpha1.WasmPlugin.match_rules:type_name -> higress.extensions.v1alpha1.MatchRule
|
||||
8, // 8: higress.extensions.v1alpha1.MatchRule.config:type_name -> google.protobuf.Struct
|
||||
7, // 9: higress.extensions.v1alpha1.VmConfig.env:type_name -> higress.extensions.v1alpha1.EnvVar
|
||||
2, // 10: higress.extensions.v1alpha1.EnvVar.value_from:type_name -> higress.extensions.v1alpha1.EnvValueSource
|
||||
11, // [11:11] is the sub-list for method output_type
|
||||
11, // [11:11] is the sub-list for method input_type
|
||||
11, // [11:11] is the sub-list for extension type_name
|
||||
11, // [11:11] is the sub-list for extension extendee
|
||||
0, // [0:11] is the sub-list for field type_name
|
||||
10, // 8: higress.extensions.v1alpha1.WasmPlugin.default_config_disable:type_name -> google.protobuf.BoolValue
|
||||
8, // 9: higress.extensions.v1alpha1.MatchRule.config:type_name -> google.protobuf.Struct
|
||||
10, // 10: higress.extensions.v1alpha1.MatchRule.config_disable:type_name -> google.protobuf.BoolValue
|
||||
7, // 11: higress.extensions.v1alpha1.VmConfig.env:type_name -> higress.extensions.v1alpha1.EnvVar
|
||||
2, // 12: higress.extensions.v1alpha1.EnvVar.value_from:type_name -> higress.extensions.v1alpha1.EnvValueSource
|
||||
13, // [13:13] is the sub-list for method output_type
|
||||
13, // [13:13] is the sub-list for method input_type
|
||||
13, // [13:13] is the sub-list for extension type_name
|
||||
13, // [13:13] is the sub-list for extension extendee
|
||||
0, // [0:13] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_extensions_v1alpha1_wasmplugin_proto_init() }
|
||||
|
||||
@@ -112,7 +112,7 @@ message WasmPlugin {
|
||||
// Extended by Higress, matching rules take effect
|
||||
repeated MatchRule match_rules = 102;
|
||||
// disable the default config
|
||||
bool default_config_disable = 103;
|
||||
google.protobuf.BoolValue default_config_disable = 103;
|
||||
}
|
||||
|
||||
// Extended by Higress
|
||||
@@ -120,7 +120,7 @@ message MatchRule {
|
||||
repeated string ingress = 1;
|
||||
repeated string domain = 2;
|
||||
google.protobuf.Struct config = 3;
|
||||
bool config_disable = 4;
|
||||
google.protobuf.BoolValue config_disable = 4;
|
||||
repeated string service = 5;
|
||||
}
|
||||
|
||||
|
||||
143
docs/architecture.md
Normal file
143
docs/architecture.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Higress 核心组件和原理
|
||||
|
||||
Higress 是基于 Envoy 和 Istio 进行二次定制化开发构建和功能增强,同时利用 Envoy 和 Istio 一些插件机制,实现了一个轻量级的网关服务。其包括 3 个核心组件:Higress Controller(控制器)、Higress Gateway(网关)和 Higress Console(控制台)。
|
||||
下图概况了其核心工作流程:
|
||||
|
||||

|
||||
|
||||
本章将重点介绍 Higress 的两个核心组件:Higress Controller 和 Higress Gateway。
|
||||
|
||||
## 1 Higress Console
|
||||
|
||||
Higress Console 是 Higress 网关的管理控制台,主要功能是管理 Higress 网关的路由配置、插件配置等。
|
||||
|
||||
### 1.1 Higress Admin SDK
|
||||
|
||||
Higress Admin SDK 脱胎于 Higress Console。起初,它作为 Higress Console 的一部分,为前端界面提供实际的功能支持。后来考虑到对接外部系统等需求,将配置管理的部分剥离出来,形成一个独立的逻辑组件,便于和各个系统进行对接。目前支持服务来源管理、服务管理、路由管理、域名管理、证书管理、插件管理等功能。
|
||||
Higress Admin SDK 现在只提供 Java 版本,且要求 JDK 版本不低于 17。具体如何集成请参考 Higress 官方 BLOG [如何使用 Higress Admin SDK 进行配置管理](https://higress.io/zh-cn/blog/admin-sdk-intro)。
|
||||
|
||||
## 2 Higress Controller
|
||||
|
||||
Higress Controller(控制器) 是 Higress 的核心组件,其功能主要是实现 Higress 网关的服务发现、动态配置管理,以及动态下发配置给数据面。Higress Controller 内部包含两个子组件:Discovery 和 Higress Core。
|
||||
|
||||
### 2.1 Discovery 组件
|
||||
|
||||
Discovery 组件(Istio Pilot-Discovery)是 Istio 的核心组件,负责服务发现、配置管理、证书签发、控制面和数据面之间的通讯和配置下发等。Discovery 内部结构比较复杂,本文只介绍 Discovery 配置管理和服务发现的基本原理,其核心功能的详细介绍可以参考赵化冰老师的 BLOG [Istio Pilot 组件介绍](https://www.zhaohuabing.com/post/2019-10-21-pilot-discovery-code-analysis/)。
|
||||
Discovery 将 Kubernetes Service、Gateway API 配置等转换成 Istio 配置,然后将所有 Istio 配置合并转成符合 xDS 接口规范的数据结构,通过 GRPC 下发到数据面的 Envoy。其工作原理如下图:
|
||||
|
||||

|
||||
|
||||
#### 2.1.1 Config Controller
|
||||
|
||||
Discovery 为了更好管理 Istio 配置来源,提供 `Config Controller` 用于管理各种配置来源,目前支持 4 种类型的 `Config Controller`:
|
||||
|
||||
- Kubernetes:使用 Kubernetes 作为配置信息来源,该方式的直接依赖 Kubernetes 强大的 CRD 机制来存储配置信息,简单方便,是 Istio 最开始使用的配置信息存储方案, 其中包括 `Kubernetes Controller` 和 `Gateway API Controller` 两个实现。
|
||||
- MCP(Mesh Configuration Protocol):使用 Kubernetes 存储配置数据导致了 Istio 和 Kubernetes 的耦合,限制了 Istio 在非 Kubernetes 环境下的运用。为了解决该耦合,Istio 社区提出了 MCP。
|
||||
- Memory:一个基于内存的 Config Controller 实现,主要用于测试。
|
||||
- File:一个基于文件的 Config Controller 实现,主要用于测试。
|
||||
|
||||
1. Istio 配置
|
||||
|
||||
Istio 配置包括:`Gateway`、`VirtualService`、`DestinationRule`、`ServiceEntry`、`EnvoyFilter`、`WasmPlugin`、`WorkloadEntry`、`WorkloadGroup` 等,可以参考 Istio 官方文档[流量管理](https://istio.io/latest/zh/docs/reference/config/networking/)了解更多配置信息。
|
||||
|
||||
2. Gateway API 配置
|
||||
|
||||
Gateway API 配置包括:`GatewayClass`、`Gateway`、`HttpRoute`、`TCPRoute`、`GRPCRoute` 等, 可以参考 Gateway API 官方文档 [Gateway API](https://gateway-api.sigs.k8s.io/api-types/gateway/) 了解更多配置信息。
|
||||
|
||||
3. MCP over xDS
|
||||
|
||||
Discovery 作为 MCP Client,任何实现了 MCP 协议的 Server 都可以通过 MCP 协议向 Discovery 下发配置信息,从而消除了 Istio 和 Kubernetes 之间的耦合, 同时使 Istio 的配置信息处理更加灵活和可扩展。
|
||||
同时 MCP 是一种基于 xDS 协议的配置管理协议,Higress Core 通过实现 MCP 协议,使 Higress Core 成为 Discovery 的 Istio 配置来源。
|
||||
|
||||
4. Config Controller 来源配置
|
||||
|
||||
在 `higress-system` 命名空间中,名为 `higress-config` 的 Configmap 中,`mesh` 配置项包含一个 `configSources` 属性用于配置来源。其 Configmap 部分配置项如下:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: higress-config
|
||||
namespace: higress-system
|
||||
data:
|
||||
mesh: |-
|
||||
accessLogEncoding: TEXT
|
||||
...
|
||||
configSources:
|
||||
- address: xds://127.0.0.1:15051
|
||||
- address: k8s://
|
||||
...
|
||||
meshNetworks: "networks: {}"
|
||||
```
|
||||
|
||||
#### 2.1.2 Service Controller
|
||||
|
||||
`Service Controller` 用于管理各种 `Service Registry`,提供服务发现数据,目前 Istio 支持的 `Service Registry` 主要包括:
|
||||
|
||||
- Kubernetes:对接 Kubernetes Registry,可以将 Kubernetes 中定义的 Service 和 Endpoint 采集到 Istio 中。
|
||||
- Memory:一个基于内存的 Service Controller 实现,主要用于测试。
|
||||
|
||||
### 2.2 Higress Core 组件
|
||||
|
||||
Higress Core 核心逻辑如下图:
|
||||
|
||||

|
||||
|
||||
|
||||
Higress Core 内部包含两个核心子组件: Ingress Config 和 Cert Server。
|
||||
|
||||
#### 2.2.1 Ingress Config
|
||||
|
||||
Ingress Config 包含 6 个控制器,各自负责不同的功能:
|
||||
|
||||
- Ingress Controller:监听 Ingress 资源,将 Ingress 转换为 Istio 的 Gateway、VirtualService、DestinationRule 等资源。
|
||||
- Gateway Controller:监听 Gateway、VirtualService、DestinationRule 等资源。
|
||||
- McpBridge Controller:根据 McpBridge 的配置,将来自 Nacos、Eureka、Consul、Zookeeper 等外部注册中心或 DNS 的服务信息转换成 Istio ServiceEntry 资源。
|
||||
- Http2Rpc Controller:监听 Http2Rpc 资源,实现 HTTP 协议到 RPC 协议的转换。用户可以通过配置协议转换,将 RPC 服务以 HTTP 接口的形式暴露,从而使用 HTTP 请求调用 RPC 接口。
|
||||
- WasmPlugin Controller:监听 WasmPlugin 资源,将 Higress WasmPlugin 转化为 Istio WasmPlugin。Higress WasmPlugin 在 Istio WasmPlugin 的基础上进行了扩展,支持全局、路由、域名、服务级别的配置。
|
||||
- ConfigmapMgr:监听 Higress 的全局配置 `higress-config` ConfigMap,可以根据 tracing、gzip 等配置构造 EnvoyFilter。
|
||||
|
||||
#### 2.2.2 Cert Server
|
||||
|
||||
Cert Server 管理 Secret 资源和证书自动签发。
|
||||
|
||||
## 3 Higress Gateway
|
||||
|
||||
Higress Gateway 内部包含两个子组件:Pilot Agent 和 Envoy。Pilot Agent 主要负责 Envoy 的启动和配置,同时代理 Envoy xDS 请求到 Discovery。 Envoy 作为数据面,负责接收控制面的配置下发,并代理请求到业务服务。 Pilot Agent 和 Envoy 之间通讯协议是使用 xDS 协议, 通过 Unix Domain Socket(UDS)进行通信。
|
||||
Envoy 核心架构如下图:
|
||||
|
||||

|
||||
|
||||
### 1 Envoy 核心组件
|
||||
|
||||
- 下游(Downstream):
|
||||
下游是 Envoy 的客户端,它们负责发起请求并接收 Envoy 的响应。下游通常是最终用户的设备或服务,它们通过 Envoy 代理与后端服务进行通信。
|
||||
|
||||
- 上游(Upstream):
|
||||
上游是 Envoy 的后端服务器,它们接收 Envoy 代理的连接和请求。上游提供服务或数据,对来自下游客户端的请求进行处理并返回响应。
|
||||
|
||||
- 监听器(Listener):
|
||||
监听器是可以接受来自下游客户端连接的网络地址(如 IP 地址和端口,Unix Domain Socket 等)。Envoy 支持在单个进程中配置任意数量的监听器。监听器可以通过 `Listener Discovery Service(LDS)`来动态发现和更新。
|
||||
|
||||
- 路由(Router):
|
||||
路由器是 Envoy 中连接下游和上游的桥梁。它负责决定如何将监听器接收到的请求路由到适当的集群。路由器根据配置的路由规则,如路径、HTTP 标头 等,来确定请求的目标集群,从而实现精确的流量控制和路由。路由器可以通过 `Route Discovery Service(RDS)`来动态发现和更新。
|
||||
|
||||
- 集群(Cluster):
|
||||
集群是一组逻辑上相似的服务提供者的集合。集群成员的选择由负载均衡策略决定,确保请求能够均匀或按需分配到不同的服务实例。集群可以通过 `Cluster Discovery Service(CDS)`来动态发现和更新。
|
||||
|
||||
- 端点(Endpoint):
|
||||
端点是上游集群中的具体服务实例,可以是 IP 地址和端口号的组合。端点可以通过 `Endpoint Discovery Service(EDS)`来动态发现和更新。
|
||||
|
||||
- SSL/TLS:
|
||||
Envoy 可以通过 `Secret Discovery Service (SDS)` 动态获取监听器和集群所需的 TLS 证书、私钥以及信任的根证书和撤销机制等配置信息。
|
||||
|
||||
通过这些组件的协同工作,Envoy 能够高效地处理网络请求,提供流量管理、负载均衡、服务发现和动态路由等关键功能。
|
||||
要详细了解 Envoy 的工作原理,可以参考[Envoy 官方文档](https://www.envoyproxy.io/docs/envoy/latest/intro/intro),最佳的方式可以通过一个请求通过 [Envoy 代理的生命周期](https://www.envoyproxy.io/docs/envoy/latest/intro/life_of_a_request)事件的过程来理解 Envoy 的工作原理。
|
||||
|
||||
|
||||
## 参考
|
||||
|
||||
- [1] [Istio Pilot 组件介绍](https://www.zhaohuabing.com/post/2019-10-21-pilot-discovery-code-analysis/)
|
||||
- [2] [Istio 服务注册插件机制代码解析](https://www.zhaohuabing.com/post/2019-02-18-pilot-service-registry-code-analysis/)
|
||||
- [3] [Istio Pilot代码深度解析](https://www.zhaohuabing.com/post/2019-10-21-pilot-discovery-code-analysis/)
|
||||
- [4] [Envoy 官方文档](https://www.envoyproxy.io/docs/envoy/latest/intro/intro)
|
||||
BIN
docs/images/img_02_01.png
Normal file
BIN
docs/images/img_02_01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
BIN
docs/images/img_02_02.png
Normal file
BIN
docs/images/img_02_02.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
BIN
docs/images/img_02_03.png
Normal file
BIN
docs/images/img_02_03.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
BIN
docs/images/img_02_04.png
Normal file
BIN
docs/images/img_02_04.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.0.6-rc.1
|
||||
appVersion: 2.0.6
|
||||
description: Helm chart for deploying higress gateways
|
||||
icon: https://higress.io/img/higress_logo_small.png
|
||||
home: http://higress.io/
|
||||
@@ -10,4 +10,4 @@ name: higress-core
|
||||
sources:
|
||||
- http://github.com/alibaba/higress
|
||||
type: application
|
||||
version: 2.0.6-rc.1
|
||||
version: 2.0.6
|
||||
|
||||
@@ -128,7 +128,7 @@ template:
|
||||
- name: ISTIO_META_REQUESTED_NETWORK_VIEW
|
||||
value: "{{.}}"
|
||||
{{- end }}
|
||||
{{- range $key, $val := .Values.env }}
|
||||
{{- range $key, $val := .Values.gateway.env }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $val | quote }}
|
||||
{{- end }}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
accessLogFile: "/dev/stdout"
|
||||
{{- end }}
|
||||
ingressControllerMode: "OFF"
|
||||
accessLogFormat: '{"authority":"%REQ(X-ENVOY-ORIGINAL-HOST?:AUTHORITY)%","bytes_received":"%BYTES_RECEIVED%","bytes_sent":"%BYTES_SENT%","downstream_local_address":"%DOWNSTREAM_LOCAL_ADDRESS%","downstream_remote_address":"%DOWNSTREAM_REMOTE_ADDRESS%","duration":"%DURATION%","istio_policy_status":"%DYNAMIC_METADATA(istio.mixer:status)%","method":"%REQ(:METHOD)%","path":"%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%","protocol":"%PROTOCOL%","request_id":"%REQ(X-REQUEST-ID)%","requested_server_name":"%REQUESTED_SERVER_NAME%","response_code":"%RESPONSE_CODE%","response_flags":"%RESPONSE_FLAGS%","route_name":"%ROUTE_NAME%","start_time":"%START_TIME%","trace_id":"%REQ(X-B3-TRACEID)%","upstream_cluster":"%UPSTREAM_CLUSTER%","upstream_host":"%UPSTREAM_HOST%","upstream_local_address":"%UPSTREAM_LOCAL_ADDRESS%","upstream_service_time":"%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%","upstream_transport_failure_reason":"%UPSTREAM_TRANSPORT_FAILURE_REASON%","user_agent":"%REQ(USER-AGENT)%","x_forwarded_for":"%REQ(X-FORWARDED-FOR)%","response_code_details":"%RESPONSE_CODE_DETAILS%"}
|
||||
accessLogFormat: '{"ai_log":"%FILTER_STATE(wasm.ai_log:PLAIN)%","authority":"%REQ(X-ENVOY-ORIGINAL-HOST?:AUTHORITY)%","bytes_received":"%BYTES_RECEIVED%","bytes_sent":"%BYTES_SENT%","downstream_local_address":"%DOWNSTREAM_LOCAL_ADDRESS%","downstream_remote_address":"%DOWNSTREAM_REMOTE_ADDRESS%","duration":"%DURATION%","istio_policy_status":"%DYNAMIC_METADATA(istio.mixer:status)%","method":"%REQ(:METHOD)%","path":"%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%","protocol":"%PROTOCOL%","request_id":"%REQ(X-REQUEST-ID)%","requested_server_name":"%REQUESTED_SERVER_NAME%","response_code":"%RESPONSE_CODE%","response_flags":"%RESPONSE_FLAGS%","route_name":"%ROUTE_NAME%","start_time":"%START_TIME%","trace_id":"%REQ(X-B3-TRACEID)%","upstream_cluster":"%UPSTREAM_CLUSTER%","upstream_host":"%UPSTREAM_HOST%","upstream_local_address":"%UPSTREAM_LOCAL_ADDRESS%","upstream_service_time":"%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%","upstream_transport_failure_reason":"%UPSTREAM_TRANSPORT_FAILURE_REASON%","user_agent":"%REQ(USER-AGENT)%","x_forwarded_for":"%REQ(X-FORWARDED-FOR)%","response_code_details":"%RESPONSE_CODE_DETAILS%"}
|
||||
|
||||
'
|
||||
dnsRefreshRate: 200s
|
||||
|
||||
@@ -136,8 +136,10 @@ spec:
|
||||
periodSeconds: 3
|
||||
timeoutSeconds: 5
|
||||
env:
|
||||
- name: ENABLE_PUSH_ALL_MCP_CLUSTERS
|
||||
value: "{{ .Values.global.enablePushAllMCPClusters }}"
|
||||
- name: PILOT_ENABLE_LDS_CACHE
|
||||
valvue: "{{ .Values.global.enableLDSCache }}"
|
||||
value: "{{ .Values.global.enableLDSCache }}"
|
||||
- name: PILOT_ENABLE_QUIC_LISTENERS
|
||||
value: "true"
|
||||
- name: VALIDATION_WEBHOOK_CONFIG_NAME
|
||||
|
||||
@@ -4,6 +4,7 @@ global:
|
||||
enableIPv6: false
|
||||
enableProxyProtocol: false
|
||||
enableLDSCache: true
|
||||
enablePushAllMCPClusters: true
|
||||
liteMetrics: false
|
||||
xdsMaxRecvMsgSize: "104857600"
|
||||
defaultUpstreamConcurrencyThreshold: 10000
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: file://../core
|
||||
version: 2.0.6-rc.1
|
||||
version: 2.0.6
|
||||
- name: higress-console
|
||||
repository: https://higress.io/helm-charts/
|
||||
version: 2.0.0
|
||||
digest: sha256:66a5261f3d68abf63d2bade50e36ac696bec8aac909442d328fd5d395bf4bc21
|
||||
generated: "2025-01-08T17:14:12.432022+08:00"
|
||||
version: 2.0.2
|
||||
digest: sha256:9c84a628df434c4bf23ec10d62ad7ddf4b15957f797b01bbaa492ede33d87003
|
||||
generated: "2025-01-17T15:10:43.589701962+08:00"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.0.6-rc.1
|
||||
appVersion: 2.0.6
|
||||
description: Helm chart for deploying Higress gateways
|
||||
icon: https://higress.io/img/higress_logo_small.png
|
||||
home: http://higress.io/
|
||||
@@ -12,9 +12,9 @@ sources:
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: "file://../core"
|
||||
version: 2.0.6-rc.1
|
||||
version: 2.0.6
|
||||
- name: higress-console
|
||||
repository: "https://higress.io/helm-charts/"
|
||||
version: 2.0.0
|
||||
version: 2.0.2
|
||||
type: application
|
||||
version: 2.0.6-rc.1
|
||||
version: 2.0.6
|
||||
|
||||
@@ -164,6 +164,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| global.enableIstioAPI | bool | `true` | If true, Higress Controller will monitor istio resources as well |
|
||||
| global.enableLDSCache | bool | `true` | |
|
||||
| global.enableProxyProtocol | bool | `false` | |
|
||||
| global.enablePushAllMCPClusters | bool | `true` | |
|
||||
| global.enableSRDS | bool | `true` | |
|
||||
| global.enableStatus | bool | `true` | If true, Higress Controller will update the status field of Ingress resources. When migrating from Nginx Ingress, in order to avoid status field of Ingress objects being overwritten, this parameter needs to be set to false, so Higress won't write the entry IP to the status field of the corresponding Ingress object. |
|
||||
| global.externalIstiod | bool | `false` | Configure a remote cluster data plane controlled by an external istiod. When set to true, istiod is not deployed locally and only a subset of the other discovery charts are enabled. |
|
||||
|
||||
Submodule istio/istio updated: 81a46c581f...ad7d051f38
@@ -881,7 +881,7 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
if result.PluginConfig != nil {
|
||||
return result, nil
|
||||
}
|
||||
if !obj.DefaultConfigDisable {
|
||||
if !isBoolValueTrue(obj.DefaultConfigDisable) {
|
||||
result.PluginConfig = obj.DefaultConfig
|
||||
}
|
||||
hasValidRule := false
|
||||
@@ -893,7 +893,7 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
}
|
||||
var ruleValues []*_struct.Value
|
||||
for _, rule := range obj.MatchRules {
|
||||
if rule.ConfigDisable {
|
||||
if isBoolValueTrue(rule.ConfigDisable) {
|
||||
continue
|
||||
}
|
||||
if rule.Config == nil {
|
||||
@@ -982,13 +982,17 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasValidRule && obj.DefaultConfigDisable {
|
||||
if !hasValidRule && isBoolValueTrue(obj.DefaultConfigDisable) {
|
||||
return nil, nil
|
||||
}
|
||||
return result, nil
|
||||
|
||||
}
|
||||
|
||||
func isBoolValueTrue(b *wrappers.BoolValue) bool {
|
||||
return b != nil && b.Value
|
||||
}
|
||||
|
||||
func (m *IngressConfig) AddOrUpdateWasmPlugin(clusterNamespacedName util.ClusterNamespacedName) {
|
||||
if clusterNamespacedName.Namespace != m.namespace {
|
||||
return
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
## 功能说明
|
||||
# 功能说明
|
||||
`model-mapper`插件实现了基于LLM协议中的model参数路由的功能
|
||||
|
||||
## 配置字段
|
||||
# 配置字段
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ----------- | --------------- | ----------------------- | ------ | ------------------------------------------- |
|
||||
| `modelKey` | string | 选填 | model | 请求body中model参数的位置 |
|
||||
| `modelMapping` | map of string | 选填 | - | AI 模型映射表,用于将请求中的模型名称映射为服务提供商支持模型名称。<br/>1. 支持前缀匹配。例如用 "gpt-3-*" 匹配所有名称以“gpt-3-”开头的模型;<br/>2. 支持使用 "*" 为键来配置通用兜底映射关系;<br/>3. 如果映射的目标名称为空字符串 "",则表示保留原模型名称。 |
|
||||
| `enableOnPathSuffix` | array of string | 选填 | ["/v1/chat/completions"] | 只对这些特定路径后缀的请求生效 ## 运行属性
|
||||
| `enableOnPathSuffix` | array of string | 选填 | ["/v1/chat/completions"] | 只对这些特定路径后缀的请求生效 |
|
||||
|
||||
|
||||
插件执行阶段:认证阶段
|
||||
插件执行优先级:800
|
||||
|
|
||||
## 效果说明
|
||||
|
||||
如下配置
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
## 功能说明
|
||||
# 功能说明
|
||||
`model-router`插件实现了基于LLM协议中的model参数路由的功能
|
||||
|
||||
## 配置字段
|
||||
# 配置字段
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ----------- | --------------- | ----------------------- | ------ | ------------------------------------------- |
|
||||
|
||||
@@ -26,6 +26,7 @@ proxy_wasm_cc_binary(
|
||||
"@com_google_absl//absl/time",
|
||||
"//common:json_util",
|
||||
"//common:http_util",
|
||||
"//common:regex_util",
|
||||
"//common:rule_util",
|
||||
],
|
||||
)
|
||||
@@ -44,6 +45,7 @@ cc_library(
|
||||
"//common:json_util",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
"//common:http_util_nullvm",
|
||||
"//common:regex_util",
|
||||
"//common:rule_util_nullvm",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,31 +1,22 @@
|
||||
---
|
||||
title: 请求屏蔽
|
||||
keywords: [higress,request block]
|
||||
description: 请求屏蔽插件配置参考
|
||||
---
|
||||
|
||||
## 功能说明
|
||||
# 功能说明
|
||||
`request-block`插件实现了基于 URL、请求头等特征屏蔽 HTTP 请求,可以用于防护部分站点资源不对外部暴露
|
||||
|
||||
## 运行属性
|
||||
# 配置字段
|
||||
|
||||
插件执行阶段:`鉴权阶段`
|
||||
插件执行优先级:`320`
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -------- | -------- | -------- | -------- | -------- |
|
||||
| block_urls | array of string | 选填,`block_urls`,`block_exact_urls`,`block_regexp_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要屏蔽 URL 的字符串 |
|
||||
| block_exact_urls | array of string | 选填,`block_urls`,`block_exact_urls`,`block_regexp_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要精确屏蔽 URL 的字符串 |
|
||||
| block_regexp_urls | array of string | 选填,`block_urls`,`block_exact_urls`,`block_regexp_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要屏蔽 URL 的正则表达式 |
|
||||
| block_headers | array of string | 选填,`block_urls`,`block_exact_urls`,`block_regexp_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要屏蔽请求 Header 的字符串 |
|
||||
| block_bodies | array of string | 选填,`block_urls`,`block_exact_urls`,`block_regexp_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要屏蔽请求 Body 的字符串 |
|
||||
| blocked_code | number | 选填 | 403 | 配置请求被屏蔽时返回的 HTTP 状态码 |
|
||||
| blocked_message | string | 选填 | - | 配置请求被屏蔽时返回的 HTTP 应答 Body |
|
||||
| case_sensitive | bool | 选填 | true | 配置匹配时是否区分大小写,默认区分 |
|
||||
|
||||
## 配置字段
|
||||
# 配置示例
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -------- | -------- | -------- | -------- | -------- |
|
||||
| block_urls | array of string | 选填,`block_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要屏蔽 URL 的字符串 |
|
||||
| block_headers | array of string | 选填,`block_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要屏蔽请求 Header 的字符串 |
|
||||
| block_bodies | array of string | 选填,`block_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要屏蔽请求 Body 的字符串 |
|
||||
| blocked_code | number | 选填 | 403 | 配置请求被屏蔽时返回的 HTTP 状态码 |
|
||||
| blocked_message | string | 选填 | - | 配置请求被屏蔽时返回的 HTTP 应答 Body |
|
||||
| case_sensitive | bool | 选填 | true | 配置匹配时是否区分大小写,默认区分 |
|
||||
|
||||
## 配置示例
|
||||
|
||||
### 屏蔽请求 url 路径
|
||||
## 屏蔽请求 url 路径
|
||||
```yaml
|
||||
block_urls:
|
||||
- swagger.html
|
||||
@@ -40,7 +31,36 @@ curl http://example.com?foo=Bar
|
||||
curl http://exmaple.com/Swagger.html
|
||||
```
|
||||
|
||||
### 屏蔽请求 header
|
||||
## 屏蔽精确匹配的请求 url 路径
|
||||
|
||||
```yaml
|
||||
block_exact_urls:
|
||||
- /swagger.html?foo=bar
|
||||
case_sensitive: false
|
||||
```
|
||||
|
||||
根据该配置,下列请求将被禁止访问:
|
||||
|
||||
```bash
|
||||
curl http://exmaple.com/Swagger.html?foo=Bar
|
||||
```
|
||||
|
||||
## 屏蔽正则匹配的请求 url 路径
|
||||
|
||||
```yaml
|
||||
block_exact_urls:
|
||||
- .*swagger.*
|
||||
case_sensitive: false
|
||||
```
|
||||
|
||||
根据该配置,下列请求将被禁止访问:
|
||||
|
||||
```bash
|
||||
curl http://exmaple.com/Swagger.html?foo=Bar
|
||||
```
|
||||
|
||||
|
||||
## 屏蔽请求 header
|
||||
```yaml
|
||||
block_headers:
|
||||
- example-key
|
||||
@@ -54,9 +74,9 @@ curl http://example.com -H 'example-key: 123'
|
||||
curl http://exmaple.com -H 'my-header: example-value'
|
||||
```
|
||||
|
||||
### 屏蔽请求 body
|
||||
## 屏蔽请求 body
|
||||
```yaml
|
||||
block_bodies:
|
||||
block_bodys:
|
||||
- "hello world"
|
||||
case_sensitive: false
|
||||
```
|
||||
@@ -68,8 +88,30 @@ curl http://example.com -d 'Hello World'
|
||||
curl http://exmaple.com -d 'hello world'
|
||||
```
|
||||
|
||||
## 对特定路由或域名开启
|
||||
```yaml
|
||||
# 使用 _rules_ 字段进行细粒度规则配置
|
||||
_rules_:
|
||||
# 规则一:按路由名称匹配生效
|
||||
- _match_route_:
|
||||
- route-a
|
||||
- route-b
|
||||
block_bodys:
|
||||
- "hello world"
|
||||
# 规则二:按域名匹配生效
|
||||
- _match_domain_:
|
||||
- "*.example.com"
|
||||
- test.com
|
||||
block_urls:
|
||||
- "swagger.html"
|
||||
block_bodys:
|
||||
- "hello world"
|
||||
```
|
||||
此例 `_match_route_` 中指定的 `route-a` 和 `route-b` 即在创建网关路由时填写的路由名称,当匹配到这两个路由时,将使用此段配置;
|
||||
此例 `_match_domain_` 中指定的 `*.example.com` 和 `test.com` 用于匹配请求的域名,当发现域名匹配时,将使用此段配置;
|
||||
配置的匹配生效顺序,将按照 `_rules_` 下规则的排列顺序,匹配第一个规则后生效对应配置,后续规则将被忽略。
|
||||
|
||||
## 请求 Body 大小限制
|
||||
# 请求 Body 大小限制
|
||||
|
||||
当配置了 `block_bodies` 时,仅支持小于 32 MB 的请求 Body 进行匹配。若请求 Body 大于此限制,并且不存在匹配到的 `block_urls` 和 `block_headers` 项时,不会对该请求执行屏蔽操作
|
||||
当配置了 `block_bodies` 时,若请求 Body 超过全局配置 DownstreamConnectionBufferLimits,将返回 `413 Payload Too Large`
|
||||
当配置了 `block_bodys` 时,仅支持小于 32 MB 的请求 Body 进行匹配。若请求 Body 大于此限制,并且不存在匹配到的 `block_urls` 和 `block_headers` 项时,不会对该请求执行屏蔽操作
|
||||
当配置了 `block_bodys` 时,若请求 Body 超过全局配置 DownstreamConnectionBufferLimits,将返回 `413 Payload Too Large`, 请在参数配置页调高此项。注意调高此参数配置后,网关内存使用将有显著增加。
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "extensions/request_block/plugin.h"
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
@@ -89,6 +90,48 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
LOG_WARN("failed to parse configuration for block_urls.");
|
||||
return false;
|
||||
}
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "block_exact_urls", [&](const json& item) -> bool {
|
||||
auto url = JsonValueAs<std::string>(item);
|
||||
if (url.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse block_exact_urls");
|
||||
return false;
|
||||
}
|
||||
if (rule.case_sensitive) {
|
||||
rule.block_exact_urls.push_back(std::move(url.first.value()));
|
||||
} else {
|
||||
rule.block_exact_urls.push_back(
|
||||
absl::AsciiStrToLower(url.first.value()));
|
||||
}
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for block_exact_urls.");
|
||||
return false;
|
||||
}
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "block_regexp_urls", [&](const json& item) -> bool {
|
||||
auto url = JsonValueAs<std::string>(item);
|
||||
if (url.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse block_regexp_urls");
|
||||
return false;
|
||||
}
|
||||
std::string regex;
|
||||
if (rule.case_sensitive) {
|
||||
regex = url.first.value();
|
||||
} else {
|
||||
regex = absl::AsciiStrToLower(url.first.value());
|
||||
}
|
||||
auto re = std::make_unique<ReMatcher>(regex);
|
||||
if (!re->error().empty()) {
|
||||
LOG_WARN(re->error());
|
||||
return false;
|
||||
}
|
||||
rule.block_regexp_urls.push_back(std::move(re));
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for block_regexp_urls.");
|
||||
return false;
|
||||
}
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "block_headers", [&](const json& item) -> bool {
|
||||
auto header = JsonValueAs<std::string>(item);
|
||||
@@ -125,8 +168,28 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
LOG_WARN("failed to parse configuration for block_bodys.");
|
||||
return false;
|
||||
}
|
||||
// compatiable
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "block_bodies", [&](const json& item) -> bool {
|
||||
auto body = JsonValueAs<std::string>(item);
|
||||
if (body.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse block_bodies");
|
||||
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_bodies.");
|
||||
return false;
|
||||
}
|
||||
if (rule.block_bodys.empty() && rule.block_headers.empty() &&
|
||||
rule.block_urls.empty()) {
|
||||
rule.block_urls.empty() && rule.block_exact_urls.empty() &&
|
||||
rule.block_regexp_urls.empty()) {
|
||||
LOG_WARN("there is no block rules");
|
||||
return false;
|
||||
}
|
||||
@@ -172,6 +235,18 @@ bool PluginRootContext::checkHeader(const RequestBlockConfigRule& rule,
|
||||
urlstr = absl::AsciiStrToLower(request_url);
|
||||
url = urlstr;
|
||||
}
|
||||
for (const auto& block_url : rule.block_exact_urls) {
|
||||
if (url == block_url) {
|
||||
sendLocalResponse(rule.blocked_code, "", rule.blocked_message, {});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const auto& block_url : rule.block_regexp_urls) {
|
||||
if (block_url->match(url)) {
|
||||
sendLocalResponse(rule.blocked_code, "", rule.blocked_message, {});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const auto& block_url : rule.block_urls) {
|
||||
if (absl::StrContains(url, block_url)) {
|
||||
sendLocalResponse(rule.blocked_code, "", rule.blocked_message, {});
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <unordered_map>
|
||||
|
||||
#include "common/http_util.h"
|
||||
#include "common/regex.h"
|
||||
#include "common/route_rule_matcher.h"
|
||||
#define ASSERT(_X) assert(_X)
|
||||
|
||||
@@ -39,11 +40,16 @@ namespace request_block {
|
||||
|
||||
#endif
|
||||
|
||||
using ReMatcher = Wasm::Common::Regex::CompiledGoogleReMatcher;
|
||||
using ReMatcherPtr = std::unique_ptr<ReMatcher>;
|
||||
|
||||
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_exact_urls;
|
||||
std::vector<ReMatcherPtr> block_regexp_urls;
|
||||
std::vector<std::string> block_headers;
|
||||
std::vector<std::string> block_bodys;
|
||||
};
|
||||
|
||||
@@ -127,6 +127,8 @@ TEST_F(RequestBlockTest, CaseSensitive) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"block_urls": ["?foo=bar", "swagger.html"],
|
||||
"block_exact_urls": ["/hello.html?abc=123"],
|
||||
"block_regexp_urls": [".*monkey.*"],
|
||||
"block_headers": ["headerKey", "headerValue"],
|
||||
"block_bodys": ["Hello World"]
|
||||
})";
|
||||
@@ -150,6 +152,22 @@ TEST_F(RequestBlockTest, CaseSensitive) {
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/hello.html?abc=123";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(403, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/black/Monkey";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
path_ = "/black/monkey";
|
||||
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::_,
|
||||
@@ -188,6 +206,8 @@ TEST_F(RequestBlockTest, CaseInsensitive) {
|
||||
"blocked_code": 404,
|
||||
"block_urls": ["?foo=bar", "swagger.html"],
|
||||
"block_headers": ["headerKey", "headerValue"],
|
||||
"block_exact_urls": ["/hello.html?abc=123"],
|
||||
"block_regexp_urls": [".*monkey.*"],
|
||||
"block_bodys": ["Hello World"]
|
||||
})";
|
||||
|
||||
@@ -206,6 +226,24 @@ TEST_F(RequestBlockTest, CaseInsensitive) {
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/Hello.html?abc=123";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(404, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/black/Monkey";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(404, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/black/monkey";
|
||||
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::_,
|
||||
@@ -232,6 +270,26 @@ TEST_F(RequestBlockTest, CaseInsensitive) {
|
||||
FilterDataStatus::StopIterationNoBuffer);
|
||||
}
|
||||
|
||||
TEST_F(RequestBlockTest, Bodies) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"case_sensitive": false,
|
||||
"blocked_code": 404,
|
||||
"block_bodies": ["Hello World"]
|
||||
})";
|
||||
|
||||
config_.set({configuration.data(), configuration.size()});
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
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
|
||||
|
||||
@@ -79,11 +79,11 @@ func (c *PluginConfig) FromJson(json gjson.Result, log wrapper.Log) {
|
||||
|
||||
c.StreamResponseTemplate = json.Get("streamResponseTemplate").String()
|
||||
if c.StreamResponseTemplate == "" {
|
||||
c.StreamResponseTemplate = `data:{"id":"from-cache","choices":[{"index":0,"delta":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}` + "\n\ndata:[DONE]\n\n"
|
||||
c.StreamResponseTemplate = `data:{"id":"from-cache","choices":[{"index":0,"delta":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"from-cache","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}` + "\n\ndata:[DONE]\n\n"
|
||||
}
|
||||
c.ResponseTemplate = json.Get("responseTemplate").String()
|
||||
if c.ResponseTemplate == "" {
|
||||
c.ResponseTemplate = `{"id":"from-cache","choices":[{"index":0,"message":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}`
|
||||
c.ResponseTemplate = `{"id":"from-cache","choices":[{"index":0,"message":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"from-cache","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}`
|
||||
}
|
||||
|
||||
if json.Get("enableSemanticCache").Exists() {
|
||||
|
||||
@@ -34,7 +34,7 @@ func parseConfig(json gjson.Result, config *AIPromptTemplateConfig, log wrapper.
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config AIPromptTemplateConfig, log wrapper.Log) types.Action {
|
||||
templateEnable, _ := proxywasm.GetHttpRequestHeader("template-enable")
|
||||
if templateEnable != "true" {
|
||||
if templateEnable == "false" {
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
@@ -157,15 +157,7 @@ Groq 所对应的 `type` 为 `groq`。它并无特有的配置字段。
|
||||
|
||||
#### 文心一言(Baidu)
|
||||
|
||||
文心一言所对应的 `type` 为 `baidu`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|--------------------|-----------------|------|-----|-----------------------------------------------------------|
|
||||
| `baiduAccessKeyAndSecret` | array of string | 必填 | - | Baidu 的 Access Key 和 Secret Key,中间用 `:` 分隔,用于申请 apiToken。 |
|
||||
| `baiduApiTokenServiceName` | string | 必填 | - | 请求刷新百度 apiToken 服务名称。 |
|
||||
| `baiduApiTokenServiceHost` | string | 非必填 | - | 请求刷新百度 apiToken 服务域名,默认是 iam.bj.baidubce.com。 |
|
||||
| `baiduApiTokenServicePort` | int64 | 非必填 | - | 请求刷新百度 apiToken 服务端口,默认是 443。 |
|
||||
|
||||
文心一言所对应的 `type` 为 `baidu`。它并无特有的配置字段。
|
||||
|
||||
#### 360智脑
|
||||
|
||||
@@ -255,6 +247,17 @@ Cohere 所对应的 `type` 为 `cohere`。它并无特有的配置字段。
|
||||
#### Together-AI
|
||||
Together-AI 所对应的 `type` 为 `together-ai`。它并无特有的配置字段。
|
||||
|
||||
#### Dify
|
||||
Dify 所对应的 `type` 为 `dify`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -- | -------- |------| ------ | ---------------------------- |
|
||||
| `difyApiUrl` | string | 非必填 | - | dify私有化部署的url |
|
||||
| `botType` | string | 非必填 | - | dify的应用类型,Chat/Completion/Agent/Workflow |
|
||||
| `inputVariable` | string | 非必填 | - | dify中应用类型为workflow时需要设置输入变量,当botType为workflow时一起使用 |
|
||||
| `outputVariable` | string | 非必填 | - | dify中应用类型为workflow时需要设置输出变量,当botType为workflow时一起使用 |
|
||||
|
||||
|
||||
## 用法示例
|
||||
|
||||
### 使用 OpenAI 协议代理 Azure OpenAI 服务
|
||||
@@ -429,25 +432,25 @@ URL: http://your-domain/v1/chat/completions
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "这个图片是哪里?"
|
||||
}
|
||||
]
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "这个图片是哪里?"
|
||||
}
|
||||
],
|
||||
"temperature": 0.3
|
||||
]
|
||||
}
|
||||
],
|
||||
"temperature": 0.3
|
||||
}
|
||||
```
|
||||
|
||||
@@ -455,28 +458,28 @@ URL: http://your-domain/v1/chat/completions
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "17c5955d-af9c-9f28-bbde-293a9c9a3515",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"text": "这张照片显示的是一位女士和一只狗在海滩上。由于我无法获取具体的地理位置信息,所以不能确定这是哪个地方的海滩。但是从视觉内容来看,它可能是一个位于沿海地区的沙滩海岸线,并且有海浪拍打着岸边。这样的场景在全球许多美丽的海滨地区都可以找到。如果您需要更精确的信息,请提供更多的背景或细节描述。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1723949230,
|
||||
"model": "qwen-vl-plus",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 1279,
|
||||
"completion_tokens": 78
|
||||
"id": "17c5955d-af9c-9f28-bbde-293a9c9a3515",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"text": "这张照片显示的是一位女士和一只狗在海滩上。由于我无法获取具体的地理位置信息,所以不能确定这是哪个地方的海滩。但是从视觉内容来看,它可能是一个位于沿海地区的沙滩海岸线,并且有海浪拍打着岸边。这样的场景在全球许多美丽的海滨地区都可以找到。如果您需要更精确的信息,请提供更多的背景或细节描述。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1723949230,
|
||||
"model": "qwen-vl-plus",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 1279,
|
||||
"completion_tokens": 78
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -599,8 +602,8 @@ provider:
|
||||
modelMapping:
|
||||
"*": "qwen-long" # 通义千问的文件上下文只能在 qwen-long 模型下使用
|
||||
qwenFileIds:
|
||||
- "file-fe-xxx"
|
||||
- "file-fe-yyy"
|
||||
- "file-fe-xxx"
|
||||
- "file-fe-yyy"
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
@@ -658,7 +661,7 @@ provider:
|
||||
```json
|
||||
{
|
||||
"input": {
|
||||
"prompt": "介绍一下Dubbo"
|
||||
"prompt": "介绍一下Dubbo"
|
||||
},
|
||||
"parameters": {},
|
||||
"debug": {}
|
||||
@@ -669,21 +672,21 @@ provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"output": {
|
||||
"finish_reason": "stop",
|
||||
"session_id": "677e7e8fbb874e1b84792b65042e1599",
|
||||
"text": "Apache Dubbo 是一个..."
|
||||
},
|
||||
"usage": {
|
||||
"models": [
|
||||
{
|
||||
"output_tokens": 449,
|
||||
"model_id": "qwen-max",
|
||||
"input_tokens": 282
|
||||
}
|
||||
]
|
||||
},
|
||||
"request_id": "b59e45e3-5af4-91df-b7c6-9d746fd3297c"
|
||||
"output": {
|
||||
"finish_reason": "stop",
|
||||
"session_id": "677e7e8fbb874e1b84792b65042e1599",
|
||||
"text": "Apache Dubbo 是一个..."
|
||||
},
|
||||
"usage": {
|
||||
"models": [
|
||||
{
|
||||
"output_tokens": 449,
|
||||
"model_id": "qwen-max",
|
||||
"input_tokens": 282
|
||||
}
|
||||
]
|
||||
},
|
||||
"request_id": "b59e45e3-5af4-91df-b7c6-9d746fd3297c"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -926,25 +929,25 @@ curl --location 'http://<your higress domain>/v1/chat/completions' \
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "fd140c3e-0b69-4b19-849b-d354d32a6162",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"role": "assistant",
|
||||
"content": "你好!我是一名专业的开发人员。"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1717493117,
|
||||
"model": "hunyuan-lite",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 15,
|
||||
"completion_tokens": 9,
|
||||
"total_tokens": 24
|
||||
"id": "fd140c3e-0b69-4b19-849b-d354d32a6162",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"role": "assistant",
|
||||
"content": "你好!我是一名专业的开发人员。"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1717493117,
|
||||
"model": "hunyuan-lite",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 15,
|
||||
"completion_tokens": 9,
|
||||
"total_tokens": 24
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -966,14 +969,14 @@ provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
"model": "gpt-4-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -981,25 +984,25 @@ provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "as-e90yfg1pk1",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "你好,我是文心一言,英文名是ERNIE Bot。我能够与人对话互动,回答问题,协助创作,高效便捷地帮助人们获取信息、知识和灵感。"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1717251488,
|
||||
"model": "ERNIE-4.0",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 4,
|
||||
"completion_tokens": 33,
|
||||
"total_tokens": 37
|
||||
"id": "as-e90yfg1pk1",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "你好,我是文心一言,英文名是ERNIE Bot。我能够与人对话互动,回答问题,协助创作,高效便捷地帮助人们获取信息、知识和灵感。"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1717251488,
|
||||
"model": "ERNIE-4.0",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 4,
|
||||
"completion_tokens": 33,
|
||||
"total_tokens": 37
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1022,14 +1025,14 @@ provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-3",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
"model": "gpt-3",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1037,37 +1040,37 @@ provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "03ac4fcfe1c6cc9c6a60f9d12046e2b4",
|
||||
"choices": [
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"index": 0,
|
||||
"message": {
|
||||
"content": "你好,我是一个由MiniMax公司研发的大型语言模型,名为MM智能助理。我可以帮助回答问题、提供信息、进行对话和执行多种语言处理任务。如果你有任何问题或需要帮助,请随时告诉我!",
|
||||
"role": "assistant",
|
||||
"name": "MM智能助理",
|
||||
"audio_content": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"created": 1734155471,
|
||||
"model": "abab6.5s-chat",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"total_tokens": 116,
|
||||
"total_characters": 0,
|
||||
"prompt_tokens": 70,
|
||||
"completion_tokens": 46
|
||||
},
|
||||
"input_sensitive": false,
|
||||
"output_sensitive": false,
|
||||
"input_sensitive_type": 0,
|
||||
"output_sensitive_type": 0,
|
||||
"output_sensitive_int": 0,
|
||||
"base_resp": {
|
||||
"status_code": 0,
|
||||
"status_msg": ""
|
||||
"id": "03ac4fcfe1c6cc9c6a60f9d12046e2b4",
|
||||
"choices": [
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"index": 0,
|
||||
"message": {
|
||||
"content": "你好,我是一个由MiniMax公司研发的大型语言模型,名为MM智能助理。我可以帮助回答问题、提供信息、进行对话和执行多种语言处理任务。如果你有任何问题或需要帮助,请随时告诉我!",
|
||||
"role": "assistant",
|
||||
"name": "MM智能助理",
|
||||
"audio_content": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"created": 1734155471,
|
||||
"model": "abab6.5s-chat",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"total_tokens": 116,
|
||||
"total_characters": 0,
|
||||
"prompt_tokens": 70,
|
||||
"completion_tokens": 46
|
||||
},
|
||||
"input_sensitive": false,
|
||||
"output_sensitive": false,
|
||||
"input_sensitive_type": 0,
|
||||
"output_sensitive_type": 0,
|
||||
"output_sensitive_int": 0,
|
||||
"base_resp": {
|
||||
"status_code": 0,
|
||||
"status_msg": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1356,18 +1359,18 @@ provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "你是一名专业的开发人员!"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "你是一名专业的开发人员!"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1375,24 +1378,24 @@ provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "cha000c23c6@dx190ef0b4b96b8f2532",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "你好!我是一名专业的开发人员,擅长编程和解决技术问题。有什么我可以帮助你的吗?"
|
||||
}
|
||||
}
|
||||
],
|
||||
"created": 1721997415,
|
||||
"model": "generalv3.5",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 19,
|
||||
"total_tokens": 29
|
||||
"id": "cha000c23c6@dx190ef0b4b96b8f2532",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "你好!我是一名专业的开发人员,擅长编程和解决技术问题。有什么我可以帮助你的吗?"
|
||||
}
|
||||
}
|
||||
],
|
||||
"created": 1721997415,
|
||||
"model": "generalv3.5",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 19,
|
||||
"total_tokens": 29
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1418,14 +1421,14 @@ provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-3.5",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Who are you?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
"model": "gpt-3.5",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Who are you?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1433,25 +1436,25 @@ provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "chatcmpl-b010867c-0d3f-40ba-95fd-4e8030551aeb",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "I am a large multi-modal model, trained by Google. I am designed to provide information and answer questions to the best of my abilities."
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1722756984,
|
||||
"model": "gemini-pro",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 5,
|
||||
"completion_tokens": 29,
|
||||
"total_tokens": 34
|
||||
"id": "chatcmpl-b010867c-0d3f-40ba-95fd-4e8030551aeb",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "I am a large multi-modal model, trained by Google. I am designed to provide information and answer questions to the best of my abilities."
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1722756984,
|
||||
"model": "gemini-pro",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 5,
|
||||
"completion_tokens": 29,
|
||||
"total_tokens": 34
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1523,13 +1526,13 @@ provider:
|
||||
**请求示例**
|
||||
```json
|
||||
{
|
||||
"model": "Qwen/Qwen2.5-72B-Instruct-Turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Who are you?"
|
||||
}
|
||||
]
|
||||
"model": "Qwen/Qwen2.5-72B-Instruct-Turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Who are you?"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1562,6 +1565,57 @@ provider:
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理 Dify 服务
|
||||
|
||||
**配置信息**
|
||||
```yaml
|
||||
provider:
|
||||
type: dify
|
||||
apiTokens:
|
||||
- "YOUR_DIFY_API_TOKEN"
|
||||
modelMapping:
|
||||
"*": "dify"
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"id": "e33fc636-f9e8-4fae-8d5e-fbd0acb09401",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "你好!我是ChatGPT,由OpenAI开发的人工智能语言模型。我可以帮助回答问题、提供建议或进行各种对话。如果你有任何需要,随时告诉我哦!"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1736657752,
|
||||
"model": "dify",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 16,
|
||||
"completion_tokens": 243,
|
||||
"total_tokens": 259
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 完整配置示例
|
||||
|
||||
@@ -1577,13 +1631,13 @@ metadata:
|
||||
namespace: higress-system
|
||||
spec:
|
||||
matchRules:
|
||||
- config:
|
||||
provider:
|
||||
type: groq
|
||||
apiTokens:
|
||||
- "YOUR_API_TOKEN"
|
||||
ingress:
|
||||
- groq
|
||||
- config:
|
||||
provider:
|
||||
type: groq
|
||||
apiTokens:
|
||||
- "YOUR_API_TOKEN"
|
||||
ingress:
|
||||
- groq
|
||||
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/ai-proxy:1.0.0
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
@@ -1601,16 +1655,16 @@ metadata:
|
||||
spec:
|
||||
ingressClassName: higress
|
||||
rules:
|
||||
- host: <YOUR-DOMAIN>
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
resource:
|
||||
apiGroup: networking.higress.io
|
||||
kind: McpBridge
|
||||
name: default
|
||||
path: /
|
||||
pathType: Prefix
|
||||
- host: <YOUR-DOMAIN>
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
resource:
|
||||
apiGroup: networking.higress.io
|
||||
kind: McpBridge
|
||||
name: default
|
||||
path: /
|
||||
pathType: Prefix
|
||||
---
|
||||
apiVersion: networking.higress.io/v1
|
||||
kind: McpBridge
|
||||
@@ -1619,10 +1673,10 @@ metadata:
|
||||
namespace: higress-system
|
||||
spec:
|
||||
registries:
|
||||
- domain: api.groq.com
|
||||
name: groq
|
||||
port: 443
|
||||
type: dns
|
||||
- domain: api.groq.com
|
||||
name: groq
|
||||
port: 443
|
||||
type: dns
|
||||
```
|
||||
|
||||
访问示例:
|
||||
|
||||
@@ -86,11 +86,6 @@ func (c *PluginConfig) Complete(log wrapper.Log) error {
|
||||
providerConfig := c.GetProviderConfig()
|
||||
err = providerConfig.SetApiTokensFailover(log, c.activeProvider)
|
||||
|
||||
if handler, ok := c.activeProvider.(provider.TickFuncHandler); ok {
|
||||
tickPeriod, tickFunc := handler.GetTickFunc(log)
|
||||
wrapper.RegisteTickFunc(tickPeriod, tickFunc)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ type ai360Provider struct {
|
||||
contextCache *contextCache
|
||||
}
|
||||
|
||||
func (m *ai360ProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *ai360ProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
type azureProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (m *azureProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *azureProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.azureServiceUrl == "" {
|
||||
return errors.New("missing azureServiceUrl in provider config")
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ const (
|
||||
type baichuanProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (m *baichuanProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *baichuanProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
@@ -21,28 +14,13 @@ import (
|
||||
const (
|
||||
baiduDomain = "qianfan.baidubce.com"
|
||||
baiduChatCompletionPath = "/v2/chat/completions"
|
||||
baiduApiTokenDomain = "iam.bj.baidubce.com"
|
||||
baiduApiTokenPort = 443
|
||||
baiduApiTokenPath = "/v1/BCE-BEARER/token"
|
||||
// refresh apiToken every 1 hour
|
||||
baiduApiTokenRefreshInterval = 3600
|
||||
// authorizationString expires in 30 minutes, authorizationString is used to generate apiToken
|
||||
// the default expiration time of apiToken is 24 hours
|
||||
baiduAuthorizationStringExpirationSeconds = 1800
|
||||
bce_prefix = "x-bce-"
|
||||
)
|
||||
|
||||
type baiduProviderInitializer struct{}
|
||||
|
||||
func (g *baiduProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.baiduAccessKeyAndSecret == nil || len(config.baiduAccessKeyAndSecret) == 0 {
|
||||
return errors.New("no baiduAccessKeyAndSecret found in provider config")
|
||||
}
|
||||
if config.baiduApiTokenServiceName == "" {
|
||||
return errors.New("no baiduApiTokenServiceName found in provider config")
|
||||
}
|
||||
if !config.failover.enabled {
|
||||
config.useGlobalApiToken = true
|
||||
func (g *baiduProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -91,203 +69,3 @@ func (g *baiduProvider) GetApiName(path string) ApiName {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func generateAuthorizationString(accessKeyAndSecret string, expirationInSeconds int) string {
|
||||
c := strings.Split(accessKeyAndSecret, ":")
|
||||
credentials := BceCredentials{
|
||||
AccessKeyId: c[0],
|
||||
SecretAccessKey: c[1],
|
||||
}
|
||||
httpMethod := "GET"
|
||||
path := baiduApiTokenPath
|
||||
headers := map[string]string{"host": baiduApiTokenDomain}
|
||||
timestamp := time.Now().Unix()
|
||||
|
||||
headersToSign := make([]string, 0, len(headers))
|
||||
for k := range headers {
|
||||
headersToSign = append(headersToSign, k)
|
||||
}
|
||||
|
||||
return sign(credentials, httpMethod, path, headers, timestamp, expirationInSeconds, headersToSign)
|
||||
}
|
||||
|
||||
// BceCredentials holds the access key and secret key
|
||||
type BceCredentials struct {
|
||||
AccessKeyId string
|
||||
SecretAccessKey string
|
||||
}
|
||||
|
||||
// normalizeString performs URI encoding according to RFC 3986
|
||||
func normalizeString(inStr string, encodingSlash bool) string {
|
||||
if inStr == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
for _, ch := range []byte(inStr) {
|
||||
if (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') ||
|
||||
(ch >= '0' && ch <= '9') || ch == '.' || ch == '-' ||
|
||||
ch == '_' || ch == '~' || (!encodingSlash && ch == '/') {
|
||||
result.WriteByte(ch)
|
||||
} else {
|
||||
result.WriteString(fmt.Sprintf("%%%02X", ch))
|
||||
}
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// getCanonicalTime generates a timestamp in UTC format
|
||||
func getCanonicalTime(timestamp int64) string {
|
||||
if timestamp == 0 {
|
||||
timestamp = time.Now().Unix()
|
||||
}
|
||||
t := time.Unix(timestamp, 0).UTC()
|
||||
return t.Format("2006-01-02T15:04:05Z")
|
||||
}
|
||||
|
||||
// getCanonicalUri generates a canonical URI
|
||||
func getCanonicalUri(path string) string {
|
||||
return normalizeString(path, false)
|
||||
}
|
||||
|
||||
// getCanonicalHeaders generates canonical headers
|
||||
func getCanonicalHeaders(headers map[string]string, headersToSign []string) string {
|
||||
if len(headers) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// If headersToSign is not specified, use default headers
|
||||
if len(headersToSign) == 0 {
|
||||
headersToSign = []string{"host", "content-md5", "content-length", "content-type"}
|
||||
}
|
||||
|
||||
// Convert headersToSign to a map for easier lookup
|
||||
headerMap := make(map[string]bool)
|
||||
for _, header := range headersToSign {
|
||||
headerMap[strings.ToLower(strings.TrimSpace(header))] = true
|
||||
}
|
||||
|
||||
// Create a slice to hold the canonical headers
|
||||
var canonicalHeaders []string
|
||||
for k, v := range headers {
|
||||
k = strings.ToLower(strings.TrimSpace(k))
|
||||
v = strings.TrimSpace(v)
|
||||
|
||||
// Add headers that start with x-bce- or are in headersToSign
|
||||
if strings.HasPrefix(k, bce_prefix) || headerMap[k] {
|
||||
canonicalHeaders = append(canonicalHeaders,
|
||||
fmt.Sprintf("%s:%s", normalizeString(k, true), normalizeString(v, true)))
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the canonical headers
|
||||
sort.Strings(canonicalHeaders)
|
||||
|
||||
return strings.Join(canonicalHeaders, "\n")
|
||||
}
|
||||
|
||||
// sign generates the authorization string
|
||||
func sign(credentials BceCredentials, httpMethod, path string, headers map[string]string,
|
||||
timestamp int64, expirationInSeconds int,
|
||||
headersToSign []string) string {
|
||||
|
||||
// Generate sign key
|
||||
signKeyInfo := fmt.Sprintf("bce-auth-v1/%s/%s/%d",
|
||||
credentials.AccessKeyId,
|
||||
getCanonicalTime(timestamp),
|
||||
expirationInSeconds)
|
||||
|
||||
// Generate sign key using HMAC-SHA256
|
||||
h := hmac.New(sha256.New, []byte(credentials.SecretAccessKey))
|
||||
h.Write([]byte(signKeyInfo))
|
||||
signKey := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
// Generate canonical URI
|
||||
canonicalUri := getCanonicalUri(path)
|
||||
|
||||
// Generate canonical headers
|
||||
canonicalHeaders := getCanonicalHeaders(headers, headersToSign)
|
||||
|
||||
// Generate string to sign
|
||||
stringToSign := strings.Join([]string{
|
||||
httpMethod,
|
||||
canonicalUri,
|
||||
"",
|
||||
canonicalHeaders,
|
||||
}, "\n")
|
||||
|
||||
// Calculate final signature
|
||||
h = hmac.New(sha256.New, []byte(signKey))
|
||||
h.Write([]byte(stringToSign))
|
||||
signature := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
// Generate final authorization string
|
||||
if len(headersToSign) > 0 {
|
||||
return fmt.Sprintf("%s/%s/%s", signKeyInfo, strings.Join(headersToSign, ";"), signature)
|
||||
}
|
||||
return fmt.Sprintf("%s//%s", signKeyInfo, signature)
|
||||
}
|
||||
|
||||
// GetTickFunc Refresh apiToken (apiToken) periodically, the maximum apiToken expiration time is 24 hours
|
||||
func (g *baiduProvider) GetTickFunc(log wrapper.Log) (tickPeriod int64, tickFunc func()) {
|
||||
vmID := generateVMID()
|
||||
|
||||
return baiduApiTokenRefreshInterval * 1000, func() {
|
||||
// Only the Wasm VM that successfully acquires the lease will refresh the apiToken
|
||||
if g.config.tryAcquireOrRenewLease(vmID, log) {
|
||||
log.Debugf("Successfully acquired or renewed lease for baidu apiToken refresh task, vmID: %v", vmID)
|
||||
// Get the apiToken that is about to expire, will be removed after the new apiToken is obtained
|
||||
oldApiTokens, _, err := getApiTokens(g.config.failover.ctxApiTokens)
|
||||
if err != nil {
|
||||
log.Errorf("Get old apiToken failed: %v", err)
|
||||
return
|
||||
}
|
||||
log.Debugf("Old apiTokens: %v", oldApiTokens)
|
||||
|
||||
for _, accessKeyAndSecret := range g.config.baiduAccessKeyAndSecret {
|
||||
authorizationString := generateAuthorizationString(accessKeyAndSecret, baiduAuthorizationStringExpirationSeconds)
|
||||
log.Debugf("Generate authorizationString: %v", authorizationString)
|
||||
g.generateNewApiToken(authorizationString, log)
|
||||
}
|
||||
|
||||
// remove old old apiToken
|
||||
for _, token := range oldApiTokens {
|
||||
log.Debugf("Remove old apiToken: %v", token)
|
||||
removeApiToken(g.config.failover.ctxApiTokens, token, log)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *baiduProvider) generateNewApiToken(authorizationString string, log wrapper.Log) {
|
||||
client := wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: g.config.baiduApiTokenServiceName,
|
||||
Host: g.config.baiduApiTokenServiceHost,
|
||||
Port: g.config.baiduApiTokenServicePort,
|
||||
})
|
||||
|
||||
headers := [][2]string{
|
||||
{"content-type", "application/json"},
|
||||
{"Authorization", authorizationString},
|
||||
}
|
||||
|
||||
var apiToken string
|
||||
err := client.Get(baiduApiTokenPath, headers, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
if statusCode == 201 {
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(responseBody, &response)
|
||||
if err != nil {
|
||||
log.Errorf("Unmarshal response failed: %v", err)
|
||||
} else {
|
||||
apiToken = response["token"].(string)
|
||||
addApiToken(g.config.failover.ctxApiTokens, apiToken, log)
|
||||
}
|
||||
} else {
|
||||
log.Errorf("Get apiToken failed, status code: %d, response body: %s", statusCode, string(responseBody))
|
||||
}
|
||||
}, 30000)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Get apiToken failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ type claudeTextGenDelta struct {
|
||||
StopSequence *string `json:"stop_sequence"`
|
||||
}
|
||||
|
||||
func (c *claudeProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (c *claudeProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ const (
|
||||
type cloudflareProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (c *cloudflareProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (c *cloudflareProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ const (
|
||||
|
||||
type cohereProviderInitializer struct{}
|
||||
|
||||
func (m *cohereProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *cohereProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ const (
|
||||
|
||||
type cozeProviderInitializer struct{}
|
||||
|
||||
func (m *cozeProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *cozeProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ type deeplResponseTranslation struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func (d *deeplProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (d *deeplProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.targetLang == "" {
|
||||
return errors.New("missing targetLang in deepl provider config")
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ const (
|
||||
type deepseekProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (m *deepseekProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *deepseekProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
|
||||
307
plugins/wasm-go/extensions/ai-proxy/provider/dify.go
Normal file
307
plugins/wasm-go/extensions/ai-proxy/provider/dify.go
Normal file
@@ -0,0 +1,307 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
difyDomain = "api.dify.ai"
|
||||
difyChatPath = "/v1/chat-messages"
|
||||
difyCompletionPath = "/v1/completion-messages"
|
||||
difyWorkflowPath = "/v1/workflows/run"
|
||||
BotTypeChat = "Chat"
|
||||
BotTypeCompletion = "Completion"
|
||||
BotTypeWorkflow = "Workflow"
|
||||
BotTypeAgent = "Agent"
|
||||
)
|
||||
|
||||
type difyProviderInitializer struct{}
|
||||
|
||||
func (d *difyProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *difyProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
return &difyProvider{
|
||||
config: config,
|
||||
contextCache: createContextCache(&config),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type difyProvider struct {
|
||||
config ProviderConfig
|
||||
contextCache *contextCache
|
||||
}
|
||||
|
||||
func (d *difyProvider) GetProviderType() string {
|
||||
return providerTypeDify
|
||||
}
|
||||
|
||||
func (d *difyProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) error {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return errUnsupportedApiName
|
||||
}
|
||||
d.config.handleRequestHeaders(d, ctx, apiName, log)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *difyProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header, log wrapper.Log) {
|
||||
if d.config.difyApiUrl != "" {
|
||||
log.Debugf("use local host: %s", d.config.difyApiUrl)
|
||||
util.OverwriteRequestHostHeader(headers, d.config.difyApiUrl)
|
||||
} else {
|
||||
util.OverwriteRequestHostHeader(headers, difyDomain)
|
||||
}
|
||||
switch d.config.botType {
|
||||
case BotTypeChat, BotTypeAgent:
|
||||
util.OverwriteRequestPathHeader(headers, difyChatPath)
|
||||
case BotTypeCompletion:
|
||||
util.OverwriteRequestPathHeader(headers, difyCompletionPath)
|
||||
case BotTypeWorkflow:
|
||||
util.OverwriteRequestPathHeader(headers, difyWorkflowPath)
|
||||
}
|
||||
util.OverwriteRequestAuthorizationHeader(headers, "Bearer "+d.config.GetApiTokenInUse(ctx))
|
||||
}
|
||||
|
||||
func (d *difyProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
return d.config.handleRequestBody(d, d.contextCache, ctx, apiName, body, log)
|
||||
}
|
||||
|
||||
func (d *difyProvider) TransformRequestBodyHeaders(ctx wrapper.HttpContext, apiName ApiName, body []byte, headers http.Header, log wrapper.Log) ([]byte, error) {
|
||||
request := &chatCompletionRequest{}
|
||||
err := d.config.parseRequestAndMapModel(ctx, request, body, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
difyRequest := d.difyChatGenRequest(request)
|
||||
|
||||
return json.Marshal(difyRequest)
|
||||
}
|
||||
|
||||
func (d *difyProvider) TransformResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) ([]byte, error) {
|
||||
difyResponse := &DifyChatResponse{}
|
||||
if err := json.Unmarshal(body, difyResponse); err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal dify response: %v", err)
|
||||
}
|
||||
response := d.responseDify2OpenAI(ctx, difyResponse)
|
||||
return json.Marshal(response)
|
||||
}
|
||||
|
||||
func (d *difyProvider) responseDify2OpenAI(ctx wrapper.HttpContext, response *DifyChatResponse) *chatCompletionResponse {
|
||||
var choice chatCompletionChoice
|
||||
var id string
|
||||
switch d.config.botType {
|
||||
case BotTypeChat, BotTypeAgent:
|
||||
choice = chatCompletionChoice{
|
||||
Index: 0,
|
||||
Message: &chatMessage{Role: roleAssistant, Content: response.Answer},
|
||||
FinishReason: finishReasonStop,
|
||||
}
|
||||
//response header中增加conversationId字段
|
||||
_ = proxywasm.ReplaceHttpResponseHeader("ConversationId", response.ConversationId)
|
||||
id = response.ConversationId
|
||||
case BotTypeCompletion:
|
||||
choice = chatCompletionChoice{
|
||||
Index: 0,
|
||||
Message: &chatMessage{Role: roleAssistant, Content: response.Answer},
|
||||
FinishReason: finishReasonStop,
|
||||
}
|
||||
id = response.MessageId
|
||||
case BotTypeWorkflow:
|
||||
choice = chatCompletionChoice{
|
||||
Index: 0,
|
||||
Message: &chatMessage{Role: roleAssistant, Content: response.Data.Outputs[d.config.outputVariable]},
|
||||
FinishReason: finishReasonStop,
|
||||
}
|
||||
id = response.Data.WorkflowId
|
||||
}
|
||||
return &chatCompletionResponse{
|
||||
Id: id,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
SystemFingerprint: "",
|
||||
Object: objectChatCompletion,
|
||||
Choices: []chatCompletionChoice{choice},
|
||||
Usage: response.MetaData.Usage,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *difyProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name ApiName, chunk []byte, isLastChunk bool, log wrapper.Log) ([]byte, error) {
|
||||
if isLastChunk || len(chunk) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
// sample event response:
|
||||
// data: {"event": "agent_thought", "id": "8dcf3648-fbad-407a-85dd-73a6f43aeb9f", "task_id": "9cf1ddd7-f94b-459b-b942-b77b26c59e9b", "message_id": "1fb10045-55fd-4040-99e6-d048d07cbad3", "position": 1, "thought": "", "observation": "", "tool": "", "tool_input": "", "created_at": 1705639511, "message_files": [], "conversation_id": "c216c595-2d89-438c-b33c-aae5ddddd142"}
|
||||
|
||||
// sample end event response:
|
||||
// data: {"event": "message_end", "id": "5e52ce04-874b-4d27-9045-b3bc80def685", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "metadata": {"usage": {"prompt_tokens": 1033, "prompt_unit_price": "0.001", "prompt_price_unit": "0.001", "prompt_price": "0.0010330", "completion_tokens": 135, "completion_unit_price": "0.002", "completion_price_unit": "0.001", "completion_price": "0.0002700", "total_tokens": 1168, "total_price": "0.0013030", "currency": "USD", "latency": 1.381760165997548}, "retriever_resources": [{"position": 1, "dataset_id": "101b4c97-fc2e-463c-90b1-5261a4cdcafb", "dataset_name": "iPhone", "document_id": "8dd1ad74-0b5f-4175-b735-7d98bbbb4e00", "document_name": "iPhone List", "segment_id": "ed599c7f-2766-4294-9d1d-e5235a61270a", "score": 0.98457545, "content": "\"Model\",\"Release Date\",\"Display Size\",\"Resolution\",\"Processor\",\"RAM\",\"Storage\",\"Camera\",\"Battery\",\"Operating System\"\n\"iPhone 13 Pro Max\",\"September 24, 2021\",\"6.7 inch\",\"1284 x 2778\",\"Hexa-core (2x3.23 GHz Avalanche + 4x1.82 GHz Blizzard)\",\"6 GB\",\"128, 256, 512 GB, 1TB\",\"12 MP\",\"4352 mAh\",\"iOS 15\""}]}}
|
||||
responseBuilder := &strings.Builder{}
|
||||
lines := strings.Split(string(chunk), "\n")
|
||||
for _, data := range lines {
|
||||
if len(data) < 6 {
|
||||
// ignore blank line or wrong format
|
||||
continue
|
||||
}
|
||||
data = data[6:]
|
||||
var difyResponse DifyChunkChatResponse
|
||||
if err := json.Unmarshal([]byte(data), &difyResponse); err != nil {
|
||||
log.Errorf("unable to unmarshal dify response: %v", err)
|
||||
continue
|
||||
}
|
||||
response := d.streamResponseDify2OpenAI(ctx, &difyResponse)
|
||||
responseBody, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
log.Errorf("unable to marshal response: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
d.appendResponse(responseBuilder, string(responseBody))
|
||||
}
|
||||
modifiedResponseChunk := responseBuilder.String()
|
||||
log.Debugf("=== modified response chunk: %s", modifiedResponseChunk)
|
||||
return []byte(modifiedResponseChunk), nil
|
||||
}
|
||||
|
||||
func (d *difyProvider) streamResponseDify2OpenAI(ctx wrapper.HttpContext, response *DifyChunkChatResponse) *chatCompletionResponse {
|
||||
var choice chatCompletionChoice
|
||||
var id string
|
||||
switch d.config.botType {
|
||||
case BotTypeChat, BotTypeAgent:
|
||||
choice = chatCompletionChoice{
|
||||
Index: 0,
|
||||
Delta: &chatMessage{Role: roleAssistant, Content: response.Answer},
|
||||
}
|
||||
id = response.ConversationId
|
||||
_ = proxywasm.ReplaceHttpResponseHeader("ConversationId", response.ConversationId)
|
||||
case BotTypeCompletion:
|
||||
choice = chatCompletionChoice{
|
||||
Index: 0,
|
||||
Delta: &chatMessage{Role: roleAssistant, Content: response.Answer},
|
||||
}
|
||||
id = response.MessageId
|
||||
case BotTypeWorkflow:
|
||||
choice = chatCompletionChoice{
|
||||
Index: 0,
|
||||
Delta: &chatMessage{Role: roleAssistant, Content: response.Data.Outputs[d.config.outputVariable]},
|
||||
}
|
||||
id = response.Data.WorkflowId
|
||||
}
|
||||
if response.Event == "message_end" || response.Event == "workflow_finished" {
|
||||
choice.FinishReason = finishReasonStop
|
||||
}
|
||||
return &chatCompletionResponse{
|
||||
Id: id,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
SystemFingerprint: "",
|
||||
Object: objectChatCompletionChunk,
|
||||
Choices: []chatCompletionChoice{choice},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *difyProvider) appendResponse(responseBuilder *strings.Builder, responseBody string) {
|
||||
responseBuilder.WriteString(fmt.Sprintf("%s %s\n\n", streamDataItemKey, responseBody))
|
||||
}
|
||||
|
||||
func (d *difyProvider) difyChatGenRequest(request *chatCompletionRequest) *DifyChatRequest {
|
||||
content := ""
|
||||
for _, message := range request.Messages {
|
||||
if message.Role == "system" {
|
||||
content += "SYSTEM: \n" + message.StringContent() + "\n"
|
||||
} else if message.Role == "assistant" {
|
||||
content += "ASSISTANT: \n" + message.StringContent() + "\n"
|
||||
} else {
|
||||
content += "USER: \n" + message.StringContent() + "\n"
|
||||
}
|
||||
}
|
||||
mode := "blocking"
|
||||
if request.Stream {
|
||||
mode = "streaming"
|
||||
}
|
||||
user := request.User
|
||||
if user == "" {
|
||||
user = "api-user"
|
||||
}
|
||||
switch d.config.botType {
|
||||
case BotTypeChat, BotTypeAgent:
|
||||
conversationId, _ := proxywasm.GetHttpRequestHeader("ConversationId")
|
||||
return &DifyChatRequest{
|
||||
Inputs: make(map[string]interface{}),
|
||||
Query: content,
|
||||
ResponseMode: mode,
|
||||
User: user,
|
||||
AutoGenerateName: false,
|
||||
ConversationId: conversationId,
|
||||
}
|
||||
case BotTypeCompletion:
|
||||
return &DifyChatRequest{
|
||||
Inputs: map[string]interface{}{
|
||||
"query": content,
|
||||
},
|
||||
ResponseMode: mode,
|
||||
User: user,
|
||||
}
|
||||
case BotTypeWorkflow:
|
||||
return &DifyChatRequest{
|
||||
Inputs: map[string]interface{}{
|
||||
d.config.inputVariable: content,
|
||||
},
|
||||
ResponseMode: mode,
|
||||
User: user,
|
||||
}
|
||||
default:
|
||||
return &DifyChatRequest{}
|
||||
}
|
||||
}
|
||||
|
||||
type DifyChatRequest struct {
|
||||
Inputs map[string]interface{} `json:"inputs"`
|
||||
Query string `json:"query"`
|
||||
ResponseMode string `json:"response_mode"`
|
||||
User string `json:"user"`
|
||||
AutoGenerateName bool `json:"auto_generate_name"`
|
||||
ConversationId string `json:"conversation_id"`
|
||||
}
|
||||
|
||||
type DifyMetaData struct {
|
||||
Usage usage `json:"usage"`
|
||||
}
|
||||
|
||||
type DifyData struct {
|
||||
WorkflowId string `json:"workflow_id"`
|
||||
Id string `json:"id"`
|
||||
Outputs map[string]interface{} `json:"outputs"`
|
||||
}
|
||||
|
||||
type DifyChatResponse struct {
|
||||
ConversationId string `json:"conversation_id"`
|
||||
MessageId string `json:"message_id"`
|
||||
Answer string `json:"answer"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
Data DifyData `json:"data"`
|
||||
MetaData DifyMetaData `json:"metadata"`
|
||||
}
|
||||
|
||||
type DifyChunkChatResponse struct {
|
||||
Event string `json:"event"`
|
||||
ConversationId string `json:"conversation_id"`
|
||||
MessageId string `json:"message_id"`
|
||||
Answer string `json:"answer"`
|
||||
Data DifyData `json:"data"`
|
||||
MetaData DifyMetaData `json:"metadata"`
|
||||
}
|
||||
@@ -17,7 +17,7 @@ const (
|
||||
|
||||
type doubaoProviderInitializer struct{}
|
||||
|
||||
func (m *doubaoProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *doubaoProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/google/uuid"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/google/uuid"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
@@ -551,13 +551,14 @@ func (c *ProviderConfig) OnRequestFailed(activeProvider Provider, ctx wrapper.Ht
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) GetApiTokenInUse(ctx wrapper.HttpContext) string {
|
||||
return ctx.GetContext(c.failover.ctxApiTokenInUse).(string)
|
||||
token, _ := ctx.GetContext(c.failover.ctxApiTokenInUse).(string)
|
||||
return token
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) SetApiTokenInUse(ctx wrapper.HttpContext, log wrapper.Log) {
|
||||
var apiToken string
|
||||
if c.isFailoverEnabled() || c.useGlobalApiToken {
|
||||
// if enable apiToken failover, only use available apiToken
|
||||
// if enable apiToken failover, only use available apiToken from global apiTokens list
|
||||
if c.isFailoverEnabled() {
|
||||
apiToken = c.GetGlobalRandomToken(log)
|
||||
} else {
|
||||
apiToken = c.GetRandomToken()
|
||||
|
||||
@@ -28,7 +28,7 @@ const (
|
||||
type geminiProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (g *geminiProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (g *geminiProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ type githubProvider struct {
|
||||
contextCache *contextCache
|
||||
}
|
||||
|
||||
func (m *githubProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *githubProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ const (
|
||||
|
||||
type groqProviderInitializer struct{}
|
||||
|
||||
func (g *groqProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (g *groqProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ type hunyuanChatMessage struct {
|
||||
Content string `json:"Content,omitempty"`
|
||||
}
|
||||
|
||||
func (m *hunyuanProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *hunyuanProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
// 校验hunyuan id 和 key的合法性
|
||||
if len(config.hunyuanAuthId) != hunyuanAuthIdLen || len(config.hunyuanAuthKey) != hunyuanAuthKeyLen {
|
||||
return errors.New("hunyuanAuthId / hunyuanAuthKey is illegal in config file")
|
||||
|
||||
@@ -38,7 +38,7 @@ const (
|
||||
type minimaxProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (m *minimaxProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *minimaxProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
// If using the chat completion Pro API, a group ID must be set.
|
||||
if minimaxApiTypePro == config.minimaxApiType && config.minimaxGroupId == "" {
|
||||
return errors.New(fmt.Sprintf("missing minimaxGroupId in provider config when minimaxApiType is %s", minimaxApiTypePro))
|
||||
@@ -93,10 +93,6 @@ func (m *minimaxProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName
|
||||
}
|
||||
}
|
||||
|
||||
func (m *minimaxProvider) TransformRequestBodyHeaders(ctx wrapper.HttpContext, apiName ApiName, body []byte, headers http.Header, log wrapper.Log) ([]byte, error) {
|
||||
return m.handleRequestBodyByChatCompletionV2(body, headers, log)
|
||||
}
|
||||
|
||||
// handleRequestBodyByChatCompletionPro processes the request body using the chat completion Pro API.
|
||||
func (m *minimaxProvider) handleRequestBodyByChatCompletionPro(body []byte, log wrapper.Log) (types.Action, error) {
|
||||
request := &chatCompletionRequest{}
|
||||
@@ -109,7 +105,7 @@ func (m *minimaxProvider) handleRequestBodyByChatCompletionPro(body []byte, log
|
||||
_ = util.OverwriteRequestPath(fmt.Sprintf("%s?GroupId=%s", minimaxChatCompletionProPath, m.config.minimaxGroupId))
|
||||
|
||||
if m.config.context == nil {
|
||||
minimaxRequest := m.buildMinimaxChatCompletionV2Request(request, "")
|
||||
minimaxRequest := m.buildMinimaxChatCompletionProRequest(request, "")
|
||||
return types.ActionContinue, replaceJsonRequestBody(minimaxRequest, log)
|
||||
}
|
||||
|
||||
@@ -124,7 +120,7 @@ func (m *minimaxProvider) handleRequestBodyByChatCompletionPro(body []byte, log
|
||||
// Since minimaxChatCompletionV2 (format consistent with OpenAI) and minimaxChatCompletionPro (different format from OpenAI) have different logic for insertHttpContextMessage, we cannot unify them within one provider.
|
||||
// For minimaxChatCompletionPro, we need to manually handle context messages.
|
||||
// minimaxChatCompletionV2 uses the default defaultInsertHttpContextMessage method to insert context messages.
|
||||
minimaxRequest := m.buildMinimaxChatCompletionV2Request(request, content)
|
||||
minimaxRequest := m.buildMinimaxChatCompletionProRequest(request, content)
|
||||
if err := replaceJsonRequestBody(minimaxRequest, log); err != nil {
|
||||
util.ErrorHandler("ai-proxy.minimax.insert_ctx_failed", fmt.Errorf("failed to replace Request body: %v", err))
|
||||
}
|
||||
@@ -135,6 +131,10 @@ func (m *minimaxProvider) handleRequestBodyByChatCompletionPro(body []byte, log
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
|
||||
func (m *minimaxProvider) TransformRequestBodyHeaders(ctx wrapper.HttpContext, apiName ApiName, body []byte, headers http.Header, log wrapper.Log) ([]byte, error) {
|
||||
return m.handleRequestBodyByChatCompletionV2(body, headers, log)
|
||||
}
|
||||
|
||||
// handleRequestBodyByChatCompletionV2 processes the request body using the chat completion V2 API.
|
||||
func (m *minimaxProvider) handleRequestBodyByChatCompletionV2(body []byte, headers http.Header, log wrapper.Log) ([]byte, error) {
|
||||
util.OverwriteRequestPathHeader(headers, minimaxChatCompletionV2Path)
|
||||
@@ -144,15 +144,13 @@ func (m *minimaxProvider) handleRequestBodyByChatCompletionV2(body []byte, heade
|
||||
return sjson.SetBytes(body, "model", mappedModel)
|
||||
}
|
||||
|
||||
// Skip OnStreamingResponseBody() and OnResponseBody() when using original protocol.
|
||||
func (m *minimaxProvider) TransformResponseHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header, log wrapper.Log) {
|
||||
if m.config.protocol == protocolOriginal {
|
||||
ctx.DontReadResponseBody()
|
||||
}
|
||||
|
||||
// Skip OnStreamingResponseBody() and OnResponseBody() when the model corresponds to the chat completion V2 interface.
|
||||
if minimaxApiTypePro != m.config.minimaxApiType {
|
||||
// Skip OnStreamingResponseBody() and OnResponseBody() when using the original protocol
|
||||
// or when the model corresponds to the chat completion V2 interface.
|
||||
if m.config.protocol == protocolOriginal || minimaxApiTypePro != m.config.minimaxApiType {
|
||||
ctx.DontReadResponseBody()
|
||||
} else {
|
||||
headers.Del("Content-Length")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,12 +172,12 @@ func (m *minimaxProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name
|
||||
continue
|
||||
}
|
||||
data = data[6:]
|
||||
var minimaxResp minimaxChatCompletionV2Resp
|
||||
var minimaxResp minimaxChatCompletionProResp
|
||||
if err := json.Unmarshal([]byte(data), &minimaxResp); err != nil {
|
||||
log.Errorf("unable to unmarshal minimax response: %v", err)
|
||||
continue
|
||||
}
|
||||
response := m.responseV2ToOpenAI(&minimaxResp)
|
||||
response := m.responseProToOpenAI(&minimaxResp)
|
||||
responseBody, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
log.Errorf("unable to marshal response: %v", err)
|
||||
@@ -192,21 +190,21 @@ func (m *minimaxProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name
|
||||
return []byte(modifiedResponseChunk), nil
|
||||
}
|
||||
|
||||
// OnResponseBody handles the final response body from the Minimax service only for requests using the OpenAI protocol and corresponding to the chat completion Pro API.
|
||||
// TransformResponseBody handles the final response body from the Minimax service only for requests using the OpenAI protocol and corresponding to the chat completion Pro API.
|
||||
func (m *minimaxProvider) TransformResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) ([]byte, error) {
|
||||
minimaxResp := &minimaxChatCompletionV2Resp{}
|
||||
minimaxResp := &minimaxChatCompletionProResp{}
|
||||
if err := json.Unmarshal(body, minimaxResp); err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal minimax response: %v", err)
|
||||
}
|
||||
if minimaxResp.BaseResp.StatusCode != 0 {
|
||||
return nil, fmt.Errorf("minimax response error, error_code: %d, error_message: %s", minimaxResp.BaseResp.StatusCode, minimaxResp.BaseResp.StatusMsg)
|
||||
}
|
||||
response := m.responseV2ToOpenAI(minimaxResp)
|
||||
response := m.responseProToOpenAI(minimaxResp)
|
||||
return json.Marshal(response)
|
||||
}
|
||||
|
||||
// minimaxChatCompletionV2Request represents the structure of a chat completion V2 request.
|
||||
type minimaxChatCompletionV2Request struct {
|
||||
// minimaxChatCompletionProRequest represents the structure of a chat completion Pro request.
|
||||
type minimaxChatCompletionProRequest struct {
|
||||
Model string `json:"model"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
TokensToGenerate int64 `json:"tokens_to_generate,omitempty"`
|
||||
@@ -237,19 +235,17 @@ type minimaxReplyConstraints struct {
|
||||
SenderName string `json:"sender_name"`
|
||||
}
|
||||
|
||||
// minimaxChatCompletionV2Resp represents the structure of a Minimax Chat Completion V2 response.
|
||||
type minimaxChatCompletionV2Resp struct {
|
||||
Created int64 `json:"created"`
|
||||
Model string `json:"model"`
|
||||
Reply string `json:"reply"`
|
||||
InputSensitive bool `json:"input_sensitive,omitempty"`
|
||||
InputSensitiveType int64 `json:"input_sensitive_type,omitempty"`
|
||||
OutputSensitive bool `json:"output_sensitive,omitempty"`
|
||||
OutputSensitiveType int64 `json:"output_sensitive_type,omitempty"`
|
||||
Choices []minimaxChoice `json:"choices,omitempty"`
|
||||
Usage minimaxUsage `json:"usage,omitempty"`
|
||||
Id string `json:"id"`
|
||||
BaseResp minimaxBaseResp `json:"base_resp"`
|
||||
// minimaxChatCompletionProResp represents the structure of a Minimax Chat Completion Pro response.
|
||||
type minimaxChatCompletionProResp struct {
|
||||
Created int64 `json:"created"`
|
||||
Model string `json:"model"`
|
||||
Reply string `json:"reply"`
|
||||
InputSensitive bool `json:"input_sensitive,omitempty"`
|
||||
OutputSensitive bool `json:"output_sensitive,omitempty"`
|
||||
Choices []minimaxChoice `json:"choices,omitempty"`
|
||||
Usage minimaxUsage `json:"usage,omitempty"`
|
||||
Id string `json:"id"`
|
||||
BaseResp minimaxBaseResp `json:"base_resp"`
|
||||
}
|
||||
|
||||
// minimaxBaseResp contains error status code and details.
|
||||
@@ -267,7 +263,9 @@ type minimaxChoice struct {
|
||||
|
||||
// minimaxUsage represents token usage statistics.
|
||||
type minimaxUsage struct {
|
||||
TotalTokens int64 `json:"total_tokens"`
|
||||
TotalTokens int64 `json:"total_tokens"`
|
||||
PromptTokens int64 `json:"prompt_tokens"`
|
||||
CompletionTokens int64 `json:"completion_tokens"`
|
||||
}
|
||||
|
||||
func (m *minimaxProvider) parseModel(body []byte) (string, error) {
|
||||
@@ -282,7 +280,7 @@ func (m *minimaxProvider) parseModel(body []byte) (string, error) {
|
||||
return model, nil
|
||||
}
|
||||
|
||||
func (m *minimaxProvider) setBotSettings(request *minimaxChatCompletionV2Request, botSettingContent string) {
|
||||
func (m *minimaxProvider) setBotSettings(request *minimaxChatCompletionProRequest, botSettingContent string) {
|
||||
if len(request.BotSettings) == 0 {
|
||||
request.BotSettings = []minimaxBotSetting{
|
||||
{
|
||||
@@ -304,7 +302,7 @@ func (m *minimaxProvider) setBotSettings(request *minimaxChatCompletionV2Request
|
||||
}
|
||||
}
|
||||
|
||||
func (m *minimaxProvider) buildMinimaxChatCompletionV2Request(request *chatCompletionRequest, botSettingContent string) *minimaxChatCompletionV2Request {
|
||||
func (m *minimaxProvider) buildMinimaxChatCompletionProRequest(request *chatCompletionRequest, botSettingContent string) *minimaxChatCompletionProRequest {
|
||||
var messages []minimaxMessage
|
||||
var botSetting []minimaxBotSetting
|
||||
var botName string
|
||||
@@ -343,7 +341,7 @@ func (m *minimaxProvider) buildMinimaxChatCompletionV2Request(request *chatCompl
|
||||
SenderType: senderTypeBot,
|
||||
SenderName: determineName(botName, defaultBotName),
|
||||
}
|
||||
result := &minimaxChatCompletionV2Request{
|
||||
result := &minimaxChatCompletionProRequest{
|
||||
Model: request.Model,
|
||||
Stream: request.Stream,
|
||||
TokensToGenerate: int64(request.MaxTokens),
|
||||
@@ -359,7 +357,7 @@ func (m *minimaxProvider) buildMinimaxChatCompletionV2Request(request *chatCompl
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *minimaxProvider) responseV2ToOpenAI(response *minimaxChatCompletionV2Resp) *chatCompletionResponse {
|
||||
func (m *minimaxProvider) responseProToOpenAI(response *minimaxChatCompletionProResp) *chatCompletionResponse {
|
||||
var choices []chatCompletionChoice
|
||||
messageIndex := 0
|
||||
for _, choice := range response.Choices {
|
||||
@@ -384,7 +382,9 @@ func (m *minimaxProvider) responseV2ToOpenAI(response *minimaxChatCompletionV2Re
|
||||
Model: response.Model,
|
||||
Choices: choices,
|
||||
Usage: usage{
|
||||
TotalTokens: int(response.Usage.TotalTokens),
|
||||
TotalTokens: int(response.Usage.TotalTokens),
|
||||
PromptTokens: int(response.Usage.PromptTokens),
|
||||
CompletionTokens: int(response.Usage.CompletionTokens),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const (
|
||||
|
||||
type mistralProviderInitializer struct{}
|
||||
|
||||
func (m *mistralProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *mistralProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ const (
|
||||
type moonshotProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (m *moonshotProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *moonshotProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.moonshotFileId != "" && config.context != nil {
|
||||
return errors.New("moonshotFileId and context cannot be configured at the same time")
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ const (
|
||||
type ollamaProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (m *ollamaProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *ollamaProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.ollamaServerHost == "" {
|
||||
return errors.New("missing ollamaServerHost in provider config")
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ const (
|
||||
type openaiProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (m *openaiProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *openaiProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ const (
|
||||
providerTypeDoubao = "doubao"
|
||||
providerTypeCoze = "coze"
|
||||
providerTypeTogetherAI = "together-ai"
|
||||
providerTypeDify = "dify"
|
||||
|
||||
protocolOpenAI = "openai"
|
||||
protocolOriginal = "original"
|
||||
@@ -76,7 +77,7 @@ const (
|
||||
)
|
||||
|
||||
type providerInitializer interface {
|
||||
ValidateConfig(ProviderConfig) error
|
||||
ValidateConfig(*ProviderConfig) error
|
||||
CreateProvider(ProviderConfig) (Provider, error)
|
||||
}
|
||||
|
||||
@@ -110,6 +111,7 @@ var (
|
||||
providerTypeDoubao: &doubaoProviderInitializer{},
|
||||
providerTypeCoze: &cozeProviderInitializer{},
|
||||
providerTypeTogetherAI: &togetherAIProviderInitializer{},
|
||||
providerTypeDify: &difyProviderInitializer{},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -155,12 +157,6 @@ type TransformResponseBodyHandler interface {
|
||||
TransformResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) ([]byte, error)
|
||||
}
|
||||
|
||||
// TickFuncHandler allows the provider to execute a function periodically
|
||||
// Use case: the maximum expiration time of baidu apiToken is 24 hours, need to refresh periodically
|
||||
type TickFuncHandler interface {
|
||||
GetTickFunc(log wrapper.Log) (tickPeriod int64, tickFunc func())
|
||||
}
|
||||
|
||||
type ProviderConfig struct {
|
||||
// @Title zh-CN ID
|
||||
// @Description zh-CN AI服务提供商标识
|
||||
@@ -246,17 +242,14 @@ type ProviderConfig struct {
|
||||
// @Title zh-CN 自定义大模型参数配置
|
||||
// @Description zh-CN 用于填充或者覆盖大模型调用时的参数
|
||||
customSettings []CustomSetting
|
||||
// @Title zh-CN Baidu 的 Access Key 和 Secret Key,中间用 : 分隔,用于申请 apiToken
|
||||
baiduAccessKeyAndSecret []string `required:"false" yaml:"baiduAccessKeyAndSecret" json:"baiduAccessKeyAndSecret"`
|
||||
// @Title zh-CN 请求刷新百度 apiToken 服务名称
|
||||
baiduApiTokenServiceName string `required:"false" yaml:"baiduApiTokenServiceName" json:"baiduApiTokenServiceName"`
|
||||
// @Title zh-CN 请求刷新百度 apiToken 服务域名
|
||||
baiduApiTokenServiceHost string `required:"false" yaml:"baiduApiTokenServiceHost" json:"baiduApiTokenServiceHost"`
|
||||
// @Title zh-CN 请求刷新百度 apiToken 服务端口
|
||||
baiduApiTokenServicePort int64 `required:"false" yaml:"baiduApiTokenServicePort" json:"baiduApiTokenServicePort"`
|
||||
// @Title zh-CN 是否使用全局的 apiToken
|
||||
// @Description zh-CN 如果没有启用 apiToken failover,但是 apiToken 的状态又需要在多个 Wasm VM 中同步时需要将该参数设置为 true,例如 Baidu 的 apiToken 需要定时刷新
|
||||
useGlobalApiToken bool `required:"false" yaml:"useGlobalApiToken" json:"useGlobalApiToken"`
|
||||
// @Title zh-CN dify私有化部署的url
|
||||
difyApiUrl string `required:"false" yaml:"difyApiUrl" json:"difyApiUrl"`
|
||||
// @Title zh-CN dify的应用类型,Chat/Completion/Agent/Workflow
|
||||
botType string `required:"false" yaml:"botType" json:"botType"`
|
||||
// @Title zh-CN dify中应用类型为workflow时需要设置输入变量,当botType为workflow时一起使用
|
||||
inputVariable string `required:"false" yaml:"inputVariable" json:"inputVariable"`
|
||||
// @Title zh-CN dify中应用类型为workflow时需要设置输出变量,当botType为workflow时一起使用
|
||||
outputVariable string `required:"false" yaml:"outputVariable" json:"outputVariable"`
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) GetId() string {
|
||||
@@ -364,19 +357,10 @@ func (c *ProviderConfig) FromJson(json gjson.Result) {
|
||||
if retryOnFailureJson.Exists() {
|
||||
c.retryOnFailure.FromJson(retryOnFailureJson)
|
||||
}
|
||||
|
||||
for _, accessKeyAndSecret := range json.Get("baiduAccessKeyAndSecret").Array() {
|
||||
c.baiduAccessKeyAndSecret = append(c.baiduAccessKeyAndSecret, accessKeyAndSecret.String())
|
||||
}
|
||||
c.baiduApiTokenServiceName = json.Get("baiduApiTokenServiceName").String()
|
||||
c.baiduApiTokenServiceHost = json.Get("baiduApiTokenServiceHost").String()
|
||||
if c.baiduApiTokenServiceHost == "" {
|
||||
c.baiduApiTokenServiceHost = baiduApiTokenDomain
|
||||
}
|
||||
c.baiduApiTokenServicePort = json.Get("baiduApiTokenServicePort").Int()
|
||||
if c.baiduApiTokenServicePort == 0 {
|
||||
c.baiduApiTokenServicePort = baiduApiTokenPort
|
||||
}
|
||||
c.difyApiUrl = json.Get("difyApiUrl").String()
|
||||
c.botType = json.Get("botType").String()
|
||||
c.inputVariable = json.Get("inputVariable").String()
|
||||
c.outputVariable = json.Get("outputVariable").String()
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) Validate() error {
|
||||
@@ -405,7 +389,7 @@ func (c *ProviderConfig) Validate() error {
|
||||
if !has {
|
||||
return errors.New("unknown provider type: " + c.typ)
|
||||
}
|
||||
if err := initializer.ValidateConfig(*c); err != nil {
|
||||
if err := initializer.ValidateConfig(c); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -42,7 +42,7 @@ const (
|
||||
type qwenProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (m *qwenProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *qwenProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if len(config.qwenFileIds) != 0 && config.context != nil {
|
||||
return errors.New("qwenFileIds and context cannot be configured at the same time")
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ type sparkStreamResponse struct {
|
||||
Created int64 `json:"created"`
|
||||
}
|
||||
|
||||
func (i *sparkProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (i *sparkProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const (
|
||||
type stepfunProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (m *stepfunProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *stepfunProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ const (
|
||||
|
||||
type togetherAIProviderInitializer struct{}
|
||||
|
||||
func (m *togetherAIProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *togetherAIProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ const (
|
||||
type yiProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (m *yiProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *yiProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ const (
|
||||
|
||||
type zhipuAiProviderInitializer struct{}
|
||||
|
||||
func (m *zhipuAiProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *zhipuAiProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -215,35 +216,51 @@ func onHttpStreamingResponseBody(ctx wrapper.HttpContext, config QuotaConfig, da
|
||||
if chatMode == ChatModeNone || chatMode == ChatModeAdmin {
|
||||
return data
|
||||
}
|
||||
var inputToken, outputToken int64
|
||||
var consumer string
|
||||
if inputToken, outputToken, ok := getUsage(data); ok {
|
||||
ctx.SetContext("input_token", inputToken)
|
||||
ctx.SetContext("output_token", outputToken)
|
||||
}
|
||||
|
||||
// chat completion mode
|
||||
if !endOfStream {
|
||||
return data
|
||||
}
|
||||
inputTokenStr, err := proxywasm.GetProperty([]string{"filter_state", "wasm.input_token"})
|
||||
if err != nil {
|
||||
|
||||
if ctx.GetContext("input_token") == nil || ctx.GetContext("output_token") == nil || ctx.GetContext("consumer") == nil {
|
||||
return data
|
||||
}
|
||||
outputTokenStr, err := proxywasm.GetProperty([]string{"filter_state", "wasm.output_token"})
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
inputToken, err := strconv.Atoi(string(inputTokenStr))
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
outputToken, err := strconv.Atoi(string(outputTokenStr))
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
consumer, ok := ctx.GetContext("consumer").(string)
|
||||
if ok {
|
||||
totalToken := int(inputToken + outputToken)
|
||||
log.Debugf("update consumer:%s, totalToken:%d", consumer, totalToken)
|
||||
config.redisClient.DecrBy(config.RedisKeyPrefix+consumer, totalToken, nil)
|
||||
}
|
||||
|
||||
inputToken = ctx.GetContext("input_token").(int64)
|
||||
outputToken = ctx.GetContext("output_token").(int64)
|
||||
consumer = ctx.GetContext("consumer").(string)
|
||||
totalToken := int(inputToken + outputToken)
|
||||
log.Debugf("update consumer:%s, totalToken:%d", consumer, totalToken)
|
||||
config.redisClient.DecrBy(config.RedisKeyPrefix+consumer, totalToken, nil)
|
||||
return data
|
||||
}
|
||||
|
||||
func getUsage(data []byte) (inputTokenUsage int64, outputTokenUsage int64, ok bool) {
|
||||
chunks := bytes.Split(bytes.TrimSpace(data), []byte("\n\n"))
|
||||
for _, chunk := range chunks {
|
||||
// the feature strings are used to identify the usage data, like:
|
||||
// {"model":"gpt2","usage":{"prompt_tokens":1,"completion_tokens":1}}
|
||||
if !bytes.Contains(chunk, []byte("prompt_tokens")) || !bytes.Contains(chunk, []byte("completion_tokens")) {
|
||||
continue
|
||||
}
|
||||
inputTokenObj := gjson.GetBytes(chunk, "usage.prompt_tokens")
|
||||
outputTokenObj := gjson.GetBytes(chunk, "usage.completion_tokens")
|
||||
if inputTokenObj.Exists() && outputTokenObj.Exists() {
|
||||
inputTokenUsage = inputTokenObj.Int()
|
||||
outputTokenUsage = outputTokenObj.Int()
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func deniedNoKeyAuthData() types.Action {
|
||||
util.SendResponse(http.StatusUnauthorized, "ai-quota.no_key", "text/plain", "Request denied by ai quota check. No Key Authentication information found.")
|
||||
return types.ActionContinue
|
||||
|
||||
@@ -41,9 +41,9 @@ const (
|
||||
LowRisk = "low"
|
||||
NoRisk = "none"
|
||||
|
||||
OpenAIResponseFormat = `{"id": "%s","object":"chat.completion","model":"%s","choices":[{"index":0,"message":{"role":"assistant","content":"%s"},"logprobs":null,"finish_reason":"stop"}]}`
|
||||
OpenAIStreamResponseChunk = `data:{"id":"%s","object":"chat.completion.chunk","model":"%s","choices":[{"index":0,"delta":{"role":"assistant","content":"%s"},"logprobs":null,"finish_reason":null}]}`
|
||||
OpenAIStreamResponseEnd = `data:{"id":"%s","object":"chat.completion.chunk","model":"%s","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]}`
|
||||
OpenAIResponseFormat = `{"id": "%s","object":"chat.completion","model":"from-security-guard","choices":[{"index":0,"message":{"role":"assistant","content":"%s"},"logprobs":null,"finish_reason":"stop"}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}`
|
||||
OpenAIStreamResponseChunk = `data:{"id":"%s","object":"chat.completion.chunk","model":"from-security-guard","choices":[{"index":0,"delta":{"role":"assistant","content":"%s"},"logprobs":null,"finish_reason":null}]}`
|
||||
OpenAIStreamResponseEnd = `data:{"id":"%s","object":"chat.completion.chunk","model":"from-security-guard","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}`
|
||||
OpenAIStreamResponseFormat = OpenAIStreamResponseChunk + "\n\n" + OpenAIStreamResponseEnd + "\n\n" + `data: [DONE]`
|
||||
|
||||
DefaultRequestCheckService = "llm_query_moderation"
|
||||
@@ -262,8 +262,6 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config AISecurityConfig, body []
|
||||
log.Debugf("checking request body...")
|
||||
startTime := time.Now().UnixMilli()
|
||||
content := gjson.GetBytes(body, config.requestContentJsonPath).String()
|
||||
model := gjson.GetBytes(body, "model").String()
|
||||
ctx.SetContext("requestModel", model)
|
||||
log.Debugf("Raw request content is: %s", content)
|
||||
if len(content) == 0 {
|
||||
log.Info("request content is empty. skip")
|
||||
@@ -308,11 +306,11 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config AISecurityConfig, body []
|
||||
proxywasm.SendHttpResponse(uint32(config.denyCode), [][2]string{{"content-type", "application/json"}}, []byte(marshalledDenyMessage), -1)
|
||||
} else if gjson.GetBytes(body, "stream").Bool() {
|
||||
randomID := generateRandomID()
|
||||
jsonData := []byte(fmt.Sprintf(OpenAIStreamResponseFormat, randomID, model, marshalledDenyMessage, randomID, model))
|
||||
jsonData := []byte(fmt.Sprintf(OpenAIStreamResponseFormat, randomID, marshalledDenyMessage, randomID))
|
||||
proxywasm.SendHttpResponse(uint32(config.denyCode), [][2]string{{"content-type", "text/event-stream;charset=UTF-8"}}, jsonData, -1)
|
||||
} else {
|
||||
randomID := generateRandomID()
|
||||
jsonData := []byte(fmt.Sprintf(OpenAIResponseFormat, randomID, model, marshalledDenyMessage))
|
||||
jsonData := []byte(fmt.Sprintf(OpenAIResponseFormat, randomID, marshalledDenyMessage))
|
||||
proxywasm.SendHttpResponse(uint32(config.denyCode), [][2]string{{"content-type", "application/json"}}, jsonData, -1)
|
||||
}
|
||||
ctx.DontReadResponseBody()
|
||||
@@ -369,15 +367,6 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config AISecurityConfig, body []
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
func convertHeaders(hs [][2]string) map[string][]string {
|
||||
ret := make(map[string][]string)
|
||||
for _, h := range hs {
|
||||
k, v := strings.ToLower(h[0]), h[1]
|
||||
ret[k] = append(ret[k], v)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config AISecurityConfig, log wrapper.Log) types.Action {
|
||||
if !config.checkResponse {
|
||||
log.Debugf("response checking is disabled")
|
||||
@@ -398,7 +387,6 @@ func onHttpResponseBody(ctx wrapper.HttpContext, config AISecurityConfig, body [
|
||||
startTime := time.Now().UnixMilli()
|
||||
contentType, _ := proxywasm.GetHttpResponseHeader("content-type")
|
||||
isStreamingResponse := strings.Contains(contentType, "event-stream")
|
||||
model := ctx.GetStringContext("requestModel", "unknown")
|
||||
var content string
|
||||
if isStreamingResponse {
|
||||
content = extractMessageFromStreamingBody(body, config.responseStreamContentJsonPath)
|
||||
@@ -449,11 +437,11 @@ func onHttpResponseBody(ctx wrapper.HttpContext, config AISecurityConfig, body [
|
||||
proxywasm.SendHttpResponse(uint32(config.denyCode), [][2]string{{"content-type", "application/json"}}, []byte(marshalledDenyMessage), -1)
|
||||
} else if isStreamingResponse {
|
||||
randomID := generateRandomID()
|
||||
jsonData := []byte(fmt.Sprintf(OpenAIStreamResponseFormat, randomID, model, marshalledDenyMessage, randomID, model))
|
||||
jsonData := []byte(fmt.Sprintf(OpenAIStreamResponseFormat, randomID, marshalledDenyMessage, randomID))
|
||||
proxywasm.SendHttpResponse(uint32(config.denyCode), [][2]string{{"content-type", "text/event-stream;charset=UTF-8"}}, jsonData, -1)
|
||||
} else {
|
||||
randomID := generateRandomID()
|
||||
jsonData := []byte(fmt.Sprintf(OpenAIResponseFormat, randomID, model, marshalledDenyMessage))
|
||||
jsonData := []byte(fmt.Sprintf(OpenAIResponseFormat, randomID, marshalledDenyMessage))
|
||||
proxywasm.SendHttpResponse(uint32(config.denyCode), [][2]string{{"content-type", "application/json"}}, jsonData, -1)
|
||||
}
|
||||
config.incrementCounter("ai_sec_response_deny", 1)
|
||||
|
||||
@@ -38,7 +38,7 @@ Attribute 配置说明:
|
||||
`value_source` 的各种取值含义如下:
|
||||
|
||||
- `fixed_value`:固定值
|
||||
- `requeset_header` : attrribute 值通过 http 请求头获取,value 配置为 header key
|
||||
- `request_header` : attrribute 值通过 http 请求头获取,value 配置为 header key
|
||||
- `request_body` :attrribute 值通过请求 body 获取,value 配置格式为 gjson 的 jsonpath
|
||||
- `response_header` :attrribute 值通过 http 响应头获取,value 配置为header key
|
||||
- `response_body` :attrribute 值通过响应 body 获取,value 配置格式为 gjson 的 jsonpath
|
||||
|
||||
@@ -38,7 +38,7 @@ Attribute Configuration instructions:
|
||||
The meanings of various values for `value_source` are as follows:
|
||||
|
||||
- `fixed_value`: fixed value
|
||||
- `requeset_header`: The attrribute is obtained through the http request header
|
||||
- `request_header`: The attrribute is obtained through the http request header
|
||||
- `request_body`: The attrribute is obtained through the http request body
|
||||
- `response_header`: The attrribute is obtained through the http response header
|
||||
- `response_body`: The attrribute is obtained through the http response body
|
||||
|
||||
@@ -36,6 +36,7 @@ const (
|
||||
RouteName = "route"
|
||||
ClusterName = "cluster"
|
||||
APIName = "api"
|
||||
ConsumerKey = "x-mse-consumer"
|
||||
|
||||
// Source Type
|
||||
FixedValue = "fixed_value"
|
||||
@@ -81,8 +82,8 @@ type AIStatisticsConfig struct {
|
||||
shouldBufferStreamingBody bool
|
||||
}
|
||||
|
||||
func generateMetricName(route, cluster, model, metricName string) string {
|
||||
return fmt.Sprintf("route.%s.upstream.%s.model.%s.metric.%s", route, cluster, model, metricName)
|
||||
func generateMetricName(route, cluster, model, consumer, metricName string) string {
|
||||
return fmt.Sprintf("route.%s.upstream.%s.model.%s.consumer.%s.metric.%s", route, cluster, model, consumer, metricName)
|
||||
}
|
||||
|
||||
func getRouteName() (string, error) {
|
||||
@@ -115,6 +116,9 @@ func getClusterName() (string, error) {
|
||||
}
|
||||
|
||||
func (config *AIStatisticsConfig) incrementCounter(metricName string, inc uint64) {
|
||||
if inc == 0 {
|
||||
return
|
||||
}
|
||||
counter, ok := config.counterMetrics[metricName]
|
||||
if !ok {
|
||||
counter = proxywasm.DefineCounterMetric(metricName)
|
||||
@@ -158,6 +162,9 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config AIStatisticsConfig, lo
|
||||
ctx.SetContext(ClusterName, cluster)
|
||||
ctx.SetUserAttribute(APIName, api)
|
||||
ctx.SetContext(StatisticsRequestStartTime, time.Now().UnixMilli())
|
||||
if consumer, _ := proxywasm.GetHttpRequestHeader(ConsumerKey); consumer != "" {
|
||||
ctx.SetContext(ConsumerKey, consumer)
|
||||
}
|
||||
|
||||
// Set user defined log & span attributes which type is fixed_value
|
||||
setAttributeBySource(ctx, config, FixedValue, nil, log)
|
||||
@@ -388,6 +395,7 @@ func writeMetric(ctx wrapper.HttpContext, config AIStatisticsConfig, log wrapper
|
||||
var ok bool
|
||||
var route, cluster, model string
|
||||
var inputToken, outputToken uint64
|
||||
consumer := ctx.GetStringContext(ConsumerKey, "none")
|
||||
route, ok = ctx.GetContext(RouteName).(string)
|
||||
if !ok {
|
||||
log.Warnf("RouteName typd assert failed, skip metric record")
|
||||
@@ -421,8 +429,8 @@ func writeMetric(ctx wrapper.HttpContext, config AIStatisticsConfig, log wrapper
|
||||
log.Warnf("inputToken and outputToken cannot equal to 0, skip metric record")
|
||||
return
|
||||
}
|
||||
config.incrementCounter(generateMetricName(route, cluster, model, InputToken), inputToken)
|
||||
config.incrementCounter(generateMetricName(route, cluster, model, OutputToken), outputToken)
|
||||
config.incrementCounter(generateMetricName(route, cluster, model, consumer, InputToken), inputToken)
|
||||
config.incrementCounter(generateMetricName(route, cluster, model, consumer, OutputToken), outputToken)
|
||||
|
||||
// Generate duration metrics
|
||||
var llmFirstTokenDuration, llmServiceDuration uint64
|
||||
@@ -433,8 +441,8 @@ func writeMetric(ctx wrapper.HttpContext, config AIStatisticsConfig, log wrapper
|
||||
log.Warnf("LLMFirstTokenDuration typd assert failed")
|
||||
return
|
||||
}
|
||||
config.incrementCounter(generateMetricName(route, cluster, model, LLMFirstTokenDuration), llmFirstTokenDuration)
|
||||
config.incrementCounter(generateMetricName(route, cluster, model, LLMStreamDurationCount), 1)
|
||||
config.incrementCounter(generateMetricName(route, cluster, model, consumer, LLMFirstTokenDuration), llmFirstTokenDuration)
|
||||
config.incrementCounter(generateMetricName(route, cluster, model, consumer, LLMStreamDurationCount), 1)
|
||||
}
|
||||
if ctx.GetUserAttribute(LLMServiceDuration) != nil {
|
||||
llmServiceDuration, ok = convertToUInt(ctx.GetUserAttribute(LLMServiceDuration))
|
||||
@@ -442,8 +450,8 @@ func writeMetric(ctx wrapper.HttpContext, config AIStatisticsConfig, log wrapper
|
||||
log.Warnf("LLMServiceDuration typd assert failed")
|
||||
return
|
||||
}
|
||||
config.incrementCounter(generateMetricName(route, cluster, model, LLMServiceDuration), llmServiceDuration)
|
||||
config.incrementCounter(generateMetricName(route, cluster, model, LLMDurationCount), 1)
|
||||
config.incrementCounter(generateMetricName(route, cluster, model, consumer, LLMServiceDuration), llmServiceDuration)
|
||||
config.incrementCounter(generateMetricName(route, cluster, model, consumer, LLMDurationCount), 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -194,3 +194,205 @@ rejected_msg: '{"code":-1,"msg":"Too many requests"}'
|
||||
redis:
|
||||
service_name: redis.static
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
AI Token 限流插件依赖 Redis 记录剩余可用的 token 数,因此首先需要部署 Redis 服务。
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
ports:
|
||||
- port: 6379
|
||||
targetPort: 6379
|
||||
selector:
|
||||
app: redis
|
||||
---
|
||||
```
|
||||
|
||||
在本例中,使用通义千问作为 AI 服务提供商。另外还需要设置 AI 统计插件,因为 AI Token 限流插件依赖 AI 统计插件计算每次请求消耗的 token 数,以下配置限制每分钟的 input 和 output token 总数为 200 个。
|
||||
|
||||
```yaml
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: ai-proxy
|
||||
namespace: higress-system
|
||||
spec:
|
||||
matchRules:
|
||||
- config:
|
||||
provider:
|
||||
type: qwen
|
||||
apiTokens:
|
||||
- "<YOUR_API_TOKEN>"
|
||||
modelMapping:
|
||||
'gpt-3': "qwen-turbo"
|
||||
'gpt-35-turbo': "qwen-plus"
|
||||
'gpt-4-turbo': "qwen-max"
|
||||
'*': "qwen-turbo"
|
||||
ingress:
|
||||
- qwen
|
||||
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/ai-proxy:v1.0.0
|
||||
phase: UNSPECIFIED_PHASE
|
||||
priority: 100
|
||||
---
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: ai-statistics
|
||||
namespace: higress-system
|
||||
spec:
|
||||
defaultConfig:
|
||||
enable: true
|
||||
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/ai-token-statistics:v1.0.0
|
||||
phase: UNSPECIFIED_PHASE
|
||||
priority: 200
|
||||
---
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: ai-token-ratelimit
|
||||
namespace: higress-system
|
||||
spec:
|
||||
defaultConfig:
|
||||
rule_name: default_limit_by_param_apikey
|
||||
rule_items:
|
||||
- limit_by_param: apikey
|
||||
limit_keys:
|
||||
- key: 123456
|
||||
token_per_minute: 200
|
||||
redis:
|
||||
# 默认情况下,为了减轻数据面的压力,Higress 的 global.onlyPushRouteCluster 配置参数被设置为 true,意味着不会自动发现 Kubernetes Service
|
||||
# 如果需要使用 Kubernetes Service 作为服务发现,可以将 global.onlyPushRouteCluster 参数设置为 false,
|
||||
# 这样就可以直接将 service_name 设置为 Kubernetes Service, 而无须为 Redis 创建 McpBridge 以及 Ingress 路由
|
||||
# service_name: redis.default.svc.cluster.local
|
||||
service_name: redis.dns
|
||||
service_port: 6379
|
||||
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/ai-token-ratelimit:v1.0.0
|
||||
phase: UNSPECIFIED_PHASE
|
||||
priority: 600
|
||||
```
|
||||
注意,AI Token 限流插件中的 Redis 配置项 `service_name` 来自 McpBridge 中配置的服务来源,另外我们还需要在 McpBridge 中配置通义千问服务的访问地址。
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.higress.io/v1
|
||||
kind: McpBridge
|
||||
metadata:
|
||||
name: default
|
||||
namespace: higress-system
|
||||
spec:
|
||||
registries:
|
||||
- domain: dashscope.aliyuncs.com
|
||||
name: qwen
|
||||
port: 443
|
||||
type: dns
|
||||
- domain: redis.default.svc.cluster.local # Kubernetes Service
|
||||
name: redis
|
||||
type: dns
|
||||
port: 6379
|
||||
```
|
||||
|
||||
分别创建两条路由规则。
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
higress.io/backend-protocol: HTTPS
|
||||
higress.io/destination: qwen.dns
|
||||
higress.io/proxy-ssl-name: dashscope.aliyuncs.com
|
||||
higress.io/proxy-ssl-server-name: "on"
|
||||
labels:
|
||||
higress.io/resource-definer: higress
|
||||
name: qwen
|
||||
namespace: higress-system
|
||||
spec:
|
||||
ingressClassName: higress
|
||||
rules:
|
||||
- host: qwen-test.com
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
resource:
|
||||
apiGroup: networking.higress.io
|
||||
kind: McpBridge
|
||||
name: default
|
||||
path: /
|
||||
pathType: Prefix
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
higress.io/destination: redis.dns
|
||||
higress.io/ignore-path-case: "false"
|
||||
labels:
|
||||
higress.io/resource-definer: higress
|
||||
name: redis
|
||||
spec:
|
||||
ingressClassName: higress
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- backend:
|
||||
resource:
|
||||
apiGroup: networking.higress.io
|
||||
kind: McpBridge
|
||||
name: default
|
||||
path: /
|
||||
pathType: Prefix
|
||||
```
|
||||
|
||||
触发限流效果如下:
|
||||
|
||||
```bash
|
||||
curl "http://qwen-test.com:18000/v1/chat/completions?apikey=123456" -H "Content-Type: application/json" -d '{
|
||||
"model": "gpt-3",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}'
|
||||
{"id":"88cfa80f-545d-93b4-8ff3-3f5245ca33ba","choices":[{"index":0,"message":{"role":"assistant","content":"我是通义千问,由阿里云开发的AI助手。我可以回答各种问题、提供信息和与用户进行对话。有什么我可以帮助你的吗?"},"finish_reason":"stop"}],"created":1719909825,"model":"qwen-turbo","object":"chat.completion","usage":{"prompt_tokens":13,"completion_tokens":33,"total_tokens":46}}
|
||||
curl "http://qwen-test.com:18000/v1/chat/completions?apikey=123456" -H "Content-Type: application/json" -d '{
|
||||
"model": "gpt-3",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}'
|
||||
Too many requests # 限流成功
|
||||
```
|
||||
|
||||
@@ -168,3 +168,207 @@ rejected_msg: '{"code":-1,"msg":"Too many requests"}'
|
||||
redis:
|
||||
service_name: redis.static
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
The AI Token Rate Limiting Plugin relies on Redis to track the remaining available tokens, so the Redis service must be deployed first.
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
ports:
|
||||
- port: 6379
|
||||
targetPort: 6379
|
||||
selector:
|
||||
app: redis
|
||||
---
|
||||
```
|
||||
|
||||
In this example, qwen is used as the AI service provider. Additionally, the AI Statistics Plugin must be configured, as the AI Token Rate Limiting Plugin depends on it to calculate the number of tokens consumed per request. The following configuration limits the total number of input and output tokens to 200 per minute.
|
||||
|
||||
```yaml
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: ai-proxy
|
||||
namespace: higress-system
|
||||
spec:
|
||||
matchRules:
|
||||
- config:
|
||||
provider:
|
||||
type: qwen
|
||||
apiTokens:
|
||||
- "<YOUR_API_TOKEN>"
|
||||
modelMapping:
|
||||
'gpt-3': "qwen-turbo"
|
||||
'gpt-35-turbo': "qwen-plus"
|
||||
'gpt-4-turbo': "qwen-max"
|
||||
'*': "qwen-turbo"
|
||||
ingress:
|
||||
- qwen
|
||||
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/ai-proxy:v1.0.0
|
||||
phase: UNSPECIFIED_PHASE
|
||||
priority: 100
|
||||
---
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: ai-statistics
|
||||
namespace: higress-system
|
||||
spec:
|
||||
defaultConfig:
|
||||
enable: true
|
||||
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/ai-token-statistics:v1.0.0
|
||||
phase: UNSPECIFIED_PHASE
|
||||
priority: 200
|
||||
---
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: ai-token-ratelimit
|
||||
namespace: higress-system
|
||||
spec:
|
||||
defaultConfig:
|
||||
rule_name: default_limit_by_param_apikey
|
||||
rule_items:
|
||||
- limit_by_param: apikey
|
||||
limit_keys:
|
||||
- key: 123456
|
||||
token_per_minute: 200
|
||||
redis:
|
||||
# By default, to reduce data plane pressure, the `global.onlyPushRouteCluster` parameter in Higress is set to true, meaning that Kubernetes Services are not automatically discovered.
|
||||
# If you need to use Kubernetes Service for service discovery, set `global.onlyPushRouteCluster` to false,
|
||||
# allowing you to directly set `service_name` to the Kubernetes Service without needing to create an McpBridge and an Ingress route for Redis.
|
||||
# service_name: redis.default.svc.cluster.local
|
||||
service_name: redis.dns
|
||||
service_port: 6379
|
||||
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/ai-token-ratelimit:v1.0.0
|
||||
phase: UNSPECIFIED_PHASE
|
||||
priority: 600
|
||||
```
|
||||
|
||||
Note that the `service_name` in the Redis configuration of the AI Token Rate Limiting Plugin is derived from the service source configured in McpBridge. Additionally, we need to configure the access address of the qnwen service in McpBridge.
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.higress.io/v1
|
||||
kind: McpBridge
|
||||
metadata:
|
||||
name: default
|
||||
namespace: higress-system
|
||||
spec:
|
||||
registries:
|
||||
- domain: dashscope.aliyuncs.com
|
||||
name: qwen
|
||||
port: 443
|
||||
type: dns
|
||||
- domain: redis.default.svc.cluster.local # Kubernetes Service
|
||||
name: redis
|
||||
type: dns
|
||||
port: 6379
|
||||
```
|
||||
|
||||
Create two routing rules separately.
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
higress.io/backend-protocol: HTTPS
|
||||
higress.io/destination: qwen.dns
|
||||
higress.io/proxy-ssl-name: dashscope.aliyuncs.com
|
||||
higress.io/proxy-ssl-server-name: "on"
|
||||
labels:
|
||||
higress.io/resource-definer: higress
|
||||
name: qwen
|
||||
namespace: higress-system
|
||||
spec:
|
||||
ingressClassName: higress
|
||||
rules:
|
||||
- host: qwen-test.com
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
resource:
|
||||
apiGroup: networking.higress.io
|
||||
kind: McpBridge
|
||||
name: default
|
||||
path: /
|
||||
pathType: Prefix
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
higress.io/destination: redis.dns
|
||||
higress.io/ignore-path-case: "false"
|
||||
labels:
|
||||
higress.io/resource-definer: higress
|
||||
name: redis
|
||||
spec:
|
||||
ingressClassName: higress
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- backend:
|
||||
resource:
|
||||
apiGroup: networking.higress.io
|
||||
kind: McpBridge
|
||||
name: default
|
||||
path: /
|
||||
pathType: Prefix
|
||||
```
|
||||
|
||||
The rate limiting effect is triggered as follows:
|
||||
|
||||
```bash
|
||||
curl "http://qwen-test.com:18000/v1/chat/completions?apikey=123456" -H "Content-Type: application/json" -d '{
|
||||
"model": "gpt-3",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello, who are you?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}'
|
||||
{"id":"88cfa80f-545d-93b4-8ff3-3f5245ca33ba","choices":[{"index":0,"message":{"role":"assistant","content":"I am Tongyi Qianwen, an AI assistant developed by Alibaba Cloud. I can answer various questions, provide information, and have conversations with users. How can I assist you?"},"finish_reason":"stop"}],"created":1719909825,"model":"qwen-turbo","object":"chat.completion","usage":{"prompt_tokens":13,"completion_tokens":33,"total_tokens":46}}
|
||||
curl "http://qwen-test.com:18000/v1/chat/completions?apikey=123456" -H "Content-Type: application/json" -d '{
|
||||
"model": "gpt-3",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello, who are you?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}'
|
||||
Too many requests # Rate limiting successful
|
||||
```
|
||||
|
||||
@@ -16,70 +16,114 @@ description: Ext 认证插件实现了调用外部授权服务进行认证鉴权
|
||||
|
||||
## 配置字段
|
||||
|
||||
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|
||||
| ------------------------------- | -------- | ---- | ------ |------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `http_service` | object | 是 | - | 外部授权服务配置 |
|
||||
| `failure_mode_allow` | bool | 否 | false | 当设置为 true 时,即使与授权服务的通信失败,或者授权服务返回了 HTTP 5xx 错误,仍会接受客户端请求 |
|
||||
| `failure_mode_allow_header_add` | bool | 否 | false | 当 `failure_mode_allow` 和 `failure_mode_allow_header_add` 都设置为 true 时,若与授权服务的通信失败,或授权服务返回了 HTTP 5xx 错误,那么请求头中将会添加 `x-envoy-auth-failure-mode-allowed: true` |
|
||||
| `status_on_error` | int | 否 | 403 | 当授权服务无法访问或状态码为 5xx 时,设置返回给客户端的 HTTP 状态码。默认状态码是 `403` |
|
||||
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|
||||
| ------------------------------- | ------------------ | ---- | ------ | ------------------------------------------------------------ |
|
||||
| `http_service` | object | 是 | - | 外部授权服务配置 |
|
||||
| `match_type` | string | 否 | | 可选 `whitelist` 或 `blacklist` |
|
||||
| `match_list` | array of MatchRule | 否 | | 一个包含 (`match_rule_domain`, `match_rule_path`, `match_rule_type`) 的列表 |
|
||||
| `failure_mode_allow` | bool | 否 | false | 当设置为 true 时,即使与授权服务的通信失败,或者授权服务返回了 HTTP 5xx 错误,仍会接受客户端请求 |
|
||||
| `failure_mode_allow_header_add` | bool | 否 | false | 当 `failure_mode_allow` 和 `failure_mode_allow_header_add` 都设置为 true 时,若与授权服务的通信失败,或授权服务返回了 HTTP 5xx 错误,那么请求头中将会添加 `x-envoy-auth-failure-mode-allowed: true` |
|
||||
| `status_on_error` | int | 否 | 403 | 当授权服务无法访问或状态码为 5xx 时,设置返回给客户端的 HTTP 状态码。默认状态码是 `403` |
|
||||
|
||||
`http_service`中每一项的配置字段说明
|
||||
`http_service` 中每一项的配置字段说明
|
||||
|
||||
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|
||||
| ------------------------ | -------- | ---- | ------ | ------------------------------------- |
|
||||
| `endpoint_mode` | string | 否 | envoy | `envoy` , `forward_auth` 中选填一项 |
|
||||
|--------------------------|----------|------|--------|---------------------------------------|
|
||||
| `endpoint_mode` | string | 否 | envoy | 可选 `envoy` 或 `forward_auth` |
|
||||
| `endpoint` | object | 是 | - | 发送鉴权请求的 HTTP 服务信息 |
|
||||
| `timeout` | int | 否 | 1000 | `ext-auth` 服务连接超时时间,单位毫秒 |
|
||||
| `authorization_request` | object | 否 | - | 发送鉴权请求配置 |
|
||||
| `authorization_response` | object | 否 | - | 处理鉴权响应配置 |
|
||||
| `authorization_response` | object | 否 | - | 处理鉴权响应配置 |
|
||||
|
||||
`endpoint`中每一项的配置字段说明
|
||||
`endpoint` 中每一项的配置字段说明
|
||||
|
||||
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|
||||
| -------- | -------- | -- | ------ | ----------------------------------------------------------------------------------------- |
|
||||
| `service_name` | string | 必填 | - | 输入授权服务名称,带服务类型的完整 FQDN 名称,例如 `ext-auth.dns` 、`ext-auth.my-ns.svc.cluster.local` |
|
||||
| `service_port` | int | 否 | 80 | 输入授权服务的服务端口 |
|
||||
| `service_host` | string | 否 | - | 请求授权服务时设置的Host头,不填时和FQDN保持一致 |
|
||||
| `path_prefix` | string | `endpoint_mode` 为`envoy`时必填 | | `endpoint_mode` 为`envoy` 时,客户端向授权服务发送请求的请求路径前缀 |
|
||||
| `request_method` | string | 否 | GET | `endpoint_mode` 为`forward_auth` 时,客户端向授权服务发送请求的HTTP Method |
|
||||
| `path` | string | `endpoint_mode` 为`forward_auth`时必填 | - | `endpoint_mode` 为`forward_auth` 时,客户端向授权服务发送请求的请求路径 |
|
||||
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|
||||
|------------------|----------|----------------------------------------|--------|--------------------------------------------------------------|
|
||||
| `service_name` | string | 是 | - | 输入授权服务名称,带服务类型的完整 FQDN 名称,例如 `ext-auth.dns` 、`ext-auth.my-ns.svc.cluster.local` |
|
||||
| `service_port` | int | 否 | 80 | 输入授权服务的服务端口 |
|
||||
| `service_host` | string | 否 | - | 请求授权服务时设置的 Host 头,不填时和 FQDN 保持一致 |
|
||||
| `path_prefix` | string | `endpoint_mode` 为 `envoy` 时必填 | - | `endpoint_mode` 为 `envoy` 时,客户端向授权服务发送请求的请求路径前缀 |
|
||||
| `request_method` | string | 否 | GET | `endpoint_mode` 为 `forward_auth` 时,客户端向授权服务发送请求的 HTTP Method |
|
||||
| `path` | string | `endpoint_mode` 为 `forward_auth` 时必填 | - | `endpoint_mode` 为 `forward_auth` 时,客户端向授权服务发送请求的请求路径 |
|
||||
|
||||
`authorization_request`中每一项的配置字段说明
|
||||
`authorization_request` 中每一项的配置字段说明
|
||||
|
||||
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|
||||
| ------------------------ | ---------------------- | ---- | ------ |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `allowed_headers` | array of StringMatcher | 否 | - | 当设置后,具有相应匹配项的客户端请求头将添加到授权服务请求中的请求头中。除了用户自定义的头部匹配规则外,授权服务请求中会自动包含 `Authorization` 这个HTTP头( `endpoint_mode` 为 `forward_auth` 时,会把原始请求的请求路径设置到 `X-Original-Uri` ,原始请求的HTTP Method设置到 `X-Original-Method` ) |
|
||||
| `headers_to_add` | `map[string]string` | 否 | - | 设置将包含在授权服务请求中的请求头列表。请注意,同名的客户端请求头将被覆盖 |
|
||||
| `with_request_body` | bool | 否 | false | 缓冲客户端请求体,并将其发送至鉴权请求中(HTTP Method为GET、OPTIONS、HEAD请求时不生效) |
|
||||
| `max_request_body_bytes` | int | 否 | 10MB | 设置在内存中保存客户端请求体的最大尺寸。当客户端请求体达到在此字段中设置的数值时,将会返回HTTP 413状态码,并且不会启动授权过程。注意,这个设置会优先于 `failure_mode_allow` 的配置 |
|
||||
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|
||||
|--------------------------|------------------------|------|--------|--------------------------------------------------------------|
|
||||
| `allowed_headers` | array of StringMatcher | 否 | - | 设置后,匹配项的客户端请求头将添加到授权服务请求中的请求头中。除了用户自定义的头部匹配规则外,授权服务请求中会自动包含 `Authorization` 这个HTTP头(`endpoint_mode` 为 `forward_auth` 时,会添加 `X-Forwarded-*` 的请求头) |
|
||||
| `headers_to_add` | map[string]string | 否 | - | 设置将包含在授权服务请求中的请求头列表。请注意,同名的客户端请求头将被覆盖 |
|
||||
| `with_request_body` | bool | 否 | false | 缓冲客户端请求体,并将其发送至鉴权请求中(HTTP Method为GET、OPTIONS、HEAD请求时不生效) |
|
||||
| `max_request_body_bytes` | int | 否 | 10MB | 设置在内存中保存客户端请求体的最大尺寸。当客户端请求体达到在此字段中设置的数值时,将会返回HTTP 413状态码,并且不会启动授权过程。注意,这个设置会优先于 `failure_mode_allow` 的配置 |
|
||||
|
||||
`authorization_response`中每一项的配置字段说明
|
||||
`authorization_response` 中每一项的配置字段说明
|
||||
|
||||
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|
||||
| -------------------------- | ---------------------- | ---- | ------ |---------------------------------------------------------------------------------|
|
||||
| `allowed_upstream_headers` | array of StringMatcher | 否 | - | 当设置后,具有相应匹配项的鉴权请求的响应头将添加到原始的客户端请求头中。请注意,同名的请求头将被覆盖 |
|
||||
| `allowed_client_headers` | array of StringMatcher | 否 | - | 如果不设置,在请求被拒绝时,所有的鉴权请求的响应头将添加到客户端的响应头中。当设置后,在请求被拒绝时,具有相应匹配项的鉴权请求的响应头将添加到客户端的响应头中 |
|
||||
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|
||||
|----------------------------|------------------------|------|--------|--------------------------------------------------------------|
|
||||
| `allowed_upstream_headers` | array of StringMatcher | 否 | - | 匹配项的鉴权请求的响应头将添加到原始的客户端请求头中。请注意,同名的请求头将被覆盖 |
|
||||
| `allowed_client_headers` | array of StringMatcher | 否 | - | 如果不设置,在请求被拒绝时,所有的鉴权请求的响应头将添加到客户端的响应头中。当设置后,在请求被拒绝时,匹配项的鉴权请求的响应头将添加到客户端的响应头中 |
|
||||
|
||||
`StringMatcher`类型每一项的配置字段说明,在使用`array of StringMatcher`时会按照数组中定义的StringMatcher顺序依次进行配置
|
||||
`StringMatcher` 类型每一项的配置字段说明,在使用 `array of StringMatcher` 时会按照数组中定义的 StringMatcher 顺序依次进行配置
|
||||
|
||||
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|
||||
| ---------- | -------- | ------------------------------------------------------------ | ------ | -------- |
|
||||
|------------|----------|-------------------------------------------------------------|--------|----------|
|
||||
| `exact` | string | 否,`exact` , `prefix` , `suffix`, `contains`, `regex` 中选填一项 | - | 精确匹配 |
|
||||
| `prefix` | string | 否,`exact` , `prefix` , `suffix`, `contains`, `regex` 中选填一项 | - | 前缀匹配 |
|
||||
| `suffix` | string | 否,`exact` , `prefix` , `suffix`, `contains`, `regex` 中选填一项 | - | 后缀匹配 |
|
||||
| `contains` | string | 否,`exact` , `prefix` , `suffix`, `contains`, `regex` 中选填一项 | - | 是否包含 |
|
||||
| `regex` | string | 否,`exact` , `prefix` , `suffix`, `contains`, `regex` 中选填一项 | - | 正则匹配 |
|
||||
|
||||
MatchRule 类型每一项的配置字段说明,在使用 `array of MatchRule` 时会按照数组中定义的 MatchRule 顺序依次进行配置
|
||||
|
||||
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|
||||
| ------------------- | -------- | ---- | ------ | ------------------------------------------------------------ |
|
||||
| `match_rule_domain` | string | 否 | - | 匹配规则域名,支持通配符模式,例如 `*.bar.com` |
|
||||
| `match_rule_path` | string | 否 | - | 匹配请求路径的规则 |
|
||||
| `match_rule_type` | string | 否 | - | 匹配请求路径的规则类型,可选 `exact` , `prefix` , `suffix`, `contains`, `regex` |
|
||||
|
||||
### 两种 `endpoint_mode` 的区别
|
||||
|
||||
`endpoint_mode` 为 `envoy` 时,鉴权请求会使用原始请求的 HTTP Method,和配置的 `path_prefix` 作为请求路径前缀拼接上原始的请求路径
|
||||
|
||||
`endpoint_mode` 为 `forward_auth` 时,鉴权请求会使用配置的 `request_method` 作为 HTTP Method,和配置的 `path` 作为请求路径,并且 Higress 会自动生成并发送以下 header 至鉴权服务:
|
||||
|
||||
| Header | 说明 |
|
||||
| -------------------- | ------------------------------------------------------ |
|
||||
| `x-forwarded-proto` | 原始请求的scheme,比如 http/https |
|
||||
| `x-forwarded-method` | 原始请求的方法,比如 get/post/delete/patch |
|
||||
| `x-forwarded-host` | 原始请求的host |
|
||||
| `x-forwarded-uri` | 原始请求的path,包含路径参数,比如 `/v1/app?test=true` |
|
||||
|
||||
### 黑白名单模式
|
||||
|
||||
支持黑白名单模式配置,默认为白名单模式,白名单为空,即所有请求都需要经过验证,匹配域名支持泛域名例如 `*.bar.com` ,匹配规则支持 `exact` , `prefix` , `suffix`, `contains`, `regex`
|
||||
|
||||
**白名单模式**
|
||||
|
||||
```yaml
|
||||
match_type: 'whitelist'
|
||||
match_list:
|
||||
- match_rule_domain: '*.bar.com'
|
||||
match_rule_path: '/foo'
|
||||
match_rule_type: 'prefix'
|
||||
```
|
||||
|
||||
泛域名 `*.bar.com` 下前缀匹配 `/foo` 的请求无需验证
|
||||
|
||||
**黑名单模式**
|
||||
|
||||
```yaml
|
||||
match_type: 'blacklist'
|
||||
match_list:
|
||||
- match_rule_domain: '*.bar.com'
|
||||
match_rule_path: '/headers'
|
||||
match_rule_type: 'prefix'
|
||||
```
|
||||
|
||||
只有泛域名 `*.bar.com` 下前缀匹配 `/header` 的请求需要验证
|
||||
|
||||
## 配置示例
|
||||
|
||||
下面假设 `ext-auth` 服务在Kubernetes中serviceName为 `ext-auth`,端口 `8090`,路径为 `/auth`,命名空间为 `backend`
|
||||
|
||||
支持两种 `endpoint_mode`:
|
||||
|
||||
- `endpoint_mode` 为 `envoy` 时,鉴权请求会使用原始请求的HTTP Method,和配置的 `path_prefix` 作为请求路径前缀拼接上原始的请求路径
|
||||
- `endpoint_mode` 为 `forward_auth` 时,鉴权请求会使用配置的 `request_method` 作为HTTP Method,和配置的 `path` 作为请求路径
|
||||
下面假设 `ext-auth` 服务在 Kubernetes 中 serviceName 为 `ext-auth`,端口 `8090`,路径为 `/auth`,命名空间为 `backend`
|
||||
|
||||
### endpoint_mode为envoy时
|
||||
|
||||
@@ -198,7 +242,7 @@ http_service:
|
||||
使用如下请求网关,当开启 `ext-auth` 插件后:
|
||||
|
||||
```shell
|
||||
curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx"
|
||||
curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx" -H "Host: foo.bar.com"
|
||||
```
|
||||
|
||||
**请求 `ext-auth` 服务成功:**
|
||||
@@ -209,8 +253,10 @@ curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
|
||||
POST /auth HTTP/1.1
|
||||
Host: ext-auth.backend.svc.cluster.local
|
||||
Authorization: xxx
|
||||
X-Original-Uri: /users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
|
||||
X-Original-Method: GET
|
||||
X-Forwarded-Proto: HTTP
|
||||
X-Forwarded-Host: foo.bar.com
|
||||
X-Forwarded-Uri: /users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
|
||||
X-Forwarded-Method: GET
|
||||
Content-Length: 0
|
||||
```
|
||||
|
||||
@@ -261,7 +307,7 @@ http_service:
|
||||
使用如下请求网关,当开启 `ext-auth` 插件后:
|
||||
|
||||
```shell
|
||||
curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx" -H "X-Auth-Version: 1.0"
|
||||
curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx" -H "X-Auth-Version: 1.0" -H "Host: foo.bar.com"
|
||||
```
|
||||
|
||||
`ext-auth` 服务将接收到如下的鉴权请求:
|
||||
@@ -270,22 +316,13 @@ curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
|
||||
POST /auth HTTP/1.1
|
||||
Host: my-domain.local
|
||||
Authorization: xxx
|
||||
X-Original-Uri: /users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
|
||||
X-Original-Method: GET
|
||||
X-Forwarded-Proto: HTTP
|
||||
X-Forwarded-Host: foo.bar.com
|
||||
X-Forwarded-Uri: /users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
|
||||
X-Forwarded-Method: GET
|
||||
X-Auth-Version: 1.0
|
||||
x-envoy-header: true
|
||||
Content-Length: 0
|
||||
```
|
||||
|
||||
`ext-auth` 服务返回响应头中如果包含 `x-user-id` 和 `x-auth-version`,网关调用upstream时的请求中会带上这两个请求头
|
||||
|
||||
#### x-forwarded-* header
|
||||
在endpoint_mode为forward_auth时,higress会自动生成并发送以下header至鉴权服务。
|
||||
|
||||
| Header | 说明 |
|
||||
|--------------------|-------------------------------------|
|
||||
| x-forwarded-proto | 原始请求的scheme,比如http/https |
|
||||
| x-forwarded-method | 原始请求的方法,比如get/post/delete/patch |
|
||||
| x-forwarded-host | 原始请求的host |
|
||||
| x-forwarded-uri | 原始请求的path,包含路径参数,比如/v1/app?test=true |
|
||||
| x-forwarded-for | 原始请求的客户端IP地址 |
|
||||
`ext-auth` 服务返回响应头中如果包含 `x-user-id` 和 `x-auth-version`,网关调用upstream时的请求中会带上这两个请求头
|
||||
@@ -3,73 +3,135 @@ title: External Authentication
|
||||
keywords: [higress, auth]
|
||||
description: The Ext Authentication plugin implements the capability to call external authorization services for authentication and authorization.
|
||||
---
|
||||
## Function Description
|
||||
The `ext-auth` plugin implements sending authentication requests to an external authorization service to check whether the client request is authorized. This plugin is implemented with reference to Envoy's native [ext_authz filter](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_authz_filter), which covers some capabilities for connecting to HTTP services.
|
||||
|
||||
## Execution Properties
|
||||
Plugin Execution Phase: `Authentication Phase`
|
||||
## Feature Description
|
||||
|
||||
The `ext-auth` plugin sends an authorization request to an external authorization service to check if the client request is authorized. When implementing this plugin, it refers to the native [ext_authz filter](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_authz_filter) of Envoy, and realizes part of the capabilities of the native filter to connect to an HTTP service.
|
||||
|
||||
## Operating Attributes
|
||||
|
||||
Plugin Execution Phase: `Authentication Phase`
|
||||
Plugin Execution Priority: `360`
|
||||
|
||||
|
||||
## Configuration Fields
|
||||
| Name | Data Type | Required | Default Value | Description |
|
||||
| ------------------------------- | --------- | -------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `http_service` | object | Yes | - | Configuration for the external authorization service |
|
||||
| `failure_mode_allow` | bool | No | false | When set to true, client requests will still be accepted even if communication with the authorization service fails or the authorization service returns an HTTP 5xx error |
|
||||
| `failure_mode_allow_header_add` | bool | No | false | When both `failure_mode_allow` and `failure_mode_allow_header_add` are set to true, if communication with the authorization service fails or returns an HTTP 5xx error, the request header will include `x-envoy-auth-failure-mode-allowed: true` |
|
||||
| `status_on_error` | int | No | 403 | Sets the HTTP status code returned to the client when the authorization service is unreachable or returns a 5xx status code. The default status code is `403` |
|
||||
|
||||
### Configuration Fields for Each Item in `http_service`
|
||||
| Name | Data Type | Required | Default Value | Description |
|
||||
| ------------------------ | --------- | -------- | ------------- | -------------------------------------------- |
|
||||
| `endpoint_mode` | string | No | envoy | Select either `envoy` or `forward_auth` as an optional choice |
|
||||
| `endpoint` | object | Yes | - | Information about the HTTP service for sending authentication requests |
|
||||
| `timeout` | int | No | 1000 | Connection timeout for `ext-auth` service, in milliseconds |
|
||||
| `authorization_request` | object | No | - | Configuration for sending authentication requests |
|
||||
| `authorization_response` | object | No | - | Configuration for processing authentication responses |
|
||||
| Name | Data Type | Required | Default Value | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `http_service` | object | Yes | - | Configuration for the external authorization service |
|
||||
| `match_type` | string | No | | Can be `whitelist` or `blacklist` |
|
||||
| `match_list` | array of MatchRule | No | | A list containing (`match_rule_domain`, `match_rule_path`, `match_rule_type`) |
|
||||
| `failure_mode_allow` | bool | No | false | When set to true, client requests will be accepted even if the communication with the authorization service fails or the authorization service returns an HTTP 5xx error |
|
||||
| `failure_mode_allow_header_add` | bool | No | false | When both `failure_mode_allow` and `failure_mode_allow_header_add` are set to true, if the communication with the authorization service fails or the authorization service returns an HTTP 5xx error, the `x-envoy-auth-failure-mode-allowed: true` header will be added to the request header |
|
||||
| `status_on_error` | int | No | 403 | Sets the HTTP status code returned to the client when the authorization service is inaccessible or has a 5xx status code. The default status code is `403` |
|
||||
|
||||
### Configuration Fields for Each Item in `endpoint`
|
||||
| Name | Data Type | Required | Default Value | Description |
|
||||
| ---------------- | --------- | ---------------------- | ------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| `service_name` | string | Required | - | Input the name of the authorization service, in complete FQDN format, e.g., `ext-auth.dns` or `ext-auth.my-ns.svc.cluster.local` |
|
||||
| `service_port` | int | No | 80 | Input the port of the authorization service |
|
||||
| `service_host` | string | No | - | The Host header set when requesting the authorization service; remains the same as FQDN if not filled |
|
||||
| `path_prefix` | string | Required when `endpoint_mode` is `envoy` | | Request path prefix for the client when sending requests to the authorization service |
|
||||
| `request_method` | string | No | GET | HTTP Method for client requests to the authorization service when `endpoint_mode` is `forward_auth` |
|
||||
| `path` | string | Required when `endpoint_mode` is `forward_auth` | - | Request path for the client when sending requests to the authorization service |
|
||||
Configuration fields for each item in `http_service`
|
||||
|
||||
### Configuration Fields for Each Item in `authorization_request`
|
||||
| Name | Data Type | Required | Default Value | Description |
|
||||
| ------------------------ | ---------------------- | -------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `allowed_headers` | array of StringMatcher | No | - | When set, client request headers with matching criteria will be added to the headers of the request to the authorization service. The `Authorization` HTTP header will be automatically included in the authorization service request, and if `endpoint_mode` is `forward_auth`, the original request path will be set to `X-Original-Uri` and the original request HTTP method will be set to `X-Original-Method`. |
|
||||
| `headers_to_add` | `map[string]string` | No | - | Sets the list of request headers to include in the authorization service request. Note that headers with the same name from the client will be overwritten. |
|
||||
| `with_request_body` | bool | No | false | Buffer the client request body and send it in the authentication request (does not take effect for HTTP Methods GET, OPTIONS, and HEAD) |
|
||||
| `max_request_body_bytes` | int | No | 10MB | Sets the maximum size of the client request body to keep in memory. When the client request body reaches the value set in this field, an HTTP 413 status code will be returned, and the authorization process will not start. Note that this setting takes precedence over the `failure_mode_allow` configuration. |
|
||||
| Name | Data Type | Required | Default Value | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `endpoint_mode` | string | No | envoy | Can be `envoy` or `forward_auth` |
|
||||
| `endpoint` | object | Yes | - | Information about the HTTP service to which the authentication request is sent |
|
||||
| `timeout` | int | No | 1000 | The connection timeout for the `ext-auth` service in milliseconds |
|
||||
| `authorization_request` | object | No | - | Configuration for sending the authentication request |
|
||||
| `authorization_response` | object | No | - | Configuration for handling the authentication response |
|
||||
|
||||
### Configuration Fields for Each Item in `authorization_response`
|
||||
| Name | Data Type | Required | Default Value | Description |
|
||||
| -------------------------- | ---------------------- | -------- | ------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `allowed_upstream_headers` | array of StringMatcher | No | - | When set, the response headers of the authorization request with matching criteria will be added to the original client request headers. Note that headers with the same name will be overwritten. |
|
||||
| `allowed_client_headers` | array of StringMatcher | No | - | If not set, all response headers from authorization requests will be added to the client’s response when a request is denied. When set, response headers from authorization requests with matching criteria will be added to the client's response when a request is denied. |
|
||||
Configuration fields for each item in `endpoint`
|
||||
|
||||
### Field Descriptions for `StringMatcher` Type
|
||||
When using `array of StringMatcher`, the fields are configured according to the order defined in the array.
|
||||
| Name | Data Type | Required | Default Value | Description |
|
||||
| ---------- | --------- | --------------------------------------------------- | ------------- | ----------- |
|
||||
| `exact` | string | No, must select one from `exact`, `prefix`, `suffix`, `contains`, `regex` | - | Exact match |
|
||||
| `prefix` | string | No, must select one from `exact`, `prefix`, `suffix`, `contains`, `regex` | - | Prefix match |
|
||||
| `suffix` | string | No, must select one from `exact`, `prefix`, `suffix`, `contains`, `regex` | - | Suffix match |
|
||||
| `contains` | string | No, must select one from `exact`, `prefix`, `suffix`, `contains`, `regex` | - | Contains match |
|
||||
| `regex` | string | No, must select one from `exact`, `prefix`, `suffix`, `contains`, `regex` | - | Regex match |
|
||||
| Name | Data Type | Required | Default Value | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `service_name` | string | Yes | - | Enter the name of the authorization service, the full FQDN name with service type, e.g., `ext-auth.dns`, `ext-auth.my-ns.svc.cluster.local` |
|
||||
| `service_port` | int | No | 80 | Enter the service port of the authorization service |
|
||||
| `service_host` | string | No | - | The Host header set when requesting the authorization service. If not filled, it will be the same as the FQDN |
|
||||
| `path_prefix` | string | Required when `endpoint_mode` is `envoy` | - | When `endpoint_mode` is `envoy`, the request path prefix for the client to send a request to the authorization service |
|
||||
| `request_method` | string | No | GET | When `endpoint_mode` is `forward_auth`, the HTTP Method for the client to send a request to the authorization service |
|
||||
| `path` | string | Required when `endpoint_mode` is `forward_auth` | - | When `endpoint_mode` is `forward_auth`, the request path for the client to send a request to the authorization service |
|
||||
|
||||
## Configuration Example
|
||||
Assuming the `ext-auth` service has a serviceName of `ext-auth`, port `8090`, path `/auth`, and namespace `backend` in Kubernetes.
|
||||
Configuration fields for each item in `authorization_request`
|
||||
|
||||
Two types of `endpoint_mode` are supported:
|
||||
- When `endpoint_mode` is `envoy`, the authentication request will use the original request HTTP Method, and the configured `path_prefix` will be concatenated with the original request path.
|
||||
- When `endpoint_mode` is `forward_auth`, the authentication request will use the configured `request_method` as the HTTP Method and the configured `path` as the request path.
|
||||
| Name | Data Type | Required | Default Value | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `allowed_headers` | array of StringMatcher | No | - | After setting, the client request headers that match the items will be added to the request headers in the authorization service request. In addition to the user-defined header matching rules, the `Authorization` HTTP header will be automatically included in the authorization service request (when `endpoint_mode` is `forward_auth`, the `X-Forwarded-*` request headers will be added) |
|
||||
| `headers_to_add` | map[string]string | No | - | Sets the list of request headers to be included in the authorization service request. Please note that the client request headers with the same name will be overwritten |
|
||||
| `with_request_body` | bool | No | false | Buffer the client request body and send it to the authentication request (not effective for HTTP Method GET, OPTIONS, HEAD requests) |
|
||||
| `max_request_body_bytes` | int | No | 10MB | Sets the maximum size of the client request body to be saved in memory. When the client request body reaches the value set in this field, an HTTP 413 status code will be returned and the authorization process will not be started. Note that this setting takes precedence over the `failure_mode_allow` configuration |
|
||||
|
||||
Configuration fields for each item in `authorization_response`
|
||||
|
||||
| Name | Data Type | Required | Default Value | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `allowed_upstream_headers` | array of StringMatcher | No | - | The response headers of the authentication request that match the items will be added to the original client request headers. Please note that the request headers with the same name will be overwritten |
|
||||
| `allowed_client_headers` | array of StringMatcher | No | - | If not set, when the request is rejected, all the response headers of the authentication request will be added to the client's response headers. When set, when the request is rejected, the response headers of the authentication request that match the items will be added to the client's response headers |
|
||||
|
||||
Configuration fields for each item of `StringMatcher` type. When using `array of StringMatcher`, the StringMatchers defined in the array will be configured in order.
|
||||
|
||||
| Name | Data Type | Required | Default Value | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `exact` | string | No, one of `exact`, `prefix`, `suffix`, `contains`, `regex` must be selected | - | Exact match |
|
||||
| `prefix` | string | No, one of `exact`, `prefix`, `suffix`, `contains`, `regex` must be selected | - | Prefix match |
|
||||
| `suffix` | string | No, one of `exact`, `prefix`, `suffix`, `contains`, `regex` must be selected | - | Suffix match |
|
||||
| `contains` | string | No, one of `exact`, `prefix`, `suffix`, `contains`, `regex` must be selected | - | Contains |
|
||||
| `regex` | string | No, one of `exact`, `prefix`, `suffix`, `contains`, `regex` must be selected | - | Regular expression match |
|
||||
|
||||
Configuration fields for each item of `MatchRule` type. When using `array of MatchRule`, the MatchRules defined in the array will be configured in order.
|
||||
|
||||
| Name | Data Type | Required | Default Value | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `match_rule_domain` | string | No | - | The domain of the matching rule, supports wildcard patterns, e.g., `*.bar.com` |
|
||||
| `match_rule_path` | string | No | - | The rule for matching the request path |
|
||||
| `match_rule_type` | string | No | - | The type of the rule for matching the request path, can be `exact`, `prefix`, `suffix`, `contains`, `regex` |
|
||||
|
||||
### Differences between the two `endpoint_mode`
|
||||
|
||||
When `endpoint_mode` is `envoy`, the authentication request will use the original request's HTTP Method and the configured `path_prefix` as the request path prefix, concatenated with the original request path.
|
||||
|
||||
When `endpoint_mode` is `forward_auth`, the authentication request will use the configured `request_method` as the HTTP Method and the configured `path` as the request path. Higress will automatically generate and send the following headers to the authorization service:
|
||||
|
||||
| Header | Description |
|
||||
| --- | --- |
|
||||
| `x-forwarded-proto` | The scheme of the original request, such as http/https |
|
||||
| `x-forwarded-method` | The method of the original request, such as get/post/delete/patch |
|
||||
| `x-forwarded-host` | The host of the original request |
|
||||
| `x-forwarded-uri` | The path of the original request, including path parameters, e.g., `/v1/app?test=true` |
|
||||
|
||||
### Blacklist and Whitelist Modes
|
||||
|
||||
Supports blacklist and whitelist mode configuration. The default is the whitelist mode. If the whitelist is empty, all requests need to be verified. The matching domain supports wildcard domains such as `*.bar.com`, and the matching rule supports `exact`, `prefix`, `suffix`, `contains`, `regex`.
|
||||
|
||||
**Whitelist Mode**
|
||||
|
||||
```yaml
|
||||
match_type: 'whitelist'
|
||||
match_list:
|
||||
- match_rule_domain: '*.bar.com'
|
||||
match_rule_path: '/foo'
|
||||
match_rule_type: 'prefix'
|
||||
```
|
||||
|
||||
Requests with a prefix match of `/foo` under the wildcard domain `*.bar.com` do not need to be verified.
|
||||
|
||||
**Blacklist Mode**
|
||||
|
||||
```yaml
|
||||
match_type: 'blacklist'
|
||||
match_list:
|
||||
- match_rule_domain: '*.bar.com'
|
||||
match_rule_path: '/headers'
|
||||
match_rule_type: 'prefix'
|
||||
```
|
||||
|
||||
Only requests with a prefix match of `/header` under the wildcard domain `*.bar.com` need to be verified.
|
||||
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
Assume that in Kubernetes, the `ext-auth` service has a `serviceName` of `ext-auth`, a port of `8090`, a path of `/auth`, and is in the `backend` namespace.
|
||||
|
||||
### When endpoint_mode is envoy
|
||||
|
||||
#### Example 1
|
||||
|
||||
Configuration of the `ext-auth` plugin:
|
||||
|
||||
### Example 1: `endpoint_mode` is `envoy`
|
||||
#### Configuration of `ext-auth` Plugin:
|
||||
```yaml
|
||||
http_service:
|
||||
endpoint_mode: envoy
|
||||
@@ -80,13 +142,16 @@ http_service:
|
||||
timeout: 1000
|
||||
```
|
||||
|
||||
Using the following request to the gateway, after enabling the `ext-auth` plugin:
|
||||
When using the following request to the gateway after enabling the `ext-auth` plugin:
|
||||
|
||||
```shell
|
||||
curl -X POST http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx"
|
||||
```
|
||||
|
||||
**Successful request to the `ext-auth` service:**
|
||||
The `ext-auth` service will receive the following authentication request:
|
||||
**When the request to the `ext-auth` service is successful**:
|
||||
|
||||
The `ext-auth` service will receive the following authorization request:
|
||||
|
||||
```
|
||||
POST /auth/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 HTTP/1.1
|
||||
Host: ext-auth.backend.svc.cluster.local
|
||||
@@ -94,10 +159,12 @@ Authorization: xxx
|
||||
Content-Length: 0
|
||||
```
|
||||
|
||||
**Failed request to the `ext-auth` service:**
|
||||
When the `ext-auth` service responds with a 5xx error, the client will receive an HTTP response code of 403 along with all response headers returned by the `ext-auth` service.
|
||||
**When the request to the `ext-auth` service fails**:
|
||||
|
||||
When the response from the `ext-auth` service is 5xx, the client will receive an HTTP response code of 403 and all the response headers returned by the `ext-auth` service.
|
||||
|
||||
If the `ext-auth` service returns response headers of `x-auth-version: 1.0` and `x-auth-failed: true`, they will be passed to the client.
|
||||
|
||||
If the `ext-auth` service returns `x-auth-version: 1.0` and `x-auth-failed: true` headers, these will be conveyed to the client:
|
||||
```
|
||||
HTTP/1.1 403 Forbidden
|
||||
x-auth-version: 1.0
|
||||
@@ -107,9 +174,14 @@ server: istio-envoy
|
||||
content-length: 0
|
||||
```
|
||||
|
||||
When the `ext-auth` service is inaccessible or returns a status code of 5xx, the client request will be denied with the status code configured in `status_on_error`. When the `ext-auth` service returns other HTTP status codes, the client request will be denied with the returned status code. If `allowed_client_headers` is configured, the matching response headers will be added to the client's response.
|
||||
When the `ext-auth` service is inaccessible or the status code is 5xx, the client request will be rejected with the status code configured in `status_on_error`.
|
||||
|
||||
When the `ext-auth` service returns other HTTP status codes, the client request will be rejected with the returned status code. If `allowed_client_headers` is configured, the response headers with corresponding matching items will be added to the client's response.
|
||||
|
||||
#### Example 2
|
||||
|
||||
Configuration of the `ext-auth` plugin:
|
||||
|
||||
#### Example 2: `ext-auth` Plugin Configuration:
|
||||
```yaml
|
||||
http_service:
|
||||
authorization_request:
|
||||
@@ -130,12 +202,14 @@ http_service:
|
||||
timeout: 1000
|
||||
```
|
||||
|
||||
Using the following request to the gateway after enabling the `ext-auth` plugin:
|
||||
When using the following request to the gateway after enabling the `ext-auth` plugin:
|
||||
|
||||
```shell
|
||||
curl -X POST http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx"
|
||||
```
|
||||
|
||||
The `ext-auth` service will receive the following authentication request:
|
||||
The `ext-auth` service will receive the following authorization request:
|
||||
|
||||
```
|
||||
POST /auth/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 HTTP/1.1
|
||||
Host: my-domain.local
|
||||
@@ -145,10 +219,14 @@ x-envoy-header: true
|
||||
Content-Length: 0
|
||||
```
|
||||
|
||||
If the `ext-auth` service returns headers containing `x-user-id` and `x-auth-version`, these two request headers will be included in requests to the upstream when the gateway calls it.
|
||||
If the response headers returned by the `ext-auth` service contain `x-user-id` and `x-auth-version`, these two headers will be included in the request when the gateway calls the upstream.
|
||||
|
||||
### When endpoint_mode is forward_auth
|
||||
|
||||
#### Example 1
|
||||
|
||||
Configuration of the `ext-auth` plugin:
|
||||
|
||||
### Example 1: `endpoint_mode` is `forward_auth`
|
||||
`ext-auth` Plugin Configuration:
|
||||
```yaml
|
||||
http_service:
|
||||
endpoint_mode: forward_auth
|
||||
@@ -160,26 +238,33 @@ http_service:
|
||||
timeout: 1000
|
||||
```
|
||||
|
||||
Using the following request to the gateway after enabling the `ext-auth` plugin:
|
||||
When using the following request to the gateway after enabling the `ext-auth` plugin:
|
||||
|
||||
```shell
|
||||
curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx"
|
||||
curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx" -H "Host: foo.bar.com"
|
||||
```
|
||||
|
||||
**Successful request to the `ext-auth` service:**
|
||||
The `ext-auth` service will receive the following authentication request:
|
||||
**When the request to the `ext-auth` service is successful**:
|
||||
|
||||
The `ext-auth` service will receive the following authorization request:
|
||||
|
||||
```
|
||||
POST /auth HTTP/1.1
|
||||
Host: ext-auth.backend.svc.cluster.local
|
||||
Authorization: xxx
|
||||
X-Original-Uri: /users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
|
||||
X-Original-Method: GET
|
||||
X-Forwarded-Proto: HTTP
|
||||
X-Forwarded-Host: foo.bar.com
|
||||
X-Forwarded-Uri: /users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
|
||||
X-Forwarded-Method: GET
|
||||
Content-Length: 0
|
||||
```
|
||||
|
||||
**Failed request to the `ext-auth` service:**
|
||||
When the `ext-auth` service responds with a 5xx error, the client will receive an HTTP response code of 403 along with all response headers returned by the `ext-auth` service.
|
||||
**When the request to the `ext-auth` service fails**:
|
||||
|
||||
When the response from the `ext-auth` service is 5xx, the client will receive an HTTP response code of 403 and all the response headers returned by the `ext-auth` service.
|
||||
|
||||
If the `ext-auth` service returns response headers of `x-auth-version: 1.0` and `x-auth-failed: true`, they will be passed to the client.
|
||||
|
||||
If the `ext-auth` service returns `x-auth-version: 1.0` and `x-auth-failed: true` headers, these will be conveyed to the client:
|
||||
```
|
||||
HTTP/1.1 403 Forbidden
|
||||
x-auth-version: 1.0
|
||||
@@ -189,9 +274,14 @@ server: istio-envoy
|
||||
content-length: 0
|
||||
```
|
||||
|
||||
When the `ext-auth` service is inaccessible or returns a status code of 5xx, the client request will be denied with the status code configured in `status_on_error`. When the `ext-auth` service returns other HTTP status codes, the client request will be denied with the returned status code. If `allowed_client_headers` is configured, the matching response headers will be added to the client's response.
|
||||
When the `ext-auth` service is inaccessible or the status code is 5xx, the client request will be rejected with the status code configured in `status_on_error`.
|
||||
|
||||
When the `ext-auth` service returns other HTTP status codes, the client request will be rejected with the returned status code. If `allowed_client_headers` is configured, the response headers with corresponding matching items will be added to the client's response.
|
||||
|
||||
#### Example 2
|
||||
|
||||
Configuration of the `ext-auth` plugin:
|
||||
|
||||
#### Example 2: `ext-auth` Plugin Configuration:
|
||||
```yaml
|
||||
http_service:
|
||||
authorization_request:
|
||||
@@ -213,31 +303,25 @@ http_service:
|
||||
timeout: 1000
|
||||
```
|
||||
|
||||
Using the following request to the gateway after enabling the `ext-auth` plugin:
|
||||
When using the following request to the gateway after enabling the `ext-auth` plugin:
|
||||
|
||||
```shell
|
||||
curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx" -H "X-Auth-Version: 1.0"
|
||||
curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx" -H "X-Auth-Version: 1.0" -H "Host: foo.bar.com"
|
||||
```
|
||||
|
||||
The `ext-auth` service will receive the following authentication request:
|
||||
The `ext-auth` service will receive the following authorization request:
|
||||
|
||||
```
|
||||
POST /auth HTTP/1.1
|
||||
Host: my-domain.local
|
||||
Authorization: xxx
|
||||
X-Original-Uri: /users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
|
||||
X-Original-Method: GET
|
||||
X-Forwarded-Proto: HTTP
|
||||
X-Forwarded-Host: foo.bar.com
|
||||
X-Forwarded-Uri: /users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
|
||||
X-Forwarded-Method: GET
|
||||
X-Auth-Version: 1.0
|
||||
x-envoy-header: true
|
||||
Content-Length: 0
|
||||
```
|
||||
|
||||
If the `ext-auth` service returns headers containing `x-user-id` and `x-auth-version`, these two request headers will be included in requests to the upstream when the gateway calls it.
|
||||
|
||||
#### x-forwarded-* Header
|
||||
When `endpoint_mode` is `forward_auth`, Higress will automatically generate and send the following headers to the authorization service.
|
||||
| Header | Description |
|
||||
|--------------------|-----------------------------------------------|
|
||||
| x-forwarded-proto | The scheme of the original request, e.g., http/https |
|
||||
| x-forwarded-method | The method of the original request, e.g., get/post/delete/patch |
|
||||
| x-forwarded-host | The host of the original request |
|
||||
| x-forwarded-uri | The path of the original request, including path parameters, e.g., /v1/app?test=true |
|
||||
| x-forwarded-for | The client IP address of the original request |
|
||||
If the response headers returned by the `ext-auth` service contain `x-user-id` and `x-auth-version`, these two headers will be included in the request when the gateway calls the upstream.
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -12,80 +12,78 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultStatusOnError uint32 = http.StatusForbidden
|
||||
DefaultStatusOnError = http.StatusForbidden
|
||||
|
||||
DefaultHttpServiceTimeout uint32 = 1000
|
||||
DefaultHttpServiceTimeout = 1000
|
||||
|
||||
DefaultMaxRequestBodyBytes uint32 = 10 * 1024 * 1024
|
||||
|
||||
EndpointModeEnvoy = "envoy"
|
||||
DefaultMaxRequestBodyBytes = 10 * 1024 * 1024
|
||||
|
||||
EndpointModeEnvoy = "envoy"
|
||||
EndpointModeForwardAuth = "forward_auth"
|
||||
)
|
||||
|
||||
type ExtAuthConfig struct {
|
||||
httpService HttpService
|
||||
failureModeAllow bool
|
||||
failureModeAllowHeaderAdd bool
|
||||
statusOnError uint32
|
||||
HttpService HttpService
|
||||
MatchRules expr.MatchRules
|
||||
FailureModeAllow bool
|
||||
FailureModeAllowHeaderAdd bool
|
||||
StatusOnError uint32
|
||||
}
|
||||
|
||||
type HttpService struct {
|
||||
endpointMode string
|
||||
client wrapper.HttpClient
|
||||
// pathPrefix is only used when endpoint_mode is envoy
|
||||
pathPrefix string
|
||||
// requestMethod is only used when endpoint_mode is forward_auth
|
||||
requestMethod string
|
||||
// path is only used when endpoint_mode is forward_auth
|
||||
path string
|
||||
timeout uint32
|
||||
authorizationRequest AuthorizationRequest
|
||||
authorizationResponse AuthorizationResponse
|
||||
EndpointMode string
|
||||
Client wrapper.HttpClient
|
||||
// PathPrefix is only used when endpoint_mode is envoy
|
||||
PathPrefix string
|
||||
// RequestMethod is only used when endpoint_mode is forward_auth
|
||||
RequestMethod string
|
||||
// Path is only used when endpoint_mode is forward_auth
|
||||
Path string
|
||||
Timeout uint32
|
||||
AuthorizationRequest AuthorizationRequest
|
||||
AuthorizationResponse AuthorizationResponse
|
||||
}
|
||||
|
||||
type AuthorizationRequest struct {
|
||||
// allowedHeaders In addition to the user’s supplied matchers,
|
||||
// Authorization are automatically included to the list.
|
||||
// When the endpoint_mode is set to forward_auth,
|
||||
// the original request's path is set in the X-Original-Uri header,
|
||||
// and the original request's HTTP method is set in the X-Original-Method header.
|
||||
allowedHeaders expr.Matcher
|
||||
headersToAdd map[string]string
|
||||
withRequestBody bool
|
||||
maxRequestBodyBytes uint32
|
||||
AllowedHeaders expr.Matcher
|
||||
HeadersToAdd map[string]string
|
||||
WithRequestBody bool
|
||||
MaxRequestBodyBytes uint32
|
||||
}
|
||||
|
||||
type AuthorizationResponse struct {
|
||||
allowedUpstreamHeaders expr.Matcher
|
||||
allowedClientHeaders expr.Matcher
|
||||
AllowedUpstreamHeaders expr.Matcher
|
||||
AllowedClientHeaders expr.Matcher
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *ExtAuthConfig, log wrapper.Log) error {
|
||||
func ParseConfig(json gjson.Result, config *ExtAuthConfig, log wrapper.Log) error {
|
||||
httpServiceConfig := json.Get("http_service")
|
||||
if !httpServiceConfig.Exists() {
|
||||
return errors.New("missing http_service in config")
|
||||
}
|
||||
err := parseHttpServiceConfig(httpServiceConfig, config, log)
|
||||
if err != nil {
|
||||
if err := parseHttpServiceConfig(httpServiceConfig, config, log); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := parseMatchRules(json, config, log); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
failureModeAllow := json.Get("failure_mode_allow")
|
||||
if failureModeAllow.Exists() {
|
||||
config.failureModeAllow = failureModeAllow.Bool()
|
||||
config.FailureModeAllow = failureModeAllow.Bool()
|
||||
}
|
||||
|
||||
failureModeAllowHeaderAdd := json.Get("failure_mode_allow_header_add")
|
||||
if failureModeAllowHeaderAdd.Exists() {
|
||||
config.failureModeAllowHeaderAdd = failureModeAllowHeaderAdd.Bool()
|
||||
config.FailureModeAllowHeaderAdd = failureModeAllowHeaderAdd.Bool()
|
||||
}
|
||||
|
||||
statusOnError := uint32(json.Get("status_on_error").Uint())
|
||||
if statusOnError == 0 {
|
||||
statusOnError = DefaultStatusOnError
|
||||
}
|
||||
config.statusOnError = statusOnError
|
||||
config.StatusOnError = statusOnError
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -101,7 +99,7 @@ func parseHttpServiceConfig(json gjson.Result, config *ExtAuthConfig, log wrappe
|
||||
if timeout == 0 {
|
||||
timeout = DefaultHttpServiceTimeout
|
||||
}
|
||||
httpService.timeout = timeout
|
||||
httpService.Timeout = timeout
|
||||
|
||||
if err := parseAuthorizationRequestConfig(json, &httpService); err != nil {
|
||||
return err
|
||||
@@ -111,7 +109,7 @@ func parseHttpServiceConfig(json gjson.Result, config *ExtAuthConfig, log wrappe
|
||||
return err
|
||||
}
|
||||
|
||||
config.httpService = httpService
|
||||
config.HttpService = httpService
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -123,7 +121,7 @@ func parseEndpointConfig(json gjson.Result, httpService *HttpService, log wrappe
|
||||
} else if endpointMode != EndpointModeEnvoy && endpointMode != EndpointModeForwardAuth {
|
||||
return errors.New(fmt.Sprintf("endpoint_mode %s is not supported", endpointMode))
|
||||
}
|
||||
httpService.endpointMode = endpointMode
|
||||
httpService.EndpointMode = endpointMode
|
||||
|
||||
endpointConfig := json.Get("endpoint")
|
||||
if !endpointConfig.Exists() {
|
||||
@@ -140,7 +138,7 @@ func parseEndpointConfig(json gjson.Result, httpService *HttpService, log wrappe
|
||||
}
|
||||
serviceHost := endpointConfig.Get("service_host").String()
|
||||
|
||||
httpService.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
httpService.Client = wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: serviceName,
|
||||
Port: servicePort,
|
||||
Host: serviceHost,
|
||||
@@ -152,7 +150,7 @@ func parseEndpointConfig(json gjson.Result, httpService *HttpService, log wrappe
|
||||
if !pathPrefixConfig.Exists() {
|
||||
return errors.New("when endpoint_mode is envoy, endpoint path_prefix must not be empty")
|
||||
}
|
||||
httpService.pathPrefix = pathPrefixConfig.String()
|
||||
httpService.PathPrefix = pathPrefixConfig.String()
|
||||
|
||||
if endpointConfig.Get("request_method").Exists() || endpointConfig.Get("path").Exists() {
|
||||
log.Warn("when endpoint_mode is envoy, endpoint request_method and path will be ignored")
|
||||
@@ -160,16 +158,16 @@ func parseEndpointConfig(json gjson.Result, httpService *HttpService, log wrappe
|
||||
case EndpointModeForwardAuth:
|
||||
requestMethodConfig := endpointConfig.Get("request_method")
|
||||
if !requestMethodConfig.Exists() {
|
||||
httpService.requestMethod = http.MethodGet
|
||||
httpService.RequestMethod = http.MethodGet
|
||||
} else {
|
||||
httpService.requestMethod = strings.ToUpper(requestMethodConfig.String())
|
||||
httpService.RequestMethod = strings.ToUpper(requestMethodConfig.String())
|
||||
}
|
||||
|
||||
pathConfig := endpointConfig.Get("path")
|
||||
if !pathConfig.Exists() {
|
||||
return errors.New("when endpoint_mode is forward_auth, endpoint path must not be empty")
|
||||
}
|
||||
httpService.path = pathConfig.String()
|
||||
httpService.Path = pathConfig.String()
|
||||
|
||||
if endpointConfig.Get("path_prefix").Exists() {
|
||||
log.Warn("when endpoint_mode is forward_auth, endpoint path_prefix will be ignored")
|
||||
@@ -189,35 +187,28 @@ func parseAuthorizationRequestConfig(json gjson.Result, httpService *HttpService
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authorizationRequest.allowedHeaders = result
|
||||
authorizationRequest.AllowedHeaders = result
|
||||
}
|
||||
|
||||
headersToAdd := map[string]string{}
|
||||
headersToAddConfig := authorizationRequestConfig.Get("headers_to_add")
|
||||
if headersToAddConfig.Exists() {
|
||||
for key, value := range headersToAddConfig.Map() {
|
||||
headersToAdd[key] = value.Str
|
||||
}
|
||||
}
|
||||
authorizationRequest.headersToAdd = headersToAdd
|
||||
authorizationRequest.HeadersToAdd = convertToStringMap(authorizationRequestConfig.Get("headers_to_add"))
|
||||
|
||||
withRequestBody := authorizationRequestConfig.Get("with_request_body")
|
||||
if withRequestBody.Exists() {
|
||||
// withRequestBody is true and the request method is GET, OPTIONS or HEAD
|
||||
if withRequestBody.Bool() &&
|
||||
(httpService.requestMethod == http.MethodGet || httpService.requestMethod == http.MethodOptions || httpService.requestMethod == http.MethodHead) {
|
||||
return errors.New(fmt.Sprintf("requestMethod %s does not support with_request_body set to true", httpService.requestMethod))
|
||||
(httpService.RequestMethod == http.MethodGet || httpService.RequestMethod == http.MethodOptions || httpService.RequestMethod == http.MethodHead) {
|
||||
return errors.New(fmt.Sprintf("requestMethod %s does not support with_request_body set to true", httpService.RequestMethod))
|
||||
}
|
||||
authorizationRequest.withRequestBody = withRequestBody.Bool()
|
||||
authorizationRequest.WithRequestBody = withRequestBody.Bool()
|
||||
}
|
||||
|
||||
maxRequestBodyBytes := uint32(authorizationRequestConfig.Get("max_request_body_bytes").Uint())
|
||||
if maxRequestBodyBytes == 0 {
|
||||
maxRequestBodyBytes = DefaultMaxRequestBodyBytes
|
||||
}
|
||||
authorizationRequest.maxRequestBodyBytes = maxRequestBodyBytes
|
||||
authorizationRequest.MaxRequestBodyBytes = maxRequestBodyBytes
|
||||
|
||||
httpService.authorizationRequest = authorizationRequest
|
||||
httpService.AuthorizationRequest = authorizationRequest
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -233,7 +224,7 @@ func parseAuthorizationResponseConfig(json gjson.Result, httpService *HttpServic
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authorizationResponse.allowedUpstreamHeaders = result
|
||||
authorizationResponse.AllowedUpstreamHeaders = result
|
||||
}
|
||||
|
||||
allowedClientHeaders := authorizationResponseConfig.Get("allowed_client_headers")
|
||||
@@ -242,10 +233,62 @@ func parseAuthorizationResponseConfig(json gjson.Result, httpService *HttpServic
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authorizationResponse.allowedClientHeaders = result
|
||||
authorizationResponse.AllowedClientHeaders = result
|
||||
}
|
||||
|
||||
httpService.authorizationResponse = authorizationResponse
|
||||
httpService.AuthorizationResponse = authorizationResponse
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseMatchRules(json gjson.Result, config *ExtAuthConfig, log wrapper.Log) error {
|
||||
matchListConfig := json.Get("match_list")
|
||||
if !matchListConfig.Exists() {
|
||||
config.MatchRules = expr.MatchRulesDefaults()
|
||||
return nil
|
||||
}
|
||||
|
||||
matchType := json.Get("match_type")
|
||||
if !matchType.Exists() {
|
||||
return errors.New("missing match_type in config")
|
||||
}
|
||||
if matchType.Str != expr.ModeWhitelist && matchType.Str != expr.ModeBlacklist {
|
||||
return errors.New("invalid match_type in config, must be 'whitelist' or 'blacklist'")
|
||||
}
|
||||
|
||||
ruleList := make([]expr.Rule, 0)
|
||||
var err error
|
||||
|
||||
matchListConfig.ForEach(func(key, value gjson.Result) bool {
|
||||
pathMatcher, err := expr.BuildStringMatcher(
|
||||
value.Get("match_rule_type").Str,
|
||||
value.Get("match_rule_path").Str, false)
|
||||
if err != nil {
|
||||
return false // stop iterating
|
||||
}
|
||||
ruleList = append(ruleList, expr.Rule{
|
||||
Domain: value.Get("match_rule_domain").Str,
|
||||
Path: pathMatcher,
|
||||
})
|
||||
return true // keep iterating
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build string matcher for rule %v: %w", matchListConfig, err)
|
||||
}
|
||||
|
||||
config.MatchRules = expr.MatchRules{
|
||||
Mode: matchType.Str,
|
||||
RuleList: ruleList,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertToStringMap(result gjson.Result) map[string]string {
|
||||
m := make(map[string]string)
|
||||
result.ForEach(func(key, value gjson.Result) bool {
|
||||
m[key.String()] = value.String()
|
||||
return true // keep iterating
|
||||
})
|
||||
return m
|
||||
}
|
||||
136
plugins/wasm-go/extensions/ext-auth/config/config_test.go
Normal file
136
plugins/wasm-go/extensions/ext-auth/config/config_test.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
)
|
||||
|
||||
const (
|
||||
MatchPatternExact string = "exact"
|
||||
MatchPatternPrefix string = "prefix"
|
||||
MatchPatternSuffix string = "suffix"
|
||||
MatchPatternContains string = "contains"
|
||||
MatchPatternRegex string = "regex"
|
||||
|
||||
MatchIgnoreCase string = "ignore_case"
|
||||
)
|
||||
|
||||
type Matcher interface {
|
||||
Match(s string) bool
|
||||
}
|
||||
|
||||
type stringExactMatcher struct {
|
||||
target string
|
||||
ignoreCase bool
|
||||
}
|
||||
|
||||
func (m *stringExactMatcher) Match(s string) bool {
|
||||
if m.ignoreCase {
|
||||
return strings.ToLower(s) == m.target
|
||||
}
|
||||
return s == m.target
|
||||
}
|
||||
|
||||
type stringPrefixMatcher struct {
|
||||
target string
|
||||
ignoreCase bool
|
||||
}
|
||||
|
||||
func (m *stringPrefixMatcher) Match(s string) bool {
|
||||
if m.ignoreCase {
|
||||
return strings.HasPrefix(strings.ToLower(s), m.target)
|
||||
}
|
||||
return strings.HasPrefix(s, m.target)
|
||||
}
|
||||
|
||||
type stringSuffixMatcher struct {
|
||||
target string
|
||||
ignoreCase bool
|
||||
}
|
||||
|
||||
func (m *stringSuffixMatcher) Match(s string) bool {
|
||||
if m.ignoreCase {
|
||||
return strings.HasSuffix(strings.ToLower(s), m.target)
|
||||
}
|
||||
return strings.HasSuffix(s, m.target)
|
||||
}
|
||||
|
||||
type stringContainsMatcher struct {
|
||||
target string
|
||||
ignoreCase bool
|
||||
}
|
||||
|
||||
func (m *stringContainsMatcher) Match(s string) bool {
|
||||
if m.ignoreCase {
|
||||
return strings.Contains(strings.ToLower(s), m.target)
|
||||
}
|
||||
return strings.Contains(s, m.target)
|
||||
}
|
||||
|
||||
type stringRegexMatcher struct {
|
||||
regex *regexp.Regexp
|
||||
}
|
||||
|
||||
func (m *stringRegexMatcher) Match(s string) bool {
|
||||
return m.regex.MatchString(s)
|
||||
}
|
||||
|
||||
type MatcherConstructor func(string, bool) (Matcher, error)
|
||||
|
||||
var matcherConstructors = map[string]MatcherConstructor{
|
||||
MatchPatternExact: newStringExactMatcher,
|
||||
MatchPatternPrefix: newStringPrefixMatcher,
|
||||
MatchPatternSuffix: newStringSuffixMatcher,
|
||||
MatchPatternContains: newStringContainsMatcher,
|
||||
MatchPatternRegex: newStringRegexMatcher,
|
||||
}
|
||||
|
||||
func newStringExactMatcher(target string, ignoreCase bool) (Matcher, error) {
|
||||
if ignoreCase {
|
||||
target = strings.ToLower(target)
|
||||
}
|
||||
return &stringExactMatcher{target: target, ignoreCase: ignoreCase}, nil
|
||||
}
|
||||
|
||||
func newStringPrefixMatcher(target string, ignoreCase bool) (Matcher, error) {
|
||||
if ignoreCase {
|
||||
target = strings.ToLower(target)
|
||||
}
|
||||
return &stringPrefixMatcher{target: target, ignoreCase: ignoreCase}, nil
|
||||
}
|
||||
|
||||
func newStringSuffixMatcher(target string, ignoreCase bool) (Matcher, error) {
|
||||
if ignoreCase {
|
||||
target = strings.ToLower(target)
|
||||
}
|
||||
return &stringSuffixMatcher{target: target, ignoreCase: ignoreCase}, nil
|
||||
}
|
||||
|
||||
func newStringContainsMatcher(target string, ignoreCase bool) (Matcher, error) {
|
||||
if ignoreCase {
|
||||
target = strings.ToLower(target)
|
||||
}
|
||||
return &stringContainsMatcher{target: target, ignoreCase: ignoreCase}, nil
|
||||
}
|
||||
|
||||
func newStringRegexMatcher(target string, ignoreCase bool) (Matcher, error) {
|
||||
if ignoreCase && !strings.HasPrefix(target, "(?i)") {
|
||||
target = "(?i)" + target
|
||||
}
|
||||
re, err := regexp.Compile(target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &stringRegexMatcher{regex: re}, nil
|
||||
}
|
||||
|
||||
func BuildStringMatcher(matchType, target string, ignoreCase bool) (Matcher, error) {
|
||||
for constructorType, constructor := range matcherConstructors {
|
||||
if constructorType == matchType {
|
||||
return constructor(target, ignoreCase)
|
||||
}
|
||||
}
|
||||
return nil, errors.New("unknown string matcher type")
|
||||
}
|
||||
79
plugins/wasm-go/extensions/ext-auth/expr/match_rules.go
Normal file
79
plugins/wasm-go/extensions/ext-auth/expr/match_rules.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
)
|
||||
|
||||
const (
|
||||
ModeWhitelist = "whitelist"
|
||||
ModeBlacklist = "blacklist"
|
||||
)
|
||||
|
||||
type MatchRules struct {
|
||||
Mode string
|
||||
RuleList []Rule
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
Domain string
|
||||
Path Matcher
|
||||
}
|
||||
|
||||
func MatchRulesDefaults() MatchRules {
|
||||
return MatchRules{
|
||||
Mode: ModeWhitelist,
|
||||
RuleList: []Rule{},
|
||||
}
|
||||
}
|
||||
|
||||
// IsAllowedByMode checks if the given domain and path are allowed based on the configuration mode.
|
||||
func (config *MatchRules) IsAllowedByMode(domain, path string) bool {
|
||||
switch config.Mode {
|
||||
case ModeWhitelist:
|
||||
for _, rule := range config.RuleList {
|
||||
if rule.matchDomainAndPath(domain, path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
case ModeBlacklist:
|
||||
for _, rule := range config.RuleList {
|
||||
if rule.matchDomainAndPath(domain, path) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// matchDomainAndPath checks if the given domain and path match the rule.
|
||||
// If rule.Domain is empty, it only checks rule.Path.
|
||||
// If rule.Path is empty, it only checks rule.Domain.
|
||||
// If both are empty, it returns false.
|
||||
func (rule *Rule) matchDomainAndPath(domain, path string) bool {
|
||||
if rule.Domain == "" && rule.Path == nil {
|
||||
return false
|
||||
}
|
||||
domainMatch := rule.Domain == "" || matchDomain(domain, rule.Domain)
|
||||
pathMatch := rule.Path == nil || rule.Path.Match(path)
|
||||
return domainMatch && pathMatch
|
||||
}
|
||||
|
||||
// matchDomain checks if the given domain matches the pattern.
|
||||
func matchDomain(domain string, pattern string) bool {
|
||||
// Convert wildcard pattern to regex pattern
|
||||
regexPattern := convertWildcardToRegex(pattern)
|
||||
matched, _ := regexp.MatchString(regexPattern, domain)
|
||||
return matched
|
||||
}
|
||||
|
||||
// convertWildcardToRegex converts a wildcard pattern to a regex pattern.
|
||||
func convertWildcardToRegex(pattern string) string {
|
||||
pattern = regexp.QuoteMeta(pattern)
|
||||
pattern = "^" + strings.ReplaceAll(pattern, "\\*", ".*") + "$"
|
||||
return pattern
|
||||
}
|
||||
259
plugins/wasm-go/extensions/ext-auth/expr/match_rules_test.go
Normal file
259
plugins/wasm-go/extensions/ext-auth/expr/match_rules_test.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsAllowedByMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config MatchRules
|
||||
domain string
|
||||
path string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Whitelist mode, rule matches",
|
||||
config: MatchRules{
|
||||
Mode: ModeWhitelist,
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "example.com",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/foo",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Whitelist mode, rule does not match",
|
||||
config: MatchRules{
|
||||
Mode: ModeWhitelist,
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "example.com",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/bar",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Blacklist mode, rule matches",
|
||||
config: MatchRules{
|
||||
Mode: ModeBlacklist,
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "example.com",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/foo",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Blacklist mode, rule does not match",
|
||||
config: MatchRules{
|
||||
Mode: ModeBlacklist,
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "example.com",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Domain matches, Path is empty",
|
||||
config: MatchRules{
|
||||
Mode: ModeWhitelist,
|
||||
RuleList: []Rule{
|
||||
{Domain: "example.com", Path: nil},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/foo",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Domain is empty, Path matches",
|
||||
config: MatchRules{
|
||||
Mode: ModeWhitelist,
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/foo",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Both Domain and Path are empty",
|
||||
config: MatchRules{
|
||||
Mode: ModeWhitelist,
|
||||
RuleList: []Rule{
|
||||
{Domain: "", Path: nil},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/foo",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid mode",
|
||||
config: MatchRules{
|
||||
Mode: "invalid",
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "example.com",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/foo",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Whitelist mode, generic domain matches",
|
||||
config: MatchRules{
|
||||
Mode: ModeWhitelist,
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "*.example.com",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "sub.example.com",
|
||||
path: "/foo",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Whitelist mode, generic domain does not match",
|
||||
config: MatchRules{
|
||||
Mode: ModeWhitelist,
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "*.example.com",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/foo",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Blacklist mode, generic domain matches",
|
||||
config: MatchRules{
|
||||
Mode: ModeBlacklist,
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "*.example.com",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "sub.example.com",
|
||||
path: "/foo",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Blacklist mode, generic domain does not match",
|
||||
config: MatchRules{
|
||||
Mode: ModeBlacklist,
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "*.example.com",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/foo",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.config.IsAllowedByMode(tt.domain, tt.path)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,17 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
)
|
||||
|
||||
const (
|
||||
matchPatternExact string = "exact"
|
||||
matchPatternPrefix string = "prefix"
|
||||
matchPatternSuffix string = "suffix"
|
||||
matchPatternContains string = "contains"
|
||||
matchPatternRegex string = "regex"
|
||||
MatchPatternExact string = "exact"
|
||||
MatchPatternPrefix string = "prefix"
|
||||
MatchPatternSuffix string = "suffix"
|
||||
MatchPatternContains string = "contains"
|
||||
MatchPatternRegex string = "regex"
|
||||
|
||||
matchIgnoreCase string = "ignore_case"
|
||||
MatchIgnoreCase string = "ignore_case"
|
||||
)
|
||||
|
||||
type Matcher interface {
|
||||
@@ -78,78 +77,16 @@ func (m *stringRegexMatcher) Match(s string) bool {
|
||||
return m.regex.MatchString(s)
|
||||
}
|
||||
|
||||
type repeatedStringMatcher struct {
|
||||
matchers []Matcher
|
||||
}
|
||||
|
||||
func (rsm *repeatedStringMatcher) Match(s string) bool {
|
||||
for _, m := range rsm.matchers {
|
||||
if m.Match(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func buildRepeatedStringMatcher(matchers []gjson.Result, allIgnoreCase bool) (Matcher, error) {
|
||||
builtMatchers := make([]Matcher, len(matchers))
|
||||
|
||||
createMatcher := func(json gjson.Result, targetKey string, ignoreCase bool, matcherType MatcherConstructor) (Matcher, error) {
|
||||
result := json.Get(targetKey)
|
||||
if result.Exists() && result.String() != "" {
|
||||
target := result.String()
|
||||
return matcherType(target, ignoreCase)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for i, item := range matchers {
|
||||
var matcher Matcher
|
||||
var err error
|
||||
|
||||
// If allIgnoreCase is true, it takes precedence over any user configuration,
|
||||
// forcing case-insensitive matching regardless of individual item settings.
|
||||
ignoreCase := allIgnoreCase
|
||||
if !allIgnoreCase {
|
||||
ignoreCaseResult := item.Get(matchIgnoreCase)
|
||||
if ignoreCaseResult.Exists() && ignoreCaseResult.Bool() {
|
||||
ignoreCase = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, matcherType := range []struct {
|
||||
key string
|
||||
creator MatcherConstructor
|
||||
}{
|
||||
{matchPatternExact, newStringExactMatcher},
|
||||
{matchPatternPrefix, newStringPrefixMatcher},
|
||||
{matchPatternSuffix, newStringSuffixMatcher},
|
||||
{matchPatternContains, newStringContainsMatcher},
|
||||
{matchPatternRegex, newStringRegexMatcher},
|
||||
} {
|
||||
if matcher, err = createMatcher(item, matcherType.key, ignoreCase, matcherType.creator); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if matcher != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matcher == nil {
|
||||
return nil, errors.New("unknown string matcher type")
|
||||
}
|
||||
|
||||
builtMatchers[i] = matcher
|
||||
|
||||
}
|
||||
|
||||
return &repeatedStringMatcher{
|
||||
matchers: builtMatchers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type MatcherConstructor func(string, bool) (Matcher, error)
|
||||
|
||||
var matcherConstructors = map[string]MatcherConstructor{
|
||||
MatchPatternExact: newStringExactMatcher,
|
||||
MatchPatternPrefix: newStringPrefixMatcher,
|
||||
MatchPatternSuffix: newStringSuffixMatcher,
|
||||
MatchPatternContains: newStringContainsMatcher,
|
||||
MatchPatternRegex: newStringRegexMatcher,
|
||||
}
|
||||
|
||||
func newStringExactMatcher(target string, ignoreCase bool) (Matcher, error) {
|
||||
if ignoreCase {
|
||||
target = strings.ToLower(target)
|
||||
@@ -189,14 +126,11 @@ func newStringRegexMatcher(target string, ignoreCase bool) (Matcher, error) {
|
||||
return &stringRegexMatcher{regex: re}, nil
|
||||
}
|
||||
|
||||
func BuildRepeatedStringMatcherIgnoreCase(matchers []gjson.Result) (Matcher, error) {
|
||||
return buildRepeatedStringMatcher(matchers, true)
|
||||
}
|
||||
|
||||
func BuildRepeatedStringMatcher(matchers []gjson.Result) (Matcher, error) {
|
||||
return buildRepeatedStringMatcher(matchers, false)
|
||||
}
|
||||
|
||||
func BuildStringMatcher(matcher gjson.Result) (Matcher, error) {
|
||||
return BuildRepeatedStringMatcher([]gjson.Result{matcher})
|
||||
func BuildStringMatcher(matchType, target string, ignoreCase bool) (Matcher, error) {
|
||||
for constructorType, constructor := range matcherConstructors {
|
||||
if constructorType == matchType {
|
||||
return constructor(target, ignoreCase)
|
||||
}
|
||||
}
|
||||
return nil, errors.New("unknown string matcher type")
|
||||
}
|
||||
|
||||
@@ -4,78 +4,96 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestStringMatcher(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg string
|
||||
matchType string
|
||||
target string
|
||||
ignoreCase bool
|
||||
matched []string
|
||||
mismatched []string
|
||||
}{
|
||||
{
|
||||
name: "exact",
|
||||
cfg: `{"exact": "foo"}`,
|
||||
matchType: MatchPatternExact,
|
||||
target: "foo",
|
||||
matched: []string{"foo"},
|
||||
mismatched: []string{"fo", "fooo"},
|
||||
},
|
||||
{
|
||||
name: "exact, ignore_case",
|
||||
cfg: `{"exact": "foo", "ignore_case": true}`,
|
||||
matched: []string{"Foo", "foo"},
|
||||
name: "exact, ignore_case",
|
||||
matchType: MatchPatternExact,
|
||||
target: "foo",
|
||||
ignoreCase: true,
|
||||
matched: []string{"Foo", "foo"},
|
||||
},
|
||||
{
|
||||
name: "prefix",
|
||||
cfg: `{"prefix": "/p"}`,
|
||||
matchType: MatchPatternPrefix,
|
||||
target: "/p",
|
||||
matched: []string{"/p", "/pa"},
|
||||
mismatched: []string{"/P"},
|
||||
},
|
||||
{
|
||||
name: "prefix, ignore_case",
|
||||
cfg: `{"prefix": "/p", "ignore_case": true}`,
|
||||
matchType: MatchPatternPrefix,
|
||||
target: "/p",
|
||||
ignoreCase: true,
|
||||
matched: []string{"/P", "/p", "/pa", "/Pa"},
|
||||
mismatched: []string{"/"},
|
||||
},
|
||||
{
|
||||
name: "suffix",
|
||||
cfg: `{"suffix": "foo"}`,
|
||||
matchType: MatchPatternSuffix,
|
||||
target: "foo",
|
||||
matched: []string{"foo", "0foo"},
|
||||
mismatched: []string{"fo", "fooo", "aFoo"},
|
||||
},
|
||||
{
|
||||
name: "suffix, ignore_case",
|
||||
cfg: `{"suffix": "foo", "ignore_case": true}`,
|
||||
matchType: MatchPatternSuffix,
|
||||
target: "foo",
|
||||
ignoreCase: true,
|
||||
matched: []string{"aFoo", "foo"},
|
||||
mismatched: []string{"fo", "fooo"},
|
||||
},
|
||||
{
|
||||
name: "contains",
|
||||
cfg: `{"contains": "foo"}`,
|
||||
matchType: MatchPatternContains,
|
||||
target: "foo",
|
||||
matched: []string{"foo", "0foo", "fooo"},
|
||||
mismatched: []string{"fo", "aFoo"},
|
||||
},
|
||||
{
|
||||
name: "contains, ignore_case",
|
||||
cfg: `{"contains": "foo", "ignore_case": true}`,
|
||||
matchType: MatchPatternContains,
|
||||
target: "foo",
|
||||
ignoreCase: true,
|
||||
matched: []string{"aFoo", "foo", "FoO"},
|
||||
mismatched: []string{"fo"},
|
||||
},
|
||||
{
|
||||
name: "regex",
|
||||
cfg: `{"regex": "fo{2}"}`,
|
||||
matchType: MatchPatternRegex,
|
||||
target: "fo{2}",
|
||||
matched: []string{"foo", "0foo", "fooo"},
|
||||
mismatched: []string{"aFoo", "fo"},
|
||||
},
|
||||
{
|
||||
name: "regex, ignore_case",
|
||||
cfg: `{"regex": "fo{2}", "ignore_case": true}`,
|
||||
matchType: MatchPatternRegex,
|
||||
target: "fo{2}",
|
||||
ignoreCase: true,
|
||||
matched: []string{"foo", "0foo", "fooo", "aFoo"},
|
||||
mismatched: []string{"fo"},
|
||||
},
|
||||
{
|
||||
name: "regex, ignore_case & case insensitive specified in regex",
|
||||
cfg: `{"regex": "(?i)fo{2}", "ignore_case": true}`,
|
||||
matchType: MatchPatternRegex,
|
||||
target: "(?i)fo{2}",
|
||||
ignoreCase: true,
|
||||
matched: []string{"foo", "0foo", "fooo", "aFoo"},
|
||||
mismatched: []string{"fo"},
|
||||
},
|
||||
@@ -83,7 +101,7 @@ func TestStringMatcher(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
built, _ := BuildStringMatcher(gjson.Parse(tt.cfg))
|
||||
built, _ := BuildStringMatcher(tt.matchType, tt.target, tt.ignoreCase)
|
||||
for _, s := range tt.matched {
|
||||
assert.True(t, built.Match(s))
|
||||
}
|
||||
@@ -93,30 +111,3 @@ func TestStringMatcher(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRepeatedStringMatcherIgnoreCase(t *testing.T) {
|
||||
cfgs := []string{
|
||||
`{"exact":"foo"}`,
|
||||
`{"prefix":"pre"}`,
|
||||
`{"regex":"^Cache"}`,
|
||||
}
|
||||
matched := []string{"Foo", "foO", "foo", "PreA", "cache-control", "Cache-Control"}
|
||||
mismatched := []string{"afoo", "fo"}
|
||||
ms := []gjson.Result{}
|
||||
for _, cfg := range cfgs {
|
||||
ms = append(ms, gjson.Parse(cfg))
|
||||
}
|
||||
built, _ := BuildRepeatedStringMatcherIgnoreCase(ms)
|
||||
for _, s := range matched {
|
||||
assert.True(t, built.Match(s))
|
||||
}
|
||||
for _, s := range mismatched {
|
||||
assert.False(t, built.Match(s))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPassOutRegexCompileErr(t *testing.T) {
|
||||
cfg := `{"regex":"(?!)aa"}`
|
||||
_, err := BuildRepeatedStringMatcher([]gjson.Result{gjson.Parse(cfg)})
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type repeatedStringMatcher struct {
|
||||
matchers []Matcher
|
||||
}
|
||||
|
||||
func (rsm *repeatedStringMatcher) Match(s string) bool {
|
||||
for _, m := range rsm.matchers {
|
||||
if m.Match(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func buildRepeatedStringMatcher(matchers []gjson.Result, allIgnoreCase bool) (Matcher, error) {
|
||||
builtMatchers := make([]Matcher, len(matchers))
|
||||
|
||||
createMatcher := func(json gjson.Result, targetKey string, ignoreCase bool, constructor MatcherConstructor) (Matcher, error) {
|
||||
result := json.Get(targetKey)
|
||||
if result.Exists() && result.String() != "" {
|
||||
target := result.String()
|
||||
return constructor(target, ignoreCase)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for i, item := range matchers {
|
||||
var matcher Matcher
|
||||
var err error
|
||||
|
||||
// If allIgnoreCase is true, it takes precedence over any user configuration,
|
||||
// forcing case-insensitive matching regardless of individual item settings.
|
||||
ignoreCase := allIgnoreCase
|
||||
if !allIgnoreCase {
|
||||
ignoreCaseResult := item.Get(MatchIgnoreCase)
|
||||
if ignoreCaseResult.Exists() && ignoreCaseResult.Bool() {
|
||||
ignoreCase = true
|
||||
}
|
||||
}
|
||||
|
||||
for key, creator := range matcherConstructors {
|
||||
if matcher, err = createMatcher(item, key, ignoreCase, creator); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if matcher != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matcher == nil {
|
||||
return nil, errors.New("unknown string matcher type")
|
||||
}
|
||||
|
||||
builtMatchers[i] = matcher
|
||||
|
||||
}
|
||||
|
||||
return &repeatedStringMatcher{
|
||||
matchers: builtMatchers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func BuildRepeatedStringMatcherIgnoreCase(matchers []gjson.Result) (Matcher, error) {
|
||||
return buildRepeatedStringMatcher(matchers, true)
|
||||
}
|
||||
|
||||
func BuildRepeatedStringMatcher(matchers []gjson.Result) (Matcher, error) {
|
||||
return buildRepeatedStringMatcher(matchers, false)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestBuildRepeatedStringMatcherIgnoreCase(t *testing.T) {
|
||||
cfg := `[
|
||||
{"exact":"foo"},
|
||||
{"prefix":"pre"},
|
||||
{"regex":"^Cache"}
|
||||
]`
|
||||
matched := []string{"Foo", "foO", "foo", "PreA", "cache-control", "Cache-Control"}
|
||||
mismatched := []string{"afoo", "fo"}
|
||||
jsonArray := gjson.Parse(cfg).Array()
|
||||
built, err := BuildRepeatedStringMatcherIgnoreCase(jsonArray)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to build RepeatedStringMatcher: %v", err)
|
||||
}
|
||||
|
||||
for _, s := range matched {
|
||||
assert.True(t, built.Match(s))
|
||||
}
|
||||
for _, s := range mismatched {
|
||||
assert.False(t, built.Match(s))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPassOutRegexCompileErr(t *testing.T) {
|
||||
cfg := `{"regex":"(?!)aa"}`
|
||||
_, err := BuildRepeatedStringMatcher([]gjson.Result{gjson.Parse(cfg)})
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
@@ -4,8 +4,7 @@ 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/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0 h1:BZRNf4R7jr9hwRivg/E29nkVaKEak5MWjBDhWjuHijU=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0/go.mod h1:iiSyFbo+rAtbtGt/bsefv8GU57h9CCLYGJA74/tF5/0=
|
||||
github.com/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a h1:tdPcGgyiH0K+SbsJBBm2oPyEIOTAvLBwD9TuUwVtZho=
|
||||
github.com/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
@@ -15,8 +14,7 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||
github.com/tidwall/gjson v1.17.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=
|
||||
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"ext-auth/config"
|
||||
"ext-auth/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
@@ -26,32 +28,41 @@ import (
|
||||
func main() {
|
||||
wrapper.SetCtx(
|
||||
"ext-auth",
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ParseConfigBy(config.ParseConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
|
||||
)
|
||||
}
|
||||
|
||||
const (
|
||||
HeaderAuthorization string = "authorization"
|
||||
HeaderFailureModeAllow string = "x-envoy-auth-failure-mode-allowed"
|
||||
HeaderOriginalMethod string = "x-original-method"
|
||||
HeaderOriginalUri string = "x-original-uri"
|
||||
|
||||
// Currently, x-forwarded-xxx headers only apply for forward_auth.
|
||||
HeaderXForwardedProto = "x-forwarded-proto"
|
||||
HeaderXForwardedMethod = "x-forwarded-method"
|
||||
HeaderXForwardedUri = "x-Forwarded-uri"
|
||||
HeaderXForwardedHost = "x-Forwarded-host"
|
||||
HeaderAuthorization = "authorization"
|
||||
HeaderFailureModeAllow = "x-envoy-auth-failure-mode-allowed"
|
||||
)
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config ExtAuthConfig, log wrapper.Log) types.Action {
|
||||
// Currently, x-forwarded-xxx headers only apply for forward_auth.
|
||||
const (
|
||||
HeaderOriginalMethod = "x-original-method"
|
||||
HeaderOriginalUri = "x-original-uri"
|
||||
HeaderXForwardedProto = "x-forwarded-proto"
|
||||
HeaderXForwardedMethod = "x-forwarded-method"
|
||||
HeaderXForwardedUri = "x-forwarded-uri"
|
||||
HeaderXForwardedHost = "x-forwarded-host"
|
||||
)
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config config.ExtAuthConfig, log wrapper.Log) types.Action {
|
||||
path, _ := wrapper.GetRequestPathWithoutQuery()
|
||||
// If the request's domain and path match the MatchRules, skip authentication
|
||||
if config.MatchRules.IsAllowedByMode(ctx.Host(), path) {
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
if wrapper.HasRequestBody() {
|
||||
ctx.SetRequestBodyBufferLimit(config.httpService.authorizationRequest.maxRequestBodyBytes)
|
||||
ctx.SetRequestBodyBufferLimit(config.HttpService.AuthorizationRequest.MaxRequestBodyBytes)
|
||||
|
||||
// If withRequestBody is true AND the HTTP request contains a request body,
|
||||
// it will be handled in the onHttpRequestBody phase.
|
||||
if config.httpService.authorizationRequest.withRequestBody {
|
||||
if config.HttpService.AuthorizationRequest.WithRequestBody {
|
||||
// Disable the route re-calculation since the plugin may modify some headers related to the chosen route.
|
||||
ctx.DisableReroute()
|
||||
// The request has a body and requires delaying the header transmission until a cache miss occurs,
|
||||
@@ -64,108 +75,115 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config ExtAuthConfig, log wra
|
||||
return checkExtAuth(ctx, config, nil, log, types.HeaderStopAllIterationAndWatermark)
|
||||
}
|
||||
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config ExtAuthConfig, body []byte, log wrapper.Log) types.Action {
|
||||
if config.httpService.authorizationRequest.withRequestBody {
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config config.ExtAuthConfig, body []byte, log wrapper.Log) types.Action {
|
||||
if config.HttpService.AuthorizationRequest.WithRequestBody {
|
||||
return checkExtAuth(ctx, config, body, log, types.ActionPause)
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func checkExtAuth(ctx wrapper.HttpContext, config ExtAuthConfig, body []byte, log wrapper.Log, pauseAction types.Action) types.Action {
|
||||
// build extAuth request headers
|
||||
extAuthReqHeaders := http.Header{}
|
||||
func checkExtAuth(ctx wrapper.HttpContext, cfg config.ExtAuthConfig, body []byte, log wrapper.Log, pauseAction types.Action) types.Action {
|
||||
httpServiceConfig := cfg.HttpService
|
||||
|
||||
httpServiceConfig := config.httpService
|
||||
requestConfig := httpServiceConfig.authorizationRequest
|
||||
reqHeaders, _ := proxywasm.GetHttpRequestHeaders()
|
||||
if requestConfig.allowedHeaders != nil {
|
||||
for _, header := range reqHeaders {
|
||||
headK := header[0]
|
||||
if requestConfig.allowedHeaders.Match(headK) {
|
||||
extAuthReqHeaders.Set(headK, header[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range requestConfig.headersToAdd {
|
||||
extAuthReqHeaders.Set(key, value)
|
||||
}
|
||||
|
||||
// add Authorization header
|
||||
authorization := extractFromHeader(reqHeaders, HeaderAuthorization)
|
||||
if authorization != "" {
|
||||
extAuthReqHeaders.Set(HeaderAuthorization, authorization)
|
||||
}
|
||||
|
||||
// when endpoint_mode is forward_auth, add x-original-method and x-original-uri headers
|
||||
if httpServiceConfig.endpointMode == EndpointModeForwardAuth {
|
||||
extAuthReqHeaders.Set(HeaderOriginalMethod, ctx.Method())
|
||||
extAuthReqHeaders.Set(HeaderOriginalUri, ctx.Path())
|
||||
extAuthReqHeaders.Set(HeaderXForwardedProto, ctx.Scheme())
|
||||
extAuthReqHeaders.Set(HeaderXForwardedMethod, ctx.Method())
|
||||
extAuthReqHeaders.Set(HeaderXForwardedUri, ctx.Path())
|
||||
extAuthReqHeaders.Set(HeaderXForwardedHost, ctx.Host())
|
||||
}
|
||||
|
||||
requestMethod := httpServiceConfig.requestMethod
|
||||
requestPath := httpServiceConfig.path
|
||||
if httpServiceConfig.endpointMode == EndpointModeEnvoy {
|
||||
extAuthReqHeaders := buildExtAuthRequestHeaders(ctx, cfg)
|
||||
requestMethod := httpServiceConfig.RequestMethod
|
||||
requestPath := httpServiceConfig.Path
|
||||
if httpServiceConfig.EndpointMode == config.EndpointModeEnvoy {
|
||||
requestMethod = ctx.Method()
|
||||
requestPath, _ = url.JoinPath(httpServiceConfig.pathPrefix, ctx.Path())
|
||||
requestPath, _ = url.JoinPath(httpServiceConfig.PathPrefix, ctx.Path())
|
||||
}
|
||||
|
||||
// call ext auth server
|
||||
err := httpServiceConfig.client.Call(requestMethod, requestPath, reconvertHeaders(extAuthReqHeaders), body,
|
||||
// Call ext auth server
|
||||
err := httpServiceConfig.Client.Call(requestMethod, requestPath, util.ReconvertHeaders(extAuthReqHeaders), body,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
defer proxywasm.ResumeHttpRequest()
|
||||
if statusCode != http.StatusOK {
|
||||
log.Errorf("failed to call ext auth server, status: %d", statusCode)
|
||||
callExtAuthServerErrorHandler(config, statusCode, responseHeaders, responseBody)
|
||||
callExtAuthServerErrorHandler(cfg, statusCode, responseHeaders, responseBody)
|
||||
return
|
||||
}
|
||||
|
||||
if httpServiceConfig.authorizationResponse.allowedUpstreamHeaders != nil {
|
||||
if httpServiceConfig.AuthorizationResponse.AllowedUpstreamHeaders != nil {
|
||||
for headK, headV := range responseHeaders {
|
||||
if httpServiceConfig.authorizationResponse.allowedUpstreamHeaders.Match(headK) {
|
||||
if httpServiceConfig.AuthorizationResponse.AllowedUpstreamHeaders.Match(headK) {
|
||||
_ = proxywasm.ReplaceHttpRequestHeader(headK, headV[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}, httpServiceConfig.timeout)
|
||||
}, httpServiceConfig.Timeout)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("failed to call ext auth server: %v", err)
|
||||
// Since the handling logic for call errors and HTTP status code 500 is the same, we directly use 500 here.
|
||||
callExtAuthServerErrorHandler(config, http.StatusInternalServerError, nil, nil)
|
||||
callExtAuthServerErrorHandler(cfg, http.StatusInternalServerError, nil, nil)
|
||||
return types.ActionContinue
|
||||
}
|
||||
return pauseAction
|
||||
}
|
||||
|
||||
func callExtAuthServerErrorHandler(config ExtAuthConfig, statusCode int, extAuthRespHeaders http.Header, responseBody []byte) {
|
||||
if statusCode >= http.StatusInternalServerError && config.failureModeAllow {
|
||||
if config.failureModeAllowHeaderAdd {
|
||||
func buildExtAuthRequestHeaders(ctx wrapper.HttpContext, cfg config.ExtAuthConfig) http.Header {
|
||||
extAuthReqHeaders := http.Header{}
|
||||
|
||||
httpServiceConfig := cfg.HttpService
|
||||
requestConfig := httpServiceConfig.AuthorizationRequest
|
||||
reqHeaders, _ := proxywasm.GetHttpRequestHeaders()
|
||||
if requestConfig.AllowedHeaders != nil {
|
||||
for _, header := range reqHeaders {
|
||||
headK := header[0]
|
||||
if requestConfig.AllowedHeaders.Match(headK) {
|
||||
extAuthReqHeaders.Set(headK, header[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range requestConfig.HeadersToAdd {
|
||||
extAuthReqHeaders.Set(key, value)
|
||||
}
|
||||
|
||||
// Add the Authorization header if present
|
||||
authorization := util.ExtractFromHeader(reqHeaders, HeaderAuthorization)
|
||||
if authorization != "" {
|
||||
extAuthReqHeaders.Set(HeaderAuthorization, authorization)
|
||||
}
|
||||
|
||||
// Add additional headers when endpoint_mode is forward_auth
|
||||
if httpServiceConfig.EndpointMode == config.EndpointModeForwardAuth {
|
||||
// Compatible with older versions
|
||||
extAuthReqHeaders.Set(HeaderOriginalMethod, ctx.Method())
|
||||
extAuthReqHeaders.Set(HeaderOriginalUri, ctx.Path())
|
||||
// Add x-forwarded-xxx headers
|
||||
extAuthReqHeaders.Set(HeaderXForwardedProto, ctx.Scheme())
|
||||
extAuthReqHeaders.Set(HeaderXForwardedMethod, ctx.Method())
|
||||
extAuthReqHeaders.Set(HeaderXForwardedUri, ctx.Path())
|
||||
extAuthReqHeaders.Set(HeaderXForwardedHost, ctx.Host())
|
||||
}
|
||||
return extAuthReqHeaders
|
||||
}
|
||||
|
||||
func callExtAuthServerErrorHandler(config config.ExtAuthConfig, statusCode int, extAuthRespHeaders http.Header, responseBody []byte) {
|
||||
if statusCode >= http.StatusInternalServerError && config.FailureModeAllow {
|
||||
if config.FailureModeAllowHeaderAdd {
|
||||
_ = proxywasm.ReplaceHttpRequestHeader(HeaderFailureModeAllow, "true")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var respHeaders = extAuthRespHeaders
|
||||
if config.httpService.authorizationResponse.allowedClientHeaders != nil {
|
||||
if config.HttpService.AuthorizationResponse.AllowedClientHeaders != nil {
|
||||
respHeaders = http.Header{}
|
||||
for headK, headV := range extAuthRespHeaders {
|
||||
if config.httpService.authorizationResponse.allowedClientHeaders.Match(headK) {
|
||||
if config.HttpService.AuthorizationResponse.AllowedClientHeaders.Match(headK) {
|
||||
respHeaders.Set(headK, headV[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rejects client requests with statusOnError on extAuth unavailability or 5xx.
|
||||
// otherwise, uses the extAuth's returned status code to reject requests
|
||||
// Rejects client requests with StatusOnError if extAuth is unavailable or returns a 5xx status.
|
||||
// Otherwise, uses the status code returned by extAuth to reject requests.
|
||||
statusToUse := statusCode
|
||||
if statusCode >= http.StatusInternalServerError {
|
||||
statusToUse = int(config.statusOnError)
|
||||
statusToUse = int(config.StatusOnError)
|
||||
}
|
||||
_ = sendResponse(uint32(statusToUse), "ext-auth.unauthorized", respHeaders, responseBody)
|
||||
_ = util.SendResponse(uint32(statusToUse), "ext-auth.unauthorized", respHeaders, responseBody)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package util
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
)
|
||||
|
||||
func sendResponse(statusCode uint32, statusCodeDetailData string, headers http.Header, body []byte) error {
|
||||
return proxywasm.SendHttpResponseWithDetail(statusCode, statusCodeDetailData, reconvertHeaders(headers), body, -1)
|
||||
func SendResponse(statusCode uint32, statusCodeDetailData string, headers http.Header, body []byte) error {
|
||||
return proxywasm.SendHttpResponseWithDetail(statusCode, statusCodeDetailData, ReconvertHeaders(headers), body, -1)
|
||||
}
|
||||
|
||||
func reconvertHeaders(headers http.Header) [][2]string {
|
||||
func ReconvertHeaders(headers http.Header) [][2]string {
|
||||
var ret [][2]string
|
||||
if headers == nil {
|
||||
return ret
|
||||
@@ -28,7 +28,7 @@ func reconvertHeaders(headers http.Header) [][2]string {
|
||||
return ret
|
||||
}
|
||||
|
||||
func extractFromHeader(headers [][2]string, headerKey string) string {
|
||||
func ExtractFromHeader(headers [][2]string, headerKey string) string {
|
||||
for _, header := range headers {
|
||||
key := header[0]
|
||||
if strings.ToLower(key) == headerKey {
|
||||
@@ -20,6 +20,7 @@ description: 前端灰度插件配置参考
|
||||
| `localStorageGrayKey` | string | 非必填 | - | 使用JWT鉴权方式,用户ID的唯一标识来自`localStorage`中,如果配置了当前参数,则`grayKey`失效 |
|
||||
| `graySubKey` | string | 非必填 | - | 用户身份信息可能以JSON形式透出,比如:`userInfo:{ userCode:"001" }`,当前例子`graySubKey`取值为`userCode` |
|
||||
| `userStickyMaxAge` | int | 非必填 | 172800 | 用户粘滞的时长:单位为秒,默认为`172800`,2天时间 |
|
||||
| `includePathPrefixes` | array of strings | 非必填 | - | 强制处理的路径。例如,在 微前端 场景下,XHR 接口如: `/resource/xxx`本质是一个资源请求,需要走插件转发逻辑。 |
|
||||
| `skippedPathPrefixes` | array of strings | 非必填 | - | 用于排除特定路径,避免当前插件处理这些请求。例如,在 rewrite 场景下,XHR 接口请求 `/api/xxx` 如果经过插件转发逻辑,可能会导致非预期的结果。 |
|
||||
| `skippedByHeaders` | map of string to string | 非必填 | - | 用于通过请求头过滤,指定哪些请求不被当前插件
|
||||
处理。`skippedPathPrefixes` 的优先级高于当前配置,且页面HTML请求不受本配置的影响。若本配置为空,默认会判断`sec-fetch-mode=cors`以及`upgrade=websocket`两个header头,进行过滤 |
|
||||
|
||||
@@ -64,6 +64,7 @@ type GrayConfig struct {
|
||||
BackendGrayTag string
|
||||
Injection *Injection
|
||||
SkippedPathPrefixes []string
|
||||
IncludePathPrefixes []string
|
||||
SkippedByHeaders map[string]string
|
||||
}
|
||||
|
||||
@@ -97,6 +98,7 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) {
|
||||
grayConfig.Html = json.Get("html").String()
|
||||
grayConfig.SkippedPathPrefixes = convertToStringList(json.Get("skippedPathPrefixes").Array())
|
||||
grayConfig.SkippedByHeaders = convertToStringMap(json.Get("skippedByHeaders"))
|
||||
grayConfig.IncludePathPrefixes = convertToStringList(json.Get("includePathPrefixes").Array())
|
||||
|
||||
if grayConfig.UserStickyMaxAge == "" {
|
||||
// 默认值2天
|
||||
|
||||
@@ -64,7 +64,13 @@ func IsRequestSkippedByHeaders(grayConfig config.GrayConfig) bool {
|
||||
}
|
||||
|
||||
func IsGrayEnabled(grayConfig config.GrayConfig, requestPath string) bool {
|
||||
// 当前路径中前缀为 SkipedRoute,则不走插件逻辑
|
||||
for _, prefix := range grayConfig.IncludePathPrefixes {
|
||||
if strings.HasPrefix(requestPath, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 当前路径中前缀为 SkippedPathPrefixes,则不走插件逻辑
|
||||
for _, prefix := range grayConfig.SkippedPathPrefixes {
|
||||
if strings.HasPrefix(requestPath, prefix) {
|
||||
return false
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -48,6 +49,15 @@ func GetRequestPath() string {
|
||||
return path
|
||||
}
|
||||
|
||||
func GetRequestPathWithoutQuery() (string, error) {
|
||||
rawPath := GetRequestPath()
|
||||
path, err := url.Parse(rawPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path.Path, nil
|
||||
}
|
||||
|
||||
func GetRequestMethod() string {
|
||||
method, err := proxywasm.GetHttpRequestHeader(":method")
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user