This commit is contained in:
unknown
2026-02-04 20:27:13 +08:00
commit 3b042d1dad
9410 changed files with 1488147 additions and 0 deletions

View 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
}

View File

@@ -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())
}
}

View 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,
},
}
}

View 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
}

View 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)))
}

View 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"`
}

View File

@@ -0,0 +1,5 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package shared
type Formatter = func(s string) string

View File

@@ -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
}

View File

@@ -0,0 +1,5 @@
package shared
// HTTP Header中Expire设置
type HTTPExpireHeaderConfig struct {
}

View 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)
}

View File

@@ -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))
}

View 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
}

View File

@@ -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
}

View File

@@ -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"))
}
}

View File

@@ -0,0 +1,7 @@
package shared
// Header引用
type HTTPHeaderRef struct {
IsOn bool `yaml:"isOn" json:"isOn"`
HeaderId int64 `yaml:"headerId" json:"headerId"`
}

View 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))
}

View File

@@ -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
}

View File

@@ -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
}))
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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
}

View 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
}

View 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
}

View 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")
}
}

View 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()
}

View 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)
}

View 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"))
}
}

View 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.-]+}`) // 命名变量
)

View 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}"))
}

View 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)
}
}

View 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": "判断参数值解析后的文件是否不存在",
},**/
}
}

View 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"},
}
}

View 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
}

View 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())
}
}

View 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)
}

View 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
}

View 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)
}
}
}