diff --git a/.gitignore b/.gitignore index f398beae7..4963ee75b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ output /**/*.log profile.out coverage.txt +.idea/* +.vscode/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a58d5ed5..bd66ae1e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,29 @@ 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). +## [v1.1.0] - 2021-04-08 + +### Added +- Support JA3 fingerprint for SSL/TLS client +- Support Slow‑Start to allow a backend instance gradually recover its weight +- Add maxConnPerHost to limit the number of connections to a backend +- mod_header: add header renaming actions +- Merge some updates from golang/net/textproto +- Merge some updates from golang/net/http +- Merge some updates from golang/net/http2 +- Documents optimization + +### Changed +- Change outlierDetectionLevel to OutlierDetectionHttpCode + +### Fixed +- Fix panic when write internal response timeout +- Fix unit test in bfe_spdy/frame_test.go under go 1.16 + +### Security +- Fix config loading for multi-value option + + ## [v1.0.0] - 2021-01-15 ### Added @@ -215,7 +238,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.1.0]: https://github.com/bfenetworks/bfe/compare/v1.0.0...v1.1.0 [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 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 012d7c12d..3327464a6 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -16,6 +16,7 @@ | Hao Dong | anotherwriter | | Haobin zhang | zhanghaobin | | Hui Yu | dblate | +| Guangze Song | coopersong | | Gen Wang | gracewang510 | | Jie Liu | freeHackOfJeff | | Jie Wan | wanjiecs | @@ -41,6 +42,7 @@ | Shan Xiao | arlingtonroad | | Shengnan Yu | goldfish-fish | | Shuai Yan | yanshuai615270 | +| Shuo Yang | yangshuothtf | | Sijie Yang | iyangsj | | Tianqi Zhang | NKztq | | Weijie Zhao | zwj13513118235 | diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 3d71e318c..cadcf4def 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -18,5 +18,6 @@ This file lists who are the maintainers of the BFE project. The responsibilities | ---- | --------- | ----------- | | [Derek Zheng](mailto:shanhu5739@gmail.com) | [shanhuhai5739](https://github.com/shanhuhai5739) | Kuaishou | | [Xiaofei Yu](mailto:nemo_00o@hotmail.com) | [xiaofei0800](https://github.com/xiaofei0800) | Baidu | -| [Wensi Yang](mailto:tianxinheihei@gmail.com) | [tianxinheihei](https://github.com/tianxinheihei) | Baidu | +| [Wensi Yang](mailto:tianxinheihei@gmail.com) | [tianxinheihei](https://github.com/tianxinheihei) | ByteDance | | [Kaiyu Zheng](mailto:412674752@qq.com) | [kaiyuzheng](https://github.com/kaiyuzheng) | ByteDance | +| [Yuqi Xiao](mailto:xiao19910705@163.com) | [Yuqi Xiao](https://github.com/YuqiXiao) | Baidu | diff --git a/VERSION b/VERSION index afaf360d3..1cc5f657e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 \ No newline at end of file +1.1.0 \ No newline at end of file diff --git a/bfe_balance/backend/bfe_backend.go b/bfe_balance/backend/bfe_backend.go index 969822a6f..75b2b79bb 100644 --- a/bfe_balance/backend/bfe_backend.go +++ b/bfe_balance/backend/bfe_backend.go @@ -41,6 +41,8 @@ type BfeBackend struct { succNum int // number of consecutive successes of health-check request closeChan chan bool // tell health-check to stop + + restarted bool // indicate if this backend is new bring-up by health-check } func NewBfeBackend() *BfeBackend { @@ -90,6 +92,19 @@ func (back *BfeBackend) setAvail(avail bool) { } } +func (back *BfeBackend) SetRestart(restart bool) { + back.Lock() + back.restarted = restart + back.Unlock() +} + +func (back *BfeBackend) GetRestart() bool { + back.RLock() + restart := back.restarted + back.RUnlock() + return restart +} + func (back *BfeBackend) ConnNum() int { back.RLock() connNum := back.connNum diff --git a/bfe_balance/backend/health_check.go b/bfe_balance/backend/health_check.go index 05f0c9ad1..c28b48542 100644 --- a/bfe_balance/backend/health_check.go +++ b/bfe_balance/backend/health_check.go @@ -96,6 +96,7 @@ loop: } log.Logger.Info("backend %s back to Normal", backend.Name) + backend.SetRestart(true) backend.SetAvail(true) break loop } diff --git a/bfe_balance/bal_gslb/bal_gslb.go b/bfe_balance/bal_gslb/bal_gslb.go index 4bd1e6c02..051d90442 100644 --- a/bfe_balance/bal_gslb/bal_gslb.go +++ b/bfe_balance/bal_gslb/bal_gslb.go @@ -89,6 +89,16 @@ func (bal *BalanceGslb) SetGslbBasic(gslbBasic cluster_conf.GslbBasicConf) { bal.lock.Unlock() } +func (bal *BalanceGslb) SetSlowStart(backendConf cluster_conf.BackendBasic) { + bal.lock.Lock() + + for _, sub := range bal.subClusters { + sub.setSlowStart(*backendConf.SlowStartTime) + } + + bal.lock.Unlock() +} + // Init inializes gslb cluster with config func (bal *BalanceGslb) Init(gslbConf gslb_conf.GslbClusterConf) error { totalWeight := 0 diff --git a/bfe_balance/bal_gslb/bal_gslb_test.go b/bfe_balance/bal_gslb/bal_gslb_test.go index 83fdf6b20..df09d6b37 100644 --- a/bfe_balance/bal_gslb/bal_gslb_test.go +++ b/bfe_balance/bal_gslb/bal_gslb_test.go @@ -133,3 +133,65 @@ func SetReqHeader(req *bfe_basic.Request, key string) { req.HttpRequest.Header.Set(key, "val") } } + +func TestSlowStart(t *testing.T) { + t.Logf("bal_gslb_test: TestSlowStart") + var c cluster_table_conf.ClusterBackend + var gb cluster_conf.GslbBasicConf + var g gslb_conf.GslbClusterConf + var err error + + loadJson("testdata/cluster1", &c) + loadJson("testdata/gb", &gb) + loadJson("testdata/g1", &g) + t.Logf("%v %v %v\n", c, gb, g) + + bal := NewBalanceGslb("cluster_dumi") + if err := bal.Init(g); err != nil { + t.Errorf("init error %s", err) + } + t.Logf("%+v\n", bal) + if bal.totalWeight != 100 || !bal.single || bal.subClusters[bal.avail].Name != "light.example.wt" || bal.retryMax != 3 || bal.crossRetry != 1 { + t.Errorf("init error") + } + + if len(bal.subClusters) != 3 { + t.Errorf("cluster len error") + } + + t.Logf("%+v", bal.subClusters[0]) + t.Logf("%+v", bal.subClusters[1]) + t.Logf("%+v", bal.subClusters[2]) + + var c1 cluster_table_conf.ClusterBackend + var gb1 cluster_conf.GslbBasicConf + var g1 gslb_conf.GslbClusterConf + loadJson("testdata/cluster2", &c1) + loadJson("testdata/gb2", &gb1) + loadJson("testdata/g2", &g1) + + err = cluster_conf.GslbBasicConfCheck(&gb1) + if err != nil { + t.Errorf("GslbBasicConfCheck err %s", err) + } + t.Logf("%v %v %v\n", c1, gb1, g1) + if err := bal.Reload(g1); err != nil { + t.Errorf("reload error %s", err) + } + + bal.SetGslbBasic(gb1) + + var backendConf cluster_conf.BackendBasic + err = cluster_conf.BackendBasicCheck(&backendConf) + if err != nil { + t.Errorf("BackendBasicCheck err %s", err) + } + var ssTime = 30 + backendConf.SlowStartTime = &ssTime + bal.SetSlowStart(backendConf) + + t.Logf("%+v\n", bal) + t.Logf("%+v", bal.subClusters[0]) + t.Logf("%+v", bal.subClusters[1]) + t.Logf("%+v", bal.subClusters[2]) +} diff --git a/bfe_balance/bal_gslb/sub_cluster.go b/bfe_balance/bal_gslb/sub_cluster.go index 6ea9c82c0..7498ba9ef 100644 --- a/bfe_balance/bal_gslb/sub_cluster.go +++ b/bfe_balance/bal_gslb/sub_cluster.go @@ -85,6 +85,10 @@ func (sub *SubCluster) balance(algor int, key []byte) (*backend.BfeBackend, erro return sub.backends.Balance(algor, key) } +func (sub *SubCluster) setSlowStart(slowStartTime int) { + sub.backends.SetSlowStart(slowStartTime) +} + // SubClusterList is a list of subcluster. type SubClusterList []*SubCluster diff --git a/bfe_balance/bal_slb/backend_rr.go b/bfe_balance/bal_slb/backend_rr.go index 54d79878b..1561aa042 100644 --- a/bfe_balance/bal_slb/backend_rr.go +++ b/bfe_balance/bal_slb/backend_rr.go @@ -16,15 +16,27 @@ package bal_slb +import ( + "time" +) + import ( "github.com/bfenetworks/bfe/bfe_balance/backend" "github.com/bfenetworks/bfe/bfe_config/bfe_cluster_conf/cluster_table_conf" ) +type WeightSS struct { + final int // final target weight after slow-start + slowStartTime int // time for backend increases the weight to the full value, in seconds + startTime time.Time // time of the first request +} + type BackendRR struct { - weight int // weight of this backend - current int // current weight - backend *backend.BfeBackend // point to BfeBackend + weight int // weight of this backend + current int // current weight + backend *backend.BfeBackend // point to BfeBackend + inSlowStart bool // indicate if in slow-start phase + weightSS WeightSS // slow_start related parameters } func NewBackendRR() *BackendRR { @@ -36,15 +48,17 @@ func NewBackendRR() *BackendRR { // Init initialize BackendRR with BackendConf func (backRR *BackendRR) Init(subClusterName string, conf *cluster_table_conf.BackendConf) { - backRR.weight = *conf.Weight - backRR.current = *conf.Weight + // scale up 100 times from conf file + backRR.weight = *conf.Weight * 100 + backRR.current = backRR.weight + backRR.weightSS.final = backRR.weight back := backRR.backend back.Init(subClusterName, conf) } func (backRR *BackendRR) UpdateWeight(weight int) { - backRR.weight = weight + backRR.weight = weight * 100 // if weight > 0, don't touch backRR.current if weight <= 0 { @@ -60,3 +74,33 @@ func (backRR *BackendRR) MatchAddrPort(addr string, port int) bool { back := backRR.backend return back.Addr == addr && back.Port == port } + +func (backRR *BackendRR) initSlowStart(ssTime int) { + backRR.weightSS.slowStartTime = ssTime + if backRR.weightSS.slowStartTime == 0 { + backRR.inSlowStart = false + } else { + backRR.weightSS.startTime = time.Now() + backRR.inSlowStart = true + + // set weight/current to 1, to avoid no traffic allowed at the beginning of start + backRR.weight = 1 + backRR.current = 1 + } +} + +func (backRR *BackendRR) updateSlowStart() { + if backRR.inSlowStart { + current := time.Duration(backRR.weightSS.final) * time.Since(backRR.weightSS.startTime) + if backRR.weightSS.slowStartTime != 0 { + current /= time.Duration(backRR.weightSS.slowStartTime) * time.Second + backRR.weight = int(current) + } else { + backRR.weight = backRR.weightSS.final + } + if backRR.weight >= backRR.weightSS.final { + backRR.weight = backRR.weightSS.final + backRR.inSlowStart = false + } + } +} diff --git a/bfe_balance/bal_slb/backend_rr_test.go b/bfe_balance/bal_slb/backend_rr_test.go index 96b1d19e6..82138d82c 100644 --- a/bfe_balance/bal_slb/backend_rr_test.go +++ b/bfe_balance/bal_slb/backend_rr_test.go @@ -36,12 +36,12 @@ func TestBackendRRInit_case1(t *testing.T) { backendRR := NewBackendRR() backendRR.Init("example.cluster", &conf) - if backendRR.weight != 10 { - t.Error("backend.weight should be 10") + if backendRR.weight != 10 * 100 { + t.Error("backend.weight should be 10 * 100") } - if backendRR.current != 10 { - t.Error("backend.current should be 10") + if backendRR.current != 10 * 100 { + t.Error("backend.current should be 10 * 100") } backend := backendRR.backend diff --git a/bfe_balance/bal_slb/bal_rr.go b/bfe_balance/bal_slb/bal_rr.go index fcd76dd31..b8b036a7e 100644 --- a/bfe_balance/bal_slb/bal_rr.go +++ b/bfe_balance/bal_slb/bal_rr.go @@ -89,10 +89,13 @@ func (s BackendListSorter) Less(i, j int) bool { type BalanceRR struct { sync.Mutex - Name string - backends BackendList // list of BackendRR - sorted bool // list of BackeneRR sorted or not - next int // next backend to schedule + Name string + backends BackendList // list of BackendRR + sorted bool // list of BackeneRR sorted or not + next int // next backend to schedule + + slowStartNum int // number of backends in slow_start phase + slowStartTime int // time for backend increases the weight to the full value, in seconds } func NewBalanceRR(name string) *BalanceRR { @@ -113,6 +116,27 @@ func (brr *BalanceRR) Init(conf cluster_table_conf.SubClusterBackend) { brr.next = 0 } +func (brr *BalanceRR) SetSlowStart(ssTime int) { + brr.Lock() + brr.slowStartTime = ssTime + brr.Unlock() +} + +func (brr *BalanceRR) checkSlowStart() { + brr.Lock() + defer brr.Unlock() + if brr.slowStartTime > 0 { + for _, backendRR := range brr.backends { + backend := backendRR.backend + if backend.GetRestart() { + backend.SetRestart(false) + backendRR.initSlowStart(brr.slowStartTime) + } + backendRR.updateSlowStart() + } + } +} + // Release releases backend list. func (brr *BalanceRR) Release() { for _, back := range brr.backends { @@ -162,6 +186,8 @@ func (brr *BalanceRR) Update(conf cluster_table_conf.SubClusterBackend) { for _, bkConf := range confMap { backendRR := NewBackendRR() backendRR.Init(brr.Name, bkConf) + backend := backendRR.backend + backend.SetRestart(true) // add to backendsNew backendsNew = append(backendsNew, backendRR) } @@ -195,6 +221,10 @@ func (brr *BalanceRR) ensureSortedUnlocked() { // Balance select one backend from sub cluster in round robin manner. func (brr *BalanceRR) Balance(algor int, key []byte) (*backend.BfeBackend, error) { + // Slow start is not supported when session sticky is enabled + if algor != WrrSticky { + brr.checkSlowStart() + } switch algor { case WrrSimple: return brr.simpleBalance() diff --git a/bfe_balance/bal_slb/bal_rr_test.go b/bfe_balance/bal_slb/bal_rr_test.go index 39905183c..2d478a577 100644 --- a/bfe_balance/bal_slb/bal_rr_test.go +++ b/bfe_balance/bal_slb/bal_rr_test.go @@ -45,18 +45,18 @@ func prepareBalanceRR() *BalanceRR { rr := &BalanceRR{ backends: []*BackendRR{ { - weight: 3, - current: 3, + weight: 300, + current: 300, backend: b1, }, { - weight: 2, - current: 2, + weight: 200, + current: 200, backend: b2, }, { - weight: 1, - current: 1, + weight: 100, + current: 100, backend: b3, }, }, @@ -80,11 +80,63 @@ func processBalance(t *testing.T, label string, algor int, key []byte, rr *Balan } } +func processSimpleBalance(t *testing.T, label string, algor int, key []byte, rr *BalanceRR, result []string) { + var l []string + loopCount := (300+200+100)+4 + + for i := 1; i < loopCount; i++ { + r, err := rr.Balance(algor, key) + if err != nil { + t.Errorf("should not error") + } + r.IncConnNum() + // append the end of backend b3 + if (i > 297) && (i <= 303) { + l = append(l, r.Name) + } + // append the end of backend b1 + if (i > 600) && (i <= 603) { + l = append(l, r.Name) + } + } + + if !reflect.DeepEqual(l, result) { + t.Errorf("balance error [%s] %v, expect %v", label, l, result) + } +} + +func processSimpleBalance3(t *testing.T, label string, algor int, key []byte, rr *BalanceRR, result []string) { + var l []string + loopCount := (200+100)*3+4 + + for i := 1; i < loopCount; i++ { + r, err := rr.Balance(algor, key) + if err != nil { + t.Errorf("should not error") + } + r.IncConnNum() + // append the end of backend b3 + if (i > 198) && (i <= 201) { + l = append(l, r.Name) + } + if (i > 498) && (i <= 501) { + l = append(l, r.Name) + } + if (i > 798) && (i <= 801) { + l = append(l, r.Name) + } + } + + if !reflect.DeepEqual(l, result) { + t.Errorf("balance error [%s] %v, expect %v", label, l, result) + } +} + func TestBalance(t *testing.T) { // case 1 rr := prepareBalanceRR() expectResult := []string{"b1", "b2", "b3", "b1", "b2", "b1", "b1", "b2", "b3"} - processBalance(t, "case 1", WrrSimple, nil, rr, expectResult) + processSimpleBalance(t, "case 1", WrrSimple, nil, rr, expectResult) // case 2 rr = prepareBalanceRR() @@ -95,7 +147,7 @@ func TestBalance(t *testing.T) { rr = prepareBalanceRR() rr.backends[0].backend.SetAvail(false) expectResult = []string{"b2", "b3", "b2", "b2", "b3", "b2", "b2", "b3", "b2"} - processBalance(t, "case 3", WrrSimple, nil, rr, expectResult) + processSimpleBalance3(t, "case 3", WrrSimple, nil, rr, expectResult) // case 4 rr = prepareBalanceRR() @@ -105,7 +157,7 @@ func TestBalance(t *testing.T) { // case 5 rr = prepareBalanceRR() - expectResult = []string{"b2", "b2", "b2", "b2", "b2", "b2", "b2", "b2", "b2"} + expectResult = []string{"b1", "b1", "b1", "b1", "b1", "b1", "b1", "b1", "b1"} processBalance(t, "case 5", WrrSticky, []byte{1}, rr, expectResult) rr.backends[0], rr.backends[2] = rr.backends[2], rr.backends[0] @@ -115,7 +167,9 @@ func TestBalance(t *testing.T) { // case 6 rr = prepareBalanceRR() rr.backends[0].backend.SetAvail(false) - expectResult = []string{"b2", "b2", "b2", "b2", "b2", "b2", "b2", "b2", "b2"} + // after scale up 100, the hash result changed + expectResult = []string{"b3", "b3", "b3", "b3", "b3", "b3", "b3", "b3", "b3"} +// expectResult = []string{"b2", "b2", "b2", "b2", "b2", "b2", "b2", "b2", "b2"} processBalance(t, "case 6", WrrSticky, []byte{1}, rr, expectResult) // case 7, lcw balance @@ -190,7 +244,7 @@ func checkBackend(t *testing.T, brr *BackendRR, name string, addr string, port i if b.Port != port { t.Errorf("backend port wrong, expect %d, actual %d", port, b.Port) } - if brr.weight != weight { + if brr.weight != weight*100 { t.Errorf("backend weight wrong, expect %d, actual %d", weight, brr.weight) } if connNum != -1 && b.ConnNum() != connNum { @@ -239,3 +293,9 @@ func BenchmarkStickyBalance(b *testing.B) { rr.stickyBalance(key) } } + +func TestSlowStart(t *testing.T) { + // case 1 + rr := prepareBalanceRR() + rr.SetSlowStart(30) +} diff --git a/bfe_balance/bal_table.go b/bfe_balance/bal_table.go index 046a49795..5f53ad9f2 100644 --- a/bfe_balance/bal_table.go +++ b/bfe_balance/bal_table.go @@ -188,6 +188,29 @@ func (t *BalTable) SetGslbBasic(clusterTable *bfe_route.ClusterTable) { } } +// SetSlowStart sets slow_start related conf (from server data conf) for BalTable. +// +// Note: +// - SetSlowStart() is called after server reload server data conf +// - SetSlowStart() should be concurrency safe +func (t *BalTable) SetSlowStart(clusterTable *bfe_route.ClusterTable) { + t.lock.RLock() + defer t.lock.RUnlock() + + if clusterTable == nil { + return + } + + for clusterName, bal := range t.balTable { + cluster, err := clusterTable.Lookup(clusterName) + if err != nil { + continue + } + + bal.SetSlowStart(*cluster.BackendConf()) + } +} + func (t *BalTable) BalTableReload(gslbConfs gslb_conf.GslbConf, backendConfs cluster_table_conf.ClusterTableConf) error { t.lock.Lock() 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 47ad35747..a42894a8b 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 @@ -40,19 +40,6 @@ const ( 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, @@ -95,13 +82,14 @@ type FCGIConf struct { // BackendBasic is conf of backend basic type BackendBasic struct { - Protocol *string // backend protocol - TimeoutConnSrv *int // timeout for connect backend, in ms - 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 *string // backend protocol + TimeoutConnSrv *int // timeout for connect backend, in ms + TimeoutResponseHeader *int // timeout for read header from backend, in ms + MaxIdleConnsPerHost *int // max idle conns for each backend + MaxConnsPerHost *int // max conns for each backend (zero means unrestricted) + RetryLevel *int // retry level if request fail + SlowStartTime *int // time for backend increases the weight to the full value, in seconds + OutlierDetectionHttpCode *string // outlier detection http status code // protocol specific configurations FCGIConf *FCGIConf } @@ -186,14 +174,28 @@ func BackendBasicCheck(conf *BackendBasic) error { conf.MaxIdleConnsPerHost = &defaultIdle } + if conf.MaxConnsPerHost == nil || *conf.MaxConnsPerHost < 0 { + defaultConns := 0 + conf.MaxConnsPerHost = &defaultConns + } + if conf.RetryLevel == nil { retryLevel := RetryConnect conf.RetryLevel = &retryLevel } - if conf.OutlierDetectionLevel == nil { - outlierDetectionLevel := OutlierDetectionBasic - conf.OutlierDetectionLevel = &outlierDetectionLevel + if conf.OutlierDetectionHttpCode == nil { + outlierDetectionCode := "" + conf.OutlierDetectionHttpCode = &outlierDetectionCode + } else { + httpCode := *conf.OutlierDetectionHttpCode + httpCode = strings.ToLower(httpCode) + conf.OutlierDetectionHttpCode = &httpCode + } + + if conf.SlowStartTime == nil { + defaultSlowStartTime := 0 + conf.SlowStartTime = &defaultSlowStartTime } if conf.FCGIConf == nil { diff --git a/bfe_config/bfe_cluster_conf/cluster_conf/testdata/cluster_conf_1.conf b/bfe_config/bfe_cluster_conf/cluster_conf/testdata/cluster_conf_1.conf index 3f16d81c7..cc76434c8 100644 --- a/bfe_config/bfe_cluster_conf/cluster_conf/testdata/cluster_conf_1.conf +++ b/bfe_config/bfe_cluster_conf/cluster_conf/testdata/cluster_conf_1.conf @@ -6,7 +6,8 @@ "TimeoutConnSrv": 1000, "TimeoutWriteSrv": 2000, "TimeoutReadSrv": 2000, - "TimeoutResponseHeader":1000 + "TimeoutResponseHeader":1000, + "SlowStartTime": 30 }, "CheckConf": { "Uri": "/health", @@ -30,7 +31,8 @@ "TimeoutConnSrv": 1000, "TimeoutWriteSrv": 2000, "TimeoutReadSrv": 2000, - "TimeoutResponseHeader":1000 + "TimeoutResponseHeader":1000, + "SlowStartTime": 0 }, "CheckConf": { "Uri": "/health", diff --git a/bfe_config/bfe_conf/bfe_config_load.go b/bfe_config/bfe_conf/bfe_config_load.go index 7adc45e8b..2dfc13ef4 100644 --- a/bfe_config/bfe_conf/bfe_config_load.go +++ b/bfe_config/bfe_conf/bfe_config_load.go @@ -34,7 +34,6 @@ type BfeConfig struct { func SetDefaultConf(conf *BfeConfig) { conf.Server.SetDefaultConf() - conf.HttpsBasic.SetDefaultConf() conf.SessionCache.SetDefaultConf() conf.SessionTicket.SetDefaultConf() } diff --git a/bfe_config/bfe_conf/conf_https_basic.go b/bfe_config/bfe_conf/conf_https_basic.go index 8e63f24a2..32b4a1395 100644 --- a/bfe_config/bfe_conf/conf_https_basic.go +++ b/bfe_config/bfe_conf/conf_https_basic.go @@ -82,38 +82,12 @@ type ConfigHttpsBasic struct { ClientCRLBaseDir string // client cert CRL base directory } -func (cfg *ConfigHttpsBasic) SetDefaultConf() { - cfg.ServerCertConf = "tls_conf/server_cert_conf.data" - cfg.TlsRuleConf = "tls_conf/tls_rule_conf.data" - - cfg.CipherSuites = []string{ - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256|TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256|TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256|TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", - "TLS_ECDHE_RSA_WITH_RC4_128_SHA", - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", - "TLS_RSA_WITH_RC4_128_SHA", - "TLS_RSA_WITH_AES_128_CBC_SHA", - "TLS_RSA_WITH_AES_256_CBC_SHA", - } - cfg.CurvePreferences = []string{ - "CurveP256", - } - - cfg.EnableSslv2ClientHello = true - - cfg.ClientCABaseDir = "tls_conf/client_ca" -} - func (cfg *ConfigHttpsBasic) Check(confRoot string) error { - // check cert file conf err := certConfCheck(cfg, confRoot) if err != nil { return err } - // check cert rule conf err = certRuleCheck(cfg, confRoot) if err != nil { return err @@ -129,24 +103,16 @@ func (cfg *ConfigHttpsBasic) Check(confRoot string) error { return err } - // check CipherSuites - for _, cipherGroup := range cfg.CipherSuites { - ciphers := strings.Split(cipherGroup, EquivCipherSep) - for _, cipher := range ciphers { - if _, ok := CipherSuitesMap[cipher]; !ok { - return fmt.Errorf("cipher (%s) not support", cipher) - } - } + err = cipherSuitesCheck(cfg) + if err != nil { + return err } - // check CurvePreferences - for _, curve := range cfg.CurvePreferences { - if _, ok := CurvesMap[curve]; !ok { - return fmt.Errorf("curve (%s) not support", curve) - } + err = curvePreferencesCheck(cfg) + if err != nil { + return err } - // check tls version err = tlsVersionCheck(cfg) if err != nil { return err @@ -169,6 +135,51 @@ func certConfCheck(cfg *ConfigHttpsBasic, confRoot string) error { return nil } +func cipherSuitesCheck(cfg *ConfigHttpsBasic) error { + if len(cfg.CipherSuites) == 0 { + log.Logger.Warn("CipherSuites not set, use default value") + cfg.CipherSuites = []string{ + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256|TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256|TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256|TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_RC4_128_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_RC4_128_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA", + } + } + + for _, cipherGroup := range cfg.CipherSuites { + ciphers := strings.Split(cipherGroup, EquivCipherSep) + for _, cipher := range ciphers { + if _, ok := CipherSuitesMap[cipher]; !ok { + return fmt.Errorf("cipher (%s) not support", cipher) + } + } + } + + return nil +} + +func curvePreferencesCheck(cfg *ConfigHttpsBasic) error { + if len(cfg.CurvePreferences) == 0 { + log.Logger.Warn("CurvePreferences not set, use default value") + cfg.CurvePreferences = []string{ + "CurveP256", + } + } + + for _, curve := range cfg.CurvePreferences { + if _, ok := CurvesMap[curve]; !ok { + return fmt.Errorf("curve (%s) not support", curve) + } + } + + return nil +} + func certRuleCheck(cfg *ConfigHttpsBasic, confRoot string) error { if cfg.TlsRuleConf == "" { log.Logger.Warn("TlsRuleConf not set, use default value") diff --git a/bfe_http/cookie.go b/bfe_http/cookie.go index c768f6196..1a23d9f81 100644 --- a/bfe_http/cookie.go +++ b/bfe_http/cookie.go @@ -20,7 +20,6 @@ package bfe_http import ( "bytes" - "fmt" "net" "strconv" "strings" @@ -243,9 +242,12 @@ func SetCookie(w ResponseWriter, cookie *Cookie) { // header (if other fields are set). func (c *Cookie) String() string { var b bytes.Buffer - fmt.Fprintf(&b, "%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value)) + b.WriteString(sanitizeCookieName(c.Name)) + b.WriteRune('=') + b.WriteString(sanitizeCookieValue(c.Value)) if len(c.Path) > 0 { - fmt.Fprintf(&b, "; Path=%s", sanitizeCookiePath(c.Path)) + b.WriteString("; Path=") + b.WriteString(sanitizeCookiePath(c.Path)) } if len(c.Domain) > 0 { if validCookieDomain(c.Domain) { @@ -257,25 +259,32 @@ func (c *Cookie) String() string { if d[0] == '.' { d = d[1:] } - fmt.Fprintf(&b, "; Domain=%s", d) + b.WriteString("; Domain=") + b.WriteString(d) } else { log.Logger.Warn("net/http: invalid Cookie.Domain %q; dropping domain attribute", c.Domain) } } if c.Expires.Unix() > 0 { - fmt.Fprintf(&b, "; Expires=%s", c.Expires.UTC().Format(TimeFormat)) + b.WriteString("; Expires=") + b2 := b.Bytes() + b.Reset() + b.Write(c.Expires.UTC().AppendFormat(b2, TimeFormat)) } if c.MaxAge > 0 { - fmt.Fprintf(&b, "; Max-Age=%d", c.MaxAge) + b.WriteString("; Max-Age=") + b2 := b.Bytes() + b.Reset() + b.Write(strconv.AppendInt(b2, int64(c.MaxAge), 10)) } else if c.MaxAge < 0 { - fmt.Fprintf(&b, "; Max-Age=0") + b.WriteString("; Max-Age=0") } if c.HttpOnly { - fmt.Fprintf(&b, "; HttpOnly") + b.WriteString("; HttpOnly") } if c.Secure { - fmt.Fprintf(&b, "; Secure") + b.WriteString("; Secure") } switch c.SameSite { case SameSiteDefaultMode: @@ -295,25 +304,28 @@ func (c *Cookie) String() string { // // if filter isn't empty, only cookies of that name are returned func readCookies(h Header, filter string) []*Cookie { - cookies := []*Cookie{} - lines, ok := h["Cookie"] - if !ok { - return cookies + lines := h["Cookie"] + if len(lines) == 0 { + return []*Cookie{} } + cookies := make([]*Cookie, 0, len(lines)+strings.Count(lines[0], ";")) + for _, line := range lines { - parts := strings.Split(strings.TrimSpace(line), ";") - if len(parts) == 1 && parts[0] == "" { - continue - } - // Per-line attributes - parsedPairs := 0 - for i := 0; i < len(parts); i++ { - parts[i] = strings.TrimSpace(parts[i]) - if len(parts[i]) == 0 { + line = strings.TrimSpace(line) + + var part string + for len(line) > 0 { + if splitIndex := strings.Index(line, ";"); splitIndex > 0 { + part, line = line[:splitIndex], line[splitIndex+1:] + } else { + part, line = line, "" + } + part = strings.TrimSpace(part) + if len(part) == 0 { continue } - name, val := parts[i], "" + name, val := part, "" if j := strings.Index(name, "="); j >= 0 { name, val = name[:j], name[j+1:] } @@ -328,7 +340,6 @@ func readCookies(h Header, filter string) []*Cookie { continue } cookies = append(cookies, &Cookie{Name: name, Value: val}) - parsedPairs++ } } return cookies diff --git a/bfe_http/cookie_test.go b/bfe_http/cookie_test.go index 98959a295..0fffcda97 100644 --- a/bfe_http/cookie_test.go +++ b/bfe_http/cookie_test.go @@ -386,3 +386,24 @@ func TestDisableSanitize(t *testing.T) { } } } + +func BenchmarkCookieString(b *testing.B) { + const wantCookieString = `cookie-9=i3e01nf61b6t23bvfmplnanol3; Path=/restricted/; Domain=example.com; Expires=Tue, 10 Nov 2009 23:00:00 GMT; Max-Age=3600` + c := &Cookie{ + Name: "cookie-9", + Value: "i3e01nf61b6t23bvfmplnanol3", + Expires: time.Unix(1257894000, 0), + Path: "/restricted/", + Domain: ".example.com", + MaxAge: 3600, + } + var benchmarkCookieString string + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + benchmarkCookieString = c.String() + } + if have, want := benchmarkCookieString, wantCookieString; have != want { + b.Fatalf("Have: %v Want: %v", have, want) + } +} diff --git a/bfe_http/header.go b/bfe_http/header.go index f8642338d..f316a983b 100644 --- a/bfe_http/header.go +++ b/bfe_http/header.go @@ -59,6 +59,15 @@ func (h Header) Get(key string) string { return textproto.MIMEHeader(h).Get(key) } +// Values returns all values associated with the given key. +// It is case insensitive; textproto.CanonicalMIMEHeaderKey is +// used to canonicalize the provided key. To use non-canonical +// keys, access the map directly. +// The returned slice is not a copy. +func (h Header) Values(key string) []string { + return textproto.MIMEHeader(h).Values(key) +} + // GetDirect gets the value associated with the given key // in CanonicalHeaderKey form. func (h Header) GetDirect(key string) string { diff --git a/bfe_http/transport.go b/bfe_http/transport.go index 1c2bae3c1..3f129d176 100644 --- a/bfe_http/transport.go +++ b/bfe_http/transport.go @@ -69,6 +69,12 @@ type Transport struct { altMu sync.RWMutex altProto map[string]RoundTripper // nil or map of URI scheme => RoundTripper + connMu sync.Mutex // mutex for conn count + // Conn count which record current connection of each backend + // when create a persistConn we count plus one of the cm key, + // and minus one when the persistConn is close. + connCnt map[string]int + // Proxy specifies a function to return a proxy for a given // Request. If the function returns a non-nil error, the // request is aborted with the provided error. @@ -103,6 +109,10 @@ type Transport struct { // DefaultMaxIdleConnsPerHost is used. MaxIdleConnsPerHost int + // MaxConnsPerHost, if non-zero, controls the maximum currency conns + // to per-host. If less than or equal zero, transport will ignore this value. + MaxConnsPerHost int + // ResponseHeaderTimeout, if non-zero, specifies the amount of // time to wait for a server's response headers after fully // writing the request (including its body, if any). This @@ -321,6 +331,16 @@ func (cm *connectMethod) proxyAuth() string { return "" } +func (t *Transport) releaseConnCnt(cacheKey string) { + t.connMu.Lock() + if t.connCnt == nil { + t.connMu.Unlock() + return + } + t.connCnt[cacheKey]-- + t.connMu.Unlock() +} + // putIdleConn adds pconn to the list of idle persistent connections awaiting // a new request. // If pconn is no longer needed or not in a good state, putIdleConn @@ -443,6 +463,21 @@ func (t *Transport) dial(network, addr string) (c net.Conn, err error) { return net.Dial(network, addr) } +// check whether we can create new conn to backend with given cachekey +func (t *Transport) checkAndIncConnCnt(cacheKey string, maxValue int) bool { + t.connMu.Lock() + if t.connCnt == nil { + t.connCnt = make(map[string]int) + } + if val, ok := t.connCnt[cacheKey]; ok && val >= maxValue { + t.connMu.Unlock() + return false + } + t.connCnt[cacheKey]++ + t.connMu.Unlock() + return true +} + // getConn dials and creates a new persistConn to the target as // specified in the connectMethod. This includes doing a proxy CONNECT // and/or setting up TLS. If this doesn't return an error, the persistConn @@ -458,11 +493,19 @@ func (t *Transport) getConn(cm *connectMethod) (*persistConn, error) { } dialc := make(chan dialRes) go func() { + cacheKey := cm.key() + if t.MaxConnsPerHost > 0 && !t.checkAndIncConnCnt(cacheKey, t.MaxConnsPerHost) { + dialc <- dialRes{nil, fmt.Errorf("cm key[%v] greater than max conns[%d]", cacheKey, t.MaxConnsPerHost)} + return + } pc, err := t.dialConn(cm) state.HttpBackendConnAll.Inc(1) if err == nil { state.HttpBackendConnSucc.Inc(1) + } else { + t.releaseConnCnt(cacheKey) } + dialc <- dialRes{pc, err} }() @@ -1035,6 +1078,9 @@ func (pc *persistConn) closeLocked() { if !pc.closed { pc.conn.Close() pc.closed = true + // there are some many reason to close a conn, in order to avoid missing release in some place, + // it is a safely way to release conn cnt in pc.close() + pc.t.releaseConnCnt(pc.cacheKey) } pc.mutateHeaderFunc = nil } diff --git a/bfe_http2/server.go b/bfe_http2/server.go index 60e387f82..9ccc0945c 100644 --- a/bfe_http2/server.go +++ b/bfe_http2/server.go @@ -1567,12 +1567,23 @@ func (sc *serverConn) processSettingInitialWindowSize(val uint32) error { func (sc *serverConn) processData(f *DataFrame) error { sc.serveG.Check() + id := f.Header().StreamID + if sc.inGoAway && (sc.goAwayCode != ErrCodeNo || id > sc.maxStreamID) { + // Discard all DATA frames if the GOAWAY is due to an + // error, or: + // + // Section 6.8: After sending a GOAWAY frame, the sender + // can discard frames for streams initiated by the + // receiver with identifiers higher than the identified + // last stream. + return nil + } + data := f.Data() // "If a DATA frame is received whose stream is not in "open" // or "half closed (local)" state, the recipient MUST respond // with a stream error (Section 5.4.2) of type STREAM_CLOSED." - id := f.Header().StreamID st, ok := sc.streams[id] if !ok || st.state != stateOpen || st.gotTrailerHeader { // This includes sending a RST_STREAM if the stream is @@ -1607,7 +1618,10 @@ func (sc *serverConn) processData(f *DataFrame) error { if st.declBodyBytes != -1 && st.bodyBytes+int64(len(data)) > st.declBodyBytes { err := fmt.Errorf("sender tried to send more than declared Content-Length of %d bytes", st.declBodyBytes) st.body.CloseWithError(err) - return StreamError{id, ErrCodeStreamClosed, err.Error()} + // RFC 7540, sec 8.1.2.6: A request or response is also malformed if the + // value of a content-length header field does not equal the sum of the + // DATA frame payload lengths that form the body. + return StreamError{id, ErrCodeProtocol, err.Error()} } if f.Length > 0 { // Check whether the client has flow control quota. diff --git a/bfe_http2/server_test.go b/bfe_http2/server_test.go index 57fe049be..a37650f85 100644 --- a/bfe_http2/server_test.go +++ b/bfe_http2/server_test.go @@ -2960,3 +2960,68 @@ y2ptGsuSmgUtWj3NM9xuwYPm+Z/F84K6+ARYiZ6PYj013sovGKUFfYAqVXVlxtIX qyUBnu3X9ps8ZfjLZO7BAkEAlT4R5Yl6cGhaJQYZHOde3JEMhNRcVFMO8dJDaFeo f9Oeos0UUothgiDktdQHxdNEwLjQf7lJJBzV+5OtwswCWA== -----END RSA PRIVATE KEY-----`) + +func TestNoRstPostAfterGOAWAY(t *testing.T) { + const msg = "Hello, world." + st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) { + n, err := io.Copy(ioutil.Discard, r.Body) + if err != nil || n > 0 { + t.Errorf("Read %d bytes, error %v; want 0 bytes.", n, err) + } + io.WriteString(w, msg) + }) + defer st.Close() + st.greet() + // Give the server quota to reply. (plus it has the the 64KB) + if err := st.fr.WriteWindowUpdate(0, uint32(1*len(msg))); err != nil { + t.Fatal(err) + } + hbf := st.encodeHeader(":method", "POST") + st.writeHeaders(HeadersFrameParam{ + StreamID: 1, + BlockFragment: hbf, + EndStream: false, + EndHeaders: true, + }) + close(st.sc.closeNotifyCh) + st.writeData(1, true, nil) + + st.wantGoAway() + for { + f, err := st.readFrame() + if err == io.EOF { + st.t.Fatal("got a EOF; want *GoAwayFrame") + } + if err != nil && err.Error() == "timeout waiting for frame" { + break + } + if err != nil { + t.Fatal(err) + } + if gf, ok := f.(*RSTStreamFrame); ok && gf.StreamID == 1 { + t.Fatal("got rst but want no ret") + break + } + } + +} + +func TestServer_Rejects_TooSmall(t *testing.T) { + testServerResponse(t, func(w http.ResponseWriter, r *http.Request) error { + ioutil.ReadAll(r.Body) + return nil + }, func(st *serverTester) { + st.writeHeaders(HeadersFrameParam{ + StreamID: 1, // clients send odd numbers + BlockFragment: st.encodeHeader( + ":method", "POST", + "content-length", "4", + ), + EndStream: false, // to say DATA frames are coming + EndHeaders: true, + }) + st.writeData(1, true, []byte("12345")) + + st.wantRSTStream(1, ErrCodeProtocol) + }) +} diff --git a/bfe_modules/mod_header/action.go b/bfe_modules/mod_header/action.go index f0d0a55f4..b302a9158 100644 --- a/bfe_modules/mod_header/action.go +++ b/bfe_modules/mod_header/action.go @@ -51,8 +51,10 @@ func ActionFileCheck(conf ActionFile) error { switch *conf.Cmd { case "REQ_HEADER_SET", "REQ_HEADER_ADD", + "REQ_HEADER_RENAME", "RSP_HEADER_SET", - "RSP_HEADER_ADD": + "RSP_HEADER_ADD", + "RSP_HEADER_RENAME": // header and value if len(conf.Params) != 2 { @@ -193,7 +195,7 @@ func expectPercent(str string) int { return index } -const variableCharset = "abcdefghijklmnopqrstuvwxyz_" +const variableCharset = "abcdefghijklmnopqrstuvwxyz0123456789_" func expectVariableParam(str string) int { index := 0 @@ -374,7 +376,11 @@ func actionConvert(actionFile ActionFile) (Action, error) { // append key values action.Params = append(action.Params, key) action.Params = append(action.Params, values...) - + case "REQ_HEADER_RENAME", "RSP_HEADER_RENAME": + originalKey := textproto.CanonicalMIMEHeaderKey(actionFile.Params[0]) + newKey := textproto.CanonicalMIMEHeaderKey(actionFile.Params[1]) + action.Params = append(action.Params, originalKey) + action.Params = append(action.Params, newKey) case "REQ_HEADER_DEL", "RSP_HEADER_DEL": // - REQ_HEADER_DEL: [referer] // - RSP_HEADER_DEL: [location] @@ -430,6 +436,8 @@ func HeaderActionDo(h *bfe_http.Header, cmd string, headerName string, value str // delete case "HEADER_DEL": headerDel(h, headerName) + case "HEADER_RENAME": + headerRename(h, headerName, value) } } @@ -447,26 +455,35 @@ func getHeader(req *bfe_basic.Request, headerType int) (h *bfe_http.Header) { func processHeader(req *bfe_basic.Request, headerType int, action Action) { var key string var value string + var cmd string h := getHeader(req, headerType) - if action.Cmd[4:] == "HEADER_MOD" { + cmd = action.Cmd[4:] + + switch cmd { + case "HEADER_MOD": key = action.Params[1] // get header value if value = h.Get(key); value == "" { // if req do not have this header, continue return } - // mod header value value = modHeaderValue(value, action) - } else { + case "HEADER_RENAME": + originalKey, newKey := action.Params[0], action.Params[1] + if h.Get(originalKey) == "" || h.Get(newKey) != "" { + return + } + key, value = originalKey, newKey + default: key = action.Params[0] value = getHeaderValue(req, action) } // trim action.Cmd prefix REQ_ and RSP_ - HeaderActionDo(h, action.Cmd[4:], key, value) + HeaderActionDo(h, cmd, key, value) } func processCookie(req *bfe_basic.Request, headerType int, action Action) { diff --git a/bfe_modules/mod_header/action_header.go b/bfe_modules/mod_header/action_header.go index cabe273f7..906b35102 100644 --- a/bfe_modules/mod_header/action_header.go +++ b/bfe_modules/mod_header/action_header.go @@ -32,3 +32,10 @@ func headerAdd(h *bfe_http.Header, key string, value string) { func headerDel(h *bfe_http.Header, key string) { h.Del(key) } + +// rename header originalKey to newKey +func headerRename(h *bfe_http.Header, originalKey, newKey string) { + val := h.Get(originalKey) + h.Set(newKey, val) + h.Del(originalKey) +} diff --git a/bfe_modules/mod_header/action_header_var.go b/bfe_modules/mod_header/action_header_var.go index cc864c488..ac5460063 100644 --- a/bfe_modules/mod_header/action_header_var.go +++ b/bfe_modules/mod_header/action_header_var.go @@ -62,6 +62,8 @@ var VariableHandlers = map[string]HeaderValueHandler{ "bfe_ssl_resume": getBfeSslResume, "bfe_ssl_cipher": getBfeSslCipher, "bfe_ssl_version": getBfeSslVersion, + "bfe_ssl_ja3_raw": getBfeSslJa3Raw, + "bfe_ssl_ja3_hash": getBfeSslJa3Hash, "bfe_protocol": getBfeProtocol, "client_cert_serial_number": getClientCertSerialNumber, "client_cert_subject_title": getClientCertSubjectTitle, @@ -180,6 +182,24 @@ func getBfeSslVersion(req *bfe_basic.Request) string { return bfe_tls.VersionTextForOpenSSL(state.Version) } +// get tls ja3 string +func getBfeSslJa3Raw(req *bfe_basic.Request) string { + if req.Session.TlsState == nil { + return "" + } + state := req.Session.TlsState + return state.JA3Raw +} + +// get tls ja3 hash +func getBfeSslJa3Hash(req *bfe_basic.Request) string { + if req.Session.TlsState == nil { + return "" + } + state := req.Session.TlsState + return state.JA3Hash +} + // get protocol for application level func getBfeProtocol(req *bfe_basic.Request) string { return req.Protocol() diff --git a/bfe_modules/mod_header/action_test.go b/bfe_modules/mod_header/action_test.go index 8018e8fc4..8c1e1454c 100644 --- a/bfe_modules/mod_header/action_test.go +++ b/bfe_modules/mod_header/action_test.go @@ -165,6 +165,27 @@ func TestHeaderActionsDo_Case4(t *testing.T) { } } +func TestHeaderActionsDo_Case5(t *testing.T) { + req := makeBasicRequest("http://www.example.org") + + cmdMod := "REQ_HEADER_RENAME" + action := Action{Cmd: cmdMod, Params: []string{"OriginalKey", "NewKey"}} + expectVal := "TestCase" + + req.HttpRequest.Header.Add("OriginalKey", expectVal) + HeaderActionsDo(req, 0, []Action{action}) + + value := req.HttpRequest.Header.Get("NewKey") + if value != expectVal { + t.Errorf("header rename newkey want[%s] got[%s]", expectVal, value) + } + + value = req.HttpRequest.Header.Get("OriginalKey") + if value != "" { + t.Errorf("header rename originalkey want[%s] got[%s]", "", value) + } +} + func TestActionsConvert(t *testing.T) { cmdSet := "REQ_HEADER_SET" cmdAdd := "REQ_HEADER_ADD" diff --git a/bfe_modules/mod_header/testdata/mod_header/header_rule.data b/bfe_modules/mod_header/testdata/mod_header/header_rule.data index 46c494953..6fe18d256 100644 --- a/bfe_modules/mod_header/testdata/mod_header/header_rule.data +++ b/bfe_modules/mod_header/testdata/mod_header/header_rule.data @@ -2,6 +2,19 @@ "Version": "1234", "Config": { "p1": [ + { + "cond": "req_path_prefix_in(\"/header_rename\", false)", + "actions": [ + { + "cmd": "REQ_HEADER_RENAME", + "params": [ + "OriginalKey", + "NewKey" + ] + } + ], + "last": true + }, { "cond": "req_path_prefix_in(\"/cookie_set\", false)", "actions": [ @@ -183,7 +196,7 @@ ] } ], - "last": true + "last": false } ] } diff --git a/bfe_net/textproto/header.go b/bfe_net/textproto/header.go index 84c2b4b32..d6db58303 100644 --- a/bfe_net/textproto/header.go +++ b/bfe_net/textproto/header.go @@ -54,6 +54,18 @@ func (h MIMEHeader) Get(key string) string { return v[0] } +// Values returns all values associated with the given key. +// It is case insensitive; CanonicalMIMEHeaderKey is +// used to canonicalize the provided key. To use non-canonical +// keys, access the map directly. +// The returned slice is not a copy. +func (h MIMEHeader) Values(key string) []string { + if h == nil { + return nil + } + return h[CanonicalMIMEHeaderKey(key)] +} + // Del deletes the values associated with key. func (h MIMEHeader) Del(key string) { delete(h, CanonicalMIMEHeaderKey(key)) diff --git a/bfe_net/textproto/header_test.go b/bfe_net/textproto/header_test.go new file mode 100644 index 000000000..e800aede9 --- /dev/null +++ b/bfe_net/textproto/header_test.go @@ -0,0 +1,68 @@ +// Copyright (c) 2021 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. + +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package textproto + +import "testing" + +type canonicalHeaderKeyTest struct { + in, out string +} + +var canonicalHeaderKeyTests = []canonicalHeaderKeyTest{ + {"a-b-c", "A-B-C"}, + {"a-1-c", "A-1-C"}, + {"User-Agent", "User-Agent"}, + {"uSER-aGENT", "User-Agent"}, + {"user-agent", "User-Agent"}, + {"USER-AGENT", "User-Agent"}, + + // Other valid tchar bytes in tokens: + {"foo-bar_baz", "Foo-Bar_baz"}, + {"foo-bar$baz", "Foo-Bar$baz"}, + {"foo-bar~baz", "Foo-Bar~baz"}, + {"foo-bar*baz", "Foo-Bar*baz"}, + + // Non-ASCII or anything with spaces or non-token chars is unchanged: + {"üser-agenT", "üser-agenT"}, + {"a B", "a B"}, + + // This caused a panic due to mishandling of a space: + {"C Ontent-Transfer-Encoding", "C Ontent-Transfer-Encoding"}, + {"foo bar", "foo bar"}, +} + +func TestCanonicalMIMEHeaderKey(t *testing.T) { + for _, tt := range canonicalHeaderKeyTests { + if s := CanonicalMIMEHeaderKey(tt.in); s != tt.out { + t.Errorf("CanonicalMIMEHeaderKey(%q) = %q, want %q", tt.in, s, tt.out) + } + } +} + +// Issue #34799 add a Header method to get multiple values []string, with canonicalized key +func TestMIMEHeaderMultipleValues(t *testing.T) { + testHeader := MIMEHeader{ + "Set-Cookie": {"cookie 1", "cookie 2"}, + } + values := testHeader.Values("set-cookie") + n := len(values) + if n != 2 { + t.Errorf("count: %d; want 2", n) + } +} diff --git a/bfe_net/textproto/pipeline.go b/bfe_net/textproto/pipeline.go index a10d294fd..5db2001d3 100644 --- a/bfe_net/textproto/pipeline.go +++ b/bfe_net/textproto/pipeline.go @@ -86,7 +86,7 @@ func (p *Pipeline) EndResponse(id uint) { type sequencer struct { mu sync.Mutex id uint - wait map[uint]chan uint + wait map[uint]chan struct{} } // Start waits until it is time for the event numbered id to begin. @@ -98,9 +98,9 @@ func (s *sequencer) Start(id uint) { s.mu.Unlock() return } - c := make(chan uint) + c := make(chan struct{}) if s.wait == nil { - s.wait = make(map[uint]chan uint) + s.wait = make(map[uint]chan struct{}) } s.wait[id] = c s.mu.Unlock() @@ -113,12 +113,13 @@ func (s *sequencer) Start(id uint) { func (s *sequencer) End(id uint) { s.mu.Lock() if s.id != id { + s.mu.Unlock() panic("out of sync") } id++ s.id = id if s.wait == nil { - s.wait = make(map[uint]chan uint) + s.wait = make(map[uint]chan struct{}) } c, ok := s.wait[id] if ok { @@ -126,6 +127,6 @@ func (s *sequencer) End(id uint) { } s.mu.Unlock() if ok { - c <- 1 + close(c) } } diff --git a/bfe_net/textproto/reader.go b/bfe_net/textproto/reader.go index e4eedb5d5..153e6c664 100644 --- a/bfe_net/textproto/reader.go +++ b/bfe_net/textproto/reader.go @@ -148,12 +148,13 @@ func (r *Reader) readContinuedLineSlice() ([]byte, error) { } // Optimistically assume that we have started to buffer the next line - // and it starts with an ASCII letter (the next header key), so we can - // avoid copying that buffered data around in memory and skipping over - // non-existent whitespace. + // and it starts with an ASCII letter (the next header key), or a blank + // line, so we can avoid copying that buffered data around in memory + // and skipping over non-existent whitespace. if r.R.Buffered() > 1 { - peek, err := r.R.Peek(1) - if err == nil && isASCIILetter(peek[0]) { + peek, _ := r.R.Peek(2) + if len(peek) > 0 && (isASCIILetter(peek[0]) || peek[0] == '\n') || + len(peek) == 2 && peek[0] == '\r' && peek[1] == '\n' { return trim(line), nil } } @@ -169,7 +170,7 @@ func (r *Reader) readContinuedLineSlice() ([]byte, error) { break } r.buf = append(r.buf, ' ') - r.buf = append(r.buf, line...) + r.buf = append(r.buf, trim(line)...) } return r.buf, nil } diff --git a/bfe_net/textproto/reader_test.go b/bfe_net/textproto/reader_test.go index 42f1dce8a..0b1632704 100644 --- a/bfe_net/textproto/reader_test.go +++ b/bfe_net/textproto/reader_test.go @@ -30,41 +30,6 @@ import ( "github.com/bfenetworks/bfe/bfe_bufio" ) -type canonicalHeaderKeyTest struct { - in, out string -} - -var canonicalHeaderKeyTests = []canonicalHeaderKeyTest{ - {"a-b-c", "A-B-C"}, - {"a-1-c", "A-1-C"}, - {"User-Agent", "User-Agent"}, - {"uSER-aGENT", "User-Agent"}, - {"user-agent", "User-Agent"}, - {"USER-AGENT", "User-Agent"}, - - // Other valid tchar bytes in tokens: - {"foo-bar_baz", "Foo-Bar_baz"}, - {"foo-bar$baz", "Foo-Bar$baz"}, - {"foo-bar~baz", "Foo-Bar~baz"}, - {"foo-bar*baz", "Foo-Bar*baz"}, - - // Non-ASCII or anything with spaces or non-token chars is unchanged: - {"üser-agenT", "üser-agenT"}, - {"a B", "a B"}, - - // This caused a panic due to mishandling of a space: - {"C Ontent-Transfer-Encoding", "C Ontent-Transfer-Encoding"}, - {"foo bar", "foo bar"}, -} - -func TestCanonicalMIMEHeaderKey(t *testing.T) { - for _, tt := range canonicalHeaderKeyTests { - if s := CanonicalMIMEHeaderKey(tt.in); s != tt.out { - t.Errorf("CanonicalMIMEHeaderKey(%q) = %q, want %q", tt.in, s, tt.out) - } - } -} - func reader(s string) *Reader { return NewReader(bfe_bufio.NewReader(strings.NewReader(s))) } @@ -226,6 +191,32 @@ func TestReadMIMEHeaderNonCompliant(t *testing.T) { } } +// Test that continued lines are properly trimmed. Issue 11204. +func TestReadMIMEHeaderTrimContinued(t *testing.T) { + // In this header, \n and \r\n terminated lines are mixed on purpose. + // We expect each line to be trimmed (prefix and suffix) before being concatenated. + // Keep the spaces as they are. + r := reader("" + // for code formatting purpose. + "a:\n" + + " 0 \r\n" + + "b:1 \t\r\n" + + "c: 2\r\n" + + " 3\t\n" + + " \t 4 \r\n\n") + m, err := r.ReadMIMEHeader() + if err != nil { + t.Fatal(err) + } + want := MIMEHeader{ + "A": {"0"}, + "B": {"1"}, + "C": {"2 3 4"}, + } + if !reflect.DeepEqual(m, want) { + t.Fatalf("ReadMIMEHeader mismatch.\n got: %q\nwant: %q", m, want) + } +} + type readResponseTest struct { in string inCode int diff --git a/bfe_net/textproto/writer.go b/bfe_net/textproto/writer.go index 118dbb878..2d23e337b 100644 --- a/bfe_net/textproto/writer.go +++ b/bfe_net/textproto/writer.go @@ -75,7 +75,8 @@ type dotWriter struct { } const ( - wstateBeginLine = iota // beginning of line; initial state; must be zero + wstateBegin = iota // initial state; must be zero + wstateBeginLine // beginning of line wstateCR // wrote \r (possibly at end of line) wstateData // writing data in middle of line ) @@ -85,7 +86,7 @@ func (d *dotWriter) Write(b []byte) (n int, err error) { for n < len(b) { c := b[n] switch d.state { - case wstateBeginLine: + case wstateBegin, wstateBeginLine: d.state = wstateData if c == '.' { // escape leading dot diff --git a/bfe_net/textproto/writer_test.go b/bfe_net/textproto/writer_test.go index b7056257b..70cba2a2c 100644 --- a/bfe_net/textproto/writer_test.go +++ b/bfe_net/textproto/writer_test.go @@ -50,3 +50,29 @@ func TestDotWriter(t *testing.T) { t.Fatalf("wrote %q", s) } } + +func TestDotWriterCloseEmptyWrite(t *testing.T) { + var buf bytes.Buffer + w := NewWriter(bfe_bufio.NewWriter(&buf)) + d := w.DotWriter() + n, err := d.Write([]byte{}) + if n != 0 || err != nil { + t.Fatalf("Write: %d, %s", n, err) + } + d.Close() + want := "\r\n.\r\n" + if s := buf.String(); s != want { + t.Fatalf("wrote %q; want %q", s, want) + } +} + +func TestDotWriterCloseNoWrite(t *testing.T) { + var buf bytes.Buffer + w := NewWriter(bfe_bufio.NewWriter(&buf)) + d := w.DotWriter() + d.Close() + want := "\r\n.\r\n" + if s := buf.String(); s != want { + t.Fatalf("wrote %q; want %q", s, want) + } +} diff --git a/bfe_route/bfe_cluster/bfe_cluster.go b/bfe_route/bfe_cluster/bfe_cluster.go index 095ea7071..19eb2bb31 100644 --- a/bfe_route/bfe_cluster/bfe_cluster.go +++ b/bfe_route/bfe_cluster/bfe_cluster.go @@ -113,12 +113,11 @@ func (cluster *BfeCluster) RetryLevel() int { return *retryLevel } -func (cluster *BfeCluster) OutlierDetectionLevel() int { +func (cluster *BfeCluster) OutlierDetectionHttpCode() string { cluster.RLock() - outlierDetectionLevel := cluster.backendConf.OutlierDetectionLevel + outlierDetectionHttpCode := cluster.backendConf.OutlierDetectionHttpCode cluster.RUnlock() - - return *outlierDetectionLevel + return *outlierDetectionHttpCode } func (cluster *BfeCluster) TimeoutReadClient() time.Duration { diff --git a/bfe_server/bfe_confdata_load.go b/bfe_server/bfe_confdata_load.go index c44cf1e0c..ae99a2112 100644 --- a/bfe_server/bfe_confdata_load.go +++ b/bfe_server/bfe_confdata_load.go @@ -56,10 +56,11 @@ func (srv *BfeServer) InitDataLoad() error { return fmt.Errorf("InitDataLoad():balTableInit Error %s", err) } - // set gslb retry config + // set gslb retry config, slow_start config if srv.ServerConf != nil { ct := srv.ServerConf.ClusterTable srv.balTable.SetGslbBasic(ct) + srv.balTable.SetSlowStart(ct) } log.Logger.Info("init bal table success") @@ -116,6 +117,8 @@ func (srv *BfeServer) serverDataConfReload(hostFile, vipFile, routeFile, cluster // set gslb basic srv.balTable.SetGslbBasic(newServerConf.ClusterTable) + // set slow_start config + srv.balTable.SetSlowStart(newServerConf.ClusterTable) return nil } @@ -152,6 +155,8 @@ func (srv *BfeServer) gslbDataConfReload(gslbFile, clusterTableFile string) erro serverConf := srv.ServerConf srv.confLock.Unlock() srv.balTable.SetGslbBasic(serverConf.ClusterTable) + // set slow_start config + srv.balTable.SetSlowStart(serverConf.ClusterTable) return nil } diff --git a/bfe_server/reverseproxy.go b/bfe_server/reverseproxy.go index 29af312b2..fdfc954e1 100644 --- a/bfe_server/reverseproxy.go +++ b/bfe_server/reverseproxy.go @@ -25,6 +25,8 @@ import ( "io" "net" "reflect" + "strconv" + "strings" "sync" "time" ) @@ -155,6 +157,7 @@ func (p *ReverseProxy) setTransports(clusterMap bfe_route.ClusterMap) { // get transport, check if transport needs update backendConf := conf.BackendConf() if (t.MaxIdleConnsPerHost != *backendConf.MaxIdleConnsPerHost) || + (t.MaxConnsPerHost != *backendConf.MaxConnsPerHost) || (t.ResponseHeaderTimeout != time.Millisecond*time.Duration(*backendConf.TimeoutResponseHeader)) || (t.ReqWriteBufferSize != conf.ReqWriteBufferSize()) || (t.ReqFlushInterval != conf.ReqFlushInterval()) { @@ -215,6 +218,7 @@ func createTransport(cluster *bfe_cluster.BfeCluster) bfe_http.RoundTripper { ReqWriteBufferSize: cluster.ReqWriteBufferSize(), ReqFlushInterval: cluster.ReqFlushInterval(), DisableCompression: true, + MaxConnsPerHost: *backendConf.MaxConnsPerHost, } case "fcgi": return &bfe_fcgi.Transport{ @@ -334,7 +338,7 @@ func (p *ReverseProxy) clusterInvoke(srv *BfeServer, cluster *bfe_cluster.BfeClu request.Backend.BackendPort = uint32(backend.Port) if err == nil { - if checkBackendStatus(cluster.OutlierDetectionLevel(), res.StatusCode) { + if checkBackendStatus(cluster.OutlierDetectionHttpCode(), res.StatusCode) { backend.OnFail(cluster.Name) } else { backend.OnSuccess() @@ -758,8 +762,16 @@ response_got: // we must timeout both conns after specified duration. 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 + if basicReq.Trans.Transport != nil { + // TODO: process bfe_fcgi.Transport & bfe_http2.Transport + switch t := basicReq.Trans.Transport.(type) { + case *bfe_http.Transport: + t.CancelRequest(req) + default: + // do nothing + } + } + }) defer writeTimer.Stop() @@ -878,6 +890,25 @@ func checkRequestWithoutBody(req *bfe_http.Request) bool { return false } -func checkBackendStatus(outlierDetectionLevel int, statusCode int) bool { - return outlierDetectionLevel == cluster_conf.OutlierDetection5XX && statusCode/100 == 5 +func checkBackendStatus(outlierDetectionHttpCodeStr string, statusCode int) bool { + if outlierDetectionHttpCodeStr == "" { + return false + } + for _, code := range strings.Split(outlierDetectionHttpCodeStr, "|") { + switch code { + case "3xx", "4xx", "5xx": + if strconv.Itoa(statusCode/100) == code[0:1] { + return true + } + default: + codeInt, err := strconv.Atoi(code) + if err != nil { + continue + } + if codeInt == statusCode { + return true + } + } + } + return false } diff --git a/bfe_spdy/frame_test.go b/bfe_spdy/frame_test.go index 28020a5e0..cbf73d89e 100644 --- a/bfe_spdy/frame_test.go +++ b/bfe_spdy/frame_test.go @@ -538,10 +538,12 @@ func TestMultipleSPDYFrames(t *testing.T) { // Start the goroutines to write the frames. go func() { if err := writer.WriteFrame(&headersFrame); err != nil { - t.Fatal("WriteFrame (HEADERS): ", err) + t.Log("WriteFrame (HEADERS): ", err) + return } if err := writer.WriteFrame(&synStreamFrame); err != nil { - t.Fatal("WriteFrame (SYN_STREAM): ", err) + t.Log("WriteFrame (SYN_STREAM): ", err) + return } }() diff --git a/bfe_tls/common.go b/bfe_tls/common.go index 8bf38041c..039aa64eb 100644 --- a/bfe_tls/common.go +++ b/bfe_tls/common.go @@ -224,6 +224,8 @@ type ConnectionState struct { ClientCiphers []uint16 // ciphers supported by client ClientAuth bool // enable TLS Client Authentication ClientCAName string // TLS client CA name + JA3Raw string // JA3 fingerprint string for TLS Client + JA3Hash string // JA3 fingerprint hash for TLS Client } // ClientAuthType declares the policy the server will follow for diff --git a/bfe_tls/conn.go b/bfe_tls/conn.go index 4948fe0b5..e9c756d20 100644 --- a/bfe_tls/conn.go +++ b/bfe_tls/conn.go @@ -76,6 +76,8 @@ type Conn struct { serverRandom []byte // random in server hello msg masterSecret []byte // master secret for conn clientCiphers []uint16 // ciphers supported by client + ja3Raw string // JA3 fingerprint string for TLS Client + ja3Hash string // JA3 fingerprint hash for TLS Client clientProtocol string clientProtocolFallback bool @@ -1276,6 +1278,8 @@ func (c *Conn) ConnectionState() ConnectionState { state.ClientAuth = true } state.ClientCAName = c.clientCAName + state.JA3Raw = c.ja3Raw + state.JA3Hash = c.ja3Hash } return state diff --git a/bfe_tls/handshake_messages.go b/bfe_tls/handshake_messages.go index 37db09f05..fb7e1ab23 100644 --- a/bfe_tls/handshake_messages.go +++ b/bfe_tls/handshake_messages.go @@ -20,6 +20,7 @@ package bfe_tls import ( "bytes" + "fmt" ) type clientHelloMsg struct { @@ -40,6 +41,7 @@ type clientHelloMsg struct { secureRenegotiation bool alpnProtocols []string padding bool + extensionIds []uint16 } func (m *clientHelloMsg) equal(i interface{}) bool { @@ -66,6 +68,45 @@ func (m *clientHelloMsg) equal(i interface{}) bool { eqStrings(m.alpnProtocols, m1.alpnProtocols) } +// JA3String returns a JA3 fingerprint string for TLS client. +// For more information, see https://github.com/salesforce/ja3 +func (m *clientHelloMsg) JA3String() string { + var buf bytes.Buffer + // version + fmt.Fprintf(&buf, "%d,", m.vers) + // cipher surites + writeJA3Uint16Values(&buf, m.cipherSuites) + fmt.Fprintf(&buf, ",") + // extensions + writeJA3Uint16Values(&buf, m.extensionIds) + fmt.Fprintf(&buf, ",") + // elliptic curves + for i, curve := range m.supportedCurves { + fmt.Fprintf(&buf, "%d", curve) + if i != len(m.supportedCurves)-1 { + fmt.Fprintf(&buf, "-") + } + } + fmt.Fprintf(&buf, ",") + // elliptic curves point formats + for i, point := range m.supportedPoints { + fmt.Fprintf(&buf, "%d", point) + if i != len(m.supportedPoints)-1 { + fmt.Fprintf(&buf, "-") + } + } + return buf.String() +} + +func writeJA3Uint16Values(buf *bytes.Buffer, values []uint16) { + for i, value := range values { + fmt.Fprintf(buf, "%d", value) + if i != len(values)-1 { + fmt.Fprintf(buf, "-") + } + } +} + func (m *clientHelloMsg) marshal() []byte { if m.raw != nil { return m.raw @@ -344,6 +385,7 @@ func (m *clientHelloMsg) unmarshal(data []byte) bool { m.signatureAndHashes = nil m.alpnProtocols = nil + m.extensionIds = make([]uint16, 0) if len(data) == 0 { // ClientHello is optionally followed by extension data return true @@ -369,6 +411,7 @@ func (m *clientHelloMsg) unmarshal(data []byte) bool { return false } + m.extensionIds = append(m.extensionIds, extension) switch extension { case extensionServerName: if length < 2 { diff --git a/bfe_tls/handshake_messages_test.go b/bfe_tls/handshake_messages_test.go index edd846d1f..04eb208fb 100644 --- a/bfe_tls/handshake_messages_test.go +++ b/bfe_tls/handshake_messages_test.go @@ -19,6 +19,8 @@ package bfe_tls import ( + "crypto/md5" + "fmt" "math/rand" "reflect" "testing" @@ -263,3 +265,34 @@ func (*sessionState) Generate(rand *rand.Rand, size int) reflect.Value { } return reflect.ValueOf(s) } + +var ja3HashTests = []struct { + vers uint16 + cipherSuites []uint16 + extensionIds []uint16 + supportedCurves []CurveID + supportedPoints []uint8 + ja3Hash string +}{ + {769, []uint16{47, 53, 5, 10, 49161, 49162, 49171, 49172, 50, 56, 19, 4}, + []uint16{0, 10, 11}, []CurveID{23, 24, 25}, []uint8{0}, + "ada70206e40642a3e4461f35503241d5"}, + {769, []uint16{4, 5, 10, 9, 100, 98, 3, 6, 19, 18, 99}, + []uint16{}, []CurveID{}, []uint8{}, + "de350869b8c85de67a350c8d186f11e6"}, +} + +func TestJA3Hash(t *testing.T) { + for i, d := range ja3HashTests { + msg := clientHelloMsg{} + msg.vers = d.vers + msg.cipherSuites = d.cipherSuites + msg.extensionIds = d.extensionIds + msg.supportedCurves = d.supportedCurves + msg.supportedPoints = d.supportedPoints + ja3Value := md5.Sum([]byte(msg.JA3String())) + if d.ja3Hash != fmt.Sprintf("%x", ja3Value) { + t.Errorf("#%d: unexpected ja3 value", i) + } + } +} diff --git a/bfe_tls/handshake_server.go b/bfe_tls/handshake_server.go index ef0730767..4118c4489 100644 --- a/bfe_tls/handshake_server.go +++ b/bfe_tls/handshake_server.go @@ -21,6 +21,7 @@ package bfe_tls import ( "crypto" "crypto/ecdsa" + "crypto/md5" "crypto/rsa" "crypto/subtle" "crypto/x509" @@ -73,6 +74,10 @@ func (c *Conn) serverHandshake() error { return err } + // Record JA3 fingerpint for TLS client + c.ja3Raw = hs.clientHello.JA3String() + c.ja3Hash = fmt.Sprintf("%x", md5.Sum([]byte(c.ja3Raw))) + // For an overview of TLS handshaking, see https://tools.ietf.org/html/rfc5246#section-7.3 if isResume { state.TlsHandshakeResumeAll.Inc(1) diff --git a/bfe_util/pipe/pipe.go b/bfe_util/pipe/pipe.go index 661ff1f7a..56919fa36 100644 --- a/bfe_util/pipe/pipe.go +++ b/bfe_util/pipe/pipe.go @@ -119,6 +119,12 @@ func (p *Pipe) closeWithError(dst *error, err error, fn func()) { } defer p.c.Signal() if *dst != nil { + // Note: Here we do not consider the existing io.EOF(i.e. *dst) as a real error + // and replace it if necessary. The error handling policy allows us to release + // underlying resource(eg. PipeBuffer) as soon as possible. + if *dst == io.EOF { + *dst = err + } // Already been done. return } diff --git a/docs/en_us/DOWNLOAD.md b/docs/en_us/DOWNLOAD.md index b73e7fdb7..fdcc7c93f 100644 --- a/docs/en_us/DOWNLOAD.md +++ b/docs/en_us/DOWNLOAD.md @@ -1,5 +1,17 @@ We provide precompiled binaries for bfe components. [Download the latest release](https://github.com/bfenetworks/bfe/releases) of BFE for your platform. +## bfe v1.0.0 + +* 2021-01-15 [Release notes](https://github.com/bfenetworks/bfe/releases/tag/v1.0.0) + +| File name | OS | Arch | Size | SHA256 Checksum | +| --------- | -- | ---- | ---- | --------------- | +| [bfe_1.0.0_darwin_amd64.tar.gz](https://github.com/bfenetworks/bfe/releases/download/v1.0.0/bfe_1.0.0_darwin_amd64.tar.gz) | darwin | amd64 | 7.03 MB | c0d13440d89ab97f52c61610d1b10dec6dcfb47b468a66078d1dd60f0541ec9e | +| [bfe_1.0.0_linux_arm64.tar.gz](https://github.com/bfenetworks/bfe/releases/download/v1.0.0/bfe_1.0.0_linux_arm64.tar.gz) | linux | arm64 | 5.63 MB | 47a3730ac90c4700c557d6c5903361c557e169102256bac870cede4eb90ff829 | +| [bfe_1.0.0_linux_amd64.tar.gz](https://github.com/bfenetworks/bfe/releases/download/v1.0.0/bfe_1.0.0_linux_amd64.tar.gz) | linux | amd64 | 6.18 MB | 5ec46c26827d554ba4c76f7f5e12b6b6afb68a9333213065802fa425fb81cbd1 | +| [bfe_1.0.0_windows_amd64.tar.gz](https://github.com/bfenetworks/bfe/releases/download/v1.0.0/bfe_1.0.0_windows_amd64.tar.gz) | windows | amd64 | 6.15 MB | 95ba788d0335ac536036c77e39249ce1629b2d159c942293077fd57ddc487f29 | + + ## bfe v0.10.0 * 2020-05-25 [Release notes](https://github.com/bfenetworks/bfe/releases/tag/v0.10.0) diff --git a/docs/en_us/condition/condition_primitive_index.md b/docs/en_us/condition/condition_primitive_index.md index 6fbe76a5e..9d1179e81 100644 --- a/docs/en_us/condition/condition_primitive_index.md +++ b/docs/en_us/condition/condition_primitive_index.md @@ -21,6 +21,7 @@ * req_proto_secure() * req_tag_match(tagName, tagValue) * req_path_in(path_list, case_insensitive) + * req_path_contain(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) diff --git a/docs/en_us/condition/request/uri.md b/docs/en_us/condition/request/uri.md index 1b302dd1f..82206ab6f 100644 --- a/docs/en_us/condition/request/uri.md +++ b/docs/en_us/condition/request/uri.md @@ -35,6 +35,22 @@ req_host_in("www.bfe-networks.com | bfe-networks.com") req_path_in("/api/search|/api/list", true) ``` +## req_path_contain(path_list, case_insensitive) +* Description: Judge if request path contains configured patterns + +* Parameters + +| Parameter | Descrption | +| --------- | ---------- | +| path_list | String
path's substring list which are concatenated with | | +| case_insensitive | Boolean
case insensitive | + +* Example + +```go +req_path_contain("search|analytics", true) +``` + ## req_path_prefix_in(prefix_list, case_insensitive) * Description: Judge if request path prefix matches configured patterns diff --git a/docs/en_us/configuration/server_data_conf/cluster_conf.data.md b/docs/en_us/configuration/server_data_conf/cluster_conf.data.md index a293045f2..e7e02a1ad 100644 --- a/docs/en_us/configuration/server_data_conf/cluster_conf.data.md +++ b/docs/en_us/configuration/server_data_conf/cluster_conf.data.md @@ -23,7 +23,9 @@ BackendConf is config for backend. | TimeoutConnSrv | Int
Timeout for connect backend, in ms | | TimeoutResponseHeader | Int
Timeout for read response header, in ms | | MaxIdleConnsPerHost | Int
Max idle conns to each backend | +| MaxConnsPerHost | Int
Max number of concurrent conns to each backend | | RetryLevel | Int
Retry level if request fail | +| BackendConf.OutlierDetectionHttpCode | String
Http status code that represent error status of backend | | FCGIConf | Object
Conf for FastCGI Protocol | | FCGIConf.Root | String
the root folder to the site | | FCGIConf.EnvVars | Map[string]string
extra environment variable | @@ -78,7 +80,9 @@ ClusterBasic is basic config for cluster. "TimeoutConnSrv": 2000, "TimeoutResponseHeader": 50000, "MaxIdleConnsPerHost": 0, - "RetryLevel": 0 + "MaxConnsPerHost": 0, + "RetryLevel": 0, + "OutlierDetectionHttpCode": "5xx|400" }, "CheckConf": { "Schem": "http", diff --git a/docs/en_us/modules/mod_header/mod_header.md b/docs/en_us/modules/mod_header/mod_header.md index 34c258743..5bed26f2e 100644 --- a/docs/en_us/modules/mod_header/mod_header.md +++ b/docs/en_us/modules/mod_header/mod_header.md @@ -108,6 +108,8 @@ See the **Example** above. | %bfe_ssl_resume | Whether the TLS/SSL session is resumed with session id or session ticket | | %bfe_ssl_cipher | TLS/SSL cipher suite | | %bfe_ssl_version | TLS/SSL version | +| %bfe_ssl_ja3_raw | JA3 fingerprint string for TLS/SSL client | +| %bfe_ssl_ja3_hash | JA3 fingerprint hash for TLS/SSL client | | %bfe_protocol | Application level protocol | | %client_cert_serial_number | Serial number of client certificate | | %client_cert_subject_title | Subject title of client certificate | diff --git a/docs/mkdocs_en.yml b/docs/mkdocs_en.yml index 0abc0ab86..1787b46a1 100644 --- a/docs/mkdocs_en.yml +++ b/docs/mkdocs_en.yml @@ -69,12 +69,14 @@ nav: - 'Request redirect': 'example/redirect.md' - 'Request rewrite': 'example/rewrite.md' - 'TLS mutual authentication': 'example/client_auth.md' + - 'FastCGI': 'example/fastcgi.md' - 'Installation': - 'Overview': 'installation/install.md' - 'Install from source': 'installation/install_from_source.md' - 'Install using binaries': 'installation/install_using_binaries.md' - 'Install using go': 'installation/install_using_go.md' - 'Install using snap': 'installation/install_using_snap.md' + - 'Install using docker': 'installation/install_using_docker.md' - 'Configuration': - 'Overview': 'configuration/config.md' - 'Core': 'configuration/bfe.conf.md' diff --git a/docs/mkdocs_zh.yml b/docs/mkdocs_zh.yml index 43304ee9b..0f046c23d 100644 --- a/docs/mkdocs_zh.yml +++ b/docs/mkdocs_zh.yml @@ -69,12 +69,14 @@ nav: - '重定向': 'example/redirect.md' - '重写': 'example/rewrite.md' - 'TLS客户端认证': 'example/client_auth.md' + - 'FastCGI': 'example/fastcgi.md' - '安装说明': - '安装概述': 'installation/install.md' - '源码编译安装': 'installation/install_from_source.md' - '二进制文件下载安装': 'installation/install_using_binaries.md' - 'go方式安装': 'installation/install_using_go.md' - 'snap方式安装': 'installation/install_using_snap.md' + - 'docker方式安装': 'installation/install_using_docker.md' - '配置说明': - '配置概述': 'configuration/config.md' - '核心配置': 'configuration/bfe.conf.md' diff --git a/docs/zh_cn/DOWNLOAD.md b/docs/zh_cn/DOWNLOAD.md index f6d2e7a67..cab6a53e7 100644 --- a/docs/zh_cn/DOWNLOAD.md +++ b/docs/zh_cn/DOWNLOAD.md @@ -1,5 +1,16 @@ BFE提供预编译二进制文件供下载。也可在GitHub下载各平台[最新版本BFE](https://github.com/bfenetworks/bfe/releases)。 +## bfe v1.0.0 + +* 2021-01-15 [发布说明](https://github.com/bfenetworks/bfe/releases/tag/v1.0.0) + +| 文件名 | 操作系统 | 平台 | 大小 | SHA256检验和 | +| --------- | -- | ---- | ---- | --------------- | +| [bfe_1.0.0_darwin_amd64.tar.gz](https://github.com/bfenetworks/bfe/releases/download/v1.0.0/bfe_1.0.0_darwin_amd64.tar.gz) | darwin | amd64 | 7.03 MB | c0d13440d89ab97f52c61610d1b10dec6dcfb47b468a66078d1dd60f0541ec9e | +| [bfe_1.0.0_linux_arm64.tar.gz](https://github.com/bfenetworks/bfe/releases/download/v1.0.0/bfe_1.0.0_linux_arm64.tar.gz) | linux | arm64 | 5.63 MB | 47a3730ac90c4700c557d6c5903361c557e169102256bac870cede4eb90ff829 | +| [bfe_1.0.0_linux_amd64.tar.gz](https://github.com/bfenetworks/bfe/releases/download/v1.0.0/bfe_1.0.0_linux_amd64.tar.gz) | linux | amd64 | 6.18 MB | 5ec46c26827d554ba4c76f7f5e12b6b6afb68a9333213065802fa425fb81cbd1 | +| [bfe_1.0.0_windows_amd64.tar.gz](https://github.com/bfenetworks/bfe/releases/download/v1.0.0/bfe_1.0.0_windows_amd64.tar.gz) | windows | amd64 | 6.15 MB | 95ba788d0335ac536036c77e39249ce1629b2d159c942293077fd57ddc487f29 | + ## bfe v0.10.0 * 2020-05-25 [发布说明](https://github.com/bfenetworks/bfe/releases/tag/v0.10.0) diff --git a/docs/zh_cn/condition/condition_primitive_index.md b/docs/zh_cn/condition/condition_primitive_index.md index fe0de0183..751061ddf 100644 --- a/docs/zh_cn/condition/condition_primitive_index.md +++ b/docs/zh_cn/condition/condition_primitive_index.md @@ -21,6 +21,7 @@ * req_proto_secure() * req_tag_match(tagName, tagValue) * req_path_in(path_list, case_insensitive) + * req_path_contain(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) diff --git a/docs/zh_cn/condition/request/uri.md b/docs/zh_cn/condition/request/uri.md index 2496cf68c..8d1f42c19 100644 --- a/docs/zh_cn/condition/request/uri.md +++ b/docs/zh_cn/condition/request/uri.md @@ -28,6 +28,21 @@ req_host_in("www.bfe-networks.com|bfe-networks.com") req_path_in("/api/search|/api/list", true) ``` +## req_path_contain(path_list, case_insensitive) +* 含义: 判断http的path是否包含path_list中的子串 + +* 参数 + +| 参数 | 描述 | +| -------- | ---------------------- | +| path_list | String
path子串列表,多个列表之间使用‘|’连接| +| case_insensitive | Boolean
是否忽略大小写 | + +* 示例 +```go +req_path_contain("search", true) +``` + ## req_path_prefix_in(prefix_list, case_insensitive) * 含义: 判断http的path是否前缀匹配prefix_list之一 diff --git a/docs/zh_cn/configuration/server_data_conf/cluster_conf.data.md b/docs/zh_cn/configuration/server_data_conf/cluster_conf.data.md index 30eaa4866..b88856ff6 100644 --- a/docs/zh_cn/configuration/server_data_conf/cluster_conf.data.md +++ b/docs/zh_cn/configuration/server_data_conf/cluster_conf.data.md @@ -27,7 +27,9 @@ cluster_conf.data为集群转发配置文件。 | BackendConf.TimeoutConnSrv | Integer
连接后端的超时时间,单位是毫秒
默认值2 | | BackendConf.TimeoutResponseHeader | Integer
从后端读响应头的超时时间,单位是毫秒
默认值60 | | BackendConf.MaxIdleConnsPerHost | Integer
BFE实例与每个后端的最大空闲长连接数
默认值2 | +| BackendConf.MaxConnsPerHost | Integer
BFE实例与每个后端的最大长连接数,0代表无限制
默认值0 | | BackendConf.RetryLevel | Integer
请求重试级别。0:连接后端失败时,进行重试;1:连接后端失败、转发GET请求失败时均进行重试
默认值0 | +| BackendConf.OutlierDetectionHttpCode | String
后端响应状态码检查,""代表不开启检查,"500"表示后端返回500则认为后端失败,失败计数加一
状态码支持"dxx"格式,例如"5xx";多个状态码之间使用'|'连接
默认值"",不开启后端响应状态码错误检查 | | BackendConf.FCGIConf | Object
FastCGI 协议的配置 | | BackendConf.FCGIConf.Root | String
网站的Root文件夹位置 | | BackendConf.FCGIConf.EnvVars | Map[string]string
拓展的环境变量 | @@ -59,11 +61,11 @@ cluster_conf.data为集群转发配置文件。 #### 集群基础配置 -| 配置项 | 描述 | -| ----------------------------------- | ------------------------------------ | -| ClusterBasic.TimeoutReadClient | Integer
读用户请求wody的超时时间,单位为毫秒
默认值30 | -| ClusterBasic.TimeoutWriteClient | Integer
写响应的超时时间,单位为毫秒
默认值60 | -| ClusterBasic.TimeoutReadClientAgain | Integer
连接闲置超时时间,单位为毫秒
默认值60 | +| 配置项 | 描述 | +| ----------------------------------- | ----------------------------------------------------------- | +| ClusterBasic.TimeoutReadClient | Integer
读用户请求body的超时时间,单位为毫秒
默认值30 | +| ClusterBasic.TimeoutWriteClient | Integer
写响应的超时时间,单位为毫秒
默认值60 | +| ClusterBasic.TimeoutReadClientAgain | Integer
连接闲置超时时间,单位为毫秒
默认值60 | ## 配置示例 @@ -76,7 +78,8 @@ cluster_conf.data为集群转发配置文件。 "TimeoutConnSrv": 2000, "TimeoutResponseHeader": 50000, "MaxIdleConnsPerHost": 0, - "RetryLevel": 0 + "RetryLevel": 0, + "OutlierDetectionHttpCode": "5xx|403" }, "CheckConf": { "Schem": "http", @@ -107,6 +110,7 @@ cluster_conf.data为集群转发配置文件。 "TimeoutConnSrv": 2000, "TimeoutResponseHeader": 50000, "MaxIdleConnsPerHost": 0, + "MaxConnsPerHost": 0, "RetryLevel": 0, "FCGIConf": { "Root": "/home/work", diff --git a/docs/zh_cn/development/local_dev_guide.md b/docs/zh_cn/development/local_dev_guide.md index 35b09b2da..4337e0645 100644 --- a/docs/zh_cn/development/local_dev_guide.md +++ b/docs/zh_cn/development/local_dev_guide.md @@ -116,7 +116,7 @@ clang-formater.......................................(no files to check)Skipped # 触发develop分支的CI单测 $ git commit -m "test=develop" -# 触发release/1.1分支的CI单侧 +# 触发release/1.1分支的CI单测 $ git commit -m "test=release/1.1" ``` diff --git a/docs/zh_cn/modules/mod_header/mod_header.md b/docs/zh_cn/modules/mod_header/mod_header.md index c365f3217..b262954af 100644 --- a/docs/zh_cn/modules/mod_header/mod_header.md +++ b/docs/zh_cn/modules/mod_header/mod_header.md @@ -102,6 +102,8 @@ BFE支持如下一系列变量并在处理请求阶段求值。关于变量的 | %bfe_ssl_resume | 是否TLS/SSL会话复用 | | %bfe_ssl_cipher | TLS/SSL加密套件 | | %bfe_ssl_version | TLS/SSL协议版本 | +| %bfe_ssl_ja3_raw | TLS/SSL客户端JA3算法指纹数据 | +| %bfe_ssl_ja3_hash | TLS/SSL客户端JA3算法指纹哈希值 | | %bfe_protocol | 访问协议 | | %client_cert_serial_number | 客户端证书序列号 | | %client_cert_subject_title | 客户端证书Subject title | diff --git a/go.mod b/go.mod index 91e651001..a5612e485 100644 --- a/go.mod +++ b/go.mod @@ -29,9 +29,9 @@ require ( go.elastic.co/apm/module/apmot v1.7.2 go.uber.org/atomic v1.6.0 // indirect golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 - golang.org/x/net v0.0.0-20200625001655-4c5254603344 - golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd - golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7 // indirect + golang.org/x/net v0.0.0-20201021035429-f5854403a974 + golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 + golang.org/x/tools v0.1.0 // indirect gopkg.in/gcfg.v1 v1.2.3 gopkg.in/square/go-jose.v2 v2.4.1 gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/go.sum b/go.sum index 862b3a750..171873994 100644 --- a/go.sum +++ b/go.sum @@ -124,7 +124,7 @@ github.com/uber/jaeger-client-go v2.22.1+incompatible h1:NHcubEkVbahf9t3p75TOCR8 github.com/uber/jaeger-client-go v2.22.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.2.0+incompatible h1:MxZXOiR2JuoANZ3J6DE/U0kSFv/eJ/GfSYVCjK7dyaw= github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zmap/go-iptree v0.0.0-20170831022036-1948b1097e25 h1:LRoXAcKX48QV4LV23W5ZtsG/MbJOgNUNvWiXwM0iLWw= github.com/zmap/go-iptree v0.0.0-20170831022036-1948b1097e25/go.mod h1:qOasALtPByO1Jk6LhgpNv6htPMK2QJfiGorUk57nO/U= go.elastic.co/apm v1.7.2 h1:0nwzVIPp4PDBXSYYtN19+1W5V+sj+C25UjqxDVoKcA8= @@ -155,15 +155,15 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -171,26 +171,28 @@ golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191025021431-6c3a3bfe00ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191018212557-ed542cd5b28a h1:UuQ+70Pi/ZdWHuP4v457pkXeOynTdgd/4enxeIO/98k= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7 h1:LHW24ah7B+uV/OePwNP0p/t889F3QSyLvY8Sg/bK0SY= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=