1.4.5.2
This commit is contained in:
77
EdgeCommon/pkg/serverconfigs/shared/bit_size_capacity.go
Normal file
77
EdgeCommon/pkg/serverconfigs/shared/bit_size_capacity.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package shared
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type BitSizeCapacityUnit = string
|
||||
|
||||
const (
|
||||
BitSizeCapacityUnitB BitSizeCapacityUnit = "b"
|
||||
BitSizeCapacityUnitKB BitSizeCapacityUnit = "kb"
|
||||
BitSizeCapacityUnitMB BitSizeCapacityUnit = "mb"
|
||||
BitSizeCapacityUnitGB BitSizeCapacityUnit = "gb"
|
||||
BitSizeCapacityUnitTB BitSizeCapacityUnit = "tb"
|
||||
BitSizeCapacityUnitPB BitSizeCapacityUnit = "pb"
|
||||
BitSizeCapacityUnitEB BitSizeCapacityUnit = "eb"
|
||||
//BitSizeCapacityUnitZB BitSizeCapacityUnit = "zb" // zb和yb超出int64范围,暂不支持
|
||||
//BitSizeCapacityUnitYB BitSizeCapacityUnit = "yb"
|
||||
)
|
||||
|
||||
type BitSizeCapacity struct {
|
||||
Count int64 `json:"count" yaml:"count"`
|
||||
Unit BitSizeCapacityUnit `json:"unit" yaml:"unit"`
|
||||
}
|
||||
|
||||
func NewBitSizeCapacity(count int64, unit BitSizeCapacityUnit) *BitSizeCapacity {
|
||||
return &BitSizeCapacity{
|
||||
Count: count,
|
||||
Unit: unit,
|
||||
}
|
||||
}
|
||||
|
||||
func DecodeBitSizeCapacityJSON(sizeCapacityJSON []byte) (*BitSizeCapacity, error) {
|
||||
var capacity = &BitSizeCapacity{}
|
||||
err := json.Unmarshal(sizeCapacityJSON, capacity)
|
||||
return capacity, err
|
||||
}
|
||||
|
||||
func (this *BitSizeCapacity) Bits() int64 {
|
||||
if this.Count < 0 {
|
||||
return -1
|
||||
}
|
||||
switch this.Unit {
|
||||
case BitSizeCapacityUnitB:
|
||||
return this.Count
|
||||
case BitSizeCapacityUnitKB:
|
||||
return this.Count * this.pow(1)
|
||||
case BitSizeCapacityUnitMB:
|
||||
return this.Count * this.pow(2)
|
||||
case BitSizeCapacityUnitGB:
|
||||
return this.Count * this.pow(3)
|
||||
case BitSizeCapacityUnitTB:
|
||||
return this.Count * this.pow(4)
|
||||
case BitSizeCapacityUnitPB:
|
||||
return this.Count * this.pow(5)
|
||||
case BitSizeCapacityUnitEB:
|
||||
return this.Count * this.pow(6)
|
||||
default:
|
||||
return this.Count
|
||||
}
|
||||
}
|
||||
|
||||
func (this *BitSizeCapacity) IsNotEmpty() bool {
|
||||
return this.Count > 0
|
||||
}
|
||||
|
||||
func (this *BitSizeCapacity) AsJSON() ([]byte, error) {
|
||||
return json.Marshal(this)
|
||||
}
|
||||
|
||||
func (this *BitSizeCapacity) pow(n int) int64 {
|
||||
if n <= 0 {
|
||||
return 1
|
||||
}
|
||||
if n == 1 {
|
||||
return 1024 // TODO 考虑是否使用1000进制
|
||||
}
|
||||
return this.pow(n-1) * 1024
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package shared_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/shared"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBitSizeCapacity_Bits(t *testing.T) {
|
||||
{
|
||||
var capacity = shared.NewBitSizeCapacity(1, shared.BitSizeCapacityUnitB)
|
||||
t.Log(capacity.Bits())
|
||||
}
|
||||
{
|
||||
var capacity = shared.NewBitSizeCapacity(2, shared.BitSizeCapacityUnitKB)
|
||||
t.Log(capacity.Bits())
|
||||
}
|
||||
{
|
||||
var capacity = shared.NewBitSizeCapacity(3, shared.BitSizeCapacityUnitMB)
|
||||
t.Log(capacity.Bits())
|
||||
}
|
||||
{
|
||||
var capacity = shared.NewBitSizeCapacity(4, shared.BitSizeCapacityUnitGB)
|
||||
t.Log(capacity.Bits())
|
||||
}
|
||||
}
|
||||
23
EdgeCommon/pkg/serverconfigs/shared/body_type.go
Normal file
23
EdgeCommon/pkg/serverconfigs/shared/body_type.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package shared
|
||||
|
||||
type BodyType = string
|
||||
|
||||
const (
|
||||
BodyTypeURL BodyType = "url"
|
||||
BodyTypeHTML BodyType = "html"
|
||||
)
|
||||
|
||||
func FindAllBodyTypes() []*Definition {
|
||||
return []*Definition{
|
||||
{
|
||||
Name: "HTML",
|
||||
Code: BodyTypeHTML,
|
||||
},
|
||||
{
|
||||
Name: "读取URL",
|
||||
Code: BodyTypeURL,
|
||||
},
|
||||
}
|
||||
}
|
||||
43
EdgeCommon/pkg/serverconfigs/shared/data_map.go
Normal file
43
EdgeCommon/pkg/serverconfigs/shared/data_map.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package shared
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var dataMapPrefix = []byte("GOEDGE_DATA_MAP:")
|
||||
|
||||
// DataMap 二进制数据共享Map
|
||||
// 用来减少相同数据占用的空间和内存
|
||||
type DataMap struct {
|
||||
Map map[string][]byte
|
||||
locker sync.Mutex
|
||||
}
|
||||
|
||||
// NewDataMap 构建对象
|
||||
func NewDataMap() *DataMap {
|
||||
return &DataMap{Map: map[string][]byte{}}
|
||||
}
|
||||
|
||||
// Put 放入数据
|
||||
func (this *DataMap) Put(data []byte) (keyData []byte) {
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
var key = string(dataMapPrefix) + fmt.Sprintf("%x", md5.Sum(data))
|
||||
this.Map[key] = data
|
||||
return []byte(key)
|
||||
}
|
||||
|
||||
// Read 读取数据
|
||||
func (this *DataMap) Read(key []byte) []byte {
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
if bytes.HasPrefix(key, dataMapPrefix) {
|
||||
return this.Map[string(key)]
|
||||
}
|
||||
return key
|
||||
}
|
||||
17
EdgeCommon/pkg/serverconfigs/shared/data_map_test.go
Normal file
17
EdgeCommon/pkg/serverconfigs/shared/data_map_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package shared_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/shared"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewDataMap(t *testing.T) {
|
||||
var m = shared.NewDataMap()
|
||||
t.Log("data:", m.Read([]byte("e10adc3949ba59abbe56e057f20f883e")))
|
||||
var key = m.Put([]byte("123456"))
|
||||
t.Log("keyData:", key)
|
||||
t.Log("keyString:", string(key))
|
||||
t.Log("data:", string(m.Read(key)))
|
||||
}
|
||||
11
EdgeCommon/pkg/serverconfigs/shared/definition.go
Normal file
11
EdgeCommon/pkg/serverconfigs/shared/definition.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package shared
|
||||
|
||||
// Definition 数据定义
|
||||
type Definition struct {
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
5
EdgeCommon/pkg/serverconfigs/shared/formatter.go
Normal file
5
EdgeCommon/pkg/serverconfigs/shared/formatter.go
Normal file
@@ -0,0 +1,5 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package shared
|
||||
|
||||
type Formatter = func(s string) string
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package shared
|
||||
|
||||
// HTTPCORSHeaderConfig 参考 https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
||||
type HTTPCORSHeaderConfig struct {
|
||||
IsOn bool `yaml:"isOn" json:"isOn"`
|
||||
AllowMethods []string `yaml:"allowMethods" json:"allowMethods"`
|
||||
AllowOrigin string `yaml:"allowOrigin" json:"allowOrigin"` // TODO
|
||||
AllowCredentials bool `yaml:"allowCredentials" json:"allowCredentials"` // TODO,实现时需要升级以往的老数据
|
||||
ExposeHeaders []string `yaml:"exposeHeaders" json:"exposeHeaders"`
|
||||
MaxAge int32 `yaml:"maxAge" json:"maxAge"`
|
||||
RequestHeaders []string `yaml:"requestHeaders" json:"requestHeaders"` // TODO
|
||||
RequestMethod string `yaml:"requestMethod" json:"requestMethod"`
|
||||
OptionsMethodOnly bool `yaml:"optionsMethodOnly" json:"optionsMethodOnly"` // 是否仅支持OPTIONS方法
|
||||
}
|
||||
|
||||
func NewHTTPCORSHeaderConfig() *HTTPCORSHeaderConfig {
|
||||
return &HTTPCORSHeaderConfig{
|
||||
AllowCredentials: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *HTTPCORSHeaderConfig) Init() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package shared
|
||||
|
||||
// HTTP Header中Expire设置
|
||||
type HTTPExpireHeaderConfig struct {
|
||||
}
|
||||
118
EdgeCommon/pkg/serverconfigs/shared/http_header_config.go
Normal file
118
EdgeCommon/pkg/serverconfigs/shared/http_header_config.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HTTPHeaderReplaceValue 值替换定义
|
||||
type HTTPHeaderReplaceValue struct {
|
||||
Pattern string `yaml:"pattern" json:"pattern"`
|
||||
Replacement string `yaml:"replacement" json:"replacement"`
|
||||
IsCaseInsensitive bool `yaml:"isCaseInsensitive" json:"isCaseInsensitive"` // TODO
|
||||
IsRegexp bool `yaml:"isRegexp" json:"isRegexp"` // TODO
|
||||
|
||||
patternReg *regexp.Regexp
|
||||
}
|
||||
|
||||
func (this *HTTPHeaderReplaceValue) Init() error {
|
||||
if this.IsRegexp {
|
||||
var pattern = this.Pattern
|
||||
if this.IsCaseInsensitive && !strings.HasPrefix(pattern, "(?i)") {
|
||||
pattern = "(?i)" + pattern
|
||||
}
|
||||
|
||||
reg, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO 支持匹配名(${name})和反向引用${1}。。。
|
||||
this.patternReg = reg
|
||||
} else {
|
||||
if this.IsCaseInsensitive {
|
||||
var pattern = "(?i)" + regexp.QuoteMeta(this.Pattern)
|
||||
reg, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.patternReg = reg
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *HTTPHeaderReplaceValue) Replace(value string) string {
|
||||
if this.patternReg != nil {
|
||||
return this.patternReg.ReplaceAllString(value, this.Replacement)
|
||||
} else {
|
||||
return strings.ReplaceAll(value, this.Pattern, this.Replacement)
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPHeaderConfig 头部信息定义
|
||||
type HTTPHeaderConfig struct {
|
||||
Id int64 `yaml:"id" json:"id"` // ID
|
||||
IsOn bool `yaml:"isOn" json:"isOn"` // 是否开启
|
||||
Name string `yaml:"name" json:"name"` // Name
|
||||
Value string `yaml:"value" json:"value"` // Value
|
||||
|
||||
Status *HTTPStatusConfig `yaml:"status" json:"status"` // 支持的状态码
|
||||
DisableRedirect bool `yaml:"disableRedirect" json:"disableRedirect"` // 在跳转时不调用
|
||||
ShouldAppend bool `yaml:"shouldAppend" json:"shouldAppend"` // 是否为附加
|
||||
ShouldReplace bool `yaml:"shouldReplace" json:"shouldReplace"` // 是否替换值
|
||||
ReplaceValues []*HTTPHeaderReplaceValue `yaml:"replaceValues" json:"replaceValues"` // 替换值
|
||||
Methods []string `yaml:"methods" json:"methods"` // 请求方法
|
||||
Domains []string `yaml:"domains" json:"domains"` // 专属域名
|
||||
|
||||
hasVariables bool
|
||||
}
|
||||
|
||||
// NewHeaderConfig 获取新Header对象
|
||||
func NewHeaderConfig() *HTTPHeaderConfig {
|
||||
return &HTTPHeaderConfig{
|
||||
IsOn: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Init 校验
|
||||
func (this *HTTPHeaderConfig) Init() error {
|
||||
this.hasVariables = configutils.HasVariables(this.Value)
|
||||
|
||||
if this.Status != nil {
|
||||
err := this.Status.Init()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if this.ShouldReplace {
|
||||
for _, v := range this.ReplaceValues {
|
||||
err := v.Init()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasVariables 是否有变量
|
||||
func (this *HTTPHeaderConfig) HasVariables() bool {
|
||||
return this.hasVariables
|
||||
}
|
||||
|
||||
// Match 判断是否匹配状态码
|
||||
func (this *HTTPHeaderConfig) Match(statusCode int) bool {
|
||||
if !this.IsOn {
|
||||
return false
|
||||
}
|
||||
|
||||
if this.Status == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.Status.Match(statusCode)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHeaderConfig_Match(t *testing.T) {
|
||||
a := assert.NewAssertion(t)
|
||||
h := NewHeaderConfig()
|
||||
err := h.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(h.Match(200))
|
||||
a.IsFalse(h.Match(400))
|
||||
|
||||
h.Status = &HTTPStatusConfig{
|
||||
Always: false,
|
||||
Codes: []int{200, 301, 302, 400},
|
||||
}
|
||||
err = h.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(h.Match(400))
|
||||
a.IsFalse(h.Match(500))
|
||||
|
||||
h.Status.Always = true
|
||||
a.IsTrue(h.Match(500))
|
||||
}
|
||||
77
EdgeCommon/pkg/serverconfigs/shared/http_header_policy.go
Normal file
77
EdgeCommon/pkg/serverconfigs/shared/http_header_policy.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package shared
|
||||
|
||||
import "strings"
|
||||
|
||||
// HTTPHeaderPolicy HeaderList定义
|
||||
type HTTPHeaderPolicy struct {
|
||||
Id int64 `yaml:"id" json:"id"` // ID
|
||||
Name string `yaml:"name" json:"name"` // 名称 TODO
|
||||
IsOn bool `yaml:"isOn" json:"isOn"` // 是否启用 TODO
|
||||
Description string `yaml:"description" json:"description"` // 描述 TODO
|
||||
|
||||
SetHeaderRefs []*HTTPHeaderRef `yaml:"setHeaderRefs" json:"setHeaderRefs"`
|
||||
SetHeaders []*HTTPHeaderConfig `yaml:"setHeaders" json:"setHeaders"`
|
||||
DeleteHeaders []string `yaml:"deleteHeaders" json:"deleteHeaders"` // 删除的Header
|
||||
|
||||
Expires *HTTPExpireHeaderConfig `yaml:"expires" json:"expires"` // 内容过期设置 TODO
|
||||
CORS *HTTPCORSHeaderConfig `yaml:"cors" json:"cors"` // CORS跨域设置
|
||||
NonStandardHeaders []string `yaml:"nonStandardHeaders" json:"nonStandardHeaders"` // 非标Header列表
|
||||
|
||||
setHeaderNames []string
|
||||
deleteHeaderMap map[string]bool // header => bool
|
||||
}
|
||||
|
||||
// Init 校验
|
||||
func (this *HTTPHeaderPolicy) Init() error {
|
||||
this.setHeaderNames = []string{}
|
||||
for _, h := range this.SetHeaders {
|
||||
err := h.Init()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.setHeaderNames = append(this.setHeaderNames, strings.ToUpper(h.Name))
|
||||
}
|
||||
|
||||
// delete
|
||||
this.deleteHeaderMap = map[string]bool{}
|
||||
for _, header := range this.DeleteHeaders {
|
||||
this.deleteHeaderMap[strings.ToUpper(header)] = true
|
||||
}
|
||||
|
||||
// cors
|
||||
if this.CORS != nil {
|
||||
err := this.CORS.Init()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsEmpty 判断是否为空
|
||||
func (this *HTTPHeaderPolicy) IsEmpty() bool {
|
||||
return len(this.SetHeaders) == 0 &&
|
||||
this.Expires == nil &&
|
||||
len(this.DeleteHeaders) == 0 &&
|
||||
len(this.NonStandardHeaders) == 0 &&
|
||||
(this.CORS == nil || !this.CORS.IsOn)
|
||||
}
|
||||
|
||||
// ContainsHeader 判断Add和Set中是否包含某个Header
|
||||
func (this *HTTPHeaderPolicy) ContainsHeader(name string) bool {
|
||||
name = strings.ToUpper(name)
|
||||
|
||||
for _, n := range this.setHeaderNames {
|
||||
if n == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ContainsDeletedHeader 判断删除列表中是否包含某个Header
|
||||
func (this *HTTPHeaderPolicy) ContainsDeletedHeader(name string) bool {
|
||||
_, ok := this.deleteHeaderMap[strings.ToUpper(name)]
|
||||
return ok
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package shared
|
||||
|
||||
type HTTPHeaderPolicyRef struct {
|
||||
IsPrior bool `yaml:"isPrior" json:"isPrior"`
|
||||
IsOn bool `yaml:"isOn" json:"isOn"`
|
||||
HeaderPolicyId int64 `yaml:"headerPolicyId" json:"headerPolicyId"`
|
||||
}
|
||||
|
||||
func (this *HTTPHeaderPolicyRef) Init() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPHeaderPolicy_FormatHeaders(t *testing.T) {
|
||||
policy := &HTTPHeaderPolicy{}
|
||||
err := policy.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPHeaderPolicy_ShouldDeleteHeader(t *testing.T) {
|
||||
a := assert.NewAssertion(t)
|
||||
|
||||
{
|
||||
policy := &HTTPHeaderPolicy{}
|
||||
err := policy.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(policy.ContainsDeletedHeader("Origin"))
|
||||
}
|
||||
{
|
||||
policy := &HTTPHeaderPolicy{
|
||||
DeleteHeaders: []string{"Hello", "World"},
|
||||
}
|
||||
err := policy.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(policy.ContainsDeletedHeader("Origin"))
|
||||
}
|
||||
{
|
||||
policy := &HTTPHeaderPolicy{
|
||||
DeleteHeaders: []string{"origin"},
|
||||
}
|
||||
err := policy.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(policy.ContainsDeletedHeader("Origin"))
|
||||
}
|
||||
{
|
||||
policy := &HTTPHeaderPolicy{
|
||||
DeleteHeaders: []string{"Origin"},
|
||||
}
|
||||
err := policy.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(policy.ContainsDeletedHeader("Origin"))
|
||||
}
|
||||
}
|
||||
7
EdgeCommon/pkg/serverconfigs/shared/http_header_ref.go
Normal file
7
EdgeCommon/pkg/serverconfigs/shared/http_header_ref.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package shared
|
||||
|
||||
// Header引用
|
||||
type HTTPHeaderRef struct {
|
||||
IsOn bool `yaml:"isOn" json:"isOn"`
|
||||
HeaderId int64 `yaml:"headerId" json:"headerId"`
|
||||
}
|
||||
437
EdgeCommon/pkg/serverconfigs/shared/http_request_cond.go
Normal file
437
EdgeCommon/pkg/serverconfigs/shared/http_request_cond.go
Normal file
@@ -0,0 +1,437 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/iwind/TeaGo/lists"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"github.com/iwind/TeaGo/utils/string"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HTTPRequestCond HTTP请求匹配条件定义
|
||||
type HTTPRequestCond struct {
|
||||
Type string `yaml:"type" json:"type"` // 类型,在特殊条件时使用
|
||||
IsRequest bool `yaml:"isRequest" json:"isRequest"` // 是否为请求的条件,用来区分在什么阶段执行
|
||||
|
||||
// 要测试的字符串
|
||||
// 其中可以使用跟请求相关的参数,比如:
|
||||
// ${arg.name}, ${requestPath}
|
||||
Param string `yaml:"param" json:"param"`
|
||||
|
||||
Operator RequestCondOperator `yaml:"operator" json:"operator"` // 运算符
|
||||
Value string `yaml:"value" json:"value"` // 对比值
|
||||
IsReverse bool `yaml:"isReverse" json:"isReverse"` // 是否反向匹配
|
||||
IsCaseInsensitive bool `yaml:"isCaseInsensitive" json:"isCaseInsensitive"` // 大小写是否敏感
|
||||
|
||||
isInt bool
|
||||
isFloat bool
|
||||
isIP bool
|
||||
|
||||
regValue *regexp.Regexp
|
||||
floatValue float64
|
||||
ipValue net.IP
|
||||
arrayValue []string
|
||||
}
|
||||
|
||||
// Init 校验配置
|
||||
func (this *HTTPRequestCond) Init() error {
|
||||
this.isInt = RegexpDigitNumber.MatchString(this.Value)
|
||||
this.isFloat = RegexpFloatNumber.MatchString(this.Value)
|
||||
|
||||
if lists.ContainsString([]string{
|
||||
RequestCondOperatorRegexp,
|
||||
RequestCondOperatorNotRegexp,
|
||||
}, this.Operator) {
|
||||
var value = this.Value
|
||||
if this.IsCaseInsensitive && !strings.HasPrefix(this.Value, "(?i)") {
|
||||
value = "(?i)" + value
|
||||
}
|
||||
reg, err := regexp.Compile(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.regValue = reg
|
||||
} else if lists.ContainsString([]string{
|
||||
RequestCondOperatorWildcardMatch,
|
||||
RequestCondOperatorWildcardNotMatch,
|
||||
}, this.Operator) {
|
||||
var pieces = strings.Split(this.Value, "*")
|
||||
for index, piece := range pieces {
|
||||
pieces[index] = regexp.QuoteMeta(piece)
|
||||
}
|
||||
var pattern = strings.Join(pieces, "(.*)")
|
||||
reg, err := regexp.Compile("(?i)" /** 大小写不敏感 **/ + "^" + pattern + "$")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.regValue = reg
|
||||
} else if lists.ContainsString([]string{
|
||||
RequestCondOperatorEqFloat,
|
||||
RequestCondOperatorGtFloat,
|
||||
RequestCondOperatorGteFloat,
|
||||
RequestCondOperatorLtFloat,
|
||||
RequestCondOperatorLteFloat,
|
||||
}, this.Operator) {
|
||||
this.floatValue = types.Float64(this.Value)
|
||||
} else if lists.ContainsString([]string{
|
||||
RequestCondOperatorEqIP,
|
||||
RequestCondOperatorGtIP,
|
||||
RequestCondOperatorGteIP,
|
||||
RequestCondOperatorLtIP,
|
||||
RequestCondOperatorLteIP,
|
||||
}, this.Operator) {
|
||||
this.ipValue = net.ParseIP(this.Value)
|
||||
this.isIP = this.ipValue != nil
|
||||
|
||||
if !this.isIP {
|
||||
return errors.New("value should be a valid ip")
|
||||
}
|
||||
} else if lists.ContainsString([]string{
|
||||
RequestCondOperatorIPRange,
|
||||
}, this.Operator) {
|
||||
if strings.Contains(this.Value, ",") {
|
||||
ipList := strings.SplitN(this.Value, ",", 2)
|
||||
ipString1 := strings.TrimSpace(ipList[0])
|
||||
ipString2 := strings.TrimSpace(ipList[1])
|
||||
|
||||
if len(ipString1) > 0 {
|
||||
ip1 := net.ParseIP(ipString1)
|
||||
if ip1 == nil {
|
||||
return errors.New("start ip is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
if len(ipString2) > 0 {
|
||||
ip2 := net.ParseIP(ipString2)
|
||||
if ip2 == nil {
|
||||
return errors.New("end ip is invalid")
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(this.Value, "/") {
|
||||
_, _, err := net.ParseCIDR(this.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return errors.New("invalid ip range")
|
||||
}
|
||||
} else if lists.ContainsString([]string{
|
||||
RequestCondOperatorIn,
|
||||
RequestCondOperatorNotIn,
|
||||
RequestCondOperatorFileExt,
|
||||
}, this.Operator) {
|
||||
stringsValue := []string{}
|
||||
err := json.Unmarshal([]byte(this.Value), &stringsValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.arrayValue = stringsValue
|
||||
} else if lists.ContainsString([]string{
|
||||
RequestCondOperatorFileMimeType,
|
||||
}, this.Operator) {
|
||||
stringsValue := []string{}
|
||||
err := json.Unmarshal([]byte(this.Value), &stringsValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for k, v := range stringsValue {
|
||||
if strings.Contains(v, "*") {
|
||||
v = regexp.QuoteMeta(v)
|
||||
v = strings.Replace(v, `\*`, ".*", -1)
|
||||
stringsValue[k] = v
|
||||
}
|
||||
}
|
||||
this.arrayValue = stringsValue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Match 将此条件应用于请求,检查是否匹配
|
||||
func (this *HTTPRequestCond) Match(formatter func(source string) string) bool {
|
||||
b := this.match(formatter)
|
||||
if this.IsReverse {
|
||||
return !b
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (this *HTTPRequestCond) match(formatter func(source string) string) bool {
|
||||
paramValue := formatter(this.Param)
|
||||
switch this.Operator {
|
||||
case RequestCondOperatorRegexp:
|
||||
if this.regValue == nil {
|
||||
return false
|
||||
}
|
||||
return this.regValue.MatchString(paramValue)
|
||||
case RequestCondOperatorNotRegexp:
|
||||
if this.regValue == nil {
|
||||
return false
|
||||
}
|
||||
return !this.regValue.MatchString(paramValue)
|
||||
case RequestCondOperatorWildcardMatch:
|
||||
if this.regValue == nil {
|
||||
return false
|
||||
}
|
||||
return this.regValue.MatchString(paramValue)
|
||||
case RequestCondOperatorWildcardNotMatch:
|
||||
if this.regValue == nil {
|
||||
return false
|
||||
}
|
||||
return !this.regValue.MatchString(paramValue)
|
||||
case RequestCondOperatorEqInt:
|
||||
return this.isInt && paramValue == this.Value
|
||||
case RequestCondOperatorEqFloat:
|
||||
return this.isFloat && types.Float64(paramValue) == this.floatValue
|
||||
case RequestCondOperatorGtFloat:
|
||||
return this.isFloat && types.Float64(paramValue) > this.floatValue
|
||||
case RequestCondOperatorGteFloat:
|
||||
return this.isFloat && types.Float64(paramValue) >= this.floatValue
|
||||
case RequestCondOperatorLtFloat:
|
||||
return this.isFloat && types.Float64(paramValue) < this.floatValue
|
||||
case RequestCondOperatorLteFloat:
|
||||
return this.isFloat && types.Float64(paramValue) <= this.floatValue
|
||||
case RequestCondOperatorMod:
|
||||
pieces := strings.SplitN(this.Value, ",", 2)
|
||||
if len(pieces) == 1 {
|
||||
rem := types.Int64(pieces[0])
|
||||
return types.Int64(paramValue)%10 == rem
|
||||
}
|
||||
div := types.Int64(pieces[0])
|
||||
if div == 0 {
|
||||
return false
|
||||
}
|
||||
rem := types.Int64(pieces[1])
|
||||
return types.Int64(paramValue)%div == rem
|
||||
case RequestCondOperatorMod10:
|
||||
return types.Int64(paramValue)%10 == types.Int64(this.Value)
|
||||
case RequestCondOperatorMod100:
|
||||
return types.Int64(paramValue)%100 == types.Int64(this.Value)
|
||||
case RequestCondOperatorEqString:
|
||||
if this.IsCaseInsensitive {
|
||||
return strings.EqualFold(paramValue, this.Value)
|
||||
}
|
||||
return paramValue == this.Value
|
||||
case RequestCondOperatorNeqString:
|
||||
if this.IsCaseInsensitive {
|
||||
return !strings.EqualFold(paramValue, this.Value)
|
||||
}
|
||||
return paramValue != this.Value
|
||||
case RequestCondOperatorHasPrefix:
|
||||
if this.IsCaseInsensitive {
|
||||
return strings.HasPrefix(strings.ToUpper(paramValue), strings.ToUpper(this.Value))
|
||||
}
|
||||
return strings.HasPrefix(paramValue, this.Value)
|
||||
case RequestCondOperatorHasSuffix:
|
||||
if this.IsCaseInsensitive {
|
||||
return strings.HasSuffix(strings.ToUpper(paramValue), strings.ToUpper(this.Value))
|
||||
}
|
||||
return strings.HasSuffix(paramValue, this.Value)
|
||||
case RequestCondOperatorContainsString:
|
||||
if this.IsCaseInsensitive {
|
||||
return strings.Contains(strings.ToUpper(paramValue), strings.ToUpper(this.Value))
|
||||
}
|
||||
return strings.Contains(paramValue, this.Value)
|
||||
case RequestCondOperatorNotContainsString:
|
||||
if this.IsCaseInsensitive {
|
||||
return !strings.Contains(strings.ToUpper(paramValue), strings.ToUpper(this.Value))
|
||||
}
|
||||
return !strings.Contains(paramValue, this.Value)
|
||||
case RequestCondOperatorEqIP:
|
||||
var ip = net.ParseIP(paramValue)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return this.isIP && ip.Equal(this.ipValue)
|
||||
case RequestCondOperatorGtIP:
|
||||
ip := net.ParseIP(paramValue)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return this.isIP && bytes.Compare(ip, this.ipValue) > 0
|
||||
case RequestCondOperatorGteIP:
|
||||
ip := net.ParseIP(paramValue)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return this.isIP && bytes.Compare(ip, this.ipValue) >= 0
|
||||
case RequestCondOperatorLtIP:
|
||||
ip := net.ParseIP(paramValue)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return this.isIP && bytes.Compare(ip, this.ipValue) < 0
|
||||
case RequestCondOperatorLteIP:
|
||||
ip := net.ParseIP(paramValue)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return this.isIP && bytes.Compare(ip, this.ipValue) <= 0
|
||||
case RequestCondOperatorIPRange:
|
||||
ip := net.ParseIP(paramValue)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查IP范围格式
|
||||
if strings.Contains(this.Value, ",") {
|
||||
ipList := strings.SplitN(this.Value, ",", 2)
|
||||
ipString1 := strings.TrimSpace(ipList[0])
|
||||
ipString2 := strings.TrimSpace(ipList[1])
|
||||
|
||||
if len(ipString1) > 0 {
|
||||
ip1 := net.ParseIP(ipString1)
|
||||
if ip1 == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if bytes.Compare(ip, ip1) < 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(ipString2) > 0 {
|
||||
ip2 := net.ParseIP(ipString2)
|
||||
if ip2 == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if bytes.Compare(ip, ip2) > 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
} else if strings.Contains(this.Value, "/") {
|
||||
_, ipNet, err := net.ParseCIDR(this.Value)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return ipNet.Contains(ip)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case RequestCondOperatorIn:
|
||||
if this.IsCaseInsensitive {
|
||||
paramValue = strings.ToUpper(paramValue)
|
||||
for _, v := range this.arrayValue {
|
||||
if strings.ToUpper(v) == paramValue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
return lists.ContainsString(this.arrayValue, paramValue)
|
||||
}
|
||||
case RequestCondOperatorNotIn:
|
||||
if this.IsCaseInsensitive {
|
||||
paramValue = strings.ToUpper(paramValue)
|
||||
for _, v := range this.arrayValue {
|
||||
if strings.ToUpper(v) == paramValue {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return !lists.ContainsString(this.arrayValue, paramValue)
|
||||
}
|
||||
case RequestCondOperatorFileExt:
|
||||
ext := filepath.Ext(paramValue)
|
||||
if len(ext) > 0 {
|
||||
ext = ext[1:] // remove dot
|
||||
}
|
||||
return lists.ContainsString(this.arrayValue, strings.ToLower(ext))
|
||||
case RequestCondOperatorFileMimeType:
|
||||
index := strings.Index(paramValue, ";")
|
||||
if index >= 0 {
|
||||
paramValue = strings.TrimSpace(paramValue[:index])
|
||||
}
|
||||
if len(this.arrayValue) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, v := range this.arrayValue {
|
||||
if strings.Contains(v, "*") {
|
||||
reg, err := stringutil.RegexpCompile("^" + v + "$")
|
||||
if err == nil && reg.MatchString(paramValue) {
|
||||
return true
|
||||
}
|
||||
} else if paramValue == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case RequestCondOperatorVersionRange:
|
||||
if strings.Contains(this.Value, ",") {
|
||||
versions := strings.SplitN(this.Value, ",", 2)
|
||||
version1 := strings.TrimSpace(versions[0])
|
||||
version2 := strings.TrimSpace(versions[1])
|
||||
if len(version1) > 0 && stringutil.VersionCompare(paramValue, version1) < 0 {
|
||||
return false
|
||||
}
|
||||
if len(version2) > 0 && stringutil.VersionCompare(paramValue, version2) > 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return stringutil.VersionCompare(paramValue, this.Value) >= 0
|
||||
}
|
||||
case RequestCondOperatorIPMod:
|
||||
pieces := strings.SplitN(this.Value, ",", 2)
|
||||
if len(pieces) == 1 {
|
||||
rem := types.Int64(pieces[0])
|
||||
return this.ipToInt64(net.ParseIP(paramValue))%10 == rem
|
||||
}
|
||||
div := types.Int64(pieces[0])
|
||||
if div == 0 {
|
||||
return false
|
||||
}
|
||||
rem := types.Int64(pieces[1])
|
||||
return this.ipToInt64(net.ParseIP(paramValue))%div == rem
|
||||
case RequestCondOperatorIPMod10:
|
||||
return this.ipToInt64(net.ParseIP(paramValue))%10 == types.Int64(this.Value)
|
||||
case RequestCondOperatorIPMod100:
|
||||
return this.ipToInt64(net.ParseIP(paramValue))%100 == types.Int64(this.Value)
|
||||
/**case RequestCondOperatorFileExist:
|
||||
index := strings.Index(paramValue, "?")
|
||||
if index > -1 {
|
||||
paramValue = paramValue[:index]
|
||||
}
|
||||
if len(paramValue) == 0 {
|
||||
return false
|
||||
}
|
||||
if !filepath.IsAbs(paramValue) {
|
||||
paramValue = Tea.Root + Tea.DS + paramValue
|
||||
}
|
||||
stat, err := os.Stat(paramValue)
|
||||
return err == nil && !stat.IsDir()
|
||||
case RequestCondOperatorFileNotExist:
|
||||
index := strings.Index(paramValue, "?")
|
||||
if index > -1 {
|
||||
paramValue = paramValue[:index]
|
||||
}
|
||||
if len(paramValue) == 0 {
|
||||
return true
|
||||
}
|
||||
if !filepath.IsAbs(paramValue) {
|
||||
paramValue = Tea.Root + Tea.DS + paramValue
|
||||
}
|
||||
stat, err := os.Stat(paramValue)
|
||||
return err != nil || stat.IsDir()**/
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (this *HTTPRequestCond) ipToInt64(ip net.IP) int64 {
|
||||
if len(ip) == 0 {
|
||||
return 0
|
||||
}
|
||||
if len(ip) == 16 {
|
||||
return int64(binary.BigEndian.Uint32(ip[12:16]))
|
||||
}
|
||||
return int64(binary.BigEndian.Uint32(ip))
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package shared
|
||||
|
||||
// HTTPRequestCondGroup 请求条件分组
|
||||
type HTTPRequestCondGroup struct {
|
||||
IsOn bool `yaml:"isOn" json:"isOn"` // 是否启用
|
||||
Connector string `yaml:"connector" json:"connector"` // 条件之间的关系
|
||||
Conds []*HTTPRequestCond `yaml:"conds" json:"conds"` // 条件列表
|
||||
IsReverse bool `yaml:"isReverse" json:"isReverse"` // 是否反向匹配
|
||||
Description string `yaml:"description" json:"description"` // 说明
|
||||
|
||||
requestConds []*HTTPRequestCond
|
||||
responseConds []*HTTPRequestCond
|
||||
}
|
||||
|
||||
// Init 初始化
|
||||
func (this *HTTPRequestCondGroup) Init() error {
|
||||
if len(this.Connector) == 0 {
|
||||
this.Connector = "or"
|
||||
}
|
||||
|
||||
this.requestConds = []*HTTPRequestCond{}
|
||||
this.responseConds = []*HTTPRequestCond{}
|
||||
if len(this.Conds) > 0 {
|
||||
for _, cond := range this.Conds {
|
||||
err := cond.Init()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cond.IsRequest {
|
||||
this.requestConds = append(this.requestConds, cond)
|
||||
} else {
|
||||
this.responseConds = append(this.responseConds, cond)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *HTTPRequestCondGroup) MatchRequest(formatter func(source string) string) bool {
|
||||
return this.match(this.requestConds, formatter)
|
||||
}
|
||||
|
||||
func (this *HTTPRequestCondGroup) MatchResponse(formatter func(source string) string) bool {
|
||||
return this.match(this.responseConds, formatter)
|
||||
}
|
||||
|
||||
func (this *HTTPRequestCondGroup) HasRequestConds() bool {
|
||||
return len(this.requestConds) > 0
|
||||
}
|
||||
|
||||
func (this *HTTPRequestCondGroup) HasResponseConds() bool {
|
||||
return len(this.responseConds) > 0
|
||||
}
|
||||
|
||||
func (this *HTTPRequestCondGroup) match(conds []*HTTPRequestCond, formatter func(source string) string) bool {
|
||||
if !this.IsOn || len(conds) == 0 {
|
||||
return !this.IsReverse
|
||||
}
|
||||
ok := false
|
||||
for _, cond := range conds {
|
||||
isMatched := cond.Match(formatter)
|
||||
if this.Connector == "or" && isMatched {
|
||||
return !this.IsReverse
|
||||
}
|
||||
if this.Connector == "and" && !isMatched {
|
||||
return this.IsReverse
|
||||
}
|
||||
if isMatched {
|
||||
// 对于OR来说至少要有一个返回true
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
if this.IsReverse {
|
||||
return !ok
|
||||
}
|
||||
return ok
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPRequestCondGroup_MatchRequest(t *testing.T) {
|
||||
a := assert.NewAssertion(t)
|
||||
|
||||
{
|
||||
group := &HTTPRequestCondGroup{}
|
||||
group.Connector = "or"
|
||||
group.IsOn = false
|
||||
err := group.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(group.MatchRequest(func(source string) string {
|
||||
return source
|
||||
}))
|
||||
a.IsTrue(group.MatchResponse(func(source string) string {
|
||||
return source
|
||||
}))
|
||||
}
|
||||
|
||||
{
|
||||
group := &HTTPRequestCondGroup{}
|
||||
group.IsOn = true
|
||||
err := group.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(group.MatchRequest(func(source string) string {
|
||||
return source
|
||||
}))
|
||||
a.IsTrue(group.MatchResponse(func(source string) string {
|
||||
return source
|
||||
}))
|
||||
}
|
||||
|
||||
{
|
||||
group := &HTTPRequestCondGroup{}
|
||||
group.IsOn = true
|
||||
group.Connector = "or"
|
||||
group.Conds = []*HTTPRequestCond{
|
||||
{
|
||||
IsRequest: true,
|
||||
Param: "456",
|
||||
Operator: "gt",
|
||||
Value: "123",
|
||||
},
|
||||
{
|
||||
IsRequest: false,
|
||||
Param: "123",
|
||||
Operator: "gt",
|
||||
Value: "456",
|
||||
},
|
||||
}
|
||||
err := group.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(group.MatchRequest(func(source string) string {
|
||||
return source
|
||||
}))
|
||||
a.IsFalse(group.MatchResponse(func(source string) string {
|
||||
return source
|
||||
}))
|
||||
}
|
||||
{
|
||||
group := &HTTPRequestCondGroup{}
|
||||
group.IsOn = true
|
||||
group.Connector = "or"
|
||||
group.Conds = []*HTTPRequestCond{
|
||||
{
|
||||
IsRequest: true,
|
||||
Param: "456",
|
||||
Operator: "gt",
|
||||
Value: "1234",
|
||||
},
|
||||
{
|
||||
IsRequest: true,
|
||||
Param: "456",
|
||||
Operator: "gt",
|
||||
Value: "123",
|
||||
},
|
||||
{
|
||||
IsRequest: false,
|
||||
Param: "123",
|
||||
Operator: "gt",
|
||||
Value: "456",
|
||||
},
|
||||
}
|
||||
err := group.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(group.MatchRequest(func(source string) string {
|
||||
return source
|
||||
}))
|
||||
a.IsFalse(group.MatchResponse(func(source string) string {
|
||||
return source
|
||||
}))
|
||||
}
|
||||
{
|
||||
group := &HTTPRequestCondGroup{}
|
||||
group.IsOn = true
|
||||
group.Connector = "and"
|
||||
group.Conds = []*HTTPRequestCond{
|
||||
{
|
||||
IsRequest: true,
|
||||
Param: "456",
|
||||
Operator: "gt",
|
||||
Value: "123",
|
||||
},
|
||||
{
|
||||
IsRequest: true,
|
||||
Param: "456",
|
||||
Operator: "gt",
|
||||
Value: "1234",
|
||||
},
|
||||
{
|
||||
IsRequest: false,
|
||||
Param: "123",
|
||||
Operator: "gt",
|
||||
Value: "456",
|
||||
},
|
||||
}
|
||||
err := group.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(group.MatchRequest(func(source string) string {
|
||||
return source
|
||||
}))
|
||||
a.IsFalse(group.MatchResponse(func(source string) string {
|
||||
return source
|
||||
}))
|
||||
}
|
||||
}
|
||||
1071
EdgeCommon/pkg/serverconfigs/shared/http_request_cond_test.go
Normal file
1071
EdgeCommon/pkg/serverconfigs/shared/http_request_cond_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,94 @@
|
||||
package shared
|
||||
|
||||
// HTTPRequestCondsConfig 条件配置
|
||||
// 数据结构:conds -> []groups -> []cond
|
||||
type HTTPRequestCondsConfig struct {
|
||||
IsOn bool `yaml:"isOn" json:"isOn"`
|
||||
Connector string `yaml:"connector" json:"connector"`
|
||||
Groups []*HTTPRequestCondGroup `yaml:"groups" json:"groups"`
|
||||
|
||||
hasRequestConds bool
|
||||
hasResponseConds bool
|
||||
}
|
||||
|
||||
// Init 初始化
|
||||
func (this *HTTPRequestCondsConfig) Init() error {
|
||||
if len(this.Connector) == 0 {
|
||||
this.Connector = "or"
|
||||
}
|
||||
|
||||
for _, group := range this.Groups {
|
||||
err := group.Init()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 是否有请求条件
|
||||
for _, group := range this.Groups {
|
||||
if group.IsOn {
|
||||
if group.HasRequestConds() {
|
||||
this.hasRequestConds = true
|
||||
}
|
||||
if group.HasResponseConds() {
|
||||
this.hasResponseConds = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MatchRequest 判断请求是否匹配
|
||||
func (this *HTTPRequestCondsConfig) MatchRequest(formatter Formatter) bool {
|
||||
if !this.IsOn || len(this.Groups) == 0 {
|
||||
return true
|
||||
}
|
||||
ok := false
|
||||
for _, group := range this.Groups {
|
||||
b := group.MatchRequest(formatter)
|
||||
if !b && this.Connector == "and" {
|
||||
return false
|
||||
}
|
||||
if b && this.Connector == "or" {
|
||||
return true
|
||||
}
|
||||
if b {
|
||||
// 对于 or 来说至少有一个分组要返回 true
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// MatchResponse 判断响应是否匹配
|
||||
func (this *HTTPRequestCondsConfig) MatchResponse(formatter func(s string) string) bool {
|
||||
if !this.IsOn || len(this.Groups) == 0 {
|
||||
return true
|
||||
}
|
||||
ok := false
|
||||
for _, group := range this.Groups {
|
||||
b := group.MatchResponse(formatter)
|
||||
if !b && this.Connector == "and" {
|
||||
return false
|
||||
}
|
||||
if b && this.Connector == "or" {
|
||||
return true
|
||||
}
|
||||
if b {
|
||||
// 对于 or 来说至少有一个分组要返回 true
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// HasRequestConds 判断是否有请求条件
|
||||
func (this *HTTPRequestCondsConfig) HasRequestConds() bool {
|
||||
return this.hasRequestConds
|
||||
}
|
||||
|
||||
// HasResponseConds 判断是否有响应条件
|
||||
func (this *HTTPRequestCondsConfig) HasResponseConds() bool {
|
||||
return this.hasResponseConds
|
||||
}
|
||||
27
EdgeCommon/pkg/serverconfigs/shared/http_status_config.go
Normal file
27
EdgeCommon/pkg/serverconfigs/shared/http_status_config.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package shared
|
||||
|
||||
// HTTPStatusConfig 状态码
|
||||
type HTTPStatusConfig struct {
|
||||
Always bool `yaml:"always" json:"always"`
|
||||
Codes []int `yaml:"codes" json:"codes"`
|
||||
}
|
||||
|
||||
func (this *HTTPStatusConfig) Init() error {
|
||||
// TODO
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *HTTPStatusConfig) Match(statusCode int) bool {
|
||||
if this.Always {
|
||||
return true
|
||||
}
|
||||
if len(this.Codes) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, c := range this.Codes {
|
||||
if c == statusCode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
147
EdgeCommon/pkg/serverconfigs/shared/ip_range.go
Normal file
147
EdgeCommon/pkg/serverconfigs/shared/ip_range.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/iwind/TeaGo/utils/string"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IPRangeType IP Range类型
|
||||
type IPRangeType = int
|
||||
|
||||
const (
|
||||
IPRangeTypeRange IPRangeType = 1
|
||||
IPRangeTypeCIDR IPRangeType = 2
|
||||
IPRangeTypeAll IPRangeType = 3
|
||||
IPRangeTypeWildcard IPRangeType = 4 // 通配符,可以使用*
|
||||
)
|
||||
|
||||
// IPRangeConfig IP Range
|
||||
type IPRangeConfig struct {
|
||||
Id string `yaml:"id" json:"id"`
|
||||
|
||||
Type IPRangeType `yaml:"type" json:"type"`
|
||||
|
||||
Param string `yaml:"param" json:"param"`
|
||||
CIDR string `yaml:"cidr" json:"cidr"`
|
||||
IPFrom string `yaml:"ipFrom" json:"ipFrom"`
|
||||
IPTo string `yaml:"ipTo" json:"ipTo"`
|
||||
|
||||
cidr *net.IPNet
|
||||
ipFrom net.IP
|
||||
ipTo net.IP
|
||||
reg *regexp.Regexp
|
||||
}
|
||||
|
||||
// NewIPRangeConfig 获取新对象
|
||||
func NewIPRangeConfig() *IPRangeConfig {
|
||||
return &IPRangeConfig{
|
||||
Id: stringutil.Rand(16),
|
||||
}
|
||||
}
|
||||
|
||||
// ParseIPRange 从字符串中分析
|
||||
func ParseIPRange(s string) (*IPRangeConfig, error) {
|
||||
if len(s) == 0 {
|
||||
return nil, errors.New("invalid ip range")
|
||||
}
|
||||
|
||||
ipRange := &IPRangeConfig{}
|
||||
|
||||
if s == "*" || s == "all" || s == "ALL" || s == "0.0.0.0" {
|
||||
ipRange.Type = IPRangeTypeAll
|
||||
return ipRange, nil
|
||||
}
|
||||
|
||||
if strings.Contains(s, "/") {
|
||||
ipRange.Type = IPRangeTypeCIDR
|
||||
ipRange.CIDR = strings.Replace(s, " ", "", -1)
|
||||
} else if strings.Contains(s, "-") {
|
||||
ipRange.Type = IPRangeTypeRange
|
||||
pieces := strings.SplitN(s, "-", 2)
|
||||
ipRange.IPFrom = strings.TrimSpace(pieces[0])
|
||||
ipRange.IPTo = strings.TrimSpace(pieces[1])
|
||||
} else if strings.Contains(s, ",") {
|
||||
ipRange.Type = IPRangeTypeRange
|
||||
pieces := strings.SplitN(s, ",", 2)
|
||||
ipRange.IPFrom = strings.TrimSpace(pieces[0])
|
||||
ipRange.IPTo = strings.TrimSpace(pieces[1])
|
||||
} else if strings.Contains(s, "*") {
|
||||
ipRange.Type = IPRangeTypeWildcard
|
||||
s = "^" + strings.Replace(regexp.QuoteMeta(s), `\*`, `\d+`, -1) + "$"
|
||||
ipRange.reg = regexp.MustCompile(s)
|
||||
} else {
|
||||
ipRange.Type = IPRangeTypeRange
|
||||
ipRange.IPFrom = s
|
||||
ipRange.IPTo = s
|
||||
}
|
||||
|
||||
err := ipRange.Init()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ipRange, nil
|
||||
}
|
||||
|
||||
// Init 初始化校验
|
||||
func (this *IPRangeConfig) Init() error {
|
||||
if this.Type == IPRangeTypeCIDR {
|
||||
if len(this.CIDR) == 0 {
|
||||
return errors.New("cidr should not be empty")
|
||||
}
|
||||
|
||||
_, cidr, err := net.ParseCIDR(this.CIDR)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.cidr = cidr
|
||||
}
|
||||
|
||||
if this.Type == IPRangeTypeRange {
|
||||
this.ipFrom = net.ParseIP(this.IPFrom)
|
||||
this.ipTo = net.ParseIP(this.IPTo)
|
||||
|
||||
if this.ipFrom.To4() == nil && this.ipFrom.To16() == nil {
|
||||
return errors.New("from ip should in IPv4 or IPV6 format")
|
||||
}
|
||||
|
||||
if this.ipTo.To4() == nil && this.ipTo.To16() == nil {
|
||||
return errors.New("to ip should in IPv4 or IPV6 format")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Contains 是否包含某个IP
|
||||
func (this *IPRangeConfig) Contains(ipString string) bool {
|
||||
ip := net.ParseIP(ipString)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
if this.Type == IPRangeTypeCIDR {
|
||||
if this.cidr == nil {
|
||||
return false
|
||||
}
|
||||
return this.cidr.Contains(ip)
|
||||
}
|
||||
if this.Type == IPRangeTypeRange {
|
||||
if this.ipFrom == nil || this.ipTo == nil {
|
||||
return false
|
||||
}
|
||||
return bytes.Compare(ip, this.ipFrom) >= 0 && bytes.Compare(ip, this.ipTo) <= 0
|
||||
}
|
||||
if this.Type == IPRangeTypeWildcard {
|
||||
if this.reg == nil {
|
||||
return false
|
||||
}
|
||||
return this.reg.MatchString(ipString)
|
||||
}
|
||||
if this.Type == IPRangeTypeAll {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
145
EdgeCommon/pkg/serverconfigs/shared/ip_range_test.go
Normal file
145
EdgeCommon/pkg/serverconfigs/shared/ip_range_test.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGeoConfig_Contains(t *testing.T) {
|
||||
a := assert.NewAssertion(t)
|
||||
|
||||
{
|
||||
r := NewIPRangeConfig()
|
||||
r.Type = IPRangeTypeRange
|
||||
r.IPFrom = "192.168.1.100"
|
||||
r.IPTo = "192.168.1.110"
|
||||
a.IsNil(r.Init())
|
||||
a.IsTrue(r.Contains("192.168.1.100"))
|
||||
a.IsTrue(r.Contains("192.168.1.101"))
|
||||
a.IsTrue(r.Contains("192.168.1.110"))
|
||||
a.IsFalse(r.Contains("192.168.1.111"))
|
||||
}
|
||||
|
||||
{
|
||||
r := NewIPRangeConfig()
|
||||
r.Type = IPRangeTypeCIDR
|
||||
r.CIDR = "192.168.1.1/24"
|
||||
a.IsNil(r.Init())
|
||||
a.IsTrue(r.Contains("192.168.1.100"))
|
||||
a.IsFalse(r.Contains("192.168.2.100"))
|
||||
}
|
||||
|
||||
{
|
||||
r := NewIPRangeConfig()
|
||||
r.Type = IPRangeTypeCIDR
|
||||
r.CIDR = "192.168.1.1/16"
|
||||
a.IsNil(r.Init())
|
||||
a.IsTrue(r.Contains("192.168.2.100"))
|
||||
}
|
||||
|
||||
{
|
||||
r := NewIPRangeConfig()
|
||||
r.Type = IPRangeTypeRange
|
||||
r.IPFrom = "::1"
|
||||
r.IPTo = "::1"
|
||||
a.IsNil(r.Init())
|
||||
a.IsTrue(r.Contains("::1"))
|
||||
}
|
||||
|
||||
{
|
||||
r := NewIPRangeConfig()
|
||||
r.Type = IPRangeTypeRange
|
||||
r.IPFrom = "::1"
|
||||
r.IPTo = "::100"
|
||||
a.IsNil(r.Init())
|
||||
a.IsTrue(r.Contains("::1"))
|
||||
a.IsTrue(r.Contains("::99"))
|
||||
a.IsFalse(r.Contains("::101"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIPRange(t *testing.T) {
|
||||
a := assert.NewAssertion(t)
|
||||
|
||||
{
|
||||
_, err := ParseIPRange("")
|
||||
a.IsNotNil(err)
|
||||
}
|
||||
|
||||
{
|
||||
r, err := ParseIPRange("192.168.1.100")
|
||||
a.IsNil(err)
|
||||
a.IsTrue(r.IPFrom == r.IPTo)
|
||||
a.IsTrue(r.IPFrom == "192.168.1.100")
|
||||
a.IsTrue(r.Contains("192.168.1.100"))
|
||||
a.IsFalse(r.Contains("192.168.1.99"))
|
||||
}
|
||||
|
||||
{
|
||||
r, err := ParseIPRange("192.168.1.100/24")
|
||||
a.IsNil(err)
|
||||
a.IsTrue(r.CIDR == "192.168.1.100/24")
|
||||
a.IsTrue(r.Contains("192.168.1.100"))
|
||||
a.IsTrue(r.Contains("192.168.1.99"))
|
||||
a.IsFalse(r.Contains("192.168.2.100"))
|
||||
}
|
||||
|
||||
{
|
||||
r, err := ParseIPRange("192.168.1.100, 192.168.1.200")
|
||||
a.IsNil(err)
|
||||
a.IsTrue(r.IPFrom == "192.168.1.100")
|
||||
a.IsTrue(r.IPTo == "192.168.1.200")
|
||||
a.IsTrue(r.Contains("192.168.1.100"))
|
||||
a.IsTrue(r.Contains("192.168.1.150"))
|
||||
a.IsFalse(r.Contains("192.168.2.100"))
|
||||
}
|
||||
|
||||
{
|
||||
r, err := ParseIPRange("192.168.1.100-192.168.1.200")
|
||||
a.IsNil(err)
|
||||
a.IsTrue(r.IPFrom == "192.168.1.100")
|
||||
a.IsTrue(r.IPTo == "192.168.1.200")
|
||||
a.IsTrue(r.Contains("192.168.1.100"))
|
||||
a.IsTrue(r.Contains("192.168.1.150"))
|
||||
a.IsFalse(r.Contains("192.168.2.100"))
|
||||
}
|
||||
|
||||
{
|
||||
r, err := ParseIPRange("all")
|
||||
a.IsNil(err)
|
||||
a.IsTrue(r.Type == IPRangeTypeAll)
|
||||
a.IsTrue(r.Contains("192.168.1.100"))
|
||||
a.IsTrue(r.Contains("192.168.1.150"))
|
||||
a.IsTrue(r.Contains("192.168.2.100"))
|
||||
}
|
||||
|
||||
{
|
||||
r, err := ParseIPRange("192.168.1.*")
|
||||
a.IsNil(err)
|
||||
if r != nil {
|
||||
a.IsTrue(r.Type == IPRangeTypeWildcard)
|
||||
a.IsTrue(r.Contains("192.168.1.100"))
|
||||
a.IsFalse(r.Contains("192.168.2.100"))
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
r, err := ParseIPRange("192.168.*.*")
|
||||
a.IsNil(err)
|
||||
if r != nil {
|
||||
a.IsTrue(r.Type == IPRangeTypeWildcard)
|
||||
a.IsTrue(r.Contains("192.168.1.100"))
|
||||
a.IsTrue(r.Contains("192.168.2.100"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIPRangeConfig_Contains(b *testing.B) {
|
||||
r, err := ParseIPRange("192.168.1.*")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = r.Contains("192.168.1.100")
|
||||
}
|
||||
}
|
||||
21
EdgeCommon/pkg/serverconfigs/shared/locker.go
Normal file
21
EdgeCommon/pkg/serverconfigs/shared/locker.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var Locker = new(FileLocker)
|
||||
|
||||
// global file modify locker
|
||||
type FileLocker struct {
|
||||
locker sync.Mutex
|
||||
}
|
||||
|
||||
// lock
|
||||
func (this *FileLocker) Lock() {
|
||||
this.locker.Lock()
|
||||
}
|
||||
|
||||
func (this *FileLocker) Unlock() {
|
||||
this.locker.Unlock()
|
||||
}
|
||||
43
EdgeCommon/pkg/serverconfigs/shared/mime_type_rule.go
Normal file
43
EdgeCommon/pkg/serverconfigs/shared/mime_type_rule.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MimeTypeRule mime type规则
|
||||
type MimeTypeRule struct {
|
||||
Value string
|
||||
|
||||
isAll bool
|
||||
regexp *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewMimeTypeRule(mimeType string) (*MimeTypeRule, error) {
|
||||
mimeType = strings.ToLower(mimeType)
|
||||
|
||||
var rule = &MimeTypeRule{
|
||||
Value: mimeType,
|
||||
}
|
||||
if mimeType == "*/*" || mimeType == "*" {
|
||||
rule.isAll = true
|
||||
} else if strings.Contains(mimeType, "*") {
|
||||
mimeType = strings.ReplaceAll(regexp.QuoteMeta(mimeType), `\*`, ".+")
|
||||
reg, err := regexp.Compile("^(?i)" + mimeType + "$")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rule.regexp = reg
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
func (this *MimeTypeRule) Match(mimeType string) bool {
|
||||
if this.isAll {
|
||||
return true
|
||||
}
|
||||
if this.regexp == nil {
|
||||
return this.Value == strings.ToLower(mimeType)
|
||||
}
|
||||
return this.regexp.MatchString(mimeType)
|
||||
}
|
||||
49
EdgeCommon/pkg/serverconfigs/shared/mime_type_rule_test.go
Normal file
49
EdgeCommon/pkg/serverconfigs/shared/mime_type_rule_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package shared
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMimeTypeRule_Match(t *testing.T) {
|
||||
a := assert.NewAssertion(t)
|
||||
|
||||
{
|
||||
rule, err := NewMimeTypeRule("text/plain")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(rule.Match("text/plain"))
|
||||
a.IsTrue(rule.Match("TEXT/plain"))
|
||||
a.IsFalse(rule.Match("text/html"))
|
||||
}
|
||||
|
||||
{
|
||||
rule, err := NewMimeTypeRule("image/*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(rule.Match("image/png"))
|
||||
a.IsTrue(rule.Match("IMAGE/jpeg"))
|
||||
a.IsFalse(rule.Match("image/"))
|
||||
a.IsFalse(rule.Match("image1/png"))
|
||||
a.IsFalse(rule.Match("x-image/png"))
|
||||
}
|
||||
|
||||
{
|
||||
_, err := NewMimeTypeRule("x-image/*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
rule, err := NewMimeTypeRule("*/*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(rule.Match("any/thing"))
|
||||
}
|
||||
}
|
||||
13
EdgeCommon/pkg/serverconfigs/shared/regexp.go
Normal file
13
EdgeCommon/pkg/serverconfigs/shared/regexp.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package shared
|
||||
|
||||
import "regexp"
|
||||
|
||||
// 常用的正则表达式
|
||||
var (
|
||||
RegexpDigitNumber = regexp.MustCompile(`^\d+$`) // 正整数
|
||||
RegexpFloatNumber = regexp.MustCompile(`^\d+(\.\d+)?$`) // 正浮点数,不支持e
|
||||
RegexpAllDigitNumber = regexp.MustCompile(`^[+-]?\d+$`) // 整数,支持正负数
|
||||
RegexpAllFloatNumber = regexp.MustCompile(`^[+-]?\d+(\.\d+)?$`) // 浮点数,支持正负数,不支持e
|
||||
RegexpExternalURL = regexp.MustCompile("(?i)^(http|https|ftp)://") // URL
|
||||
RegexpNamedVariable = regexp.MustCompile(`\${[\w.-]+}`) // 命名变量
|
||||
)
|
||||
21
EdgeCommon/pkg/serverconfigs/shared/regexp_test.go
Normal file
21
EdgeCommon/pkg/serverconfigs/shared/regexp_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package shared_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/shared"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegexp(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
|
||||
a.IsTrue(shared.RegexpFloatNumber.MatchString("123"))
|
||||
a.IsTrue(shared.RegexpFloatNumber.MatchString("123.456"))
|
||||
a.IsFalse(shared.RegexpFloatNumber.MatchString(".456"))
|
||||
a.IsFalse(shared.RegexpFloatNumber.MatchString("abc"))
|
||||
a.IsFalse(shared.RegexpFloatNumber.MatchString("123."))
|
||||
a.IsFalse(shared.RegexpFloatNumber.MatchString("123.456e7"))
|
||||
a.IsTrue(shared.RegexpNamedVariable.MatchString("${abc.efg}"))
|
||||
a.IsTrue(shared.RegexpNamedVariable.MatchString("${abc}"))
|
||||
a.IsFalse(shared.RegexpNamedVariable.MatchString("{abc.efg}"))
|
||||
}
|
||||
43
EdgeCommon/pkg/serverconfigs/shared/request_call.go
Normal file
43
EdgeCommon/pkg/serverconfigs/shared/request_call.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// RequestCall 请求调用
|
||||
type RequestCall struct {
|
||||
Formatter func(source string) string // 当前变量格式化函数
|
||||
Request *http.Request // 当前请求
|
||||
Domain string // 当前域名
|
||||
|
||||
ResponseCallbacks []func(resp http.ResponseWriter)
|
||||
Options maps.Map
|
||||
}
|
||||
|
||||
// NewRequestCall 获取新对象
|
||||
func NewRequestCall() *RequestCall {
|
||||
return &RequestCall{
|
||||
Options: maps.Map{},
|
||||
}
|
||||
}
|
||||
|
||||
// Reset 重置
|
||||
func (this *RequestCall) Reset() {
|
||||
this.Formatter = nil
|
||||
this.Request = nil
|
||||
this.ResponseCallbacks = nil
|
||||
this.Options = maps.Map{}
|
||||
}
|
||||
|
||||
// AddResponseCall 添加响应回调
|
||||
func (this *RequestCall) AddResponseCall(callback func(resp http.ResponseWriter)) {
|
||||
this.ResponseCallbacks = append(this.ResponseCallbacks, callback)
|
||||
}
|
||||
|
||||
// CallResponseCallbacks 执行响应回调
|
||||
func (this *RequestCall) CallResponseCallbacks(resp http.ResponseWriter) {
|
||||
for _, callback := range this.ResponseCallbacks {
|
||||
callback(resp)
|
||||
}
|
||||
}
|
||||
249
EdgeCommon/pkg/serverconfigs/shared/request_operators.go
Normal file
249
EdgeCommon/pkg/serverconfigs/shared/request_operators.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package shared
|
||||
|
||||
import "github.com/iwind/TeaGo/maps"
|
||||
|
||||
// RequestCondOperator 运算符定义
|
||||
type RequestCondOperator = string
|
||||
|
||||
const (
|
||||
|
||||
// 正则
|
||||
|
||||
RequestCondOperatorRegexp RequestCondOperator = "regexp"
|
||||
RequestCondOperatorNotRegexp RequestCondOperator = "not regexp"
|
||||
|
||||
// 通配符
|
||||
|
||||
RequestCondOperatorWildcardMatch RequestCondOperator = "wildcard match"
|
||||
RequestCondOperatorWildcardNotMatch RequestCondOperator = "wildcard not match"
|
||||
|
||||
// 数字相关
|
||||
|
||||
RequestCondOperatorEqInt RequestCondOperator = "eq int" // 整数等于
|
||||
RequestCondOperatorEqFloat RequestCondOperator = "eq float" // 浮点数等于
|
||||
RequestCondOperatorGtFloat RequestCondOperator = "gt"
|
||||
RequestCondOperatorGteFloat RequestCondOperator = "gte"
|
||||
RequestCondOperatorLtFloat RequestCondOperator = "lt"
|
||||
RequestCondOperatorLteFloat RequestCondOperator = "lte"
|
||||
|
||||
// 取模
|
||||
|
||||
RequestCondOperatorMod10 RequestCondOperator = "mod 10"
|
||||
RequestCondOperatorMod100 RequestCondOperator = "mod 100"
|
||||
RequestCondOperatorMod RequestCondOperator = "mod"
|
||||
|
||||
// 字符串相关
|
||||
|
||||
RequestCondOperatorEqString RequestCondOperator = "eq"
|
||||
RequestCondOperatorNeqString RequestCondOperator = "not"
|
||||
RequestCondOperatorHasPrefix RequestCondOperator = "prefix"
|
||||
RequestCondOperatorHasSuffix RequestCondOperator = "suffix"
|
||||
RequestCondOperatorContainsString RequestCondOperator = "contains"
|
||||
RequestCondOperatorNotContainsString RequestCondOperator = "not contains"
|
||||
RequestCondOperatorIn RequestCondOperator = "in"
|
||||
RequestCondOperatorNotIn RequestCondOperator = "not in"
|
||||
RequestCondOperatorFileExt RequestCondOperator = "file ext"
|
||||
RequestCondOperatorFileMimeType RequestCondOperator = "mime type"
|
||||
RequestCondOperatorVersionRange RequestCondOperator = "version range"
|
||||
|
||||
// IP相关
|
||||
|
||||
RequestCondOperatorEqIP RequestCondOperator = "eq ip"
|
||||
RequestCondOperatorGtIP RequestCondOperator = "gt ip"
|
||||
RequestCondOperatorGteIP RequestCondOperator = "gte ip"
|
||||
RequestCondOperatorLtIP RequestCondOperator = "lt ip"
|
||||
RequestCondOperatorLteIP RequestCondOperator = "lte ip"
|
||||
RequestCondOperatorIPRange RequestCondOperator = "ip range"
|
||||
RequestCondOperatorIPMod10 RequestCondOperator = "ip mod 10"
|
||||
RequestCondOperatorIPMod100 RequestCondOperator = "ip mod 100"
|
||||
RequestCondOperatorIPMod RequestCondOperator = "ip mod"
|
||||
|
||||
// 文件相关
|
||||
// 为了安全暂时不提供
|
||||
|
||||
//RequestCondOperatorFileExist RequestCondOperator = "file exist"
|
||||
//RequestCondOperatorFileNotExist RequestCondOperator = "file not exist"
|
||||
)
|
||||
|
||||
// AllRequestOperators 所有的运算符
|
||||
func AllRequestOperators() []maps.Map {
|
||||
return []maps.Map{
|
||||
{
|
||||
"name": "正则表达式匹配",
|
||||
"op": RequestCondOperatorRegexp,
|
||||
"description": "判断是否正则表达式匹配",
|
||||
},
|
||||
{
|
||||
"name": "正则表达式不匹配",
|
||||
"op": RequestCondOperatorNotRegexp,
|
||||
"description": "判断是否正则表达式不匹配",
|
||||
},
|
||||
{
|
||||
"name": "通配符匹配",
|
||||
"op": RequestCondOperatorWildcardMatch,
|
||||
"description": "判断是否和指定的通配符匹配",
|
||||
},
|
||||
{
|
||||
"name": "通配符不匹配",
|
||||
"op": RequestCondOperatorWildcardNotMatch,
|
||||
"description": "判断是否和指定的通配符不匹配",
|
||||
},
|
||||
{
|
||||
"name": "字符串等于",
|
||||
"op": RequestCondOperatorEqString,
|
||||
"description": "使用字符串对比参数值是否相等于某个值",
|
||||
},
|
||||
{
|
||||
"name": "字符串前缀",
|
||||
"op": RequestCondOperatorHasPrefix,
|
||||
"description": "参数值包含某个前缀",
|
||||
},
|
||||
{
|
||||
"name": "字符串后缀",
|
||||
"op": RequestCondOperatorHasSuffix,
|
||||
"description": "参数值包含某个后缀",
|
||||
},
|
||||
{
|
||||
"name": "字符串包含",
|
||||
"op": RequestCondOperatorContainsString,
|
||||
"description": "参数值包含另外一个字符串",
|
||||
},
|
||||
{
|
||||
"name": "字符串不包含",
|
||||
"op": RequestCondOperatorNotContainsString,
|
||||
"description": "参数值不包含另外一个字符串",
|
||||
},
|
||||
{
|
||||
"name": "字符串不等于",
|
||||
"op": RequestCondOperatorNeqString,
|
||||
"description": "使用字符串对比参数值是否不相等于某个值",
|
||||
},
|
||||
{
|
||||
"name": "在列表中",
|
||||
"op": RequestCondOperatorIn,
|
||||
"description": "判断参数值在某个列表中",
|
||||
},
|
||||
{
|
||||
"name": "不在列表中",
|
||||
"op": RequestCondOperatorNotIn,
|
||||
"description": "判断参数值不在某个列表中",
|
||||
},
|
||||
{
|
||||
"name": "扩展名",
|
||||
"op": RequestCondOperatorFileExt,
|
||||
"description": "判断小写的扩展名(不带点)在某个列表中",
|
||||
},
|
||||
{
|
||||
"name": "MimeType",
|
||||
"op": RequestCondOperatorFileMimeType,
|
||||
"description": "判断MimeType在某个列表中,支持类似于image/*的语法",
|
||||
},
|
||||
{
|
||||
"name": "版本号范围",
|
||||
"op": RequestCondOperatorVersionRange,
|
||||
"description": "判断版本号在某个范围内,格式为version1,version2",
|
||||
},
|
||||
{
|
||||
"name": "整数等于",
|
||||
"op": RequestCondOperatorEqInt,
|
||||
"description": "将参数转换为整数数字后进行对比",
|
||||
},
|
||||
{
|
||||
"name": "浮点数等于",
|
||||
"op": RequestCondOperatorEqFloat,
|
||||
"description": "将参数转换为可以有小数的浮点数字进行对比",
|
||||
},
|
||||
{
|
||||
"name": "数字大于",
|
||||
"op": RequestCondOperatorGtFloat,
|
||||
"description": "将参数转换为数字进行对比",
|
||||
},
|
||||
{
|
||||
"name": "数字大于等于",
|
||||
"op": RequestCondOperatorGteFloat,
|
||||
"description": "将参数转换为数字进行对比",
|
||||
},
|
||||
{
|
||||
"name": "数字小于",
|
||||
"op": RequestCondOperatorLtFloat,
|
||||
"description": "将参数转换为数字进行对比",
|
||||
},
|
||||
{
|
||||
"name": "数字小于等于",
|
||||
"op": RequestCondOperatorLteFloat,
|
||||
"description": "将参数转换为数字进行对比",
|
||||
},
|
||||
{
|
||||
"name": "整数取模10",
|
||||
"op": RequestCondOperatorMod10,
|
||||
"description": "对整数参数值取模,除数为10,对比值为余数",
|
||||
},
|
||||
{
|
||||
"name": "整数取模100",
|
||||
"op": RequestCondOperatorMod100,
|
||||
"description": "对整数参数值取模,除数为100,对比值为余数",
|
||||
},
|
||||
{
|
||||
"name": "整数取模",
|
||||
"op": RequestCondOperatorMod,
|
||||
"description": "对整数参数值取模,对比值格式为:除数,余数,比如10,1",
|
||||
},
|
||||
{
|
||||
"name": "IP等于",
|
||||
"op": RequestCondOperatorEqIP,
|
||||
"description": "将参数转换为IP进行对比",
|
||||
},
|
||||
{
|
||||
"name": "IP大于",
|
||||
"op": RequestCondOperatorGtIP,
|
||||
"description": "将参数转换为IP进行对比",
|
||||
},
|
||||
{
|
||||
"name": "IP大于等于",
|
||||
"op": RequestCondOperatorGteIP,
|
||||
"description": "将参数转换为IP进行对比",
|
||||
},
|
||||
{
|
||||
"name": "IP小于",
|
||||
"op": RequestCondOperatorLtIP,
|
||||
"description": "将参数转换为IP进行对比",
|
||||
},
|
||||
{
|
||||
"name": "IP小于等于",
|
||||
"op": RequestCondOperatorLteIP,
|
||||
"description": "将参数转换为IP进行对比",
|
||||
},
|
||||
{
|
||||
"name": "IP范围",
|
||||
"op": RequestCondOperatorIPRange,
|
||||
"description": "IP在某个范围之内,范围格式可以是英文逗号分隔的<code-label>开始IP,结束IP</code-label>,比如<code-label>192.168.1.100,192.168.2.200</code-label>,或者CIDR格式的ip/bits,比如<code-label>192.168.2.1/24</code-label>",
|
||||
},
|
||||
{
|
||||
"name": "IP取模10",
|
||||
"op": RequestCondOperatorIPMod10,
|
||||
"description": "对IP参数值取模,除数为10,对比值为余数",
|
||||
},
|
||||
{
|
||||
"name": "IP取模100",
|
||||
"op": RequestCondOperatorIPMod100,
|
||||
"description": "对IP参数值取模,除数为100,对比值为余数",
|
||||
},
|
||||
{
|
||||
"name": "IP取模",
|
||||
"op": RequestCondOperatorIPMod,
|
||||
"description": "对IP参数值取模,对比值格式为:除数,余数,比如10,1",
|
||||
},
|
||||
|
||||
/**{
|
||||
"name": "文件存在",
|
||||
"op": RequestCondOperatorFileExist,
|
||||
"description": "判断参数值解析后的文件是否存在",
|
||||
},
|
||||
|
||||
{
|
||||
"name": "文件不存在",
|
||||
"op": RequestCondOperatorFileNotExist,
|
||||
"description": "判断参数值解析后的文件是否不存在",
|
||||
},**/
|
||||
}
|
||||
}
|
||||
58
EdgeCommon/pkg/serverconfigs/shared/request_variables.go
Normal file
58
EdgeCommon/pkg/serverconfigs/shared/request_variables.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package shared
|
||||
|
||||
import "github.com/iwind/TeaGo/maps"
|
||||
|
||||
// DefaultRequestVariables 默认的请求变量列表
|
||||
func DefaultRequestVariables() []maps.Map {
|
||||
return []maps.Map{
|
||||
{"code": "${edgeVersion}", "name": "边缘节点版本", "description": ""},
|
||||
{"code": "${remoteAddr}", "name": "客户端地址(IP)", "description": "会依次根据X-Forwarded-For、X-Real-IP、RemoteAddr获取,适合前端有别的反向代理服务时使用,存在伪造的风险"},
|
||||
{"code": "${rawRemoteAddr}", "name": "客户端地址(IP)", "description": "返回直接连接服务的客户端原始IP地址"},
|
||||
{"code": "${remotePort}", "name": "客户端端口", "description": ""},
|
||||
{"code": "${remoteUser}", "name": "客户端用户名", "description": ""},
|
||||
{"code": "${requestURI}", "name": "请求URI", "description": "比如/hello?name=lily"},
|
||||
{"code": "${requestPath}", "name": "请求路径(不包括参数)", "description": "比如/hello"},
|
||||
{"code": "${requestURL}", "name": "完整的请求URL", "description": "比如https://example.com/hello?name=lily"},
|
||||
{"code": "${requestLength}", "name": "请求内容长度", "description": ""},
|
||||
{"code": "${requestMethod}", "name": "请求方法", "description": "比如GET、POST"},
|
||||
{"code": "${requestFilename}", "name": "请求文件路径", "description": ""},
|
||||
{"code": "${requestPathExtension}", "name": "请求文件扩展名", "description": "请求路径中的文件扩展名,包括点符号,比如.html、.png"},
|
||||
{"code": "${requestPathLowerExtension}", "name": "请求文件小写扩展名", "description": "请求路径中的文件扩展名,其中大写字母会被自动转换为小写,包括点符号,比如.html、.png"},
|
||||
{"code": "${scheme}", "name": "请求协议,http或https", "description": ""},
|
||||
{"code": "${proto}", "name": "包含版本的HTTP请求协议", "description:": "类似于HTTP/1.0"},
|
||||
{"code": "${timeISO8601}", "name": "ISO 8601格式的时间", "description": "比如2018-07-16T23:52:24.839+08:00"},
|
||||
{"code": "${timeLocal}", "name": "本地时间", "description": "比如17/Jul/2018:09:52:24 +0800"},
|
||||
{"code": "${msec}", "name": "带有毫秒的时间", "description": "比如1531756823.054"},
|
||||
{"code": "${timestamp}", "name": "unix时间戳,单位为秒", "description": ""},
|
||||
{"code": "${host}", "name": "主机名", "description": ""},
|
||||
{"code": "${cname}", "name": "当前网站的CNAME", "description": "比如38b48e4f.goedge.cn"},
|
||||
{"code": "${serverName}", "name": "接收请求的服务器名", "description": ""},
|
||||
{"code": "${serverPort}", "name": "接收请求的服务器端口", "description": ""},
|
||||
{"code": "${referer}", "name": "请求来源URL", "description": ""},
|
||||
{"code": "${referer.host}", "name": "请求来源URL域名", "description": ""},
|
||||
{"code": "${userAgent}", "name": "客户端信息", "description": ""},
|
||||
{"code": "${contentType}", "name": "请求头部的Content-Type", "description": ""},
|
||||
{"code": "${cookies}", "name": "所有cookie组合字符串", "description": ""},
|
||||
{"code": "${cookie.NAME}", "name": "单个cookie值", "description": ""},
|
||||
{"code": "${isArgs}", "name": "问号(?)标记", "description": "如果URL有参数,则值为`?`;否则,则值为空"},
|
||||
{"code": "${args}", "name": "所有参数组合字符串", "description": ""},
|
||||
{"code": "${arg.NAME}", "name": "单个参数值", "description": ""},
|
||||
{"code": "${headers}", "name": "所有Header信息组合字符串", "description": ""},
|
||||
{"code": "${header.NAME}", "name": "单个Header值", "description": ""},
|
||||
{"code": "${geo.country.name}", "name": "国家/地区名称", "description": ""},
|
||||
{"code": "${geo.country.id}", "name": "国家/地区ID", "description": ""},
|
||||
{"code": "${geo.province.name}", "name": "省份名称", "description": "目前只包含中国省份"},
|
||||
{"code": "${geo.province.id}", "name": "省份ID", "description": "目前只包含中国省份"},
|
||||
{"code": "${geo.city.name}", "name": "城市名称", "description": "目前只包含中国城市"},
|
||||
{"code": "${geo.city.id}", "name": "城市名称", "description": "目前只包含中国城市"},
|
||||
{"code": "${isp.name}", "name": "ISP服务商名称", "description": ""},
|
||||
{"code": "${isp.id}", "name": "ISP服务商ID", "description": ""},
|
||||
{"code": "${browser.os.name}", "name": "操作系统名称", "description": "客户端所在操作系统名称"},
|
||||
{"code": "${browser.os.version}", "name": "操作系统版本", "description": "客户端所在操作系统版本"},
|
||||
{"code": "${browser.name}", "name": "浏览器名称", "description": "客户端浏览器名称"},
|
||||
{"code": "${browser.version}", "name": "浏览器版本", "description": "客户端浏览器版本"},
|
||||
{"code": "${browser.isMobile}", "name": "手机标识", "description": "如果客户端是手机,则值为1,否则为0"},
|
||||
}
|
||||
}
|
||||
77
EdgeCommon/pkg/serverconfigs/shared/size_capacity.go
Normal file
77
EdgeCommon/pkg/serverconfigs/shared/size_capacity.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package shared
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type SizeCapacityUnit = string
|
||||
|
||||
const (
|
||||
SizeCapacityUnitByte SizeCapacityUnit = "byte"
|
||||
SizeCapacityUnitKB SizeCapacityUnit = "kb"
|
||||
SizeCapacityUnitMB SizeCapacityUnit = "mb"
|
||||
SizeCapacityUnitGB SizeCapacityUnit = "gb"
|
||||
SizeCapacityUnitTB SizeCapacityUnit = "tb"
|
||||
SizeCapacityUnitPB SizeCapacityUnit = "pb"
|
||||
SizeCapacityUnitEB SizeCapacityUnit = "eb"
|
||||
//SizeCapacityUnitZB SizeCapacityUnit = "zb" // zb和yb超出int64范围,暂不支持
|
||||
//SizeCapacityUnitYB SizeCapacityUnit = "yb"
|
||||
)
|
||||
|
||||
type SizeCapacity struct {
|
||||
Count int64 `json:"count" yaml:"count"`
|
||||
Unit SizeCapacityUnit `json:"unit" yaml:"unit"`
|
||||
}
|
||||
|
||||
func NewSizeCapacity(count int64, unit SizeCapacityUnit) *SizeCapacity {
|
||||
return &SizeCapacity{
|
||||
Count: count,
|
||||
Unit: unit,
|
||||
}
|
||||
}
|
||||
|
||||
func DecodeSizeCapacityJSON(sizeCapacityJSON []byte) (*SizeCapacity, error) {
|
||||
var capacity = &SizeCapacity{}
|
||||
err := json.Unmarshal(sizeCapacityJSON, capacity)
|
||||
return capacity, err
|
||||
}
|
||||
|
||||
func (this *SizeCapacity) Bytes() int64 {
|
||||
if this.Count < 0 {
|
||||
return -1
|
||||
}
|
||||
switch this.Unit {
|
||||
case SizeCapacityUnitByte:
|
||||
return this.Count
|
||||
case SizeCapacityUnitKB:
|
||||
return this.Count * this.pow(1)
|
||||
case SizeCapacityUnitMB:
|
||||
return this.Count * this.pow(2)
|
||||
case SizeCapacityUnitGB:
|
||||
return this.Count * this.pow(3)
|
||||
case SizeCapacityUnitTB:
|
||||
return this.Count * this.pow(4)
|
||||
case SizeCapacityUnitPB:
|
||||
return this.Count * this.pow(5)
|
||||
case SizeCapacityUnitEB:
|
||||
return this.Count * this.pow(6)
|
||||
default:
|
||||
return this.Count
|
||||
}
|
||||
}
|
||||
|
||||
func (this *SizeCapacity) IsNotEmpty() bool {
|
||||
return this.Count > 0
|
||||
}
|
||||
|
||||
func (this *SizeCapacity) AsJSON() ([]byte, error) {
|
||||
return json.Marshal(this)
|
||||
}
|
||||
|
||||
func (this *SizeCapacity) pow(n int) int64 {
|
||||
if n <= 0 {
|
||||
return 1
|
||||
}
|
||||
if n == 1 {
|
||||
return 1024
|
||||
}
|
||||
return this.pow(n-1) * 1024
|
||||
}
|
||||
23
EdgeCommon/pkg/serverconfigs/shared/size_capacity_test.go
Normal file
23
EdgeCommon/pkg/serverconfigs/shared/size_capacity_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package shared
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSizeCapacity_Bytes(t *testing.T) {
|
||||
for _, unit := range []string{
|
||||
SizeCapacityUnitByte,
|
||||
SizeCapacityUnitKB,
|
||||
SizeCapacityUnitMB,
|
||||
SizeCapacityUnitGB,
|
||||
SizeCapacityUnitTB,
|
||||
SizeCapacityUnitPB,
|
||||
SizeCapacityUnitEB,
|
||||
} {
|
||||
var capacity = &SizeCapacity{
|
||||
Count: 1,
|
||||
Unit: unit,
|
||||
}
|
||||
t.Log(unit, capacity.Bytes())
|
||||
}
|
||||
}
|
||||
86
EdgeCommon/pkg/serverconfigs/shared/time_duration.go
Normal file
86
EdgeCommon/pkg/serverconfigs/shared/time_duration.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TimeDurationUnit = string
|
||||
|
||||
const (
|
||||
TimeDurationUnitMS TimeDurationUnit = "ms"
|
||||
TimeDurationUnitSecond TimeDurationUnit = "second"
|
||||
TimeDurationUnitMinute TimeDurationUnit = "minute"
|
||||
TimeDurationUnitHour TimeDurationUnit = "hour"
|
||||
TimeDurationUnitDay TimeDurationUnit = "day"
|
||||
TimeDurationUnitWeek TimeDurationUnit = "week"
|
||||
)
|
||||
|
||||
// TimeDuration 时间间隔
|
||||
type TimeDuration struct {
|
||||
Count int64 `yaml:"count" json:"count"` // 数量
|
||||
Unit TimeDurationUnit `yaml:"unit" json:"unit"` // 单位
|
||||
}
|
||||
|
||||
func (this *TimeDuration) Duration() time.Duration {
|
||||
switch this.Unit {
|
||||
case TimeDurationUnitMS:
|
||||
return time.Duration(this.Count) * time.Millisecond
|
||||
case TimeDurationUnitSecond:
|
||||
return time.Duration(this.Count) * time.Second
|
||||
case TimeDurationUnitMinute:
|
||||
return time.Duration(this.Count) * time.Minute
|
||||
case TimeDurationUnitHour:
|
||||
return time.Duration(this.Count) * time.Hour
|
||||
case TimeDurationUnitDay:
|
||||
return time.Duration(this.Count) * 24 * time.Hour
|
||||
case TimeDurationUnitWeek:
|
||||
return time.Duration(this.Count) * 24 * 7 * time.Hour
|
||||
default:
|
||||
return time.Duration(this.Count) * time.Second
|
||||
}
|
||||
}
|
||||
|
||||
func (this *TimeDuration) Seconds() int64 {
|
||||
switch this.Unit {
|
||||
case TimeDurationUnitMS:
|
||||
return this.Count / 1000
|
||||
case TimeDurationUnitSecond:
|
||||
return this.Count
|
||||
case TimeDurationUnitMinute:
|
||||
return this.Count * 60
|
||||
case TimeDurationUnitHour:
|
||||
return this.Count * 3600
|
||||
case TimeDurationUnitDay:
|
||||
return this.Count * 3600 * 24
|
||||
case TimeDurationUnitWeek:
|
||||
return this.Count * 3600 * 24 * 7
|
||||
default:
|
||||
return this.Count
|
||||
}
|
||||
}
|
||||
|
||||
func (this *TimeDuration) Description() string {
|
||||
var countString = types.String(this.Count)
|
||||
switch this.Unit {
|
||||
case TimeDurationUnitMS:
|
||||
return countString + "毫秒"
|
||||
case TimeDurationUnitSecond:
|
||||
return countString + "秒"
|
||||
case TimeDurationUnitMinute:
|
||||
return countString + "分钟"
|
||||
case TimeDurationUnitHour:
|
||||
return countString + "小时"
|
||||
case TimeDurationUnitDay:
|
||||
return countString + "天"
|
||||
case TimeDurationUnitWeek:
|
||||
return countString + "周"
|
||||
default:
|
||||
return countString + "秒"
|
||||
}
|
||||
}
|
||||
|
||||
func (this *TimeDuration) AsJSON() ([]byte, error) {
|
||||
return json.Marshal(this)
|
||||
}
|
||||
109
EdgeCommon/pkg/serverconfigs/shared/url_pattern.go
Normal file
109
EdgeCommon/pkg/serverconfigs/shared/url_pattern.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package shared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type URLPatternType = string
|
||||
|
||||
const (
|
||||
URLPatternTypeWildcard URLPatternType = "wildcard" // 通配符
|
||||
URLPatternTypeRegexp URLPatternType = "regexp" // 正则表达式
|
||||
URLPatternTypeImages URLPatternType = "images" // 常见图片
|
||||
URLPatternTypeAudios URLPatternType = "audios" // 常见音频
|
||||
URLPatternTypeVideos URLPatternType = "videos" // 常见视频
|
||||
)
|
||||
|
||||
var commonImageExtensions = []string{".apng", ".avif", ".gif", ".jpg", ".jpeg", ".jfif", ".pjpeg", ".pjp", ".png", ".svg", ".webp", ".bmp", ".ico", ".cur", ".tif", ".tiff"}
|
||||
var commonAudioExtensions = []string{".mp3", ".flac", ".wav", ".aac", ".ogg", ".m4a", ".wma", ".m3u8"} // m3u8 is special
|
||||
var commonVideoExtensions = []string{".mp4", ".avi", ".mkv", ".mov", ".wmv", ".mpeg", ".3gp", ".webm", ".ts", ".m3u8"}
|
||||
|
||||
type URLPattern struct {
|
||||
Type URLPatternType `yaml:"type" json:"type"`
|
||||
Pattern string `yaml:"pattern" json:"pattern"`
|
||||
|
||||
reg *regexp.Regexp
|
||||
}
|
||||
|
||||
func (this *URLPattern) Init() error {
|
||||
switch this.Type {
|
||||
case URLPatternTypeWildcard:
|
||||
if len(this.Pattern) > 0 {
|
||||
// 只支持星号
|
||||
var pieces = strings.Split(this.Pattern, "*")
|
||||
for index, piece := range pieces {
|
||||
pieces[index] = regexp.QuoteMeta(piece)
|
||||
}
|
||||
var pattern = strings.Join(pieces, "(.*)")
|
||||
if len(pattern) > 0 && pattern[0] == '/' {
|
||||
pattern = "(http|https)://[\\w.-]+" + pattern
|
||||
}
|
||||
reg, err := regexp.Compile("(?i)" /** 大小写不敏感 **/ + "^" + pattern + "$")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.reg = reg
|
||||
}
|
||||
case URLPatternTypeRegexp:
|
||||
if len(this.Pattern) > 0 {
|
||||
var pattern = this.Pattern
|
||||
if !strings.HasPrefix(pattern, "(?i)") { // 大小写不敏感
|
||||
pattern = "(?i)" + pattern
|
||||
}
|
||||
reg, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compile '%s' failed: %w", pattern, err)
|
||||
}
|
||||
this.reg = reg
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *URLPattern) Match(url string) bool {
|
||||
if len(this.Pattern) == 0 && len(url) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
switch this.Type {
|
||||
case URLPatternTypeImages:
|
||||
var urlExt = strings.ToLower(filepath.Ext(url))
|
||||
if len(urlExt) > 0 {
|
||||
for _, ext := range commonImageExtensions {
|
||||
if ext == urlExt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
case URLPatternTypeAudios:
|
||||
var urlExt = strings.ToLower(filepath.Ext(url))
|
||||
if len(urlExt) > 0 {
|
||||
for _, ext := range commonAudioExtensions {
|
||||
if ext == urlExt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
case URLPatternTypeVideos:
|
||||
var urlExt = strings.ToLower(filepath.Ext(url))
|
||||
if len(urlExt) > 0 {
|
||||
for _, ext := range commonVideoExtensions {
|
||||
if ext == urlExt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
if this.reg != nil {
|
||||
return this.reg.MatchString(url)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
175
EdgeCommon/pkg/serverconfigs/shared/url_pattern_test.go
Normal file
175
EdgeCommon/pkg/serverconfigs/shared/url_pattern_test.go
Normal file
@@ -0,0 +1,175 @@
|
||||
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package shared_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/shared"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestURLPattern_Match(t *testing.T) {
|
||||
type unitTest struct {
|
||||
patternType string
|
||||
pattern string
|
||||
url string
|
||||
result bool
|
||||
}
|
||||
|
||||
for _, ut := range []*unitTest{
|
||||
{
|
||||
patternType: "wildcard",
|
||||
pattern: "*",
|
||||
url: "https://example.com",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
patternType: "wildcard",
|
||||
pattern: "https://example*",
|
||||
url: "https://example.com",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
patternType: "wildcard",
|
||||
pattern: "*com",
|
||||
url: "https://example.com",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
patternType: "wildcard",
|
||||
pattern: "*COM",
|
||||
url: "https://example.com",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
patternType: "wildcard",
|
||||
pattern: "*COM",
|
||||
url: "https://example.com/hello",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
patternType: "wildcard",
|
||||
pattern: "http://*",
|
||||
url: "https://example.com",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
patternType: "wildcard",
|
||||
pattern: "https://example.com",
|
||||
url: "https://example.com",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
patternType: "wildcard",
|
||||
pattern: "/hello/world",
|
||||
url: "https://example-test.com/hello/world",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
patternType: "wildcard",
|
||||
pattern: "/hello/world",
|
||||
url: "https://example-test.com/123/hello/world",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
patternType: "wildcard",
|
||||
pattern: "/hidden/*",
|
||||
url: "/hidden/index.html",
|
||||
result: false, // because don't have https://HOST in url
|
||||
},
|
||||
{
|
||||
patternType: "wildcard",
|
||||
pattern: "*.jpg",
|
||||
url: "https://example.com/index.jpg",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
patternType: "wildcard",
|
||||
pattern: "*.jpg",
|
||||
url: "https://example.com/index.js",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
patternType: "regexp",
|
||||
pattern: ".*",
|
||||
url: "https://example.com",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
patternType: "regexp",
|
||||
pattern: "^https://.*",
|
||||
url: "https://example.com",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
patternType: "regexp",
|
||||
pattern: "^https://.*EXAMPLE.COM",
|
||||
url: "https://example.com",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
patternType: "regexp",
|
||||
pattern: "(?i)https://.*EXAMPLE.COM/\\d+",
|
||||
url: "https://example.com/123456",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
patternType: "regexp",
|
||||
pattern: "(?i)https://.*EXAMPLE.COM/\\d+$",
|
||||
url: "https://example.com/123456/789",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
patternType: "images",
|
||||
url: "https://example.com/images/logo.png",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
patternType: "images",
|
||||
url: "https://example.com/images/logo.webp",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
patternType: "images",
|
||||
url: "https://example.com/images/logo.mp3",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
patternType: "audios",
|
||||
url: "https://example.com/audios/music.mp3",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
patternType: "audios",
|
||||
url: "https://example.com/audios/music.mm",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
patternType: "videos",
|
||||
url: "https://example.com/images/movie.mp4",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
patternType: "videos",
|
||||
url: "https://example.com/images/movie.ts",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
patternType: "videos",
|
||||
url: "https://example.com/images/movie.mp5",
|
||||
result: false,
|
||||
},
|
||||
} {
|
||||
var p = &shared.URLPattern{
|
||||
Type: ut.patternType,
|
||||
Pattern: ut.pattern,
|
||||
}
|
||||
err := p.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var b = p.Match(ut.url) == ut.result
|
||||
if !b {
|
||||
t.Fatal("not matched pattern:", ut.pattern, "url:", ut.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user