From f5a1e1dd34adfb769b6d5205105128c038f1ae50 Mon Sep 17 00:00:00 2001 From: Sijie Yang Date: Fri, 15 Jan 2021 13:13:16 +0800 Subject: [PATCH] Release/v1.0.0 (#688) --- .golangci.toml | 27 -- ADOPTERS.md | 7 +- CHANGELOG.md | 16 +- CONTRIBUTORS.md | 9 + Dockerfile | 6 +- MAINTAINERS.md | 4 +- VERSION | 2 +- bfe_basic/condition/build.go | 21 ++ bfe_basic/condition/build_test.go | 13 + bfe_basic/condition/parser/ast.go | 2 - bfe_basic/condition/parser/semant.go | 3 + bfe_basic/condition/primitive.go | 57 ++++ bfe_basic/condition/primitive_test.go | 33 ++ bfe_basic/request.go | 18 +- bfe_bufio/bufio_test.go | 2 +- .../cluster_conf/cluster_conf_load.go | 32 +- .../cluster_table_conf/cluster_table_load.go | 4 +- bfe_modules/bfe_modules.go | 4 + .../mod_compress/compress_rule_load_test.go | 4 +- bfe_modules/mod_geo/conf_mod_geo_test.go | 6 +- ...od_geo1.conf => mod_geo.conf.default_path} | 0 bfe_modules/mod_header/action_header_var.go | 14 +- bfe_modules/mod_prison/mod_prison.go | 1 - bfe_modules/mod_prison/rule_test.go | 4 +- bfe_modules/mod_waf/conf_mod_waf.go | 85 +++++ bfe_modules/mod_waf/conf_mod_waf_test.go | 290 ++++++++++++++++ bfe_modules/mod_waf/mod_waf.go | 188 +++++++++++ bfe_modules/mod_waf/mod_waf_test.go | 82 +++++ bfe_modules/mod_waf/testdata/mod_waf.conf | 8 + .../mod_waf/testdata/mod_waf/mod_waf.conf | 8 + .../mod_waf/testdata/mod_waf/waf_rule.data | 13 + .../testdata/mod_waf/waf_rule_check.data | 13 + bfe_modules/mod_waf/testdata/waf_rule.data | 13 + .../testdata/waf_rule_both_block_rules.data | 15 + .../testdata/waf_rule_both_check_rules.data | 16 + .../mod_waf/testdata/waf_rule_both_empty.data | 14 + .../mod_waf/testdata/waf_rule_both_nil.data | 10 + .../testdata/waf_rule_invalid_cond.data | 13 + .../testdata/waf_rule_invalid_json.data | 13 + .../mod_waf/testdata/waf_rule_no_config.data | 3 + .../mod_waf/testdata/waf_rule_no_version.data | 12 + bfe_modules/mod_waf/waf_handler.go | 78 +++++ bfe_modules/mod_waf/waf_job.go | 47 +++ bfe_modules/mod_waf/waf_log.go | 50 +++ bfe_modules/mod_waf/waf_rule/rule_bash_cmd.go | 186 +++++++++++ .../mod_waf/waf_rule/rule_bash_cmd_test.go | 312 ++++++++++++++++++ .../mod_waf/waf_rule/rule_request_info.go | 46 +++ bfe_modules/mod_waf/waf_rule/waf_rule.go | 57 ++++ bfe_modules/mod_waf/waf_rule/waf_rule_test.go | 157 +++++++++ bfe_modules/mod_waf/waf_rule_load.go | 190 +++++++++++ bfe_modules/mod_waf/waf_rule_load_test.go | 151 +++++++++ bfe_modules/mod_waf/waf_rule_table.go | 43 +++ bfe_modules/mod_waf/waf_rule_table_test.go | 97 ++++++ bfe_route/bfe_cluster/bfe_cluster.go | 14 +- bfe_server/reverseproxy.go | 58 ++-- bfe_server/set_client_addr.go | 6 +- bfe_util/signal_table/signal_table.go | 6 +- conf/mod_waf/mod_waf.conf | 8 + conf/mod_waf/waf_rule.data | 16 + docs/en_us/SUMMARY.md | 1 + .../condition/condition_primitive_index.md | 1 + docs/en_us/condition/request/uri.md | 15 + docs/en_us/example/fastcgi.md | 151 +++++++++ docs/en_us/faq/development.md | 2 +- docs/en_us/installation/install.md | 1 + .../installation/install_using_docker.md | 25 ++ .../assets/images/china_everbright_bank.jpg | Bin 15004 -> 0 bytes docs/material/assets/images/post.jpg | Bin 0 -> 45660 bytes docs/material/assets/images/shenzhen.png | Bin 0 -> 37744 bytes docs/material/overrides/home_en.html | 35 +- docs/material/overrides/home_zh.html | 31 +- docs/mkdocs_en.yml | 2 +- docs/mkdocs_zh.yml | 2 +- docs/zh_cn/SUMMARY.md | 1 + .../condition/condition_primitive_index.md | 1 + docs/zh_cn/condition/request/uri.md | 15 + docs/zh_cn/example/fastcgi.md | 154 +++++++++ docs/zh_cn/faq/development.md | 2 +- docs/zh_cn/installation/install.md | 1 + .../installation/install_using_docker.md | 25 ++ go.mod | 2 +- go.sum | 4 +- 82 files changed, 2947 insertions(+), 131 deletions(-) delete mode 100644 .golangci.toml rename bfe_modules/mod_geo/test_data/mod_geo/{mod_geo1.conf => mod_geo.conf.default_path} (100%) create mode 100644 bfe_modules/mod_waf/conf_mod_waf.go create mode 100644 bfe_modules/mod_waf/conf_mod_waf_test.go create mode 100644 bfe_modules/mod_waf/mod_waf.go create mode 100644 bfe_modules/mod_waf/mod_waf_test.go create mode 100644 bfe_modules/mod_waf/testdata/mod_waf.conf create mode 100644 bfe_modules/mod_waf/testdata/mod_waf/mod_waf.conf create mode 100644 bfe_modules/mod_waf/testdata/mod_waf/waf_rule.data create mode 100644 bfe_modules/mod_waf/testdata/mod_waf/waf_rule_check.data create mode 100644 bfe_modules/mod_waf/testdata/waf_rule.data create mode 100644 bfe_modules/mod_waf/testdata/waf_rule_both_block_rules.data create mode 100644 bfe_modules/mod_waf/testdata/waf_rule_both_check_rules.data create mode 100644 bfe_modules/mod_waf/testdata/waf_rule_both_empty.data create mode 100644 bfe_modules/mod_waf/testdata/waf_rule_both_nil.data create mode 100644 bfe_modules/mod_waf/testdata/waf_rule_invalid_cond.data create mode 100644 bfe_modules/mod_waf/testdata/waf_rule_invalid_json.data create mode 100644 bfe_modules/mod_waf/testdata/waf_rule_no_config.data create mode 100644 bfe_modules/mod_waf/testdata/waf_rule_no_version.data create mode 100644 bfe_modules/mod_waf/waf_handler.go create mode 100644 bfe_modules/mod_waf/waf_job.go create mode 100644 bfe_modules/mod_waf/waf_log.go create mode 100644 bfe_modules/mod_waf/waf_rule/rule_bash_cmd.go create mode 100644 bfe_modules/mod_waf/waf_rule/rule_bash_cmd_test.go create mode 100644 bfe_modules/mod_waf/waf_rule/rule_request_info.go create mode 100644 bfe_modules/mod_waf/waf_rule/waf_rule.go create mode 100644 bfe_modules/mod_waf/waf_rule/waf_rule_test.go create mode 100644 bfe_modules/mod_waf/waf_rule_load.go create mode 100644 bfe_modules/mod_waf/waf_rule_load_test.go create mode 100644 bfe_modules/mod_waf/waf_rule_table.go create mode 100644 bfe_modules/mod_waf/waf_rule_table_test.go create mode 100644 conf/mod_waf/mod_waf.conf create mode 100644 conf/mod_waf/waf_rule.data create mode 100644 docs/en_us/example/fastcgi.md create mode 100644 docs/en_us/installation/install_using_docker.md delete mode 100644 docs/material/assets/images/china_everbright_bank.jpg create mode 100644 docs/material/assets/images/post.jpg create mode 100644 docs/material/assets/images/shenzhen.png create mode 100644 docs/zh_cn/example/fastcgi.md create mode 100644 docs/zh_cn/installation/install_using_docker.md diff --git a/.golangci.toml b/.golangci.toml deleted file mode 100644 index d4bee6812..000000000 --- a/.golangci.toml +++ /dev/null @@ -1,27 +0,0 @@ -[run] - deadline = "10m" - tests = true - -[linters] - disable-all = true - enable = [ - "deadcode", - "depguard", - "goconst", - "gocyclo", - "gocritic", - "gosec", - "gosimple", - "ineffassign", - "maligned", - "misspell", - "nakedret", - "staticcheck", - "structcheck", - "typecheck", - "unconvert", - "unparam", - "varcheck", - "vet", - "vetshadow", - ] diff --git a/ADOPTERS.md b/ADOPTERS.md index 2a75547aa..d4241fab4 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,6 +1,3 @@ -A non-exhaustive list of bfe adopters is provided below. Send pull request if you want to be listed here. +See https://www.bfe-networks.net/en_us/#users . -| Company | Description | -| ------- | ----------- | -| Baidu | [HTTPDNS](https://cloud.baidu.com/product/httpdns.html) | -| Baidu | GDP 2.0: Go Develop Platform | +Please [create an issue](https://github.com/bfenetworks/bfe/issues/new?&title=New%20BFE%20User) if you want to be listed there. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a4135557..5a9949e42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [v0.12.0] - 2020-09-03 +## [v1.0.0] - 2021-01-15 + +### Added +- Add condition primitive: req_path_contain/req_path_element_prefix_in/req_context_value_in +- Add outlier detection options +- Add mod_waf with rule to detect exploitation of "Shellshock" GNU Bash RCE vulnerability. + +### Fixed +- Fix build issue under go1.15 environment +- Fix processing X-Forwarded-For header value +- Fix write timeout of internal response generated by bfe + + +## [v0.12.0] - 2020-09-03 ### Added - Support gRPC over HTTP/2 @@ -202,6 +215,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Flexible plugin framework to extend functionality. Based on the framework, developer can add new features rapidly - Detailed built-in metrics available for service status monitor +[v1.0.0]: https://github.com/bfenetworks/bfe/compare/v0.12.0...v1.0.0 [v0.12.0]: https://github.com/bfenetworks/bfe/compare/v0.11.0...v0.12.0 [v0.11.0]: https://github.com/bfenetworks/bfe/compare/v0.10.0...v0.11.0 [v0.10.0]: https://github.com/bfenetworks/bfe/compare/v0.9.0...v0.10.0 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 79025a172..012d7c12d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -8,12 +8,15 @@ | Chao Wang | Corey-Wang | | Chengjin Wu | ChengjinWu | | Chongmiao Liu | lcmmhcc | +| Chunlin An | Adolph-An | | Daniel Sutton | ducksecops | | Dechuang Gu | hellogdc | | Derek Zheng | shanhuhai5739 | | Di Zhao | zd0106 | | Hao Dong | anotherwriter | | Haobin zhang | zhanghaobin | +| Hui Yu | dblate | +| Gen Wang | gracewang510 | | Jie Liu | freeHackOfJeff | | Jie Wan | wanjiecs | | Jin Tong | cumirror | @@ -21,6 +24,7 @@ | Kaiyu Zheng | kaiyuzheng | | Lidong Chang | changlidong68 | | Lihua Chen | clh651188968 | +| Liujia Wei | weiliujia | | Limei Xiao | limeix | | Lei Zhang | deancn | | Lu Guo | guolu60 | @@ -39,6 +43,7 @@ | Shuai Yan | yanshuai615270 | | Sijie Yang | iyangsj | | Tianqi Zhang | NKztq | +| Weijie Zhao | zwj13513118235 | | Weiqiang Zheng | wrayzheng | | Wenjie Tian | WJTian | | Wenlong Chen | LeroChen | @@ -48,6 +53,7 @@ | Xiaonan chen | two | | Xiaoye Jiang | kidleaf-jiang | | Xin Li | lx-or-xxxl | +| Yang Guo | Marswin | | Yang Liu | dut-yangliu | | Yusheng Sun | wodedipanr | | Yuan Liu | lewisay | @@ -58,7 +64,10 @@ | Zhankang Han | leceshide | | | 0xflotus | | | calify | +| | Gii16 | | | MoonShining | | | odidev | +| | pirDOL | +| | tianlan2011 | | | u5surf | | | xiaocongwjb | diff --git a/Dockerfile b/Dockerfile index 9ff02d9d1..c84410650 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,10 +6,10 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X main.version=`ca FROM alpine:3.10 AS run RUN apk update && apk add --no-cache ca-certificates -COPY --from=build /bfe/bfe /bfe/output/bin/ -COPY conf /bfe/output/conf/ +COPY --from=build /bfe/bfe /bfe/bin/ +COPY conf /bfe/conf/ EXPOSE 8080 8443 8421 -WORKDIR /bfe/output/bin +WORKDIR /bfe/bin ENTRYPOINT ["./bfe"] CMD ["-c", "../conf/", "-l", "../log"] diff --git a/MAINTAINERS.md b/MAINTAINERS.md index e863fe31d..3d71e318c 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -6,12 +6,12 @@ This file lists who are the maintainers of the BFE project. The responsibilities | Name | GitHub ID | Affiliation | | ---- | --------- | ----------- | | [Miao Zhang](mailto:zhangmiao02@baidu.com) | [mileszhang2016](https://github.com/mileszhang2016) | Baidu | -| [Sijie Yang](mailto:yangsijie@baidu.com) | [iyangsj](https://github.com/iyangsj) | Baidu | +| [Sijie Yang](mailto:iyangsj@gmail.com) | [iyangsj](https://github.com/iyangsj) | Baidu | ## Senior Maintainers | Name | GitHub ID | Affiliation | | ---- | --------- | ----------- | -| [Sijie Yang](mailto:yangsijie@baidu.com) | [iyangsj](https://github.com/iyangsj) | Baidu | +| [Sijie Yang](mailto:iyangsj@gmail.com) | [iyangsj](https://github.com/iyangsj) | Baidu | ## Maintainers | Name | GitHub ID | Affiliation | diff --git a/VERSION b/VERSION index ac454c6a1..3eefcb9dd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.12.0 +1.0.0 diff --git a/bfe_basic/condition/build.go b/bfe_basic/condition/build.go index 5413b8119..384deefed 100644 --- a/bfe_basic/condition/build.go +++ b/bfe_basic/condition/build.go @@ -200,6 +200,13 @@ func buildPrimitive(node *parser.CallExpr) (Condition, error) { fetcher: &PathFetcher{}, matcher: NewSuffixInMatcher(node.Args[0].Value, node.Args[1].ToBool()), }, nil + case "req_path_element_prefix_in": + return &PrimitiveCond{ + name: node.Fun.Name, + node: node, + fetcher: &PathFetcher{}, + matcher: NewPathElementPrefixMatcher(node.Args[0].Value, node.Args[1].ToBool()), + }, nil case "req_path_regmatch": reg, err := regexp.Compile(node.Args[0].Value) if err != nil { @@ -211,6 +218,13 @@ func buildPrimitive(node *parser.CallExpr) (Condition, error) { fetcher: &PathFetcher{}, matcher: NewRegMatcher(reg), }, nil + case "req_path_contain": + return &PrimitiveCond{ + name: node.Fun.Name, + node: node, + fetcher: &PathFetcher{}, + matcher: NewContainMatcher(node.Args[0].Value, node.Args[1].ToBool()), + }, nil case "req_url_regmatch": reg, err := regexp.Compile(node.Args[0].Value) if err != nil { @@ -503,6 +517,13 @@ func buildPrimitive(node *parser.CallExpr) (Condition, error) { fetcher: &ClientCANameFetcher{}, matcher: NewInMatcher(node.Args[0].Value, false), }, nil + case "req_context_value_in": + return &PrimitiveCond{ + name: node.Fun.Name, + node: node, + fetcher: &ContextValueFetcher{node.Args[0].Value}, + matcher: NewInMatcher(node.Args[1].Value, false), + }, nil default: return nil, fmt.Errorf("unsupported primitive %s", node.Fun.Name) } diff --git a/bfe_basic/condition/build_test.go b/bfe_basic/condition/build_test.go index e5d140e1c..867551fb8 100644 --- a/bfe_basic/condition/build_test.go +++ b/bfe_basic/condition/build_test.go @@ -111,6 +111,19 @@ var buildPrimitiveTests = []struct { }, false, }, + { + "testBuildReqPathElementPrefixIn", + "req_path_element_prefix_in(\"/abc\", true)", + &PrimitiveCond{ + name: "req_path_element_prefix_in", + fetcher: &PathFetcher{}, + matcher: &PathElementPrefixMatcher{ + patterns: []string{"/ABC/"}, + foldCase: true, + }, + }, + false, + }, { "testBuildQueRegMatch", "req_query_value_regmatch(\"abc\", \"123\")", diff --git a/bfe_basic/condition/parser/ast.go b/bfe_basic/condition/parser/ast.go index da4664c30..d9b4e6bea 100644 --- a/bfe_basic/condition/parser/ast.go +++ b/bfe_basic/condition/parser/ast.go @@ -138,7 +138,6 @@ func (b BasicLitList) End() token.Pos { return b[len(b)].End() } -//TODO: not accurate func (p ParenExpr) Pos() token.Pos { return p.X.Pos() } @@ -147,7 +146,6 @@ func (p ParenExpr) End() token.Pos { return p.X.End() } -// calller should check its Kind func (b *BasicLit) ToBool() bool { if b.Kind != BOOL { return false diff --git a/bfe_basic/condition/parser/semant.go b/bfe_basic/condition/parser/semant.go index 830bac216..1f1bd47b0 100644 --- a/bfe_basic/condition/parser/semant.go +++ b/bfe_basic/condition/parser/semant.go @@ -35,7 +35,9 @@ var funcProtos = map[string][]Token{ "req_path_in": {STRING, BOOL}, "req_path_prefix_in": {STRING, BOOL}, "req_path_suffix_in": {STRING, BOOL}, + "req_path_contain": {STRING, BOOL}, "req_path_regmatch": {STRING}, + "req_path_element_prefix_in": {STRING, BOOL}, "req_query_key_prefix_in": {STRING}, "req_query_key_in": {STRING}, "req_query_exist": nil, @@ -74,6 +76,7 @@ var funcProtos = map[string][]Token{ "ses_tls_sni_in": {STRING}, "ses_tls_client_auth": nil, "ses_tls_client_ca_in": {STRING}, + "req_context_value_in": {STRING, STRING}, } func prototypeCheck(expr *CallExpr) error { diff --git a/bfe_basic/condition/primitive.go b/bfe_basic/condition/primitive.go index 1a1eb413f..cfe48dd6a 100644 --- a/bfe_basic/condition/primitive.go +++ b/bfe_basic/condition/primitive.go @@ -461,6 +461,51 @@ func NewPrefixInMatcher(patterns string, foldCase bool) *PrefixInMatcher { } } +type PathElementPrefixMatcher struct { + patterns []string + foldCase bool +} + +func (p *PathElementPrefixMatcher) Match(v interface{}) bool { + vs, ok := v.(string) + if !ok { + return false + } + + if !strings.HasSuffix(vs, "/") { + vs += "/" + } + + if p.foldCase { + vs = strings.ToUpper(vs) + } + + return prefixIn(vs, p.patterns) +} + +func NewPathElementPrefixMatcher(patterns string, foldCase bool) *PathElementPrefixMatcher { + p := strings.Split(patterns, "|") + + elementPatterns := make([]string, len(p)) + + for i, v := range p { + if !strings.HasSuffix(v, "/") { + v += "/" + } + if foldCase { + elementPatterns[i] = strings.ToUpper(v) + } else { + elementPatterns[i] = v + } + + } + + return &PathElementPrefixMatcher{ + patterns: elementPatterns, + foldCase: foldCase, + } +} + type SuffixInMatcher struct { patterns []string foldCase bool @@ -926,3 +971,15 @@ func (fetcher *ClientCANameFetcher) Fetch(req *bfe_basic.Request) (interface{}, return req.Session.TlsState.ClientCAName, nil } + +type ContextValueFetcher struct { + key string +} + +func (f *ContextValueFetcher) Fetch(req *bfe_basic.Request) (interface{}, error) { + if req == nil || req.HttpRequest == nil || req.Context == nil || f.key == "" { + return nil, fmt.Errorf("fetcher: nil pointer") + } + + return req.GetContext(f.key), nil +} diff --git a/bfe_basic/condition/primitive_test.go b/bfe_basic/condition/primitive_test.go index 6088520a9..ade6e6399 100644 --- a/bfe_basic/condition/primitive_test.go +++ b/bfe_basic/condition/primitive_test.go @@ -221,3 +221,36 @@ func TestContainMatcher_2(t *testing.T) { t.Fatalf("should match Yingwen") } } + +// test ContextFetcher +func TestContextValueFetcher(t *testing.T) { + // prepare input data + hf := ContextValueFetcher{"hello"} + req := bfe_basic.NewRequest(nil, nil, nil, nil, nil) + req.HttpRequest = &bfe_http.Request{} + req.SetContext("hello", "world") + // Fetch + contextVal, err := hf.Fetch(req) + if err != nil { + t.Fatalf("Fetch(): %v", err) + t.FailNow() + } + + // check + if contextVal.(string) != "world" { + t.Errorf("Fetch contextVal error, want=%v, got=%v", "world", contextVal) + } +} + +func TestPathElementPrefixMatcher(t *testing.T) { + matcher := NewPathElementPrefixMatcher("/path|/path/ab", true) + if !matcher.Match("/path/a/c") { + t.Fatalf("should match /path/a/c") + } + if !matcher.Match("/path/ab") { + t.Fatalf("should match /path/ab") + } + if matcher.Match("/pathabc") { + t.Fatalf("should not match /pathabc") + } +} diff --git a/bfe_basic/request.go b/bfe_basic/request.go index db375015e..d3d40059e 100644 --- a/bfe_basic/request.go +++ b/bfe_basic/request.go @@ -161,24 +161,24 @@ func (req *Request) Protocol() string { return req.HttpRequest.Proto } -func (r *Request) AddTags(name string, ntags []string) { +func (req *Request) AddTags(name string, ntags []string) { if len(ntags) == 0 { return } - tags := r.Tags.TagTable[name] + tags := req.Tags.TagTable[name] tags = append(tags, ntags...) - r.Tags.TagTable[name] = tags + req.Tags.TagTable[name] = tags } -func (r *Request) GetTags(name string) []string { - return r.Tags.TagTable[name] +func (req *Request) GetTags(name string) []string { + return req.Tags.TagTable[name] } -func (r *Request) SetContext(key, val interface{}) { - r.Context[key] = val +func (req *Request) SetContext(key, val interface{}) { + req.Context[key] = val } -func (r *Request) GetContext(key interface{}) interface{} { - return r.Context[key] +func (req *Request) GetContext(key interface{}) interface{} { + return req.Context[key] } diff --git a/bfe_bufio/bufio_test.go b/bfe_bufio/bufio_test.go index ab5f11f3f..7045a0ec0 100644 --- a/bfe_bufio/bufio_test.go +++ b/bfe_bufio/bufio_test.go @@ -160,7 +160,7 @@ func TestReader(t *testing.T) { for i := 0; i < len(texts)-1; i++ { texts[i] = str + "\n" all += texts[i] - str += string(i%26 + 'a') + str += string(rune(i%26) + 'a') } texts[len(texts)-1] = all diff --git a/bfe_config/bfe_cluster_conf/cluster_conf/cluster_conf_load.go b/bfe_config/bfe_cluster_conf/cluster_conf/cluster_conf_load.go index 51c7aa23b..47ad35747 100644 --- a/bfe_config/bfe_cluster_conf/cluster_conf/cluster_conf_load.go +++ b/bfe_config/bfe_cluster_conf/cluster_conf/cluster_conf_load.go @@ -33,6 +33,26 @@ const ( RetryGet = 1 // retry if forward GET request fail (plus RetryConnect) ) +// DefaultTimeout +const ( + DefaultReadClientTimeout = 30000 + DefaultWriteClientTimeout = 60000 + DefaultReadClientAgainTimeout = 60000 +) + +// Outlier detection levels +const ( + // Abnormal events about backend: + // - connect backend error + // - write request error(caused by backend) + // - read response header error + OutlierDetectionBasic = 0 + + // All abnormal events in basic level and: + // - response code is 5xx + OutlierDetection5XX = 1 +) + // HashStrategy for subcluster-level load balance (GSLB). // Note: // - CLIENTID is a special request header which represents a unique client, @@ -80,6 +100,7 @@ type BackendBasic struct { TimeoutResponseHeader *int // timeout for read header from backend, in ms MaxIdleConnsPerHost *int // max idle conns for each backend RetryLevel *int // retry level if request fail + OutlierDetectionLevel *int // outlier detection level // protocol specific configurations FCGIConf *FCGIConf @@ -170,6 +191,11 @@ func BackendBasicCheck(conf *BackendBasic) error { conf.RetryLevel = &retryLevel } + if conf.OutlierDetectionLevel == nil { + outlierDetectionLevel := OutlierDetectionBasic + conf.OutlierDetectionLevel = &outlierDetectionLevel + } + if conf.FCGIConf == nil { defaultFCGIConf := new(FCGIConf) defaultFCGIConf.EnvVars = make(map[string]string) @@ -380,17 +406,17 @@ func HashConfCheck(conf *HashConf) error { // ClusterBasicConfCheck check ClusterBasicConf. func ClusterBasicConfCheck(conf *ClusterBasicConf) error { if conf.TimeoutReadClient == nil { - timeoutReadClient := 30000 + timeoutReadClient := DefaultReadClientTimeout conf.TimeoutReadClient = &timeoutReadClient } if conf.TimeoutWriteClient == nil { - timoutWriteClient := 60000 + timoutWriteClient := DefaultWriteClientTimeout conf.TimeoutWriteClient = &timoutWriteClient } if conf.TimeoutReadClientAgain == nil { - timeoutReadClientAgain := 60000 + timeoutReadClientAgain := DefaultReadClientAgainTimeout conf.TimeoutReadClientAgain = &timeoutReadClientAgain } diff --git a/bfe_config/bfe_cluster_conf/cluster_table_conf/cluster_table_load.go b/bfe_config/bfe_cluster_conf/cluster_table_conf/cluster_table_load.go index 26715a0ad..629597ca8 100644 --- a/bfe_config/bfe_cluster_conf/cluster_table_conf/cluster_table_load.go +++ b/bfe_config/bfe_cluster_conf/cluster_table_conf/cluster_table_load.go @@ -162,9 +162,9 @@ func (conf *AllClusterBackend) Check() error { return AllClusterBackendCheck(conf) } -func (sub *SubClusterBackend) Check() error { +func (s *SubClusterBackend) Check() error { availBackend := false - for index, backendConf := range *sub { + for index, backendConf := range *s { err := BackendConfCheck(backendConf) if err != nil { diff --git a/bfe_modules/bfe_modules.go b/bfe_modules/bfe_modules.go index ec1b74170..6135fc7a6 100644 --- a/bfe_modules/bfe_modules.go +++ b/bfe_modules/bfe_modules.go @@ -42,6 +42,7 @@ import ( "github.com/bfenetworks/bfe/bfe_modules/mod_trace" "github.com/bfenetworks/bfe/bfe_modules/mod_trust_clientip" "github.com/bfenetworks/bfe/bfe_modules/mod_userid" + "github.com/bfenetworks/bfe/bfe_modules/mod_waf" ) // list of all modules, the order is very important @@ -87,6 +88,9 @@ var moduleList = []bfe_module.BfeModule{ // mod_secure_link mod_secure_link.NewModuleSecureLink(), + // mod_waf + mod_waf.NewModuleWaf(), + // mod_doh mod_doh.NewModuleDoh(), diff --git a/bfe_modules/mod_compress/compress_rule_load_test.go b/bfe_modules/mod_compress/compress_rule_load_test.go index a51d57a21..275df8c6e 100644 --- a/bfe_modules/mod_compress/compress_rule_load_test.go +++ b/bfe_modules/mod_compress/compress_rule_load_test.go @@ -18,7 +18,7 @@ import ( "testing" ) -func TestProductRuleConfLoad_1(t *testing.T) { +func TestProductRuleConfLoadCorrect(t *testing.T) { config, err := ProductRuleConfLoad("./testdata/mod_compress/compress_rule.data") if err != nil { t.Errorf("ProductRuleConfLoad() error: %v", err) @@ -30,7 +30,7 @@ func TestProductRuleConfLoad_1(t *testing.T) { } } -func TestProductRuleConfLoad_2(t *testing.T) { +func TestProductRuleConfLoadCmdError(t *testing.T) { _, err := ProductRuleConfLoad("./testdata/mod_compress/compress_rule.data.cmd_error") if err == nil || err.Error() != "Config: ProductRules: unittest, compressRule: 0, invalid cmd: ERR_COMPRESS" { diff --git a/bfe_modules/mod_geo/conf_mod_geo_test.go b/bfe_modules/mod_geo/conf_mod_geo_test.go index 21e45fe90..9dd8117ee 100644 --- a/bfe_modules/mod_geo/conf_mod_geo_test.go +++ b/bfe_modules/mod_geo/conf_mod_geo_test.go @@ -19,7 +19,7 @@ import ( "testing" ) -func TestConfModGeoCase1(t *testing.T) { +func TestConfModGeo(t *testing.T) { config, err := ConfLoad("./test_data/mod_geo/mod_geo.conf", "") if err != nil { msg := fmt.Sprintf("confModGeoLoad():err=%s", err.Error()) @@ -32,8 +32,8 @@ func TestConfModGeoCase1(t *testing.T) { } } -func TestConfModGeoCase2(t *testing.T) { - config, err := ConfLoad("./test_data/mod_geo/mod_geo1.conf", "") +func TestConfModGeoDefaultPath(t *testing.T) { + config, err := ConfLoad("./test_data/mod_geo/mod_geo.conf.default_path", "") if err != nil { msg := fmt.Sprintf("confModGeoLoad():err=%s", err.Error()) t.Error(msg) diff --git a/bfe_modules/mod_geo/test_data/mod_geo/mod_geo1.conf b/bfe_modules/mod_geo/test_data/mod_geo/mod_geo.conf.default_path similarity index 100% rename from bfe_modules/mod_geo/test_data/mod_geo/mod_geo1.conf rename to bfe_modules/mod_geo/test_data/mod_geo/mod_geo.conf.default_path diff --git a/bfe_modules/mod_header/action_header_var.go b/bfe_modules/mod_header/action_header_var.go index 7b96f2075..cc864c488 100644 --- a/bfe_modules/mod_header/action_header_var.go +++ b/bfe_modules/mod_header/action_header_var.go @@ -34,7 +34,7 @@ import ( type HeaderValueHandler func(req *bfe_basic.Request) string const ( - UNKNOWN = "unknown" + Unknown = "unknown" ) var VariableHandlers = map[string]HeaderValueHandler{ @@ -283,7 +283,7 @@ func getBfeVip(req *bfe_basic.Request) string { return req.Session.Vip.String() } - return UNKNOWN + return Unknown } func getAddressFetcher(conn net.Conn) bfe_util.AddressFetcher { @@ -299,16 +299,16 @@ func getAddressFetcher(conn net.Conn) bfe_util.AddressFetcher { func getBfeBip(req *bfe_basic.Request) string { f := getAddressFetcher(req.Session.Connection) if f == nil { - return UNKNOWN + return Unknown } baddr := f.BalancerAddr() if baddr == nil { - return UNKNOWN + return Unknown } bip, _, err := net.SplitHostPort(baddr.String()) if err != nil { /* never come here */ - return UNKNOWN + return Unknown } return bip @@ -319,7 +319,7 @@ func getBfeRip(req *bfe_basic.Request) string { raddr := conn.LocalAddr() rip, _, err := net.SplitHostPort(raddr.String()) if err != nil { /* never come here */ - return UNKNOWN + return Unknown } return rip @@ -334,7 +334,7 @@ func getBfeBackendInfo(req *bfe_basic.Request) string { func getBfeServerName(req *bfe_basic.Request) string { hostname, err := os.Hostname() if err != nil { - return UNKNOWN + return Unknown } return hostname diff --git a/bfe_modules/mod_prison/mod_prison.go b/bfe_modules/mod_prison/mod_prison.go index 7f034f7a6..8d1fc3c0b 100644 --- a/bfe_modules/mod_prison/mod_prison.go +++ b/bfe_modules/mod_prison/mod_prison.go @@ -66,7 +66,6 @@ type ModulePrison struct { state ModulePrisonState // module state metrics metrics.Metrics productConfPath string // path for prodct rule - conf ProductRuleConf // config for prison productTable *productRuleTable // product rule table } diff --git a/bfe_modules/mod_prison/rule_test.go b/bfe_modules/mod_prison/rule_test.go index 49da3d3cf..b4a42118e 100644 --- a/bfe_modules/mod_prison/rule_test.go +++ b/bfe_modules/mod_prison/rule_test.go @@ -191,14 +191,14 @@ func TestRecordAccess(t *testing.T) { // meet threshod, should be zero rule.recordAccess(sign) - value, ok = rule.accessDict.Get(sign) + _, ok = rule.accessDict.Get(sign) if ok { t.Errorf("access counter should be deleted") return } // should get failed - value, ok = rule.accessDict.Get(AccessSign(md5.Sum([]byte("1234")))) + _, ok = rule.accessDict.Get(AccessSign(md5.Sum([]byte("1234")))) if ok { t.Error("should get failed") return diff --git a/bfe_modules/mod_waf/conf_mod_waf.go b/bfe_modules/mod_waf/conf_mod_waf.go new file mode 100644 index 000000000..92168499a --- /dev/null +++ b/bfe_modules/mod_waf/conf_mod_waf.go @@ -0,0 +1,85 @@ +// Copyright (c) 2020 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package mod_waf + +import ( + "fmt" +) + +import ( + "github.com/baidu/go-lib/log" + "github.com/baidu/go-lib/log/log4go" + "gopkg.in/gcfg.v1" +) + +import ( + "github.com/bfenetworks/bfe/bfe_util" +) + +const ( + DefaultRulePath = "mod_waf/waf_rule.data" // default product rule path +) + +type ConfModWaf struct { + Basic struct { + ProductRulePath string // path of waf rule data + } + Log struct { + LogPrefix string // log file prefix + LogDir string // log file dir + RotateWhen string // rotate time + BackupCount int // log file backup number + } +} + +func (cfg *ConfModWaf) Check(confRoot string) error { + if cfg.Basic.ProductRulePath == "" { + log.Logger.Warn("ConfModWaf.ProductRulePath not set, use default value") + cfg.Basic.ProductRulePath = DefaultRulePath + } + + cfg.Basic.ProductRulePath = bfe_util.ConfPathProc(cfg.Basic.ProductRulePath, confRoot) + + if cfg.Log.LogPrefix == "" { + return fmt.Errorf("ConfModWaf.LogPrefix is empty") + } + + if cfg.Log.LogDir == "" { + return fmt.Errorf("ConfModWaf.LogDir is empty") + } + cfg.Log.LogDir = bfe_util.ConfPathProc(cfg.Log.LogDir, confRoot) + + if !log4go.WhenIsValid(cfg.Log.RotateWhen) { + return fmt.Errorf("ConfModWaf.RotateWhen invalid: %s", cfg.Log.RotateWhen) + } + + if cfg.Log.BackupCount <= 0 { + return fmt.Errorf("ConfModWaf.BackupCount should > 0: %d", cfg.Log.BackupCount) + } + + return nil +} + +func ConfLoad(filePath string, confRoot string) (*ConfModWaf, error) { + var cfg ConfModWaf + err := gcfg.ReadFileInto(&cfg, filePath) + if err != nil { + return nil, err + } + err = cfg.Check(confRoot) + if err != nil { + return nil, err + } + return &cfg, nil +} diff --git a/bfe_modules/mod_waf/conf_mod_waf_test.go b/bfe_modules/mod_waf/conf_mod_waf_test.go new file mode 100644 index 000000000..3ca8d88f9 --- /dev/null +++ b/bfe_modules/mod_waf/conf_mod_waf_test.go @@ -0,0 +1,290 @@ +// Copyright (c) 2020 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package mod_waf + +import ( + "reflect" + "testing" +) + +func TestConfModWafCheck(t *testing.T) { + type fields struct { + Basic struct { + ProductRulePath string + } + Log struct { + LogPrefix string + LogDir string + RotateWhen string + BackupCount int + } + } + type args struct { + confRoot string + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "normal", + fields: fields{ + Basic: struct { + ProductRulePath string + }{ + ProductRulePath: "mod_waf/waf_rule.data", + }, + Log: struct { + LogPrefix string + LogDir string + RotateWhen string + BackupCount int + }{ + LogPrefix: "waf", + LogDir: "../log", + RotateWhen: "NEXTHOUR", + BackupCount: 24, + }, + }, + args: args{ + confRoot: "mod_waf.conf", + }, + wantErr: false, + }, + { + name: "normal-empty rule path ", + fields: fields{ + Basic: struct { + ProductRulePath string + }{ + ProductRulePath: "", + }, + Log: struct { + LogPrefix string + LogDir string + RotateWhen string + BackupCount int + }{ + LogPrefix: "waf", + LogDir: "../log", + RotateWhen: "NEXTHOUR", + BackupCount: 24, + }, + }, + args: args{ + confRoot: "mod_waf.conf", + }, + wantErr: false, + }, + { + name: "normal-empty concurrency", + fields: fields{ + Basic: struct { + ProductRulePath string + }{ + ProductRulePath: "mod_waf/waf_rule.data", + }, + Log: struct { + LogPrefix string + LogDir string + RotateWhen string + BackupCount int + }{ + LogPrefix: "waf", + LogDir: "../log", + RotateWhen: "NEXTHOUR", + BackupCount: 24, + }, + }, + args: args{ + confRoot: "mod_waf.conf", + }, + wantErr: false, + }, + { + name: "abnormal-empty log prefix", + fields: fields{ + Basic: struct { + ProductRulePath string + }{ + ProductRulePath: "mod_waf/waf_rule.data", + }, + Log: struct { + LogPrefix string + LogDir string + RotateWhen string + BackupCount int + }{ + LogPrefix: "", + LogDir: "../log", + RotateWhen: "NEXTHOUR", + BackupCount: 24, + }, + }, + args: args{ + confRoot: "mod_waf.conf", + }, + wantErr: true, + }, + { + name: "abnormal-empty LogDir", + fields: fields{ + Basic: struct { + ProductRulePath string + }{ + ProductRulePath: "mod_waf/waf_rule.data", + }, + Log: struct { + LogPrefix string + LogDir string + RotateWhen string + BackupCount int + }{ + LogPrefix: "waf", + LogDir: "", + RotateWhen: "NEXTHOUR", + BackupCount: 24, + }, + }, + args: args{ + confRoot: "mod_waf.conf", + }, + wantErr: true, + }, + { + name: "abnormal-empty Backup", + fields: fields{ + Basic: struct { + ProductRulePath string + }{ + ProductRulePath: "mod_waf/waf_rule.data", + }, + Log: struct { + LogPrefix string + LogDir string + RotateWhen string + BackupCount int + }{ + LogPrefix: "waf", + LogDir: "", + RotateWhen: "NEXTHOUR", + BackupCount: 0, + }, + }, + args: args{ + confRoot: "mod_waf.conf", + }, + wantErr: true, + }, + { + name: "abnormal-empty when", + fields: fields{ + Basic: struct { + ProductRulePath string + }{ + ProductRulePath: "mod_waf/waf_rule.data", + }, + Log: struct { + LogPrefix string + LogDir string + RotateWhen string + BackupCount int + }{ + LogPrefix: "waf", + LogDir: "../log", + RotateWhen: "HHHH", + BackupCount: 24, + }, + }, + args: args{ + confRoot: "mod_waf.conf", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := &ConfModWaf{ + Basic: tt.fields.Basic, + Log: tt.fields.Log, + } + if err := cfg.Check(tt.args.confRoot); (err != nil) != tt.wantErr { + t.Errorf("ConfModWaf.Check() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestConfLoad(t *testing.T) { + type args struct { + filePath string + confRoot string + } + tests := []struct { + name string + args args + want *ConfModWaf + wantErr bool + }{ + { + name: "normal", + args: args{ + filePath: "./testdata/mod_waf.conf", + confRoot: "./testdata", + }, + want: &ConfModWaf{ + Basic: struct { + ProductRulePath string + }{ + ProductRulePath: "testdata/mod_waf/waf_rule.data", + }, + Log: struct { + LogPrefix string + LogDir string + RotateWhen string + BackupCount int + }{ + LogPrefix: "waf", + LogDir: "log", + RotateWhen: "NEXTHOUR", + BackupCount: 24, + }, + }, + wantErr: false, + }, + { + name: "normal", + args: args{ + filePath: "./testdata/notexist.conf", + confRoot: "./testdata", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ConfLoad(tt.args.filePath, tt.args.confRoot) + if (err != nil) != tt.wantErr { + t.Errorf("ConfLoad() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ConfLoad() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/bfe_modules/mod_waf/mod_waf.go b/bfe_modules/mod_waf/mod_waf.go new file mode 100644 index 000000000..35da2e7ed --- /dev/null +++ b/bfe_modules/mod_waf/mod_waf.go @@ -0,0 +1,188 @@ +// Copyright (c) 2020 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package mod_waf + +import ( + "errors" + "fmt" + "net/url" +) + +import ( + "github.com/baidu/go-lib/log" + "github.com/baidu/go-lib/web-monitor/metrics" + "github.com/baidu/go-lib/web-monitor/web_monitor" +) + +import ( + "github.com/bfenetworks/bfe/bfe_basic" + "github.com/bfenetworks/bfe/bfe_http" + "github.com/bfenetworks/bfe/bfe_module" +) + +const ( + ModWaf = "mod_waf" // mod waf +) + +var ( + ErrWaf = errors.New("WAF") // deny by Waf +) + +type ModuleWafState struct { + CheckedReq *metrics.Counter // record how many requests check waf rule + + HitBlockedReq *metrics.Counter // record how many requests check waf rule + HitCheckedRule *metrics.Counter // hit checked rule + + BlockedRuleError *metrics.Counter //err times of check blocked rule + CheckedRuleError *metrics.Counter // err times of check checked rule +} + +type ModuleWaf struct { + name string // mod name + conf *ConfModWaf // mod waf config + handler *wafHandler // mod waf handler + state ModuleWafState // state of waf + ruleTable *WarRuleTable // rule table of waf + metrics metrics.Metrics // metric info of waf +} + +func NewModuleWaf() *ModuleWaf { + m := new(ModuleWaf) + m.name = ModWaf + m.handler = NewWafHandler() + m.metrics.Init(&m.state, m.name, 0) + m.ruleTable = NewWarRuleTable() + return m +} + +func (m *ModuleWaf) Name() string { + return m.name +} + +func (m *ModuleWaf) loadProductRuleConf(query url.Values) error { + // get file path + path := query.Get("path") + if path == "" { + // use default + path = m.conf.Basic.ProductRulePath + } + + // load from config file + conf, err := ProductWafRuleConfLoad(path) + if err != nil { + return fmt.Errorf("%s: loadProductRuleConf(%s) error: %v", m.name, path, err) + } + + // update to rule table + m.ruleTable.Update(&conf) + return nil +} + +func (m *ModuleWaf) getState(params map[string][]string) ([]byte, error) { + s := m.metrics.GetAll() + return s.Format(params) +} + +func (m *ModuleWaf) getStateDiff(params map[string][]string) ([]byte, error) { + s := m.metrics.GetDiff() + return s.Format(params) +} + +func (m *ModuleWaf) monitorHandlers() map[string]interface{} { + handlers := map[string]interface{}{ + m.name: m.getState, + m.name + ".diff": m.getStateDiff, + } + return handlers +} + +func (m *ModuleWaf) reloadHandlers() map[string]interface{} { + handlers := map[string]interface{}{ + m.name: m.loadProductRuleConf, + } + return handlers +} + +func (m *ModuleWaf) handleWaf(req *bfe_basic.Request) (int, *bfe_http.Response) { + rules, ok := m.ruleTable.Search(req.Route.Product) + if !ok { + return bfe_module.BfeHandlerGoOn, nil + } + for _, rule := range *rules { + if !rule.Cond.Match(req) { + continue + } + m.state.CheckedReq.Inc(1) + for _, blockRule := range rule.BlockRules { + blocked, err := m.handler.HandleBlockJob(blockRule, req) + if err != nil { + m.state.BlockedRuleError.Inc(1) + log.Logger.Debug("ModuleWaf.handleWaf() block job err=%v, rule=%s", err, blockRule) + continue + } + if blocked { + req.ErrCode = ErrWaf + m.state.HitBlockedReq.Inc(1) + return bfe_module.BfeHandlerFinish, nil + } + } + for _, checkRule := range rule.CheckRules { + hit, err := m.handler.HandleCheckJob(checkRule, req) + if err != nil { + m.state.CheckedRuleError.Inc(1) + log.Logger.Debug("ModuleWaf.handleWaf() checkjob err=%v, rule=%s", err, checkRule) + continue + } + if hit { + m.state.HitCheckedRule.Inc(1) + } + } + break + } + return bfe_module.BfeHandlerGoOn, nil +} + +func (m *ModuleWaf) Init(cbs *bfe_module.BfeCallbacks, whs *web_monitor.WebHandlers, cr string) error { + var err error + + confPath := bfe_module.ModConfPath(cr, m.Name()) + if m.conf, err = ConfLoad(confPath, cr); err != nil { + return fmt.Errorf("%s: conf load err %v", m.name, err) + } + + if err = m.loadProductRuleConf(nil); err != nil { + return fmt.Errorf("%s: loadProductRuleConf() err %v", m.Name(), err) + } + + if err = m.handler.Init(m.conf); err != nil { + return fmt.Errorf("%s: handler.Init() err %v", m.Name(), err) + } + + err = cbs.AddFilter(bfe_module.HandleFoundProduct, m.handleWaf) + if err != nil { + return fmt.Errorf("%s.Init(): AddFilter(m.handleWaf): %v", m.name, err) + } + + err = web_monitor.RegisterHandlers(whs, web_monitor.WebHandleMonitor, m.monitorHandlers()) + if err != nil { + return fmt.Errorf("%s.Init(): RegisterHandlers(m.monitorHandlers): %v", m.Name(), err) + } + + err = web_monitor.RegisterHandlers(whs, web_monitor.WebHandleReload, m.reloadHandlers()) + if err != nil { + return fmt.Errorf("%s.Init(): RegisterHandlers(m.reloadHandlerr): %v", m.Name(), err) + } + return nil +} diff --git a/bfe_modules/mod_waf/mod_waf_test.go b/bfe_modules/mod_waf/mod_waf_test.go new file mode 100644 index 000000000..a1f5621f0 --- /dev/null +++ b/bfe_modules/mod_waf/mod_waf_test.go @@ -0,0 +1,82 @@ +// Copyright (c) 2020 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package mod_waf + +import ( + "net/url" + "os" + "testing" +) + +import ( + "github.com/baidu/go-lib/web-monitor/web_monitor" +) + +import ( + "github.com/bfenetworks/bfe/bfe_basic" + "github.com/bfenetworks/bfe/bfe_http" + "github.com/bfenetworks/bfe/bfe_module" +) + +func getModWaf() *ModuleWaf { + mw := NewModuleWaf() + cbs := bfe_module.NewBfeCallbacks() + whs := web_monitor.NewWebHandlers() + cr := "./testdata" + if err := mw.Init(cbs, whs, cr); err != nil { + return nil + } + return mw +} +func prepareRequest(product, path string) *bfe_basic.Request { + req := new(bfe_basic.Request) + req.HttpRequest = new(bfe_http.Request) + req.HttpRequest.Header = make(bfe_http.Header) + req.Route.Product = product + req.Session = new(bfe_basic.Session) + req.Context = make(map[interface{}]interface{}) + req.HttpRequest.URL = &url.URL{} + req.HttpRequest.URL.Path = path + return req +} + +func TestModuleWafHandleWaf(t *testing.T) { + mw := getModWaf() + defer os.RemoveAll(mw.conf.Log.LogDir) + req := prepareRequest("unittest", "/md") + ret, _ := mw.handleWaf(req) + if ret != bfe_module.BfeHandlerGoOn { + t.Errorf("handleWaf(), got=%v, want=%v", ret, bfe_module.BfeHandlerGoOn) + } + + reqBashcmd := prepareRequest("unittest", "/md") + reqBashcmd.HttpRequest.Header["user-agent"] = []string{"() { :; }; echo; echo; rm -rf ./*"} + ret, _ = mw.handleWaf(reqBashcmd) + if ret != bfe_module.BfeHandlerFinish { + t.Errorf("handleWaf(), got=%v, want=%v", ret, bfe_module.BfeHandlerFinish) + } + + queryV := map[string][]string{"path": {"./testdata/mod_waf/waf_rule_check.data"}} + err := mw.loadProductRuleConf(queryV) + if err != nil { + t.Errorf("reload waf rule err=%s", err) + t.FailNow() + } + + ret, _ = mw.handleWaf(reqBashcmd) + if ret != bfe_module.BfeHandlerGoOn { + t.Errorf("handleWaf(), got=%v, want=%v", ret, bfe_module.BfeHandlerGoOn) + } + +} diff --git a/bfe_modules/mod_waf/testdata/mod_waf.conf b/bfe_modules/mod_waf/testdata/mod_waf.conf new file mode 100644 index 000000000..1fede351d --- /dev/null +++ b/bfe_modules/mod_waf/testdata/mod_waf.conf @@ -0,0 +1,8 @@ +[basic] +ProductRulePath = mod_waf/waf_rule.data + +[Log] +LogPrefix = waf +LogDir = ../log +RotateWhen = NEXTHOUR +BackUpCount = 24 diff --git a/bfe_modules/mod_waf/testdata/mod_waf/mod_waf.conf b/bfe_modules/mod_waf/testdata/mod_waf/mod_waf.conf new file mode 100644 index 000000000..1fede351d --- /dev/null +++ b/bfe_modules/mod_waf/testdata/mod_waf/mod_waf.conf @@ -0,0 +1,8 @@ +[basic] +ProductRulePath = mod_waf/waf_rule.data + +[Log] +LogPrefix = waf +LogDir = ../log +RotateWhen = NEXTHOUR +BackUpCount = 24 diff --git a/bfe_modules/mod_waf/testdata/mod_waf/waf_rule.data b/bfe_modules/mod_waf/testdata/mod_waf/waf_rule.data new file mode 100644 index 000000000..229203a25 --- /dev/null +++ b/bfe_modules/mod_waf/testdata/mod_waf/waf_rule.data @@ -0,0 +1,13 @@ +{ + "Version": "2019-12-10184356", + "Config": { + "unittest": [ + { + "Cond": "default_t()", + "BlockRules": [ + "RuleBashCmd" + ] + } + ] + } +} diff --git a/bfe_modules/mod_waf/testdata/mod_waf/waf_rule_check.data b/bfe_modules/mod_waf/testdata/mod_waf/waf_rule_check.data new file mode 100644 index 000000000..bba8446e9 --- /dev/null +++ b/bfe_modules/mod_waf/testdata/mod_waf/waf_rule_check.data @@ -0,0 +1,13 @@ +{ + "Version": "2019-12-11184356", + "Config": { + "unittest": [ + { + "Cond": "default_t()", + "CheckRules": [ + "RuleBashCmd" + ] + } + ] + } +} diff --git a/bfe_modules/mod_waf/testdata/waf_rule.data b/bfe_modules/mod_waf/testdata/waf_rule.data new file mode 100644 index 000000000..309a4454a --- /dev/null +++ b/bfe_modules/mod_waf/testdata/waf_rule.data @@ -0,0 +1,13 @@ +{ + "Version": "2019-12-10184356", + "Config": { + "example_product": [ + { + "Cond": "default_t()", + "BlockRules": [ + "RuleBashCmd" + ] + } + ] + } +} diff --git a/bfe_modules/mod_waf/testdata/waf_rule_both_block_rules.data b/bfe_modules/mod_waf/testdata/waf_rule_both_block_rules.data new file mode 100644 index 000000000..bf63a6907 --- /dev/null +++ b/bfe_modules/mod_waf/testdata/waf_rule_both_block_rules.data @@ -0,0 +1,15 @@ +{ + "Version": "2019-12-10184356", + "Config": { + "example_product": [ + { + "Cond": "default_t()", + "BlockRules": [ + "invalid rule" + ], + "CheckRules": [ + ] + } + ] + } +} diff --git a/bfe_modules/mod_waf/testdata/waf_rule_both_check_rules.data b/bfe_modules/mod_waf/testdata/waf_rule_both_check_rules.data new file mode 100644 index 000000000..86a24c740 --- /dev/null +++ b/bfe_modules/mod_waf/testdata/waf_rule_both_check_rules.data @@ -0,0 +1,16 @@ +{ + "Version": "2019-12-10184356", + "Config": { + "example_product": [ + { + "Cond": "default_t()", + "BlockRules": [ + + ], + "CheckRules": [ + "invalid rule" + ] + } + ] + } +} diff --git a/bfe_modules/mod_waf/testdata/waf_rule_both_empty.data b/bfe_modules/mod_waf/testdata/waf_rule_both_empty.data new file mode 100644 index 000000000..dceb10550 --- /dev/null +++ b/bfe_modules/mod_waf/testdata/waf_rule_both_empty.data @@ -0,0 +1,14 @@ +{ + "Version": "2019-12-10184356", + "Config": { + "example_product": [ + { + "Cond": "default_t()", + "BlockRules": [ + ], + "CheckRules": [ + ] + } + ] + } +} diff --git a/bfe_modules/mod_waf/testdata/waf_rule_both_nil.data b/bfe_modules/mod_waf/testdata/waf_rule_both_nil.data new file mode 100644 index 000000000..49ad9b83c --- /dev/null +++ b/bfe_modules/mod_waf/testdata/waf_rule_both_nil.data @@ -0,0 +1,10 @@ +{ + "Version": "2019-12-10184356", + "Config": { + "example_product": [ + { + "Cond": "default_t()" + } + ] + } +} diff --git a/bfe_modules/mod_waf/testdata/waf_rule_invalid_cond.data b/bfe_modules/mod_waf/testdata/waf_rule_invalid_cond.data new file mode 100644 index 000000000..4310f1e22 --- /dev/null +++ b/bfe_modules/mod_waf/testdata/waf_rule_invalid_cond.data @@ -0,0 +1,13 @@ +{ + "Version": "2019-12-10184356", + "Config": { + "example_product": [ + { + "Cond": "default()", + "BlockRules": [ + "RuleBashCmd" + ] + } + ] + } +} diff --git a/bfe_modules/mod_waf/testdata/waf_rule_invalid_json.data b/bfe_modules/mod_waf/testdata/waf_rule_invalid_json.data new file mode 100644 index 000000000..aed6e5719 --- /dev/null +++ b/bfe_modules/mod_waf/testdata/waf_rule_invalid_json.data @@ -0,0 +1,13 @@ +{ + "Version": "2019-12-10184356", + "Config": { + "example_product": [ + { + "Cond": "default_t()" + "BlockRules": [ + "RuleBashCmd" + ] + } + ] + } +} diff --git a/bfe_modules/mod_waf/testdata/waf_rule_no_config.data b/bfe_modules/mod_waf/testdata/waf_rule_no_config.data new file mode 100644 index 000000000..37f393ad4 --- /dev/null +++ b/bfe_modules/mod_waf/testdata/waf_rule_no_config.data @@ -0,0 +1,3 @@ +{ + "Version": "2019-12-10184356" +} diff --git a/bfe_modules/mod_waf/testdata/waf_rule_no_version.data b/bfe_modules/mod_waf/testdata/waf_rule_no_version.data new file mode 100644 index 000000000..384d46243 --- /dev/null +++ b/bfe_modules/mod_waf/testdata/waf_rule_no_version.data @@ -0,0 +1,12 @@ +{ + "Config": { + "example_product": [ + { + "Cond": "default_t()", + "BlockRules": [ + "RuleBashCmd" + ] + } + ] + } +} diff --git a/bfe_modules/mod_waf/waf_handler.go b/bfe_modules/mod_waf/waf_handler.go new file mode 100644 index 000000000..7d0274478 --- /dev/null +++ b/bfe_modules/mod_waf/waf_handler.go @@ -0,0 +1,78 @@ +// Copyright (c) 2020 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package mod_waf + +import ( + "fmt" +) + +import ( + "github.com/baidu/go-lib/log" +) + +import ( + "github.com/bfenetworks/bfe/bfe_basic" + "github.com/bfenetworks/bfe/bfe_modules/mod_waf/waf_rule" +) + +type wafHandler struct { + wafLogger *wafLogger // waf detail log in the waf logger + + wafTable *waf_rule.WafRuleTable // waf table, which do the check stuff +} + +func NewWafHandler() *wafHandler { + wh := new(wafHandler) + return wh +} + +func (wh *wafHandler) Init(conf *ConfModWaf) error { + wh.wafLogger = NewWafLogger() + err := wh.wafLogger.Init(conf) + if err != nil { + return err + } + + wh.wafTable = waf_rule.NewWafRuleTable() + wh.wafTable.Init() + return err +} + +func (wh *wafHandler) HandleBlockJob(rule string, req *bfe_basic.Request) (bool, error) { + if !waf_rule.IsValidRule(rule) { + return false, fmt.Errorf("HandleBlockJob() err=unknown rule: %s", rule) + } + job := NewWafJob(req, rule, BlockType) + return wh.doJob(job) +} + +func (wh *wafHandler) HandleCheckJob(rule string, req *bfe_basic.Request) (bool, error) { + if !waf_rule.IsValidRule(rule) { + return false, fmt.Errorf("HandleCheckJob() err=unknown rule: %s", rule) + } + job := NewWafJob(req, rule, CheckType) + return wh.doJob(job) +} + +func (wh *wafHandler) doJob(job *wafJob) (bool, error) { + wafRule, ok := wh.wafTable.GetRule(job.Rule) + if !ok { + return true, fmt.Errorf("wafHandler.doJob(), err=invalid rule %s", job.Rule) + } + log.Logger.Debug("wafHandler.doJob() %v rule=%s", job.RuleRequest, job.Rule) + hit := wafRule.Check(job.RuleRequest) + job.SetHit(hit) + wh.wafLogger.DumpLog(job) + return hit, nil +} diff --git a/bfe_modules/mod_waf/waf_job.go b/bfe_modules/mod_waf/waf_job.go new file mode 100644 index 000000000..f580424f2 --- /dev/null +++ b/bfe_modules/mod_waf/waf_job.go @@ -0,0 +1,47 @@ +// Copyright (c) 2020 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package mod_waf + +import ( + "github.com/bfenetworks/bfe/bfe_basic" + "github.com/bfenetworks/bfe/bfe_modules/mod_waf/waf_rule" + "github.com/bfenetworks/bfe/bfe_util/json" +) + +const ( + CheckType = "Check" // check job is async job, which just check and log, but never block + BlockType = "Block" // block job is sync job, which check, log and maybe block +) + +type wafJob struct { + Rule string // rule name of this job + Type string // type of this job + Hit bool // is job hit rule + RuleRequest *waf_rule.RuleRequestInfo // waf check request info +} + +func NewWafJob(req *bfe_basic.Request, rule string, jtype string) *wafJob { + wj := new(wafJob) + wj.Rule = rule + wj.Type = jtype + wj.RuleRequest = waf_rule.NewRuleRequestInfo(req) + return wj +} + +func (j *wafJob) SetHit(hit bool) { j.Hit = hit } + +func (j *wafJob) String() string { + bytes, _ := json.Marshal(*j) + return string(bytes) +} diff --git a/bfe_modules/mod_waf/waf_log.go b/bfe_modules/mod_waf/waf_log.go new file mode 100644 index 000000000..5401d1711 --- /dev/null +++ b/bfe_modules/mod_waf/waf_log.go @@ -0,0 +1,50 @@ +// Copyright (c) 2020 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package mod_waf + +import ( + "fmt" +) + +import ( + "github.com/baidu/go-lib/log/log4go" +) + +import ( + "github.com/bfenetworks/bfe/bfe_util/access_log" +) + +type wafLogger struct { + log log4go.Logger // wrapper log info +} + +func NewWafLogger() *wafLogger { + return new(wafLogger) +} + +func (wf *wafLogger) Init(conf *ConfModWaf) error { + var err error + // WAF LOG Demo:[2020/08/25 13:40:58 CST] [INFO] [69613] {"Rule":"RuleBashCmd","Type":"Block","Hit":true ... + logFormatter := "[%D %T] [%L] [%P] %M" + wf.log, err = access_log.LoggerInitWithFormat(conf.Log.LogPrefix, conf.Log.LogDir, + conf.Log.RotateWhen, conf.Log.BackupCount, logFormatter) + if err != nil { + return fmt.Errorf("WafLogger.Init(): create logger error:%v", err) + } + return nil +} + +func (wl *wafLogger) DumpLog(v interface{}) { + wl.log.Info("%s", v) +} diff --git a/bfe_modules/mod_waf/waf_rule/rule_bash_cmd.go b/bfe_modules/mod_waf/waf_rule/rule_bash_cmd.go new file mode 100644 index 000000000..4711f65b2 --- /dev/null +++ b/bfe_modules/mod_waf/waf_rule/rule_bash_cmd.go @@ -0,0 +1,186 @@ +// Copyright (c) 2020 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +/* +This rule is used to detect exploitation of "Shellshock" GNU Bash RCE vulnerability. +ModSecurity rule see: https://github.com/coreruleset/coreruleset/blob/v3.4/dev/rules/REQUEST-932-APPLICATION-ATTACK-RCE.conf + +# [ Shellshock vulnerability (CVE-2014-6271 and CVE-2014-7169) ] +# Detect exploitation of "Shellshock" GNU Bash RCE vulnerability. +# +# Based on ModSecurity rules created by Red Hat. +# +SecRule REQUEST_HEADERS|REQUEST_LINE "@rx ^\(\s*\)\s+{" \ + "id:932170,\ + phase:2,\ + block,\ + capture,\ + t:none,t:urlDecode,\ + msg:'Remote Command Execution: Shellshock (CVE-2014-6271)',\ + logdata:'Matched Data: %{TX.0} found within %{MATCHED_VAR_NAME}: %{MATCHED_VAR}',\ + tag:'application-multi',\ + tag:'language-shell',\ + tag:'platform-unix',\ + tag:'attack-rce',\ + tag:'paranoia-level/1',\ + tag:'OWASP_CRS',\ + tag:'capec/1000/152/248/88',\ + tag:'PCI/6.5.2',\ + ctl:auditLogParts=+E,\ + ver:'OWASP_CRS/3.3.0',\ + severity:'CRITICAL',\ + setvar:'tx.rce_score=+%{tx.critical_anomaly_score}',\ + setvar:'tx.anomaly_score_pl1=+%{tx.critical_anomaly_score}'" + +SecRule ARGS_NAMES|ARGS|FILES_NAMES "@rx ^\(\s*\)\s+{" \ + "id:932171,\ + phase:2,\ + block,\ + capture,\ + t:none,t:urlDecode,t:urlDecodeUni,\ + msg:'Remote Command Execution: Shellshock (CVE-2014-6271)',\ + logdata:'Matched Data: %{TX.0} found within %{MATCHED_VAR_NAME}: %{MATCHED_VAR}',\ + tag:'application-multi',\ + tag:'language-shell',\ + tag:'platform-unix',\ + tag:'attack-rce',\ + tag:'paranoia-level/1',\ + tag:'OWASP_CRS',\ + tag:'capec/1000/152/248/88',\ + tag:'PCI/6.5.2',\ + ctl:auditLogParts=+E,\ + ver:'OWASP_CRS/3.3.0',\ + severity:'CRITICAL',\ + setvar:'tx.rce_score=+%{tx.critical_anomaly_score}',\ + setvar:'tx.anomaly_score_pl1=+%{tx.critical_anomaly_score}'" +*/ + +package waf_rule + +import "strings" + +type RuleBashCmdExe struct { +} + +func NewRuleBashCmdExe() *RuleBashCmdExe { + rule := new(RuleBashCmdExe) + return rule +} + +func (rule *RuleBashCmdExe) Init() error { + return nil +} + +func (rule *RuleBashCmdExe) Check(req *RuleRequestInfo) bool { + return ruleBashCmdExeCheck(req) +} + +func (rule *RuleBashCmdExe) CheckString(pStr *string) bool { + return checkHeaderValue(*pStr) +} + +// checkSemicolon check if first non-space/tab char is ";" +func checkSemicolon(value string) bool { + length := len(value) + + for i := 0; i < length; i++ { + if value[i] == ' ' || value[i] == '\t' { + continue + } else if value[i] != ';' { + return false + } else { + return true + } + } + + return false +} + +// checkHeaderValueContent check if header value content matches the spedific rules +func checkHeaderValueContent(value string) bool { + index := strings.Index(value, "}") + if index != -1 { + if checkSemicolon(value[index+1:]) { + return true + } + } + + return false +} + +// checkSpecificChar check if value started with the specific char +func checkSpecificChar(value string, c string) (int, bool) { + length := len(value) + + for i := 0; i < length; i++ { + if value[i] == ' ' || value[i] == '\t' { + continue + } else if value[i] != c[0] { + return -1, false + } else { + return i, true + } + } + + return -1, false +} + +// checkHeaderValuePrefix check if header value matches "^\s+\(\s+\)\s+{" +func checkHeaderValuePrefix(value string) (int, bool) { + var index, gIndex int + var hit bool + + index, hit = checkSpecificChar(value[gIndex:], "(") + if !hit { + return -1, false + } + + gIndex += index + 1 + index, hit = checkSpecificChar(value[gIndex:], ")") + if !hit { + return -1, false + } + + gIndex += index + 1 + index, hit = checkSpecificChar(value[gIndex:], "{") + if !hit { + return -1, false + } + + gIndex += index + return gIndex, true +} + +// checkHeaderValue check header value +func checkHeaderValue(value string) bool { + index, hit := checkHeaderValuePrefix(value) + if hit { + if checkHeaderValueContent(value[index+1:]) { + return true + } + } + + return false +} + +func ruleBashCmdExeCheck(req *RuleRequestInfo) bool { + for _, values := range req.Headers { + for _, value := range values { + if checkHeaderValue(value) { + return true + } + } + } + + return false +} diff --git a/bfe_modules/mod_waf/waf_rule/rule_bash_cmd_test.go b/bfe_modules/mod_waf/waf_rule/rule_bash_cmd_test.go new file mode 100644 index 000000000..d83921410 --- /dev/null +++ b/bfe_modules/mod_waf/waf_rule/rule_bash_cmd_test.go @@ -0,0 +1,312 @@ +// Copyright (c) 2020 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package waf_rule + +import "testing" + +// hit cases +func TestCheckSemicolonCase0(t *testing.T) { + var s string + + s = ";" + if !checkSemicolon(s) { + t.Errorf("TestCheckSemicolonCase0(): string \"%s\" should hit!", s) + } + + s = " ;" + if !checkSemicolon(s) { + t.Errorf("TestCheckSemicolonCase0(): string \"%s\" should hit!", s) + } + + s = "\t;" + if !checkSemicolon(s) { + t.Errorf("TestCheckSemicolonCase0(): string \"%s\" should hit!", s) + } + + s = " \t \t;" + if !checkSemicolon(s) { + t.Errorf("TestCheckSemicolonCase0(): string \"%s\" should hit!", s) + } +} + +// no hit cases +func TestCheckSemicolonCase1(t *testing.T) { + var s string + + s = "" + if checkSemicolon(s) { + t.Errorf("TestCheckSemicolonCase1(): string \"%s\" should not hit!", s) + } + + s = "123" + if checkSemicolon(s) { + t.Errorf("TestCheckSemicolonCase1(): string \"%s\" should not hit!", s) + } + + s = "a;" + if checkSemicolon(s) { + t.Errorf("TestCheckSemicolonCase1(): string \"%s\" should not hit!", s) + } + + s = "a ;" + if checkSemicolon(s) { + t.Errorf("TestCheckSemicolonCase1(): string \"%s\" should not hit!", s) + } +} + +// hit cases +func TestCheckHeaderValueContentCase0(t *testing.T) { + var s string + + s = "};" + if !checkHeaderValueContent(s) { + t.Errorf("TestCheckHeaderValueContentCase0(): string \"%s\" should hit!", s) + } + + s = "12} ;" + if !checkHeaderValueContent(s) { + t.Errorf("TestCheckHeaderValueContentCase0(): string \"%s\" should hit!", s) + } + + s = " }\t;" + if !checkHeaderValueContent(s) { + t.Errorf("TestCheckHeaderValueContentCase0(): string \"%s\" should hit!", s) + } +} + +// no hit cases +func TestCheckHeaderValueContentCase1(t *testing.T) { + var s string + + s = "" + if checkHeaderValueContent(s) { + t.Errorf("TestCheckHeaderValueContentCase1(): string \"%s\" should not hit!", s) + } + + s = "}" + if checkHeaderValueContent(s) { + t.Errorf("TestCheckHeaderValueContentCase1(): string \"%s\" should not hit!", s) + } + + s = " }\t1;" + if checkHeaderValueContent(s) { + t.Errorf("TestCheckHeaderValueContentCase1(): string \"%s\" should not hit!", s) + } + + s = " }1;" + if checkHeaderValueContent(s) { + t.Errorf("TestCheckHeaderValueContentCase1(): string \"%s\" should not hit!", s) + } +} + +// hit cases +func TestCheckSpecificCharCase0(t *testing.T) { + var s string + var c string + var i int + var hit bool + + c = "(" + + s = "(" + i, hit = checkSpecificChar(s, c) + if !hit || i != 0 { + t.Errorf("TestCheckSpecificCharCase0(): string \"%s\" should hit!", s) + } + + s = " (" + i, hit = checkSpecificChar(s, c) + if !hit || i != 1 { + t.Errorf("TestCheckSpecificCharCase0(): string \"%s\" should hit!", s) + } + + s = "\t(" + i, hit = checkSpecificChar(s, c) + if !hit || i != 1 { + t.Errorf("TestCheckSpecificCharCase0(): string \"%s\" should hit!", s) + } + + s = " \t(" + i, hit = checkSpecificChar(s, c) + if !hit || i != 2 { + t.Errorf("TestCheckSpecificCharCase0(): string \"%s\" should hit!", s) + } +} + +// no hit cases +func TestCheckSpecificCharCase1(t *testing.T) { + var s string + var c string + var hit bool + + c = "(" + + s = "" + _, hit = checkSpecificChar(s, c) + if hit { + t.Errorf("TestCheckSpecificCharCase1(): string \"%s\" should no thit!", s) + } + + s = "i" + _, hit = checkSpecificChar(s, c) + if hit { + t.Errorf("TestCheckSpecificCharCase1(): string \"%s\" should no thit!", s) + } + + s = "1(" + _, hit = checkSpecificChar(s, c) + if hit { + t.Errorf("TestCheckSpecificCharCase1(): string \"%s\" should no thit!", s) + } + + s = " 1(" + _, hit = checkSpecificChar(s, c) + if hit { + t.Errorf("TestCheckSpecificCharCase1(): string \"%s\" should no thit!", s) + } + + s = "1 (" + _, hit = checkSpecificChar(s, c) + if hit { + t.Errorf("TestCheckSpecificCharCase1(): string \"%s\" should no thit!", s) + } +} + +// hit cases +func TestCheckHeaderValuePrefixCase0(t *testing.T) { + var s string + var hit bool + var i int + + s = "(){" + i, hit = checkHeaderValuePrefix(s) + if !hit || i != 2 { + t.Errorf("TestCheckHeaderValuePrefixCase0(): string \"%s\" should hit!", s) + } + + s = " (){" + i, hit = checkHeaderValuePrefix(s) + if !hit || i != 3 { + t.Errorf("TestCheckHeaderValuePrefixCase0(): string \"%s\" should hit!", s) + } + + s = " (\t){" + i, hit = checkHeaderValuePrefix(s) + if !hit || i != 4 { + t.Errorf("TestCheckHeaderValuePrefixCase0(): string \"%s\" should hit!", s) + } + + s = " ( )\t{" + i, hit = checkHeaderValuePrefix(s) + if !hit || i != 5 { + t.Errorf("TestCheckHeaderValuePrefixCase0(): string \"%s\" should hit!", s) + } + + s = "\t( ) { " + i, hit = checkHeaderValuePrefix(s) + if !hit || i != 5 { + t.Errorf("TestCheckHeaderValuePrefixCase0(): string \"%s\" should hit!", s) + } +} + +// no hit cases +func TestCheckHeaderValuePrefix_case1(t *testing.T) { + var s string + var hit bool + + s = "" + _, hit = checkHeaderValuePrefix(s) + if hit { + t.Errorf("TestCheckHeaderValuePrefix_case1(): string \"%s\" should not hit!", s) + } + + s = "1(){" + _, hit = checkHeaderValuePrefix(s) + if hit { + t.Errorf("TestCheckHeaderValuePrefix_case1(): string \"%s\" should not hit!", s) + } + + s = " (1){" + _, hit = checkHeaderValuePrefix(s) + if hit { + t.Errorf("TestCheckHeaderValuePrefix_case1(): string \"%s\" should not hit!", s) + } + + s = " ()x{" + _, hit = checkHeaderValuePrefix(s) + if hit { + t.Errorf("TestCheckHeaderValuePrefix_case1(): string \"%s\" should not hit!", s) + } +} + +// hit cases +func TestCheckHeaderValue_case0(t *testing.T) { + var s string + var hit bool + + s = "(){};" + hit = checkHeaderValue(s) + if !hit { + t.Errorf("TestCheckHeaderValuePrefixCase0(): string \"%s\" should hit!", s) + } + + s = "(){xx};" + hit = checkHeaderValue(s) + if !hit { + t.Errorf("TestCheckHeaderValuePrefixCase0(): string \"%s\" should hit!", s) + } + + s = "(){xx} ;" + hit = checkHeaderValue(s) + if !hit { + t.Errorf("TestCheckHeaderValuePrefixCase0(): string \"%s\" should hit!", s) + } + + s = "(){xx}\t;" + hit = checkHeaderValue(s) + if !hit { + t.Errorf("TestCheckHeaderValuePrefixCase0(): string \"%s\" should hit!", s) + } + + s = "(){xx} \t;" + hit = checkHeaderValue(s) + if !hit { + t.Errorf("TestCheckHeaderValuePrefixCase0(): string \"%s\" should hit!", s) + } +} + +// no hit cases +func TestCheckHeaderValueCase1(t *testing.T) { + var s string + var hit bool + + s = "(){}1;" + hit = checkHeaderValue(s) + if hit { + t.Errorf("TestCheckHeaderValuePrefixCase0(): string \"%s\" should not hit!", s) + } + + s = "(){;" + hit = checkHeaderValue(s) + if hit { + t.Errorf("TestCheckHeaderValuePrefixCase0(): string \"%s\" should not hit!", s) + } + + s = "(){}" + hit = checkHeaderValue(s) + if hit { + t.Errorf("TestCheckHeaderValuePrefixCase0(): string \"%s\" should not hit!", s) + } +} diff --git a/bfe_modules/mod_waf/waf_rule/rule_request_info.go b/bfe_modules/mod_waf/waf_rule/rule_request_info.go new file mode 100644 index 000000000..9d0952860 --- /dev/null +++ b/bfe_modules/mod_waf/waf_rule/rule_request_info.go @@ -0,0 +1,46 @@ +// Copyright (c) 2020 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package waf_rule + +import ( + "net/url" +) +import ( + "github.com/bfenetworks/bfe/bfe_basic" +) + +type RuleRequestInfo struct { + Method string // "GET", "POST", "PUT", "DELETE" + Version string // "HTTP_1_0", "HTTP_1_1" + + Headers map[string][]string //Header + + Uri string // uri + UriUnquote string // unquoted uri + UriParsed *url.URL // parsed uri + + QueryValues url.Values // parsed query string values +} + +func NewRuleRequestInfo(req *bfe_basic.Request) *RuleRequestInfo { + wj := new(RuleRequestInfo) + wj.Method = req.HttpRequest.Method + wj.Uri = req.HttpRequest.RequestURI + wj.Headers = req.HttpRequest.Header + + wj.UriUnquote, _ = url.QueryUnescape(wj.Uri) + wj.UriParsed, _ = url.Parse(wj.Uri) + wj.QueryValues = wj.UriParsed.Query() + return wj +} diff --git a/bfe_modules/mod_waf/waf_rule/waf_rule.go b/bfe_modules/mod_waf/waf_rule/waf_rule.go new file mode 100644 index 000000000..58a7ed8a7 --- /dev/null +++ b/bfe_modules/mod_waf/waf_rule/waf_rule.go @@ -0,0 +1,57 @@ +// Copyright (c) 2020 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package waf_rule + +const ( + RuleBashCmd = "RuleBashCmd" // bash cmd +) + +var implementedRule = map[string]WafRule{ + RuleBashCmd: NewRuleBashCmdExe(), +} + +func IsValidRule(rule string) bool { + _, ok := implementedRule[rule] + return ok +} + +type WafRule interface { + Init() error + Check(req *RuleRequestInfo) bool +} + +type WafRuleTable struct { + rules map[string]WafRule // name to WafRule +} + +func NewWafRuleTable() *WafRuleTable { + wafRules := new(WafRuleTable) + + wafRules.rules = make(map[string]WafRule) + return wafRules +} + +func (wr *WafRuleTable) Init() { + for k, v := range implementedRule { + wr.rules[k] = v + } + for _, v := range wr.rules { + v.Init() + } +} + +func (wr *WafRuleTable) GetRule(ruleName string) (WafRule, bool) { + rule, ok := wr.rules[ruleName] + return rule, ok +} diff --git a/bfe_modules/mod_waf/waf_rule/waf_rule_test.go b/bfe_modules/mod_waf/waf_rule/waf_rule_test.go new file mode 100644 index 000000000..22a991026 --- /dev/null +++ b/bfe_modules/mod_waf/waf_rule/waf_rule_test.go @@ -0,0 +1,157 @@ +// Copyright (c) 2020 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package waf_rule + +import ( + "reflect" + "testing" +) + +func TestIsValidRule(t *testing.T) { + type args struct { + rule string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "normal", + args: args{ + rule: RuleBashCmd, + }, + want: true, + }, + { + name: "abnormal", + args: args{ + rule: "invalid", + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsValidRule(tt.args.rule); got != tt.want { + t.Errorf("IsValidRule() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewWafRuleTable(t *testing.T) { + tests := []struct { + name string + want *WafRuleTable + }{ + { + name: "normal", + want: &WafRuleTable{ + rules: make(map[string]WafRule), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewWafRuleTable(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewWafRuleTable() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestWafRuleTable_Init(t *testing.T) { + type fields struct { + rules map[string]WafRule + } + tests := []struct { + name string + fields fields + }{ + { + name: "normal", + fields: fields{ + rules: map[string]WafRule{ + RuleBashCmd: NewRuleBashCmdExe(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + wr := &WafRuleTable{ + rules: tt.fields.rules, + } + wr.Init() + }) + } +} + +func TestWafRuleTable_GetRule(t *testing.T) { + type fields struct { + rules map[string]WafRule + } + type args struct { + ruleName string + } + tests := []struct { + name string + fields fields + args args + want WafRule + want1 bool + }{ + { + name: "normal", + fields: fields{ + rules: map[string]WafRule{ + RuleBashCmd: NewRuleBashCmdExe(), + }, + }, + args: args{ + ruleName: RuleBashCmd, + }, + want: NewRuleBashCmdExe(), + want1: true, + }, + { + name: "abnormal", + fields: fields{ + rules: map[string]WafRule{ + RuleBashCmd: NewRuleBashCmdExe(), + }, + }, + args: args{ + ruleName: "SQLInjection", + }, + want: nil, + want1: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + wr := &WafRuleTable{ + rules: tt.fields.rules, + } + got, got1 := wr.GetRule(tt.args.ruleName) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("WafRuleTable.GetRule() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("WafRuleTable.GetRule() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} diff --git a/bfe_modules/mod_waf/waf_rule_load.go b/bfe_modules/mod_waf/waf_rule_load.go new file mode 100644 index 000000000..97497f87c --- /dev/null +++ b/bfe_modules/mod_waf/waf_rule_load.go @@ -0,0 +1,190 @@ +// Copyright (c) 2020 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package mod_waf + +import ( + "fmt" + "os" +) + +import ( + "github.com/bfenetworks/bfe/bfe_basic/condition" + "github.com/bfenetworks/bfe/bfe_modules/mod_waf/waf_rule" + "github.com/bfenetworks/bfe/bfe_util/json" +) + +type wafRule struct { + Cond condition.Condition + BlockRules []string + CheckRules []string +} + +type ruleList []*wafRule + +type productWafRule map[string]*ruleList + +type productWafRuleConfig struct { + Version string + Config productWafRule +} + +type wafRuleFile struct { + Cond string + BlockRules []string + CheckRules []string +} + +type ruleListFile []*wafRuleFile + +type productWafRuleFile map[string]*ruleListFile + +type productWafRuleConfigFile struct { + Version *string + Config *productWafRuleFile +} + +func wafRuleConvert(ruleFile *wafRuleFile) (*wafRule, error) { + if ruleFile == nil { + return nil, fmt.Errorf("wafRuleConvert(), err= empty ruleFile") + } + var rule wafRule + var err error + rule.Cond, err = condition.Build(ruleFile.Cond) + if err != nil { + return nil, err + } + rule.BlockRules = make([]string, len(ruleFile.BlockRules)) + rule.CheckRules = make([]string, len(ruleFile.CheckRules)) + + copy(rule.BlockRules, ruleFile.BlockRules) + copy(rule.CheckRules, ruleFile.CheckRules) + return &rule, nil +} + +func productWafRuleConvert(prf *productWafRuleFile) (productWafRule, error) { + var wr productWafRule + wr = make(productWafRule) + if prf == nil { + return nil, fmt.Errorf("ruleConvert(), err= empty productWafRuleFile") + } + + for product, fruleList := range *prf { + rlist := make(ruleList, 0) + for _, frule := range *fruleList { + rule, err := wafRuleConvert(frule) + if err != nil { + return nil, fmt.Errorf("ruleConvert(), err=%s", err) + } + rlist = append(rlist, rule) + } + wr[product] = &rlist + } + return wr, nil +} + +func wafRuleFileCheck(conf *wafRuleFile) error { + if conf == nil { + return fmt.Errorf("wafRuleFileCheck(), err=nil config") + } + if len(conf.Cond) == 0 { + return fmt.Errorf("wafRuleFileCheck(), err=empty cond") + } + if len(conf.BlockRules) == 0 && len(conf.CheckRules) == 0 { + return fmt.Errorf("wafRuleFileCheck(), err=block rules and check rule both empty") + } + if len(conf.BlockRules) != 0 { + for _, rule := range conf.BlockRules { + if !waf_rule.IsValidRule(rule) { + return fmt.Errorf("wafRuleFileCheck(), err:= unknow rule %s", rule) + } + } + } + if len(conf.CheckRules) != 0 { + for _, rule := range conf.CheckRules { + if !waf_rule.IsValidRule(rule) { + return fmt.Errorf("wafRuleFileCheck(), err:= unknow rule %s", rule) + } + } + } + return nil +} + +func ruleListFileCheck(conf *ruleListFile) error { + if conf == nil { + return fmt.Errorf("ruleListFileCheck(), err=nil config") + } + for index, rule := range *conf { + if err := wafRuleFileCheck(rule); err != nil { + return fmt.Errorf("ruleListFileCheck(), err=%d, %s", index, err) + } + } + return nil +} + +func productWafRuleFileCheck(conf *productWafRuleFile) error { + if conf == nil { + return fmt.Errorf("productWafRuleFileCheck(), err=nil config") + } + for product, ruleList := range *conf { + if ruleList == nil { + return fmt.Errorf("productWafRuleFileCheck(), err=product[%s] has empty rulelist", product) + } + err := ruleListFileCheck(ruleList) + if err != nil { + return err + } + } + return nil +} + +func productWafRuleConfFileCheck(conf *productWafRuleConfigFile) error { + if conf == nil { + return fmt.Errorf("productWafRuleConfFileCheck(), err=nil config") + } + + if conf.Version == nil { + return fmt.Errorf("productWafRuleConfFileCheck(), err=no version") + } + if conf.Config == nil { + return fmt.Errorf("productWafRuleConfFileCheck(), err=no Config") + } + + return productWafRuleFileCheck(conf.Config) +} + +func ProductWafRuleConfLoad(fileName string) (productWafRuleConfig, error) { + var conf productWafRuleConfig + var fileConf productWafRuleConfigFile + + f, err := os.Open(fileName) + if err != nil { + return conf, fmt.Errorf("ProductWafRuleConfLoad(), err=%s", err) + } + defer f.Close() + err = json.NewDecoder(f).Decode(&fileConf) + if err != nil { + return conf, fmt.Errorf("ProductWafRuleConfLoad(), err=%s", err) + } + err = productWafRuleConfFileCheck(&fileConf) + if err != nil { + return conf, fmt.Errorf("ProductWafRuleConfLoad(), err=%s", err) + } + pwr, err := productWafRuleConvert(fileConf.Config) + if err != nil { + return conf, fmt.Errorf("ProductWafRuleConfLoad(), err=%s", err) + } + conf.Version = *fileConf.Version + conf.Config = pwr + return conf, nil +} diff --git a/bfe_modules/mod_waf/waf_rule_load_test.go b/bfe_modules/mod_waf/waf_rule_load_test.go new file mode 100644 index 000000000..7dabb5714 --- /dev/null +++ b/bfe_modules/mod_waf/waf_rule_load_test.go @@ -0,0 +1,151 @@ +// Copyright (c) 2020 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package mod_waf + +import ( + "reflect" + "testing" +) + +import ( + "github.com/bfenetworks/bfe/bfe_basic/condition" +) + +func TestProductWafRuleConfLoad(t *testing.T) { + cond, err := condition.Build("default_t()") + if err != nil { + t.FailNow() + } + type args struct { + fileName string + } + tests := []struct { + name string + args args + want productWafRuleConfig + wantErr bool + }{ + { + name: "normal", + args: args{ + fileName: "./testdata/waf_rule.data", + }, + want: productWafRuleConfig{ + Version: "2019-12-10184356", + Config: map[string]*ruleList{ + "example_product": &ruleList{&wafRule{ + Cond: cond, + CheckRules: []string{}, + BlockRules: []string{ + "RuleBashCmd", + }, + }}, + }, + }, + wantErr: false, + }, + { + name: "not_exist_waf_rule", + args: args{ + fileName: "./testdata/not_exist_waf_rule.data", + }, + want: *new(productWafRuleConfig), + wantErr: true, + }, + { + name: "waf_rule_invalid_json", + args: args{ + fileName: "./testdata/waf_rule_invalid_json.data", + }, + want: *new(productWafRuleConfig), + wantErr: true, + }, + { + name: "waf_rule_no_version", + args: args{ + fileName: "./testdata/waf_rule_no_version.data", + }, + want: *new(productWafRuleConfig), + wantErr: true, + }, + { + name: "waf_rule_no_config", + args: args{ + fileName: "./testdata/waf_rule_no_config.data", + }, + want: *new(productWafRuleConfig), + wantErr: true, + }, + { + name: "waf_rule_invalid_json", + args: args{ + fileName: "./testdata/waf_rule_invalid_json.data", + }, + want: *new(productWafRuleConfig), + wantErr: true, + }, + { + name: "waf_rule_invalid_cond", + args: args{ + fileName: "./testdata/waf_rule_invalid_cond.data", + }, + want: *new(productWafRuleConfig), + wantErr: true, + }, + { + name: "waf_rule_both_empty", + args: args{ + fileName: "./testdata/waf_rule_both_empty.data", + }, + want: *new(productWafRuleConfig), + wantErr: true, + }, + { + name: "waf_rule_both_block_rules", + args: args{ + fileName: "./testdata/waf_rule_both_block_rules.data", + }, + want: *new(productWafRuleConfig), + wantErr: true, + }, + { + name: "waf_rule_both_check_rules", + args: args{ + fileName: "./testdata/waf_rule_both_check_rules.data", + }, + want: *new(productWafRuleConfig), + wantErr: true, + }, + { + name: "waf_rule_both_nil", + args: args{ + fileName: "./testdata/waf_rule_both_nil.data", + }, + want: *new(productWafRuleConfig), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ProductWafRuleConfLoad(tt.args.fileName) + if (err != nil) != tt.wantErr { + t.Errorf("ProductWafRuleConfLoad() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ProductWafRuleConfLoad() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/bfe_modules/mod_waf/waf_rule_table.go b/bfe_modules/mod_waf/waf_rule_table.go new file mode 100644 index 000000000..5a7623287 --- /dev/null +++ b/bfe_modules/mod_waf/waf_rule_table.go @@ -0,0 +1,43 @@ +// Copyright (c) 2020 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package mod_waf + +import "sync" + +type WarRuleTable struct { + lock sync.RWMutex // lock for following fields + version string // version + productRule productWafRule // rule +} + +func NewWarRuleTable() *WarRuleTable { + t := new(WarRuleTable) + t.productRule = make(productWafRule) + return t +} + +func (t *WarRuleTable) Update(ruleConf *productWafRuleConfig) { + t.lock.Lock() + t.version = ruleConf.Version + t.productRule = ruleConf.Config + t.lock.Unlock() +} + +func (t *WarRuleTable) Search(product string) (*ruleList, bool) { + t.lock.RLock() + ruleList, ok := t.productRule[product] + t.lock.RUnlock() + + return ruleList, ok +} diff --git a/bfe_modules/mod_waf/waf_rule_table_test.go b/bfe_modules/mod_waf/waf_rule_table_test.go new file mode 100644 index 000000000..fb7952196 --- /dev/null +++ b/bfe_modules/mod_waf/waf_rule_table_test.go @@ -0,0 +1,97 @@ +// Copyright (c) 2020 The BFE Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package mod_waf + +import ( + "reflect" + "sync" + "testing" +) + +func TestNewWarRuleTable(t *testing.T) { + tests := []struct { + name string + want *WarRuleTable + }{ + { + name: "normal", + want: &WarRuleTable{ + lock: sync.RWMutex{}, + version: "", + productRule: make(productWafRule), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewWarRuleTable(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewWarRuleTable() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestWarRuleTableUpdate(t *testing.T) { + config := map[string]*ruleList{ + "example": &ruleList{&wafRule{ + Cond: nil, + BlockRules: []string{"RuleBashCmd"}, + CheckRules: []string{}, + }}, + } + type fields struct { + version string + productRule productWafRule + } + type args struct { + ruleConf *productWafRuleConfig + } + tests := []struct { + name string + fields fields + args args + }{ + { + name: "normal", + fields: fields{ + version: "ut", + productRule: make(productWafRule), + }, + args: args{ + ruleConf: &productWafRuleConfig{ + Version: "utnew", + Config: config, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &WarRuleTable{ + version: tt.fields.version, + productRule: tt.fields.productRule, + } + w.Update(tt.args.ruleConf) + for key, value := range config { + realValue, ok := w.Search(key) + if !ok { + t.Errorf("missing product[%s] rule", key) + } + if !reflect.DeepEqual(value, realValue) { + t.Errorf("product update, want[%v] got[%v]", value, realValue) + } + } + }) + } +} diff --git a/bfe_route/bfe_cluster/bfe_cluster.go b/bfe_route/bfe_cluster/bfe_cluster.go index a1d940df5..095ea7071 100644 --- a/bfe_route/bfe_cluster/bfe_cluster.go +++ b/bfe_route/bfe_cluster/bfe_cluster.go @@ -33,9 +33,9 @@ type BfeCluster struct { sync.RWMutex Name string // cluster's name - backendConf *cluster_conf.BackendBasic // backend's basic conf - CheckConf *cluster_conf.BackendCheck // how to check backend - GslbBasic *cluster_conf.GslbBasicConf // gslb basic + backendConf *cluster_conf.BackendBasic // backend's basic conf + CheckConf *cluster_conf.BackendCheck // how to check backend + GslbBasic *cluster_conf.GslbBasicConf // gslb basic timeoutReadClient time.Duration // timeout for read client body timeoutReadClientAgain time.Duration // timeout for read client again @@ -113,6 +113,14 @@ func (cluster *BfeCluster) RetryLevel() int { return *retryLevel } +func (cluster *BfeCluster) OutlierDetectionLevel() int { + cluster.RLock() + outlierDetectionLevel := cluster.backendConf.OutlierDetectionLevel + cluster.RUnlock() + + return *outlierDetectionLevel +} + func (cluster *BfeCluster) TimeoutReadClient() time.Duration { cluster.RLock() res := cluster.timeoutReadClient diff --git a/bfe_server/reverseproxy.go b/bfe_server/reverseproxy.go index c00fe26e6..29af312b2 100644 --- a/bfe_server/reverseproxy.go +++ b/bfe_server/reverseproxy.go @@ -150,22 +150,26 @@ func (p *ReverseProxy) setTransports(clusterMap bfe_route.ClusterMap) { continue } - t := transport.(*bfe_http.Transport) - - // get transport, check if transport needs update - backendConf := conf.BackendConf() - if (t.MaxIdleConnsPerHost != *backendConf.MaxIdleConnsPerHost) || - (t.ResponseHeaderTimeout != time.Millisecond*time.Duration(*backendConf.TimeoutResponseHeader)) || - (t.ReqWriteBufferSize != conf.ReqWriteBufferSize()) || - (t.ReqFlushInterval != conf.ReqFlushInterval()) { - // create new transport with newConf instead of update transport - // update transport needs lock + switch t := transport.(type) { + case *bfe_http.Transport: + // get transport, check if transport needs update + backendConf := conf.BackendConf() + if (t.MaxIdleConnsPerHost != *backendConf.MaxIdleConnsPerHost) || + (t.ResponseHeaderTimeout != time.Millisecond*time.Duration(*backendConf.TimeoutResponseHeader)) || + (t.ReqWriteBufferSize != conf.ReqWriteBufferSize()) || + (t.ReqFlushInterval != conf.ReqFlushInterval()) { + // create new transport with newConf instead of update transport + // update transport needs lock + transport = createTransport(conf) + newTransports[cluster] = transport + continue + } + + newTransports[cluster] = transport + default: transport = createTransport(conf) newTransports[cluster] = transport - continue } - - newTransports[cluster] = transport } p.transports = newTransports @@ -330,8 +334,11 @@ func (p *ReverseProxy) clusterInvoke(srv *BfeServer, cluster *bfe_cluster.BfeClu request.Backend.BackendPort = uint32(backend.Port) if err == nil { - // succeed in invoking backend - backend.OnSuccess() + if checkBackendStatus(cluster.OutlierDetectionLevel(), res.StatusCode) { + backend.OnFail(cluster.Name) + } else { + backend.OnSuccess() + } // clear err msg in req. // this step is required, if finally succeed after retry @@ -570,6 +577,9 @@ func (p *ReverseProxy) ServeHTTP(rw bfe_http.ResponseWriter, basicReq *bfe_basic resFlushInterval := time.Duration(0) cancelOnClientClose := false + timeoutWriteClient := time.Duration(cluster_conf.DefaultWriteClientTimeout) * time.Millisecond + timeoutReadClientAgain := time.Duration(cluster_conf.DefaultReadClientAgainTimeout) * time.Millisecond + // get instance of BfeServer srv := p.server @@ -675,6 +685,10 @@ func (p *ReverseProxy) ServeHTTP(rw bfe_http.ResponseWriter, basicReq *bfe_basic // set deadline to finish read client request body p.setTimeout(bfe_basic.StageReadReqBody, basicReq.Connection, req, cluster.TimeoutReadClient()) + resFlushInterval = cluster.ResFlushInterval() + cancelOnClientClose = cluster.CancelOnClientClose() + timeoutWriteClient = cluster.TimeoutWriteClient() + timeoutReadClientAgain = cluster.TimeoutReadClientAgain() // Callback for HandleAfterLocation hl = srv.CallBacks.GetHandlerList(bfe_module.HandleAfterLocation) @@ -733,27 +747,25 @@ func (p *ReverseProxy) ServeHTTP(rw bfe_http.ResponseWriter, basicReq *bfe_basic res = bfe_basic.CreateInternalSrvErrResp(basicReq) goto response_got } - resFlushInterval = cluster.ResFlushInterval() - cancelOnClientClose = cluster.CancelOnClientClose() if resFlushInterval == 0 && basicReq.HttpRequest.Header.Get("Accept") == "text/event-stream" { resFlushInterval = cluster.DefaultSSEFlushInterval() } +response_got: // timeout for write response to client // Note: we use io.Copy() to read from backend and write to client. // For avoid from blocking on client conn or backend conn forever, // we must timeout both conns after specified duration. - p.setTimeout(bfe_basic.StageWriteClient, basicReq.Connection, req, cluster.TimeoutWriteClient()) - writeTimer = time.AfterFunc(cluster.TimeoutWriteClient(), func() { + p.setTimeout(bfe_basic.StageWriteClient, basicReq.Connection, req, timeoutWriteClient) + writeTimer = time.AfterFunc(timeoutWriteClient, func() { transport := basicReq.Trans.Transport.(*bfe_http.Transport) transport.CancelRequest(basicReq.OutRequest) // force close connection to backend }) defer writeTimer.Stop() // for read next request - defer p.setTimeout(bfe_basic.StageEndRequest, basicReq.Connection, req, cluster.TimeoutReadClientAgain()) + defer p.setTimeout(bfe_basic.StageEndRequest, basicReq.Connection, req, timeoutReadClientAgain) -response_got: defer res.Body.Close() // Callback for HandleReadResponse @@ -865,3 +877,7 @@ func checkRequestWithoutBody(req *bfe_http.Request) bool { } return false } + +func checkBackendStatus(outlierDetectionLevel int, statusCode int) bool { + return outlierDetectionLevel == cluster_conf.OutlierDetection5XX && statusCode/100 == 5 +} diff --git a/bfe_server/set_client_addr.go b/bfe_server/set_client_addr.go index aa6451508..572b9d020 100644 --- a/bfe_server/set_client_addr.go +++ b/bfe_server/set_client_addr.go @@ -36,8 +36,8 @@ func setClientAddr(req *bfe_basic.Request) { clientip := req.HttpRequest.Header.Get(bfe_basic.HeaderRealIP) clientport := req.HttpRequest.Header.Get(bfe_basic.HeaderRealPort) if clientip == "" { - clientip = getFirstSplitFromHeader(req, bfe_basic.HeaderForwardedFor, ", ") - clientport = getFirstSplitFromHeader(req, bfe_basic.HeaderForwardedPort, ", ") + clientip = getFirstSplitFromHeader(req, bfe_basic.HeaderForwardedFor, ",") + clientport = getFirstSplitFromHeader(req, bfe_basic.HeaderForwardedPort, ",") } if clientip != "" { parseClientAddr(req, clientip, clientport) @@ -48,7 +48,7 @@ func getFirstSplitFromHeader(req *bfe_basic.Request, header string, sep string) ret := "" if str := req.HttpRequest.Header.Get(header); str != "" { l := strings.SplitN(str, sep, 2) - ret = l[0] // get first split from header + ret = strings.TrimSpace(l[0]) // get first split from header } return ret } diff --git a/bfe_util/signal_table/signal_table.go b/bfe_util/signal_table/signal_table.go index 9e7f8a755..bb8445fd0 100644 --- a/bfe_util/signal_table/signal_table.go +++ b/bfe_util/signal_table/signal_table.go @@ -59,10 +59,10 @@ func (t *SignalTable) handle(sig os.Signal) { } // signalHandle is the signal handle loop -func (table *SignalTable) signalHandle() { +func (t *SignalTable) signalHandle() { var sigs []os.Signal - for sig := range table.shs { + for sig := range t.shs { sigs = append(sigs, sig) } @@ -71,7 +71,7 @@ func (table *SignalTable) signalHandle() { for { sig := <-c - table.handle(sig) + t.handle(sig) } } diff --git a/conf/mod_waf/mod_waf.conf b/conf/mod_waf/mod_waf.conf new file mode 100644 index 000000000..719f9bdc9 --- /dev/null +++ b/conf/mod_waf/mod_waf.conf @@ -0,0 +1,8 @@ +[Basic] +ProductRulePath = mod_waf/waf_rule.data + +[Log] +LogPrefix = waf +LogDir = ./log +RotateWhen = NEXTHOUR +BackUpCount = 24 \ No newline at end of file diff --git a/conf/mod_waf/waf_rule.data b/conf/mod_waf/waf_rule.data new file mode 100644 index 000000000..682b5856c --- /dev/null +++ b/conf/mod_waf/waf_rule.data @@ -0,0 +1,16 @@ +{ + "Version": "2019-12-10184356", + "Config": { + "example_product": [ + { + "Cond": "default_t()", + "CheckRules": [ + "RuleBashCmd" + ], + "BlockRules": [ + "RuleBashCmd" + ] + } + ] + } +} diff --git a/docs/en_us/SUMMARY.md b/docs/en_us/SUMMARY.md index abdc23241..b80653897 100644 --- a/docs/en_us/SUMMARY.md +++ b/docs/en_us/SUMMARY.md @@ -18,6 +18,7 @@ * [Traffic blocking](example/block.md) * [Request redirect](example/redirect.md) * [Request rewrite](example/rewrite.md) + * [FastCGI procotol](example/fastcgi.md) * [TLS mutual authentication](example/client_auth.md) * [Installation](installation/install.md) * [Install from source](installation/install_from_source.md) diff --git a/docs/en_us/condition/condition_primitive_index.md b/docs/en_us/condition/condition_primitive_index.md index ce156d446..6fbe76a5e 100644 --- a/docs/en_us/condition/condition_primitive_index.md +++ b/docs/en_us/condition/condition_primitive_index.md @@ -22,6 +22,7 @@ * req_tag_match(tagName, tagValue) * req_path_in(path_list, case_insensitive) * req_path_prefix_in(prefix_list, case_insensitive) + * req_path_element_prefix_in(prefix_list, case_insensitive) * req_path_suffix_in(suffix_list, case_insensitive) * req_query_key_in(key_list) * req_query_key_prefix_in(prefix_list) diff --git a/docs/en_us/condition/request/uri.md b/docs/en_us/condition/request/uri.md index a62399d50..1b302dd1f 100644 --- a/docs/en_us/condition/request/uri.md +++ b/docs/en_us/condition/request/uri.md @@ -67,6 +67,21 @@ req_path_prefix_in("/api/report|/api/analytics", false) req_path_suffix_in(".php|.jsp", false) ``` +## req_path_element_prefix_in(prefix_list, case_insensitive) +* Description: Judge if request path element prefix matches configured patterns + +* Parameters + +| Parameter | Descrption | +| --------- | ---------- | +| prefix_list | String
a list of path element prefixs which are concatenated using |
Each path prefix should start with '/' and end with '/', Automatic add '/' suffix when not end with '/' | +| case_insensitive | Boolean
case insensitive | + +* Example + +```go +req_path_element_prefix_in("/api/report/|/api/analytics/", false) +``` ## req_query_key_in(key_list) * Description: Judge if query key matches configured patterns diff --git a/docs/en_us/example/fastcgi.md b/docs/en_us/example/fastcgi.md new file mode 100644 index 000000000..20fb64a8f --- /dev/null +++ b/docs/en_us/example/fastcgi.md @@ -0,0 +1,151 @@ +# FastCGI protocol + +## Scenario + +* Imagine we have an http server which has two instances. One is responsible for processing fcgi protocol requests, and the other is responsible for http requests. + * Host:example.org + * Requests that start with / fcgi are forwarded to the fcgi protocol service instance with address 10.0.0.1:8001 + * Other requests are forwarded to http service instance with address 10.0.0.1:8002 + +## Configuration + +Modify example configurations (conf/) as the following steps: + +* Step 1. Config path of forward rules in conf/bfe.conf + +```ini +hostRuleConf = server_data_conf/host_rule.data +routeRuleConf = server_data_conf/route_rule.data +clusterConf = server_data_conf/cluster_conf.data + +clusterTableConf = cluster_conf/cluster_table.data +gslbConf = cluster_conf/gslb.data +``` + +* Step 2. Config host rules (conf/server_data_conf/host_rule.data) + +```json +{ + "Version": "init version", + "DefaultProduct": null, + "Hosts": { + "exampleTag":[ + "example.org" // host name: example.org=>host tag: exampleTag + ] + }, + "HostTags": { + "example_product":[ + "exampleTag" // host tag: exampleTag=>product name: example_product + ] + } +} +``` + +* Step 3. Config cluster configuration (conf/server_data_conf/cluster_conf.data) +Note: Set backend conf params and use default value for other params + +```json +{ + "Version": "init version", + "Config": { + "cluster_demo_http": { + "BackendConf": { + "TimeoutConnSrv": 2000, + "TimeoutResponseHeader": 50000, + "MaxIdleConnsPerHost": 0, + "RetryLevel": 0 + } + }, + "cluster_demo_fcgi": { + "BackendConf": { + "Protocol": "fcgi", + "TimeoutConnSrv": 2000, + "TimeoutResponseHeader": 50000, + "MaxIdleConnsPerHost": 0, + "RetryLevel": 0, + "FCGIConf": { + "Root": "/home/work", + "EnvVars": { + "VarKey": "VarVal" + } + } + } + } + } +} +``` + +* Step 4. Config instances of cluster (conf/cluster_conf/cluster_table.data) + +```json +{ + "Version": "init version", + "Config": { + "cluster_demo_fcgi": { // cluster => sub_cluster => instance list + "demo_fcgi.all": [{ // subcluster: demo_fcgi.all + "Addr": "10.0.0.1", + "Name": "fcgi.A", + "Port": 8001, + "Weight": 1 + }] + }, + "cluster_demo_http": { + "demo_http.all": [{ + "Addr": "10.0.0.1", + "Name": "http.A", + "Port": 8002, + "Weight": 1 + }] + } + } +} +``` + +* Step 5. Config gslb configuration (conf/cluster_conf/gslb.data) + +```json +{ + "Hostname": "", + "Ts": "0", + "Clusters": { + "cluster_demo_fcgi": { // cluster => weight of subcluster + "GSLB_BLACKHOLE": 0, // GSLB_BLACKHOLE == 0 means do not discard traffic + "demo_fcgi.all": 100 // weight 100 means all traffic routes to demo_fcgi.all + }, + "cluster_demo_http": { + "GSLB_BLACKHOLE": 0, + "demo_http.all": 100 + } + } +} +``` + +* Step 6. Config route rules (conf/server_data_conf/route_rule.data) + +```json +{ + "Version": "init version", + "ProductRule": { + "example_product": [ // product => route rules + { + "Cond": "req_path_prefix_in(\"/fcgi\", false)", + "ClusterName": "cluster_demo_fcgi" + }, + { + "Cond": "default_t()", + "ClusterName": "cluster_demo_http" + } + ] + } +} +``` + +* Step 7. Verify configured rules + +```bash +curl -H "host: example.org" "http://127.1:8080/fcgi/test" +# request will route to 10.0.0.1:8001 + +curl -H "host: example.org" "http://127.1:8080/http/test" +# request will route to 10.0.0.1:8002 +``` diff --git a/docs/en_us/faq/development.md b/docs/en_us/faq/development.md index e64a36b6e..5edeecc12 100644 --- a/docs/en_us/faq/development.md +++ b/docs/en_us/faq/development.md @@ -1,4 +1,4 @@ # Development FAQ ## How to develop a module -- For more details, see [introduction to module development](https://github.com/bfenetworks/bfe/blob/develop/docs/en_us/module/overview.md) +- For more details, see [introduction to module development](https://github.com/bfenetworks/bfe/blob/develop/docs/en_us/module/modules.md) diff --git a/docs/en_us/installation/install.md b/docs/en_us/installation/install.md index 16f96ce82..018656e94 100644 --- a/docs/en_us/installation/install.md +++ b/docs/en_us/installation/install.md @@ -5,6 +5,7 @@ - [Install using binaries](install_using_binaries.md) - [Install using go](install_using_go.md) - [Install using snap](install_using_snap.md) +- [Install using docker](install_using_docker.md) ## Supported platform | Operating System | Description | diff --git a/docs/en_us/installation/install_using_docker.md b/docs/en_us/installation/install_using_docker.md new file mode 100644 index 000000000..2ed7b35cf --- /dev/null +++ b/docs/en_us/installation/install_using_docker.md @@ -0,0 +1,25 @@ +# Install using docker + +## Install && Run + +- Run BFE with example configuration files: + +```bash +docker run -p 8080:8080 -p 8443:8443 -p 8421:8421 bfenetworks/bfe +``` + +you can access http://127.0.0.1:8080/ and got status code 500 because of there is rule be matched. +you can access http://127.0.0.1:8421/ got monitor infomation. + + +- Run BFE with your configuration files: +```bash +// prepare your configuration (see section Configuration if you need) to dir /Users/BFE/conf + +docker run -p 8080:8080 -p 8443:8443 -p 8421:8421 -v /Users/BFE/Desktop/log:/bfe/log -v /Users/BFE/Desktop/conf:/bfe/conf bfenetworks/bfe +``` + +## Further reading +- Get familiar with [Command options](../operation/command.md) +- Get started with [Beginner's Guide](../example/guide.md) + diff --git a/docs/material/assets/images/china_everbright_bank.jpg b/docs/material/assets/images/china_everbright_bank.jpg deleted file mode 100644 index 7f6db9ad380223ad3964bcf4e0addd96cef03f39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15004 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%0DqdYFpB7#W94vA(0$1G=9ud6fu9gZp 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 literal 0 HcmV?d00001 diff --git a/docs/material/assets/images/shenzhen.png b/docs/material/assets/images/shenzhen.png new file mode 100644 index 0000000000000000000000000000000000000000..b73f3cffb7eeeec92d36614099775e1cca4fc384 GIT binary patch literal 37744 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

>ejIdhp$_B_-Gl$n)|>PxeB3DJM&J?3TgjY_R_g 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 literal 0 HcmV?d00001 diff --git a/docs/material/overrides/home_en.html b/docs/material/overrides/home_en.html index 7b240f091..531a2b792 100644 --- a/docs/material/overrides/home_en.html +++ b/docs/material/overrides/home_en.html @@ -365,7 +365,7 @@

d="M6 20a6 6 0 01-6-6c0-3.09 2.34-5.64 5.35-5.96A7.496 7.496 0 0112 4a7.5 7.5 0 017.35 6A5.02 5.02 0 0124 15a5 5 0 01-5 5H6M9.09 8.4L4.5 13l4.59 4.6 1.41-1.42L7.32 13l3.18-3.18L9.09 8.4m5.82 0L13.5 9.82 16.68 13l-3.18 3.18 1.41 1.42L19.5 13l-4.59-4.6z" /> Multiple protocols

-

BFE supports HTTP, HTTPS, SPDY, HTTP2, WebSocket, TLS, FastCGI, etc. Future support is planned for gRPC and +

BFE supports HTTP, HTTPS, SPDY, HTTP2, gRPC, WebSocket, TLS, FastCGI, etc. Future support is planned for HTTP/3.

@@ -445,7 +445,9 @@

-
Some of our users include:
+
+ Some of our users include: +
@@ -479,10 +484,11 @@

- + {% endblock %} {% block content %}{% endblock %} -{% block footer %}{% endblock %} +{% block footer %}{% endblock %} \ No newline at end of file diff --git a/docs/material/overrides/home_zh.html b/docs/material/overrides/home_zh.html index c9b068720..2695e5796 100644 --- a/docs/material/overrides/home_zh.html +++ b/docs/material/overrides/home_zh.html @@ -361,7 +361,7 @@

d="M6 20a6 6 0 01-6-6c0-3.09 2.34-5.64 5.35-5.96A7.496 7.496 0 0112 4a7.5 7.5 0 017.35 6A5.02 5.02 0 0124 15a5 5 0 01-5 5H6M9.09 8.4L4.5 13l4.59 4.6 1.41-1.42L7.32 13l3.18-3.18L9.09 8.4m5.82 0L13.5 9.82 16.68 13l-3.18 3.18 1.41 1.42L19.5 13l-4.59-4.6z" /> 支持丰富的接入协议

-

支持HTTP、HTTPS、SPDY、HTTP/2、WebSocket、TLS、FastCGI等。未来计划支持gRPC、HTTP/3

+

支持HTTP、HTTPS、SPDY、HTTP/2、gRPC、WebSocket、TLS、FastCGI等。未来计划支持HTTP/3

@@ -432,7 +432,7 @@

-
合作案例包括:
+
合作案例包括:
@@ -466,10 +469,10 @@

+ {% endblock %} {% block content %}{% endblock %} -{% block footer %}{% endblock %} +{% block footer %}{% endblock %} \ No newline at end of file diff --git a/docs/mkdocs_en.yml b/docs/mkdocs_en.yml index 99cd72ef9..0abc0ab86 100644 --- a/docs/mkdocs_en.yml +++ b/docs/mkdocs_en.yml @@ -24,7 +24,7 @@ theme: logo: logo favicon: assets/favicon.png -copyright: 'Copyright © 2020 The BFE Authors | Documentation Distributed under CC-BY-4.0
Copyright © 2020 The Linux Foundation. All rights reserved. The Linux +copyright: 'Copyright © 2021 The BFE Authors | Documentation Distributed under CC-BY-4.0
Copyright © 2021 The Linux Foundation. All rights reserved. The Linux Foundation has registered trademarks and uses trademarks
For a list of trademarks of The Linux Foundation, please
see our Trademark Usage page ' diff --git a/docs/mkdocs_zh.yml b/docs/mkdocs_zh.yml index 8d8c4ab01..43304ee9b 100644 --- a/docs/mkdocs_zh.yml +++ b/docs/mkdocs_zh.yml @@ -24,7 +24,7 @@ theme: logo: logo favicon: assets/favicon.png -copyright: 'Copyright © 2020 BFE作者 | 文档发布基于CC-BY-4.0授权许可
Copyright © 2020 +copyright: 'Copyright © 2021 BFE作者 | 文档发布基于CC-BY-4.0授权许可
Copyright © 2021 Linux基金会。保留所有权利。Linux基金会已注册并使用商标
如需了解Linux基金会的商标列表, 请访问商标使用页面 ' diff --git a/docs/zh_cn/SUMMARY.md b/docs/zh_cn/SUMMARY.md index 565ec36d1..176d587ce 100644 --- a/docs/zh_cn/SUMMARY.md +++ b/docs/zh_cn/SUMMARY.md @@ -18,6 +18,7 @@ * [黑名单封禁](example/block.md) * [重定向](example/redirect.md) * [重写](example/rewrite.md) + * [FastCGI协议](example/fastcgi.md) * [TLS客户端认证](example/client_auth.md) * [安装说明](installation/install.md) * [源码编译安装](installation/install_from_source.md) diff --git a/docs/zh_cn/condition/condition_primitive_index.md b/docs/zh_cn/condition/condition_primitive_index.md index 572fb09fb..fe0de0183 100644 --- a/docs/zh_cn/condition/condition_primitive_index.md +++ b/docs/zh_cn/condition/condition_primitive_index.md @@ -23,6 +23,7 @@ * req_path_in(path_list, case_insensitive) * req_path_prefix_in(prefix_list, case_insensitive) * req_path_suffix_in(suffix_list, case_insensitive) + * req_path_element_suffix_in(suffix_list, case_insensitive) * req_query_key_in(key_list) * req_query_key_prefix_in(prefix_list) * req_query_value_in(key, value_list, case_insensitive) diff --git a/docs/zh_cn/condition/request/uri.md b/docs/zh_cn/condition/request/uri.md index d904e172b..2496cf68c 100644 --- a/docs/zh_cn/condition/request/uri.md +++ b/docs/zh_cn/condition/request/uri.md @@ -43,6 +43,21 @@ req_path_in("/api/search|/api/list", true) req_path_prefix_in("/api/report|/api/analytics", false) ``` +## req_path_element_prefix_in(prefix_list, case_insensitive) +* 含义:判断http的path element是否前缀匹配prefix_list之一 + +* 参数 + +| 参数 | 描述 | +| -------- | ---------------------- | +| prefix_list | String
path element prefix列表, 多个之间使用‘|’连接
每个path prefix应以"/"开头且以"/"结尾,非"/"结尾时会自动补充"/" | +| case_insensitive | Boolean
是否忽略大小写 | + +* 示例 +```go +req_path_element_prefix_in("/api/report/|/api/analytics/", false) +``` + ## req_path_suffix_in(suffix_list, case_insensitive) * 含义: 判断http的path是否后缀匹配suffix_list之一 * 参数 diff --git a/docs/zh_cn/example/fastcgi.md b/docs/zh_cn/example/fastcgi.md new file mode 100644 index 000000000..830f93b7e --- /dev/null +++ b/docs/zh_cn/example/fastcgi.md @@ -0,0 +1,154 @@ +# FCGI协议 + +## 场景说明 + +* 假设我们有一个http server对外提供服务,并且有2个服务实例;1个负责处理FastCGI协议请求,另外1个负责处理HTTP协议请求 + * 域名:example.org + * 以/fcgi开头的请求都转发至FastCGI协议服务实例;地址:10.0.0.1:8001 + * 其他的请求都转发至HTTP协议服务实例;地址:10.0.0.1:8002 + +## 配置说明 +在[样例配置](../../../conf/)上稍做修改,就可以实现上述转发功能 + +* Step 1.在 conf/bfe.conf配置转发功能使用的配置文件路径 + +```ini +hostRuleConf = server_data_conf/host_rule.data #域名规则配置文件 +routeRuleConf = server_data_conf/route_rule.data #分流规则配置文件 +clusterConf = server_data_conf/cluster_conf.data #集群配置文件 + +clusterTableConf = cluster_conf/cluster_table.data #集群实例列表配置文件 +gslbConf = cluster_conf/gslb.data #子集群负载均衡配置文件 +``` + +* Step 2. 配置域名规则 (conf/server_data_conf/host_rule.data) + +```json +{ + "Version": "init version", + "DefaultProduct": null, + "Hosts": { + "exampleTag":[ + "example.org" // 域名example.org=>域名标签exampleTag + ] + }, + "HostTags": { + "example_product":[ + "exampleTag" // 域名标签exampleTag=>产品线名称example_product + ] + } +} +``` + +* Step 3. 配置集群的基础信息 (conf/server_data_conf/cluster_conf.data) +配置集群cluster_demo_fcgi和cluster_demo_http 后端配置的参数,其他均使用默认值 + +```json +{ + "Version": "init version", + "Config": { + "cluster_demo_http": { // 集群cluster_demo_http的配置 + "BackendConf": { + "TimeoutConnSrv": 2000, + "TimeoutResponseHeader": 50000, + "MaxIdleConnsPerHost": 0, + "RetryLevel": 0 + } + }, + "cluster_demo_fcgi": { // 集群cluster_demo_fcgi的配置 + "BackendConf": { + "Protocol": "fcgi", + "TimeoutConnSrv": 2000, + "TimeoutResponseHeader": 50000, + "MaxIdleConnsPerHost": 0, + "RetryLevel": 0, + "FCGIConf": { + "Root": "/home/work", + "EnvVars": { + "VarKey": "VarVal" + } + } + } + } + } +} +``` + +* Step 4. 配置集群下实例信息 (conf/cluster_conf/cluster_table.data) + +```json +{ + "Version": "init version", + "Config": { + "cluster_demo_fcgi": { // 集群 => 子集群 => 实例列表 + "demo_fcgi.all": [{ // 子集群demo_fcgi.all + "Addr": "10.0.0.1", // 实例地址:10.0.0.1 + "Name": "fcgi.A", // 实例名:fcgi.A + "Port": 8001, // 实例端口:8001 + "Weight": 1 // 实例权重:1 + }] + }, + "cluster_demo_http": { + "demo_http.all": [{ + "Addr": "10.0.0.1", + "Name": "http.A", + "Port": 8002, + "Weight": 1 + }] + } + } +} +``` + +* Step 5. 配置子集群内负载均衡 (conf/cluster_conf/gslb.data) + +```json +{ + "Hostname": "", + "Ts": "0", + "Clusters": { + "cluster_demo_fcgi": { // 集群 => 子集群权重 + "GSLB_BLACKHOLE": 0, // 黑洞的分流权重为0,表示不丢弃流量 + "demo_fcgi.all": 100 // 权重为100,表示全部分流到demo_fcgi.all + }, + "cluster_demo_http": { + "GSLB_BLACKHOLE": 0, + "demo_http.all": 100 + } + } +} +``` + +* Step 6. 配置分流规则 (conf/server_data_conf/route_rule.data) + * 将/fcgi开头的流量转发到cluster_demo_fcgi集群 + * 其余流量转发到cluster_demo_http集群 + +```json +{ + "Version": "init version", + "ProductRule": { + "example_product": [ // 产品线 => 分流规则 + { + // 以/fcgi开头的path分流到cluster_demo_fcgi集群 + "Cond": "req_path_prefix_in(\"/fcgi\", false)", + "ClusterName": "cluster_demo_fcgi" + }, + { + // 其他流量分流到cluster_demo_http集群 + "Cond": "default_t()", + "ClusterName": "cluster_demo_http" + } + ] + } +} +``` + +* Step 7. 验证配置规则 + +```bash +curl -H "host: example.org" "http://127.1:8080/fcgi/test" +# 将请求转发至10.0.0.1:8001 + +curl -H "host: example.org" "http://127.1:8080/http/test" +# 将请求转发至10.0.0.1:8002 +``` diff --git a/docs/zh_cn/faq/development.md b/docs/zh_cn/faq/development.md index 69049487f..b02dca872 100644 --- a/docs/zh_cn/faq/development.md +++ b/docs/zh_cn/faq/development.md @@ -1,4 +1,4 @@ # 开发常见问题 ## 如何开发BFE扩展模块 -- 具体见[模块开发介绍](https://github.com/bfenetworks/bfe/blob/develop/docs/zh_cn/module/overview.md) +- 具体见[模块开发介绍](https://github.com/bfenetworks/bfe/blob/develop/docs/zh_cn/module/modules.md) diff --git a/docs/zh_cn/installation/install.md b/docs/zh_cn/installation/install.md index 868553082..59aa05435 100644 --- a/docs/zh_cn/installation/install.md +++ b/docs/zh_cn/installation/install.md @@ -5,6 +5,7 @@ - [二进制文件下载安装](install_using_binaries.md) - [go方式安装](install_using_go.md) - [snap方式安装](install_using_snap.md) +- [docker方式安装](install_using_docker.md) ## 平台支持 | 操作系统 | 支持说明 | diff --git a/docs/zh_cn/installation/install_using_docker.md b/docs/zh_cn/installation/install_using_docker.md new file mode 100644 index 000000000..1056d549d --- /dev/null +++ b/docs/zh_cn/installation/install_using_docker.md @@ -0,0 +1,25 @@ +# docker安装 + + +## 安装 && 运行 + +- 基于示例配置运行BFE: + +```bash +docker run -p 8080:8080 -p 8443:8443 -p 8421:8421 bfenetworks/bfe +``` + +你可以访问 http://127.0.0.1:8080/ 因为没有匹配的配置,将得到 status code 500 +你可以访问 http://127.0.0.1:8421/ 查看监控信息 + +- 自定义配置文件路径 + +```bash +// 事先准备好你自己的配置放到 (可以参考 配置 章节) /Users/BFE/conf + +docker run -p 8080:8080 -p 8443:8443 -p 8421:8421 -v /Users/BFE/Desktop/log:/bfe/log -v /Users/BFE/Desktop/conf:/bfe/conf bfenetworks/bfe +``` + +## 下一步 +* 了解[命令行参数](../operation/command.md) +* 了解[基本功能配置使用](../example/guide.md) diff --git a/go.mod b/go.mod index 3cbc2a081..91e651001 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/abbot/go-http-auth v0.4.1-0.20181019201920-860ed7f246ff github.com/andybalholm/brotli v1.0.0 github.com/asergeyev/nradix v0.0.0-20170505151046-3872ab85bb56 // indirect - github.com/baidu/go-lib v0.0.0-20191217050907-c1bbbad6b030 + github.com/baidu/go-lib v0.0.0-20200819072111-21df249f5e6a github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/gomodule/redigo v2.0.0+incompatible diff --git a/go.sum b/go.sum index 3bd2f7361..862b3a750 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ github.com/asergeyev/nradix v0.0.0-20170505151046-3872ab85bb56 h1:Wi5Tgn8K+jDcBY github.com/asergeyev/nradix v0.0.0-20170505151046-3872ab85bb56/go.mod h1:8BhOLuqtSuT5NZtZMwfvEibi09RO3u79uqfHZzfDTR4= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/baidu/go-lib v0.0.0-20191217050907-c1bbbad6b030 h1:P8Bwa/d4AEH5qnHroVFI4hUqvy/1kh6UsfbDI+JJ2GI= -github.com/baidu/go-lib v0.0.0-20191217050907-c1bbbad6b030/go.mod h1:FneHDqz3wLeDGdWfRyW4CzBbCwaqesLGIFb09N80/ww= +github.com/baidu/go-lib v0.0.0-20200819072111-21df249f5e6a h1:m/u39GNhkoUSC9WxTuM5hWShEqEfVioeXDiqiQd6tKg= +github.com/baidu/go-lib v0.0.0-20200819072111-21df249f5e6a/go.mod h1:FneHDqz3wLeDGdWfRyW4CzBbCwaqesLGIFb09N80/ww= github.com/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU= github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=