实现WAF
This commit is contained in:
48
internal/waf/README.md
Normal file
48
internal/waf/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# WAF
|
||||
A basic WAF for TeaWeb.
|
||||
|
||||
## Config Constructions
|
||||
~~~
|
||||
WAF
|
||||
Inbound
|
||||
Rule Groups
|
||||
Rule Sets
|
||||
Rules
|
||||
Checkpoint Param <Operator> Value
|
||||
Outbound
|
||||
Rule Groups
|
||||
...
|
||||
~~~
|
||||
|
||||
## Apply WAF
|
||||
~~~
|
||||
Request --> WAF --> Backends
|
||||
/
|
||||
Response <-- WAF <----
|
||||
~~~
|
||||
|
||||
## Coding
|
||||
~~~go
|
||||
waf := teawaf.NewWAF()
|
||||
|
||||
// add rule groups here
|
||||
|
||||
err := waf.Init()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
waf.Start()
|
||||
|
||||
// match http request
|
||||
// (req *http.Request, responseWriter http.ResponseWriter)
|
||||
goNext, ruleSet, _ := waf.MatchRequest(req, responseWriter)
|
||||
if ruleSet != nil {
|
||||
log.Println("meet rule set:", ruleSet.Name, "action:", ruleSet.Action)
|
||||
}
|
||||
if !goNext {
|
||||
return
|
||||
}
|
||||
|
||||
// stop the waf
|
||||
// waf.Stop()
|
||||
~~~
|
||||
14
internal/waf/action_allow.go
Normal file
14
internal/waf/action_allow.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type AllowAction struct {
|
||||
}
|
||||
|
||||
func (this *AllowAction) Perform(waf *WAF, request *requests.Request, writer http.ResponseWriter) (allow bool) {
|
||||
// do nothing
|
||||
return true
|
||||
}
|
||||
94
internal/waf/action_block.go
Normal file
94
internal/waf/action_block.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
// url client configure
|
||||
var urlPrefixReg = regexp.MustCompile("^(?i)(http|https)://")
|
||||
var httpClient = utils.SharedHttpClient(5 * time.Second)
|
||||
|
||||
type BlockAction struct {
|
||||
StatusCode int `yaml:"statusCode" json:"statusCode"`
|
||||
Body string `yaml:"body" json:"body"` // supports HTML
|
||||
URL string `yaml:"url" json:"url"`
|
||||
}
|
||||
|
||||
func (this *BlockAction) Perform(waf *WAF, request *requests.Request, writer http.ResponseWriter) (allow bool) {
|
||||
if writer != nil {
|
||||
// if status code eq 444, we close the connection
|
||||
if this.StatusCode == 444 {
|
||||
hijack, ok := writer.(http.Hijacker)
|
||||
if ok {
|
||||
conn, _, _ := hijack.Hijack()
|
||||
if conn != nil {
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// output response
|
||||
if this.StatusCode > 0 {
|
||||
writer.WriteHeader(this.StatusCode)
|
||||
} else {
|
||||
writer.WriteHeader(http.StatusForbidden)
|
||||
}
|
||||
if len(this.URL) > 0 {
|
||||
if urlPrefixReg.MatchString(this.URL) {
|
||||
req, err := http.NewRequest(http.MethodGet, this.URL, nil)
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
return false
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
return false
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
for k, v := range resp.Header {
|
||||
for _, v1 := range v {
|
||||
writer.Header().Add(k, v1)
|
||||
}
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
_, _ = io.CopyBuffer(writer, resp.Body, buf)
|
||||
} else {
|
||||
path := this.URL
|
||||
if !filepath.IsAbs(this.URL) {
|
||||
path = Tea.Root + string(os.PathSeparator) + path
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
return false
|
||||
}
|
||||
_, _ = writer.Write(data)
|
||||
}
|
||||
return false
|
||||
}
|
||||
if len(this.Body) > 0 {
|
||||
_, _ = writer.Write([]byte(this.Body))
|
||||
} else {
|
||||
_, _ = writer.Write([]byte("The request is blocked by " + teaconst.ProductName))
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
39
internal/waf/action_captcha.go
Normal file
39
internal/waf/action_captcha.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
stringutil "github.com/iwind/TeaGo/utils/string"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
var captchaSalt = stringutil.Rand(32)
|
||||
|
||||
const (
|
||||
CaptchaSeconds = 600 // 10 minutes
|
||||
)
|
||||
|
||||
type CaptchaAction struct {
|
||||
}
|
||||
|
||||
func (this *CaptchaAction) Perform(waf *WAF, request *requests.Request, writer http.ResponseWriter) (allow bool) {
|
||||
// TEAWEB_CAPTCHA:
|
||||
cookie, err := request.Cookie("TEAWEB_WAF_CAPTCHA")
|
||||
if err == nil && cookie != nil && len(cookie.Value) > 32 {
|
||||
m := cookie.Value[:32]
|
||||
timestamp := cookie.Value[32:]
|
||||
if stringutil.Md5(captchaSalt+timestamp) == m && time.Now().Unix() < types.Int64(timestamp) { // verify md5
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
refURL := request.URL.String()
|
||||
if len(request.Referer()) > 0 {
|
||||
refURL = request.Referer()
|
||||
}
|
||||
http.Redirect(writer, request.Raw(), "/WAFCAPTCHA?url="+url.QueryEscape(refURL), http.StatusTemporaryRedirect)
|
||||
|
||||
return false
|
||||
}
|
||||
12
internal/waf/action_definition.go
Normal file
12
internal/waf/action_definition.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package waf
|
||||
|
||||
import "reflect"
|
||||
|
||||
// action definition
|
||||
type ActionDefinition struct {
|
||||
Name string
|
||||
Code ActionString
|
||||
Description string
|
||||
Instance ActionInterface
|
||||
Type reflect.Type
|
||||
}
|
||||
34
internal/waf/action_go_group.go
Normal file
34
internal/waf/action_go_group.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type GoGroupAction struct {
|
||||
GroupId string `yaml:"groupId" json:"groupId"`
|
||||
}
|
||||
|
||||
func (this *GoGroupAction) Perform(waf *WAF, request *requests.Request, writer http.ResponseWriter) (allow bool) {
|
||||
group := waf.FindRuleGroup(this.GroupId)
|
||||
if group == nil || !group.IsOn {
|
||||
return true
|
||||
}
|
||||
|
||||
b, set, err := group.MatchRequest(request)
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
return true
|
||||
}
|
||||
|
||||
if !b {
|
||||
return true
|
||||
}
|
||||
|
||||
actionObject := FindActionInstance(set.Action, set.ActionOptions)
|
||||
if actionObject == nil {
|
||||
return true
|
||||
}
|
||||
return actionObject.Perform(waf, request, writer)
|
||||
}
|
||||
37
internal/waf/action_go_set.go
Normal file
37
internal/waf/action_go_set.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type GoSetAction struct {
|
||||
GroupId string `yaml:"groupId" json:"groupId"`
|
||||
SetId string `yaml:"setId" json:"setId"`
|
||||
}
|
||||
|
||||
func (this *GoSetAction) Perform(waf *WAF, request *requests.Request, writer http.ResponseWriter) (allow bool) {
|
||||
group := waf.FindRuleGroup(this.GroupId)
|
||||
if group == nil || !group.IsOn {
|
||||
return true
|
||||
}
|
||||
set := group.FindRuleSet(this.SetId)
|
||||
if set == nil || !set.IsOn {
|
||||
return true
|
||||
}
|
||||
|
||||
b, err := set.MatchRequest(request)
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
return true
|
||||
}
|
||||
if !b {
|
||||
return true
|
||||
}
|
||||
actionObject := FindActionInstance(set.Action, set.ActionOptions)
|
||||
if actionObject == nil {
|
||||
return true
|
||||
}
|
||||
return actionObject.Perform(waf, request, writer)
|
||||
}
|
||||
5
internal/waf/action_instance.go
Normal file
5
internal/waf/action_instance.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package waf
|
||||
|
||||
type Action struct {
|
||||
|
||||
}
|
||||
13
internal/waf/action_log.go
Normal file
13
internal/waf/action_log.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type LogAction struct {
|
||||
}
|
||||
|
||||
func (this *LogAction) Perform(waf *WAF, request *requests.Request, writer http.ResponseWriter) (allow bool) {
|
||||
return true
|
||||
}
|
||||
21
internal/waf/action_type.go
Normal file
21
internal/waf/action_type.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ActionString = string
|
||||
|
||||
const (
|
||||
ActionLog = "log" // allow and log
|
||||
ActionBlock = "block" // block
|
||||
ActionCaptcha = "captcha" // block and show captcha
|
||||
ActionAllow = "allow" // allow
|
||||
ActionGoGroup = "go_group" // go to next rule group
|
||||
ActionGoSet = "go_set" // go to next rule set
|
||||
)
|
||||
|
||||
type ActionInterface interface {
|
||||
Perform(waf *WAF, request *requests.Request, writer http.ResponseWriter) (allow bool)
|
||||
}
|
||||
82
internal/waf/action_utils.go
Normal file
82
internal/waf/action_utils.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
var AllActions = []*ActionDefinition{
|
||||
{
|
||||
Name: "阻止",
|
||||
Code: ActionBlock,
|
||||
Instance: new(BlockAction),
|
||||
},
|
||||
{
|
||||
Name: "允许通过",
|
||||
Code: ActionAllow,
|
||||
Instance: new(AllowAction),
|
||||
},
|
||||
{
|
||||
Name: "允许并记录日志",
|
||||
Code: ActionLog,
|
||||
Instance: new(LogAction),
|
||||
},
|
||||
{
|
||||
Name: "Captcha验证码",
|
||||
Code: ActionCaptcha,
|
||||
Instance: new(CaptchaAction),
|
||||
},
|
||||
{
|
||||
Name: "跳到下一个规则分组",
|
||||
Code: ActionGoGroup,
|
||||
Instance: new(GoGroupAction),
|
||||
Type: reflect.TypeOf(new(GoGroupAction)).Elem(),
|
||||
},
|
||||
{
|
||||
Name: "跳到下一个规则集",
|
||||
Code: ActionGoSet,
|
||||
Instance: new(GoSetAction),
|
||||
Type: reflect.TypeOf(new(GoSetAction)).Elem(),
|
||||
},
|
||||
}
|
||||
|
||||
func FindActionInstance(action ActionString, options maps.Map) ActionInterface {
|
||||
for _, def := range AllActions {
|
||||
if def.Code == action {
|
||||
if def.Type != nil {
|
||||
// create new instance
|
||||
ptrValue := reflect.New(def.Type)
|
||||
instance := ptrValue.Interface().(ActionInterface)
|
||||
|
||||
if len(options) > 0 {
|
||||
count := def.Type.NumField()
|
||||
for i := 0; i < count; i++ {
|
||||
field := def.Type.Field(i)
|
||||
tag, ok := field.Tag.Lookup("yaml")
|
||||
if ok {
|
||||
v, ok := options[tag]
|
||||
if ok && reflect.TypeOf(v) == field.Type {
|
||||
ptrValue.Elem().FieldByName(field.Name).Set(reflect.ValueOf(v))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
// return shared instance
|
||||
return def.Instance
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindActionName(action ActionString) string {
|
||||
for _, def := range AllActions {
|
||||
if def.Code == action {
|
||||
return def.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
29
internal/waf/action_utils_test.go
Normal file
29
internal/waf/action_utils_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFindActionInstance(t *testing.T) {
|
||||
a := assert.NewAssertion(t)
|
||||
|
||||
t.Logf("ActionBlock: %p", FindActionInstance(ActionBlock, nil))
|
||||
t.Logf("ActionBlock: %p", FindActionInstance(ActionBlock, nil))
|
||||
t.Logf("ActionGoGroup: %p", FindActionInstance(ActionGoGroup, nil))
|
||||
t.Logf("ActionGoGroup: %p", FindActionInstance(ActionGoGroup, nil))
|
||||
t.Logf("ActionGoSet: %p", FindActionInstance(ActionGoSet, nil))
|
||||
t.Logf("ActionGoSet: %p", FindActionInstance(ActionGoSet, nil))
|
||||
t.Logf("ActionGoSet: %#v", FindActionInstance(ActionGoSet, maps.Map{"groupId": "a", "setId": "b",}))
|
||||
|
||||
a.IsTrue(FindActionInstance(ActionGoSet, nil) != FindActionInstance(ActionGoSet, nil))
|
||||
}
|
||||
|
||||
func BenchmarkFindActionInstance(b *testing.B) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
for i := 0; i < b.N; i++ {
|
||||
FindActionInstance(ActionGoSet, nil)
|
||||
}
|
||||
}
|
||||
84
internal/waf/captcha_validator.go
Normal file
84
internal/waf/captcha_validator.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"github.com/dchest/captcha"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
stringutil "github.com/iwind/TeaGo/utils/string"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var captchaValidator = &CaptchaValidator{}
|
||||
|
||||
type CaptchaValidator struct {
|
||||
}
|
||||
|
||||
func (this *CaptchaValidator) Run(request *requests.Request, writer http.ResponseWriter) {
|
||||
if request.Method == http.MethodPost && len(request.FormValue("TEAWEB_WAF_CAPTCHA_ID")) > 0 {
|
||||
this.validate(request, writer)
|
||||
} else {
|
||||
this.show(request, writer)
|
||||
}
|
||||
}
|
||||
|
||||
func (this *CaptchaValidator) show(request *requests.Request, writer http.ResponseWriter) {
|
||||
// show captcha
|
||||
captchaId := captcha.NewLen(6)
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
err := captcha.WriteImage(buf, captchaId, 200, 100)
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = writer.Write([]byte(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Verify Yourself</title>
|
||||
</head>
|
||||
<body>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="TEAWEB_WAF_CAPTCHA_ID" value="` + captchaId + `"/>
|
||||
<img src="data:image/png;base64, ` + base64.StdEncoding.EncodeToString(buf.Bytes()) + `"/>` + `
|
||||
<div>
|
||||
<p>Input verify code above:</p>
|
||||
<input type="text" name="TEAWEB_WAF_CAPTCHA_CODE" maxlength="6" size="18" autocomplete="off" z-index="1" style="font-size:16px;line-height:24px; letter-spacing: 15px; padding-left: 4px"/>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" onclick="window.location = '/webhook'" style="line-height:24px;margin-top:10px">Verify Yourself</button>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>`))
|
||||
}
|
||||
|
||||
func (this *CaptchaValidator) validate(request *requests.Request, writer http.ResponseWriter) (allow bool) {
|
||||
captchaId := request.FormValue("TEAWEB_WAF_CAPTCHA_ID")
|
||||
if len(captchaId) > 0 {
|
||||
captchaCode := request.FormValue("TEAWEB_WAF_CAPTCHA_CODE")
|
||||
if captcha.VerifyString(captchaId, captchaCode) {
|
||||
// set cookie
|
||||
timestamp := fmt.Sprintf("%d", time.Now().Unix()+CaptchaSeconds)
|
||||
m := stringutil.Md5(captchaSalt + timestamp)
|
||||
http.SetCookie(writer, &http.Cookie{
|
||||
Name: "TEAWEB_WAF_CAPTCHA",
|
||||
Value: m + timestamp,
|
||||
MaxAge: CaptchaSeconds,
|
||||
Path: "/", // all of dirs
|
||||
})
|
||||
|
||||
rawURL := request.URL.Query().Get("url")
|
||||
http.Redirect(writer, request.Raw(), rawURL, http.StatusSeeOther)
|
||||
|
||||
return false
|
||||
} else {
|
||||
http.Redirect(writer, request.Raw(), request.URL.String(), http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
246
internal/waf/checkpoints/cc.go
Normal file
246
internal/waf/checkpoints/cc.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/grids"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ${cc.arg}
|
||||
// TODO implement more traffic rules
|
||||
type CCCheckpoint struct {
|
||||
Checkpoint
|
||||
|
||||
grid *grids.Grid
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (this *CCCheckpoint) Init() {
|
||||
|
||||
}
|
||||
|
||||
func (this *CCCheckpoint) Start() {
|
||||
if this.grid != nil {
|
||||
this.grid.Destroy()
|
||||
}
|
||||
this.grid = grids.NewGrid(32, grids.NewLimitCountOpt(1000_0000))
|
||||
}
|
||||
|
||||
func (this *CCCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
value = 0
|
||||
|
||||
if this.grid == nil {
|
||||
this.once.Do(func() {
|
||||
this.Start()
|
||||
})
|
||||
if this.grid == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
periodString, ok := options["period"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
period := types.Int64(periodString)
|
||||
if period < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
v, _ := options["userType"]
|
||||
userType := types.String(v)
|
||||
|
||||
v, _ = options["userField"]
|
||||
userField := types.String(v)
|
||||
|
||||
v, _ = options["userIndex"]
|
||||
userIndex := types.Int(v)
|
||||
|
||||
if param == "requests" { // requests
|
||||
var key = ""
|
||||
switch userType {
|
||||
case "ip":
|
||||
key = this.ip(req)
|
||||
case "cookie":
|
||||
if len(userField) == 0 {
|
||||
key = this.ip(req)
|
||||
} else {
|
||||
cookie, _ := req.Cookie(userField)
|
||||
if cookie != nil {
|
||||
v := cookie.Value
|
||||
if userIndex > 0 && len(v) > userIndex {
|
||||
v = v[userIndex:]
|
||||
}
|
||||
key = "USER@" + userType + "@" + userField + "@" + v
|
||||
}
|
||||
}
|
||||
case "get":
|
||||
if len(userField) == 0 {
|
||||
key = this.ip(req)
|
||||
} else {
|
||||
v := req.URL.Query().Get(userField)
|
||||
if userIndex > 0 && len(v) > userIndex {
|
||||
v = v[userIndex:]
|
||||
}
|
||||
key = "USER@" + userType + "@" + userField + "@" + v
|
||||
}
|
||||
case "post":
|
||||
if len(userField) == 0 {
|
||||
key = this.ip(req)
|
||||
} else {
|
||||
v := req.PostFormValue(userField)
|
||||
if userIndex > 0 && len(v) > userIndex {
|
||||
v = v[userIndex:]
|
||||
}
|
||||
key = "USER@" + userType + "@" + userField + "@" + v
|
||||
}
|
||||
case "header":
|
||||
if len(userField) == 0 {
|
||||
key = this.ip(req)
|
||||
} else {
|
||||
v := req.Header.Get(userField)
|
||||
if userIndex > 0 && len(v) > userIndex {
|
||||
v = v[userIndex:]
|
||||
}
|
||||
key = "USER@" + userType + "@" + userField + "@" + v
|
||||
}
|
||||
default:
|
||||
key = this.ip(req)
|
||||
}
|
||||
if len(key) == 0 {
|
||||
key = this.ip(req)
|
||||
}
|
||||
value = this.grid.IncreaseInt64([]byte(key), 1, period)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (this *CCCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (this *CCCheckpoint) ParamOptions() *ParamOptions {
|
||||
option := NewParamOptions()
|
||||
option.AddParam("请求数", "requests")
|
||||
return option
|
||||
}
|
||||
|
||||
func (this *CCCheckpoint) Options() []OptionInterface {
|
||||
options := []OptionInterface{}
|
||||
|
||||
// period
|
||||
{
|
||||
option := NewFieldOption("统计周期", "period")
|
||||
option.Value = "60"
|
||||
option.RightLabel = "秒"
|
||||
option.Size = 8
|
||||
option.MaxLength = 8
|
||||
option.Validate = func(value string) (ok bool, message string) {
|
||||
if regexp.MustCompile("^\\d+$").MatchString(value) {
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
message = "周期需要是一个整数数字"
|
||||
return
|
||||
}
|
||||
options = append(options, option)
|
||||
}
|
||||
|
||||
// type
|
||||
{
|
||||
option := NewOptionsOption("用户识别读取来源", "userType")
|
||||
option.Size = 10
|
||||
option.SetOptions([]maps.Map{
|
||||
{
|
||||
"name": "IP",
|
||||
"value": "ip",
|
||||
},
|
||||
{
|
||||
"name": "Cookie",
|
||||
"value": "cookie",
|
||||
},
|
||||
{
|
||||
"name": "URL参数",
|
||||
"value": "get",
|
||||
},
|
||||
{
|
||||
"name": "POST参数",
|
||||
"value": "post",
|
||||
},
|
||||
{
|
||||
"name": "HTTP Header",
|
||||
"value": "header",
|
||||
},
|
||||
})
|
||||
options = append(options, option)
|
||||
}
|
||||
|
||||
// user field
|
||||
{
|
||||
option := NewFieldOption("用户识别字段", "userField")
|
||||
option.Comment = "识别用户的唯一性字段,在用户读取来源不是IP时使用"
|
||||
options = append(options, option)
|
||||
}
|
||||
|
||||
// user value index
|
||||
{
|
||||
option := NewFieldOption("字段读取位置", "userIndex")
|
||||
option.Size = 5
|
||||
option.MaxLength = 5
|
||||
option.Comment = "读取用户识别字段的位置,从0开始,比如user12345的数字ID 12345的位置就是5,在用户读取来源不是IP时使用"
|
||||
options = append(options, option)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func (this *CCCheckpoint) Stop() {
|
||||
if this.grid != nil {
|
||||
this.grid.Destroy()
|
||||
this.grid = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (this *CCCheckpoint) ip(req *requests.Request) string {
|
||||
// X-Forwarded-For
|
||||
forwardedFor := req.Header.Get("X-Forwarded-For")
|
||||
if len(forwardedFor) > 0 {
|
||||
commaIndex := strings.Index(forwardedFor, ",")
|
||||
if commaIndex > 0 {
|
||||
return forwardedFor[:commaIndex]
|
||||
}
|
||||
return forwardedFor
|
||||
}
|
||||
|
||||
// Real-IP
|
||||
{
|
||||
realIP, ok := req.Header["X-Real-IP"]
|
||||
if ok && len(realIP) > 0 {
|
||||
return realIP[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Real-Ip
|
||||
{
|
||||
realIP, ok := req.Header["X-Real-Ip"]
|
||||
if ok && len(realIP) > 0 {
|
||||
return realIP[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Remote-Addr
|
||||
host, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err == nil {
|
||||
return host
|
||||
}
|
||||
return req.RemoteAddr
|
||||
}
|
||||
42
internal/waf/checkpoints/cc_test.go
Normal file
42
internal/waf/checkpoints/cc_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCCCheckpoint_RequestValue(t *testing.T) {
|
||||
raw, err := http.NewRequest(http.MethodGet, "http://teaos.cn/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := requests.NewRequest(raw)
|
||||
req.RemoteAddr = "127.0.0.1"
|
||||
|
||||
checkpoint := new(CCCheckpoint)
|
||||
checkpoint.Init()
|
||||
checkpoint.Start()
|
||||
|
||||
options := map[string]string{
|
||||
"period": "5",
|
||||
}
|
||||
t.Log(checkpoint.RequestValue(req, "requests", options))
|
||||
t.Log(checkpoint.RequestValue(req, "requests", options))
|
||||
|
||||
req.RemoteAddr = "127.0.0.2"
|
||||
t.Log(checkpoint.RequestValue(req, "requests", options))
|
||||
|
||||
req.RemoteAddr = "127.0.0.1"
|
||||
t.Log(checkpoint.RequestValue(req, "requests", options))
|
||||
|
||||
req.RemoteAddr = "127.0.0.2"
|
||||
t.Log(checkpoint.RequestValue(req, "requests", options))
|
||||
|
||||
req.RemoteAddr = "127.0.0.2"
|
||||
t.Log(checkpoint.RequestValue(req, "requests", options))
|
||||
|
||||
req.RemoteAddr = "127.0.0.2"
|
||||
t.Log(checkpoint.RequestValue(req, "requests", options))
|
||||
}
|
||||
28
internal/waf/checkpoints/checkpoint.go
Normal file
28
internal/waf/checkpoints/checkpoint.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package checkpoints
|
||||
|
||||
type Checkpoint struct {
|
||||
}
|
||||
|
||||
func (this *Checkpoint) Init() {
|
||||
|
||||
}
|
||||
|
||||
func (this *Checkpoint) IsRequest() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (this *Checkpoint) ParamOptions() *ParamOptions {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *Checkpoint) Options() []OptionInterface {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *Checkpoint) Start() {
|
||||
|
||||
}
|
||||
|
||||
func (this *Checkpoint) Stop() {
|
||||
|
||||
}
|
||||
10
internal/waf/checkpoints/checkpoint_definition.go
Normal file
10
internal/waf/checkpoints/checkpoint_definition.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package checkpoints
|
||||
|
||||
// check point definition
|
||||
type CheckpointDefinition struct {
|
||||
Name string
|
||||
Description string
|
||||
Prefix string
|
||||
HasParams bool // has sub params
|
||||
Instance CheckpointInterface
|
||||
}
|
||||
32
internal/waf/checkpoints/checkpoint_interface.go
Normal file
32
internal/waf/checkpoints/checkpoint_interface.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
// Check Point
|
||||
type CheckpointInterface interface {
|
||||
// initialize
|
||||
Init()
|
||||
|
||||
// is request?
|
||||
IsRequest() bool
|
||||
|
||||
// get request value
|
||||
RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error)
|
||||
|
||||
// get response value
|
||||
ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error)
|
||||
|
||||
// param option list
|
||||
ParamOptions() *ParamOptions
|
||||
|
||||
// options
|
||||
Options() []OptionInterface
|
||||
|
||||
// start
|
||||
Start()
|
||||
|
||||
// stop
|
||||
Stop()
|
||||
}
|
||||
5
internal/waf/checkpoints/option.go
Normal file
5
internal/waf/checkpoints/option.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package checkpoints
|
||||
|
||||
type OptionInterface interface {
|
||||
Type() string
|
||||
}
|
||||
26
internal/waf/checkpoints/option_field.go
Normal file
26
internal/waf/checkpoints/option_field.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package checkpoints
|
||||
|
||||
// attach option
|
||||
type FieldOption struct {
|
||||
Name string
|
||||
Code string
|
||||
Value string // default value
|
||||
IsRequired bool
|
||||
Size int
|
||||
Comment string
|
||||
Placeholder string
|
||||
RightLabel string
|
||||
MaxLength int
|
||||
Validate func(value string) (ok bool, message string)
|
||||
}
|
||||
|
||||
func NewFieldOption(name string, code string) *FieldOption {
|
||||
return &FieldOption{
|
||||
Name: name,
|
||||
Code: code,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *FieldOption) Type() string {
|
||||
return "field"
|
||||
}
|
||||
30
internal/waf/checkpoints/option_options.go
Normal file
30
internal/waf/checkpoints/option_options.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package checkpoints
|
||||
|
||||
import "github.com/iwind/TeaGo/maps"
|
||||
|
||||
type OptionsOption struct {
|
||||
Name string
|
||||
Code string
|
||||
Value string // default value
|
||||
IsRequired bool
|
||||
Size int
|
||||
Comment string
|
||||
RightLabel string
|
||||
Validate func(value string) (ok bool, message string)
|
||||
Options []maps.Map
|
||||
}
|
||||
|
||||
func NewOptionsOption(name string, code string) *OptionsOption {
|
||||
return &OptionsOption{
|
||||
Name: name,
|
||||
Code: code,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *OptionsOption) Type() string {
|
||||
return "options"
|
||||
}
|
||||
|
||||
func (this *OptionsOption) SetOptions(options []maps.Map) {
|
||||
this.Options = options
|
||||
}
|
||||
21
internal/waf/checkpoints/param_option.go
Normal file
21
internal/waf/checkpoints/param_option.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package checkpoints
|
||||
|
||||
type KeyValue struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type ParamOptions struct {
|
||||
Options []*KeyValue `json:"options"`
|
||||
}
|
||||
|
||||
func NewParamOptions() *ParamOptions {
|
||||
return &ParamOptions{}
|
||||
}
|
||||
|
||||
func (this *ParamOptions) AddParam(name string, value string) {
|
||||
this.Options = append(this.Options, &KeyValue{
|
||||
Name: name,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
46
internal/waf/checkpoints/request_all.go
Normal file
46
internal/waf/checkpoints/request_all.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
// ${requestAll}
|
||||
type RequestAllCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestAllCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
valueBytes := []byte{}
|
||||
if len(req.RequestURI) > 0 {
|
||||
valueBytes = append(valueBytes, req.RequestURI...)
|
||||
} else if req.URL != nil {
|
||||
valueBytes = append(valueBytes, req.URL.RequestURI()...)
|
||||
}
|
||||
|
||||
if req.Body != nil {
|
||||
valueBytes = append(valueBytes, ' ')
|
||||
|
||||
if len(req.BodyData) == 0 {
|
||||
data, err := req.ReadBody(int64(32 * 1024 * 1024)) // read 32m bytes
|
||||
if err != nil {
|
||||
return "", err, nil
|
||||
}
|
||||
|
||||
req.BodyData = data
|
||||
req.RestoreBody(data)
|
||||
}
|
||||
valueBytes = append(valueBytes, req.BodyData...)
|
||||
}
|
||||
|
||||
value = valueBytes
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestAllCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
value = ""
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
70
internal/waf/checkpoints/request_all_test.go
Normal file
70
internal/waf/checkpoints/request_all_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRequestAllCheckpoint_RequestValue(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodPost, "http://teaos.cn/hello/world", bytes.NewBuffer([]byte("123456")))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkpoint := new(RequestAllCheckpoint)
|
||||
v, sysErr, userErr := checkpoint.RequestValue(requests.NewRequest(req), "", nil)
|
||||
if sysErr != nil {
|
||||
t.Fatal(sysErr)
|
||||
}
|
||||
if userErr != nil {
|
||||
t.Fatal(userErr)
|
||||
}
|
||||
t.Log(v)
|
||||
t.Log(types.String(v))
|
||||
|
||||
body, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(body))
|
||||
}
|
||||
|
||||
func TestRequestAllCheckpoint_RequestValue_Max(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodPost, "http://teaos.cn", bytes.NewBuffer([]byte(strings.Repeat("123456", 10240000))))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkpoint := new(RequestBodyCheckpoint)
|
||||
value, err, _ := checkpoint.RequestValue(requests.NewRequest(req), "", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("value bytes:", len(types.String(value)))
|
||||
|
||||
body, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("raw bytes:", len(body))
|
||||
}
|
||||
|
||||
func BenchmarkRequestAllCheckpoint_RequestValue(b *testing.B) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "http://teaos.cn/hello/world", bytes.NewBuffer(bytes.Repeat([]byte("HELLO"), 1024)))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
checkpoint := new(RequestAllCheckpoint)
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _, _ = checkpoint.RequestValue(requests.NewRequest(req), "", nil)
|
||||
}
|
||||
}
|
||||
20
internal/waf/checkpoints/request_arg.go
Normal file
20
internal/waf/checkpoints/request_arg.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
type RequestArgCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestArgCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
return req.URL.Query().Get(param), nil, nil
|
||||
}
|
||||
|
||||
func (this *RequestArgCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
21
internal/waf/checkpoints/request_arg_test.go
Normal file
21
internal/waf/checkpoints/request_arg_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestArgParam_RequestValue(t *testing.T) {
|
||||
rawReq, err := http.NewRequest(http.MethodGet, "http://teaos.cn/?name=lu", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := requests.NewRequest(rawReq)
|
||||
|
||||
checkpoint := new(RequestArgCheckpoint)
|
||||
t.Log(checkpoint.RequestValue(req, "name", nil))
|
||||
t.Log(checkpoint.ResponseValue(req, nil, "name", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "name2", nil))
|
||||
}
|
||||
21
internal/waf/checkpoints/request_args.go
Normal file
21
internal/waf/checkpoints/request_args.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
type RequestArgsCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestArgsCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
value = req.URL.RawQuery
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestArgsCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
36
internal/waf/checkpoints/request_body.go
Normal file
36
internal/waf/checkpoints/request_body.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
// ${requestBody}
|
||||
type RequestBodyCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestBodyCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if req.Body == nil {
|
||||
value = ""
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.BodyData) == 0 {
|
||||
data, err := req.ReadBody(int64(32 * 1024 * 1024)) // read 32m bytes
|
||||
if err != nil {
|
||||
return "", err, nil
|
||||
}
|
||||
|
||||
req.BodyData = data
|
||||
req.RestoreBody(data)
|
||||
}
|
||||
|
||||
return req.BodyData, nil, nil
|
||||
}
|
||||
|
||||
func (this *RequestBodyCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
47
internal/waf/checkpoints/request_body_test.go
Normal file
47
internal/waf/checkpoints/request_body_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRequestBodyCheckpoint_RequestValue(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodPost, "http://teaos.cn", bytes.NewBuffer([]byte("123456")))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkpoint := new(RequestBodyCheckpoint)
|
||||
t.Log(checkpoint.RequestValue(requests.NewRequest(req), "", nil))
|
||||
|
||||
body, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(body))
|
||||
}
|
||||
|
||||
func TestRequestBodyCheckpoint_RequestValue_Max(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodPost, "http://teaos.cn", bytes.NewBuffer([]byte(strings.Repeat("123456", 10240000))))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkpoint := new(RequestBodyCheckpoint)
|
||||
value, err, _ := checkpoint.RequestValue(requests.NewRequest(req), "", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("value bytes:", len(types.String(value)))
|
||||
|
||||
body, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("raw bytes:", len(body))
|
||||
}
|
||||
21
internal/waf/checkpoints/request_content_type.go
Normal file
21
internal/waf/checkpoints/request_content_type.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
type RequestContentTypeCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestContentTypeCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
value = req.Header.Get("Content-Type")
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestContentTypeCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
27
internal/waf/checkpoints/request_cookie.go
Normal file
27
internal/waf/checkpoints/request_cookie.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
type RequestCookieCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestCookieCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
cookie, err := req.Cookie(param)
|
||||
if err != nil {
|
||||
value = ""
|
||||
return
|
||||
}
|
||||
|
||||
value = cookie.Value
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestCookieCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
27
internal/waf/checkpoints/request_cookies.go
Normal file
27
internal/waf/checkpoints/request_cookies.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RequestCookiesCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestCookiesCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
var cookies = []string{}
|
||||
for _, cookie := range req.Cookies() {
|
||||
cookies = append(cookies, url.QueryEscape(cookie.Name)+"="+url.QueryEscape(cookie.Value))
|
||||
}
|
||||
value = strings.Join(cookies, "&")
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestCookiesCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
39
internal/waf/checkpoints/request_form_arg.go
Normal file
39
internal/waf/checkpoints/request_form_arg.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// ${requestForm.arg}
|
||||
type RequestFormArgCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestFormArgCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if req.Body == nil {
|
||||
value = ""
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.BodyData) == 0 {
|
||||
data, err := req.ReadBody(32 * 1024 * 1024) // read 32m bytes
|
||||
if err != nil {
|
||||
return "", err, nil
|
||||
}
|
||||
|
||||
req.BodyData = data
|
||||
req.RestoreBody(data)
|
||||
}
|
||||
|
||||
// TODO improve performance
|
||||
values, _ := url.ParseQuery(string(req.BodyData))
|
||||
return values.Get(param), nil, nil
|
||||
}
|
||||
|
||||
func (this *RequestFormArgCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
32
internal/waf/checkpoints/request_form_arg_test.go
Normal file
32
internal/waf/checkpoints/request_form_arg_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRequestFormArgCheckpoint_RequestValue(t *testing.T) {
|
||||
rawReq, err := http.NewRequest(http.MethodPost, "http://teaos.cn", bytes.NewBuffer([]byte("name=lu&age=20&encoded="+url.QueryEscape("<strong>ENCODED STRING</strong>"))))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := requests.NewRequest(rawReq)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
checkpoint := new(RequestFormArgCheckpoint)
|
||||
t.Log(checkpoint.RequestValue(req, "name", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "age", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "Hello", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "encoded", nil))
|
||||
|
||||
body, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(body))
|
||||
}
|
||||
27
internal/waf/checkpoints/request_header.go
Normal file
27
internal/waf/checkpoints/request_header.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RequestHeaderCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestHeaderCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
v, found := req.Header[param]
|
||||
if !found {
|
||||
value = ""
|
||||
return
|
||||
}
|
||||
value = strings.Join(v, ";")
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestHeaderCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
30
internal/waf/checkpoints/request_headers.go
Normal file
30
internal/waf/checkpoints/request_headers.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RequestHeadersCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestHeadersCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
var headers = []string{}
|
||||
for k, v := range req.Header {
|
||||
for _, subV := range v {
|
||||
headers = append(headers, k+": "+subV)
|
||||
}
|
||||
}
|
||||
sort.Strings(headers)
|
||||
value = strings.Join(headers, "\n")
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestHeadersCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
21
internal/waf/checkpoints/request_host.go
Normal file
21
internal/waf/checkpoints/request_host.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
type RequestHostCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestHostCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
value = req.Host
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestHostCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
20
internal/waf/checkpoints/request_host_test.go
Normal file
20
internal/waf/checkpoints/request_host_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRequestHostCheckpoint_RequestValue(t *testing.T) {
|
||||
rawReq, err := http.NewRequest(http.MethodGet, "https://teaos.cn/?name=lu", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := requests.NewRequest(rawReq)
|
||||
req.Header.Set("Host", "cloud.teaos.cn")
|
||||
|
||||
checkpoint := new(RequestHostCheckpoint)
|
||||
t.Log(checkpoint.RequestValue(req, "", nil))
|
||||
}
|
||||
44
internal/waf/checkpoints/request_json_arg.go
Normal file
44
internal/waf/checkpoints/request_json_arg.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ${requestJSON.arg}
|
||||
type RequestJSONArgCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestJSONArgCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if len(req.BodyData) == 0 {
|
||||
data, err := req.ReadBody(int64(32 * 1024 * 1024)) // read 32m bytes
|
||||
if err != nil {
|
||||
return "", err, nil
|
||||
}
|
||||
req.BodyData = data
|
||||
defer req.RestoreBody(data)
|
||||
}
|
||||
|
||||
// TODO improve performance
|
||||
var m interface{} = nil
|
||||
err := json.Unmarshal(req.BodyData, &m)
|
||||
if err != nil || m == nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
value = utils.Get(m, strings.Split(param, "."))
|
||||
if value != nil {
|
||||
return value, nil, err
|
||||
}
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
func (this *RequestJSONArgCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
99
internal/waf/checkpoints/request_json_arg_test.go
Normal file
99
internal/waf/checkpoints/request_json_arg_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRequestJSONArgCheckpoint_RequestValue_Map(t *testing.T) {
|
||||
rawReq, err := http.NewRequest(http.MethodPost, "http://teaos.cn", bytes.NewBuffer([]byte(`
|
||||
{
|
||||
"name": "lu",
|
||||
"age": 20,
|
||||
"books": [ "PHP", "Golang", "Python" ]
|
||||
}
|
||||
`)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := requests.NewRequest(rawReq)
|
||||
//req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
checkpoint := new(RequestJSONArgCheckpoint)
|
||||
t.Log(checkpoint.RequestValue(req, "name", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "age", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "Hello", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "books", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "books.1", nil))
|
||||
|
||||
body, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(body))
|
||||
}
|
||||
|
||||
func TestRequestJSONArgCheckpoint_RequestValue_Array(t *testing.T) {
|
||||
rawReq, err := http.NewRequest(http.MethodPost, "http://teaos.cn", bytes.NewBuffer([]byte(`
|
||||
[{
|
||||
"name": "lu",
|
||||
"age": 20,
|
||||
"books": [ "PHP", "Golang", "Python" ]
|
||||
}]
|
||||
`)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := requests.NewRequest(rawReq)
|
||||
//req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
checkpoint := new(RequestJSONArgCheckpoint)
|
||||
t.Log(checkpoint.RequestValue(req, "0.name", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "0.age", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "0.Hello", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "0.books", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "0.books.1", nil))
|
||||
|
||||
body, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(body))
|
||||
}
|
||||
|
||||
func TestRequestJSONArgCheckpoint_RequestValue_Error(t *testing.T) {
|
||||
rawReq, err := http.NewRequest(http.MethodPost, "http://teaos.cn", bytes.NewBuffer([]byte(`
|
||||
[{
|
||||
"name": "lu",
|
||||
"age": 20,
|
||||
"books": [ "PHP", "Golang", "Python" ]
|
||||
}]
|
||||
`)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := requests.NewRequest(rawReq)
|
||||
//req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
checkpoint := new(RequestJSONArgCheckpoint)
|
||||
t.Log(checkpoint.RequestValue(req, "0.name", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "0.age", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "0.Hello", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "0.books", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "0.books.1", nil))
|
||||
|
||||
body, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(body))
|
||||
}
|
||||
21
internal/waf/checkpoints/request_length.go
Normal file
21
internal/waf/checkpoints/request_length.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
type RequestLengthCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestLengthCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
value = req.ContentLength
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestLengthCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
21
internal/waf/checkpoints/request_method.go
Normal file
21
internal/waf/checkpoints/request_method.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
type RequestMethodCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestMethodCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
value = req.Method
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestMethodCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
20
internal/waf/checkpoints/request_path.go
Normal file
20
internal/waf/checkpoints/request_path.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
type RequestPathCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestPathCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
return req.URL.Path, nil, nil
|
||||
}
|
||||
|
||||
func (this *RequestPathCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
18
internal/waf/checkpoints/request_path_test.go
Normal file
18
internal/waf/checkpoints/request_path_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRequestPathCheckpoint_RequestValue(t *testing.T) {
|
||||
rawReq, err := http.NewRequest(http.MethodGet, "http://teaos.cn/index?name=lu", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := requests.NewRequest(rawReq)
|
||||
checkpoint := new(RequestPathCheckpoint)
|
||||
t.Log(checkpoint.RequestValue(req, "", nil))
|
||||
}
|
||||
21
internal/waf/checkpoints/request_proto.go
Normal file
21
internal/waf/checkpoints/request_proto.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
type RequestProtoCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestProtoCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
value = req.Proto
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestProtoCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
27
internal/waf/checkpoints/request_raw_remote_addr.go
Normal file
27
internal/waf/checkpoints/request_raw_remote_addr.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"net"
|
||||
)
|
||||
|
||||
type RequestRawRemoteAddrCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestRawRemoteAddrCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
host, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err == nil {
|
||||
value = host
|
||||
} else {
|
||||
value = req.RemoteAddr
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestRawRemoteAddrCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
21
internal/waf/checkpoints/request_referer.go
Normal file
21
internal/waf/checkpoints/request_referer.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
type RequestRefererCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestRefererCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
value = req.Referer()
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestRefererCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
59
internal/waf/checkpoints/request_remote_addr.go
Normal file
59
internal/waf/checkpoints/request_remote_addr.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RequestRemoteAddrCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestRemoteAddrCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
// X-Forwarded-For
|
||||
forwardedFor := req.Header.Get("X-Forwarded-For")
|
||||
if len(forwardedFor) > 0 {
|
||||
commaIndex := strings.Index(forwardedFor, ",")
|
||||
if commaIndex > 0 {
|
||||
value = forwardedFor[:commaIndex]
|
||||
return
|
||||
}
|
||||
value = forwardedFor
|
||||
return
|
||||
}
|
||||
|
||||
// Real-IP
|
||||
{
|
||||
realIP, ok := req.Header["X-Real-IP"]
|
||||
if ok && len(realIP) > 0 {
|
||||
value = realIP[0]
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Real-Ip
|
||||
{
|
||||
realIP, ok := req.Header["X-Real-Ip"]
|
||||
if ok && len(realIP) > 0 {
|
||||
value = realIP[0]
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Remote-Addr
|
||||
host, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err == nil {
|
||||
value = host
|
||||
} else {
|
||||
value = req.RemoteAddr
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestRemoteAddrCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
28
internal/waf/checkpoints/request_remote_port.go
Normal file
28
internal/waf/checkpoints/request_remote_port.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"net"
|
||||
)
|
||||
|
||||
type RequestRemotePortCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestRemotePortCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
_, port, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err == nil {
|
||||
value = types.Int(port)
|
||||
} else {
|
||||
value = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestRemotePortCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
26
internal/waf/checkpoints/request_remote_user.go
Normal file
26
internal/waf/checkpoints/request_remote_user.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
type RequestRemoteUserCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestRemoteUserCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
username, _, ok := req.BasicAuth()
|
||||
if !ok {
|
||||
value = ""
|
||||
return
|
||||
}
|
||||
value = username
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestRemoteUserCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
21
internal/waf/checkpoints/request_scheme.go
Normal file
21
internal/waf/checkpoints/request_scheme.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
type RequestSchemeCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestSchemeCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
value = req.URL.Scheme
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestSchemeCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
18
internal/waf/checkpoints/request_scheme_test.go
Normal file
18
internal/waf/checkpoints/request_scheme_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRequestSchemeCheckpoint_RequestValue(t *testing.T) {
|
||||
rawReq, err := http.NewRequest(http.MethodGet, "https://teaos.cn/?name=lu", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := requests.NewRequest(rawReq)
|
||||
checkpoint := new(RequestSchemeCheckpoint)
|
||||
t.Log(checkpoint.RequestValue(req, "", nil))
|
||||
}
|
||||
130
internal/waf/checkpoints/request_upload.go
Normal file
130
internal/waf/checkpoints/request_upload.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"github.com/iwind/TeaGo/lists"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ${requestUpload.arg}
|
||||
type RequestUploadCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestUploadCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
value = ""
|
||||
if param == "minSize" || param == "maxSize" {
|
||||
value = 0
|
||||
}
|
||||
|
||||
if req.Method != http.MethodPost {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Body == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if req.MultipartForm == nil {
|
||||
if len(req.BodyData) == 0 {
|
||||
data, err := req.ReadBody(32 * 1024 * 1024)
|
||||
if err != nil {
|
||||
sysErr = err
|
||||
return
|
||||
}
|
||||
|
||||
req.BodyData = data
|
||||
defer req.RestoreBody(data)
|
||||
}
|
||||
oldBody := req.Body
|
||||
req.Body = ioutil.NopCloser(bytes.NewBuffer(req.BodyData))
|
||||
|
||||
err := req.ParseMultipartForm(32 * 1024 * 1024)
|
||||
|
||||
// 还原
|
||||
req.Body = oldBody
|
||||
|
||||
if err != nil {
|
||||
userErr = err
|
||||
return
|
||||
}
|
||||
|
||||
if req.MultipartForm == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if param == "field" { // field
|
||||
fields := []string{}
|
||||
for field := range req.MultipartForm.File {
|
||||
fields = append(fields, field)
|
||||
}
|
||||
value = strings.Join(fields, ",")
|
||||
} else if param == "minSize" { // minSize
|
||||
minSize := int64(0)
|
||||
for _, files := range req.MultipartForm.File {
|
||||
for _, file := range files {
|
||||
if minSize == 0 || minSize > file.Size {
|
||||
minSize = file.Size
|
||||
}
|
||||
}
|
||||
}
|
||||
value = minSize
|
||||
} else if param == "maxSize" { // maxSize
|
||||
maxSize := int64(0)
|
||||
for _, files := range req.MultipartForm.File {
|
||||
for _, file := range files {
|
||||
if maxSize < file.Size {
|
||||
maxSize = file.Size
|
||||
}
|
||||
}
|
||||
}
|
||||
value = maxSize
|
||||
} else if param == "name" { // name
|
||||
names := []string{}
|
||||
for _, files := range req.MultipartForm.File {
|
||||
for _, file := range files {
|
||||
if !lists.ContainsString(names, file.Filename) {
|
||||
names = append(names, file.Filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
value = strings.Join(names, ",")
|
||||
} else if param == "ext" { // ext
|
||||
extensions := []string{}
|
||||
for _, files := range req.MultipartForm.File {
|
||||
for _, file := range files {
|
||||
if len(file.Filename) > 0 {
|
||||
exit := strings.ToLower(filepath.Ext(file.Filename))
|
||||
if !lists.ContainsString(extensions, exit) {
|
||||
extensions = append(extensions, exit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
value = strings.Join(extensions, ",")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestUploadCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestUploadCheckpoint) ParamOptions() *ParamOptions {
|
||||
option := NewParamOptions()
|
||||
option.AddParam("最小文件尺寸", "minSize")
|
||||
option.AddParam("最大文件尺寸", "maxSize")
|
||||
option.AddParam("扩展名(如.txt)", "ext")
|
||||
option.AddParam("原始文件名", "name")
|
||||
option.AddParam("表单字段名", "field")
|
||||
return option
|
||||
}
|
||||
81
internal/waf/checkpoints/request_upload_test.go
Normal file
81
internal/waf/checkpoints/request_upload_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRequestUploadCheckpoint_RequestValue(t *testing.T) {
|
||||
body := bytes.NewBuffer([]byte{})
|
||||
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
{
|
||||
part, err := writer.CreateFormField("name")
|
||||
if err == nil {
|
||||
part.Write([]byte("lu"))
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
part, err := writer.CreateFormField("age")
|
||||
if err == nil {
|
||||
part.Write([]byte("20"))
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
part, err := writer.CreateFormFile("myFile", "hello.txt")
|
||||
if err == nil {
|
||||
part.Write([]byte("Hello, World!"))
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
part, err := writer.CreateFormFile("myFile2", "hello.PHP")
|
||||
if err == nil {
|
||||
part.Write([]byte("Hello, World, PHP!"))
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
part, err := writer.CreateFormFile("myFile3", "hello.asp")
|
||||
if err == nil {
|
||||
part.Write([]byte("Hello, World, ASP Pages!"))
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
part, err := writer.CreateFormFile("myFile4", "hello.asp")
|
||||
if err == nil {
|
||||
part.Write([]byte("Hello, World, ASP Pages!"))
|
||||
}
|
||||
}
|
||||
|
||||
writer.Close()
|
||||
|
||||
rawReq, err := http.NewRequest(http.MethodPost, "http://teaos.cn/", body)
|
||||
if err != nil {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
req := requests.NewRequest(rawReq)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
|
||||
checkpoint := new(RequestUploadCheckpoint)
|
||||
t.Log(checkpoint.RequestValue(req, "field", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "minSize", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "maxSize", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "name", nil))
|
||||
t.Log(checkpoint.RequestValue(req, "ext", nil))
|
||||
|
||||
data, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(string(data))
|
||||
}
|
||||
25
internal/waf/checkpoints/request_uri.go
Normal file
25
internal/waf/checkpoints/request_uri.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
type RequestURICheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestURICheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if len(req.RequestURI) > 0 {
|
||||
value = req.RequestURI
|
||||
} else if req.URL != nil {
|
||||
value = req.URL.RequestURI()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestURICheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
21
internal/waf/checkpoints/request_user_agent.go
Normal file
21
internal/waf/checkpoints/request_user_agent.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
type RequestUserAgentCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *RequestUserAgentCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
value = req.UserAgent()
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RequestUserAgentCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
41
internal/waf/checkpoints/response_body.go
Normal file
41
internal/waf/checkpoints/response_body.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
// ${responseBody}
|
||||
type ResponseBodyCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *ResponseBodyCheckpoint) IsRequest() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (this *ResponseBodyCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
value = ""
|
||||
return
|
||||
}
|
||||
|
||||
func (this *ResponseBodyCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
value = ""
|
||||
if resp != nil && resp.Body != nil {
|
||||
if len(resp.BodyData) > 0 {
|
||||
value = string(resp.BodyData)
|
||||
return
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
sysErr = err
|
||||
return
|
||||
}
|
||||
resp.BodyData = body
|
||||
_ = resp.Body.Close()
|
||||
value = body
|
||||
resp.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
}
|
||||
return
|
||||
}
|
||||
29
internal/waf/checkpoints/response_body_test.go
Normal file
29
internal/waf/checkpoints/response_body_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResponseBodyCheckpoint_ResponseValue(t *testing.T) {
|
||||
resp := requests.NewResponse(new(http.Response))
|
||||
resp.StatusCode = 200
|
||||
resp.Header = http.Header{}
|
||||
resp.Header.Set("Hello", "World")
|
||||
resp.Body = ioutil.NopCloser(bytes.NewBuffer([]byte("Hello, World")))
|
||||
|
||||
checkpoint := new(ResponseBodyCheckpoint)
|
||||
t.Log(checkpoint.ResponseValue(nil, resp, "", nil))
|
||||
t.Log(checkpoint.ResponseValue(nil, resp, "", nil))
|
||||
t.Log(checkpoint.ResponseValue(nil, resp, "", nil))
|
||||
t.Log(checkpoint.ResponseValue(nil, resp, "", nil))
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("after read:", string(data))
|
||||
}
|
||||
27
internal/waf/checkpoints/response_bytes_sent.go
Normal file
27
internal/waf/checkpoints/response_bytes_sent.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
// ${bytesSent}
|
||||
type ResponseBytesSentCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *ResponseBytesSentCheckpoint) IsRequest() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (this *ResponseBytesSentCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
value = 0
|
||||
return
|
||||
}
|
||||
|
||||
func (this *ResponseBytesSentCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
value = 0
|
||||
if resp != nil {
|
||||
value = resp.ContentLength
|
||||
}
|
||||
return
|
||||
}
|
||||
28
internal/waf/checkpoints/response_header.go
Normal file
28
internal/waf/checkpoints/response_header.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
// ${responseHeader.arg}
|
||||
type ResponseHeaderCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *ResponseHeaderCheckpoint) IsRequest() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (this *ResponseHeaderCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
value = ""
|
||||
return
|
||||
}
|
||||
|
||||
func (this *ResponseHeaderCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if resp != nil && resp.Header != nil {
|
||||
value = resp.Header.Get(param)
|
||||
} else {
|
||||
value = ""
|
||||
}
|
||||
return
|
||||
}
|
||||
17
internal/waf/checkpoints/response_header_test.go
Normal file
17
internal/waf/checkpoints/response_header_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResponseHeaderCheckpoint_ResponseValue(t *testing.T) {
|
||||
resp := requests.NewResponse(new(http.Response))
|
||||
resp.StatusCode = 200
|
||||
resp.Header = http.Header{}
|
||||
resp.Header.Set("Hello", "World")
|
||||
|
||||
checkpoint := new(ResponseHeaderCheckpoint)
|
||||
t.Log(checkpoint.ResponseValue(nil, resp, "Hello", nil))
|
||||
}
|
||||
26
internal/waf/checkpoints/response_status.go
Normal file
26
internal/waf/checkpoints/response_status.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
// ${bytesSent}
|
||||
type ResponseStatusCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *ResponseStatusCheckpoint) IsRequest() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (this *ResponseStatusCheckpoint) RequestValue(req *requests.Request, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
value = 0
|
||||
return
|
||||
}
|
||||
|
||||
func (this *ResponseStatusCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]interface{}) (value interface{}, sysErr error, userErr error) {
|
||||
if resp != nil {
|
||||
value = resp.StatusCode
|
||||
}
|
||||
return
|
||||
}
|
||||
15
internal/waf/checkpoints/response_status_test.go
Normal file
15
internal/waf/checkpoints/response_status_test.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResponseStatusCheckpoint_ResponseValue(t *testing.T) {
|
||||
resp := requests.NewResponse(new(http.Response))
|
||||
resp.StatusCode = 200
|
||||
|
||||
checkpoint := new(ResponseStatusCheckpoint)
|
||||
t.Log(checkpoint.ResponseValue(nil, resp, "", nil))
|
||||
}
|
||||
21
internal/waf/checkpoints/sample_request.go
Normal file
21
internal/waf/checkpoints/sample_request.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
// just a sample checkpoint, copy and change it for your new checkpoint
|
||||
type SampleRequestCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *SampleRequestCheckpoint) RequestValue(req *requests.Request, param string, options map[string]string) (value interface{}, sysErr error, userErr error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (this *SampleRequestCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]string) (value interface{}, sysErr error, userErr error) {
|
||||
if this.IsRequest() {
|
||||
return this.RequestValue(req, param, options)
|
||||
}
|
||||
return
|
||||
}
|
||||
22
internal/waf/checkpoints/sample_response.go
Normal file
22
internal/waf/checkpoints/sample_response.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
// just a sample checkpoint, copy and change it for your new checkpoint
|
||||
type SampleResponseCheckpoint struct {
|
||||
Checkpoint
|
||||
}
|
||||
|
||||
func (this *SampleResponseCheckpoint) IsRequest() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (this *SampleResponseCheckpoint) RequestValue(req *requests.Request, param string, options map[string]string) (value interface{}, sysErr error, userErr error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (this *SampleResponseCheckpoint) ResponseValue(req *requests.Request, resp *requests.Response, param string, options map[string]string) (value interface{}, sysErr error, userErr error) {
|
||||
return
|
||||
}
|
||||
235
internal/waf/checkpoints/utils.go
Normal file
235
internal/waf/checkpoints/utils.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package checkpoints
|
||||
|
||||
// all check points list
|
||||
var AllCheckpoints = []*CheckpointDefinition{
|
||||
{
|
||||
Name: "客户端地址(IP)",
|
||||
Prefix: "remoteAddr",
|
||||
Description: "试图通过分析X-Forwarded-For等Header获取的客户端地址,比如192.168.1.100",
|
||||
HasParams: false,
|
||||
Instance: new(RequestRemoteAddrCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "客户端源地址(IP)",
|
||||
Prefix: "rawRemoteAddr",
|
||||
Description: "直接连接的客户端地址,比如192.168.1.100",
|
||||
HasParams: false,
|
||||
Instance: new(RequestRawRemoteAddrCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "客户端端口",
|
||||
Prefix: "remotePort",
|
||||
Description: "直接连接的客户端地址端口",
|
||||
HasParams: false,
|
||||
Instance: new(RequestRemotePortCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "客户端用户名",
|
||||
Prefix: "remoteUser",
|
||||
Description: "通过BasicAuth登录的客户端用户名",
|
||||
HasParams: false,
|
||||
Instance: new(RequestRemoteUserCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "请求URI",
|
||||
Prefix: "requestURI",
|
||||
Description: "包含URL参数的请求URI,比如/hello/world?lang=go",
|
||||
HasParams: false,
|
||||
Instance: new(RequestURICheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "请求路径",
|
||||
Prefix: "requestPath",
|
||||
Description: "不包含URL参数的请求路径,比如/hello/world",
|
||||
HasParams: false,
|
||||
Instance: new(RequestPathCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "请求内容长度",
|
||||
Prefix: "requestLength",
|
||||
Description: "请求Header中的Content-Length",
|
||||
HasParams: false,
|
||||
Instance: new(RequestLengthCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "请求体内容",
|
||||
Prefix: "requestBody",
|
||||
Description: "通常在POST或者PUT等操作时会附带请求体,最大限制32M",
|
||||
HasParams: false,
|
||||
Instance: new(RequestBodyCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "请求URI和请求体组合",
|
||||
Prefix: "requestAll",
|
||||
Description: "${requestURI}和${requestBody}组合",
|
||||
HasParams: false,
|
||||
Instance: new(RequestAllCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "请求表单参数",
|
||||
Prefix: "requestForm",
|
||||
Description: "获取POST或者其他方法发送的表单参数,最大请求体限制32M",
|
||||
HasParams: true,
|
||||
Instance: new(RequestFormArgCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "上传文件",
|
||||
Prefix: "requestUpload",
|
||||
Description: "获取POST上传的文件信息,最大请求体限制32M",
|
||||
HasParams: true,
|
||||
Instance: new(RequestUploadCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "请求JSON参数",
|
||||
Prefix: "requestJSON",
|
||||
Description: "获取POST或者其他方法发送的JSON,最大请求体限制32M,使用点(.)符号表示多级数据",
|
||||
HasParams: true,
|
||||
Instance: new(RequestJSONArgCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "请求方法",
|
||||
Prefix: "requestMethod",
|
||||
Description: "比如GET、POST",
|
||||
HasParams: false,
|
||||
Instance: new(RequestMethodCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "请求协议",
|
||||
Prefix: "scheme",
|
||||
Description: "比如http或https",
|
||||
HasParams: false,
|
||||
Instance: new(RequestSchemeCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "HTTP协议版本",
|
||||
Prefix: "proto",
|
||||
Description: "比如HTTP/1.1",
|
||||
HasParams: false,
|
||||
Instance: new(RequestProtoCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "主机名",
|
||||
Prefix: "host",
|
||||
Description: "比如teaos.cn",
|
||||
HasParams: false,
|
||||
Instance: new(RequestHostCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "请求来源URL",
|
||||
Prefix: "referer",
|
||||
Description: "请求Header中的Referer值",
|
||||
HasParams: false,
|
||||
Instance: new(RequestRefererCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "客户端信息",
|
||||
Prefix: "userAgent",
|
||||
Description: "比如Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103",
|
||||
HasParams: false,
|
||||
Instance: new(RequestUserAgentCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "内容类型",
|
||||
Prefix: "contentType",
|
||||
Description: "请求Header的Content-Type",
|
||||
HasParams: false,
|
||||
Instance: new(RequestContentTypeCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "所有cookie组合字符串",
|
||||
Prefix: "cookies",
|
||||
Description: "比如sid=IxZVPFhE&city=beijing&uid=18237",
|
||||
HasParams: false,
|
||||
Instance: new(RequestCookiesCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "单个cookie值",
|
||||
Prefix: "cookie",
|
||||
Description: "单个cookie值",
|
||||
HasParams: true,
|
||||
Instance: new(RequestCookieCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "所有URL参数组合",
|
||||
Prefix: "args",
|
||||
Description: "比如name=lu&age=20",
|
||||
HasParams: false,
|
||||
Instance: new(RequestArgsCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "单个URL参数值",
|
||||
Prefix: "arg",
|
||||
Description: "单个URL参数值",
|
||||
HasParams: true,
|
||||
Instance: new(RequestArgCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "所有Header信息",
|
||||
Prefix: "headers",
|
||||
Description: "使用\n隔开的Header信息字符串",
|
||||
HasParams: false,
|
||||
Instance: new(RequestHeadersCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "单个Header值",
|
||||
Prefix: "header",
|
||||
Description: "单个Header值",
|
||||
HasParams: true,
|
||||
Instance: new(RequestHeaderCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "CC统计",
|
||||
Prefix: "cc",
|
||||
Description: "统计某段时间段内的请求信息",
|
||||
HasParams: true,
|
||||
Instance: new(CCCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "响应状态码",
|
||||
Prefix: "status",
|
||||
Description: "响应状态码,比如200、404、500",
|
||||
HasParams: false,
|
||||
Instance: new(ResponseStatusCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "响应Header",
|
||||
Prefix: "responseHeader",
|
||||
Description: "响应Header值",
|
||||
HasParams: true,
|
||||
Instance: new(ResponseHeaderCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "响应内容",
|
||||
Prefix: "responseBody",
|
||||
Description: "响应内容字符串",
|
||||
HasParams: false,
|
||||
Instance: new(ResponseBodyCheckpoint),
|
||||
},
|
||||
{
|
||||
Name: "响应内容长度",
|
||||
Prefix: "bytesSent",
|
||||
Description: "响应内容长度,通过响应的Header Content-Length获取",
|
||||
HasParams: false,
|
||||
Instance: new(ResponseBytesSentCheckpoint),
|
||||
},
|
||||
}
|
||||
|
||||
// find a check point
|
||||
func FindCheckpoint(prefix string) CheckpointInterface {
|
||||
for _, def := range AllCheckpoints {
|
||||
if def.Prefix == prefix {
|
||||
return def.Instance
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// find a check point definition
|
||||
func FindCheckpointDefinition(prefix string) *CheckpointDefinition {
|
||||
for _, def := range AllCheckpoints {
|
||||
if def.Prefix == prefix {
|
||||
return def
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
31
internal/waf/checkpoints/utils_test.go
Normal file
31
internal/waf/checkpoints/utils_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package checkpoints
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFindCheckpointDefinition_Markdown(t *testing.T) {
|
||||
result := []string{}
|
||||
for _, def := range AllCheckpoints {
|
||||
row := "## " + def.Name + "\n* 前缀:`${" + def.Prefix + "}`\n* 描述:" + def.Description
|
||||
if def.HasParams {
|
||||
row += "\n* 是否有子参数:YES"
|
||||
|
||||
paramOptions := def.Instance.ParamOptions()
|
||||
if paramOptions != nil && len(paramOptions.Options) > 0 {
|
||||
row += "\n* 可选子参数"
|
||||
for _, option := range paramOptions.Options {
|
||||
row += "\n * `" + option.Name + "`:值为 `" + option.Value + "`"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
row += "\n* 是否有子参数:NO"
|
||||
}
|
||||
row += "\n"
|
||||
result = append(result, row)
|
||||
}
|
||||
|
||||
fmt.Print(strings.Join(result, "\n") + "\n")
|
||||
}
|
||||
154
internal/waf/ip_table.go
Normal file
154
internal/waf/ip_table.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/shared"
|
||||
"github.com/iwind/TeaGo/lists"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
stringutil "github.com/iwind/TeaGo/utils/string"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type IPAction = string
|
||||
|
||||
var RegexpDigitNumber = regexp.MustCompile("^\\d+$")
|
||||
|
||||
const (
|
||||
IPActionAccept IPAction = "accept"
|
||||
IPActionReject IPAction = "reject"
|
||||
)
|
||||
|
||||
// ip table
|
||||
type IPTable struct {
|
||||
Id string `yaml:"id" json:"id"`
|
||||
On bool `yaml:"on" json:"on"`
|
||||
IP string `yaml:"ip" json:"ip"` // single ip, cidr, ip range, TODO support *
|
||||
Port string `yaml:"port" json:"port"` // single port, range, *
|
||||
Action IPAction `yaml:"action" json:"action"` // accept, reject
|
||||
TimeFrom int64 `yaml:"timeFrom" json:"timeFrom"` // from timestamp
|
||||
TimeTo int64 `yaml:"timeTo" json:"timeTo"` // zero means forever
|
||||
Remark string `yaml:"remark" json:"remark"`
|
||||
|
||||
// port
|
||||
minPort int
|
||||
maxPort int
|
||||
|
||||
minPortWildcard bool
|
||||
maxPortWildcard bool
|
||||
|
||||
ports []int
|
||||
|
||||
// ip
|
||||
ipRange *shared.IPRangeConfig
|
||||
}
|
||||
|
||||
func NewIPTable() *IPTable {
|
||||
return &IPTable{
|
||||
On: true,
|
||||
Id: stringutil.Rand(16),
|
||||
}
|
||||
}
|
||||
|
||||
func (this *IPTable) Init() error {
|
||||
// parse port
|
||||
if RegexpDigitNumber.MatchString(this.Port) {
|
||||
this.minPort = types.Int(this.Port)
|
||||
this.maxPort = types.Int(this.Port)
|
||||
} else if regexp.MustCompile(`[:-]`).MatchString(this.Port) {
|
||||
pieces := regexp.MustCompile(`[:-]`).Split(this.Port, 2)
|
||||
if pieces[0] == "*" {
|
||||
this.minPortWildcard = true
|
||||
} else {
|
||||
this.minPort = types.Int(pieces[0])
|
||||
}
|
||||
if pieces[1] == "*" {
|
||||
this.maxPortWildcard = true
|
||||
} else {
|
||||
this.maxPort = types.Int(pieces[1])
|
||||
}
|
||||
} else if strings.Contains(this.Port, ",") {
|
||||
pieces := strings.Split(this.Port, ",")
|
||||
for _, piece := range pieces {
|
||||
piece = strings.TrimSpace(piece)
|
||||
if len(piece) > 0 {
|
||||
this.ports = append(this.ports, types.Int(piece))
|
||||
}
|
||||
}
|
||||
} else if this.Port == "*" {
|
||||
this.minPortWildcard = true
|
||||
this.maxPortWildcard = true
|
||||
}
|
||||
|
||||
// parse ip
|
||||
if len(this.IP) > 0 {
|
||||
ipRange, err := shared.ParseIPRange(this.IP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.ipRange = ipRange
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// check ip
|
||||
func (this *IPTable) Match(ip string, port int) (isMatched bool) {
|
||||
if !this.On {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
if this.TimeFrom > 0 && now < this.TimeFrom {
|
||||
return
|
||||
}
|
||||
if this.TimeTo > 0 && now > this.TimeTo {
|
||||
return
|
||||
}
|
||||
|
||||
if !this.matchPort(port) {
|
||||
return
|
||||
}
|
||||
|
||||
if !this.matchIP(ip) {
|
||||
return
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (this *IPTable) matchPort(port int) bool {
|
||||
if port == 0 {
|
||||
return false
|
||||
}
|
||||
if this.minPortWildcard {
|
||||
if this.maxPortWildcard {
|
||||
return true
|
||||
}
|
||||
if this.maxPort >= port {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if this.maxPortWildcard {
|
||||
if this.minPortWildcard {
|
||||
return true
|
||||
}
|
||||
if this.minPort <= port {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (this.minPort > 0 || this.maxPort > 0) && this.minPort <= port && this.maxPort >= port {
|
||||
return true
|
||||
}
|
||||
if len(this.ports) > 0 {
|
||||
return lists.ContainsInt(this.ports, port)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (this *IPTable) matchIP(ip string) bool {
|
||||
if this.ipRange == nil {
|
||||
return false
|
||||
}
|
||||
return this.ipRange.Contains(ip)
|
||||
}
|
||||
142
internal/waf/ip_table_test.go
Normal file
142
internal/waf/ip_table_test.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIPTable_MatchIP(t *testing.T) {
|
||||
a := assert.NewAssertion(t)
|
||||
|
||||
{
|
||||
table := NewIPTable()
|
||||
err := table.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(table.Match("192.168.1.100", 8080))
|
||||
}
|
||||
|
||||
{
|
||||
table := NewIPTable()
|
||||
table.IP = "*"
|
||||
table.Port = "8080"
|
||||
err := table.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("port:", table.minPort, table.maxPort)
|
||||
a.IsTrue(table.Match("192.168.1.100", 8080))
|
||||
a.IsFalse(table.Match("192.168.1.100", 8081))
|
||||
}
|
||||
|
||||
{
|
||||
table := NewIPTable()
|
||||
table.IP = "*"
|
||||
table.Port = "8080-8082"
|
||||
err := table.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("port:", table.minPort, table.maxPort)
|
||||
a.IsTrue(table.Match("192.168.1.100", 8080))
|
||||
a.IsTrue(table.Match("192.168.1.100", 8081))
|
||||
a.IsFalse(table.Match("192.168.1.100", 8083))
|
||||
}
|
||||
|
||||
{
|
||||
table := NewIPTable()
|
||||
table.IP = "*"
|
||||
table.Port = "*-8082"
|
||||
err := table.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("port:", table.minPort, table.maxPort)
|
||||
a.IsTrue(table.Match("192.168.1.100", 8079))
|
||||
a.IsTrue(table.Match("192.168.1.100", 8080))
|
||||
a.IsTrue(table.Match("192.168.1.100", 8081))
|
||||
a.IsFalse(table.Match("192.168.1.100", 8083))
|
||||
}
|
||||
|
||||
{
|
||||
table := NewIPTable()
|
||||
table.IP = "*"
|
||||
table.Port = "8080-*"
|
||||
err := table.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("port:", table.minPort, table.maxPort)
|
||||
a.IsFalse(table.Match("192.168.1.100", 8079))
|
||||
a.IsTrue(table.Match("192.168.1.100", 8080))
|
||||
a.IsTrue(table.Match("192.168.1.100", 8081))
|
||||
a.IsTrue(table.Match("192.168.1.100", 8083))
|
||||
}
|
||||
|
||||
{
|
||||
table := NewIPTable()
|
||||
table.IP = "*"
|
||||
table.Port = "*"
|
||||
err := table.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("port:", table.minPort, table.maxPort)
|
||||
a.IsTrue(table.Match("192.168.1.100", 8079))
|
||||
a.IsTrue(table.Match("192.168.1.100", 8080))
|
||||
a.IsTrue(table.Match("192.168.1.100", 8081))
|
||||
a.IsTrue(table.Match("192.168.1.100", 8083))
|
||||
}
|
||||
|
||||
{
|
||||
table := NewIPTable()
|
||||
table.IP = "192.168.1.100"
|
||||
table.Port = "*"
|
||||
err := table.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("port:", table.minPort, table.maxPort)
|
||||
a.IsTrue(table.Match("192.168.1.100", 8080))
|
||||
}
|
||||
|
||||
{
|
||||
table := NewIPTable()
|
||||
table.IP = "192.168.1.99-192.168.1.101"
|
||||
table.Port = "*"
|
||||
err := table.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("port:", table.minPort, table.maxPort)
|
||||
a.IsTrue(table.Match("192.168.1.100", 8080))
|
||||
}
|
||||
|
||||
{
|
||||
table := NewIPTable()
|
||||
table.IP = "192.168.1.99/24"
|
||||
table.Port = "*"
|
||||
err := table.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("ip:", table.ipRange)
|
||||
a.IsTrue(table.Match("192.168.1.100", 8080))
|
||||
a.IsFalse(table.Match("192.168.2.100", 8080))
|
||||
}
|
||||
|
||||
{
|
||||
table := NewIPTable()
|
||||
table.IP = "192.168.1.99/24"
|
||||
table.TimeTo = time.Now().Unix() - 10
|
||||
table.Port = "*"
|
||||
err := table.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(table.Match("192.168.1.100", 8080))
|
||||
a.IsFalse(table.Match("192.168.2.100", 8080))
|
||||
}
|
||||
}
|
||||
35
internal/waf/requests/request.go
Normal file
35
internal/waf/requests/request.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
*http.Request
|
||||
BodyData []byte
|
||||
}
|
||||
|
||||
func NewRequest(raw *http.Request) *Request {
|
||||
return &Request{
|
||||
Request: raw,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Request) Raw() *http.Request {
|
||||
return this.Request
|
||||
}
|
||||
|
||||
func (this *Request) ReadBody(max int64) (data []byte, err error) {
|
||||
data, err = ioutil.ReadAll(io.LimitReader(this.Request.Body, max))
|
||||
return
|
||||
}
|
||||
|
||||
func (this *Request) RestoreBody(data []byte) {
|
||||
rawReader := bytes.NewBuffer(data)
|
||||
buf := make([]byte, 1024)
|
||||
io.CopyBuffer(rawReader, this.Request.Body, buf)
|
||||
this.Request.Body = ioutil.NopCloser(rawReader)
|
||||
}
|
||||
15
internal/waf/requests/response.go
Normal file
15
internal/waf/requests/response.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package requests
|
||||
|
||||
import "net/http"
|
||||
|
||||
type Response struct {
|
||||
*http.Response
|
||||
|
||||
BodyData []byte
|
||||
}
|
||||
|
||||
func NewResponse(resp *http.Response) *Response {
|
||||
return &Response{
|
||||
Response: resp,
|
||||
}
|
||||
}
|
||||
584
internal/waf/rule.go
Normal file
584
internal/waf/rule.go
Normal file
@@ -0,0 +1,584 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/checkpoints"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/utils"
|
||||
"github.com/iwind/TeaGo/lists"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"github.com/iwind/TeaGo/utils/string"
|
||||
"net"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var singleParamRegexp = regexp.MustCompile("^\\${[\\w.-]+}$")
|
||||
|
||||
// rule
|
||||
type Rule struct {
|
||||
Description string `yaml:"description" json:"description"`
|
||||
Param string `yaml:"param" json:"param"` // such as ${arg.name} or ${args}, can be composite as ${arg.firstName}${arg.lastName}
|
||||
Operator RuleOperator `yaml:"operator" json:"operator"` // such as contains, gt, ...
|
||||
Value string `yaml:"value" json:"value"` // compared value
|
||||
IsCaseInsensitive bool `yaml:"isCaseInsensitive" json:"isCaseInsensitive"`
|
||||
CheckpointOptions map[string]interface{} `yaml:"checkpointOptions" json:"checkpointOptions"`
|
||||
|
||||
checkpointFinder func(prefix string) checkpoints.CheckpointInterface
|
||||
|
||||
singleParam string // real param after prefix
|
||||
singleCheckpoint checkpoints.CheckpointInterface // if is single check point
|
||||
|
||||
multipleCheckpoints map[string]checkpoints.CheckpointInterface
|
||||
|
||||
isIP bool
|
||||
ipValue net.IP
|
||||
|
||||
floatValue float64
|
||||
reg *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewRule() *Rule {
|
||||
return &Rule{}
|
||||
}
|
||||
|
||||
func (this *Rule) Init() error {
|
||||
// operator
|
||||
switch this.Operator {
|
||||
case RuleOperatorGt:
|
||||
this.floatValue = types.Float64(this.Value)
|
||||
case RuleOperatorGte:
|
||||
this.floatValue = types.Float64(this.Value)
|
||||
case RuleOperatorLt:
|
||||
this.floatValue = types.Float64(this.Value)
|
||||
case RuleOperatorLte:
|
||||
this.floatValue = types.Float64(this.Value)
|
||||
case RuleOperatorEq:
|
||||
this.floatValue = types.Float64(this.Value)
|
||||
case RuleOperatorNeq:
|
||||
this.floatValue = types.Float64(this.Value)
|
||||
case RuleOperatorMatch:
|
||||
v := this.Value
|
||||
if this.IsCaseInsensitive && !strings.HasPrefix(v, "(?i)") {
|
||||
v = "(?i)" + v
|
||||
}
|
||||
|
||||
v = this.unescape(v)
|
||||
|
||||
reg, err := regexp.Compile(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.reg = reg
|
||||
case RuleOperatorNotMatch:
|
||||
v := this.Value
|
||||
if this.IsCaseInsensitive && !strings.HasPrefix(v, "(?i)") {
|
||||
v = "(?i)" + v
|
||||
}
|
||||
|
||||
v = this.unescape(v)
|
||||
|
||||
reg, err := regexp.Compile(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.reg = reg
|
||||
case RuleOperatorEqIP, RuleOperatorGtIP, RuleOperatorGteIP, RuleOperatorLtIP, RuleOperatorLteIP:
|
||||
this.ipValue = net.ParseIP(this.Value)
|
||||
this.isIP = this.ipValue != nil
|
||||
|
||||
if !this.isIP {
|
||||
return errors.New("value should be a valid ip")
|
||||
}
|
||||
case RuleOperatorIPRange, RuleOperatorNotIPRange:
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if singleParamRegexp.MatchString(this.Param) {
|
||||
param := this.Param[2 : len(this.Param)-1]
|
||||
pieces := strings.SplitN(param, ".", 2)
|
||||
prefix := pieces[0]
|
||||
if len(pieces) == 1 {
|
||||
this.singleParam = ""
|
||||
} else {
|
||||
this.singleParam = pieces[1]
|
||||
}
|
||||
|
||||
if this.checkpointFinder != nil {
|
||||
checkpoint := this.checkpointFinder(prefix)
|
||||
if checkpoint == nil {
|
||||
return errors.New("no check point '" + prefix + "' found")
|
||||
}
|
||||
this.singleCheckpoint = checkpoint
|
||||
} else {
|
||||
checkpoint := checkpoints.FindCheckpoint(prefix)
|
||||
if checkpoint == nil {
|
||||
return errors.New("no check point '" + prefix + "' found")
|
||||
}
|
||||
checkpoint.Init()
|
||||
this.singleCheckpoint = checkpoint
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
this.multipleCheckpoints = map[string]checkpoints.CheckpointInterface{}
|
||||
var err error = nil
|
||||
configutils.ParseVariables(this.Param, func(varName string) (value string) {
|
||||
pieces := strings.SplitN(varName, ".", 2)
|
||||
prefix := pieces[0]
|
||||
if this.checkpointFinder != nil {
|
||||
checkpoint := this.checkpointFinder(prefix)
|
||||
if checkpoint == nil {
|
||||
err = errors.New("no check point '" + prefix + "' found")
|
||||
} else {
|
||||
this.multipleCheckpoints[prefix] = checkpoint
|
||||
}
|
||||
} else {
|
||||
checkpoint := checkpoints.FindCheckpoint(prefix)
|
||||
if checkpoint == nil {
|
||||
err = errors.New("no check point '" + prefix + "' found")
|
||||
} else {
|
||||
checkpoint.Init()
|
||||
this.multipleCheckpoints[prefix] = checkpoint
|
||||
}
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (this *Rule) MatchRequest(req *requests.Request) (b bool, err error) {
|
||||
if this.singleCheckpoint != nil {
|
||||
value, err, _ := this.singleCheckpoint.RequestValue(req, this.singleParam, this.CheckpointOptions)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return this.Test(value), nil
|
||||
}
|
||||
|
||||
value := configutils.ParseVariables(this.Param, func(varName string) (value string) {
|
||||
pieces := strings.SplitN(varName, ".", 2)
|
||||
prefix := pieces[0]
|
||||
point, ok := this.multipleCheckpoints[prefix]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(pieces) == 1 {
|
||||
value1, err1, _ := point.RequestValue(req, "", this.CheckpointOptions)
|
||||
if err1 != nil {
|
||||
err = err1
|
||||
}
|
||||
return types.String(value1)
|
||||
}
|
||||
|
||||
value1, err1, _ := point.RequestValue(req, pieces[1], this.CheckpointOptions)
|
||||
if err1 != nil {
|
||||
err = err1
|
||||
}
|
||||
return types.String(value1)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return this.Test(value), nil
|
||||
}
|
||||
|
||||
func (this *Rule) MatchResponse(req *requests.Request, resp *requests.Response) (b bool, err error) {
|
||||
if this.singleCheckpoint != nil {
|
||||
// if is request param
|
||||
if this.singleCheckpoint.IsRequest() {
|
||||
value, err, _ := this.singleCheckpoint.RequestValue(req, this.singleParam, this.CheckpointOptions)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return this.Test(value), nil
|
||||
}
|
||||
|
||||
// response param
|
||||
value, err, _ := this.singleCheckpoint.ResponseValue(req, resp, this.singleParam, this.CheckpointOptions)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return this.Test(value), nil
|
||||
}
|
||||
|
||||
value := configutils.ParseVariables(this.Param, func(varName string) (value string) {
|
||||
pieces := strings.SplitN(varName, ".", 2)
|
||||
prefix := pieces[0]
|
||||
point, ok := this.multipleCheckpoints[prefix]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(pieces) == 1 {
|
||||
if point.IsRequest() {
|
||||
value1, err1, _ := point.RequestValue(req, "", this.CheckpointOptions)
|
||||
if err1 != nil {
|
||||
err = err1
|
||||
}
|
||||
return types.String(value1)
|
||||
} else {
|
||||
value1, err1, _ := point.ResponseValue(req, resp, "", this.CheckpointOptions)
|
||||
if err1 != nil {
|
||||
err = err1
|
||||
}
|
||||
return types.String(value1)
|
||||
}
|
||||
}
|
||||
|
||||
if point.IsRequest() {
|
||||
value1, err1, _ := point.RequestValue(req, pieces[1], this.CheckpointOptions)
|
||||
if err1 != nil {
|
||||
err = err1
|
||||
}
|
||||
return types.String(value1)
|
||||
} else {
|
||||
value1, err1, _ := point.ResponseValue(req, resp, pieces[1], this.CheckpointOptions)
|
||||
if err1 != nil {
|
||||
err = err1
|
||||
}
|
||||
return types.String(value1)
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return this.Test(value), nil
|
||||
}
|
||||
|
||||
func (this *Rule) Test(value interface{}) bool {
|
||||
// operator
|
||||
switch this.Operator {
|
||||
case RuleOperatorGt:
|
||||
return types.Float64(value) > this.floatValue
|
||||
case RuleOperatorGte:
|
||||
return types.Float64(value) >= this.floatValue
|
||||
case RuleOperatorLt:
|
||||
return types.Float64(value) < this.floatValue
|
||||
case RuleOperatorLte:
|
||||
return types.Float64(value) <= this.floatValue
|
||||
case RuleOperatorEq:
|
||||
return types.Float64(value) == this.floatValue
|
||||
case RuleOperatorNeq:
|
||||
return types.Float64(value) != this.floatValue
|
||||
case RuleOperatorEqString:
|
||||
if this.IsCaseInsensitive {
|
||||
return strings.ToLower(types.String(value)) == strings.ToLower(this.Value)
|
||||
} else {
|
||||
return types.String(value) == this.Value
|
||||
}
|
||||
case RuleOperatorNeqString:
|
||||
if this.IsCaseInsensitive {
|
||||
return strings.ToLower(types.String(value)) != strings.ToLower(this.Value)
|
||||
} else {
|
||||
return types.String(value) != this.Value
|
||||
}
|
||||
case RuleOperatorMatch:
|
||||
if value == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// strings
|
||||
stringList, ok := value.([]string)
|
||||
if ok {
|
||||
for _, s := range stringList {
|
||||
if utils.MatchStringCache(this.reg, s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// bytes
|
||||
byteSlice, ok := value.([]byte)
|
||||
if ok {
|
||||
return utils.MatchBytesCache(this.reg, byteSlice)
|
||||
}
|
||||
|
||||
// string
|
||||
return utils.MatchStringCache(this.reg, types.String(value))
|
||||
case RuleOperatorNotMatch:
|
||||
if value == nil {
|
||||
return true
|
||||
}
|
||||
stringList, ok := value.([]string)
|
||||
if ok {
|
||||
for _, s := range stringList {
|
||||
if utils.MatchStringCache(this.reg, s) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// bytes
|
||||
byteSlice, ok := value.([]byte)
|
||||
if ok {
|
||||
return !utils.MatchBytesCache(this.reg, byteSlice)
|
||||
}
|
||||
|
||||
return !utils.MatchStringCache(this.reg, types.String(value))
|
||||
case RuleOperatorContains:
|
||||
if types.IsSlice(value) {
|
||||
ok := false
|
||||
lists.Each(value, func(k int, v interface{}) {
|
||||
if types.String(v) == this.Value {
|
||||
ok = true
|
||||
}
|
||||
})
|
||||
return ok
|
||||
}
|
||||
if types.IsMap(value) {
|
||||
lowerValue := ""
|
||||
if this.IsCaseInsensitive {
|
||||
lowerValue = strings.ToLower(this.Value)
|
||||
}
|
||||
for _, v := range maps.NewMap(value) {
|
||||
if this.IsCaseInsensitive {
|
||||
if strings.ToLower(types.String(v)) == lowerValue {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if types.String(v) == this.Value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if this.IsCaseInsensitive {
|
||||
return strings.Contains(strings.ToLower(types.String(value)), strings.ToLower(this.Value))
|
||||
} else {
|
||||
return strings.Contains(types.String(value), this.Value)
|
||||
}
|
||||
case RuleOperatorNotContains:
|
||||
if this.IsCaseInsensitive {
|
||||
return !strings.Contains(strings.ToLower(types.String(value)), strings.ToLower(this.Value))
|
||||
} else {
|
||||
return !strings.Contains(types.String(value), this.Value)
|
||||
}
|
||||
case RuleOperatorPrefix:
|
||||
if this.IsCaseInsensitive {
|
||||
return strings.HasPrefix(strings.ToLower(types.String(value)), strings.ToLower(this.Value))
|
||||
} else {
|
||||
return strings.HasPrefix(types.String(value), this.Value)
|
||||
}
|
||||
case RuleOperatorSuffix:
|
||||
if this.IsCaseInsensitive {
|
||||
return strings.HasSuffix(strings.ToLower(types.String(value)), strings.ToLower(this.Value))
|
||||
} else {
|
||||
return strings.HasSuffix(types.String(value), this.Value)
|
||||
}
|
||||
case RuleOperatorHasKey:
|
||||
if types.IsSlice(value) {
|
||||
index := types.Int(this.Value)
|
||||
if index < 0 {
|
||||
return false
|
||||
}
|
||||
return reflect.ValueOf(value).Len() > index
|
||||
} else if types.IsMap(value) {
|
||||
m := maps.NewMap(value)
|
||||
if this.IsCaseInsensitive {
|
||||
lowerValue := strings.ToLower(this.Value)
|
||||
for k := range m {
|
||||
if strings.ToLower(k) == lowerValue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return m.Has(this.Value)
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
case RuleOperatorVersionGt:
|
||||
return stringutil.VersionCompare(this.Value, types.String(value)) > 0
|
||||
case RuleOperatorVersionLt:
|
||||
return stringutil.VersionCompare(this.Value, types.String(value)) < 0
|
||||
case RuleOperatorVersionRange:
|
||||
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(types.String(value), version1) < 0 {
|
||||
return false
|
||||
}
|
||||
if len(version2) > 0 && stringutil.VersionCompare(types.String(value), version2) > 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return stringutil.VersionCompare(types.String(value), this.Value) >= 0
|
||||
}
|
||||
case RuleOperatorEqIP:
|
||||
ip := net.ParseIP(types.String(value))
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return this.isIP && bytes.Compare(this.ipValue, ip) == 0
|
||||
case RuleOperatorGtIP:
|
||||
ip := net.ParseIP(types.String(value))
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return this.isIP && bytes.Compare(ip, this.ipValue) > 0
|
||||
case RuleOperatorGteIP:
|
||||
ip := net.ParseIP(types.String(value))
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return this.isIP && bytes.Compare(ip, this.ipValue) >= 0
|
||||
case RuleOperatorLtIP:
|
||||
ip := net.ParseIP(types.String(value))
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return this.isIP && bytes.Compare(ip, this.ipValue) < 0
|
||||
case RuleOperatorLteIP:
|
||||
ip := net.ParseIP(types.String(value))
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return this.isIP && bytes.Compare(ip, this.ipValue) <= 0
|
||||
case RuleOperatorIPRange:
|
||||
return this.containsIP(value)
|
||||
case RuleOperatorNotIPRange:
|
||||
return !this.containsIP(value)
|
||||
case RuleOperatorIPMod:
|
||||
pieces := strings.SplitN(this.Value, ",", 2)
|
||||
if len(pieces) == 1 {
|
||||
rem := types.Int64(pieces[0])
|
||||
return this.ipToInt64(net.ParseIP(types.String(value)))%10 == rem
|
||||
}
|
||||
div := types.Int64(pieces[0])
|
||||
if div == 0 {
|
||||
return false
|
||||
}
|
||||
rem := types.Int64(pieces[1])
|
||||
return this.ipToInt64(net.ParseIP(types.String(value)))%div == rem
|
||||
case RuleOperatorIPMod10:
|
||||
return this.ipToInt64(net.ParseIP(types.String(value)))%10 == types.Int64(this.Value)
|
||||
case RuleOperatorIPMod100:
|
||||
return this.ipToInt64(net.ParseIP(types.String(value)))%100 == types.Int64(this.Value)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (this *Rule) IsSingleCheckpoint() bool {
|
||||
return this.singleCheckpoint != nil
|
||||
}
|
||||
|
||||
func (this *Rule) SetCheckpointFinder(finder func(prefix string) checkpoints.CheckpointInterface) {
|
||||
this.checkpointFinder = finder
|
||||
}
|
||||
|
||||
func (this *Rule) unescape(v string) string {
|
||||
//replace urlencoded characters
|
||||
v = strings.Replace(v, `\s`, `(\s|%09|%0A|\+)`, -1)
|
||||
v = strings.Replace(v, `\(`, `(\(|%28)`, -1)
|
||||
v = strings.Replace(v, `=`, `(=|%3D)`, -1)
|
||||
v = strings.Replace(v, `<`, `(<|%3C)`, -1)
|
||||
v = strings.Replace(v, `\*`, `(\*|%2A)`, -1)
|
||||
v = strings.Replace(v, `\\`, `(\\|%2F)`, -1)
|
||||
v = strings.Replace(v, `!`, `(!|%21)`, -1)
|
||||
v = strings.Replace(v, `/`, `(/|%2F)`, -1)
|
||||
v = strings.Replace(v, `;`, `(;|%3B)`, -1)
|
||||
v = strings.Replace(v, `\+`, `(\+|%20)`, -1)
|
||||
return v
|
||||
}
|
||||
|
||||
func (this *Rule) containsIP(value interface{}) bool {
|
||||
ip := net.ParseIP(types.String(value))
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Rule) 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))
|
||||
}
|
||||
147
internal/waf/rule_group.go
Normal file
147
internal/waf/rule_group.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
)
|
||||
|
||||
// rule group
|
||||
type RuleGroup struct {
|
||||
Id string `yaml:"id" json:"id"`
|
||||
IsOn bool `yaml:"isOn" json:"isOn"`
|
||||
Name string `yaml:"name" json:"name"` // such as SQL Injection
|
||||
Description string `yaml:"description" json:"description"`
|
||||
Code string `yaml:"code" json:"code"` // identify the group
|
||||
RuleSets []*RuleSet `yaml:"ruleSets" json:"ruleSets"`
|
||||
IsInbound bool `yaml:"isInbound" json:"isInbound"`
|
||||
|
||||
hasRuleSets bool
|
||||
}
|
||||
|
||||
func NewRuleGroup() *RuleGroup {
|
||||
return &RuleGroup{
|
||||
IsOn: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *RuleGroup) Init() error {
|
||||
this.hasRuleSets = len(this.RuleSets) > 0
|
||||
|
||||
if this.hasRuleSets {
|
||||
for _, set := range this.RuleSets {
|
||||
err := set.Init()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *RuleGroup) AddRuleSet(ruleSet *RuleSet) {
|
||||
this.RuleSets = append(this.RuleSets, ruleSet)
|
||||
}
|
||||
|
||||
func (this *RuleGroup) FindRuleSet(id string) *RuleSet {
|
||||
if len(id) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, ruleSet := range this.RuleSets {
|
||||
if ruleSet.Id == id {
|
||||
return ruleSet
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *RuleGroup) FindRuleSetWithCode(code string) *RuleSet {
|
||||
if len(code) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, ruleSet := range this.RuleSets {
|
||||
if ruleSet.Code == code {
|
||||
return ruleSet
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *RuleGroup) RemoveRuleSet(id string) {
|
||||
if len(id) == 0 {
|
||||
return
|
||||
}
|
||||
result := []*RuleSet{}
|
||||
for _, ruleSet := range this.RuleSets {
|
||||
if ruleSet.Id == id {
|
||||
continue
|
||||
}
|
||||
result = append(result, ruleSet)
|
||||
}
|
||||
this.RuleSets = result
|
||||
}
|
||||
|
||||
func (this *RuleGroup) MatchRequest(req *requests.Request) (b bool, set *RuleSet, err error) {
|
||||
if !this.hasRuleSets {
|
||||
return
|
||||
}
|
||||
for _, set := range this.RuleSets {
|
||||
if !set.IsOn {
|
||||
continue
|
||||
}
|
||||
b, err = set.MatchRequest(req)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if b {
|
||||
return true, set, nil
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RuleGroup) MatchResponse(req *requests.Request, resp *requests.Response) (b bool, set *RuleSet, err error) {
|
||||
if !this.hasRuleSets {
|
||||
return
|
||||
}
|
||||
for _, set := range this.RuleSets {
|
||||
if !set.IsOn {
|
||||
continue
|
||||
}
|
||||
b, err = set.MatchResponse(req, resp)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if b {
|
||||
return true, set, nil
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RuleGroup) MoveRuleSet(fromIndex int, toIndex int) {
|
||||
if fromIndex < 0 || fromIndex >= len(this.RuleSets) {
|
||||
return
|
||||
}
|
||||
if toIndex < 0 || toIndex >= len(this.RuleSets) {
|
||||
return
|
||||
}
|
||||
if fromIndex == toIndex {
|
||||
return
|
||||
}
|
||||
|
||||
location := this.RuleSets[fromIndex]
|
||||
result := []*RuleSet{}
|
||||
for i := 0; i < len(this.RuleSets); i++ {
|
||||
if i == fromIndex {
|
||||
continue
|
||||
}
|
||||
if fromIndex > toIndex && i == toIndex {
|
||||
result = append(result, location)
|
||||
}
|
||||
result = append(result, this.RuleSets[i])
|
||||
if fromIndex < toIndex && i == toIndex {
|
||||
result = append(result, location)
|
||||
}
|
||||
}
|
||||
|
||||
this.RuleSets = result
|
||||
}
|
||||
219
internal/waf/rule_operator.go
Normal file
219
internal/waf/rule_operator.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package waf
|
||||
|
||||
type RuleOperator = string
|
||||
type RuleCaseInsensitive = string
|
||||
|
||||
const (
|
||||
RuleOperatorGt RuleOperator = "gt"
|
||||
RuleOperatorGte RuleOperator = "gte"
|
||||
RuleOperatorLt RuleOperator = "lt"
|
||||
RuleOperatorLte RuleOperator = "lte"
|
||||
RuleOperatorEq RuleOperator = "eq"
|
||||
RuleOperatorNeq RuleOperator = "neq"
|
||||
RuleOperatorEqString RuleOperator = "eq string"
|
||||
RuleOperatorNeqString RuleOperator = "neq string"
|
||||
RuleOperatorMatch RuleOperator = "match"
|
||||
RuleOperatorNotMatch RuleOperator = "not match"
|
||||
RuleOperatorContains RuleOperator = "contains"
|
||||
RuleOperatorNotContains RuleOperator = "not contains"
|
||||
RuleOperatorPrefix RuleOperator = "prefix"
|
||||
RuleOperatorSuffix RuleOperator = "suffix"
|
||||
RuleOperatorHasKey RuleOperator = "has key" // has key in slice or map
|
||||
RuleOperatorVersionGt RuleOperator = "version gt"
|
||||
RuleOperatorVersionLt RuleOperator = "version lt"
|
||||
RuleOperatorVersionRange RuleOperator = "version range"
|
||||
|
||||
// ip
|
||||
RuleOperatorEqIP RuleOperator = "eq ip"
|
||||
RuleOperatorGtIP RuleOperator = "gt ip"
|
||||
RuleOperatorGteIP RuleOperator = "gte ip"
|
||||
RuleOperatorLtIP RuleOperator = "lt ip"
|
||||
RuleOperatorLteIP RuleOperator = "lte ip"
|
||||
RuleOperatorIPRange RuleOperator = "ip range"
|
||||
RuleOperatorNotIPRange RuleOperator = "not ip range"
|
||||
RuleOperatorIPMod10 RuleOperator = "ip mod 10"
|
||||
RuleOperatorIPMod100 RuleOperator = "ip mod 100"
|
||||
RuleOperatorIPMod RuleOperator = "ip mod"
|
||||
|
||||
RuleCaseInsensitiveNone = "none"
|
||||
RuleCaseInsensitiveYes = "yes"
|
||||
RuleCaseInsensitiveNo = "no"
|
||||
)
|
||||
|
||||
type RuleOperatorDefinition struct {
|
||||
Name string
|
||||
Code string
|
||||
Description string
|
||||
CaseInsensitive RuleCaseInsensitive // default caseInsensitive setting
|
||||
}
|
||||
|
||||
var AllRuleOperators = []*RuleOperatorDefinition{
|
||||
{
|
||||
Name: "数值大于",
|
||||
Code: RuleOperatorGt,
|
||||
Description: "使用数值对比大于",
|
||||
CaseInsensitive: RuleCaseInsensitiveNone,
|
||||
},
|
||||
{
|
||||
Name: "数值大于等于",
|
||||
Code: RuleOperatorGte,
|
||||
Description: "使用数值对比大于等于",
|
||||
CaseInsensitive: RuleCaseInsensitiveNone,
|
||||
},
|
||||
{
|
||||
Name: "数值小于",
|
||||
Code: RuleOperatorLt,
|
||||
Description: "使用数值对比小于",
|
||||
CaseInsensitive: RuleCaseInsensitiveNone,
|
||||
},
|
||||
{
|
||||
Name: "数值小于等于",
|
||||
Code: RuleOperatorLte,
|
||||
Description: "使用数值对比小于等于",
|
||||
CaseInsensitive: RuleCaseInsensitiveNone,
|
||||
},
|
||||
{
|
||||
Name: "数值等于",
|
||||
Code: RuleOperatorEq,
|
||||
Description: "使用数值对比等于",
|
||||
CaseInsensitive: RuleCaseInsensitiveNone,
|
||||
},
|
||||
{
|
||||
Name: "数值不等于",
|
||||
Code: RuleOperatorNeq,
|
||||
Description: "使用数值对比不等于",
|
||||
CaseInsensitive: RuleCaseInsensitiveNone,
|
||||
},
|
||||
{
|
||||
Name: "字符串等于",
|
||||
Code: RuleOperatorEqString,
|
||||
Description: "使用字符串对比等于",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "字符串不等于",
|
||||
Code: RuleOperatorNeqString,
|
||||
Description: "使用字符串对比不等于",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "正则匹配",
|
||||
Code: RuleOperatorMatch,
|
||||
Description: "使用正则表达式匹配,在头部使用(?i)表示不区分大小写,<a href=\"http://teaos.cn/doc/regexp/Regexp.md\" target=\"_blank\">正则表达式语法 »</a>",
|
||||
CaseInsensitive: RuleCaseInsensitiveYes,
|
||||
},
|
||||
{
|
||||
Name: "正则不匹配",
|
||||
Code: RuleOperatorNotMatch,
|
||||
Description: "使用正则表达式不匹配,在头部使用(?i)表示不区分大小写,<a href=\"http://teaos.cn/doc/regexp/Regexp.md\" target=\"_blank\">正则表达式语法 »</a>",
|
||||
CaseInsensitive: RuleCaseInsensitiveYes,
|
||||
},
|
||||
{
|
||||
Name: "包含字符串",
|
||||
Code: RuleOperatorContains,
|
||||
Description: "包含某个字符串",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "不包含字符串",
|
||||
Code: RuleOperatorNotContains,
|
||||
Description: "不包含某个字符串",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "包含前缀",
|
||||
Code: RuleOperatorPrefix,
|
||||
Description: "包含某个前缀",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "包含后缀",
|
||||
Code: RuleOperatorSuffix,
|
||||
Description: "包含某个后缀",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "包含索引",
|
||||
Code: RuleOperatorHasKey,
|
||||
Description: "对于一组数据拥有某个键值或者索引",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "版本号大于",
|
||||
Code: RuleOperatorVersionGt,
|
||||
Description: "对比版本号大于",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "版本号小于",
|
||||
Code: RuleOperatorVersionLt,
|
||||
Description: "对比版本号小于",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "版本号范围",
|
||||
Code: RuleOperatorVersionRange,
|
||||
Description: "判断版本号在某个范围内,格式为version1,version2",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "IP等于",
|
||||
Code: RuleOperatorEqIP,
|
||||
Description: "将参数转换为IP进行对比",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "IP大于",
|
||||
Code: RuleOperatorGtIP,
|
||||
Description: "将参数转换为IP进行对比",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "IP大于等于",
|
||||
Code: RuleOperatorGteIP,
|
||||
Description: "将参数转换为IP进行对比",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "IP小于",
|
||||
Code: RuleOperatorLtIP,
|
||||
Description: "将参数转换为IP进行对比",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "IP小于等于",
|
||||
Code: RuleOperatorLteIP,
|
||||
Description: "将参数转换为IP进行对比",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "IP范围",
|
||||
Code: RuleOperatorIPRange,
|
||||
Description: "IP在某个范围之内,范围格式可以是英文逗号分隔的ip1,ip2,或者CIDR格式的ip/bits",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "不在IP范围",
|
||||
Code: RuleOperatorNotIPRange,
|
||||
Description: "IP不在某个范围之内,范围格式可以是英文逗号分隔的ip1,ip2,或者CIDR格式的ip/bits",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "IP取模10",
|
||||
Code: RuleOperatorIPMod10,
|
||||
Description: "对IP参数值取模,除数为10,对比值为余数",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "IP取模100",
|
||||
Code: RuleOperatorIPMod100,
|
||||
Description: "对IP参数值取模,除数为100,对比值为余数",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
{
|
||||
Name: "IP取模",
|
||||
Code: RuleOperatorIPMod,
|
||||
Description: "对IP参数值取模,对比值格式为:除数,余数,比如10,1",
|
||||
CaseInsensitive: RuleCaseInsensitiveNo,
|
||||
},
|
||||
}
|
||||
135
internal/waf/rule_set.go
Normal file
135
internal/waf/rule_set.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/TeaGo/utils/string"
|
||||
)
|
||||
|
||||
type RuleConnector = string
|
||||
|
||||
const (
|
||||
RuleConnectorAnd = "and"
|
||||
RuleConnectorOr = "or"
|
||||
)
|
||||
|
||||
type RuleSet struct {
|
||||
Id string `yaml:"id" json:"id"`
|
||||
Code string `yaml:"code" json:"code"`
|
||||
IsOn bool `yaml:"isOn" json:"isOn"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
Rules []*Rule `yaml:"rules" json:"rules"`
|
||||
Connector RuleConnector `yaml:"connector" json:"connector"` // rules connector
|
||||
|
||||
Action ActionString `yaml:"action" json:"action"`
|
||||
ActionOptions maps.Map `yaml:"actionOptions" json:"actionOptions"` // TODO TO BE IMPLEMENTED
|
||||
|
||||
hasRules bool
|
||||
}
|
||||
|
||||
func NewRuleSet() *RuleSet {
|
||||
return &RuleSet{
|
||||
Id: stringutil.Rand(16),
|
||||
IsOn: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *RuleSet) Init() error {
|
||||
this.hasRules = len(this.Rules) > 0
|
||||
if this.hasRules {
|
||||
for _, rule := range this.Rules {
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *RuleSet) AddRule(rule ...*Rule) {
|
||||
this.Rules = append(this.Rules, rule...)
|
||||
}
|
||||
|
||||
func (this *RuleSet) MatchRequest(req *requests.Request) (b bool, err error) {
|
||||
if !this.hasRules {
|
||||
return false, nil
|
||||
}
|
||||
switch this.Connector {
|
||||
case RuleConnectorAnd:
|
||||
for _, rule := range this.Rules {
|
||||
b1, err1 := rule.MatchRequest(req)
|
||||
if err1 != nil {
|
||||
return false, err1
|
||||
}
|
||||
if !b1 {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
case RuleConnectorOr:
|
||||
for _, rule := range this.Rules {
|
||||
b1, err1 := rule.MatchRequest(req)
|
||||
if err1 != nil {
|
||||
return false, err1
|
||||
}
|
||||
if b1 {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
default: // same as And
|
||||
for _, rule := range this.Rules {
|
||||
b1, err1 := rule.MatchRequest(req)
|
||||
if err1 != nil {
|
||||
return false, err1
|
||||
}
|
||||
if !b1 {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (this *RuleSet) MatchResponse(req *requests.Request, resp *requests.Response) (b bool, err error) {
|
||||
if !this.hasRules {
|
||||
return false, nil
|
||||
}
|
||||
switch this.Connector {
|
||||
case RuleConnectorAnd:
|
||||
for _, rule := range this.Rules {
|
||||
b1, err1 := rule.MatchResponse(req, resp)
|
||||
if err1 != nil {
|
||||
return false, err1
|
||||
}
|
||||
if !b1 {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
case RuleConnectorOr:
|
||||
for _, rule := range this.Rules {
|
||||
b1, err1 := rule.MatchResponse(req, resp)
|
||||
if err1 != nil {
|
||||
return false, err1
|
||||
}
|
||||
if b1 {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
default: // same as And
|
||||
for _, rule := range this.Rules {
|
||||
b1, err1 := rule.MatchResponse(req, resp)
|
||||
if err1 != nil {
|
||||
return false, err1
|
||||
}
|
||||
if !b1 {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
return
|
||||
}
|
||||
180
internal/waf/rule_set_test.go
Normal file
180
internal/waf/rule_set_test.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"github.com/dchest/siphash"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRuleSet_MatchRequest(t *testing.T) {
|
||||
set := NewRuleSet()
|
||||
set.Connector = RuleConnectorAnd
|
||||
|
||||
set.Rules = []*Rule{
|
||||
{
|
||||
Param: "${arg.name}",
|
||||
Operator: RuleOperatorEqString,
|
||||
Value: "lu",
|
||||
},
|
||||
{
|
||||
Param: "${arg.age}",
|
||||
Operator: RuleOperatorEq,
|
||||
Value: "20",
|
||||
},
|
||||
}
|
||||
|
||||
err := set.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rawReq, err := http.NewRequest(http.MethodGet, "http://teaos.cn/hello?name=lu&age=20", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := requests.NewRequest(rawReq)
|
||||
t.Log(set.MatchRequest(req))
|
||||
}
|
||||
|
||||
func TestRuleSet_MatchRequest2(t *testing.T) {
|
||||
a := assert.NewAssertion(t)
|
||||
|
||||
set := NewRuleSet()
|
||||
set.Connector = RuleConnectorOr
|
||||
|
||||
set.Rules = []*Rule{
|
||||
{
|
||||
Param: "${arg.name}",
|
||||
Operator: RuleOperatorEqString,
|
||||
Value: "lu",
|
||||
},
|
||||
{
|
||||
Param: "${arg.age}",
|
||||
Operator: RuleOperatorEq,
|
||||
Value: "21",
|
||||
},
|
||||
}
|
||||
|
||||
err := set.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rawReq, err := http.NewRequest(http.MethodGet, "http://teaos.cn/hello?name=lu&age=20", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := requests.NewRequest(rawReq)
|
||||
a.IsTrue(set.MatchRequest(req))
|
||||
}
|
||||
|
||||
func BenchmarkRuleSet_MatchRequest(b *testing.B) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
|
||||
set := NewRuleSet()
|
||||
set.Connector = RuleConnectorOr
|
||||
|
||||
set.Rules = []*Rule{
|
||||
{
|
||||
Param: "${requestAll}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `(onmouseover|onmousemove|onmousedown|onmouseup|onerror|onload|onclick|ondblclick|onkeydown|onkeyup|onkeypress)\s*=`,
|
||||
},
|
||||
{
|
||||
Param: "${requestAll}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `\b(eval|system|exec|execute|passthru|shell_exec|phpinfo)\s*\(`,
|
||||
},
|
||||
{
|
||||
Param: "${arg.name}",
|
||||
Operator: RuleOperatorEqString,
|
||||
Value: "lu",
|
||||
},
|
||||
{
|
||||
Param: "${arg.age}",
|
||||
Operator: RuleOperatorEq,
|
||||
Value: "21",
|
||||
},
|
||||
}
|
||||
|
||||
err := set.Init()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
rawReq, err := http.NewRequest(http.MethodPost, "http://teaos.cn/hello?name=lu&age=20", bytes.NewBuffer(bytes.Repeat([]byte("HELLO"), 1024)))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
req := requests.NewRequest(rawReq)
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = set.MatchRequest(req)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRuleSet_MatchRequest_Regexp(b *testing.B) {
|
||||
runtime.GOMAXPROCS(1)
|
||||
|
||||
set := NewRuleSet()
|
||||
set.Connector = RuleConnectorOr
|
||||
|
||||
set.Rules = []*Rule{
|
||||
{
|
||||
Param: "${requestBody}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `\b(eval|system|exec|execute|passthru|shell_exec|phpinfo)\s*\(`,
|
||||
IsCaseInsensitive: false,
|
||||
},
|
||||
}
|
||||
|
||||
err := set.Init()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
rawReq, err := http.NewRequest(http.MethodPost, "http://teaos.cn/hello?name=lu&age=20", bytes.NewBuffer(bytes.Repeat([]byte("HELLO"), 2048)))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
req := requests.NewRequest(rawReq)
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = set.MatchRequest(req)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRuleSet_MatchRequest_Regexp2(b *testing.B) {
|
||||
reg, err := regexp.Compile(`(?iU)\b(eval|system|exec|execute|passthru|shell_exec|phpinfo)\b`)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
buf := bytes.Repeat([]byte(" HELLO "), 10240)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = reg.Match(buf)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRuleSet_MatchRequest_Regexp3(b *testing.B) {
|
||||
reg, err := regexp.Compile(`(?iU)^(eval|system|exec|execute|passthru|shell_exec|phpinfo)`)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
buf := bytes.Repeat([]byte(" HELLO "), 1024)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = reg.Match(buf)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHash(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = siphash.Hash(0, 0, bytes.Repeat([]byte("HELLO"), 10240))
|
||||
}
|
||||
}
|
||||
733
internal/waf/rule_test.go
Normal file
733
internal/waf/rule_test.go
Normal file
@@ -0,0 +1,733 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/checkpoints"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRule_Init_Single(t *testing.T) {
|
||||
rule := NewRule()
|
||||
rule.Param = "${arg.name}"
|
||||
rule.Operator = RuleOperatorEqString
|
||||
rule.Value = "lu"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(rule.singleParam, rule.singleCheckpoint)
|
||||
rawReq, err := http.NewRequest(http.MethodGet, "http://teaos.cn/hello?name=lu&age=20", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := requests.NewRequest(rawReq)
|
||||
t.Log(rule.MatchRequest(req))
|
||||
}
|
||||
|
||||
func TestRule_Init_Composite(t *testing.T) {
|
||||
rule := NewRule()
|
||||
rule.Param = "${arg.name} ${arg.age}"
|
||||
rule.Operator = RuleOperatorContains
|
||||
rule.Value = "lu"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(rule.singleParam, rule.singleCheckpoint)
|
||||
|
||||
rawReq, err := http.NewRequest(http.MethodGet, "http://teaos.cn/hello?name=lu&age=20", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := requests.NewRequest(rawReq)
|
||||
t.Log(rule.MatchRequest(req))
|
||||
}
|
||||
|
||||
func TestRule_Test(t *testing.T) {
|
||||
a := assert.NewAssertion(t)
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorGt
|
||||
rule.Value = "123"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(rule.Test("124"))
|
||||
a.IsFalse(rule.Test("123"))
|
||||
a.IsFalse(rule.Test("122"))
|
||||
a.IsFalse(rule.Test("abcdef"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorGte
|
||||
rule.Value = "123"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(rule.Test("124"))
|
||||
a.IsTrue(rule.Test("123"))
|
||||
a.IsFalse(rule.Test("122"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorLt
|
||||
rule.Value = "123"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(rule.Test("124"))
|
||||
a.IsFalse(rule.Test("123"))
|
||||
a.IsTrue(rule.Test("122"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorLte
|
||||
rule.Value = "123"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(rule.Test("124"))
|
||||
a.IsTrue(rule.Test("123"))
|
||||
a.IsTrue(rule.Test("122"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorEq
|
||||
rule.Value = "123"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(rule.Test("124"))
|
||||
a.IsTrue(rule.Test("123"))
|
||||
a.IsFalse(rule.Test("122"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorNeq
|
||||
rule.Value = "123"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(rule.Test("124"))
|
||||
a.IsFalse(rule.Test("123"))
|
||||
a.IsTrue(rule.Test("122"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorEqString
|
||||
rule.Value = "123"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(rule.Test("124"))
|
||||
a.IsTrue(rule.Test("123"))
|
||||
a.IsFalse(rule.Test("122"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorEqString
|
||||
rule.Value = "abc"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(rule.Test("ABC"))
|
||||
a.IsTrue(rule.Test("abc"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorEqString
|
||||
rule.IsCaseInsensitive = true
|
||||
rule.Value = "abc"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(rule.Test("ABC"))
|
||||
a.IsTrue(rule.Test("abc"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorNeqString
|
||||
rule.Value = "abc"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(rule.Test("124"))
|
||||
a.IsFalse(rule.Test("abc"))
|
||||
a.IsTrue(rule.Test("122"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorNeqString
|
||||
rule.IsCaseInsensitive = true
|
||||
rule.Value = "abc"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(rule.Test("ABC"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorMatch
|
||||
rule.Value = "^\\d+"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(rule.Test("123"))
|
||||
a.IsFalse(rule.Test("abc123"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorMatch
|
||||
rule.Value = "abc"
|
||||
rule.IsCaseInsensitive = true
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(rule.Test("ABC"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorMatch
|
||||
rule.Value = "^\\d+"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(rule.Test([]string{"123", "456", "abc"}))
|
||||
a.IsFalse(rule.Test([]string{"abc123"}))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorNotMatch
|
||||
rule.Value = "\\d+"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(rule.Test("123"))
|
||||
a.IsTrue(rule.Test("abc"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorNotMatch
|
||||
rule.Value = "abc"
|
||||
rule.IsCaseInsensitive = true
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(rule.Test("ABC"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorNotMatch
|
||||
rule.Value = "^\\d+"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(rule.Test([]string{"123", "456", "abc"}))
|
||||
a.IsTrue(rule.Test([]string{"abc123"}))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorMatch
|
||||
rule.Value = "^(?i)[a-z]+$"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(rule.Test("ABC"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorContains
|
||||
rule.Value = "Hello"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(rule.Test("Hello, World"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorContains
|
||||
rule.Value = "hello"
|
||||
rule.IsCaseInsensitive = true
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(rule.Test("Hello, World"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorContains
|
||||
rule.Value = "Hello"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(rule.Test([]string{"Hello", "World"}))
|
||||
a.IsTrue(rule.Test(maps.Map{
|
||||
"a": "World", "b": "Hello",
|
||||
}))
|
||||
a.IsFalse(rule.Test(maps.Map{
|
||||
"a": "World", "b": "Hello2",
|
||||
}))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorNotContains
|
||||
rule.Value = "Hello"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(rule.Test("Hello, World"))
|
||||
a.IsTrue(rule.Test("World"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorNotContains
|
||||
rule.Value = "hello"
|
||||
rule.IsCaseInsensitive = true
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(rule.Test("Hello, World"))
|
||||
a.IsTrue(rule.Test("World"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorPrefix
|
||||
rule.Value = "Hello"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(rule.Test("Hello, World"))
|
||||
a.IsFalse(rule.Test("World, Hello"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorPrefix
|
||||
rule.Value = "hello"
|
||||
rule.IsCaseInsensitive = true
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(rule.Test("Hello, World"))
|
||||
a.IsFalse(rule.Test("World, Hello"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorSuffix
|
||||
rule.Value = "Hello"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(rule.Test("Hello, World"))
|
||||
a.IsTrue(rule.Test("World, Hello"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorSuffix
|
||||
rule.Value = "hello"
|
||||
rule.IsCaseInsensitive = true
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(rule.Test("Hello, World"))
|
||||
a.IsTrue(rule.Test("World, Hello"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorHasKey
|
||||
rule.Value = "Hello"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(rule.Test("Hello, World"))
|
||||
a.IsTrue(rule.Test(maps.Map{
|
||||
"Hello": "World",
|
||||
}))
|
||||
a.IsFalse(rule.Test(maps.Map{
|
||||
"Hello1": "World",
|
||||
}))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorHasKey
|
||||
rule.Value = "hello"
|
||||
rule.IsCaseInsensitive = true
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(rule.Test("Hello, World"))
|
||||
a.IsTrue(rule.Test(maps.Map{
|
||||
"Hello": "World",
|
||||
}))
|
||||
a.IsFalse(rule.Test(maps.Map{
|
||||
"Hello1": "World",
|
||||
}))
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorHasKey
|
||||
rule.Value = "3"
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsFalse(rule.Test("Hello, World"))
|
||||
a.IsFalse(rule.Test(maps.Map{
|
||||
"Hello": "World",
|
||||
}))
|
||||
a.IsTrue(rule.Test([]int{1, 2, 3, 4}))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRule_MatchStar(t *testing.T) {
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Operator = RuleOperatorMatch
|
||||
rule.Value = `/\*(!|\x00)`
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(rule.Test("/*!"))
|
||||
t.Log(rule.Test(url.QueryEscape("/*!")))
|
||||
t.Log(url.QueryEscape("/*!"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRule_SetCheckpointFinder(t *testing.T) {
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Param = "${arg.abc}"
|
||||
rule.Operator = RuleOperatorMatch
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("%#v", rule.singleCheckpoint)
|
||||
}
|
||||
|
||||
{
|
||||
rule := NewRule()
|
||||
rule.Param = "${arg.abc}"
|
||||
rule.Operator = RuleOperatorMatch
|
||||
rule.checkpointFinder = func(prefix string) checkpoints.CheckpointInterface {
|
||||
return new(checkpoints.SampleRequestCheckpoint)
|
||||
}
|
||||
err := rule.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("%#v", rule.singleCheckpoint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRule_Version(t *testing.T) {
|
||||
a := assert.NewAssertion(t)
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorVersionRange,
|
||||
Value: `1.0,1.1`,
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsTrue(rule.Test("1.0"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorVersionRange,
|
||||
Value: `1.0,`,
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsTrue(rule.Test("1.0"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorVersionRange,
|
||||
Value: `,1.1`,
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsTrue(rule.Test("1.0"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorVersionRange,
|
||||
Value: `1.0,1.1`,
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsFalse(rule.Test("0.9"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorVersionRange,
|
||||
Value: `1.0`,
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsFalse(rule.Test("0.9"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorVersionRange,
|
||||
Value: `1.0`,
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsTrue(rule.Test("1.1"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRule_IP(t *testing.T) {
|
||||
a := assert.NewAssertion(t)
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorEqIP,
|
||||
Value: "hello",
|
||||
}
|
||||
a.IsNotNil(rule.Init())
|
||||
a.IsFalse(rule.Test("hello"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorEqIP,
|
||||
Value: "hello",
|
||||
}
|
||||
a.IsNotNil(rule.Init())
|
||||
a.IsFalse(rule.Test("192.168.1.100"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorEqIP,
|
||||
Value: "192.168.1.100",
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsTrue(rule.Test("192.168.1.100"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorGtIP,
|
||||
Value: "192.168.1.90",
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsTrue(rule.Test("192.168.1.100"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorGteIP,
|
||||
Value: "192.168.1.90",
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsTrue(rule.Test("192.168.1.100"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorLtIP,
|
||||
Value: "192.168.1.90",
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsTrue(rule.Test("192.168.1.80"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorLteIP,
|
||||
Value: "192.168.1.90",
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsTrue(rule.Test("192.168.0.100"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorIPRange,
|
||||
Value: "192.168.0.90,",
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsTrue(rule.Test("192.168.0.100"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorIPRange,
|
||||
Value: "192.168.0.90,192.168.1.100",
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsTrue(rule.Test("192.168.0.100"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorIPRange,
|
||||
Value: ",192.168.1.100",
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsTrue(rule.Test("192.168.0.100"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorIPRange,
|
||||
Value: "192.168.0.90,192.168.1.99",
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsFalse(rule.Test("192.168.1.100"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorIPRange,
|
||||
Value: "192.168.0.90/24",
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsFalse(rule.Test("192.168.1.100"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorIPRange,
|
||||
Value: "192.168.0.90/18",
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsTrue(rule.Test("192.168.1.100"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorIPRange,
|
||||
Value: "a/18",
|
||||
}
|
||||
a.IsNotNil(rule.Init())
|
||||
a.IsFalse(rule.Test("192.168.1.100"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorIPMod10,
|
||||
Value: "6",
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsTrue(rule.Test("192.168.1.100"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorIPMod100,
|
||||
Value: "76",
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsTrue(rule.Test("192.168.1.100"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorIPMod,
|
||||
Value: "10,6",
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsTrue(rule.Test("192.168.1.100"))
|
||||
}
|
||||
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorNotIPRange,
|
||||
Value: "192.168.0.90,192.168.1.100",
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsFalse(rule.Test("192.168.0.100"))
|
||||
}
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorNotIPRange,
|
||||
Value: "192.168.0.90,192.168.1.100",
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsTrue(rule.Test("192.168.2.100"))
|
||||
}
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorNotIPRange,
|
||||
Value: "192.168.0.90/8",
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsFalse(rule.Test("192.168.2.100"))
|
||||
}
|
||||
{
|
||||
rule := Rule{
|
||||
Operator: RuleOperatorNotIPRange,
|
||||
Value: "192.168.0.90/16",
|
||||
}
|
||||
a.IsNil(rule.Init())
|
||||
a.IsTrue(rule.Test("192.169.2.100"))
|
||||
}
|
||||
}
|
||||
492
internal/waf/template.go
Normal file
492
internal/waf/template.go
Normal file
@@ -0,0 +1,492 @@
|
||||
package waf
|
||||
|
||||
// 感谢以下规则来源:
|
||||
// - Janusec: https://www.janusec.com/
|
||||
func Template() *WAF {
|
||||
waf := NewWAF()
|
||||
waf.Id = "template"
|
||||
waf.IsOn = true
|
||||
|
||||
// black list
|
||||
{
|
||||
group := NewRuleGroup()
|
||||
group.IsOn = false
|
||||
group.IsInbound = true
|
||||
group.Name = "白名单"
|
||||
group.Code = "whiteList"
|
||||
group.Description = "在此名单中的IP地址可以直接跳过防火墙设置"
|
||||
|
||||
{
|
||||
|
||||
set := NewRuleSet()
|
||||
set.IsOn = true
|
||||
set.Name = "IP白名单"
|
||||
set.Code = "9001"
|
||||
set.Connector = RuleConnectorOr
|
||||
set.Action = ActionAllow
|
||||
set.AddRule(&Rule{
|
||||
Param: "${remoteAddr}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `127\.0\.0\.1|0\.0\.0\.0`,
|
||||
IsCaseInsensitive: false,
|
||||
})
|
||||
group.AddRuleSet(set)
|
||||
}
|
||||
|
||||
waf.AddRuleGroup(group)
|
||||
}
|
||||
|
||||
// black list
|
||||
{
|
||||
group := NewRuleGroup()
|
||||
group.IsOn = false
|
||||
group.IsInbound = true
|
||||
group.Name = "黑名单"
|
||||
group.Code = "blackList"
|
||||
group.Description = "在此名单中的IP地址直接阻止"
|
||||
|
||||
{
|
||||
|
||||
set := NewRuleSet()
|
||||
set.IsOn = true
|
||||
set.Name = "IP黑名单"
|
||||
set.Code = "10001"
|
||||
set.Connector = RuleConnectorOr
|
||||
set.Action = ActionBlock
|
||||
set.AddRule(&Rule{
|
||||
Param: "${remoteAddr}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `1\.1\.1\.1|2\.2\.2\.2`,
|
||||
IsCaseInsensitive: false,
|
||||
})
|
||||
group.AddRuleSet(set)
|
||||
}
|
||||
|
||||
waf.AddRuleGroup(group)
|
||||
}
|
||||
|
||||
// xss
|
||||
{
|
||||
group := NewRuleGroup()
|
||||
group.IsOn = true
|
||||
group.IsInbound = true
|
||||
group.Name = "XSS"
|
||||
group.Code = "xss"
|
||||
group.Description = "防跨站脚本攻击(Cross Site Scripting)"
|
||||
|
||||
{
|
||||
set := NewRuleSet()
|
||||
set.IsOn = true
|
||||
set.Name = "Javascript事件"
|
||||
set.Code = "1001"
|
||||
set.Connector = RuleConnectorOr
|
||||
set.Action = ActionBlock
|
||||
set.AddRule(&Rule{
|
||||
Param: "${requestURI}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `(onmouseover|onmousemove|onmousedown|onmouseup|onerror|onload|onclick|ondblclick|onkeydown|onkeyup|onkeypress)\s*=`, // TODO more keywords here
|
||||
IsCaseInsensitive: true,
|
||||
})
|
||||
group.AddRuleSet(set)
|
||||
}
|
||||
|
||||
{
|
||||
set := NewRuleSet()
|
||||
set.IsOn = true
|
||||
set.Name = "Javascript函数"
|
||||
set.Code = "1002"
|
||||
set.Connector = RuleConnectorOr
|
||||
set.Action = ActionBlock
|
||||
set.AddRule(&Rule{
|
||||
Param: "${requestURI}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `(alert|eval|prompt|confirm)\s*\(`, // TODO more keywords here
|
||||
IsCaseInsensitive: true,
|
||||
})
|
||||
group.AddRuleSet(set)
|
||||
}
|
||||
|
||||
{
|
||||
set := NewRuleSet()
|
||||
set.IsOn = true
|
||||
set.Name = "HTML标签"
|
||||
set.Code = "1003"
|
||||
set.Connector = RuleConnectorOr
|
||||
set.Action = ActionBlock
|
||||
set.AddRule(&Rule{
|
||||
Param: "${requestURI}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `<(script|iframe|link)`, // TODO more keywords here
|
||||
IsCaseInsensitive: true,
|
||||
})
|
||||
group.AddRuleSet(set)
|
||||
}
|
||||
|
||||
waf.AddRuleGroup(group)
|
||||
}
|
||||
|
||||
// upload
|
||||
{
|
||||
group := NewRuleGroup()
|
||||
group.IsOn = true
|
||||
group.IsInbound = true
|
||||
group.Name = "文件上传"
|
||||
group.Code = "upload"
|
||||
group.Description = "防止上传可执行脚本文件到服务器"
|
||||
|
||||
{
|
||||
set := NewRuleSet()
|
||||
set.IsOn = true
|
||||
set.Name = "上传文件扩展名"
|
||||
set.Code = "2001"
|
||||
set.Connector = RuleConnectorOr
|
||||
set.Action = ActionBlock
|
||||
set.AddRule(&Rule{
|
||||
Param: "${requestUpload.ext}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `\.(php|jsp|aspx|asp|exe|asa|rb|py)\b`, // TODO more keywords here
|
||||
IsCaseInsensitive: true,
|
||||
})
|
||||
group.AddRuleSet(set)
|
||||
}
|
||||
|
||||
waf.AddRuleGroup(group)
|
||||
}
|
||||
|
||||
// web shell
|
||||
{
|
||||
group := NewRuleGroup()
|
||||
group.IsOn = true
|
||||
group.IsInbound = true
|
||||
group.Name = "Web Shell"
|
||||
group.Code = "webShell"
|
||||
group.Description = "防止远程执行服务器命令"
|
||||
|
||||
{
|
||||
set := NewRuleSet()
|
||||
set.IsOn = true
|
||||
set.Name = "Web Shell"
|
||||
set.Code = "3001"
|
||||
set.Connector = RuleConnectorOr
|
||||
set.Action = ActionBlock
|
||||
set.AddRule(&Rule{
|
||||
Param: "${requestAll}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `\b(eval|system|exec|execute|passthru|shell_exec|phpinfo)\s*\(`, // TODO more keywords here
|
||||
IsCaseInsensitive: true,
|
||||
})
|
||||
group.AddRuleSet(set)
|
||||
}
|
||||
|
||||
waf.AddRuleGroup(group)
|
||||
}
|
||||
|
||||
// command injection
|
||||
{
|
||||
group := NewRuleGroup()
|
||||
group.IsOn = true
|
||||
group.IsInbound = true
|
||||
group.Name = "命令注入"
|
||||
group.Code = "commandInjection"
|
||||
|
||||
{
|
||||
set := NewRuleSet()
|
||||
set.IsOn = true
|
||||
set.Name = "命令注入"
|
||||
set.Code = "4001"
|
||||
set.Connector = RuleConnectorOr
|
||||
set.Action = ActionBlock
|
||||
set.AddRule(&Rule{
|
||||
Param: "${requestURI}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `\b(pwd|ls|ll|whoami|id|net\s+user)\s*$`, // TODO more keywords here
|
||||
IsCaseInsensitive: false,
|
||||
})
|
||||
set.AddRule(&Rule{
|
||||
Param: "${requestBody}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `\b(pwd|ls|ll|whoami|id|net\s+user)\s*$`, // TODO more keywords here
|
||||
IsCaseInsensitive: false,
|
||||
})
|
||||
group.AddRuleSet(set)
|
||||
}
|
||||
|
||||
waf.AddRuleGroup(group)
|
||||
}
|
||||
|
||||
// path traversal
|
||||
{
|
||||
group := NewRuleGroup()
|
||||
group.IsOn = true
|
||||
group.IsInbound = true
|
||||
group.Name = "路径穿越"
|
||||
group.Code = "pathTraversal"
|
||||
group.Description = "防止读取网站目录之外的其他系统文件"
|
||||
|
||||
{
|
||||
set := NewRuleSet()
|
||||
set.IsOn = true
|
||||
set.Name = "路径穿越"
|
||||
set.Code = "5001"
|
||||
set.Connector = RuleConnectorOr
|
||||
set.Action = ActionBlock
|
||||
set.AddRule(&Rule{
|
||||
Param: "${requestURI}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `((\.+)(/+)){2,}`, // TODO more keywords here
|
||||
IsCaseInsensitive: false,
|
||||
})
|
||||
group.AddRuleSet(set)
|
||||
}
|
||||
|
||||
waf.AddRuleGroup(group)
|
||||
}
|
||||
|
||||
// special dirs
|
||||
{
|
||||
group := NewRuleGroup()
|
||||
group.IsOn = true
|
||||
group.IsInbound = true
|
||||
group.Name = "特殊目录"
|
||||
group.Code = "denyDirs"
|
||||
group.Description = "防止通过Web访问到一些特殊目录"
|
||||
|
||||
{
|
||||
set := NewRuleSet()
|
||||
set.IsOn = true
|
||||
set.Name = "特殊目录"
|
||||
set.Code = "6001"
|
||||
set.Connector = RuleConnectorOr
|
||||
set.Action = ActionBlock
|
||||
set.AddRule(&Rule{
|
||||
Param: "${requestPath}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `/\.(git|svn|htaccess|idea)\b`, // TODO more keywords here
|
||||
IsCaseInsensitive: true,
|
||||
})
|
||||
group.AddRuleSet(set)
|
||||
}
|
||||
|
||||
waf.AddRuleGroup(group)
|
||||
}
|
||||
|
||||
// sql injection
|
||||
{
|
||||
group := NewRuleGroup()
|
||||
group.IsOn = true
|
||||
group.IsInbound = true
|
||||
group.Name = "SQL注入"
|
||||
group.Code = "sqlInjection"
|
||||
group.Description = "防止SQL注入漏洞"
|
||||
|
||||
{
|
||||
set := NewRuleSet()
|
||||
set.IsOn = true
|
||||
set.Name = "Union SQL Injection"
|
||||
set.Code = "7001"
|
||||
set.Connector = RuleConnectorOr
|
||||
set.Action = ActionBlock
|
||||
|
||||
set.AddRule(&Rule{
|
||||
Param: "${requestAll}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `union[\s/\*]+select`,
|
||||
IsCaseInsensitive: true,
|
||||
})
|
||||
|
||||
group.AddRuleSet(set)
|
||||
}
|
||||
|
||||
{
|
||||
set := NewRuleSet()
|
||||
set.IsOn = true
|
||||
set.Name = "SQL注释"
|
||||
set.Code = "7002"
|
||||
set.Connector = RuleConnectorOr
|
||||
set.Action = ActionBlock
|
||||
|
||||
set.AddRule(&Rule{
|
||||
Param: "${requestAll}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `/\*(!|\x00)`,
|
||||
IsCaseInsensitive: true,
|
||||
})
|
||||
|
||||
group.AddRuleSet(set)
|
||||
}
|
||||
|
||||
{
|
||||
set := NewRuleSet()
|
||||
set.IsOn = true
|
||||
set.Name = "SQL条件"
|
||||
set.Code = "7003"
|
||||
set.Connector = RuleConnectorOr
|
||||
set.Action = ActionBlock
|
||||
|
||||
set.AddRule(&Rule{
|
||||
Param: "${requestAll}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `\s(and|or|rlike)\s+(if|updatexml)\s*\(`,
|
||||
IsCaseInsensitive: true,
|
||||
})
|
||||
set.AddRule(&Rule{
|
||||
Param: "${requestAll}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `\s+(and|or|rlike)\s+(select|case)\s+`,
|
||||
IsCaseInsensitive: true,
|
||||
})
|
||||
set.AddRule(&Rule{
|
||||
Param: "${requestAll}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `\s+(and|or|procedure)\s+[\w\p{L}]+\s*=\s*[\w\p{L}]+(\s|$|--|#)`,
|
||||
IsCaseInsensitive: true,
|
||||
})
|
||||
set.AddRule(&Rule{
|
||||
Param: "${requestAll}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `\(\s*case\s+when\s+[\w\p{L}]+\s*=\s*[\w\p{L}]+\s+then\s+`,
|
||||
IsCaseInsensitive: true,
|
||||
})
|
||||
|
||||
group.AddRuleSet(set)
|
||||
}
|
||||
|
||||
{
|
||||
set := NewRuleSet()
|
||||
set.IsOn = true
|
||||
set.Name = "SQL函数"
|
||||
set.Code = "7004"
|
||||
set.Connector = RuleConnectorOr
|
||||
set.Action = ActionBlock
|
||||
|
||||
set.AddRule(&Rule{
|
||||
Param: "${requestAll}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `(updatexml|extractvalue|ascii|ord|char|chr|count|concat|rand|floor|substr|length|len|user|database|benchmark|analyse)\s*\(`,
|
||||
IsCaseInsensitive: true,
|
||||
})
|
||||
|
||||
group.AddRuleSet(set)
|
||||
}
|
||||
|
||||
{
|
||||
set := NewRuleSet()
|
||||
set.IsOn = true
|
||||
set.Name = "SQL附加语句"
|
||||
set.Code = "7005"
|
||||
set.Connector = RuleConnectorOr
|
||||
set.Action = ActionBlock
|
||||
|
||||
set.AddRule(&Rule{
|
||||
Param: "${requestAll}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `;\s*(declare|use|drop|create|exec|delete|update|insert)\s`,
|
||||
IsCaseInsensitive: true,
|
||||
})
|
||||
|
||||
group.AddRuleSet(set)
|
||||
}
|
||||
|
||||
waf.AddRuleGroup(group)
|
||||
}
|
||||
|
||||
// bot
|
||||
{
|
||||
group := NewRuleGroup()
|
||||
group.IsOn = false
|
||||
group.IsInbound = true
|
||||
group.Name = "网络爬虫"
|
||||
group.Code = "bot"
|
||||
group.Description = "禁止一些网络爬虫"
|
||||
|
||||
{
|
||||
set := NewRuleSet()
|
||||
set.IsOn = true
|
||||
set.Name = "常见网络爬虫"
|
||||
set.Code = "20001"
|
||||
set.Connector = RuleConnectorOr
|
||||
set.Action = ActionBlock
|
||||
|
||||
set.AddRule(&Rule{
|
||||
Param: "${userAgent}",
|
||||
Operator: RuleOperatorMatch,
|
||||
Value: `Googlebot|AdsBot|bingbot|BingPreview|facebookexternalhit|Slurp|Sogou|proximic|Baiduspider|yandex|twitterbot|spider|python`,
|
||||
IsCaseInsensitive: true,
|
||||
})
|
||||
|
||||
group.AddRuleSet(set)
|
||||
}
|
||||
|
||||
waf.AddRuleGroup(group)
|
||||
}
|
||||
|
||||
// cc
|
||||
{
|
||||
group := NewRuleGroup()
|
||||
group.IsOn = false
|
||||
group.IsInbound = true
|
||||
group.Name = "CC攻击"
|
||||
group.Description = "Challenge Collapsar,防止短时间大量请求涌入,请谨慎开启和设置"
|
||||
group.Code = "cc"
|
||||
|
||||
{
|
||||
set := NewRuleSet()
|
||||
set.IsOn = true
|
||||
set.Name = "CC请求数"
|
||||
set.Description = "限制单IP在一定时间内的请求数"
|
||||
set.Code = "8001"
|
||||
set.Connector = RuleConnectorAnd
|
||||
set.Action = ActionBlock
|
||||
set.AddRule(&Rule{
|
||||
Param: "${cc.requests}",
|
||||
Operator: RuleOperatorGt,
|
||||
Value: "1000",
|
||||
CheckpointOptions: map[string]interface{}{
|
||||
"period": "60",
|
||||
},
|
||||
IsCaseInsensitive: false,
|
||||
})
|
||||
set.AddRule(&Rule{
|
||||
Param: "${remoteAddr}",
|
||||
Operator: RuleOperatorNotIPRange,
|
||||
Value: `127.0.0.1/8`,
|
||||
IsCaseInsensitive: false,
|
||||
})
|
||||
set.AddRule(&Rule{
|
||||
Param: "${remoteAddr}",
|
||||
Operator: RuleOperatorNotIPRange,
|
||||
Value: `192.168.0.1/16`,
|
||||
IsCaseInsensitive: false,
|
||||
})
|
||||
set.AddRule(&Rule{
|
||||
Param: "${remoteAddr}",
|
||||
Operator: RuleOperatorNotIPRange,
|
||||
Value: `10.0.0.1/8`,
|
||||
IsCaseInsensitive: false,
|
||||
})
|
||||
set.AddRule(&Rule{
|
||||
Param: "${remoteAddr}",
|
||||
Operator: RuleOperatorNotIPRange,
|
||||
Value: `172.16.0.1/12`,
|
||||
IsCaseInsensitive: false,
|
||||
})
|
||||
|
||||
group.AddRuleSet(set)
|
||||
}
|
||||
|
||||
waf.AddRuleGroup(group)
|
||||
}
|
||||
|
||||
// custom
|
||||
{
|
||||
group := NewRuleGroup()
|
||||
group.IsOn = true
|
||||
group.IsInbound = true
|
||||
group.Name = "自定义规则分组"
|
||||
group.Description = "我的自定义规则分组,可以将自定义的规则放在这个分组下"
|
||||
group.Code = "custom"
|
||||
waf.AddRuleGroup(group)
|
||||
}
|
||||
|
||||
return waf
|
||||
}
|
||||
352
internal/waf/template_test.go
Normal file
352
internal/waf/template_test.go
Normal file
@@ -0,0 +1,352 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"github.com/iwind/TeaGo/lists"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test_Template(t *testing.T) {
|
||||
a := assert.NewAssertion(t)
|
||||
|
||||
template := Template()
|
||||
err := template.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
template.OnAction(func(action ActionString) (goNext bool) {
|
||||
return action != ActionBlock
|
||||
})
|
||||
|
||||
testTemplate1001(a, t, template)
|
||||
testTemplate1002(a, t, template)
|
||||
testTemplate1003(a, t, template)
|
||||
testTemplate2001(a, t, template)
|
||||
testTemplate3001(a, t, template)
|
||||
testTemplate4001(a, t, template)
|
||||
testTemplate5001(a, t, template)
|
||||
testTemplate6001(a, t, template)
|
||||
testTemplate7001(a, t, template)
|
||||
testTemplate20001(a, t, template)
|
||||
}
|
||||
|
||||
func Test_Template2(t *testing.T) {
|
||||
reader := bytes.NewReader([]byte(strings.Repeat("HELLO", 1024)))
|
||||
req, err := http.NewRequest(http.MethodGet, "http://example.com/index.php?id=123", reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
waf := Template()
|
||||
err = waf.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
goNext, _, set, err := waf.MatchRequest(req, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(time.Since(now).Seconds()*1000, "ms")
|
||||
|
||||
if goNext {
|
||||
t.Log("ok")
|
||||
return
|
||||
}
|
||||
|
||||
logs.PrintAsJSON(set, t)
|
||||
}
|
||||
|
||||
func BenchmarkTemplate(b *testing.B) {
|
||||
waf := Template()
|
||||
err := waf.Init()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
reader := bytes.NewReader([]byte(strings.Repeat("Hello", 1024)))
|
||||
req, err := http.NewRequest(http.MethodGet, "http://example.com/index.php?id=123", reader)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
_, _, _, _ = waf.MatchRequest(req, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func testTemplate1001(a *assert.Assertion, t *testing.T, template *WAF) {
|
||||
req, err := http.NewRequest(http.MethodGet, "http://example.com/index.php?id=onmousedown%3D123", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, result, err := template.MatchRequest(req, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsNotNil(result)
|
||||
if result != nil {
|
||||
a.IsTrue(result.Code == "1001")
|
||||
}
|
||||
}
|
||||
|
||||
func testTemplate1002(a *assert.Assertion, t *testing.T, template *WAF) {
|
||||
req, err := http.NewRequest(http.MethodGet, "http://example.com/index.php?id=eval%28", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, result, err := template.MatchRequest(req, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsNotNil(result)
|
||||
if result != nil {
|
||||
a.IsTrue(result.Code == "1002")
|
||||
}
|
||||
}
|
||||
|
||||
func testTemplate1003(a *assert.Assertion, t *testing.T, template *WAF) {
|
||||
req, err := http.NewRequest(http.MethodGet, "http://example.com/index.php?id=<script src=\"123.js\">", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, result, err := template.MatchRequest(req, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsNotNil(result)
|
||||
if result != nil {
|
||||
a.IsTrue(result.Code == "1003")
|
||||
}
|
||||
}
|
||||
|
||||
func testTemplate2001(a *assert.Assertion, t *testing.T, template *WAF) {
|
||||
body := bytes.NewBuffer([]byte{})
|
||||
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
{
|
||||
part, err := writer.CreateFormField("name")
|
||||
if err == nil {
|
||||
_, _ = part.Write([]byte("lu"))
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
part, err := writer.CreateFormField("age")
|
||||
if err == nil {
|
||||
_, _ = part.Write([]byte("20"))
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
part, err := writer.CreateFormFile("myFile", "hello.txt")
|
||||
if err == nil {
|
||||
_, _ = part.Write([]byte("Hello, World!"))
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
part, err := writer.CreateFormFile("myFile2", "hello.PHP")
|
||||
if err == nil {
|
||||
_, _ = part.Write([]byte("Hello, World, PHP!"))
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
part, err := writer.CreateFormFile("myFile3", "hello.asp")
|
||||
if err == nil {
|
||||
_, _ = part.Write([]byte("Hello, World, ASP Pages!"))
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
part, err := writer.CreateFormFile("myFile4", "hello.asp")
|
||||
if err == nil {
|
||||
_, _ = part.Write([]byte("Hello, World, ASP Pages!"))
|
||||
}
|
||||
}
|
||||
|
||||
_ = writer.Close()
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "http://teaos.cn/", body)
|
||||
if err != nil {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
|
||||
_, _, result, err := template.MatchRequest(req, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsNotNil(result)
|
||||
if result != nil {
|
||||
a.IsTrue(result.Code == "2001")
|
||||
}
|
||||
}
|
||||
|
||||
func testTemplate3001(a *assert.Assertion, t *testing.T, template *WAF) {
|
||||
req, err := http.NewRequest(http.MethodPost, "http://example.com/index.php?exec1+(", bytes.NewReader([]byte("exec('rm -rf /hello');")))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, result, err := template.MatchRequest(req, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsNotNil(result)
|
||||
if result != nil {
|
||||
a.IsTrue(result.Code == "3001")
|
||||
}
|
||||
}
|
||||
|
||||
func testTemplate4001(a *assert.Assertion, t *testing.T, template *WAF) {
|
||||
req, err := http.NewRequest(http.MethodPost, "http://example.com/index.php?whoami", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, result, err := template.MatchRequest(req, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsNotNil(result)
|
||||
if result != nil {
|
||||
a.IsTrue(result.Code == "4001")
|
||||
}
|
||||
}
|
||||
|
||||
func testTemplate5001(a *assert.Assertion, t *testing.T, template *WAF) {
|
||||
{
|
||||
req, err := http.NewRequest(http.MethodPost, "http://example.com/.././..", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, result, err := template.MatchRequest(req, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsNotNil(result)
|
||||
if result != nil {
|
||||
a.IsTrue(result.Code == "5001")
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
req, err := http.NewRequest(http.MethodPost, "http://example.com/..///./", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, result, err := template.MatchRequest(req, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsNotNil(result)
|
||||
if result != nil {
|
||||
a.IsTrue(result.Code == "5001")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testTemplate6001(a *assert.Assertion, t *testing.T, template *WAF) {
|
||||
{
|
||||
req, err := http.NewRequest(http.MethodPost, "http://example.com/.svn/123.txt", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, result, err := template.MatchRequest(req, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsNotNil(result)
|
||||
if result != nil {
|
||||
a.IsTrue(result.Code == "6001")
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
req, err := http.NewRequest(http.MethodPost, "http://example.com/123.git", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, result, err := template.MatchRequest(req, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsNil(result)
|
||||
}
|
||||
}
|
||||
|
||||
func testTemplate7001(a *assert.Assertion, t *testing.T, template *WAF) {
|
||||
for _, id := range []string{
|
||||
"union select",
|
||||
" and if(",
|
||||
"/*!",
|
||||
" and select ",
|
||||
" and id=123 ",
|
||||
"(case when a=1 then ",
|
||||
"updatexml (",
|
||||
"; delete from table",
|
||||
} {
|
||||
req, err := http.NewRequest(http.MethodPost, "http://example.com/?id="+url.QueryEscape(id), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, result, err := template.MatchRequest(req, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsNotNil(result)
|
||||
if result != nil {
|
||||
a.IsTrue(lists.ContainsAny([]string{"7001", "7002", "7003", "7004", "7005"}, result.Code))
|
||||
} else {
|
||||
t.Log("break:", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testTemplate20001(a *assert.Assertion, t *testing.T, template *WAF) {
|
||||
// enable bot rule set
|
||||
for _, g := range template.Inbound {
|
||||
if g.Code == "bot" {
|
||||
g.On = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, bot := range []string{
|
||||
"Googlebot",
|
||||
"AdsBot",
|
||||
"bingbot",
|
||||
"BingPreview",
|
||||
"facebookexternalhit",
|
||||
"Slurp",
|
||||
"Sogou",
|
||||
"Baiduspider http://baidu.com",
|
||||
} {
|
||||
req, err := http.NewRequest(http.MethodPost, "http://example.com/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("User-Agent", bot)
|
||||
_, _, result, err := template.MatchRequest(req, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsNotNil(result)
|
||||
if result != nil {
|
||||
a.IsTrue(lists.ContainsAny([]string{"20001"}, result.Code))
|
||||
} else {
|
||||
t.Log("break:", bot)
|
||||
}
|
||||
}
|
||||
}
|
||||
56
internal/waf/utils/utils.go
Normal file
56
internal/waf/utils/utils.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/grids"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils"
|
||||
"github.com/dchest/siphash"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var grid = grids.NewGrid(32, grids.NewLimitCountOpt(1000_0000))
|
||||
|
||||
// 正则表达式匹配字符串,并缓存结果
|
||||
func MatchStringCache(regex *regexp.Regexp, s string) bool {
|
||||
// 如果长度超过4096,大概率是不能重用的
|
||||
if len(s) > 4096 {
|
||||
return regex.MatchString(s)
|
||||
}
|
||||
|
||||
hash := siphash.Hash(0, 0, utils.UnsafeStringToBytes(s))
|
||||
key := []byte(fmt.Sprintf("%p_", regex) + strconv.FormatUint(hash, 10))
|
||||
item := grid.Read(key)
|
||||
if item != nil {
|
||||
return item.ValueInt64 == 1
|
||||
}
|
||||
b := regex.MatchString(s)
|
||||
if b {
|
||||
grid.WriteInt64(key, 1, 1800)
|
||||
} else {
|
||||
grid.WriteInt64(key, 0, 1800)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// 正则表达式匹配字节slice,并缓存结果
|
||||
func MatchBytesCache(regex *regexp.Regexp, byteSlice []byte) bool {
|
||||
// 如果长度超过4096,大概率是不能重用的
|
||||
if len(byteSlice) > 4096 {
|
||||
return regex.Match(byteSlice)
|
||||
}
|
||||
|
||||
hash := siphash.Hash(0, 0, byteSlice)
|
||||
key := []byte(fmt.Sprintf("%p_", regex) + strconv.FormatUint(hash, 10))
|
||||
item := grid.Read(key)
|
||||
if item != nil {
|
||||
return item.ValueInt64 == 1
|
||||
}
|
||||
b := regex.Match(byteSlice)
|
||||
if b {
|
||||
grid.WriteInt64(key, 1, 1800)
|
||||
} else {
|
||||
grid.WriteInt64(key, 0, 1800)
|
||||
}
|
||||
return b
|
||||
}
|
||||
57
internal/waf/utils/utils_test.go
Normal file
57
internal/waf/utils/utils_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMatchStringCache(t *testing.T) {
|
||||
regex := regexp.MustCompile(`\d+`)
|
||||
t.Log(MatchStringCache(regex, "123"))
|
||||
t.Log(MatchStringCache(regex, "123"))
|
||||
t.Log(MatchStringCache(regex, "123"))
|
||||
}
|
||||
|
||||
func TestMatchBytesCache(t *testing.T) {
|
||||
regex := regexp.MustCompile(`\d+`)
|
||||
t.Log(MatchBytesCache(regex, []byte("123")))
|
||||
t.Log(MatchBytesCache(regex, []byte("123")))
|
||||
t.Log(MatchBytesCache(regex, []byte("123")))
|
||||
}
|
||||
|
||||
func TestMatchRemoteCache(t *testing.T) {
|
||||
client := http.Client{}
|
||||
for i := 0; i < 200_0000; i++ {
|
||||
req, err := http.NewRequest(http.MethodGet, "http://192.168.2.30:8882/?arg="+strconv.Itoa(i), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "GoTest/"+strconv.Itoa(i))
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMatchStringCache(b *testing.B) {
|
||||
data := strings.Repeat("HELLO", 512)
|
||||
regex := regexp.MustCompile(`(?iU)\b(eval|system|exec|execute|passthru|shell_exec|phpinfo)\b`)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = MatchStringCache(regex, data)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMatchStringCache_WithoutCache(b *testing.B) {
|
||||
data := strings.Repeat("HELLO", 512)
|
||||
regex := regexp.MustCompile(`(?iU)\b(eval|system|exec|execute|passthru|shell_exec|phpinfo)\b`)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = regex.MatchString(data)
|
||||
}
|
||||
}
|
||||
443
internal/waf/waf.go
Normal file
443
internal/waf/waf.go
Normal file
@@ -0,0 +1,443 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/checkpoints"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf/requests"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/files"
|
||||
"github.com/iwind/TeaGo/utils/string"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type WAF struct {
|
||||
Id string `yaml:"id" json:"id"`
|
||||
IsOn bool `yaml:"isOn" json:"isOn"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Inbound []*RuleGroup `yaml:"inbound" json:"inbound"`
|
||||
Outbound []*RuleGroup `yaml:"outbound" json:"outbound"`
|
||||
CreatedVersion string `yaml:"createdVersion" json:"createdVersion"`
|
||||
|
||||
ActionBlock *BlockAction `yaml:"actionBlock" json:"actionBlock"` // action block config
|
||||
|
||||
IPTables []*IPTable `yaml:"ipTables" json:"ipTables"` // IP table list
|
||||
|
||||
hasInboundRules bool
|
||||
hasOutboundRules bool
|
||||
onActionCallback func(action ActionString) (goNext bool)
|
||||
|
||||
checkpointsMap map[string]checkpoints.CheckpointInterface // prefix => checkpoint
|
||||
}
|
||||
|
||||
func NewWAF() *WAF {
|
||||
return &WAF{
|
||||
Id: stringutil.Rand(16),
|
||||
IsOn: true,
|
||||
}
|
||||
}
|
||||
|
||||
func NewWAFFromFile(path string) (waf *WAF, err error) {
|
||||
if len(path) == 0 {
|
||||
return nil, errors.New("'path' should not be empty")
|
||||
}
|
||||
file := files.NewFile(path)
|
||||
if !file.Exists() {
|
||||
return nil, errors.New("'" + path + "' not exist")
|
||||
}
|
||||
|
||||
reader, err := file.Reader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = reader.Close()
|
||||
}()
|
||||
|
||||
waf = &WAF{}
|
||||
err = reader.ReadYAML(waf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return waf, nil
|
||||
}
|
||||
|
||||
func (this *WAF) Init() error {
|
||||
// checkpoint
|
||||
this.checkpointsMap = map[string]checkpoints.CheckpointInterface{}
|
||||
for _, def := range checkpoints.AllCheckpoints {
|
||||
instance := reflect.New(reflect.Indirect(reflect.ValueOf(def.Instance)).Type()).Interface().(checkpoints.CheckpointInterface)
|
||||
instance.Init()
|
||||
this.checkpointsMap[def.Prefix] = instance
|
||||
}
|
||||
|
||||
// rules
|
||||
this.hasInboundRules = len(this.Inbound) > 0
|
||||
this.hasOutboundRules = len(this.Outbound) > 0
|
||||
|
||||
if this.hasInboundRules {
|
||||
for _, group := range this.Inbound {
|
||||
// finder
|
||||
for _, set := range group.RuleSets {
|
||||
for _, rule := range set.Rules {
|
||||
rule.SetCheckpointFinder(this.FindCheckpointInstance)
|
||||
}
|
||||
}
|
||||
|
||||
err := group.Init()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if this.hasOutboundRules {
|
||||
for _, group := range this.Outbound {
|
||||
// finder
|
||||
for _, set := range group.RuleSets {
|
||||
for _, rule := range set.Rules {
|
||||
rule.SetCheckpointFinder(this.FindCheckpointInstance)
|
||||
}
|
||||
}
|
||||
|
||||
err := group.Init()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *WAF) AddRuleGroup(ruleGroup *RuleGroup) {
|
||||
if ruleGroup.IsInbound {
|
||||
this.Inbound = append(this.Inbound, ruleGroup)
|
||||
} else {
|
||||
this.Outbound = append(this.Outbound, ruleGroup)
|
||||
}
|
||||
}
|
||||
|
||||
func (this *WAF) RemoveRuleGroup(ruleGroupId string) {
|
||||
if len(ruleGroupId) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
{
|
||||
result := []*RuleGroup{}
|
||||
for _, group := range this.Inbound {
|
||||
if group.Id == ruleGroupId {
|
||||
continue
|
||||
}
|
||||
result = append(result, group)
|
||||
}
|
||||
this.Inbound = result
|
||||
}
|
||||
|
||||
{
|
||||
result := []*RuleGroup{}
|
||||
for _, group := range this.Outbound {
|
||||
if group.Id == ruleGroupId {
|
||||
continue
|
||||
}
|
||||
result = append(result, group)
|
||||
}
|
||||
this.Outbound = result
|
||||
}
|
||||
}
|
||||
|
||||
func (this *WAF) FindRuleGroup(ruleGroupId string) *RuleGroup {
|
||||
if len(ruleGroupId) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, group := range this.Inbound {
|
||||
if group.Id == ruleGroupId {
|
||||
return group
|
||||
}
|
||||
}
|
||||
for _, group := range this.Outbound {
|
||||
if group.Id == ruleGroupId {
|
||||
return group
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *WAF) FindRuleGroupWithCode(ruleGroupCode string) *RuleGroup {
|
||||
if len(ruleGroupCode) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, group := range this.Inbound {
|
||||
if group.Code == ruleGroupCode {
|
||||
return group
|
||||
}
|
||||
}
|
||||
for _, group := range this.Outbound {
|
||||
if group.Code == ruleGroupCode {
|
||||
return group
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *WAF) MoveInboundRuleGroup(fromIndex int, toIndex int) {
|
||||
if fromIndex < 0 || fromIndex >= len(this.Inbound) {
|
||||
return
|
||||
}
|
||||
if toIndex < 0 || toIndex >= len(this.Inbound) {
|
||||
return
|
||||
}
|
||||
if fromIndex == toIndex {
|
||||
return
|
||||
}
|
||||
|
||||
group := this.Inbound[fromIndex]
|
||||
result := []*RuleGroup{}
|
||||
for i := 0; i < len(this.Inbound); i++ {
|
||||
if i == fromIndex {
|
||||
continue
|
||||
}
|
||||
if fromIndex > toIndex && i == toIndex {
|
||||
result = append(result, group)
|
||||
}
|
||||
result = append(result, this.Inbound[i])
|
||||
if fromIndex < toIndex && i == toIndex {
|
||||
result = append(result, group)
|
||||
}
|
||||
}
|
||||
|
||||
this.Inbound = result
|
||||
}
|
||||
|
||||
func (this *WAF) MoveOutboundRuleGroup(fromIndex int, toIndex int) {
|
||||
if fromIndex < 0 || fromIndex >= len(this.Outbound) {
|
||||
return
|
||||
}
|
||||
if toIndex < 0 || toIndex >= len(this.Outbound) {
|
||||
return
|
||||
}
|
||||
if fromIndex == toIndex {
|
||||
return
|
||||
}
|
||||
|
||||
group := this.Outbound[fromIndex]
|
||||
result := []*RuleGroup{}
|
||||
for i := 0; i < len(this.Outbound); i++ {
|
||||
if i == fromIndex {
|
||||
continue
|
||||
}
|
||||
if fromIndex > toIndex && i == toIndex {
|
||||
result = append(result, group)
|
||||
}
|
||||
result = append(result, this.Outbound[i])
|
||||
if fromIndex < toIndex && i == toIndex {
|
||||
result = append(result, group)
|
||||
}
|
||||
}
|
||||
|
||||
this.Outbound = result
|
||||
}
|
||||
|
||||
func (this *WAF) MatchRequest(rawReq *http.Request, writer http.ResponseWriter) (goNext bool, group *RuleGroup, set *RuleSet, err error) {
|
||||
if !this.hasInboundRules {
|
||||
return true, nil, nil, nil
|
||||
}
|
||||
|
||||
req := requests.NewRequest(rawReq)
|
||||
|
||||
// validate captcha
|
||||
if rawReq.URL.Path == "/WAFCAPTCHA" {
|
||||
captchaValidator.Run(req, writer)
|
||||
return
|
||||
}
|
||||
|
||||
// match rules
|
||||
for _, group := range this.Inbound {
|
||||
if !group.IsOn {
|
||||
continue
|
||||
}
|
||||
b, set, err := group.MatchRequest(req)
|
||||
if err != nil {
|
||||
return true, nil, nil, err
|
||||
}
|
||||
if b {
|
||||
if this.onActionCallback == nil {
|
||||
if set.Action == ActionBlock && this.ActionBlock != nil {
|
||||
return this.ActionBlock.Perform(this, req, writer), group, set, nil
|
||||
} else {
|
||||
actionObject := FindActionInstance(set.Action, set.ActionOptions)
|
||||
if actionObject == nil {
|
||||
return true, group, set, errors.New("no action called '" + set.Action + "'")
|
||||
}
|
||||
goNext := actionObject.Perform(this, req, writer)
|
||||
return goNext, group, set, nil
|
||||
}
|
||||
} else {
|
||||
goNext = this.onActionCallback(set.Action)
|
||||
}
|
||||
return goNext, group, set, nil
|
||||
}
|
||||
}
|
||||
return true, nil, nil, nil
|
||||
}
|
||||
|
||||
func (this *WAF) MatchResponse(rawReq *http.Request, rawResp *http.Response, writer http.ResponseWriter) (goNext bool, group *RuleGroup, set *RuleSet, err error) {
|
||||
if !this.hasOutboundRules {
|
||||
return true, nil, nil, nil
|
||||
}
|
||||
req := requests.NewRequest(rawReq)
|
||||
resp := requests.NewResponse(rawResp)
|
||||
for _, group := range this.Outbound {
|
||||
if !group.IsOn {
|
||||
continue
|
||||
}
|
||||
b, set, err := group.MatchResponse(req, resp)
|
||||
if err != nil {
|
||||
return true, nil, nil, err
|
||||
}
|
||||
if b {
|
||||
if this.onActionCallback == nil {
|
||||
if set.Action == ActionBlock && this.ActionBlock != nil {
|
||||
return this.ActionBlock.Perform(this, req, writer), group, set, nil
|
||||
} else {
|
||||
actionObject := FindActionInstance(set.Action, set.ActionOptions)
|
||||
if actionObject == nil {
|
||||
return true, group, set, errors.New("no action called '" + set.Action + "'")
|
||||
}
|
||||
goNext := actionObject.Perform(this, req, writer)
|
||||
return goNext, group, set, nil
|
||||
}
|
||||
} else {
|
||||
goNext = this.onActionCallback(set.Action)
|
||||
}
|
||||
return goNext, group, set, nil
|
||||
}
|
||||
}
|
||||
return true, nil, nil, nil
|
||||
}
|
||||
|
||||
// save to file path
|
||||
func (this *WAF) Save(path string) error {
|
||||
if len(path) == 0 {
|
||||
return errors.New("path should not be empty")
|
||||
}
|
||||
if len(this.CreatedVersion) == 0 {
|
||||
this.CreatedVersion = teaconst.Version
|
||||
}
|
||||
data, err := yaml.Marshal(this)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(path, data, 0644)
|
||||
}
|
||||
|
||||
func (this *WAF) ContainsGroupCode(code string) bool {
|
||||
if len(code) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, group := range this.Inbound {
|
||||
if group.Code == code {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, group := range this.Outbound {
|
||||
if group.Code == code {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (this *WAF) Copy() *WAF {
|
||||
waf := &WAF{
|
||||
Id: this.Id,
|
||||
IsOn: this.IsOn,
|
||||
Name: this.Name,
|
||||
Inbound: this.Inbound,
|
||||
Outbound: this.Outbound,
|
||||
}
|
||||
return waf
|
||||
}
|
||||
|
||||
func (this *WAF) CountInboundRuleSets() int {
|
||||
count := 0
|
||||
for _, group := range this.Inbound {
|
||||
count += len(group.RuleSets)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func (this *WAF) CountOutboundRuleSets() int {
|
||||
count := 0
|
||||
for _, group := range this.Outbound {
|
||||
count += len(group.RuleSets)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func (this *WAF) OnAction(onActionCallback func(action ActionString) (goNext bool)) {
|
||||
this.onActionCallback = onActionCallback
|
||||
}
|
||||
|
||||
func (this *WAF) FindCheckpointInstance(prefix string) checkpoints.CheckpointInterface {
|
||||
instance, ok := this.checkpointsMap[prefix]
|
||||
if ok {
|
||||
return instance
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// start
|
||||
func (this *WAF) Start() {
|
||||
for _, checkpoint := range this.checkpointsMap {
|
||||
checkpoint.Start()
|
||||
}
|
||||
}
|
||||
|
||||
// call stop() when the waf was deleted
|
||||
func (this *WAF) Stop() {
|
||||
for _, checkpoint := range this.checkpointsMap {
|
||||
checkpoint.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// merge with template
|
||||
func (this *WAF) MergeTemplate() (changedItems []string) {
|
||||
changedItems = []string{}
|
||||
|
||||
// compare versions
|
||||
if !Tea.IsTesting() && this.CreatedVersion == teaconst.Version {
|
||||
return
|
||||
}
|
||||
this.CreatedVersion = teaconst.Version
|
||||
|
||||
template := Template()
|
||||
groups := []*RuleGroup{}
|
||||
groups = append(groups, template.Inbound...)
|
||||
groups = append(groups, template.Outbound...)
|
||||
for _, group := range groups {
|
||||
oldGroup := this.FindRuleGroupWithCode(group.Code)
|
||||
if oldGroup == nil {
|
||||
group.Id = stringutil.Rand(16)
|
||||
this.AddRuleGroup(group)
|
||||
changedItems = append(changedItems, "+group "+group.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// check rule sets
|
||||
for _, set := range group.RuleSets {
|
||||
oldSet := oldGroup.FindRuleSetWithCode(set.Code)
|
||||
if oldSet == nil {
|
||||
oldGroup.AddRuleSet(set)
|
||||
changedItems = append(changedItems, "+group "+group.Name+" rule set:"+set.Name)
|
||||
} else if len(oldSet.Rules) < len(set.Rules) {
|
||||
oldSet.Rules = set.Rules
|
||||
changedItems = append(changedItems, "*group "+group.Name+" rule set:"+set.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
58
internal/waf/waf_test.go
Normal file
58
internal/waf/waf_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package waf
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWAF_MatchRequest(t *testing.T) {
|
||||
a := assert.NewAssertion(t)
|
||||
|
||||
set := NewRuleSet()
|
||||
set.Name = "Name_Age"
|
||||
set.Connector = RuleConnectorAnd
|
||||
set.Rules = []*Rule{
|
||||
{
|
||||
Param: "${arg.name}",
|
||||
Operator: RuleOperatorEqString,
|
||||
Value: "lu",
|
||||
},
|
||||
{
|
||||
Param: "${arg.age}",
|
||||
Operator: RuleOperatorEq,
|
||||
Value: "20",
|
||||
},
|
||||
}
|
||||
set.Action = ActionBlock
|
||||
|
||||
group := NewRuleGroup()
|
||||
group.AddRuleSet(set)
|
||||
group.IsInbound = true
|
||||
|
||||
waf := NewWAF()
|
||||
waf.AddRuleGroup(group)
|
||||
err := waf.Init()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
waf.OnAction(func(action ActionString) (goNext bool) {
|
||||
return action != ActionBlock
|
||||
})
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "http://teaos.cn/hello?name=lu&age=20", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
goNext, _, set, err := waf.MatchRequest(req, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if set == nil {
|
||||
t.Log("not match")
|
||||
return
|
||||
}
|
||||
t.Log("goNext:", goNext, "set:", set.Name)
|
||||
a.IsFalse(goNext)
|
||||
}
|
||||
Reference in New Issue
Block a user