diff options
author | Valery Piashchynski <[email protected]> | 2020-12-26 00:47:21 +0300 |
---|---|---|
committer | GitHub <[email protected]> | 2020-12-26 00:47:21 +0300 |
commit | 566d7f4c95eb5dedcb2da5afcda4bbea8eba077f (patch) | |
tree | 0007a6b8c8ac9e7d31b8a5f3f7f27669c860d261 | |
parent | 1bc3db2ea9b95edd0101676d7bfd75df3782c3bd (diff) | |
parent | 7a0dee1a416705c621edbf50e1f43fb39845348f (diff) |
Merge pull request #463 from spiral/experiment/core_pluginsv2.0.0-beta1
[RR2] Plugins
144 files changed, 15465 insertions, 286 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 22a2440e..b624451e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -80,6 +80,9 @@ jobs: go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/pool.txt -covermode=atomic ./pkg/pool go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/socket.txt -covermode=atomic ./pkg/socket go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/worker.txt -covermode=atomic ./pkg/worker + go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/http.txt -covermode=atomic ./tests/plugins/http + go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/informer.txt -covermode=atomic ./tests/plugins/informer + go test -v -race -cover -tags=debug -coverprofile=./coverage-ci/server.txt -covermode=atomic ./tests/plugins/server cat ./coverage-ci/*.txt > ./coverage-ci/summary.txt - uses: codecov/codecov-action@v1 # Docs: <https://github.com/codecov/codecov-action> @@ -24,12 +24,26 @@ uninstall: ## Uninstall locally installed RR rm -f /usr/local/bin/rr test: ## Run application tests - go clean -testcache + #go clean -testcache go test -v -race -cover -tags=debug -covermode=atomic ./util go test -v -race -cover -tags=debug -covermode=atomic ./pkg/pipe go test -v -race -cover -tags=debug -covermode=atomic ./pkg/pool go test -v -race -cover -tags=debug -covermode=atomic ./pkg/socket go test -v -race -cover -tags=debug -covermode=atomic ./pkg/worker + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/http + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/informer + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/reload + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/server + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/checker + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/config + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/gzip + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/headers + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/logger + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/metrics + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/redis + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/resetter + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/rpc + go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/static lint: ## Run application linters go fmt ./... diff --git a/cmd/cli/root.go b/cmd/cli/root.go index febe410b..eb15f565 100644 --- a/cmd/cli/root.go +++ b/cmd/cli/root.go @@ -8,9 +8,9 @@ import ( "github.com/spiral/errors" goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" - rpcPlugin "github.com/spiral/roadrunner-plugins/rpc" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" - "github.com/spiral/roadrunner-plugins/config" + "github.com/spiral/roadrunner/v2/plugins/config" "github.com/spf13/cobra" "github.com/spiral/endure" diff --git a/cmd/cli/workers.go b/cmd/cli/workers.go index d34de40a..03639aa4 100644 --- a/cmd/cli/workers.go +++ b/cmd/cli/workers.go @@ -13,7 +13,7 @@ import ( "github.com/fatih/color" "github.com/spf13/cobra" "github.com/spiral/errors" - "github.com/spiral/roadrunner-plugins/informer" + "github.com/spiral/roadrunner/v2/plugins/informer" "github.com/spiral/roadrunner/v2/tools" ) @@ -22,6 +22,7 @@ var ( ) const InformerList string = "informer.List" +const InformerWorkers string = "informer.Workers" func init() { workersCommand := &cobra.Command{ @@ -42,7 +43,7 @@ func init() { } func workersHandler(cmd *cobra.Command, args []string) error { - const op = errors.Op("workers handler") + const op = errors.Op("workers cobra handler") // get RPC client client, err := RPCClient() if err != nil { @@ -94,25 +95,16 @@ func workersHandler(cmd *cobra.Command, args []string) error { } func showWorkers(plugins []string, client *rpc.Client) error { + const op = errors.Op("show workers") for _, plugin := range plugins { list := &informer.WorkerList{} - err := client.Call("informer.Workers", plugin, &list) + err := client.Call(InformerWorkers, plugin, &list) if err != nil { - return err - } - - // it's a golang :) - ps := make([]tools.ProcessState, len(list.Workers)) - for i := 0; i < len(list.Workers); i++ { - ps[i].Created = list.Workers[i].Created - ps[i].NumJobs = list.Workers[i].NumJobs - ps[i].MemoryUsage = list.Workers[i].MemoryUsage - ps[i].Pid = list.Workers[i].Pid - ps[i].Status = list.Workers[i].Status + return errors.E(op, err) } fmt.Printf("Workers of [%s]:\n", color.HiYellowString(plugin)) - tools.WorkerTable(os.Stdout, ps).Render() + tools.WorkerTable(os.Stdout, list.Workers).Render() } return nil } diff --git a/cmd/main.go b/cmd/main.go index 8151d4fe..4e7fc099 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,16 +4,17 @@ import ( "log" "github.com/spiral/endure" - "github.com/spiral/roadrunner-plugins/http" - "github.com/spiral/roadrunner-plugins/informer" - "github.com/spiral/roadrunner-plugins/logger" - "github.com/spiral/roadrunner-plugins/metrics" - "github.com/spiral/roadrunner-plugins/redis" - "github.com/spiral/roadrunner-plugins/reload" - "github.com/spiral/roadrunner-plugins/resetter" - "github.com/spiral/roadrunner-plugins/rpc" - "github.com/spiral/roadrunner-plugins/server" + "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/informer" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/spiral/roadrunner/v2/cmd/cli" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/metrics" + "github.com/spiral/roadrunner/v2/plugins/redis" + "github.com/spiral/roadrunner/v2/plugins/reload" + "github.com/spiral/roadrunner/v2/plugins/resetter" + "github.com/spiral/roadrunner/v2/plugins/rpc" ) func main() { @@ -3,30 +3,34 @@ module github.com/spiral/roadrunner/v2 go 1.15 require ( + github.com/NYTimes/gziphandler v1.1.1 + github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect + github.com/alicebob/miniredis/v2 v2.14.1 github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129 github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 github.com/fatih/color v1.10.0 + github.com/go-ole/go-ole v1.2.4 // indirect + github.com/go-redis/redis/v8 v8.4.4 + github.com/gofiber/fiber/v2 v2.3.0 + github.com/golang/mock v1.4.4 + github.com/hashicorp/go-multierror v1.1.0 github.com/json-iterator/go v1.1.10 github.com/mattn/go-runewidth v0.0.9 github.com/olekukonko/tablewriter v0.0.4 + github.com/prometheus/client_golang v0.9.3 github.com/shirou/gopsutil v3.20.11+incompatible github.com/spf13/cobra v1.1.1 + github.com/spf13/viper v1.7.0 github.com/spiral/endure v1.0.0-beta20 github.com/spiral/errors v1.0.6 github.com/spiral/goridge/v3 v3.0.0-beta8 - github.com/spiral/roadrunner-plugins/config v1.0.1 - github.com/spiral/roadrunner-plugins/http v1.0.5 - github.com/spiral/roadrunner-plugins/informer v1.0.5 - github.com/spiral/roadrunner-plugins/logger v1.0.2 - github.com/spiral/roadrunner-plugins/metrics v1.0.1 - github.com/spiral/roadrunner-plugins/redis v1.0.1 - github.com/spiral/roadrunner-plugins/reload v1.0.1 - github.com/spiral/roadrunner-plugins/resetter v1.0.1 - github.com/spiral/roadrunner-plugins/rpc v1.0.2 - github.com/spiral/roadrunner-plugins/server v1.0.6 github.com/stretchr/testify v1.6.1 github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a github.com/vbauerster/mpb/v5 v5.4.0 + github.com/yookoala/gofast v0.4.0 go.uber.org/multierr v1.6.0 + go.uber.org/zap v1.16.0 + golang.org/x/net v0.0.0-20201216054612-986b41b23924 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a + golang.org/x/sys v0.0.0-20201221093633-bc327ba9c2f0 ) @@ -14,52 +14,35 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= -github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis/v2 v2.14.1 h1:GjlbSeoJ24bzdLRs13HoMEeaRZx9kg5nHoRW7QV/nCs= github.com/alicebob/miniredis/v2 v2.14.1/go.mod h1:uS970Sw5Gs9/iK3yBg0l9Uj9s25wXxSpQUE9EaJ/Blg= github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= -github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= -github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129 h1:gfAMKE626QEuKG3si0pdTRcr/YEbBoxY+3GOH3gWvl4= github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U= -github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= -github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= -github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc= github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= @@ -67,21 +50,13 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -91,19 +66,10 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= -github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= @@ -112,28 +78,20 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-ini/ini v1.38.1/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/go-redis/redis/v8 v8.4.4 h1:fGqgxCTR1sydaKI00oQf3OmkU/DIe/I/fYXvGklCIuc= github.com/go-redis/redis/v8 v8.4.4/go.mod h1:nA0bQuF0i5JFx4Ta9RZxGKXFrQ8cRWntra97f0196iY= github.com/go-restit/lzjson v0.0.0-20161206095556-efe3c53acc68/go.mod h1:7vXSKQt83WmbPeyVjCfNT9YDJ5BUFmcwFsEjI9SCvYM= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gofiber/fiber/v2 v2.3.0 h1:82ufvLne0cxzdkDOeLkUmteA+z1uve9JQ/ZFsMOnkzc= github.com/gofiber/fiber/v2 v2.3.0/go.mod h1:f8BRRIMjMdRyt2qmJ/0Sea3j3rwwfufPrh9WNBRiVZ0= -github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= @@ -149,10 +107,8 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -166,26 +122,17 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= @@ -200,7 +147,6 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -211,16 +157,10 @@ github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0m github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -228,23 +168,18 @@ github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVY github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.10.7 h1:7rix8v8GpI3ZBb0nSozFRgbtXKv+hOe+qfEpZqybrAg= github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= -github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= @@ -253,10 +188,8 @@ github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -279,107 +212,51 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= -github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= -github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= -github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= -github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.4 h1:NiTx7EEvBzu9sFOD1zORteLSt3o8gnlvZZwSE9TnY9U= github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= -github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= -github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= -github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= -github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= -github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= -github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.9.0 h1:Rrch9mh17XcxvEu9D9DEpb4isxjGBtcevQjKvxPRQIU= -github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM= -github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= -github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shirou/gopsutil v3.20.11+incompatible h1:LJr4ZQK4mPpIV5gOa4jCOKOGb4ty4DZO54I4FGqIpto= github.com/shirou/gopsutil v3.20.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -387,25 +264,21 @@ github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:X github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= -github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spiral/endure v1.0.0-beta20 h1:QD3EJ6CRLgeo/6trfnlUcQhH3vrK8Hvf9ceDpde+yss= github.com/spiral/endure v1.0.0-beta20/go.mod h1:qCU2/4gAItVESzUK0yPExmUTlTcpRLqJUgcV+nqxn+o= github.com/spiral/errors v1.0.4/go.mod h1:SwMSZVdZkkJVgXNNafccqOaxWg0XPzVU/dEdUEInE0o= @@ -415,66 +288,6 @@ github.com/spiral/errors v1.0.6 h1:berk5ShEILSw6DplUVv9Ea1wGdk2WlVKQpuvDngll0U= github.com/spiral/errors v1.0.6/go.mod h1:SwMSZVdZkkJVgXNNafccqOaxWg0XPzVU/dEdUEInE0o= github.com/spiral/goridge/v3 v3.0.0-beta8 h1:x8uXCdhY49U1LEvmehnTaD2El6J9ZHAefRdh/QIZ6A4= github.com/spiral/goridge/v3 v3.0.0-beta8/go.mod h1:XFQGc42KNzo/hPIXPki7mEkFTf9v/T7qFk/TYJjMtzE= -github.com/spiral/roadrunner-plugins/checker v1.0.1 h1:UVMX9zUF0R3ScRFUQg89e5+CLn3I2e5uAKsD4A52iDU= -github.com/spiral/roadrunner-plugins/checker v1.0.1/go.mod h1:/gnVybeKye5uq/8hgjI7YDoK/ntPd0ej6tWkxlYdiz8= -github.com/spiral/roadrunner-plugins/checker v1.0.3 h1:fnWX2PBXlNhYp8UERgFx/eMM1Qf4pDJCeUY4NoiWwbo= -github.com/spiral/roadrunner-plugins/checker v1.0.3/go.mod h1:ZLMg9NJsqUuCG7akU2V6JAFd/9ZLjawzvoJRYZxr5b0= -github.com/spiral/roadrunner-plugins/config v1.0.0 h1:RPYB8Ha/UeuBGRwtcqNb0uU8R5qe4XpUXVmv/lGSUdQ= -github.com/spiral/roadrunner-plugins/config v1.0.0/go.mod h1:XcVJLFDUlYPvZ3kVzssmP4fJbEzUvVJf534+eZaotAo= -github.com/spiral/roadrunner-plugins/config v1.0.1 h1:6n9mFqvy+Fk3LaOWfNSA7lb5o2vJOnGJivNFP7Fg8QA= -github.com/spiral/roadrunner-plugins/config v1.0.1/go.mod h1:MQGXiAP8Sr6/LjFVcr+AeUElNMKJXnwVEKXn1I5ztXk= -github.com/spiral/roadrunner-plugins/http v1.0.0 h1:8WGAuZOrkYZQWo6n13ip+ZtzhKugZZ+b5W+ivVr7FjI= -github.com/spiral/roadrunner-plugins/http v1.0.0/go.mod h1:37ReUuAKJDtXH3GjMjRH5q3plBXq5r5lUfltRTVZzDE= -github.com/spiral/roadrunner-plugins/http v1.0.2 h1:BRmdbx0DYlgYTkvmd1IDZJewC1y30GqBCKRjbTn3OLc= -github.com/spiral/roadrunner-plugins/http v1.0.2/go.mod h1:JRgVSgJRh6wDmVs1rFJHQ9PM0xtCpzX9vBtVDItXZ/E= -github.com/spiral/roadrunner-plugins/http v1.0.5 h1:6uHbc2OwhExBE9Psfwc2WDn6KeIXzLjiCCwO9yBH9JU= -github.com/spiral/roadrunner-plugins/http v1.0.5/go.mod h1:9ZlPZJ1A7M3aBQ/tJYjmuXYPox3smFP8ccsf+EBHd9c= -github.com/spiral/roadrunner-plugins/informer v1.0.3 h1:6DXs8IRDjBvAr2mGKXVlhx1KLLfVqpXgvP2va79kR1s= -github.com/spiral/roadrunner-plugins/informer v1.0.3/go.mod h1:OPEJNADBbNQyx0/KuXQbY3Mqemo30vZh6duf6YpRf7M= -github.com/spiral/roadrunner-plugins/informer v1.0.4 h1:J2DXrQkRRyYDGw/6TIo7qF74Zn3AOHQe+9g0Hj5BwR4= -github.com/spiral/roadrunner-plugins/informer v1.0.4/go.mod h1:4qvFbVw0ESwE5RJQRiP939QN3SJ8l6aDnrQhL09f5lQ= -github.com/spiral/roadrunner-plugins/informer v1.0.5 h1:L27oLcHONKL3L4X3uR/6ZSZF9iz27urT+uIsWGOM4e4= -github.com/spiral/roadrunner-plugins/informer v1.0.5/go.mod h1:BcJewp6L0cEvX8uaYwemsWa7roVCLjQGEsAeBsRvmN0= -github.com/spiral/roadrunner-plugins/logger v1.0.1 h1:cc/pRE+8bUQgLiF05G7f948UpWtrZmJk/axTh1ywrks= -github.com/spiral/roadrunner-plugins/logger v1.0.1/go.mod h1:/oX1P56+/joLaAPwZaiRqPVVzM36701kdYqOHu+U3EM= -github.com/spiral/roadrunner-plugins/logger v1.0.2 h1:4dBDKMUYdwE3jQ6ld1cR7E+ApRE30cDFudHSwDpgmx8= -github.com/spiral/roadrunner-plugins/logger v1.0.2/go.mod h1:TGafnqSIQak7e+7mkrdatl379VVlK0kZE874ZC5exPk= -github.com/spiral/roadrunner-plugins/metrics v1.0.0 h1:PEK+AlcMv6yrH/vag7yQm/5sV7gMoWsNn9KWOsDPw/0= -github.com/spiral/roadrunner-plugins/metrics v1.0.0/go.mod h1:zLd9TdARJSwo6tQjTb0auD7pxN2ODXnsy4evInCMp9o= -github.com/spiral/roadrunner-plugins/metrics v1.0.1 h1:Ifbioo0BgsMk83sQfv36hpDwqC5iL4SFaEsLKeTRdXs= -github.com/spiral/roadrunner-plugins/metrics v1.0.1/go.mod h1:1Ixk+pmFcZQEigB/km6ABkCwhTl4ph3YWDZMN/g4YHI= -github.com/spiral/roadrunner-plugins/redis v1.0.0 h1:/J4JpbNLd0G23BD9VaHsXKR8OHyAnrRQf7rRZMFx57U= -github.com/spiral/roadrunner-plugins/redis v1.0.0/go.mod h1:VzXRQcNaNONaUxyCW9t9eC+p8UxkXx2kwGeVw3s7zR0= -github.com/spiral/roadrunner-plugins/redis v1.0.1 h1:RGVbCsADfoEXnIeIJvBRsxoMxVh7Gx5p/YrVnYPTyd8= -github.com/spiral/roadrunner-plugins/redis v1.0.1/go.mod h1:r9FiOvBzwiWEWNMuQf45j7c8X01nrR3bpS1BxZl0bfY= -github.com/spiral/roadrunner-plugins/reload v1.0.0 h1:cBevNueyJjG4ykTjd9C4WSV8xNJq3UuXh3ympp3GU0I= -github.com/spiral/roadrunner-plugins/reload v1.0.0/go.mod h1:IEDqSrV0zbnvCNXigLtp0EoGVsHc34merlU2gokiuDs= -github.com/spiral/roadrunner-plugins/reload v1.0.1 h1:K7uvITE2B9bZyYWXk2b82erCeWjFBdjcCdfcXDVBjlU= -github.com/spiral/roadrunner-plugins/reload v1.0.1/go.mod h1:IEDqSrV0zbnvCNXigLtp0EoGVsHc34merlU2gokiuDs= -github.com/spiral/roadrunner-plugins/resetter v1.0.0 h1:Nx4mIzeoH/IcUfY4LM9xhY0yAXXYkAIAqWCRdMcsr68= -github.com/spiral/roadrunner-plugins/resetter v1.0.0/go.mod h1:DLFifJk1n3PWViXkT5+qAmzeRcPTowDRSbRqotf+WlE= -github.com/spiral/roadrunner-plugins/resetter v1.0.1 h1:U9nrxhz8graviJ8LjClFCA3+H346Obhr/xsoCwTlAsQ= -github.com/spiral/roadrunner-plugins/resetter v1.0.1/go.mod h1:vz7RFxWu0P79OGoGN0w2x63RIqszZCCGjEvMwtHHjDE= -github.com/spiral/roadrunner-plugins/rpc v1.0.0 h1:cC17yCNqQUNtedKefdeT2P6z7q52L0QakxS4qwB7n+g= -github.com/spiral/roadrunner-plugins/rpc v1.0.0/go.mod h1:p+ClRf1ibW+xvekf+nGQtvipyrtPJP6WZ/J/DSp+Qck= -github.com/spiral/roadrunner-plugins/rpc v1.0.1 h1:rbMJNM+4opzoB7ZXi9dbntfix9+cTuQdUMcDIWsaC7k= -github.com/spiral/roadrunner-plugins/rpc v1.0.1/go.mod h1:iz1a3poEKCppX1fYfneMWOxWBfToYSi2TE1rYD0yBls= -github.com/spiral/roadrunner-plugins/rpc v1.0.2 h1:1ICsIasYP8W6cEAMEzU6H+VqGh3AFaCWyy4w8VfRYg4= -github.com/spiral/roadrunner-plugins/rpc v1.0.2/go.mod h1:DAioNb54q5WIOOzdo+2IJB9IysM2WI7f9nzQcNBp2Xk= -github.com/spiral/roadrunner-plugins/server v1.0.1 h1:yzCw5rgz+KHpJb56QIID3gWH/IBbNtHZD2zvkck1bYc= -github.com/spiral/roadrunner-plugins/server v1.0.1/go.mod h1:qedfnQFlK1+Jwv5M8mRXJCOWTvF/qfFyULK/0UMBeOk= -github.com/spiral/roadrunner-plugins/server v1.0.3 h1:Bhr5tK0eW+52/qeew6QMowowOpeUKJv/qqECCmW1X+Y= -github.com/spiral/roadrunner-plugins/server v1.0.3/go.mod h1:XHfOZwvITuKyUwU79KaeMm8gU5AqzKQg0MCHJ7eBU5c= -github.com/spiral/roadrunner-plugins/server v1.0.4 h1:i6QWbVM/gLnM3Tb/DDFSFpRnxccJd3mCpENW2rBuTeE= -github.com/spiral/roadrunner-plugins/server v1.0.4/go.mod h1:XHfOZwvITuKyUwU79KaeMm8gU5AqzKQg0MCHJ7eBU5c= -github.com/spiral/roadrunner-plugins/server v1.0.6 h1:JOrA/O/DJjuqKE1YTxLSyYh+/FLsJHY9biZkLwZk+LE= -github.com/spiral/roadrunner-plugins/server v1.0.6/go.mod h1:FyUujaAfNNdXCJHDkWD5pcRolNQ4cOMb6N+KapIMVpk= -github.com/spiral/roadrunner/v2 v2.0.0-alpha26/go.mod h1:r7ojuHm9qCVbg4fKcqr4Aqk7VXqZ9YPefr1LOv7HNys= -github.com/spiral/roadrunner/v2 v2.0.0-alpha28/go.mod h1:Uqtk/47S+qweVjSl4R9XVImEVZc3nMWku4yg3A+f18Q= -github.com/spiral/roadrunner/v2 v2.0.0-alpha30/go.mod h1:fKmPelVQxWql3cpjt/hYsf/Y2zDKb+DbZXgxdwj2nTc= -github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -484,10 +297,7 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.18.0 h1:IV0DdMlatq9QO1Cr6wGJPVW1sV1Q8HvZXAIcjorylyM= @@ -499,32 +309,25 @@ github.com/vbauerster/mpb/v5 v5.4.0/go.mod h1:fi4wVo7BVQ22QcvFObm+VwliQXlV1eBT8J github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yookoala/gofast v0.4.0 h1:dLBjghcsbbZNOEHN8N1X/gh9S6srmJed4WQfG7DlKwo= github.com/yookoala/gofast v0.4.0/go.mod h1:rfbkoKaQG1bnuTUZcmV3vAlnfpF4FTq8WbQJf2vcpg8= +github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0= github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= -go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/otel v0.15.0 h1:CZFy2lPhxd4HlhZnYK8gRyDotksO3Ip9rBweY1vVYJw= go.opentelemetry.io/otel v0.15.0/go.mod h1:e4GKElweB8W2gWUqbghw0B8t5MCTccc9212eNHnOHwA= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -533,8 +336,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -555,7 +356,6 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -564,19 +364,15 @@ golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY= @@ -590,7 +386,6 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -600,35 +395,26 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201210223839-7e3030f88018/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201218084310-7d0127a74742 h1:+CBz4km/0KPU3RGTwARGh/noP3bEwtHcq+0YcBQM2JQ= golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201221093633-bc327ba9c2f0 h1:n+DPcgTwkgWzIFpLmoimYR2K2b0Ga5+Os4kayIN0vGo= @@ -640,13 +426,10 @@ 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/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180726210403-bfb5194568d3/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -656,7 +439,6 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -667,20 +449,15 @@ golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114 h1:DnSr2mCsxyCE6ZgIkmcWUQY2R5cH/6wL7eIxEmQOMSE= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 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= -google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= @@ -690,21 +467,13 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -716,37 +485,28 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/ini.v1 v1.38.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/plugins/checker/config.go b/plugins/checker/config.go new file mode 100644 index 00000000..5f952592 --- /dev/null +++ b/plugins/checker/config.go @@ -0,0 +1,5 @@ +package checker + +type Config struct { + Address string +} diff --git a/plugins/checker/interface.go b/plugins/checker/interface.go new file mode 100644 index 00000000..dd9dcada --- /dev/null +++ b/plugins/checker/interface.go @@ -0,0 +1,11 @@ +package checker + +// Status consists of status code from the service +type Status struct { + Code int +} + +// Checker interface used to get latest status from plugin +type Checker interface { + Status() Status +} diff --git a/plugins/checker/plugin.go b/plugins/checker/plugin.go new file mode 100644 index 00000000..95f4f68c --- /dev/null +++ b/plugins/checker/plugin.go @@ -0,0 +1,151 @@ +package checker + +import ( + "fmt" + "net/http" + "time" + + "github.com/gofiber/fiber/v2" + fiberLogger "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +const ( + // PluginName declares public plugin name. + PluginName = "status" +) + +type Plugin struct { + registry map[string]Checker + server *fiber.App + log logger.Logger + cfg *Config +} + +func (c *Plugin) Init(log logger.Logger, cfg config.Configurer) error { + const op = errors.Op("status plugin init") + err := cfg.UnmarshalKey(PluginName, &c.cfg) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + if c.cfg == nil { + return errors.E(errors.Disabled) + } + + c.registry = make(map[string]Checker) + c.log = log + return nil +} + +func (c *Plugin) Serve() chan error { + errCh := make(chan error, 1) + c.server = fiber.New(fiber.Config{ + ReadTimeout: time.Second * 5, + WriteTimeout: time.Second * 5, + IdleTimeout: time.Second * 5, + }) + c.server.Group("/v1", c.healthHandler) + c.server.Use(fiberLogger.New()) + c.server.Use("/health", c.healthHandler) + + go func() { + err := c.server.Listen(c.cfg.Address) + if err != nil { + errCh <- err + } + }() + + return errCh +} + +func (c *Plugin) Stop() error { + const op = errors.Op("checker stop") + err := c.server.Shutdown() + if err != nil { + return errors.E(op, err) + } + return nil +} + +// Reset named service. +func (c *Plugin) Status(name string) (Status, error) { + const op = errors.Op("get status") + svc, ok := c.registry[name] + if !ok { + return Status{}, errors.E(op, errors.Errorf("no such service: %s", name)) + } + + return svc.Status(), nil +} + +// CollectTarget collecting services which can provide Status. +func (c *Plugin) CollectTarget(name endure.Named, r Checker) error { + c.registry[name.Name()] = r + return nil +} + +// Collects declares services to be collected. +func (c *Plugin) Collects() []interface{} { + return []interface{}{ + c.CollectTarget, + } +} + +// Name of the service. +func (c *Plugin) Name() string { + return PluginName +} + +// RPCService returns associated rpc service. +func (c *Plugin) RPC() interface{} { + return &rpc{srv: c, log: c.log} +} + +type Plugins struct { + Plugins []string `query:"plugin"` +} + +const template string = "Service: %s: Status: %d\n" + +func (c *Plugin) healthHandler(ctx *fiber.Ctx) error { + const op = errors.Op("health_handler") + plugins := &Plugins{} + err := ctx.QueryParser(plugins) + if err != nil { + return errors.E(op, err) + } + + if len(plugins.Plugins) == 0 { + ctx.Status(http.StatusOK) + _, _ = ctx.WriteString("No plugins provided in query. Query should be in form of: /v1/health?plugin=plugin1&plugin=plugin2 \n") + return nil + } + + failed := false + // iterate over all provided plugins + for i := 0; i < len(plugins.Plugins); i++ { + // check if the plugin exists + if plugin, ok := c.registry[plugins.Plugins[i]]; ok { + st := plugin.Status() + if st.Code >= 500 { + failed = true + continue + } else if st.Code >= 100 && st.Code <= 400 { + _, _ = ctx.WriteString(fmt.Sprintf(template, plugins.Plugins[i], st.Code)) + } + } else { + _, _ = ctx.WriteString(fmt.Sprintf("Service: %s not found", plugins.Plugins[i])) + } + } + if failed { + ctx.Status(http.StatusInternalServerError) + return nil + } + + ctx.Status(http.StatusOK) + return nil +} diff --git a/plugins/checker/rpc.go b/plugins/checker/rpc.go new file mode 100644 index 00000000..0daa62fe --- /dev/null +++ b/plugins/checker/rpc.go @@ -0,0 +1,27 @@ +package checker + +import ( + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +type rpc struct { + srv *Plugin + log logger.Logger +} + +// Status return current status of the provided plugin +func (rpc *rpc) Status(service string, status *Status) error { + const op = errors.Op("status") + rpc.log.Debug("started Status method", "service", service) + st, err := rpc.srv.Status(service) + if err != nil { + return errors.E(op, err) + } + + *status = st + + rpc.log.Debug("status code", "code", st.Code) + rpc.log.Debug("successfully finished Status method") + return nil +} diff --git a/plugins/config/interface.go b/plugins/config/interface.go new file mode 100644 index 00000000..2a7c67ce --- /dev/null +++ b/plugins/config/interface.go @@ -0,0 +1,22 @@ +package config + +type Configurer interface { + // UnmarshalKey reads configuration section into configuration object. + // + // func (h *HttpService) Init(cp config.Configurer) error { + // h.config := &HttpConfig{} + // if err := configProvider.UnmarshalKey("http", h.config); err != nil { + // return err + // } + // } + UnmarshalKey(name string, out interface{}) error + + // Get used to get config section + Get(name string) interface{} + + // Overwrite used to overwrite particular values in the unmarshalled config + Overwrite(values map[string]interface{}) error + + // Has checks if config section exists. + Has(name string) bool +} diff --git a/plugins/config/plugin.go b/plugins/config/plugin.go new file mode 100755 index 00000000..1a170448 --- /dev/null +++ b/plugins/config/plugin.go @@ -0,0 +1,75 @@ +package config + +import ( + "bytes" + "strings" + + "github.com/spf13/viper" + "github.com/spiral/errors" +) + +type Viper struct { + viper *viper.Viper + Path string + Prefix string + Type string + ReadInCfg []byte +} + +// Inits config provider. +func (v *Viper) Init() error { + const op = errors.Op("viper plugin init") + v.viper = viper.New() + // If user provided []byte data with config, read it and ignore Path and Prefix + if v.ReadInCfg != nil && v.Type != "" { + v.viper.SetConfigType("yaml") + return v.viper.ReadConfig(bytes.NewBuffer(v.ReadInCfg)) + } + + // read in environment variables that match + v.viper.AutomaticEnv() + if v.Prefix == "" { + return errors.E(op, errors.Str("prefix should be set")) + } + + v.viper.SetEnvPrefix(v.Prefix) + if v.Path == "" { + return errors.E(op, errors.Str("path should be set")) + } + + v.viper.SetConfigFile(v.Path) + v.viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + + return v.viper.ReadInConfig() +} + +// Overwrite overwrites existing config with provided values +func (v *Viper) Overwrite(values map[string]interface{}) error { + if len(values) != 0 { + for key, value := range values { + v.viper.Set(key, value) + } + } + + return nil +} + +// UnmarshalKey reads configuration section into configuration object. +func (v *Viper) UnmarshalKey(name string, out interface{}) error { + const op = errors.Op("unmarshal key") + err := v.viper.UnmarshalKey(name, &out) + if err != nil { + return errors.E(op, err) + } + return nil +} + +// Get raw config in a form of config section. +func (v *Viper) Get(name string) interface{} { + return v.viper.Get(name) +} + +// Has checks if config section exists. +func (v *Viper) Has(name string) bool { + return v.viper.IsSet(name) +} diff --git a/plugins/doc/graphviz.svg b/plugins/doc/graphviz.svg new file mode 100644 index 00000000..86f6ab5c --- /dev/null +++ b/plugins/doc/graphviz.svg @@ -0,0 +1,169 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><!-- Generated by graphviz version 2.40.1 (20161225.0304) + --><!-- Title: endure Pages: 1 --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="625pt" height="479pt" viewBox="0.00 0.00 624.94 478.79"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 474.786)"> +<title>endure</title> +<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-474.786 620.9357,-474.786 620.9357,4 -4,4"/> +<!-- checker --> +<g id="node1" class="node"> +<title>checker</title> +<ellipse fill="none" stroke="#000000" cx="412.2429" cy="-377.2862" rx="41.1103" ry="18"/> +<text text-anchor="middle" x="412.2429" y="-373.0862" font-family="Times,serif" font-size="14.00" fill="#000000">checker</text> +</g> +<!-- config --> +<g id="node2" class="node"> +<title>config</title> +<ellipse fill="none" stroke="#000000" cx="463.8878" cy="-235.393" rx="35.9154" ry="18"/> +<text text-anchor="middle" x="463.8878" y="-231.193" font-family="Times,serif" font-size="14.00" fill="#000000">config</text> +</g> +<!-- checker->config --> +<g id="edge1" class="edge"> +<title>checker->config</title> +<path fill="none" stroke="#000000" d="M418.7837,-359.3154C427.6313,-335.0068 443.4953,-291.4209 453.8554,-262.9568"/> +<polygon fill="#000000" stroke="#000000" points="457.2687,-263.812 457.4,-253.218 450.6908,-261.4178 457.2687,-263.812"/> +</g> +<!-- logger --> +<g id="node3" class="node"> +<title>logger</title> +<ellipse fill="none" stroke="#000000" cx="35.7071" cy="-310.8928" rx="35.9154" ry="18"/> +<text text-anchor="middle" x="35.7071" y="-306.6928" font-family="Times,serif" font-size="14.00" fill="#000000">logger</text> +</g> +<!-- checker->logger --> +<g id="edge2" class="edge"> +<title>checker->logger</title> +<path fill="none" stroke="#000000" d="M374.0665,-370.5547C303.7112,-358.1492 154.0014,-331.7513 79.586,-318.6299"/> +<polygon fill="#000000" stroke="#000000" points="80.0574,-315.1591 69.6015,-316.8693 78.8418,-322.0527 80.0574,-315.1591"/> +</g> +<!-- logger->config --> +<g id="edge4" class="edge"> +<title>logger->config</title> +<path fill="none" stroke="#000000" d="M69.6636,-304.9054C146.6435,-291.3317 334.3698,-258.2305 420.0048,-243.1308"/> +<polygon fill="#000000" stroke="#000000" points="420.6875,-246.5645 429.9277,-241.3811 419.4719,-239.6708 420.6875,-246.5645"/> +</g> +<!-- gzip --> +<g id="node4" class="node"> +<title>gzip</title> +<ellipse fill="none" stroke="#000000" cx="531.6651" cy="-102.393" rx="27.8286" ry="18"/> +<text text-anchor="middle" x="531.6651" y="-98.193" font-family="Times,serif" font-size="14.00" fill="#000000">gzip</text> +</g> +<!-- headers --> +<g id="node5" class="node"> +<title>headers</title> +<ellipse fill="none" stroke="#000000" cx="576.4118" cy="-235.393" rx="40.548" ry="18"/> +<text text-anchor="middle" x="576.4118" y="-231.193" font-family="Times,serif" font-size="14.00" fill="#000000">headers</text> +</g> +<!-- headers->config --> +<g id="edge3" class="edge"> +<title>headers->config</title> +<path fill="none" stroke="#000000" d="M535.788,-235.393C527.3742,-235.393 518.4534,-235.393 509.8639,-235.393"/> +<polygon fill="#000000" stroke="#000000" points="509.607,-231.8931 499.607,-235.393 509.607,-238.8931 509.607,-231.8931"/> +</g> +<!-- metrics --> +<g id="node6" class="node"> +<title>metrics</title> +<ellipse fill="none" stroke="#000000" cx="412.2429" cy="-93.4998" rx="39.4196" ry="18"/> +<text text-anchor="middle" x="412.2429" y="-89.2998" font-family="Times,serif" font-size="14.00" fill="#000000">metrics</text> +</g> +<!-- metrics->config --> +<g id="edge6" class="edge"> +<title>metrics->config</title> +<path fill="none" stroke="#000000" d="M418.7837,-111.4707C427.6313,-135.7792 443.4953,-179.3651 453.8554,-207.8292"/> +<polygon fill="#000000" stroke="#000000" points="450.6908,-209.3682 457.4,-217.5681 457.2687,-206.974 450.6908,-209.3682"/> +</g> +<!-- metrics->logger --> +<g id="edge5" class="edge"> +<title>metrics->logger</title> +<path fill="none" stroke="#000000" d="M387.5373,-107.7636C321.7958,-145.7194 142.5487,-249.2078 68.4432,-291.9926"/> +<polygon fill="#000000" stroke="#000000" points="66.4391,-289.1082 59.5289,-297.1393 69.9391,-295.1704 66.4391,-289.1082"/> +</g> +<!-- redis --> +<g id="node7" class="node"> +<title>redis</title> +<ellipse fill="none" stroke="#000000" cx="281.4734" cy="-18" rx="29.6127" ry="18"/> +<text text-anchor="middle" x="281.4734" y="-13.8" font-family="Times,serif" font-size="14.00" fill="#000000">redis</text> +</g> +<!-- redis->config --> +<g id="edge8" class="edge"> +<title>redis->config</title> +<path fill="none" stroke="#000000" d="M295.1841,-34.3398C326.9308,-72.174 405.6399,-165.9759 443.2445,-210.7914"/> +<polygon fill="#000000" stroke="#000000" points="440.6581,-213.1541 449.7672,-218.5648 446.0204,-208.6545 440.6581,-213.1541"/> +</g> +<!-- redis->logger --> +<g id="edge7" class="edge"> +<title>redis->logger</title> +<path fill="none" stroke="#000000" d="M267.9098,-34.1644C227.1471,-82.7435 105.5381,-227.6715 56.5241,-286.0841"/> +<polygon fill="#000000" stroke="#000000" points="53.5843,-284.1426 49.8376,-294.0528 58.9466,-288.6421 53.5843,-284.1426"/> +</g> +<!-- reload --> +<g id="node8" class="node"> +<title>reload</title> +<ellipse fill="none" stroke="#000000" cx="281.4734" cy="-452.786" rx="35.3315" ry="18"/> +<text text-anchor="middle" x="281.4734" y="-448.586" font-family="Times,serif" font-size="14.00" fill="#000000">reload</text> +</g> +<!-- reload->config --> +<g id="edge10" class="edge"> +<title>reload->config</title> +<path fill="none" stroke="#000000" d="M295.4842,-436.0885C327.4495,-397.9939 405.8819,-304.5217 443.3335,-259.8887"/> +<polygon fill="#000000" stroke="#000000" points="446.0824,-262.0576 449.8292,-252.1474 440.7201,-257.5581 446.0824,-262.0576"/> +</g> +<!-- reload->logger --> +<g id="edge9" class="edge"> +<title>reload->logger</title> +<path fill="none" stroke="#000000" d="M257.9083,-439.1807C213.6848,-413.6483 118.2025,-358.5216 68.0211,-329.5493"/> +<polygon fill="#000000" stroke="#000000" points="69.6111,-326.4259 59.2009,-324.457 66.1111,-332.4881 69.6111,-326.4259"/> +</g> +<!-- resetter --> +<g id="node9" class="node"> +<title>resetter</title> +<ellipse fill="none" stroke="#000000" cx="132.7678" cy="-426.5652" rx="39.3984" ry="18"/> +<text text-anchor="middle" x="132.7678" y="-422.3652" font-family="Times,serif" font-size="14.00" fill="#000000">resetter</text> +</g> +<!-- reload->resetter --> +<g id="edge11" class="edge"> +<title>reload->resetter</title> +<path fill="none" stroke="#000000" d="M248.1009,-446.9016C227.9026,-443.3401 201.8366,-438.7439 179.5962,-434.8224"/> +<polygon fill="#000000" stroke="#000000" points="180.1376,-431.3639 169.6817,-433.0742 178.922,-438.2575 180.1376,-431.3639"/> +</g> +<!-- resetter->logger --> +<g id="edge12" class="edge"> +<title>resetter->logger</title> +<path fill="none" stroke="#000000" d="M118.4461,-409.4974C102.0084,-389.9077 74.9173,-357.6218 56.2379,-335.3605"/> +<polygon fill="#000000" stroke="#000000" points="58.881,-333.0653 49.7719,-327.6546 53.5187,-337.5649 58.881,-333.0653"/> +</g> +<!-- rpc --> +<g id="node10" class="node"> +<title>rpc</title> +<ellipse fill="none" stroke="#000000" cx="132.7678" cy="-44.2208" rx="27" ry="18"/> +<text text-anchor="middle" x="132.7678" y="-40.0208" font-family="Times,serif" font-size="14.00" fill="#000000">rpc</text> +</g> +<!-- rpc->config --> +<g id="edge13" class="edge"> +<title>rpc->config</title> +<path fill="none" stroke="#000000" d="M153.4808,-56.1795C209.3277,-88.4227 363.359,-177.3527 431.1448,-216.4889"/> +<polygon fill="#000000" stroke="#000000" points="429.7078,-219.7006 440.1181,-221.6696 433.2078,-213.6384 429.7078,-219.7006"/> +</g> +<!-- rpc->logger --> +<g id="edge14" class="edge"> +<title>rpc->logger</title> +<path fill="none" stroke="#000000" d="M126.3994,-61.7179C109.8827,-107.097 65.5725,-228.8383 45.6502,-283.5745"/> +<polygon fill="#000000" stroke="#000000" points="42.3576,-282.3876 42.2262,-292.9816 48.9354,-284.7818 42.3576,-282.3876"/> +</g> +<!-- static --> +<g id="node11" class="node"> +<title>static</title> +<ellipse fill="none" stroke="#000000" cx="35.7071" cy="-159.8932" rx="31.3333" ry="18"/> +<text text-anchor="middle" x="35.7071" y="-155.6932" font-family="Times,serif" font-size="14.00" fill="#000000">static</text> +</g> +<!-- static->config --> +<g id="edge15" class="edge"> +<title>static->config</title> +<path fill="none" stroke="#000000" d="M65.8159,-165.2022C140.1736,-178.3135 332.7753,-212.2743 419.9157,-227.6396"/> +<polygon fill="#000000" stroke="#000000" points="419.5489,-231.1288 430.0048,-229.4185 420.7645,-224.2351 419.5489,-231.1288"/> +</g> +<!-- static->logger --> +<g id="edge16" class="edge"> +<title>static->logger</title> +<path fill="none" stroke="#000000" d="M35.7071,-178.1073C35.7071,-204.0691 35.7071,-251.9543 35.7071,-282.5696"/> +<polygon fill="#000000" stroke="#000000" points="32.2072,-282.6141 35.7071,-292.6141 39.2072,-282.6142 32.2072,-282.6141"/> +</g> +</g> +</svg>
\ No newline at end of file diff --git a/plugins/gzip/plugin.go b/plugins/gzip/plugin.go new file mode 100644 index 00000000..e5b9e4f5 --- /dev/null +++ b/plugins/gzip/plugin.go @@ -0,0 +1,25 @@ +package gzip + +import ( + "net/http" + + "github.com/NYTimes/gziphandler" +) + +const PluginName = "gzip" + +type Gzip struct{} + +func (g *Gzip) Init() error { + return nil +} + +func (g *Gzip) Middleware(next http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + gziphandler.GzipHandler(next).ServeHTTP(w, r) + } +} + +func (g *Gzip) Name() string { + return PluginName +} diff --git a/plugins/headers/config.go b/plugins/headers/config.go new file mode 100644 index 00000000..8d4e29c2 --- /dev/null +++ b/plugins/headers/config.go @@ -0,0 +1,36 @@ +package headers + +// Config declares headers service configuration. +type Config struct { + Headers struct { + // CORS settings. + CORS *CORSConfig + + // Request headers to add to every payload send to PHP. + Request map[string]string + + // Response headers to add to every payload generated by PHP. + Response map[string]string + } +} + +// CORSConfig headers configuration. +type CORSConfig struct { + // AllowedOrigin: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin + AllowedOrigin string + + // AllowedHeaders: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers + AllowedHeaders string + + // AllowedMethods: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods + AllowedMethods string + + // AllowCredentials https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials + AllowCredentials *bool + + // ExposeHeaders: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers + ExposedHeaders string + + // MaxAge of CORS headers in seconds/ + MaxAge int +} diff --git a/plugins/headers/plugin.go b/plugins/headers/plugin.go new file mode 100644 index 00000000..f1c6e6f3 --- /dev/null +++ b/plugins/headers/plugin.go @@ -0,0 +1,117 @@ +package headers + +import ( + "net/http" + "strconv" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" +) + +// ID contains default service name. +const PluginName = "headers" +const RootPluginName = "http" + +// Service serves headers files. Potentially convert into middleware? +type Plugin struct { + // server configuration (location, forbidden files and etc) + cfg *Config +} + +// Init must return configure service and return true if service hasStatus enabled. Must return error in case of +// misconfiguration. Services must not be used without proper configuration pushed first. +func (s *Plugin) Init(cfg config.Configurer) error { + const op = errors.Op("headers plugin init") + err := cfg.UnmarshalKey(RootPluginName, &s.cfg) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + return nil +} + +// middleware must return true if request/response pair is handled within the middleware. +func (s *Plugin) Middleware(next http.Handler) http.HandlerFunc { + // Define the http.HandlerFunc + return func(w http.ResponseWriter, r *http.Request) { + if s.cfg.Headers.Request != nil { + for k, v := range s.cfg.Headers.Request { + r.Header.Add(k, v) + } + } + + if s.cfg.Headers.Response != nil { + for k, v := range s.cfg.Headers.Response { + w.Header().Set(k, v) + } + } + + if s.cfg.Headers.CORS != nil { + if r.Method == http.MethodOptions { + s.preflightRequest(w) + return + } + s.corsHeaders(w) + } + + next.ServeHTTP(w, r) + } +} + +func (s *Plugin) Name() string { + return PluginName +} + +// configure OPTIONS response +func (s *Plugin) preflightRequest(w http.ResponseWriter) { + headers := w.Header() + + headers.Add("Vary", "Origin") + headers.Add("Vary", "Access-Control-Request-Method") + headers.Add("Vary", "Access-Control-Request-Headers") + + if s.cfg.Headers.CORS.AllowedOrigin != "" { + headers.Set("Access-Control-Allow-Origin", s.cfg.Headers.CORS.AllowedOrigin) + } + + if s.cfg.Headers.CORS.AllowedHeaders != "" { + headers.Set("Access-Control-Allow-Headers", s.cfg.Headers.CORS.AllowedHeaders) + } + + if s.cfg.Headers.CORS.AllowedMethods != "" { + headers.Set("Access-Control-Allow-Methods", s.cfg.Headers.CORS.AllowedMethods) + } + + if s.cfg.Headers.CORS.AllowCredentials != nil { + headers.Set("Access-Control-Allow-Credentials", strconv.FormatBool(*s.cfg.Headers.CORS.AllowCredentials)) + } + + if s.cfg.Headers.CORS.MaxAge > 0 { + headers.Set("Access-Control-Max-Age", strconv.Itoa(s.cfg.Headers.CORS.MaxAge)) + } + + w.WriteHeader(http.StatusOK) +} + +// configure CORS headers +func (s *Plugin) corsHeaders(w http.ResponseWriter) { + headers := w.Header() + + headers.Add("Vary", "Origin") + + if s.cfg.Headers.CORS.AllowedOrigin != "" { + headers.Set("Access-Control-Allow-Origin", s.cfg.Headers.CORS.AllowedOrigin) + } + + if s.cfg.Headers.CORS.AllowedHeaders != "" { + headers.Set("Access-Control-Allow-Headers", s.cfg.Headers.CORS.AllowedHeaders) + } + + if s.cfg.Headers.CORS.ExposedHeaders != "" { + headers.Set("Access-Control-Expose-Headers", s.cfg.Headers.CORS.ExposedHeaders) + } + + if s.cfg.Headers.CORS.AllowCredentials != nil { + headers.Set("Access-Control-Allow-Credentials", strconv.FormatBool(*s.cfg.Headers.CORS.AllowCredentials)) + } +} diff --git a/plugins/http/attributes/attributes.go b/plugins/http/attributes/attributes.go new file mode 100644 index 00000000..4c453766 --- /dev/null +++ b/plugins/http/attributes/attributes.go @@ -0,0 +1,85 @@ +package attributes + +import ( + "context" + "errors" + "net/http" +) + +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. +type contextKey struct { + name string +} + +func (k *contextKey) String() string { return k.name } + +var ( + // PsrContextKey is a context key. It can be used in the http attributes + PsrContextKey = &contextKey{"psr_attributes"} +) + +type attrs map[string]interface{} + +func (v attrs) get(key string) interface{} { + if v == nil { + return "" + } + + return v[key] +} + +func (v attrs) set(key string, value interface{}) { + v[key] = value +} + +func (v attrs) del(key string) { + delete(v, key) +} + +// Init returns request with new context and attribute bag. +func Init(r *http.Request) *http.Request { + return r.WithContext(context.WithValue(r.Context(), PsrContextKey, attrs{})) +} + +// All returns all context attributes. +func All(r *http.Request) map[string]interface{} { + v := r.Context().Value(PsrContextKey) + if v == nil { + return attrs{} + } + + return v.(attrs) +} + +// Get gets the value from request context. It replaces any existing +// values. +func Get(r *http.Request, key string) interface{} { + v := r.Context().Value(PsrContextKey) + if v == nil { + return nil + } + + return v.(attrs).get(key) +} + +// Set sets the key to value. It replaces any existing +// values. Context specific. +func Set(r *http.Request, key string, value interface{}) error { + v := r.Context().Value(PsrContextKey) + if v == nil { + return errors.New("unable to find `psr:attributes` context key") + } + + v.(attrs).set(key, value) + return nil +} + +// Delete deletes values associated with attribute key. +func (v attrs) Delete(key string) { + if v == nil { + return + } + + v.del(key) +} diff --git a/plugins/http/config.go b/plugins/http/config.go new file mode 100644 index 00000000..00d2940b --- /dev/null +++ b/plugins/http/config.go @@ -0,0 +1,291 @@ +package http + +import ( + "net" + "os" + "runtime" + "strings" + "time" + + "github.com/spiral/errors" + poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" +) + +type Cidrs []*net.IPNet + +func (c *Cidrs) IsTrusted(ip string) bool { + if len(*c) == 0 { + return false + } + + i := net.ParseIP(ip) + if i == nil { + return false + } + + for _, cird := range *c { + if cird.Contains(i) { + return true + } + } + + return false +} + +// Config configures RoadRunner HTTP server. +type Config struct { + // Port and port to handle as http server. + Address string + + // SSL defines https server options. + SSL *SSLConfig + + // FCGI configuration. You can use FastCGI without HTTP server. + FCGI *FCGIConfig + + // HTTP2 configuration + HTTP2 *HTTP2Config + + // MaxRequestSize specified max size for payload body in megabytes, set 0 to unlimited. + MaxRequestSize uint64 + + // TrustedSubnets declare IP subnets which are allowed to set ip using X-Real-Ip and X-Forwarded-For + TrustedSubnets []string + + // Uploads configures uploads configuration. + Uploads *UploadsConfig + + // Pool configures worker pool. + Pool *poolImpl.Config + + // Env is environment variables passed to the http pool + Env map[string]string + + // List of the middleware names (order will be preserved) + Middleware []string + + // slice of net.IPNet + cidrs Cidrs +} + +// FCGIConfig for FastCGI server. +type FCGIConfig struct { + // Address and port to handle as http server. + Address string +} + +// HTTP2Config HTTP/2 server customizations. +type HTTP2Config struct { + // Enable or disable HTTP/2 extension, default enable. + Enabled bool + + // H2C enables HTTP/2 over TCP + H2C bool + + // MaxConcurrentStreams defaults to 128. + MaxConcurrentStreams uint32 +} + +// InitDefaults sets default values for HTTP/2 configuration. +func (cfg *HTTP2Config) InitDefaults() error { + cfg.Enabled = true + cfg.MaxConcurrentStreams = 128 + + return nil +} + +// SSLConfig defines https server configuration. +type SSLConfig struct { + // Port to listen as HTTPS server, defaults to 443. + Port int + + // Redirect when enabled forces all http connections to switch to https. + Redirect bool + + // Key defined private server key. + Key string + + // Cert is https certificate. + Cert string + + // Root CA file + RootCA string +} + +// EnableHTTP is true when http server must run. +func (c *Config) EnableHTTP() bool { + return c.Address != "" +} + +// EnableTLS returns true if pool must listen TLS connections. +func (c *Config) EnableTLS() bool { + return c.SSL.Key != "" || c.SSL.Cert != "" || c.SSL.RootCA != "" +} + +// EnableHTTP2 when HTTP/2 extension must be enabled (only with TSL). +func (c *Config) EnableHTTP2() bool { + return c.HTTP2.Enabled +} + +// EnableH2C when HTTP/2 extension must be enabled on TCP. +func (c *Config) EnableH2C() bool { + return c.HTTP2.H2C +} + +// EnableFCGI is true when FastCGI server must be enabled. +func (c *Config) EnableFCGI() bool { + return c.FCGI.Address != "" +} + +// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. +func (c *Config) InitDefaults() error { + if c.Pool == nil { + // default pool + c.Pool = &poolImpl.Config{ + Debug: false, + NumWorkers: int64(runtime.NumCPU()), + MaxJobs: 1000, + AllocateTimeout: time.Second * 60, + DestroyTimeout: time.Second * 60, + Supervisor: nil, + } + } + + if c.HTTP2 == nil { + c.HTTP2 = &HTTP2Config{} + } + + if c.FCGI == nil { + c.FCGI = &FCGIConfig{} + } + + if c.Uploads == nil { + c.Uploads = &UploadsConfig{} + } + + if c.SSL == nil { + c.SSL = &SSLConfig{} + } + + if c.SSL.Port == 0 { + c.SSL.Port = 443 + } + + err := c.HTTP2.InitDefaults() + if err != nil { + return err + } + err = c.Uploads.InitDefaults() + if err != nil { + return err + } + + if c.TrustedSubnets == nil { + // @see https://en.wikipedia.org/wiki/Reserved_IP_addresses + c.TrustedSubnets = []string{ + "10.0.0.0/8", + "127.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "::1/128", + "fc00::/7", + "fe80::/10", + } + } + + cidrs, err := ParseCIDRs(c.TrustedSubnets) + if err != nil { + return err + } + c.cidrs = cidrs + + return c.Valid() +} + +func ParseCIDRs(subnets []string) (Cidrs, error) { + c := make(Cidrs, 0, len(subnets)) + for _, cidr := range subnets { + _, cr, err := net.ParseCIDR(cidr) + if err != nil { + return nil, err + } + + c = append(c, cr) + } + + return c, nil +} + +// IsTrusted if api can be trusted to use X-Real-Ip, X-Forwarded-For +func (c *Config) IsTrusted(ip string) bool { + if c.cidrs == nil { + return false + } + + i := net.ParseIP(ip) + if i == nil { + return false + } + + for _, cird := range c.cidrs { + if cird.Contains(i) { + return true + } + } + + return false +} + +// Valid validates the configuration. +func (c *Config) Valid() error { + const op = errors.Op("validation") + if c.Uploads == nil { + return errors.E(op, errors.Str("malformed uploads config")) + } + + if c.HTTP2 == nil { + return errors.E(op, errors.Str("malformed http2 config")) + } + + if c.Pool == nil { + return errors.E(op, "malformed pool config") + } + + if !c.EnableHTTP() && !c.EnableTLS() && !c.EnableFCGI() { + return errors.E(op, errors.Str("unable to run http service, no method has been specified (http, https, http/2 or FastCGI)")) + } + + if c.Address != "" && !strings.Contains(c.Address, ":") { + return errors.E(op, errors.Str("malformed http server address")) + } + + if c.EnableTLS() { + if _, err := os.Stat(c.SSL.Key); err != nil { + if os.IsNotExist(err) { + return errors.E(op, errors.Errorf("key file '%s' does not exists", c.SSL.Key)) + } + + return err + } + + if _, err := os.Stat(c.SSL.Cert); err != nil { + if os.IsNotExist(err) { + return errors.E(op, errors.Errorf("cert file '%s' does not exists", c.SSL.Cert)) + } + + return err + } + + // RootCA is optional, but if provided - check it + if c.SSL.RootCA != "" { + if _, err := os.Stat(c.SSL.RootCA); err != nil { + if os.IsNotExist(err) { + return errors.E(op, errors.Errorf("root ca path provided, but path '%s' does not exists", c.SSL.RootCA)) + } + return err + } + } + } + + return nil +} diff --git a/plugins/http/constants.go b/plugins/http/constants.go new file mode 100644 index 00000000..773d1f46 --- /dev/null +++ b/plugins/http/constants.go @@ -0,0 +1,6 @@ +package http + +import "net/http" + +var http2pushHeaderKey = http.CanonicalHeaderKey("http2-push") +var TrailerHeaderKey = http.CanonicalHeaderKey("trailer") diff --git a/plugins/http/errors.go b/plugins/http/errors.go new file mode 100644 index 00000000..fb8762ef --- /dev/null +++ b/plugins/http/errors.go @@ -0,0 +1,25 @@ +// +build !windows + +package http + +import ( + "errors" + "net" + "os" + "syscall" +) + +// Broken pipe +var errEPIPE = errors.New("EPIPE(32) -> connection reset by peer") + +// handleWriteError just check if error was caused by aborted connection on linux +func handleWriteError(err error) error { + if netErr, ok2 := err.(*net.OpError); ok2 { + if syscallErr, ok3 := netErr.Err.(*os.SyscallError); ok3 { + if syscallErr.Err == syscall.EPIPE { + return errEPIPE + } + } + } + return err +} diff --git a/plugins/http/errors_windows.go b/plugins/http/errors_windows.go new file mode 100644 index 00000000..3d0ba04c --- /dev/null +++ b/plugins/http/errors_windows.go @@ -0,0 +1,27 @@ +// +build windows + +package http + +import ( + "errors" + "net" + "os" + "syscall" +) + +//Software caused connection abort. +//An established connection was aborted by the software in your host computer, +//possibly due to a data transmission time-out or protocol error. +var errEPIPE = errors.New("WSAECONNABORTED (10053) -> an established connection was aborted by peer") + +// handleWriteError just check if error was caused by aborted connection on windows +func handleWriteError(err error) error { + if netErr, ok2 := err.(*net.OpError); ok2 { + if syscallErr, ok3 := netErr.Err.(*os.SyscallError); ok3 { + if syscallErr.Err == syscall.WSAECONNABORTED { + return errEPIPE + } + } + } + return err +} diff --git a/plugins/http/handler.go b/plugins/http/handler.go new file mode 100644 index 00000000..1889ed6d --- /dev/null +++ b/plugins/http/handler.go @@ -0,0 +1,240 @@ +package http + +import ( + "net" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/hashicorp/go-multierror" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/events" + "github.com/spiral/roadrunner/v2/interfaces/pool" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +const ( + // EventResponse thrown after the request been processed. See ErrorEvent as payload. + EventResponse = iota + 500 + + // EventError thrown on any non job error provided by road runner server. + EventError +) + +const MB = 1024 * 1024 + +type Handle interface { + AddListener(l events.EventListener) + ServeHTTP(w http.ResponseWriter, r *http.Request) +} + +// ErrorEvent represents singular http error event. +type ErrorEvent struct { + // Request contains client request, must not be stored. + Request *http.Request + + // Error - associated error, if any. + Error error + + // event timings + start time.Time + elapsed time.Duration +} + +// Elapsed returns duration of the invocation. +func (e *ErrorEvent) Elapsed() time.Duration { + return e.elapsed +} + +// ResponseEvent represents singular http response event. +type ResponseEvent struct { + // Request contains client request, must not be stored. + Request *Request + + // Response contains service response. + Response *Response + + // event timings + start time.Time + elapsed time.Duration +} + +// Elapsed returns duration of the invocation. +func (e *ResponseEvent) Elapsed() time.Duration { + return e.elapsed +} + +// Handler serves http connections to underlying PHP application using PSR-7 protocol. Context will include request headers, +// parsed files and query, payload will include parsed form dataTree (if any). +type handler struct { + maxRequestSize uint64 + uploads UploadsConfig + trusted Cidrs + log logger.Logger + pool pool.Pool + mul sync.Mutex + lsn events.EventListener +} + +func NewHandler(maxReqSize uint64, uploads UploadsConfig, trusted Cidrs, pool pool.Pool) (Handle, error) { + if pool == nil { + return nil, errors.E(errors.Str("pool should be initialized")) + } + return &handler{ + maxRequestSize: maxReqSize * MB, + uploads: uploads, + pool: pool, + trusted: trusted, + }, nil +} + +// Listen attaches handler event controller. +func (h *handler) AddListener(l events.EventListener) { + h.mul.Lock() + defer h.mul.Unlock() + + h.lsn = l +} + +// mdwr serve using PSR-7 requests passed to underlying application. Attempts to serve static files first if enabled. +func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + const op = errors.Op("ServeHTTP") + start := time.Now() + + // validating request size + if h.maxRequestSize != 0 { + err := h.maxSize(w, r, start, op) + if err != nil { + return + } + } + + req, err := NewRequest(r, h.uploads) + if err != nil { + h.handleError(w, r, err, start) + return + } + + // proxy IP resolution + h.resolveIP(req) + + req.Open(h.log) + defer req.Close(h.log) + + p, err := req.Payload() + if err != nil { + h.handleError(w, r, err, start) + return + } + + rsp, err := h.pool.Exec(p) + if err != nil { + h.handleError(w, r, err, start) + return + } + + resp, err := NewResponse(rsp) + if err != nil { + h.handleError(w, r, err, start) + return + } + + h.handleResponse(req, resp, start) + err = resp.Write(w) + if err != nil { + h.handleError(w, r, err, start) + } +} + +func (h *handler) maxSize(w http.ResponseWriter, r *http.Request, start time.Time, op errors.Op) error { + if length := r.Header.Get("content-length"); length != "" { + if size, err := strconv.ParseInt(length, 10, 64); err != nil { + h.handleError(w, r, err, start) + return err + } else if size > int64(h.maxRequestSize) { + h.handleError(w, r, errors.E(op, errors.Str("request body max size is exceeded")), start) + return err + } + } + return nil +} + +// handleError sends error. +func (h *handler) handleError(w http.ResponseWriter, r *http.Request, err error, start time.Time) { + h.mul.Lock() + defer h.mul.Unlock() + // if pipe is broken, there is no sense to write the header + // in this case we just report about error + if err == errEPIPE { + h.throw(ErrorEvent{Request: r, Error: err, start: start, elapsed: time.Since(start)}) + return + } + err = multierror.Append(err) + // ResponseWriter is ok, write the error code + w.WriteHeader(500) + _, err2 := w.Write([]byte(err.Error())) + // error during the writing to the ResponseWriter + if err2 != nil { + err = multierror.Append(err2, err) + // concat original error with ResponseWriter error + h.throw(ErrorEvent{Request: r, Error: errors.E(err), start: start, elapsed: time.Since(start)}) + return + } + h.throw(ErrorEvent{Request: r, Error: err, start: start, elapsed: time.Since(start)}) +} + +// handleResponse triggers response event. +func (h *handler) handleResponse(req *Request, resp *Response, start time.Time) { + h.throw(ResponseEvent{Request: req, Response: resp, start: start, elapsed: time.Since(start)}) +} + +// throw invokes event handler if any. +func (h *handler) throw(event interface{}) { + if h.lsn != nil { + h.lsn(event) + } +} + +// get real ip passing multiple proxy +func (h *handler) resolveIP(r *Request) { + if h.trusted.IsTrusted(r.RemoteAddr) == false { + return + } + + if r.Header.Get("X-Forwarded-For") != "" { + ips := strings.Split(r.Header.Get("X-Forwarded-For"), ",") + ipCount := len(ips) + + for i := ipCount - 1; i >= 0; i-- { + addr := strings.TrimSpace(ips[i]) + if net.ParseIP(addr) != nil { + r.RemoteAddr = addr + return + } + } + + return + } + + // The logic here is the following: + // In general case, we only expect X-Real-Ip header. If it exist, we get the IP address from header and set request Remote address + // But, if there is no X-Real-Ip header, we also trying to check CloudFlare headers + // True-Client-IP is a general CF header in which copied information from X-Real-Ip in CF. + // CF-Connecting-IP is an Enterprise feature and we check it last in order. + // This operations are near O(1) because Headers struct are the map type -> type MIMEHeader map[string][]string + if r.Header.Get("X-Real-Ip") != "" { + r.RemoteAddr = fetchIP(r.Header.Get("X-Real-Ip")) + return + } + + if r.Header.Get("True-Client-IP") != "" { + r.RemoteAddr = fetchIP(r.Header.Get("True-Client-IP")) + return + } + + if r.Header.Get("CF-Connecting-IP") != "" { + r.RemoteAddr = fetchIP(r.Header.Get("CF-Connecting-IP")) + } +} diff --git a/plugins/http/parse.go b/plugins/http/parse.go new file mode 100644 index 00000000..d4a1604b --- /dev/null +++ b/plugins/http/parse.go @@ -0,0 +1,147 @@ +package http + +import ( + "net/http" +) + +// MaxLevel defines maximum tree depth for incoming request data and files. +const MaxLevel = 127 + +type dataTree map[string]interface{} +type fileTree map[string]interface{} + +// parseData parses incoming request body into data tree. +func parseData(r *http.Request) dataTree { + data := make(dataTree) + if r.PostForm != nil { + for k, v := range r.PostForm { + data.push(k, v) + } + } + + if r.MultipartForm != nil { + for k, v := range r.MultipartForm.Value { + data.push(k, v) + } + } + + return data +} + +// pushes value into data tree. +func (d dataTree) push(k string, v []string) { + keys := FetchIndexes(k) + if len(keys) <= MaxLevel { + d.mount(keys, v) + } +} + +// mount mounts data tree recursively. +func (d dataTree) mount(i []string, v []string) { + if len(i) == 1 { + // single value context (last element) + d[i[0]] = v[len(v)-1] + return + } + + if len(i) == 2 && i[1] == "" { + // non associated array of elements + d[i[0]] = v + return + } + + if p, ok := d[i[0]]; ok { + p.(dataTree).mount(i[1:], v) + return + } + + d[i[0]] = make(dataTree) + d[i[0]].(dataTree).mount(i[1:], v) +} + +// parse incoming dataTree request into JSON (including contentMultipart form dataTree) +func parseUploads(r *http.Request, cfg UploadsConfig) *Uploads { + u := &Uploads{ + cfg: cfg, + tree: make(fileTree), + list: make([]*FileUpload, 0), + } + + for k, v := range r.MultipartForm.File { + files := make([]*FileUpload, 0, len(v)) + for _, f := range v { + files = append(files, NewUpload(f)) + } + + u.list = append(u.list, files...) + u.tree.push(k, files) + } + + return u +} + +// pushes new file upload into it's proper place. +func (d fileTree) push(k string, v []*FileUpload) { + keys := FetchIndexes(k) + if len(keys) <= MaxLevel { + d.mount(keys, v) + } +} + +// mount mounts data tree recursively. +func (d fileTree) mount(i []string, v []*FileUpload) { + if len(i) == 1 { + // single value context + d[i[0]] = v[0] + return + } + + if len(i) == 2 && i[1] == "" { + // non associated array of elements + d[i[0]] = v + return + } + + if p, ok := d[i[0]]; ok { + p.(fileTree).mount(i[1:], v) + return + } + + d[i[0]] = make(fileTree) + d[i[0]].(fileTree).mount(i[1:], v) +} + +// FetchIndexes parses input name and splits it into separate indexes list. +func FetchIndexes(s string) []string { + var ( + pos int + ch string + keys = make([]string, 1) + ) + + for _, c := range s { + ch = string(c) + switch ch { + case " ": + // ignore all spaces + continue + case "[": + pos = 1 + continue + case "]": + if pos == 1 { + keys = append(keys, "") + } + pos = 2 + default: + if pos == 1 || pos == 2 { + keys = append(keys, "") + } + + keys[len(keys)-1] += ch + pos = 0 + } + } + + return keys +} diff --git a/plugins/http/plugin.go b/plugins/http/plugin.go new file mode 100644 index 00000000..f3708ced --- /dev/null +++ b/plugins/http/plugin.go @@ -0,0 +1,528 @@ +package http + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "net/http" + "net/http/fcgi" + "net/url" + "strings" + "sync" + + "github.com/hashicorp/go-multierror" + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/pool" + "github.com/spiral/roadrunner/v2/interfaces/worker" + poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" + "github.com/spiral/roadrunner/v2/plugins/checker" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/http/attributes" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/spiral/roadrunner/v2/util" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + "golang.org/x/sys/cpu" +) + +const ( + // PluginName declares plugin name. + PluginName = "http" + + // EventInitSSL thrown at moment of https initialization. SSL server passed as context. + EventInitSSL = 750 +) + +// Middleware interface +type Middleware interface { + Middleware(f http.Handler) http.HandlerFunc +} + +type middleware map[string]Middleware + +// Service manages pool, http servers. +type Plugin struct { + sync.RWMutex + + configurer config.Configurer + server server.Server + log logger.Logger + + cfg *Config + // middlewares to chain + mdwr middleware + + // Pool which attached to all servers + pool pool.Pool + + // servers RR handler + handler Handle + + // servers + http *http.Server + https *http.Server + fcgi *http.Server +} + +// Init must return configure svc and return true if svc hasStatus enabled. Must return error in case of +// misconfiguration. Services must not be used without proper configuration pushed first. +func (s *Plugin) Init(cfg config.Configurer, log logger.Logger, server server.Server) error { + const op = errors.Op("http Init") + err := cfg.UnmarshalKey(PluginName, &s.cfg) + if err != nil { + return errors.E(op, err) + } + + err = s.cfg.InitDefaults() + if err != nil { + return errors.E(op, err) + } + + s.configurer = cfg + s.log = log + s.mdwr = make(map[string]Middleware) + + if !s.cfg.EnableHTTP() && !s.cfg.EnableTLS() && !s.cfg.EnableFCGI() { + return errors.E(op, errors.Disabled) + } + + s.pool, err = server.NewWorkerPool(context.Background(), poolImpl.Config{ + Debug: s.cfg.Pool.Debug, + NumWorkers: s.cfg.Pool.NumWorkers, + MaxJobs: s.cfg.Pool.MaxJobs, + AllocateTimeout: s.cfg.Pool.AllocateTimeout, + DestroyTimeout: s.cfg.Pool.DestroyTimeout, + Supervisor: s.cfg.Pool.Supervisor, + }, s.cfg.Env, s.logCallback) + if err != nil { + return errors.E(op, err) + } + + s.server = server + + return nil +} + +func (s *Plugin) logCallback(event interface{}) { + if ev, ok := event.(ResponseEvent); ok { + s.log.Debug("http handler response received", "elapsed", ev.Elapsed().String(), "remote address", ev.Request.RemoteAddr) + } +} + +// Serve serves the svc. +func (s *Plugin) Serve() chan error { + s.Lock() + defer s.Unlock() + + const op = errors.Op("serve http") + errCh := make(chan error, 2) + + var err error + s.handler, err = NewHandler( + s.cfg.MaxRequestSize, + *s.cfg.Uploads, + s.cfg.cidrs, + s.pool, + ) + if err != nil { + errCh <- errors.E(op, err) + return errCh + } + + s.handler.AddListener(s.logCallback) + + if s.cfg.EnableHTTP() { + if s.cfg.EnableH2C() { + s.http = &http.Server{Addr: s.cfg.Address, Handler: h2c.NewHandler(s, &http2.Server{})} + } else { + s.http = &http.Server{Addr: s.cfg.Address, Handler: s} + } + } + + if s.cfg.EnableTLS() { + s.https = s.initSSL() + if s.cfg.SSL.RootCA != "" { + err = s.appendRootCa() + if err != nil { + errCh <- errors.E(op, err) + return errCh + } + } + + if s.cfg.EnableHTTP2() { + if err := s.initHTTP2(); err != nil { + errCh <- errors.E(op, err) + return errCh + } + } + } + + if s.cfg.EnableFCGI() { + s.fcgi = &http.Server{Handler: s} + } + + // apply middlewares before starting the server + if len(s.mdwr) > 0 { + s.addMiddlewares() + } + + if s.http != nil { + go func() { + httpErr := s.http.ListenAndServe() + if httpErr != nil && httpErr != http.ErrServerClosed { + errCh <- errors.E(op, httpErr) + return + } + }() + } + + if s.https != nil { + go func() { + httpErr := s.https.ListenAndServeTLS( + s.cfg.SSL.Cert, + s.cfg.SSL.Key, + ) + + if httpErr != nil && httpErr != http.ErrServerClosed { + errCh <- errors.E(op, httpErr) + return + } + }() + } + + if s.fcgi != nil { + go func() { + httpErr := s.serveFCGI() + if httpErr != nil && httpErr != http.ErrServerClosed { + errCh <- errors.E(op, httpErr) + return + } + }() + } + + return errCh +} + +// Stop stops the http. +func (s *Plugin) Stop() error { + s.Lock() + defer s.Unlock() + + var err error + if s.fcgi != nil { + err = s.fcgi.Shutdown(context.Background()) + if err != nil && err != http.ErrServerClosed { + s.log.Error("error shutting down the fcgi server", "error", err) + // write error and try to stop other transport + err = multierror.Append(err) + } + } + + if s.https != nil { + err = s.https.Shutdown(context.Background()) + if err != nil && err != http.ErrServerClosed { + s.log.Error("error shutting down the https server", "error", err) + // write error and try to stop other transport + err = multierror.Append(err) + } + } + + if s.http != nil { + err = s.http.Shutdown(context.Background()) + if err != nil && err != http.ErrServerClosed { + s.log.Error("error shutting down the http server", "error", err) + // write error and try to stop other transport + err = multierror.Append(err) + } + } + + s.pool.Destroy(context.Background()) + + return err +} + +// ServeHTTP handles connection using set of middleware and pool PSR-7 server. +func (s *Plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if headerContainsUpgrade(r, s) { + http.Error(w, "server does not support upgrade header", http.StatusInternalServerError) + return + } + + if s.redirect(w, r) { + return + } + + if s.https != nil && r.TLS != nil { + w.Header().Add("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload") + } + + r = attributes.Init(r) + // protect the case, when user send Reset and we are replacing handler with pool + s.RLock() + s.handler.ServeHTTP(w, r) + s.RUnlock() +} + +// Server returns associated pool workers +func (s *Plugin) Workers() []worker.BaseProcess { + return s.pool.Workers() +} + +func (s *Plugin) Name() string { + return PluginName +} + +func (s *Plugin) Reset() error { + s.Lock() + defer s.Unlock() + const op = errors.Op("http reset") + s.log.Info("HTTP plugin got restart request. Restarting...") + s.pool.Destroy(context.Background()) + s.pool = nil + + // re-read the config + err := s.configurer.UnmarshalKey(PluginName, &s.cfg) + if err != nil { + return errors.E(op, err) + } + + s.pool, err = s.server.NewWorkerPool(context.Background(), poolImpl.Config{ + Debug: s.cfg.Pool.Debug, + NumWorkers: s.cfg.Pool.NumWorkers, + MaxJobs: s.cfg.Pool.MaxJobs, + AllocateTimeout: s.cfg.Pool.AllocateTimeout, + DestroyTimeout: s.cfg.Pool.DestroyTimeout, + Supervisor: s.cfg.Pool.Supervisor, + }, s.cfg.Env, s.logCallback) + if err != nil { + return errors.E(op, err) + } + + s.log.Info("HTTP listeners successfully re-added") + + s.log.Info("HTTP workers Pool successfully restarted") + s.handler, err = NewHandler( + s.cfg.MaxRequestSize, + *s.cfg.Uploads, + s.cfg.cidrs, + s.pool, + ) + if err != nil { + return errors.E(op, err) + } + + s.log.Info("HTTP plugin successfully restarted") + return nil +} + +func (s *Plugin) Collects() []interface{} { + return []interface{}{ + s.AddMiddleware, + } +} + +func (s *Plugin) AddMiddleware(name endure.Named, m Middleware) { + s.mdwr[name.Name()] = m +} + +// Status return status of the particular plugin +func (s *Plugin) Status() checker.Status { + workers := s.Workers() + for i := 0; i < len(workers); i++ { + if workers[i].State().IsActive() { + return checker.Status{ + Code: http.StatusOK, + } + } + } + // if there are no workers, threat this as error + return checker.Status{ + Code: http.StatusInternalServerError, + } +} + +func (s *Plugin) redirect(w http.ResponseWriter, r *http.Request) bool { + if s.https != nil && r.TLS == nil && s.cfg.SSL.Redirect { + target := &url.URL{ + Scheme: "https", + Host: s.tlsAddr(r.Host, false), + Path: r.URL.Path, + RawQuery: r.URL.RawQuery, + } + + http.Redirect(w, r, target.String(), http.StatusTemporaryRedirect) + return true + } + return false +} + +func headerContainsUpgrade(r *http.Request, s *Plugin) bool { + if _, ok := r.Header["Upgrade"]; ok { + // https://golang.org/pkg/net/http/#Hijacker + s.log.Error("server does not support Upgrade header") + return true + } + return false +} + +// append RootCA to the https server TLS config +func (s *Plugin) appendRootCa() error { + const op = errors.Op("append root CA") + rootCAs, err := x509.SystemCertPool() + if err != nil { + return nil + } + if rootCAs == nil { + rootCAs = x509.NewCertPool() + } + + CA, err := ioutil.ReadFile(s.cfg.SSL.RootCA) + if err != nil { + return err + } + + // should append our CA cert + ok := rootCAs.AppendCertsFromPEM(CA) + if !ok { + return errors.E(op, errors.Str("could not append Certs from PEM")) + } + // disable "G402 (CWE-295): TLS MinVersion too low. (Confidence: HIGH, Severity: HIGH)" + // #nosec G402 + cfg := &tls.Config{ + InsecureSkipVerify: false, + RootCAs: rootCAs, + } + s.http.TLSConfig = cfg + + return nil +} + +// Init https server +func (s *Plugin) initSSL() *http.Server { + var topCipherSuites []uint16 + var defaultCipherSuitesTLS13 []uint16 + + hasGCMAsmAMD64 := cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ + hasGCMAsmARM64 := cpu.ARM64.HasAES && cpu.ARM64.HasPMULL + // Keep in sync with crypto/aes/cipher_s390x.go. + hasGCMAsmS390X := cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasAESCTR && (cpu.S390X.HasGHASH || cpu.S390X.HasAESGCM) + + hasGCMAsm := hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X + + if hasGCMAsm { + // If AES-GCM hardware is provided then prioritise AES-GCM + // cipher suites. + topCipherSuites = []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + } + defaultCipherSuitesTLS13 = []uint16{ + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_AES_256_GCM_SHA384, + } + } else { + // Without AES-GCM hardware, we put the ChaCha20-Poly1305 + // cipher suites first. + topCipherSuites = []uint16{ + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + } + defaultCipherSuitesTLS13 = []uint16{ + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + } + } + + DefaultCipherSuites := make([]uint16, 0, 22) + DefaultCipherSuites = append(DefaultCipherSuites, topCipherSuites...) + DefaultCipherSuites = append(DefaultCipherSuites, defaultCipherSuitesTLS13...) + + server := &http.Server{ + Addr: s.tlsAddr(s.cfg.Address, true), + Handler: s, + TLSConfig: &tls.Config{ + CurvePreferences: []tls.CurveID{ + tls.CurveP256, + tls.CurveP384, + tls.CurveP521, + tls.X25519, + }, + CipherSuites: DefaultCipherSuites, + MinVersion: tls.VersionTLS12, + PreferServerCipherSuites: true, + }, + } + + return server +} + +// init http/2 server +func (s *Plugin) initHTTP2() error { + return http2.ConfigureServer(s.https, &http2.Server{ + MaxConcurrentStreams: s.cfg.HTTP2.MaxConcurrentStreams, + }) +} + +// serveFCGI starts FastCGI server. +func (s *Plugin) serveFCGI() error { + l, err := util.CreateListener(s.cfg.FCGI.Address) + if err != nil { + return err + } + + err = fcgi.Serve(l, s.fcgi.Handler) + if err != nil { + return err + } + + return nil +} + +// tlsAddr replaces listen or host port with port configured by SSL config. +func (s *Plugin) tlsAddr(host string, forcePort bool) string { + // remove current forcePort first + host = strings.Split(host, ":")[0] + + if forcePort || s.cfg.SSL.Port != 443 { + host = fmt.Sprintf("%s:%v", host, s.cfg.SSL.Port) + } + + return host +} + +func (s *Plugin) addMiddlewares() { + if s.http != nil { + applyMiddlewares(s.http, s.mdwr, s.cfg.Middleware, s.log) + } + if s.https != nil { + applyMiddlewares(s.https, s.mdwr, s.cfg.Middleware, s.log) + } + + if s.fcgi != nil { + applyMiddlewares(s.fcgi, s.mdwr, s.cfg.Middleware, s.log) + } +} + +func applyMiddlewares(server *http.Server, middlewares map[string]Middleware, order []string, log logger.Logger) { + for i := 0; i < len(order); i++ { + if mdwr, ok := middlewares[order[i]]; ok { + server.Handler = mdwr.Middleware(server.Handler) + } else { + log.Warn("requested middleware does not exist", "requested", order[i]) + } + } +} diff --git a/plugins/http/request.go b/plugins/http/request.go new file mode 100644 index 00000000..3983fdde --- /dev/null +++ b/plugins/http/request.go @@ -0,0 +1,186 @@ +package http + +import ( + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "strings" + + j "github.com/json-iterator/go" + "github.com/spiral/roadrunner/v2/pkg/payload" + "github.com/spiral/roadrunner/v2/plugins/http/attributes" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +var json = j.ConfigCompatibleWithStandardLibrary + +const ( + defaultMaxMemory = 32 << 20 // 32 MB + contentNone = iota + 900 + contentStream + contentMultipart + contentFormData +) + +// Request maps net/http requests to PSR7 compatible structure and managed state of temporary uploaded files. +type Request struct { + // RemoteAddr contains ip address of client, make sure to check X-Real-Ip and X-Forwarded-For for real client address. + RemoteAddr string `json:"remoteAddr"` + + // Protocol includes HTTP protocol version. + Protocol string `json:"protocol"` + + // Method contains name of HTTP method used for the request. + Method string `json:"method"` + + // URI contains full request URI with scheme and query. + URI string `json:"uri"` + + // Header contains list of request headers. + Header http.Header `json:"headers"` + + // Cookies contains list of request cookies. + Cookies map[string]string `json:"cookies"` + + // RawQuery contains non parsed query string (to be parsed on php end). + RawQuery string `json:"rawQuery"` + + // Parsed indicates that request body has been parsed on RR end. + Parsed bool `json:"parsed"` + + // Uploads contains list of uploaded files, their names, sized and associations with temporary files. + Uploads *Uploads `json:"uploads"` + + // Attributes can be set by chained mdwr to safely pass value from Golang to PHP. See: GetAttribute, SetAttribute functions. + Attributes map[string]interface{} `json:"attributes"` + + // request body can be parsedData or []byte + body interface{} +} + +func fetchIP(pair string) string { + if !strings.ContainsRune(pair, ':') { + return pair + } + + addr, _, _ := net.SplitHostPort(pair) + return addr +} + +// NewRequest creates new PSR7 compatible request using net/http request. +func NewRequest(r *http.Request, cfg UploadsConfig) (*Request, error) { + req := &Request{ + RemoteAddr: fetchIP(r.RemoteAddr), + Protocol: r.Proto, + Method: r.Method, + URI: uri(r), + Header: r.Header, + Cookies: make(map[string]string), + RawQuery: r.URL.RawQuery, + Attributes: attributes.All(r), + } + + for _, c := range r.Cookies() { + if v, err := url.QueryUnescape(c.Value); err == nil { + req.Cookies[c.Name] = v + } + } + + switch req.contentType() { + case contentNone: + return req, nil + + case contentStream: + var err error + req.body, err = ioutil.ReadAll(r.Body) + return req, err + + case contentMultipart: + if err := r.ParseMultipartForm(defaultMaxMemory); err != nil { + return nil, err + } + + req.Uploads = parseUploads(r, cfg) + fallthrough + case contentFormData: + if err := r.ParseForm(); err != nil { + return nil, err + } + + req.body = parseData(r) + } + + req.Parsed = true + return req, nil +} + +// Open moves all uploaded files to temporary directory so it can be given to php later. +func (r *Request) Open(log logger.Logger) { + if r.Uploads == nil { + return + } + + r.Uploads.Open(log) +} + +// Close clears all temp file uploads +func (r *Request) Close(log logger.Logger) { + if r.Uploads == nil { + return + } + + r.Uploads.Clear(log) +} + +// Payload request marshaled RoadRunner payload based on PSR7 data. values encode method is JSON. Make sure to open +// files prior to calling this method. +func (r *Request) Payload() (payload.Payload, error) { + p := payload.Payload{} + + var err error + if p.Context, err = json.Marshal(r); err != nil { + return payload.Payload{}, err + } + + if r.Parsed { + if p.Body, err = json.Marshal(r.body); err != nil { + return payload.Payload{}, err + } + } else if r.body != nil { + p.Body = r.body.([]byte) + } + + return p, nil +} + +// contentType returns the payload content type. +func (r *Request) contentType() int { + if r.Method == "HEAD" || r.Method == "OPTIONS" { + return contentNone + } + + ct := r.Header.Get("content-type") + if strings.Contains(ct, "application/x-www-form-urlencoded") { + return contentFormData + } + + if strings.Contains(ct, "multipart/form-data") { + return contentMultipart + } + + return contentStream +} + +// uri fetches full uri from request in a form of string (including https scheme if TLS connection is enabled). +func uri(r *http.Request) string { + if r.URL.Host != "" { + return r.URL.String() + } + if r.TLS != nil { + return fmt.Sprintf("https://%s%s", r.Host, r.URL.String()) + } + + return fmt.Sprintf("http://%s%s", r.Host, r.URL.String()) +} diff --git a/plugins/http/response.go b/plugins/http/response.go new file mode 100644 index 00000000..17049ce1 --- /dev/null +++ b/plugins/http/response.go @@ -0,0 +1,105 @@ +package http + +import ( + "io" + "net/http" + "strings" + "sync" + + "github.com/spiral/roadrunner/v2/pkg/payload" +) + +// Response handles PSR7 response logic. +type Response struct { + // Status contains response status. + Status int `json:"status"` + + // Header contains list of response headers. + Headers map[string][]string `json:"headers"` + + // associated Body payload. + Body interface{} + sync.Mutex +} + +// NewResponse creates new response based on given pool payload. +func NewResponse(p payload.Payload) (*Response, error) { + r := &Response{Body: p.Body} + if err := json.Unmarshal(p.Context, r); err != nil { + return nil, err + } + + return r, nil +} + +// Write writes response headers, status and body into ResponseWriter. +func (r *Response) Write(w http.ResponseWriter) error { + // INFO map is the reference type in golang + p := handlePushHeaders(r.Headers) + if pusher, ok := w.(http.Pusher); ok { + for _, v := range p { + err := pusher.Push(v, nil) + if err != nil { + return err + } + } + } + + handleTrailers(r.Headers) + for n, h := range r.Headers { + for _, v := range h { + w.Header().Add(n, v) + } + } + + w.WriteHeader(r.Status) + + if data, ok := r.Body.([]byte); ok { + _, err := w.Write(data) + if err != nil { + return handleWriteError(err) + } + } + + if rc, ok := r.Body.(io.Reader); ok { + if _, err := io.Copy(w, rc); err != nil { + return err + } + } + + return nil +} + +func handlePushHeaders(h map[string][]string) []string { + var p []string + pushHeader, ok := h[http2pushHeaderKey] + if !ok { + return p + } + + p = append(p, pushHeader...) + + delete(h, http2pushHeaderKey) + + return p +} + +func handleTrailers(h map[string][]string) { + trailers, ok := h[TrailerHeaderKey] + if !ok { + return + } + + for _, tr := range trailers { + for _, n := range strings.Split(tr, ",") { + n = strings.Trim(n, "\t ") + if v, ok := h[n]; ok { + h["Trailer:"+n] = v + + delete(h, n) + } + } + } + + delete(h, TrailerHeaderKey) +} diff --git a/plugins/http/uploads.go b/plugins/http/uploads.go new file mode 100644 index 00000000..d5196844 --- /dev/null +++ b/plugins/http/uploads.go @@ -0,0 +1,158 @@ +package http + +import ( + "github.com/spiral/roadrunner/v2/plugins/logger" + + "io" + "io/ioutil" + "mime/multipart" + "os" + "sync" +) + +const ( + // UploadErrorOK - no error, the file uploaded with success. + UploadErrorOK = 0 + + // UploadErrorNoFile - no file was uploaded. + UploadErrorNoFile = 4 + + // UploadErrorNoTmpDir - missing a temporary folder. + UploadErrorNoTmpDir = 6 + + // UploadErrorCantWrite - failed to write file to disk. + UploadErrorCantWrite = 7 + + // UploadErrorExtension - forbidden file extension. + UploadErrorExtension = 8 +) + +// Uploads tree manages uploaded files tree and temporary files. +type Uploads struct { + // associated temp directory and forbidden extensions. + cfg UploadsConfig + + // pre processed data tree for Uploads. + tree fileTree + + // flat list of all file Uploads. + list []*FileUpload +} + +// MarshalJSON marshal tree tree into JSON. +func (u *Uploads) MarshalJSON() ([]byte, error) { + return json.Marshal(u.tree) +} + +// Open moves all uploaded files to temp directory, return error in case of issue with temp directory. File errors +// will be handled individually. +func (u *Uploads) Open(log logger.Logger) { + var wg sync.WaitGroup + for _, f := range u.list { + wg.Add(1) + go func(f *FileUpload) { + defer wg.Done() + err := f.Open(u.cfg) + if err != nil && log != nil { + log.Error("error opening the file", "err", err) + } + }(f) + } + + wg.Wait() +} + +// Clear deletes all temporary files. +func (u *Uploads) Clear(log logger.Logger) { + for _, f := range u.list { + if f.TempFilename != "" && exists(f.TempFilename) { + err := os.Remove(f.TempFilename) + if err != nil && log != nil { + log.Error("error removing the file", "err", err) + } + } + } +} + +// FileUpload represents singular file NewUpload. +type FileUpload struct { + // ID contains filename specified by the client. + Name string `json:"name"` + + // Mime contains mime-type provided by the client. + Mime string `json:"mime"` + + // Size of the uploaded file. + Size int64 `json:"size"` + + // Error indicates file upload error (if any). See http://php.net/manual/en/features.file-upload.errors.php + Error int `json:"error"` + + // TempFilename points to temporary file location. + TempFilename string `json:"tmpName"` + + // associated file header + header *multipart.FileHeader +} + +// NewUpload wraps net/http upload into PRS-7 compatible structure. +func NewUpload(f *multipart.FileHeader) *FileUpload { + return &FileUpload{ + Name: f.Filename, + Mime: f.Header.Get("Content-Type"), + Error: UploadErrorOK, + header: f, + } +} + +// Open moves file content into temporary file available for PHP. +// NOTE: +// There is 2 deferred functions, and in case of getting 2 errors from both functions +// error from close of temp file would be overwritten by error from the main file +// STACK +// DEFER FILE CLOSE (2) +// DEFER TMP CLOSE (1) +func (f *FileUpload) Open(cfg UploadsConfig) (err error) { + if cfg.Forbids(f.Name) { + f.Error = UploadErrorExtension + return nil + } + + file, err := f.header.Open() + if err != nil { + f.Error = UploadErrorNoFile + return err + } + + defer func() { + // close the main file + err = file.Close() + }() + + tmp, err := ioutil.TempFile(cfg.TmpDir(), "upload") + if err != nil { + // most likely cause of this issue is missing tmp dir + f.Error = UploadErrorNoTmpDir + return err + } + + f.TempFilename = tmp.Name() + defer func() { + // close the temp file + err = tmp.Close() + }() + + if f.Size, err = io.Copy(tmp, file); err != nil { + f.Error = UploadErrorCantWrite + } + + return err +} + +// exists if file exists. +func exists(path string) bool { + if _, err := os.Stat(path); os.IsNotExist(err) { + return false + } + return true +} diff --git a/plugins/http/uploads_config.go b/plugins/http/uploads_config.go new file mode 100644 index 00000000..4c20c8e8 --- /dev/null +++ b/plugins/http/uploads_config.go @@ -0,0 +1,46 @@ +package http + +import ( + "os" + "path" + "strings" +) + +// UploadsConfig describes file location and controls access to them. +type UploadsConfig struct { + // Dir contains name of directory to control access to. + Dir string + + // Forbid specifies list of file extensions which are forbidden for access. + // Example: .php, .exe, .bat, .htaccess and etc. + Forbid []string +} + +// InitDefaults sets missing values to their default values. +func (cfg *UploadsConfig) InitDefaults() error { + cfg.Forbid = []string{".php", ".exe", ".bat"} + cfg.Dir = os.TempDir() + return nil +} + +// TmpDir returns temporary directory. +func (cfg *UploadsConfig) TmpDir() string { + if cfg.Dir != "" { + return cfg.Dir + } + + return os.TempDir() +} + +// Forbids must return true if file extension is not allowed for the upload. +func (cfg *UploadsConfig) Forbids(filename string) bool { + ext := strings.ToLower(path.Ext(filename)) + + for _, v := range cfg.Forbid { + if ext == v { + return true + } + } + + return false +} diff --git a/plugins/informer/interface.go b/plugins/informer/interface.go new file mode 100644 index 00000000..27139ae1 --- /dev/null +++ b/plugins/informer/interface.go @@ -0,0 +1,8 @@ +package informer + +import "github.com/spiral/roadrunner/v2/interfaces/worker" + +// Informer used to get workers from particular plugin or set of plugins +type Informer interface { + Workers() []worker.BaseProcess +} diff --git a/plugins/informer/plugin.go b/plugins/informer/plugin.go new file mode 100644 index 00000000..3359cd7e --- /dev/null +++ b/plugins/informer/plugin.go @@ -0,0 +1,55 @@ +package informer + +import ( + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/worker" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +const PluginName = "informer" + +type Plugin struct { + registry map[string]Informer + log logger.Logger +} + +func (p *Plugin) Init(log logger.Logger) error { + p.registry = make(map[string]Informer) + p.log = log + return nil +} + +// Workers provides BaseProcess slice with workers for the requested plugin +func (p *Plugin) Workers(name string) ([]worker.BaseProcess, error) { + const op = errors.Op("get workers") + svc, ok := p.registry[name] + if !ok { + return nil, errors.E(op, errors.Errorf("no such service: %s", name)) + } + + return svc.Workers(), nil +} + +// CollectTarget resettable service. +func (p *Plugin) CollectTarget(name endure.Named, r Informer) error { + p.registry[name.Name()] = r + return nil +} + +// Collects declares services to be collected. +func (p *Plugin) Collects() []interface{} { + return []interface{}{ + p.CollectTarget, + } +} + +// Name of the service. +func (p *Plugin) Name() string { + return PluginName +} + +// RPCService returns associated rpc service. +func (p *Plugin) RPC() interface{} { + return &rpc{srv: p, log: p.log} +} diff --git a/plugins/informer/rpc.go b/plugins/informer/rpc.go new file mode 100644 index 00000000..98b5681c --- /dev/null +++ b/plugins/informer/rpc.go @@ -0,0 +1,54 @@ +package informer + +import ( + "github.com/spiral/roadrunner/v2/interfaces/worker" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/tools" +) + +type rpc struct { + srv *Plugin + log logger.Logger +} + +// WorkerList contains list of workers. +type WorkerList struct { + // Workers is list of workers. + Workers []tools.ProcessState `json:"workers"` +} + +// List all resettable services. +func (rpc *rpc) List(_ bool, list *[]string) error { + rpc.log.Debug("Started List method") + *list = make([]string, 0, len(rpc.srv.registry)) + + for name := range rpc.srv.registry { + *list = append(*list, name) + } + rpc.log.Debug("list of services", "list", *list) + + rpc.log.Debug("successfully finished List method") + return nil +} + +// Workers state of a given service. +func (rpc *rpc) Workers(service string, list *WorkerList) error { + rpc.log.Debug("started Workers method", "service", service) + workers, err := rpc.srv.Workers(service) + if err != nil { + return err + } + + list.Workers = make([]tools.ProcessState, 0) + for _, w := range workers { + ps, err := tools.WorkerProcessState(w.(worker.BaseProcess)) + if err != nil { + continue + } + + list.Workers = append(list.Workers, ps) + } + rpc.log.Debug("list of workers", "workers", list.Workers) + rpc.log.Debug("successfully finished Workers method") + return nil +} diff --git a/plugins/logger/config.go b/plugins/logger/config.go new file mode 100644 index 00000000..f7a5742c --- /dev/null +++ b/plugins/logger/config.go @@ -0,0 +1,94 @@ +package logger + +import ( + "strings" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// ChannelConfig configures loggers per channel. +type ChannelConfig struct { + // Dedicated channels per logger. By default logger allocated via named logger. + Channels map[string]Config `json:"channels" yaml:"channels"` +} + +type Config struct { + // Mode configures logger based on some default template (development, production, off). + Mode string `json:"mode" yaml:"mode"` + + // Level is the minimum enabled logging level. Note that this is a dynamic + // level, so calling ChannelConfig.Level.SetLevel will atomically change the log + // level of all loggers descended from this config. + Level string `json:"level" yaml:"level"` + + // Encoding sets the logger's encoding. Valid values are "json" and + // "console", as well as any third-party encodings registered via + // RegisterEncoder. + Encoding string `json:"encoding" yaml:"encoding"` + + // Output is a list of URLs or file paths to write logging output to. + // See Open for details. + Output []string `json:"output" yaml:"output"` + + // ErrorOutput is a list of URLs to write internal logger errors to. + // The default is standard error. + // + // Note that this setting only affects internal errors; for sample code that + // sends error-level logs to a different location from info- and debug-level + // logs, see the package-level AdvancedConfiguration example. + ErrorOutput []string `json:"errorOutput" yaml:"errorOutput"` +} + +// ZapConfig converts config into Zap configuration. +func (cfg *Config) BuildLogger() (*zap.Logger, error) { + var zCfg zap.Config + switch strings.ToLower(cfg.Mode) { + case "off", "none": + return zap.NewNop(), nil + case "production": + zCfg = zap.NewProductionConfig() + case "development": + zCfg = zap.NewDevelopmentConfig() + default: + zCfg = zap.Config{ + Level: zap.NewAtomicLevelAt(zap.DebugLevel), + Encoding: "console", + EncoderConfig: zapcore.EncoderConfig{ + MessageKey: "message", + LevelKey: "level", + TimeKey: "time", + NameKey: "name", + EncodeName: ColoredHashedNameEncoder, + EncodeLevel: ColoredLevelEncoder, + EncodeTime: UTCTimeEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + }, + OutputPaths: []string{"stderr"}, + ErrorOutputPaths: []string{"stderr"}, + } + } + + if cfg.Level != "" { + level := zap.NewAtomicLevel() + if err := level.UnmarshalText([]byte(cfg.Level)); err == nil { + zCfg.Level = level + } + } + + if cfg.Encoding != "" { + zCfg.Encoding = cfg.Encoding + } + + if len(cfg.Output) != 0 { + zCfg.OutputPaths = cfg.Output + } + + if len(cfg.ErrorOutput) != 0 { + zCfg.ErrorOutputPaths = cfg.ErrorOutput + } + + // todo: https://github.com/uber-go/zap/blob/master/FAQ.md#does-zap-support-log-rotation + + return zCfg.Build() +} diff --git a/plugins/logger/encoder.go b/plugins/logger/encoder.go new file mode 100644 index 00000000..4ff583c4 --- /dev/null +++ b/plugins/logger/encoder.go @@ -0,0 +1,66 @@ +package logger + +import ( + "hash/fnv" + "strings" + "time" + + "github.com/fatih/color" + "go.uber.org/zap/zapcore" +) + +var colorMap = []func(string, ...interface{}) string{ + color.HiYellowString, + color.HiGreenString, + color.HiBlueString, + color.HiRedString, + color.HiCyanString, + color.HiMagentaString, +} + +// ColoredLevelEncoder colorizes log levels. +func ColoredLevelEncoder(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { + switch level { + case zapcore.DebugLevel: + enc.AppendString(color.HiWhiteString(level.CapitalString())) + case zapcore.InfoLevel: + enc.AppendString(color.HiCyanString(level.CapitalString())) + case zapcore.WarnLevel: + enc.AppendString(color.HiYellowString(level.CapitalString())) + case zapcore.ErrorLevel, zapcore.DPanicLevel: + enc.AppendString(color.HiRedString(level.CapitalString())) + case zapcore.PanicLevel, zapcore.FatalLevel: + enc.AppendString(color.HiMagentaString(level.CapitalString())) + } +} + +// ColoredNameEncoder colorizes service names. +func ColoredNameEncoder(s string, enc zapcore.PrimitiveArrayEncoder) { + if len(s) < 12 { + s += strings.Repeat(" ", 12-len(s)) + } + + enc.AppendString(color.HiGreenString(s)) +} + +// ColoredHashedNameEncoder colorizes service names and assigns different colors to different names. +func ColoredHashedNameEncoder(s string, enc zapcore.PrimitiveArrayEncoder) { + if len(s) < 12 { + s += strings.Repeat(" ", 12-len(s)) + } + + colorID := stringHash(s, len(colorMap)) + enc.AppendString(colorMap[colorID](s)) +} + +// UTCTimeEncoder encodes time into short UTC specific timestamp. +func UTCTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.UTC().Format("2006/01/02 15:04:05")) +} + +// returns string hash +func stringHash(name string, base int) int { + h := fnv.New32a() + _, _ = h.Write([]byte(name)) + return int(h.Sum32()) % base +} diff --git a/plugins/logger/interface.go b/plugins/logger/interface.go new file mode 100644 index 00000000..876629a9 --- /dev/null +++ b/plugins/logger/interface.go @@ -0,0 +1,16 @@ +package logger + +type ( + // Logger is an general RR log interface + Logger interface { + Debug(msg string, keyvals ...interface{}) + Info(msg string, keyvals ...interface{}) + Warn(msg string, keyvals ...interface{}) + Error(msg string, keyvals ...interface{}) + } +) + +// With creates a child logger and adds structured context to it +type WithLogger interface { + With(keyvals ...interface{}) Logger +} diff --git a/plugins/logger/plugin.go b/plugins/logger/plugin.go new file mode 100644 index 00000000..01bf5cc0 --- /dev/null +++ b/plugins/logger/plugin.go @@ -0,0 +1,69 @@ +package logger + +import ( + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "go.uber.org/zap" +) + +// PluginName declares plugin name. +const PluginName = "logs" + +// ZapLogger manages zap logger. +type ZapLogger struct { + base *zap.Logger + cfg Config + channels ChannelConfig +} + +// Init logger service. +func (z *ZapLogger) Init(cfg config.Configurer) error { + const op = errors.Op("zap logger init") + err := cfg.UnmarshalKey(PluginName, &z.cfg) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + err = cfg.UnmarshalKey(PluginName, &z.channels) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + z.base, err = z.cfg.BuildLogger() + if err != nil { + return errors.E(op, errors.Disabled, err) + } + return nil +} + +// DefaultLogger returns default logger. +func (z *ZapLogger) DefaultLogger() (Logger, error) { + return NewZapAdapter(z.base), nil +} + +// NamedLogger returns logger dedicated to the specific channel. Similar to Named() but also reads the core params. +func (z *ZapLogger) NamedLogger(name string) (Logger, error) { + if cfg, ok := z.channels.Channels[name]; ok { + l, err := cfg.BuildLogger() + if err != nil { + return nil, err + } + return NewZapAdapter(l), nil + } + + return NewZapAdapter(z.base.Named(name)), nil +} + +// NamedLogger returns logger dedicated to the specific channel. Similar to Named() but also reads the core params. +func (z *ZapLogger) ServiceLogger(n endure.Named) (Logger, error) { + return z.NamedLogger(n.Name()) +} + +// Provides declares factory methods. +func (z *ZapLogger) Provides() []interface{} { + return []interface{}{ + z.ServiceLogger, + z.DefaultLogger, + } +} diff --git a/plugins/logger/zap_adapter.go b/plugins/logger/zap_adapter.go new file mode 100644 index 00000000..0a0855b8 --- /dev/null +++ b/plugins/logger/zap_adapter.go @@ -0,0 +1,56 @@ +package logger + +import ( + "fmt" + + "go.uber.org/zap" +) + +type ZapAdapter struct { + zl *zap.Logger +} + +// Create NewZapAdapter which uses general log interface +func NewZapAdapter(zapLogger *zap.Logger) *ZapAdapter { + return &ZapAdapter{ + zl: zapLogger.WithOptions(zap.AddCallerSkip(1)), + } +} + +func (log *ZapAdapter) fields(keyvals []interface{}) []zap.Field { + // we should have even number of keys and values + if len(keyvals)%2 != 0 { + return []zap.Field{zap.Error(fmt.Errorf("odd number of keyvals pairs: %v", keyvals))} + } + + var fields []zap.Field + for i := 0; i < len(keyvals); i += 2 { + key, ok := keyvals[i].(string) + if !ok { + key = fmt.Sprintf("%v", keyvals[i]) + } + fields = append(fields, zap.Any(key, keyvals[i+1])) + } + + return fields +} + +func (log *ZapAdapter) Debug(msg string, keyvals ...interface{}) { + log.zl.Debug(msg, log.fields(keyvals)...) +} + +func (log *ZapAdapter) Info(msg string, keyvals ...interface{}) { + log.zl.Info(msg, log.fields(keyvals)...) +} + +func (log *ZapAdapter) Warn(msg string, keyvals ...interface{}) { + log.zl.Warn(msg, log.fields(keyvals)...) +} + +func (log *ZapAdapter) Error(msg string, keyvals ...interface{}) { + log.zl.Error(msg, log.fields(keyvals)...) +} + +func (log *ZapAdapter) With(keyvals ...interface{}) Logger { + return NewZapAdapter(log.zl.With(log.fields(keyvals)...)) +} diff --git a/plugins/metrics/config.go b/plugins/metrics/config.go new file mode 100644 index 00000000..9459bc9b --- /dev/null +++ b/plugins/metrics/config.go @@ -0,0 +1,138 @@ +package metrics + +import ( + "fmt" + + "github.com/prometheus/client_golang/prometheus" +) + +// Config configures metrics service. +type Config struct { + // Address to listen + Address string + + // Collect define application specific metrics. + Collect map[string]Collector +} + +type NamedCollector struct { + // Name of the collector + Name string `json:"name"` + + // Collector structure + Collector `json:"collector"` +} + +// CollectorType represents prometheus collector types +type CollectorType string + +const ( + // Histogram type + Histogram CollectorType = "histogram" + + // Gauge type + Gauge CollectorType = "gauge" + + // Counter type + Counter CollectorType = "counter" + + // Summary type + Summary CollectorType = "summary" +) + +// Collector describes single application specific metric. +type Collector struct { + // Namespace of the metric. + Namespace string `json:"namespace"` + // Subsystem of the metric. + Subsystem string `json:"subsystem"` + // Collector type (histogram, gauge, counter, summary). + Type CollectorType `json:"type"` + // Help of collector. + Help string `json:"help"` + // Labels for vectorized metrics. + Labels []string `json:"labels"` + // Buckets for histogram metric. + Buckets []float64 `json:"buckets"` + // Objectives for the summary opts + Objectives map[float64]float64 `json:"objectives"` +} + +// register application specific metrics. +func (c *Config) getCollectors() (map[string]prometheus.Collector, error) { + if c.Collect == nil { + return nil, nil + } + + collectors := make(map[string]prometheus.Collector) + + for name, m := range c.Collect { + var collector prometheus.Collector + switch m.Type { + case Histogram: + opts := prometheus.HistogramOpts{ + Name: name, + Namespace: m.Namespace, + Subsystem: m.Subsystem, + Help: m.Help, + Buckets: m.Buckets, + } + + if len(m.Labels) != 0 { + collector = prometheus.NewHistogramVec(opts, m.Labels) + } else { + collector = prometheus.NewHistogram(opts) + } + case Gauge: + opts := prometheus.GaugeOpts{ + Name: name, + Namespace: m.Namespace, + Subsystem: m.Subsystem, + Help: m.Help, + } + + if len(m.Labels) != 0 { + collector = prometheus.NewGaugeVec(opts, m.Labels) + } else { + collector = prometheus.NewGauge(opts) + } + case Counter: + opts := prometheus.CounterOpts{ + Name: name, + Namespace: m.Namespace, + Subsystem: m.Subsystem, + Help: m.Help, + } + + if len(m.Labels) != 0 { + collector = prometheus.NewCounterVec(opts, m.Labels) + } else { + collector = prometheus.NewCounter(opts) + } + case Summary: + opts := prometheus.SummaryOpts{ + Name: name, + Namespace: m.Namespace, + Subsystem: m.Subsystem, + Help: m.Help, + Objectives: m.Objectives, + } + + if len(m.Labels) != 0 { + collector = prometheus.NewSummaryVec(opts, m.Labels) + } else { + collector = prometheus.NewSummary(opts) + } + default: + return nil, fmt.Errorf("invalid metric type `%s` for `%s`", m.Type, name) + } + + collectors[name] = collector + } + + return collectors, nil +} + +func (c *Config) InitDefaults() { + +} diff --git a/plugins/metrics/config_test.go b/plugins/metrics/config_test.go new file mode 100644 index 00000000..665ec9cd --- /dev/null +++ b/plugins/metrics/config_test.go @@ -0,0 +1,89 @@ +package metrics + +import ( + "bytes" + "testing" + + j "github.com/json-iterator/go" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" +) + +var json = j.ConfigCompatibleWithStandardLibrary + +func Test_Config_Hydrate_Error1(t *testing.T) { + cfg := `{"request": {"From": "Something"}}` + c := &Config{} + f := new(bytes.Buffer) + f.WriteString(cfg) + + err := json.Unmarshal(f.Bytes(), &c) + if err != nil { + t.Fatal(err) + } +} + +func Test_Config_Hydrate_Error2(t *testing.T) { + cfg := `{"dir": "/dir/"` + c := &Config{} + + f := new(bytes.Buffer) + f.WriteString(cfg) + + err := json.Unmarshal(f.Bytes(), &c) + assert.Error(t, err) +} + +func Test_Config_Metrics(t *testing.T) { + cfg := `{ +"collect":{ + "metric1":{"type": "gauge"}, + "metric2":{ "type": "counter"}, + "metric3":{"type": "summary"}, + "metric4":{"type": "histogram"} +} +}` + c := &Config{} + f := new(bytes.Buffer) + f.WriteString(cfg) + + err := json.Unmarshal(f.Bytes(), &c) + if err != nil { + t.Fatal(err) + } + + m, err := c.getCollectors() + assert.NoError(t, err) + + assert.IsType(t, prometheus.NewGauge(prometheus.GaugeOpts{}), m["metric1"]) + assert.IsType(t, prometheus.NewCounter(prometheus.CounterOpts{}), m["metric2"]) + assert.IsType(t, prometheus.NewSummary(prometheus.SummaryOpts{}), m["metric3"]) + assert.IsType(t, prometheus.NewHistogram(prometheus.HistogramOpts{}), m["metric4"]) +} + +func Test_Config_MetricsVector(t *testing.T) { + cfg := `{ +"collect":{ + "metric1":{"type": "gauge","labels":["label"]}, + "metric2":{ "type": "counter","labels":["label"]}, + "metric3":{"type": "summary","labels":["label"]}, + "metric4":{"type": "histogram","labels":["label"]} +} +}` + c := &Config{} + f := new(bytes.Buffer) + f.WriteString(cfg) + + err := json.Unmarshal(f.Bytes(), &c) + if err != nil { + t.Fatal(err) + } + + m, err := c.getCollectors() + assert.NoError(t, err) + + assert.IsType(t, prometheus.NewGaugeVec(prometheus.GaugeOpts{}, []string{}), m["metric1"]) + assert.IsType(t, prometheus.NewCounterVec(prometheus.CounterOpts{}, []string{}), m["metric2"]) + assert.IsType(t, prometheus.NewSummaryVec(prometheus.SummaryOpts{}, []string{}), m["metric3"]) + assert.IsType(t, prometheus.NewHistogramVec(prometheus.HistogramOpts{}, []string{}), m["metric4"]) +} diff --git a/plugins/metrics/doc.go b/plugins/metrics/doc.go new file mode 100644 index 00000000..1abe097a --- /dev/null +++ b/plugins/metrics/doc.go @@ -0,0 +1 @@ +package metrics diff --git a/plugins/metrics/interface.go b/plugins/metrics/interface.go new file mode 100644 index 00000000..87ba4017 --- /dev/null +++ b/plugins/metrics/interface.go @@ -0,0 +1,7 @@ +package metrics + +import "github.com/prometheus/client_golang/prometheus" + +type StatProvider interface { + MetricsCollector() []prometheus.Collector +} diff --git a/plugins/metrics/plugin.go b/plugins/metrics/plugin.go new file mode 100644 index 00000000..fb9096a1 --- /dev/null +++ b/plugins/metrics/plugin.go @@ -0,0 +1,229 @@ +package metrics + +import ( + "context" + "crypto/tls" + "net/http" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "golang.org/x/sys/cpu" +) + +const ( + // PluginName declares plugin name. + PluginName = "metrics" + // maxHeaderSize declares max header size for prometheus server + maxHeaderSize = 1024 * 1024 * 100 // 104MB +) + +type statsProvider struct { + collectors []prometheus.Collector + name string +} + +// Plugin to manage application metrics using Prometheus. +type Plugin struct { + cfg Config + log logger.Logger + mu sync.Mutex // all receivers are pointers + http *http.Server + collectors sync.Map // all receivers are pointers + registry *prometheus.Registry +} + +// Init service. +func (m *Plugin) Init(cfg config.Configurer, log logger.Logger) error { + const op = errors.Op("metrics init") + err := cfg.UnmarshalKey(PluginName, &m.cfg) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + // TODO figure out what is Init + m.cfg.InitDefaults() + + m.log = log + m.registry = prometheus.NewRegistry() + + // Default + err = m.registry.Register(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{})) + if err != nil { + return errors.E(op, err) + } + + // Default + err = m.registry.Register(prometheus.NewGoCollector()) + if err != nil { + return errors.E(op, err) + } + + collectors, err := m.cfg.getCollectors() + if err != nil { + return errors.E(op, err) + } + + // Register invocation will be later in the Serve method + for k, v := range collectors { + m.collectors.Store(k, statsProvider{ + collectors: []prometheus.Collector{v}, + name: k, + }) + } + return nil +} + +// Register new prometheus collector. +func (m *Plugin) Register(c prometheus.Collector) error { + return m.registry.Register(c) +} + +// Serve prometheus metrics service. +func (m *Plugin) Serve() chan error { + errCh := make(chan error, 1) + m.collectors.Range(func(key, value interface{}) bool { + // key - name + // value - statsProvider struct + c := value.(statsProvider) + for _, v := range c.collectors { + if err := m.registry.Register(v); err != nil { + errCh <- err + return false + } + } + + return true + }) + + var topCipherSuites []uint16 + var defaultCipherSuitesTLS13 []uint16 + + hasGCMAsmAMD64 := cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ + hasGCMAsmARM64 := cpu.ARM64.HasAES && cpu.ARM64.HasPMULL + // Keep in sync with crypto/aes/cipher_s390x.go. + hasGCMAsmS390X := cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasAESCTR && (cpu.S390X.HasGHASH || cpu.S390X.HasAESGCM) + + hasGCMAsm := hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X + + if hasGCMAsm { + // If AES-GCM hardware is provided then prioritise AES-GCM + // cipher suites. + topCipherSuites = []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + } + defaultCipherSuitesTLS13 = []uint16{ + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_AES_256_GCM_SHA384, + } + } else { + // Without AES-GCM hardware, we put the ChaCha20-Poly1305 + // cipher suites first. + topCipherSuites = []uint16{ + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + } + defaultCipherSuitesTLS13 = []uint16{ + tls.TLS_CHACHA20_POLY1305_SHA256, + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + } + } + + DefaultCipherSuites := make([]uint16, 0, 22) + DefaultCipherSuites = append(DefaultCipherSuites, topCipherSuites...) + DefaultCipherSuites = append(DefaultCipherSuites, defaultCipherSuitesTLS13...) + + m.http = &http.Server{ + Addr: m.cfg.Address, + Handler: promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{}), + IdleTimeout: time.Hour * 24, + ReadTimeout: time.Minute * 60, + MaxHeaderBytes: maxHeaderSize, + ReadHeaderTimeout: time.Minute * 60, + WriteTimeout: time.Minute * 60, + TLSConfig: &tls.Config{ + CurvePreferences: []tls.CurveID{ + tls.CurveP256, + tls.CurveP384, + tls.CurveP521, + tls.X25519, + }, + CipherSuites: DefaultCipherSuites, + MinVersion: tls.VersionTLS12, + PreferServerCipherSuites: true, + }, + } + + go func() { + err := m.http.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + errCh <- err + return + } + }() + + return errCh +} + +// Stop prometheus metrics service. +func (m *Plugin) Stop() error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.http != nil { + // timeout is 10 seconds + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + err := m.http.Shutdown(ctx) + if err != nil { + // Function should be Stop() error + m.log.Error("stop error", "error", errors.Errorf("error shutting down the metrics server: error %v", err)) + } + } + return nil +} + +// Collects used to collect all plugins which implement metrics.StatProvider interface (and Named) +func (m *Plugin) Collects() []interface{} { + return []interface{}{ + m.AddStatProvider, + } +} + +// Collector returns application specific collector by name or nil if collector not found. +func (m *Plugin) AddStatProvider(name endure.Named, stat StatProvider) error { + m.collectors.Store(name.Name(), statsProvider{ + collectors: stat.MetricsCollector(), + name: name.Name(), + }) + return nil +} + +// RPC interface satisfaction +func (m *Plugin) Name() string { + return PluginName +} + +// RPC interface satisfaction +func (m *Plugin) RPC() interface{} { + return &rpcServer{ + svc: m, + log: m.log, + } +} diff --git a/plugins/metrics/rpc.go b/plugins/metrics/rpc.go new file mode 100644 index 00000000..f9c6accb --- /dev/null +++ b/plugins/metrics/rpc.go @@ -0,0 +1,294 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +type rpcServer struct { + svc *Plugin + log logger.Logger +} + +// Metric represent single metric produced by the application. +type Metric struct { + // Collector name. + Name string + + // Collector value. + Value float64 + + // Labels associated with metric. Only for vector metrics. Must be provided in a form of label values. + Labels []string +} + +// Add new metric to the designated collector. +func (rpc *rpcServer) Add(m *Metric, ok *bool) error { + const op = errors.Op("Add metric") + rpc.log.Info("Adding metric", "name", m.Name, "value", m.Value, "labels", m.Labels) + c, exist := rpc.svc.collectors.Load(m.Name) + if !exist { + rpc.log.Error("undefined collector", "collector", m.Name) + return errors.E(op, errors.Errorf("undefined collector %s, try first Declare the desired collector", m.Name)) + } + + switch c := c.(type) { + case prometheus.Gauge: + c.Add(m.Value) + + case *prometheus.GaugeVec: + if len(m.Labels) == 0 { + rpc.log.Error("required labels for collector", "collector", m.Name) + return errors.E(op, errors.Errorf("required labels for collector %s", m.Name)) + } + + gauge, err := c.GetMetricWithLabelValues(m.Labels...) + if err != nil { + rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) + return errors.E(op, err) + } + gauge.Add(m.Value) + case prometheus.Counter: + c.Add(m.Value) + + case *prometheus.CounterVec: + if len(m.Labels) == 0 { + return errors.E(op, errors.Errorf("required labels for collector `%s`", m.Name)) + } + + gauge, err := c.GetMetricWithLabelValues(m.Labels...) + if err != nil { + rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) + return errors.E(op, err) + } + gauge.Add(m.Value) + + default: + return errors.E(op, errors.Errorf("collector %s does not support method `Add`", m.Name)) + } + + // RPC, set ok to true as return value. Need by rpc.Call reply argument + *ok = true + rpc.log.Info("new metric successfully added", "name", m.Name, "labels", m.Labels, "value", m.Value) + return nil +} + +// Sub subtract the value from the specific metric (gauge only). +func (rpc *rpcServer) Sub(m *Metric, ok *bool) error { + const op = errors.Op("Subtracting metric") + rpc.log.Info("Subtracting value from metric", "name", m.Name, "value", m.Value, "labels", m.Labels) + c, exist := rpc.svc.collectors.Load(m.Name) + if !exist { + rpc.log.Error("undefined collector", "name", m.Name, "value", m.Value, "labels", m.Labels) + return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) + } + if c == nil { + // can it be nil ??? I guess can't + return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) + } + + switch c := c.(type) { + case prometheus.Gauge: + c.Sub(m.Value) + + case *prometheus.GaugeVec: + if len(m.Labels) == 0 { + rpc.log.Error("required labels for collector, but none was provided", "name", m.Name, "value", m.Value) + return errors.E(op, errors.Errorf("required labels for collector %s", m.Name)) + } + + gauge, err := c.GetMetricWithLabelValues(m.Labels...) + if err != nil { + rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) + return errors.E(op, err) + } + gauge.Sub(m.Value) + default: + return errors.E(op, errors.Errorf("collector `%s` does not support method `Sub`", m.Name)) + } + rpc.log.Info("Subtracting operation applied successfully", "name", m.Name, "labels", m.Labels, "value", m.Value) + + *ok = true + return nil +} + +// Observe the value (histogram and summary only). +func (rpc *rpcServer) Observe(m *Metric, ok *bool) error { + const op = errors.Op("Observe metrics") + rpc.log.Info("Observing metric", "name", m.Name, "value", m.Value, "labels", m.Labels) + + c, exist := rpc.svc.collectors.Load(m.Name) + if !exist { + rpc.log.Error("undefined collector", "name", m.Name, "value", m.Value, "labels", m.Labels) + return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) + } + if c == nil { + return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) + } + + switch c := c.(type) { + case *prometheus.SummaryVec: + if len(m.Labels) == 0 { + return errors.E(op, errors.Errorf("required labels for collector `%s`", m.Name)) + } + + observer, err := c.GetMetricWithLabelValues(m.Labels...) + if err != nil { + return errors.E(op, err) + } + observer.Observe(m.Value) + + case prometheus.Histogram: + c.Observe(m.Value) + + case *prometheus.HistogramVec: + if len(m.Labels) == 0 { + return errors.E(op, errors.Errorf("required labels for collector `%s`", m.Name)) + } + + observer, err := c.GetMetricWithLabelValues(m.Labels...) + if err != nil { + rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) + return errors.E(op, err) + } + observer.Observe(m.Value) + default: + return errors.E(op, errors.Errorf("collector `%s` does not support method `Observe`", m.Name)) + } + + rpc.log.Info("observe operation finished successfully", "name", m.Name, "labels", m.Labels, "value", m.Value) + + *ok = true + return nil +} + +// Declare is used to register new collector in prometheus +// THE TYPES ARE: +// NamedCollector -> Collector with the name +// bool -> RPC reply value +// RETURNS: +// error +func (rpc *rpcServer) Declare(nc *NamedCollector, ok *bool) error { + const op = errors.Op("Declare metric") + rpc.log.Info("Declaring new metric", "name", nc.Name, "type", nc.Type, "namespace", nc.Namespace) + _, exist := rpc.svc.collectors.Load(nc.Name) + if exist { + rpc.log.Error("metric with provided name already exist", "name", nc.Name, "type", nc.Type, "namespace", nc.Namespace) + return errors.E(op, errors.Errorf("tried to register existing collector with the name `%s`", nc.Name)) + } + + var collector prometheus.Collector + switch nc.Type { + case Histogram: + opts := prometheus.HistogramOpts{ + Name: nc.Name, + Namespace: nc.Namespace, + Subsystem: nc.Subsystem, + Help: nc.Help, + Buckets: nc.Buckets, + } + + if len(nc.Labels) != 0 { + collector = prometheus.NewHistogramVec(opts, nc.Labels) + } else { + collector = prometheus.NewHistogram(opts) + } + case Gauge: + opts := prometheus.GaugeOpts{ + Name: nc.Name, + Namespace: nc.Namespace, + Subsystem: nc.Subsystem, + Help: nc.Help, + } + + if len(nc.Labels) != 0 { + collector = prometheus.NewGaugeVec(opts, nc.Labels) + } else { + collector = prometheus.NewGauge(opts) + } + case Counter: + opts := prometheus.CounterOpts{ + Name: nc.Name, + Namespace: nc.Namespace, + Subsystem: nc.Subsystem, + Help: nc.Help, + } + + if len(nc.Labels) != 0 { + collector = prometheus.NewCounterVec(opts, nc.Labels) + } else { + collector = prometheus.NewCounter(opts) + } + case Summary: + opts := prometheus.SummaryOpts{ + Name: nc.Name, + Namespace: nc.Namespace, + Subsystem: nc.Subsystem, + Help: nc.Help, + } + + if len(nc.Labels) != 0 { + collector = prometheus.NewSummaryVec(opts, nc.Labels) + } else { + collector = prometheus.NewSummary(opts) + } + + default: + return errors.E(op, errors.Errorf("unknown collector type %s", nc.Type)) + } + + // add collector to sync.Map + rpc.svc.collectors.Store(nc.Name, collector) + // that method might panic, we handle it by recover + err := rpc.svc.Register(collector) + if err != nil { + *ok = false + return errors.E(op, err) + } + + rpc.log.Info("metric successfully added", "name", nc.Name, "type", nc.Type, "namespace", nc.Namespace) + + *ok = true + return nil +} + +// Set the metric value (only for gaude). +func (rpc *rpcServer) Set(m *Metric, ok *bool) (err error) { + const op = errors.Op("Set metric") + rpc.log.Info("Observing metric", "name", m.Name, "value", m.Value, "labels", m.Labels) + + c, exist := rpc.svc.collectors.Load(m.Name) + if !exist { + return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) + } + if c == nil { + return errors.E(op, errors.Errorf("undefined collector %s", m.Name)) + } + + switch c := c.(type) { + case prometheus.Gauge: + c.Set(m.Value) + + case *prometheus.GaugeVec: + if len(m.Labels) == 0 { + rpc.log.Error("required labels for collector", "collector", m.Name) + return errors.E(op, errors.Errorf("required labels for collector %s", m.Name)) + } + + gauge, err := c.GetMetricWithLabelValues(m.Labels...) + if err != nil { + rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels) + return errors.E(op, err) + } + gauge.Set(m.Value) + + default: + return errors.E(op, errors.Errorf("collector `%s` does not support method Set", m.Name)) + } + + rpc.log.Info("set operation finished successfully", "name", m.Name, "labels", m.Labels, "value", m.Value) + + *ok = true + return nil +} diff --git a/plugins/redis/config.go b/plugins/redis/config.go new file mode 100644 index 00000000..ebcefed1 --- /dev/null +++ b/plugins/redis/config.go @@ -0,0 +1,32 @@ +package redis + +import "time" + +type Config struct { + Addrs []string `yaml:"addrs"` + DB int `yaml:"db"` + Username string `yaml:"username"` + Password string `yaml:"password"` + MasterName string `yaml:"master_name"` + SentinelPassword string `yaml:"sentinel_password"` + RouteByLatency bool `yaml:"route_by_latency"` + RouteRandomly bool `yaml:"route_randomly"` + MaxRetries int `yaml:"max_retries"` + DialTimeout time.Duration `yaml:"dial_timeout"` + MinRetryBackoff time.Duration `yaml:"min_retry_backoff"` + MaxRetryBackoff time.Duration `yaml:"max_retry_backoff"` + PoolSize int `yaml:"pool_size"` + MinIdleConns int `yaml:"min_idle_conns"` + MaxConnAge time.Duration `yaml:"max_conn_age"` + ReadTimeout time.Duration `yaml:"read_timeout"` + WriteTimeout time.Duration `yaml:"write_timeout"` + PoolTimeout time.Duration `yaml:"pool_timeout"` + IdleTimeout time.Duration `yaml:"idle_timeout"` + IdleCheckFreq time.Duration `yaml:"idle_check_freq"` + ReadOnly bool `yaml:"read_only"` +} + +// InitDefaults initializing fill config with default values +func (s *Config) InitDefaults() { + s.Addrs = []string{"localhost:6379"} // default addr is pointing to local storage +} diff --git a/plugins/redis/interface.go b/plugins/redis/interface.go new file mode 100644 index 00000000..909c8ca4 --- /dev/null +++ b/plugins/redis/interface.go @@ -0,0 +1,9 @@ +package redis + +import "github.com/go-redis/redis/v8" + +// Redis in the redis KV plugin interface +type Redis interface { + // GetClient + GetClient() redis.UniversalClient +} diff --git a/plugins/redis/plugin.go b/plugins/redis/plugin.go new file mode 100644 index 00000000..fe465340 --- /dev/null +++ b/plugins/redis/plugin.go @@ -0,0 +1,75 @@ +package redis + +import ( + "github.com/go-redis/redis/v8" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +const PluginName = "redis" + +type Plugin struct { + // config for RR integration + cfg *Config + // logger + log logger.Logger + // redis universal client + universalClient redis.UniversalClient +} + +func (s *Plugin) GetClient() redis.UniversalClient { + return s.universalClient +} + +func (s *Plugin) Init(cfg config.Configurer, log logger.Logger) error { + const op = errors.Op("redis plugin init") + s.cfg = &Config{} + s.cfg.InitDefaults() + + err := cfg.UnmarshalKey(PluginName, &s.cfg) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + s.log = log + + s.universalClient = redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: s.cfg.Addrs, + DB: s.cfg.DB, + Username: s.cfg.Username, + Password: s.cfg.Password, + SentinelPassword: s.cfg.SentinelPassword, + MaxRetries: s.cfg.MaxRetries, + MinRetryBackoff: s.cfg.MaxRetryBackoff, + MaxRetryBackoff: s.cfg.MaxRetryBackoff, + DialTimeout: s.cfg.DialTimeout, + ReadTimeout: s.cfg.ReadTimeout, + WriteTimeout: s.cfg.WriteTimeout, + PoolSize: s.cfg.PoolSize, + MinIdleConns: s.cfg.MinIdleConns, + MaxConnAge: s.cfg.MaxConnAge, + PoolTimeout: s.cfg.PoolTimeout, + IdleTimeout: s.cfg.IdleTimeout, + IdleCheckFrequency: s.cfg.IdleCheckFreq, + ReadOnly: s.cfg.ReadOnly, + RouteByLatency: s.cfg.RouteByLatency, + RouteRandomly: s.cfg.RouteRandomly, + MasterName: s.cfg.MasterName, + }) + + return nil +} + +func (s *Plugin) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (s Plugin) Stop() error { + return s.universalClient.Close() +} + +func (s *Plugin) Name() string { + return PluginName +} diff --git a/plugins/reload/config.go b/plugins/reload/config.go new file mode 100644 index 00000000..9ca2c0dc --- /dev/null +++ b/plugins/reload/config.go @@ -0,0 +1,58 @@ +package reload + +import ( + "time" + + "github.com/spiral/errors" +) + +// Config is a Reload configuration point. +type Config struct { + // Interval is a global refresh interval + Interval time.Duration + + // Patterns is a global file patterns to watch. It will be applied to every directory in project + Patterns []string + + // Services is set of services which would be reloaded in case of FS changes + Services map[string]ServiceConfig +} + +type ServiceConfig struct { + // Enabled indicates that service must be watched, doest not required when any other option specified + Enabled bool + + // Recursive is options to use nested files from root folder + Recursive bool + + // Patterns is per-service specific files to watch + Patterns []string + + // Dirs is per-service specific dirs which will be combined with Patterns + Dirs []string + + // Ignore is set of files which would not be watched + Ignore []string +} + +// InitDefaults sets missing values to their default values. +func InitDefaults(c *Config) { + c.Interval = time.Second + c.Patterns = []string{".php"} +} + +// Valid validates the configuration. +func (c *Config) Valid() error { + const op = errors.Op("config validation [reload plugin]") + if c.Interval < time.Second { + return errors.E(op, errors.Str("too short interval")) + } + + if c.Services == nil { + return errors.E(op, errors.Str("should add at least 1 service")) + } else if len(c.Services) == 0 { + return errors.E(op, errors.Str("service initialized, however, no config added")) + } + + return nil +} diff --git a/plugins/reload/plugin.go b/plugins/reload/plugin.go new file mode 100644 index 00000000..452e03a3 --- /dev/null +++ b/plugins/reload/plugin.go @@ -0,0 +1,159 @@ +package reload + +import ( + "os" + "strings" + "time" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/resetter" +) + +// PluginName contains default plugin name. +const PluginName string = "reload" +const thresholdChanBuffer uint = 1000 + +type Plugin struct { + cfg *Config + log logger.Logger + watcher *Watcher + services map[string]interface{} + res resetter.Resetter + stopc chan struct{} +} + +// Init controller service +func (s *Plugin) Init(cfg config.Configurer, log logger.Logger, res resetter.Resetter) error { + const op = errors.Op("reload plugin init") + s.cfg = &Config{} + InitDefaults(s.cfg) + err := cfg.UnmarshalKey(PluginName, &s.cfg) + if err != nil { + // disable plugin in case of error + return errors.E(op, errors.Disabled, err) + } + + s.log = log + s.res = res + s.stopc = make(chan struct{}, 1) + s.services = make(map[string]interface{}) + + var configs []WatcherConfig + + for serviceName, serviceConfig := range s.cfg.Services { + ignored, err := ConvertIgnored(serviceConfig.Ignore) + if err != nil { + return errors.E(op, err) + } + configs = append(configs, WatcherConfig{ + ServiceName: serviceName, + Recursive: serviceConfig.Recursive, + Directories: serviceConfig.Dirs, + FilterHooks: func(filename string, patterns []string) error { + for i := 0; i < len(patterns); i++ { + if strings.Contains(filename, patterns[i]) { + return nil + } + } + return errors.E(op, errors.Skip) + }, + Files: make(map[string]os.FileInfo), + Ignored: ignored, + FilePatterns: append(serviceConfig.Patterns, s.cfg.Patterns...), + }) + } + + s.watcher, err = NewWatcher(configs, s.log) + if err != nil { + return errors.E(op, err) + } + + return nil +} + +func (s *Plugin) Serve() chan error { + const op = errors.Op("reload plugin serve") + errCh := make(chan error, 1) + if s.cfg.Interval < time.Second { + errCh <- errors.E(op, errors.Str("reload interval is too fast")) + return errCh + } + + // make a map with unique services + // so, if we would have a 100 events from http service + // in map we would see only 1 key and it's config + treshholdc := make(chan struct { + serviceConfig ServiceConfig + service string + }, thresholdChanBuffer) + + // use the same interval + timer := time.NewTimer(s.cfg.Interval) + + go func() { + for e := range s.watcher.Event { + treshholdc <- struct { + serviceConfig ServiceConfig + service string + }{serviceConfig: s.cfg.Services[e.service], service: e.service} + } + }() + + // map with configs by services + updated := make(map[string]ServiceConfig, len(s.cfg.Services)) + + go func() { + for { + select { + case cfg := <-treshholdc: + // logic is following: + // restart + timer.Stop() + // replace previous value in map by more recent without adding new one + updated[cfg.service] = cfg.serviceConfig + // if we getting a lot of events, we shouldn't restart particular service on each of it (user doing batch move or very fast typing) + // instead, we are resetting the timer and wait for s.cfg.Interval time + // If there is no more events, we restart service only once + timer.Reset(s.cfg.Interval) + case <-timer.C: + if len(updated) > 0 { + for name := range updated { + err := s.res.ResetByName(name) + if err != nil { + timer.Stop() + errCh <- errors.E(op, err) + return + } + } + // zero map + updated = make(map[string]ServiceConfig, len(s.cfg.Services)) + } + case <-s.stopc: + timer.Stop() + return + } + } + }() + + go func() { + err := s.watcher.StartPolling(s.cfg.Interval) + if err != nil { + errCh <- errors.E(op, err) + return + } + }() + + return errCh +} + +func (s *Plugin) Stop() error { + s.watcher.Stop() + s.stopc <- struct{}{} + return nil +} + +func (s *Plugin) Name() string { + return PluginName +} diff --git a/plugins/reload/watcher.go b/plugins/reload/watcher.go new file mode 100644 index 00000000..c232f16f --- /dev/null +++ b/plugins/reload/watcher.go @@ -0,0 +1,374 @@ +package reload + +import ( + "io/ioutil" + "os" + "path/filepath" + "sync" + "time" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +// SimpleHook is used to filter by simple criteria, CONTAINS +type SimpleHook func(filename string, pattern []string) error + +// An Event describes an event that is received when files or directory +// changes occur. It includes the os.FileInfo of the changed file or +// directory and the type of event that's occurred and the full path of the file. +type Event struct { + Path string + Info os.FileInfo + + service string // type of service, http, grpc, etc... +} + +type WatcherConfig struct { + // service name + ServiceName string + + // Recursive or just add by singe directory + Recursive bool + + // Directories used per-service + Directories []string + + // simple hook, just CONTAINS + FilterHooks func(filename string, pattern []string) error + + // path to file with Files + Files map[string]os.FileInfo + + // Ignored Directories, used map for O(1) amortized get + Ignored map[string]struct{} + + // FilePatterns to ignore + FilePatterns []string +} + +type Watcher struct { + // main event channel + Event chan Event + close chan struct{} + + // ============================= + mu *sync.Mutex + + // indicates is walker started or not + started bool + + // config for each service + // need pointer here to assign files + watcherConfigs map[string]WatcherConfig + + // logger + log logger.Logger +} + +// Options is used to set Watcher Options +type Options func(*Watcher) + +// NewWatcher returns new instance of File Watcher +func NewWatcher(configs []WatcherConfig, log logger.Logger, options ...Options) (*Watcher, error) { + w := &Watcher{ + Event: make(chan Event), + mu: &sync.Mutex{}, + + log: log, + + close: make(chan struct{}), + + //workingDir: workDir, + watcherConfigs: make(map[string]WatcherConfig), + } + + // add watcherConfigs by service names + for _, v := range configs { + w.watcherConfigs[v.ServiceName] = v + } + + // apply options + for _, option := range options { + option(w) + } + err := w.initFs() + if err != nil { + return nil, err + } + + return w, nil +} + +// initFs makes initial map with files +func (w *Watcher) initFs() error { + const op = errors.Op("init fs") + for srvName, config := range w.watcherConfigs { + fileList, err := w.retrieveFileList(srvName, config) + if err != nil { + return errors.E(op, err) + } + // workaround. in golang you can't assign to map in struct field + tmp := w.watcherConfigs[srvName] + tmp.Files = fileList + w.watcherConfigs[srvName] = tmp + } + return nil +} + +// ConvertIgnored is used to convert slice to map with ignored files +func ConvertIgnored(ignored []string) (map[string]struct{}, error) { + if len(ignored) == 0 { + return nil, nil + } + + ign := make(map[string]struct{}, len(ignored)) + for i := 0; i < len(ignored); i++ { + abs, err := filepath.Abs(ignored[i]) + if err != nil { + return nil, err + } + ign[abs] = struct{}{} + } + + return ign, nil +} + +// https://en.wikipedia.org/wiki/Inotify +// SetMaxFileEvents sets max file notify events for Watcher +// In case of file watch errors, this value can be increased system-wide +// For linux: set --> fs.inotify.max_user_watches = 600000 (under /etc/<choose_name_here>.conf) +// Add apply: sudo sysctl -p --system +// func SetMaxFileEvents(events int) Options { +// return func(watcher *Watcher) { +// watcher.maxFileWatchEvents = events +// } +// +// } + +// pass map from outside +func (w *Watcher) retrieveFilesSingle(serviceName, path string) (map[string]os.FileInfo, error) { + const op = errors.Op("retrieve") + stat, err := os.Stat(path) + if err != nil { + return nil, err + } + + filesList := make(map[string]os.FileInfo, 10) + filesList[path] = stat + + // if it's not a dir, return + if !stat.IsDir() { + return filesList, nil + } + + fileInfoList, err := ioutil.ReadDir(path) + if err != nil { + return nil, err + } + + // recursive calls are slow in compare to goto + // so, we will add files with goto pattern +outer: + for i := 0; i < len(fileInfoList); i++ { + // if file in ignored --> continue + if _, ignored := w.watcherConfigs[serviceName].Ignored[path]; ignored { + continue + } + + // if filename does not contain pattern --> ignore that file + if w.watcherConfigs[serviceName].FilePatterns != nil && w.watcherConfigs[serviceName].FilterHooks != nil { + err = w.watcherConfigs[serviceName].FilterHooks(fileInfoList[i].Name(), w.watcherConfigs[serviceName].FilePatterns) + if errors.Is(errors.Skip, err) { + continue outer + } + } + + filesList[fileInfoList[i].Name()] = fileInfoList[i] + } + + return filesList, nil +} + +func (w *Watcher) StartPolling(duration time.Duration) error { + w.mu.Lock() + const op = errors.Op("start polling") + if w.started { + w.mu.Unlock() + return errors.E(op, errors.Str("already started")) + } + + w.started = true + w.mu.Unlock() + + return w.waitEvent(duration) +} + +// this is blocking operation +func (w *Watcher) waitEvent(d time.Duration) error { + ticker := time.NewTicker(d) + for { + select { + case <-w.close: + ticker.Stop() + // just exit + // no matter for the pollEvents + return nil + case <-ticker.C: + // this is not very effective way + // because we have to wait on Lock + // better is to listen files in parallel, but, since that would be used in debug... TODO + for serviceName := range w.watcherConfigs { + // TODO sync approach + fileList, _ := w.retrieveFileList(serviceName, w.watcherConfigs[serviceName]) + w.pollEvents(w.watcherConfigs[serviceName].ServiceName, fileList) + } + } + } +} + +// retrieveFileList get file list for service +func (w *Watcher) retrieveFileList(serviceName string, config WatcherConfig) (map[string]os.FileInfo, error) { + fileList := make(map[string]os.FileInfo) + if config.Recursive { + // walk through directories recursively + for i := 0; i < len(config.Directories); i++ { + // full path is workdir/relative_path + fullPath, err := filepath.Abs(config.Directories[i]) + if err != nil { + return nil, err + } + list, err := w.retrieveFilesRecursive(serviceName, fullPath) + if err != nil { + return nil, err + } + + for k := range list { + fileList[k] = list[k] + } + } + return fileList, nil + } + + for i := 0; i < len(config.Directories); i++ { + // full path is workdir/relative_path + fullPath, err := filepath.Abs(config.Directories[i]) + if err != nil { + return nil, err + } + + // list is pathToFiles with files + list, err := w.retrieveFilesSingle(serviceName, fullPath) + if err != nil { + return nil, err + } + + for pathToFile, file := range list { + fileList[pathToFile] = file + } + } + + return fileList, nil +} + +func (w *Watcher) retrieveFilesRecursive(serviceName, root string) (map[string]os.FileInfo, error) { + fileList := make(map[string]os.FileInfo) + + return fileList, filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + const op = errors.Op("retrieve files recursive") + if err != nil { + return errors.E(op, err) + } + + // If path is ignored and it's a directory, skip the directory. If it's + // ignored and it's a single file, skip the file. + _, ignored := w.watcherConfigs[serviceName].Ignored[path] + if ignored { + if info.IsDir() { + // if it's dir, ignore whole + return filepath.SkipDir + } + return nil + } + + // if filename does not contain pattern --> ignore that file + err = w.watcherConfigs[serviceName].FilterHooks(info.Name(), w.watcherConfigs[serviceName].FilePatterns) + if errors.Is(errors.Skip, err) { + return nil + } + + // Add the path and it's info to the file list. + fileList[path] = info + return nil + }) +} + +func (w *Watcher) pollEvents(serviceName string, files map[string]os.FileInfo) { + w.mu.Lock() + defer w.mu.Unlock() + + // Store create and remove events for use to check for rename events. + creates := make(map[string]os.FileInfo) + removes := make(map[string]os.FileInfo) + + // Check for removed files. + for pth := range w.watcherConfigs[serviceName].Files { + if _, found := files[pth]; !found { + removes[pth] = w.watcherConfigs[serviceName].Files[pth] + w.log.Debug("file added to the list of removed files", "path", pth, "name", w.watcherConfigs[serviceName].Files[pth].Name(), "size", w.watcherConfigs[serviceName].Files[pth].Size()) + } + } + + // Check for created files, writes and chmods. + for pth := range files { + if files[pth].IsDir() { + continue + } + oldInfo, found := w.watcherConfigs[serviceName].Files[pth] + if !found { + // A file was created. + creates[pth] = files[pth] + w.log.Debug("file was created", "path", pth, "name", files[pth].Name(), "size", files[pth].Size()) + continue + } + + if oldInfo.ModTime() != files[pth].ModTime() || oldInfo.Mode() != files[pth].Mode() { + w.watcherConfigs[serviceName].Files[pth] = files[pth] + w.log.Debug("file was updated", "path", pth, "name", files[pth].Name(), "size", files[pth].Size()) + w.Event <- Event{ + Path: pth, + Info: files[pth], + service: serviceName, + } + } + } + + // Send all the remaining create and remove events. + for pth := range creates { + // add file to the plugin watch files + w.watcherConfigs[serviceName].Files[pth] = creates[pth] + w.log.Debug("file was added to watcher", "path", pth, "name", creates[pth].Name(), "size", creates[pth].Size()) + + w.Event <- Event{ + Path: pth, + Info: creates[pth], + service: serviceName, + } + } + + for pth := range removes { + // delete path from the config + delete(w.watcherConfigs[serviceName].Files, pth) + w.log.Debug("file was removed from watcher", "path", pth, "name", removes[pth].Name(), "size", removes[pth].Size()) + + w.Event <- Event{ + Path: pth, + Info: removes[pth], + service: serviceName, + } + } +} + +func (w *Watcher) Stop() { + w.close <- struct{}{} +} diff --git a/plugins/resetter/interface.go b/plugins/resetter/interface.go new file mode 100644 index 00000000..47d8d791 --- /dev/null +++ b/plugins/resetter/interface.go @@ -0,0 +1,17 @@ +package resetter + +// If plugin implements Resettable interface, than it state can be resetted without reload in runtime via RPC/HTTP +type Resettable interface { + // Reset reload all plugins + Reset() error +} + +// Resetter interface is the Resetter plugin main interface +type Resetter interface { + // Reset all registered plugins + ResetAll() error + // Reset by plugin name + ResetByName(string) error + // GetAll registered plugins + GetAll() []string +} diff --git a/plugins/resetter/plugin.go b/plugins/resetter/plugin.go new file mode 100644 index 00000000..5d294086 --- /dev/null +++ b/plugins/resetter/plugin.go @@ -0,0 +1,80 @@ +package resetter + +import ( + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +const PluginName = "resetter" + +type Plugin struct { + registry map[string]Resettable + log logger.Logger +} + +func (p *Plugin) ResetAll() error { + const op = errors.Op("reset all") + for name := range p.registry { + err := p.registry[name].Reset() + if err != nil { + return errors.E(op, err) + } + } + return nil +} + +func (p *Plugin) ResetByName(plugin string) error { + const op = errors.Op("reset by name") + if plugin, ok := p.registry[plugin]; ok { + return plugin.Reset() + } + return errors.E(op, errors.Errorf("can't find plugin: %s", plugin)) +} + +func (p *Plugin) GetAll() []string { + all := make([]string, 0, len(p.registry)) + for name := range p.registry { + all = append(all, name) + } + return all +} + +func (p *Plugin) Init(log logger.Logger) error { + p.registry = make(map[string]Resettable) + p.log = log + return nil +} + +// Reset named service. +func (p *Plugin) Reset(name string) error { + svc, ok := p.registry[name] + if !ok { + return errors.E("no such service", errors.Str(name)) + } + + return svc.Reset() +} + +// RegisterTarget resettable service. +func (p *Plugin) RegisterTarget(name endure.Named, r Resettable) error { + p.registry[name.Name()] = r + return nil +} + +// Collects declares services to be collected. +func (p *Plugin) Collects() []interface{} { + return []interface{}{ + p.RegisterTarget, + } +} + +// Name of the service. +func (p *Plugin) Name() string { + return PluginName +} + +// RPCService returns associated rpc service. +func (p *Plugin) RPC() interface{} { + return &rpc{srv: p, log: p.log} +} diff --git a/plugins/resetter/rpc.go b/plugins/resetter/rpc.go new file mode 100644 index 00000000..69c955b0 --- /dev/null +++ b/plugins/resetter/rpc.go @@ -0,0 +1,30 @@ +package resetter + +import "github.com/spiral/roadrunner/v2/plugins/logger" + +type rpc struct { + srv *Plugin + log logger.Logger +} + +// List all resettable plugins. +func (rpc *rpc) List(_ bool, list *[]string) error { + rpc.log.Debug("started List method") + *list = make([]string, 0) + + for name := range rpc.srv.registry { + *list = append(*list, name) + } + rpc.log.Debug("services list", "services", *list) + + rpc.log.Debug("finished List method") + return nil +} + +// Reset named plugin. +func (rpc *rpc) Reset(service string, done *bool) error { + rpc.log.Debug("started Reset method for the service", "service", service) + defer rpc.log.Debug("finished Reset method for the service", "service", service) + *done = true + return rpc.srv.Reset(service) +} diff --git a/plugins/rpc/config.go b/plugins/rpc/config.go new file mode 100644 index 00000000..7f3474d7 --- /dev/null +++ b/plugins/rpc/config.go @@ -0,0 +1,47 @@ +package rpc + +import ( + "errors" + "net" + "strings" +) + +// Config defines RPC service config. +type Config struct { + // Listen string + Listen string + + // Disabled disables RPC service. + Disabled bool +} + +// InitDefaults allows to init blank config with pre-defined set of default values. +func (c *Config) InitDefaults() { + if c.Listen == "" { + c.Listen = "tcp://127.0.0.1:6001" + } +} + +// Valid returns nil if config is valid. +func (c *Config) Valid() error { + if dsn := strings.Split(c.Listen, "://"); len(dsn) != 2 { + return errors.New("invalid socket DSN (tcp://:6001, unix://file.sock)") + } + + return nil +} + +// Listener creates new rpc socket Listener. +func (c *Config) Listener() (net.Listener, error) { + return CreateListener(c.Listen) +} + +// Dialer creates rpc socket Dialer. +func (c *Config) Dialer() (net.Conn, error) { + dsn := strings.Split(c.Listen, "://") + if len(dsn) != 2 { + return nil, errors.New("invalid socket DSN (tcp://:6001, unix://file.sock)") + } + + return net.Dial(dsn[0], dsn[1]) +} diff --git a/plugins/rpc/doc/plugin_arch.drawio b/plugins/rpc/doc/plugin_arch.drawio new file mode 100644 index 00000000..dec5f0b2 --- /dev/null +++ b/plugins/rpc/doc/plugin_arch.drawio @@ -0,0 +1 @@ +<mxfile host="Electron" modified="2020-10-19T17:14:19.125Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.7.9 Chrome/85.0.4183.121 Electron/10.1.3 Safari/537.36" etag="2J39x4EyFr1zaE9BXKM4" version="13.7.9" type="device"><diagram id="q2oMKs6VHyn7y0AfAXBL" name="Page-1">7Vttc9o4EP41zLQfksE2GPIxQHPXu7RlQntt7ptiC1sX2XJlOUB//a1sGdtIJDQFnE6YyUys1YutfR7trlai44yj5R8cJeEH5mPasbv+suNMOrZtORcO/JOSVSEZWv1CEHDiq0aVYEZ+YCXsKmlGfJw2GgrGqCBJU+ixOMaeaMgQ52zRbDZntPnWBAVYE8w8RHXpV+KLUEkt96Kq+BOTIFSvHtqDoiJCZWM1kzREPlvURM67jjPmjIniKVqOMZXKK/VS9LvaUrv+MI5jsUuHL/zu0yx7//HT3Pln8vfN59vvS/usVHMqVuWMsQ8KUEXGRcgCFiP6rpKOOMtiH8thu1Cq2lwzloDQAuF/WIiVQhNlgoEoFBFVtXhJxLfa860c6ryvSpOlGjkvrMpCLPjqW71Q6yWLVbe8VPabs1hcoYhQKRizjBPMYcIf8UJVqq+8gGKhC6mArTpWohQG8lSrfz88xF8/ds/+uiLe7MsXtLiyZ2clVxEPsHik3WDNBFhCmEUYvh36cUyRIA/N70CKy8G6XQU3PCjEfwZ9q030K8RvazVPoV8BftvA+7dE33KOBP9jX/mAaKbedDOFkbpTmgUk1qjRBH4REoFnCcr1sADj3wT55xVv0PMD5gIvayJdU6rWGSi3otyMYw3OlWRRme21VwlrFtsdHEi9jqbe9zERha+ak0DTL0xVNJWIKAliePZAMaA+ZyQVQsA5XaqKiPh+sShxSn6gu3woiU7CSCzyCfVHnf5EjgXrMC103go+3Q18hho6QwM4pfPcOzg9DZwJTnDspyBk8Rqk8ylnDxCB8N8DLcveD1z2BlxWWa4vpu4x8epreOmuK/YvZcQnIaAoTYm34XeO5kMMun/aFRjdj45QDYG+AYBStrMHUW+YSgpWBOgNtxCgHKJwgapXPercGKhvbwxkbQxUKEYbKCfJetrP542r8aa0vt0U9gsE1rpzKfWVeK97ia+Xc41glolhB1viA32Jj+3O5YhIXc9loAHFEczdpRKWO95Ay/2eyZ1UrqqzQq8S14tkmeurrIanQP0vRvmVQYA052WwVAwHE7+rXrHBp/bCI3f4tPu1jMGReyCwLT06KoLPVPDMExnHmvrSBYkoinGpIVWz07oUcm8y8kJC/Wu0YpmcXiqQd1+WRiHj5AcMi0qIoJqXMNhuo8VM9lQLO1/oeFqiY22IPqBlo+E1SoUSeIxSlKTkbj2NCGwhiUdMCBbt0/k8P47uuQarULapE8Vye4diytDg+ke7R2hAKHaPx4wyIMYkZgWBCKUbopJDFM/FVgalsOEhcXCdt5n0KsmNUoUUMeg7p3kgEoI/wHG+axZIbPUHI9DyWIYl4BnsMZStqpw7iwT22WMWw1wQycHFwKMFTsUvU+Tx1fk0cUr34e7GE/tQBqV0SxpNpJGeYf6QK+VNjMX5TeK9PbGlTbb07ZbZYl1sYUsKTCEeltvAIlKr+aNuSqHqxJw2mTMwBC7HZY6eOSiYMydYni3IeHH8aILnxIk9c8Lq9tomxQ7pCUpyqAszUZ4lWc/iw3qXqQjwOc+8n1kaSRydJI6BEBTdYTqF3WixH57woq1h0/ryueDsGLAOD0UFPeNQ2AcYPmT+G7FK8NvCTMjHkzdply1HdCfmIzhDHvMIR3Av9jDVrKTOjjnUCzPaRzpN1Ra+Ciafk9Xo/nK6wmAsfpMMhrZ+DazZmsHoNTNdPcvgD1xDpmuwB4dgpIX9dLxY8aTKdZ78wp7osn2t/lQyw8SZg3kFPTmqcSZGkTIsgNeJLS2yxZTMOCpb9IizMigcByQFmyITGlYxV4A2o0iqyc+PvOGvYYPmTNbl2Xgzq17Wgdie/Ia1cYFkqO8pHftAx2FGVPUMVVJkul8VLK61cXJl67gc6pTSbAvcVgJ245259TW5Vm5M1k6i9xPlO7uG+b1Ww3zdOVdXCk5h/pHsgtM0C64p7WNywqWz3j8tdsgLX0tXHJ+itiNFbVsu176UIN/SL7xMOQOFR2lOl7a9fN3MP4rYHpbzxq7dsGk/1O1QMzT6nYOAqSAZFqaPvY78hYecQIBjzJGQgbNgsk2UeaH8Ji93RdLvefdY3ohDeZyNlx7G8iGjJMqvA5/pV61fE9YGy93fU6ANxer3NcWNwupXSs67/wE=</diagram></mxfile>
\ No newline at end of file diff --git a/plugins/rpc/interface.go b/plugins/rpc/interface.go new file mode 100644 index 00000000..683fd2ec --- /dev/null +++ b/plugins/rpc/interface.go @@ -0,0 +1,7 @@ +package rpc + +// RPCer declares the ability to create set of public RPC methods. +type RPCer interface { + // Provides RPC methods for the given service. + RPC() interface{} +} diff --git a/plugins/rpc/plugin.go b/plugins/rpc/plugin.go new file mode 100644 index 00000000..6a83326b --- /dev/null +++ b/plugins/rpc/plugin.go @@ -0,0 +1,164 @@ +package rpc + +import ( + "net" + "net/rpc" + "sync/atomic" + + "github.com/spiral/endure" + "github.com/spiral/errors" + goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +// PluginName contains default plugin name. +const PluginName = "RPC" + +type pluggable struct { + service RPCer + name string +} + +// Plugin is RPC service. +type Plugin struct { + cfg Config + log logger.Logger + rpc *rpc.Server + services []pluggable + listener net.Listener + closed *uint32 +} + +// Init rpc service. Must return true if service is enabled. +func (s *Plugin) Init(cfg config.Configurer, log logger.Logger) error { + const op = errors.Op("RPC plugin init") + if !cfg.Has(PluginName) { + return errors.E(op, errors.Disabled) + } + + err := cfg.UnmarshalKey(PluginName, &s.cfg) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + s.cfg.InitDefaults() + + if s.cfg.Disabled { + return errors.E(op, errors.Disabled) + } + + s.log = log + state := uint32(0) + s.closed = &state + atomic.StoreUint32(s.closed, 0) + + return s.cfg.Valid() +} + +// Serve serves the service. +func (s *Plugin) Serve() chan error { + const op = errors.Op("register service") + errCh := make(chan error, 1) + + s.rpc = rpc.NewServer() + + services := make([]string, 0, len(s.services)) + + // Attach all services + for i := 0; i < len(s.services); i++ { + err := s.Register(s.services[i].name, s.services[i].service.RPC()) + if err != nil { + errCh <- errors.E(op, err) + return errCh + } + + services = append(services, s.services[i].name) + } + + var err error + s.listener, err = s.cfg.Listener() + if err != nil { + errCh <- err + return errCh + } + + s.log.Debug("Started RPC service", "address", s.cfg.Listen, "services", services) + + go func() { + for { + conn, err := s.listener.Accept() + if err != nil { + if atomic.LoadUint32(s.closed) == 1 { + // just log and continue, this is not a critical issue, we just called Stop + s.log.Warn("listener accept error, connection closed", "error", err) + return + } + + s.log.Error("listener accept error", "error", err) + errCh <- errors.E(errors.Op("listener accept"), errors.Serve, err) + return + } + + go s.rpc.ServeCodec(goridgeRpc.NewCodec(conn)) + } + }() + + return errCh +} + +// Stop stops the service. +func (s *Plugin) Stop() error { + // store closed state + atomic.StoreUint32(s.closed, 1) + err := s.listener.Close() + if err != nil { + return errors.E(errors.Op("stop RPC socket"), err) + } + return nil +} + +// Name contains service name. +func (s *Plugin) Name() string { + return PluginName +} + +// Depends declares services to collect for RPC. +func (s *Plugin) Collects() []interface{} { + return []interface{}{ + s.RegisterPlugin, + } +} + +// RegisterPlugin registers RPC service plugin. +func (s *Plugin) RegisterPlugin(name endure.Named, p RPCer) { + s.services = append(s.services, pluggable{ + service: p, + name: name.Name(), + }) +} + +// Register publishes in the server the set of methods of the +// receiver value that satisfy the following conditions: +// - exported method of exported type +// - two arguments, both of exported type +// - the second argument is a pointer +// - one return value, of type error +// It returns an error if the receiver is not an exported type or has +// no suitable methods. It also logs the error using package log. +func (s *Plugin) Register(name string, svc interface{}) error { + if s.rpc == nil { + return errors.E("RPC service is not configured") + } + + return s.rpc.RegisterName(name, svc) +} + +// Client creates new RPC client. +func (s *Plugin) Client() (*rpc.Client, error) { + conn, err := s.cfg.Dialer() + if err != nil { + return nil, err + } + + return rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)), nil +} diff --git a/plugins/rpc/util.go b/plugins/rpc/util.go new file mode 100644 index 00000000..29a475a4 --- /dev/null +++ b/plugins/rpc/util.go @@ -0,0 +1,57 @@ +package rpc + +import ( + "errors" + "fmt" + "net" + "os" + "strings" + "syscall" + + "github.com/valyala/tcplisten" +) + +// CreateListener crates socket listener based on DSN definition. +func CreateListener(address string) (net.Listener, error) { + dsn := strings.Split(address, "://") + if len(dsn) != 2 { + return nil, errors.New("invalid DSN (tcp://:6001, unix://file.sock)") + } + + if dsn[0] != "unix" && dsn[0] != "tcp" { + return nil, errors.New("invalid Protocol (tcp://:6001, unix://file.sock)") + } + + // create unix listener + if dsn[0] == "unix" { + // check if the file exist + if fileExists(dsn[1]) { + err := syscall.Unlink(dsn[1]) + if err != nil { + return nil, fmt.Errorf("error during the unlink syscall: error %v", err) + } + } + return net.Listen(dsn[0], dsn[1]) + } + + // configure and create tcp4 listener + cfg := tcplisten.Config{ + ReusePort: true, + DeferAccept: true, + FastOpen: true, + Backlog: 0, + } + + // only tcp4 is currently supported + return cfg.NewListener("tcp4", dsn[1]) +} + +// fileExists checks if a file exists and is not a directory before we +// try using it to prevent further errors. +func fileExists(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} diff --git a/plugins/server/config.go b/plugins/server/config.go new file mode 100644 index 00000000..4bef3c5f --- /dev/null +++ b/plugins/server/config.go @@ -0,0 +1,39 @@ +package server + +import ( + "time" +) + +// Config config combines factory, pool and cmd configurations. +type Config struct { + // Command to run as application. + Command string + + // User to run application under. + User string + + // Group to run application under. + Group string + + // Env represents application environment. + Env Env + + // Listen defines connection method and factory to be used to connect to workers: + // "pipes", "tcp://:6001", "unix://rr.sock" + // This config section must not change on re-configuration. + Relay string + + // RelayTimeout defines for how long socket factory will be waiting for worker connection. This config section + // must not change on re-configuration. Defaults to 60s. + RelayTimeout time.Duration +} + +func (cfg *Config) InitDefaults() { + if cfg.Relay == "" { + cfg.Relay = "pipes" + } + + if cfg.RelayTimeout == 0 { + cfg.RelayTimeout = time.Second * 60 + } +} diff --git a/plugins/server/interface.go b/plugins/server/interface.go new file mode 100644 index 00000000..9c1079ea --- /dev/null +++ b/plugins/server/interface.go @@ -0,0 +1,20 @@ +package server + +import ( + "context" + "os/exec" + + "github.com/spiral/roadrunner/v2/interfaces/events" + "github.com/spiral/roadrunner/v2/interfaces/pool" + "github.com/spiral/roadrunner/v2/interfaces/worker" + poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" +) + +type Env map[string]string + +// Server creates workers for the application. +type Server interface { + CmdFactory(env Env) (func() *exec.Cmd, error) + NewWorker(ctx context.Context, env Env, listeners ...events.EventListener) (worker.BaseProcess, error) + NewWorkerPool(ctx context.Context, opt poolImpl.Config, env Env, listeners ...events.EventListener) (pool.Pool, error) +} diff --git a/plugins/server/plugin.go b/plugins/server/plugin.go new file mode 100644 index 00000000..b280d253 --- /dev/null +++ b/plugins/server/plugin.go @@ -0,0 +1,229 @@ +package server + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + + // core imports + "github.com/spiral/roadrunner/v2/interfaces/events" + "github.com/spiral/roadrunner/v2/interfaces/pool" + "github.com/spiral/roadrunner/v2/interfaces/worker" + "github.com/spiral/roadrunner/v2/pkg/pipe" + poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" + "github.com/spiral/roadrunner/v2/pkg/socket" + "github.com/spiral/roadrunner/v2/util" +) + +const PluginName = "server" + +// Plugin manages worker +type Plugin struct { + cfg Config + log logger.Logger + factory worker.Factory +} + +// Init application provider. +func (server *Plugin) Init(cfg config.Configurer, log logger.Logger) error { + const op = errors.Op("Init") + err := cfg.UnmarshalKey(PluginName, &server.cfg) + if err != nil { + return errors.E(op, errors.Init, err) + } + server.cfg.InitDefaults() + server.log = log + + server.factory, err = server.initFactory() + if err != nil { + return errors.E(errors.Op("Init factory"), err) + } + + return nil +} + +// Name contains service name. +func (server *Plugin) Name() string { + return PluginName +} + +func (server *Plugin) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (server *Plugin) Stop() error { + if server.factory == nil { + return nil + } + + return server.factory.Close() +} + +// CmdFactory provides worker command factory associated with given context. +func (server *Plugin) CmdFactory(env Env) (func() *exec.Cmd, error) { + const op = errors.Op("cmd factory") + var cmdArgs []string + + // create command according to the config + cmdArgs = append(cmdArgs, strings.Split(server.cfg.Command, " ")...) + if len(cmdArgs) < 2 { + return nil, errors.E(op, errors.Str("should be in form of `php <script>")) + } + if cmdArgs[0] != "php" { + return nil, errors.E(op, errors.Str("first arg in command should be `php`")) + } + + _, err := os.Stat(cmdArgs[1]) + if err != nil { + return nil, errors.E(op, err) + } + return func() *exec.Cmd { + cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) //nolint:gosec + util.IsolateProcess(cmd) + + // if user is not empty, and OS is linux or macos + // execute php worker from that particular user + if server.cfg.User != "" { + err := util.ExecuteFromUser(cmd, server.cfg.User) + if err != nil { + return nil + } + } + + cmd.Env = server.setEnv(env) + + return cmd + }, nil +} + +// NewWorker issues new standalone worker. +func (server *Plugin) NewWorker(ctx context.Context, env Env, listeners ...events.EventListener) (worker.BaseProcess, error) { + const op = errors.Op("new worker") + + list := make([]events.EventListener, 0, len(listeners)) + list = append(list, server.collectWorkerLogs) + + spawnCmd, err := server.CmdFactory(env) + if err != nil { + return nil, errors.E(op, err) + } + + w, err := server.factory.SpawnWorkerWithTimeout(ctx, spawnCmd(), list...) + if err != nil { + return nil, errors.E(op, err) + } + + return w, nil +} + +// NewWorkerPool issues new worker pool. +func (server *Plugin) NewWorkerPool(ctx context.Context, opt poolImpl.Config, env Env, listeners ...events.EventListener) (pool.Pool, error) { + const op = errors.Op("server plugins new worker pool") + spawnCmd, err := server.CmdFactory(env) + if err != nil { + return nil, errors.E(op, err) + } + + list := make([]events.EventListener, 0, len(listeners)) + list = append(list, server.collectPoolLogs) + + p, err := poolImpl.Initialize(ctx, spawnCmd, server.factory, opt, poolImpl.AddListeners(list...)) + if err != nil { + return nil, errors.E(op, err) + } + + return p, nil +} + +// creates relay and worker factory. +func (server *Plugin) initFactory() (worker.Factory, error) { + const op = errors.Op("network factory init") + if server.cfg.Relay == "" || server.cfg.Relay == "pipes" { + return pipe.NewPipeFactory(), nil + } + + dsn := strings.Split(server.cfg.Relay, "://") + if len(dsn) != 2 { + return nil, errors.E(op, errors.Network, errors.Str("invalid DSN (tcp://:6001, unix://file.sock)")) + } + + lsn, err := util.CreateListener(server.cfg.Relay) + if err != nil { + return nil, errors.E(op, errors.Network, err) + } + + switch dsn[0] { + // sockets group + case "unix": + return socket.NewSocketServer(lsn, server.cfg.RelayTimeout), nil + case "tcp": + return socket.NewSocketServer(lsn, server.cfg.RelayTimeout), nil + default: + return nil, errors.E(op, errors.Network, errors.Str("invalid DSN (tcp://:6001, unix://file.sock)")) + } +} + +func (server *Plugin) setEnv(e Env) []string { + env := append(os.Environ(), fmt.Sprintf("RR_RELAY=%s", server.cfg.Relay)) + for k, v := range e { + env = append(env, fmt.Sprintf("%s=%s", strings.ToUpper(k), v)) + } + + return env +} + +func (server *Plugin) collectPoolLogs(event interface{}) { + if we, ok := event.(events.PoolEvent); ok { + switch we.Event { + case events.EventMaxMemory: + server.log.Info("worker max memory reached", "pid", we.Payload.(worker.BaseProcess).Pid()) + case events.EventNoFreeWorkers: + server.log.Info("no free workers in pool", "error", we.Payload.(error).Error()) + case events.EventPoolError: + server.log.Info("pool error", "error", we.Payload.(error).Error()) + case events.EventSupervisorError: + server.log.Info("pool supervizor error", "error", we.Payload.(error).Error()) + case events.EventTTL: + server.log.Info("worker TTL reached", "pid", we.Payload.(worker.BaseProcess).Pid()) + case events.EventWorkerConstruct: + if _, ok := we.Payload.(error); ok { + server.log.Error("worker construction error", "error", we.Payload.(error).Error()) + return + } + server.log.Info("worker constructed", "pid", we.Payload.(worker.BaseProcess).Pid()) + case events.EventWorkerDestruct: + server.log.Info("worker destructed", "pid", we.Payload.(worker.BaseProcess).Pid()) + case events.EventExecTTL: + server.log.Info("EVENT EXEC TTL PLACEHOLDER") + case events.EventIdleTTL: + server.log.Info("worker IDLE timeout reached", "pid", we.Payload.(worker.BaseProcess).Pid()) + } + } + + if we, ok := event.(events.WorkerEvent); ok { + switch we.Event { + case events.EventWorkerError: + server.log.Info(we.Payload.(error).Error(), "pid", we.Worker.(worker.BaseProcess).Pid()) + case events.EventWorkerLog: + server.log.Info(strings.TrimRight(string(we.Payload.([]byte)), " \n\t"), "pid", we.Worker.(worker.BaseProcess).Pid()) + } + } +} + +func (server *Plugin) collectWorkerLogs(event interface{}) { + if we, ok := event.(events.WorkerEvent); ok { + switch we.Event { + case events.EventWorkerError: + server.log.Error(we.Payload.(error).Error(), "pid", we.Worker.(worker.BaseProcess).Pid()) + case events.EventWorkerLog: + server.log.Info(strings.TrimRight(string(we.Payload.([]byte)), " \n\t"), "pid", we.Worker.(worker.BaseProcess).Pid()) + } + } +} diff --git a/plugins/static/config.go b/plugins/static/config.go new file mode 100644 index 00000000..f5d26b2d --- /dev/null +++ b/plugins/static/config.go @@ -0,0 +1,76 @@ +package static + +import ( + "os" + "path" + "strings" + + "github.com/spiral/errors" +) + +// Config describes file location and controls access to them. +type Config struct { + Static struct { + // Dir contains name of directory to control access to. + Dir string + + // Forbid specifies list of file extensions which are forbidden for access. + // Example: .php, .exe, .bat, .htaccess and etc. + Forbid []string + + // Always specifies list of extensions which must always be served by static + // service, even if file not found. + Always []string + + // Request headers to add to every static. + Request map[string]string + + // Response headers to add to every static. + Response map[string]string + } +} + +// Valid returns nil if config is valid. +func (c *Config) Valid() error { + const op = errors.Op("static plugin validation") + st, err := os.Stat(c.Static.Dir) + if err != nil { + if os.IsNotExist(err) { + return errors.E(op, errors.Errorf("root directory '%s' does not exists", c.Static.Dir)) + } + + return err + } + + if !st.IsDir() { + return errors.E(op, errors.Errorf("invalid root directory '%s'", c.Static.Dir)) + } + + return nil +} + +// AlwaysForbid must return true if file extension is not allowed for the upload. +func (c *Config) AlwaysForbid(filename string) bool { + ext := strings.ToLower(path.Ext(filename)) + + for _, v := range c.Static.Forbid { + if ext == v { + return true + } + } + + return false +} + +// AlwaysServe must indicate that file is expected to be served by static service. +func (c *Config) AlwaysServe(filename string) bool { + ext := strings.ToLower(path.Ext(filename)) + + for _, v := range c.Static.Always { + if ext == v { + return true + } + } + + return false +} diff --git a/plugins/static/plugin.go b/plugins/static/plugin.go new file mode 100644 index 00000000..06b384df --- /dev/null +++ b/plugins/static/plugin.go @@ -0,0 +1,110 @@ +package static + +import ( + "net/http" + "path" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +// ID contains default service name. +const PluginName = "static" + +const RootPluginName = "http" + +// Plugin serves static files. Potentially convert into middleware? +type Plugin struct { + // server configuration (location, forbidden files and etc) + cfg *Config + + log logger.Logger + + // root is initiated http directory + root http.Dir +} + +// Init must return configure service and return true if service hasStatus enabled. Must return error in case of +// misconfiguration. Services must not be used without proper configuration pushed first. +func (s *Plugin) Init(cfg config.Configurer, log logger.Logger) error { + const op = errors.Op("static plugin init") + err := cfg.UnmarshalKey(RootPluginName, &s.cfg) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + s.log = log + s.root = http.Dir(s.cfg.Static.Dir) + + err = s.cfg.Valid() + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + return nil +} + +func (s *Plugin) Name() string { + return PluginName +} + +// middleware must return true if request/response pair is handled within the middleware. +func (s *Plugin) Middleware(next http.Handler) http.HandlerFunc { + // Define the http.HandlerFunc + return func(w http.ResponseWriter, r *http.Request) { + if s.cfg.Static.Request != nil { + for k, v := range s.cfg.Static.Request { + r.Header.Add(k, v) + } + } + + if s.cfg.Static.Response != nil { + for k, v := range s.cfg.Static.Response { + w.Header().Set(k, v) + } + } + + if !s.handleStatic(w, r) { + next.ServeHTTP(w, r) + } + } +} + +func (s *Plugin) handleStatic(w http.ResponseWriter, r *http.Request) bool { + fPath := path.Clean(r.URL.Path) + + if s.cfg.AlwaysForbid(fPath) { + return false + } + + f, err := s.root.Open(fPath) + if err != nil { + s.log.Error("file open error", "error", err) + if s.cfg.AlwaysServe(fPath) { + w.WriteHeader(404) + return true + } + + return false + } + defer func() { + err = f.Close() + if err != nil { + s.log.Error("file closing error", "error", err) + } + }() + + d, err := f.Stat() + if err != nil { + return false + } + + // do not serve directories + if d.IsDir() { + return false + } + + http.ServeContent(w, r, d.Name(), d.ModTime(), f) + return true +} diff --git a/tests/mocks/mock_log.go b/tests/mocks/mock_log.go new file mode 100644 index 00000000..e9631805 --- /dev/null +++ b/tests/mocks/mock_log.go @@ -0,0 +1,150 @@ +package mocks + +import ( + "reflect" + + "github.com/golang/mock/gomock" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +// MockLogger is a mock of Logger interface. +type MockLogger struct { + ctrl *gomock.Controller + recorder *MockLoggerMockRecorder +} + +// MockLoggerMockRecorder is the mock recorder for MockLogger. +type MockLoggerMockRecorder struct { + mock *MockLogger +} + +// NewMockLogger creates a new mock instance. +func NewMockLogger(ctrl *gomock.Controller) *MockLogger { + mock := &MockLogger{ctrl: ctrl} + mock.recorder = &MockLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLogger) EXPECT() *MockLoggerMockRecorder { + return m.recorder +} + +func (m *MockLogger) Init() error { + mock := &MockLogger{ctrl: m.ctrl} + mock.recorder = &MockLoggerMockRecorder{mock} + return nil +} + +// Debug mocks base method. +func (m *MockLogger) Debug(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debug", varargs...) +} + +// Warn mocks base method. +func (m *MockLogger) Warn(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Warn", varargs...) +} + +// Info mocks base method. +func (m *MockLogger) Info(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Info", varargs...) +} + +// Error mocks base method. +func (m *MockLogger) Error(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Error", varargs...) +} + +// Warn indicates an expected call of Warn. +func (mr *MockLoggerMockRecorder) Warn(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), varargs...) +} + +// Debug indicates an expected call of Debug. +func (mr *MockLoggerMockRecorder) Debug(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), varargs...) +} + +// Error indicates an expected call of Error. +func (mr *MockLoggerMockRecorder) Error(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), varargs...) +} + +func (mr *MockLoggerMockRecorder) Init() error { + return nil +} + +// Info indicates an expected call of Info. +func (mr *MockLoggerMockRecorder) Info(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), varargs...) +} + +// MockWithLogger is a mock of WithLogger interface. +type MockWithLogger struct { + ctrl *gomock.Controller + recorder *MockWithLoggerMockRecorder +} + +// MockWithLoggerMockRecorder is the mock recorder for MockWithLogger. +type MockWithLoggerMockRecorder struct { + mock *MockWithLogger +} + +// NewMockWithLogger creates a new mock instance. +func NewMockWithLogger(ctrl *gomock.Controller) *MockWithLogger { + mock := &MockWithLogger{ctrl: ctrl} + mock.recorder = &MockWithLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockWithLogger) EXPECT() *MockWithLoggerMockRecorder { + return m.recorder +} + +// With mocks base method. +func (m *MockWithLogger) With(keyvals ...interface{}) logger.Logger { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range keyvals { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "With", varargs...) + ret0, _ := ret[0].(logger.Logger) + return ret0 +} + +// With indicates an expected call of With. +func (mr *MockWithLoggerMockRecorder) With(keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "With", reflect.TypeOf((*MockWithLogger)(nil).With), keyvals...) +} diff --git a/tests/plugins/checker/configs/.rr-checker-init.yaml b/tests/plugins/checker/configs/.rr-checker-init.yaml new file mode 100755 index 00000000..1273529a --- /dev/null +++ b/tests/plugins/checker/configs/.rr-checker-init.yaml @@ -0,0 +1,31 @@ +rpc: + listen: tcp://127.0.0.1:6005 + disabled: false + +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +status: + address: "127.0.0.1:34333" +logs: + mode: development + level: debug +http: + debug: true + address: 127.0.0.1:11933 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s
\ No newline at end of file diff --git a/tests/plugins/checker/plugin_test.go b/tests/plugins/checker/plugin_test.go new file mode 100644 index 00000000..c346d91a --- /dev/null +++ b/tests/plugins/checker/plugin_test.go @@ -0,0 +1,190 @@ +package checker + +import ( + "io/ioutil" + "net" + "net/http" + "net/rpc" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" + "github.com/spiral/roadrunner/v2/plugins/checker" + "github.com/spiral/roadrunner/v2/plugins/config" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/logger" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +func TestStatusHttp(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-checker-init.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &checker.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("CheckerGetStatus", checkHTTPStatus) + + stopCh <- struct{}{} + wg.Wait() +} + +const resp = `Service: http: Status: 200 +Service: rpc not found` + +func checkHTTPStatus(t *testing.T) { + req, err := http.NewRequest("GET", "http://127.0.0.1:34333/v1/health?plugin=http&plugin=rpc", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, resp, string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestStatusRPC(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-checker-init.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &checker.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("CheckerGetStatusRpc", checkRPCStatus) + stopCh <- struct{}{} + wg.Wait() +} + +func checkRPCStatus(t *testing.T) { + conn, err := net.Dial("tcp", "127.0.0.1:6005") + assert.NoError(t, err) + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + + st := &checker.Status{} + + err = client.Call("status.Status", "http", &st) + assert.NoError(t, err) + assert.Equal(t, st.Code, 200) +} diff --git a/tests/plugins/config/.rr.yaml b/tests/plugins/config/.rr.yaml new file mode 100755 index 00000000..732a1366 --- /dev/null +++ b/tests/plugins/config/.rr.yaml @@ -0,0 +1,18 @@ +reload: + enabled: true + interval: 1s + patterns: [".php"] + services: + http: + recursive: true + ignore: ["vendor"] + patterns: [".php", ".go",".md",] + dirs: ["."] + jobs: + recursive: false + ignore: ["service/metrics"] + dirs: ["./jobs"] + rpc: + recursive: true + patterns: [".json"] + dirs: [""] diff --git a/tests/plugins/config/config_test.go b/tests/plugins/config/config_test.go new file mode 100755 index 00000000..858fcb80 --- /dev/null +++ b/tests/plugins/config/config_test.go @@ -0,0 +1,66 @@ +package config + +import ( + "os" + "os/signal" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/stretchr/testify/assert" +) + +func TestViperProvider_Init(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + vp := &config.Viper{} + vp.Path = ".rr.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Foo{}) + if err != nil { + t.Fatal(err) + } + + err = container.Init() + if err != nil { + t.Fatal(err) + } + + errCh, err := container.Serve() + if err != nil { + t.Fatal(err) + } + + // stop by CTRL+C + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + tt := time.NewTicker(time.Second * 2) + + for { + select { + case e := <-errCh: + assert.NoError(t, e.Error) + assert.NoError(t, container.Stop()) + return + case <-c: + er := container.Stop() + if er != nil { + panic(er) + } + return + case <-tt.C: + tt.Stop() + assert.NoError(t, container.Stop()) + return + } + } +} diff --git a/tests/plugins/config/plugin1.go b/tests/plugins/config/plugin1.go new file mode 100755 index 00000000..2afe79a4 --- /dev/null +++ b/tests/plugins/config/plugin1.go @@ -0,0 +1,53 @@ +package config + +import ( + "errors" + "time" + + "github.com/spiral/roadrunner/v2/plugins/config" +) + +// ReloadConfig is a Reload configuration point. +type ReloadConfig struct { + Interval time.Duration + Patterns []string + Services map[string]ServiceConfig +} + +type ServiceConfig struct { + Enabled bool + Recursive bool + Patterns []string + Dirs []string + Ignore []string +} + +type Foo struct { + configProvider config.Configurer +} + +// Depends on S2 and DB (S3 in the current case) +func (f *Foo) Init(p config.Configurer) error { + f.configProvider = p + return nil +} + +func (f *Foo) Serve() chan error { + errCh := make(chan error, 1) + + r := &ReloadConfig{} + err := f.configProvider.UnmarshalKey("reload", r) + if err != nil { + errCh <- err + } + + if len(r.Patterns) == 0 { + errCh <- errors.New("should be at least one pattern, but got 0") + } + + return errCh +} + +func (f *Foo) Stop() error { + return nil +} diff --git a/tests/plugins/gzip/configs/.rr-http-middlewareNotExist.yaml b/tests/plugins/gzip/configs/.rr-http-middlewareNotExist.yaml new file mode 100644 index 00000000..a2d12706 --- /dev/null +++ b/tests/plugins/gzip/configs/.rr-http-middlewareNotExist.yaml @@ -0,0 +1,25 @@ +server: + command: "php ../../psr-worker.php" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:18103 + maxRequestSize: 1024 + middleware: [ "gzip", "foo" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/gzip/configs/.rr-http-withGzip.yaml b/tests/plugins/gzip/configs/.rr-http-withGzip.yaml new file mode 100644 index 00000000..aff3efdb --- /dev/null +++ b/tests/plugins/gzip/configs/.rr-http-withGzip.yaml @@ -0,0 +1,25 @@ +server: + command: "php ../../psr-worker.php" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:18953 + maxRequestSize: 1024 + middleware: [ "gzip" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/gzip/plugin_test.go b/tests/plugins/gzip/plugin_test.go new file mode 100644 index 00000000..b09d430e --- /dev/null +++ b/tests/plugins/gzip/plugin_test.go @@ -0,0 +1,176 @@ +package gzip + +import ( + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "testing" + + "github.com/golang/mock/gomock" + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/gzip" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/spiral/roadrunner/v2/tests/mocks" + "github.com/stretchr/testify/assert" +) + +func TestGzipPlugin(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-withGzip.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("GzipCheckHeader", headerCheck) + + stopCh <- struct{}{} + wg.Wait() +} + +func headerCheck(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:18953", nil) + assert.NoError(t, err) + client := &http.Client{ + Transport: &http.Transport{ + DisableCompression: false, + }, + } + + r, err := client.Do(req) + assert.NoError(t, err) + assert.True(t, r.Uncompressed) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestMiddlewareNotExist(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-middlewareNotExist.yaml", + Prefix: "rr", + } + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Warn("requested middleware does not exist", "requested", "foo").AnyTimes() + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + stopCh <- struct{}{} + wg.Wait() +} diff --git a/tests/plugins/headers/configs/.rr-cors-headers.yaml b/tests/plugins/headers/configs/.rr-cors-headers.yaml new file mode 100644 index 00000000..9d4e8b36 --- /dev/null +++ b/tests/plugins/headers/configs/.rr-cors-headers.yaml @@ -0,0 +1,39 @@ +server: + command: "php ../../http/client.php headers pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:22855 + maxRequestSize: 1024 + middleware: [ "headers" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + # Additional HTTP headers and CORS control. + headers: + cors: + allowedOrigin: "*" + allowedHeaders: "*" + allowedMethods: "GET,POST,PUT,DELETE" + allowCredentials: true + exposedHeaders: "Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma" + maxAge: 600 + request: + "input": "custom-header" + response: + "output": "output-header" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error + diff --git a/tests/plugins/headers/configs/.rr-headers-init.yaml b/tests/plugins/headers/configs/.rr-headers-init.yaml new file mode 100644 index 00000000..8d63a187 --- /dev/null +++ b/tests/plugins/headers/configs/.rr-headers-init.yaml @@ -0,0 +1,39 @@ +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:33453 + maxRequestSize: 1024 + middleware: [ "headers" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + # Additional HTTP headers and CORS control. + headers: + cors: + allowedOrigin: "*" + allowedHeaders: "*" + allowedMethods: "GET,POST,PUT,DELETE" + allowCredentials: true + exposedHeaders: "Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma" + maxAge: 600 + request: + "Example-Request-Header": "Value" + response: + "X-Powered-By": "RoadRunner" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error + diff --git a/tests/plugins/headers/configs/.rr-req-headers.yaml b/tests/plugins/headers/configs/.rr-req-headers.yaml new file mode 100644 index 00000000..f8ab9bec --- /dev/null +++ b/tests/plugins/headers/configs/.rr-req-headers.yaml @@ -0,0 +1,32 @@ +server: + command: "php ../../http/client.php header pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:22655 + maxRequestSize: 1024 + middleware: [ "headers" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + # Additional HTTP headers and CORS control. + headers: + request: + "input": "custom-header" + response: + "output": "output-header" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error + diff --git a/tests/plugins/headers/configs/.rr-res-headers.yaml b/tests/plugins/headers/configs/.rr-res-headers.yaml new file mode 100644 index 00000000..36ab4eb3 --- /dev/null +++ b/tests/plugins/headers/configs/.rr-res-headers.yaml @@ -0,0 +1,32 @@ +server: + command: "php ../../http/client.php header pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:22455 + maxRequestSize: 1024 + middleware: [ "headers" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + # Additional HTTP headers and CORS control. + headers: + request: + "input": "custom-header" + response: + "output": "output-header" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error + diff --git a/tests/plugins/headers/headers_plugin_test.go b/tests/plugins/headers/headers_plugin_test.go new file mode 100644 index 00000000..a2ad3357 --- /dev/null +++ b/tests/plugins/headers/headers_plugin_test.go @@ -0,0 +1,367 @@ +package headers + +import ( + "io/ioutil" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/headers" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +func TestHeadersInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-headers-init.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &headers.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + stopCh <- struct{}{} + wg.Wait() +} + +func TestRequestHeaders(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-req-headers.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &headers.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("RequestHeaders", reqHeaders) + + stopCh <- struct{}{} + wg.Wait() +} + +func reqHeaders(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:22655?hello=value", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "CUSTOM-HEADER", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestResponseHeaders(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-res-headers.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &headers.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("ResponseHeaders", resHeaders) + + stopCh <- struct{}{} + wg.Wait() +} + +func resHeaders(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:22455?hello=value", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + assert.Equal(t, "output-header", r.Header.Get("output")) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "CUSTOM-HEADER", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestCORSHeaders(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-cors-headers.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &headers.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("CORSHeaders", corsHeaders) + t.Run("CORSHeadersPass", corsHeadersPass) + + stopCh <- struct{}{} + wg.Wait() +} + +func corsHeadersPass(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:22855", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) + assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Headers")) + assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Origin")) + assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) + + _, err = ioutil.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func corsHeaders(t *testing.T) { + req, err := http.NewRequest("OPTIONS", "http://localhost:22855", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) + assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Headers")) + assert.Equal(t, "GET,POST,PUT,DELETE", r.Header.Get("Access-Control-Allow-Methods")) + assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Origin")) + assert.Equal(t, "600", r.Header.Get("Access-Control-Max-Age")) + assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) + + _, err = ioutil.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + err = r.Body.Close() + assert.NoError(t, err) +} diff --git a/tests/plugins/http/attributes_test.go b/tests/plugins/http/attributes_test.go new file mode 100644 index 00000000..69200a30 --- /dev/null +++ b/tests/plugins/http/attributes_test.go @@ -0,0 +1,78 @@ +package http + +import ( + "net/http" + "testing" + + "github.com/spiral/roadrunner/v2/plugins/http/attributes" + "github.com/stretchr/testify/assert" +) + +func TestAllAttributes(t *testing.T) { + r := &http.Request{} + r = attributes.Init(r) + + err := attributes.Set(r, "key", "value") + if err != nil { + t.Errorf("error during the Set: error %v", err) + } + + assert.Equal(t, attributes.All(r), map[string]interface{}{ + "key": "value", + }) +} + +func TestAllAttributesNone(t *testing.T) { + r := &http.Request{} + r = attributes.Init(r) + + assert.Equal(t, attributes.All(r), map[string]interface{}{}) +} + +func TestAllAttributesNone2(t *testing.T) { + r := &http.Request{} + + assert.Equal(t, attributes.All(r), map[string]interface{}{}) +} + +func TestGetAttribute(t *testing.T) { + r := &http.Request{} + r = attributes.Init(r) + + err := attributes.Set(r, "key", "value") + if err != nil { + t.Errorf("error during the Set: error %v", err) + } + assert.Equal(t, attributes.Get(r, "key"), "value") +} + +func TestGetAttributeNone(t *testing.T) { + r := &http.Request{} + r = attributes.Init(r) + + assert.Equal(t, attributes.Get(r, "key"), nil) +} + +func TestGetAttributeNone2(t *testing.T) { + r := &http.Request{} + + assert.Equal(t, attributes.Get(r, "key"), nil) +} + +func TestSetAttribute(t *testing.T) { + r := &http.Request{} + r = attributes.Init(r) + + err := attributes.Set(r, "key", "value") + if err != nil { + t.Errorf("error during the Set: error %v", err) + } + assert.Equal(t, attributes.Get(r, "key"), "value") +} + +func TestSetAttributeNone(t *testing.T) { + r := &http.Request{} + err := attributes.Set(r, "key", "value") + assert.Error(t, err) + assert.Equal(t, attributes.Get(r, "key"), nil) +} diff --git a/tests/plugins/http/configs/.rr-broken-pipes.yaml b/tests/plugins/http/configs/.rr-broken-pipes.yaml new file mode 100644 index 00000000..8006cb5f --- /dev/null +++ b/tests/plugins/http/configs/.rr-broken-pipes.yaml @@ -0,0 +1,31 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../http/client.php broken pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:12384 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error + + diff --git a/tests/plugins/http/configs/.rr-env.yaml b/tests/plugins/http/configs/.rr-env.yaml new file mode 100644 index 00000000..1cce5dab --- /dev/null +++ b/tests/plugins/http/configs/.rr-env.yaml @@ -0,0 +1,33 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../http/client.php env pipes" + user: "" + group: "" + env: + "env_key": "ENV_VALUE" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:12084 + maxRequestSize: 1024 + middleware: [ "" ] + env: + "RR_HTTP": "true" + "env_key": "ENV_VALUE" + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error + diff --git a/tests/plugins/http/configs/.rr-fcgi-reqUri.yaml b/tests/plugins/http/configs/.rr-fcgi-reqUri.yaml new file mode 100644 index 00000000..725ae724 --- /dev/null +++ b/tests/plugins/http/configs/.rr-fcgi-reqUri.yaml @@ -0,0 +1,38 @@ +server: + command: "php ../../http/client.php request-uri pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: :8082 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 1 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8890 + redirect: false + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://127.0.0.1:6921 + http2: + enabled: false + h2c: false + maxConcurrentStreams: 128 +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/http/configs/.rr-fcgi.yaml b/tests/plugins/http/configs/.rr-fcgi.yaml new file mode 100644 index 00000000..ba119a88 --- /dev/null +++ b/tests/plugins/http/configs/.rr-fcgi.yaml @@ -0,0 +1,38 @@ +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: :8081 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 1 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8889 + redirect: false + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://0.0.0.0:6920 + http2: + enabled: false + h2c: false + maxConcurrentStreams: 128 +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/http/configs/.rr-h2c.yaml b/tests/plugins/http/configs/.rr-h2c.yaml new file mode 100644 index 00000000..287b7929 --- /dev/null +++ b/tests/plugins/http/configs/.rr-h2c.yaml @@ -0,0 +1,29 @@ +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: :8083 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 1 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + http2: + enabled: true + h2c: true + maxConcurrentStreams: 128 +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/http/configs/.rr-http.yaml b/tests/plugins/http/configs/.rr-http.yaml new file mode 100644 index 00000000..93f131f8 --- /dev/null +++ b/tests/plugins/http/configs/.rr-http.yaml @@ -0,0 +1,31 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:18903 + maxRequestSize: 1024 + middleware: [ "pluginMiddleware", "pluginMiddleware2" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error + + diff --git a/tests/plugins/http/configs/.rr-init.yaml b/tests/plugins/http/configs/.rr-init.yaml new file mode 100644 index 00000000..79303eab --- /dev/null +++ b/tests/plugins/http/configs/.rr-init.yaml @@ -0,0 +1,43 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:15395 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8892 + redirect: false + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://0.0.0.0:7921 + http2: + enabled: false + h2c: false + maxConcurrentStreams: 128 +logs: + mode: development + level: error + diff --git a/tests/plugins/http/configs/.rr-resetter.yaml b/tests/plugins/http/configs/.rr-resetter.yaml new file mode 100644 index 00000000..e2edafc6 --- /dev/null +++ b/tests/plugins/http/configs/.rr-resetter.yaml @@ -0,0 +1,30 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:10084 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error + diff --git a/tests/plugins/http/configs/.rr-ssl-push.yaml b/tests/plugins/http/configs/.rr-ssl-push.yaml new file mode 100644 index 00000000..81699a21 --- /dev/null +++ b/tests/plugins/http/configs/.rr-ssl-push.yaml @@ -0,0 +1,31 @@ +server: + command: "php ../../http/client.php push pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: :8086 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 1 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8894 + redirect: true + cert: fixtures/server.crt + key: fixtures/server.key +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/http/configs/.rr-ssl-redirect.yaml b/tests/plugins/http/configs/.rr-ssl-redirect.yaml new file mode 100644 index 00000000..fe6c5a86 --- /dev/null +++ b/tests/plugins/http/configs/.rr-ssl-redirect.yaml @@ -0,0 +1,31 @@ +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: :8087 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 1 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8895 + redirect: true + cert: fixtures/server.crt + key: fixtures/server.key +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/http/configs/.rr-ssl.yaml b/tests/plugins/http/configs/.rr-ssl.yaml new file mode 100644 index 00000000..3255383a --- /dev/null +++ b/tests/plugins/http/configs/.rr-ssl.yaml @@ -0,0 +1,38 @@ +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: :8085 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 1 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: 8893 + redirect: false + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://0.0.0.0:16920 + http2: + enabled: false + h2c: false + maxConcurrentStreams: 128 +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/http/fixtures/server.crt b/tests/plugins/http/fixtures/server.crt new file mode 100644 index 00000000..24d67fd7 --- /dev/null +++ b/tests/plugins/http/fixtures/server.crt @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICTTCCAdOgAwIBAgIJAOKyUd+llTRKMAoGCCqGSM49BAMCMGMxCzAJBgNVBAYT +AlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2Nv +MRMwEQYDVQQKDApSb2FkUnVubmVyMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTgw +OTMwMTMzNDUzWhcNMjgwOTI3MTMzNDUzWjBjMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzETMBEGA1UECgwK +Um9hZFJ1bm5lcjESMBAGA1UEAwwJbG9jYWxob3N0MHYwEAYHKoZIzj0CAQYFK4EE +ACIDYgAEVnbShsM+l5RR3wfWWmGhzuFGwNzKCk7i9xyobDIyBUxG/UUSfj7KKlUX +puDnDEtF5xXcepl744CyIAYFLOXHb5WqI4jCOzG0o9f/00QQ4bQudJOdbqV910QF +C2vb7Fxro1MwUTAdBgNVHQ4EFgQU9xUexnbB6ORKayA7Pfjzs33otsAwHwYDVR0j +BBgwFoAU9xUexnbB6ORKayA7Pfjzs33otsAwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAgNoADBlAjEAue3HhR/MUhxoa9tSDBtOJT3FYbDQswrsdqBTz97CGKst +e7XeZ3HMEvEXy0hGGEMhAjAqcD/4k9vViVppgWFtkk6+NFbm+Kw/QeeAiH5FgFSj +8xQcb+b7nPwNLp3JOkXkVd4= +-----END CERTIFICATE----- diff --git a/tests/plugins/http/fixtures/server.key b/tests/plugins/http/fixtures/server.key new file mode 100644 index 00000000..7501dd46 --- /dev/null +++ b/tests/plugins/http/fixtures/server.key @@ -0,0 +1,9 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQAIg== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDCQP8utxNbHR6xZOLAJgUhn88r6IrPqmN0MsgGJM/jePB+T9UhkmIU8 +PMm2HeScbcugBwYFK4EEACKhZANiAARWdtKGwz6XlFHfB9ZaYaHO4UbA3MoKTuL3 +HKhsMjIFTEb9RRJ+PsoqVRem4OcMS0XnFdx6mXvjgLIgBgUs5cdvlaojiMI7MbSj +1//TRBDhtC50k51upX3XRAULa9vsXGs= +-----END EC PRIVATE KEY----- diff --git a/tests/plugins/http/handler_test.go b/tests/plugins/http/handler_test.go new file mode 100644 index 00000000..18558296 --- /dev/null +++ b/tests/plugins/http/handler_test.go @@ -0,0 +1,1860 @@ +package http + +import ( + "bytes" + "context" + "io/ioutil" + "mime/multipart" + "net/url" + "os/exec" + "runtime" + "strings" + + "github.com/spiral/roadrunner/v2/pkg/pipe" + poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/stretchr/testify/assert" + + "net/http" + "os" + "testing" + "time" +) + +func TestHandler_Echo(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "echo", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + go func(server *http.Server) { + err := server.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }(hs) + time.Sleep(time.Millisecond * 10) + + body, r, err := get("http://localhost:8177/?hello=world") + assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", body) +} + +func Test_HandlerErrors(t *testing.T) { + _, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, nil) + assert.Error(t, err) +} + +func TestHandler_Headers(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "header", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8078", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 100) + + req, err := http.NewRequest("GET", "http://localhost:8078?hello=world", nil) + assert.NoError(t, err) + + req.Header.Add("input", "sample") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "world", r.Header.Get("Header")) + assert.Equal(t, "SAMPLE", string(b)) +} + +func TestHandler_Empty_User_Agent(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "user-agent", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8088", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + req, err := http.NewRequest("GET", "http://localhost:8088?hello=world", nil) + assert.NoError(t, err) + + req.Header.Add("user-agent", "") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "", string(b)) +} + +func TestHandler_User_Agent(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "user-agent", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8088", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + req, err := http.NewRequest("GET", "http://localhost:8088?hello=world", nil) + assert.NoError(t, err) + + req.Header.Add("User-Agent", "go-agent") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "go-agent", string(b)) +} + +func TestHandler_Cookies(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "cookie", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8079", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + req, err := http.NewRequest("GET", "http://localhost:8079", nil) + assert.NoError(t, err) + + req.AddCookie(&http.Cookie{Name: "input", Value: "input-value"}) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "INPUT-VALUE", string(b)) + + for _, c := range r.Cookies() { + assert.Equal(t, "output", c.Name) + assert.Equal(t, "cookie-output", c.Value) + } +} + +func TestHandler_JsonPayload_POST(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "payload", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8090", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + req, err := http.NewRequest( + "POST", + "http://localhost"+hs.Addr, + bytes.NewBufferString(`{"key":"value"}`), + ) + assert.NoError(t, err) + + req.Header.Add("Content-Type", "application/json") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, `{"value":"key"}`, string(b)) +} + +func TestHandler_JsonPayload_PUT(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "payload", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8081", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + req, err := http.NewRequest("PUT", "http://localhost"+hs.Addr, bytes.NewBufferString(`{"key":"value"}`)) + assert.NoError(t, err) + + req.Header.Add("Content-Type", "application/json") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, `{"value":"key"}`, string(b)) +} + +func TestHandler_JsonPayload_PATCH(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "payload", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8082", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + req, err := http.NewRequest("PATCH", "http://localhost"+hs.Addr, bytes.NewBufferString(`{"key":"value"}`)) + assert.NoError(t, err) + + req.Header.Add("Content-Type", "application/json") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, `{"value":"key"}`, string(b)) +} + +func TestHandler_FormData_POST(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "data", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8083", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 500) + + form := url.Values{} + + form.Add("key", "value") + form.Add("name[]", "name1") + form.Add("name[]", "name2") + form.Add("name[]", "name3") + form.Add("arr[x][y][z]", "y") + form.Add("arr[x][y][e]", "f") + form.Add("arr[c]p", "l") + form.Add("arr[c]z", "") + + req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) + assert.NoError(t, err) + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + // Sorted + assert.Equal(t, "{\"arr\":{\"c\":{\"p\":\"l\",\"z\":\"\"},\"x\":{\"y\":{\"e\":\"f\",\"z\":\"y\"}}},\"key\":\"value\",\"name\":[\"name1\",\"name2\",\"name3\"]}", string(b)) +} + +func TestHandler_FormData_POST_Overwrite(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "data", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8083", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + form := url.Values{} + + form.Add("key", "value") + form.Add("key", "value2") + form.Add("name[]", "name1") + form.Add("name[]", "name2") + form.Add("name[]", "name3") + form.Add("arr[x][y][z]", "y") + form.Add("arr[x][y][e]", "f") + form.Add("arr[c]p", "l") + form.Add("arr[c]z", "") + + req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) + assert.NoError(t, err) + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value2","name":["name1","name2","name3"]}`, string(b)) +} + +func TestHandler_FormData_POST_Form_UrlEncoded_Charset(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "data", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8083", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + form := url.Values{} + + form.Add("key", "value") + form.Add("name[]", "name1") + form.Add("name[]", "name2") + form.Add("name[]", "name3") + form.Add("arr[x][y][z]", "y") + form.Add("arr[x][y][e]", "f") + form.Add("arr[c]p", "l") + form.Add("arr[c]z", "") + + req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) + assert.NoError(t, err) + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) +} + +func TestHandler_FormData_PUT(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "data", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":17834", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 500) + + form := url.Values{} + + form.Add("key", "value") + form.Add("name[]", "name1") + form.Add("name[]", "name2") + form.Add("name[]", "name3") + form.Add("arr[x][y][z]", "y") + form.Add("arr[x][y][e]", "f") + form.Add("arr[c]p", "l") + form.Add("arr[c]z", "") + + req, err := http.NewRequest("PUT", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) + assert.NoError(t, err) + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) +} + +func TestHandler_FormData_PATCH(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "data", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8085", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + form := url.Values{} + + form.Add("key", "value") + form.Add("name[]", "name1") + form.Add("name[]", "name2") + form.Add("name[]", "name3") + form.Add("arr[x][y][z]", "y") + form.Add("arr[x][y][e]", "f") + form.Add("arr[c]p", "l") + form.Add("arr[c]z", "") + + req, err := http.NewRequest("PATCH", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) + assert.NoError(t, err) + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + assert.Equal(t, "{\"arr\":{\"c\":{\"p\":\"l\",\"z\":\"\"},\"x\":{\"y\":{\"e\":\"f\",\"z\":\"y\"}}},\"key\":\"value\",\"name\":[\"name1\",\"name2\",\"name3\"]}", string(b)) +} + +func TestHandler_Multipart_POST(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "data", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8019", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + var mb bytes.Buffer + w := multipart.NewWriter(&mb) + err = w.WriteField("key", "value") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("key", "value") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("name[]", "name1") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("name[]", "name2") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("name[]", "name3") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[x][y][z]", "y") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[x][y][e]", "f") + + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[c]p", "l") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[c]z", "") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.Close() + if err != nil { + t.Errorf("error closing the writer: error %v", err) + } + + req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) + assert.NoError(t, err) + + req.Header.Set("Content-Type", w.FormDataContentType()) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + assert.Equal(t, "{\"arr\":{\"c\":{\"p\":\"l\",\"z\":\"\"},\"x\":{\"y\":{\"e\":\"f\",\"z\":\"y\"}}},\"key\":\"value\",\"name\":[\"name1\",\"name2\",\"name3\"]}", string(b)) +} + +func TestHandler_Multipart_PUT(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "data", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8020", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 500) + + var mb bytes.Buffer + w := multipart.NewWriter(&mb) + err = w.WriteField("key", "value") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("key", "value") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("name[]", "name1") + + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("name[]", "name2") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("name[]", "name3") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[x][y][z]", "y") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[x][y][e]", "f") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[c]p", "l") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[c]z", "") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.Close() + if err != nil { + t.Errorf("error closing the writer: error %v", err) + } + + req, err := http.NewRequest("PUT", "http://localhost"+hs.Addr, &mb) + assert.NoError(t, err) + + req.Header.Set("Content-Type", w.FormDataContentType()) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) +} + +func TestHandler_Multipart_PATCH(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "data", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8021", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 500) + + var mb bytes.Buffer + w := multipart.NewWriter(&mb) + err = w.WriteField("key", "value") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("key", "value") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("name[]", "name1") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("name[]", "name2") + + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("name[]", "name3") + + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[x][y][z]", "y") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[x][y][e]", "f") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[c]p", "l") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.WriteField("arr[c]z", "") + if err != nil { + t.Errorf("error writing the field: error %v", err) + } + + err = w.Close() + if err != nil { + t.Errorf("error closing the writer: error %v", err) + } + + req, err := http.NewRequest("PATCH", "http://localhost"+hs.Addr, &mb) + assert.NoError(t, err) + + req.Header.Set("Content-Type", w.FormDataContentType()) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) +} + +func TestHandler_Error(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "error", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + _, r, err := get("http://localhost:8177/?hello=world") + assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() + assert.Equal(t, 500, r.StatusCode) +} + +func TestHandler_Error2(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "error2", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + _, r, err := get("http://localhost:8177/?hello=world") + assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() + assert.Equal(t, 500, r.StatusCode) +} + +func TestHandler_Error3(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "pid", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err = hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + b2 := &bytes.Buffer{} + for i := 0; i < 1024*1024; i++ { + b2.Write([]byte(" ")) + } + + req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, b2) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err = r.Body.Close() + if err != nil { + t.Errorf("error during the closing Body: error %v", err) + } + }() + + assert.NoError(t, err) + assert.Equal(t, 500, r.StatusCode) +} + +func TestHandler_ResponseDuration(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "echo", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + gotresp := make(chan interface{}) + h.AddListener(func(event interface{}) { + switch t := event.(type) { + case httpPlugin.ResponseEvent: + if t.Elapsed() > 0 { + close(gotresp) + } + default: + } + }) + + body, r, err := get("http://localhost:8177/?hello=world") + assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() + + <-gotresp + + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", body) +} + +func TestHandler_ResponseDurationDelayed(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "echoDelay", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + gotresp := make(chan interface{}) + h.AddListener(func(event interface{}) { + switch tp := event.(type) { + case httpPlugin.ResponseEvent: + if tp.Elapsed() > time.Second { + close(gotresp) + } + default: + } + }) + + body, r, err := get("http://localhost:8177/?hello=world") + assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() + <-gotresp + + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", body) +} + +func TestHandler_ErrorDuration(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "error", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err = hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + goterr := make(chan interface{}) + h.AddListener(func(event interface{}) { + switch tp := event.(type) { + case httpPlugin.ErrorEvent: + if tp.Elapsed() > 0 { + close(goterr) + } + default: + } + }) + + _, r, err := get("http://localhost:8177/?hello=world") + assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() + + <-goterr + + assert.Equal(t, 500, r.StatusCode) +} + +func TestHandler_IP(t *testing.T) { + trusted := []string{ + "10.0.0.0/8", + "127.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "::1/128", + "fc00::/7", + "fe80::/10", + } + + cidrs, err := httpPlugin.ParseCIDRs(trusted) + assert.NoError(t, err) + assert.NotNil(t, cidrs) + + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "ip", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, cidrs, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: "127.0.0.1:8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + body, r, err := get("http://127.0.0.1:8177/") + assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "127.0.0.1", body) +} + +func TestHandler_XRealIP(t *testing.T) { + trusted := []string{ + "10.0.0.0/8", + "127.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "::1/128", + "fc00::/7", + "fe80::/10", + } + + cidrs, err := httpPlugin.ParseCIDRs(trusted) + assert.NoError(t, err) + assert.NotNil(t, cidrs) + + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "ip", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, cidrs, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: "127.0.0.1:8179", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + body, r, err := getHeader("http://127.0.0.1:8179/", map[string]string{ + "X-Real-Ip": "200.0.0.1", + }) + + assert.NoError(t, err) + defer func() { + _ = r.Body.Close() + }() + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "200.0.0.1", body) +} + +func TestHandler_XForwardedFor(t *testing.T) { + trusted := []string{ + "10.0.0.0/8", + "127.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "100.0.0.0/16", + "200.0.0.0/16", + "::1/128", + "fc00::/7", + "fe80::/10", + } + + cidrs, err := httpPlugin.ParseCIDRs(trusted) + assert.NoError(t, err) + assert.NotNil(t, cidrs) + + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "ip", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, cidrs, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: "127.0.0.1:8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + body, r, err := getHeader("http://127.0.0.1:8177/", map[string]string{ + "X-Forwarded-For": "100.0.0.1, 200.0.0.1, invalid, 101.0.0.1", + }) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "101.0.0.1", body) + _ = r.Body.Close() + + body, r, err = getHeader("http://127.0.0.1:8177/", map[string]string{ + "X-Forwarded-For": "100.0.0.1, 200.0.0.1, 101.0.0.1, invalid", + }) + + assert.NoError(t, err) + _ = r.Body.Close() + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "101.0.0.1", body) +} + +func TestHandler_XForwardedFor_NotTrustedRemoteIp(t *testing.T) { + trusted := []string{ + "10.0.0.0/8", + } + + cidrs, err := httpPlugin.ParseCIDRs(trusted) + assert.NoError(t, err) + assert.NotNil(t, cidrs) + + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "ip", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, cidrs, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: "127.0.0.1:8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + body, r, err := getHeader("http://127.0.0.1:8177/", map[string]string{ + "X-Forwarded-For": "100.0.0.1, 200.0.0.1, invalid, 101.0.0.1", + }) + + assert.NoError(t, err) + _ = r.Body.Close() + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "127.0.0.1", body) +} + +func BenchmarkHandler_Listen_Echo(b *testing.B) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "echo", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: int64(runtime.NumCPU()), + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + b.Fatal(err) + } + defer func() { + pool.Destroy(context.Background()) + }() + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(b, err) + + hs := &http.Server{Addr: ":8177", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + b.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err = hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + b.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + b.ResetTimer() + b.ReportAllocs() + bb := "WORLD" + for n := 0; n < b.N; n++ { + r, err := http.Get("http://localhost:8177/?hello=world") + if err != nil { + b.Fail() + } + // Response might be nil here + if r != nil { + br, err := ioutil.ReadAll(r.Body) + if err != nil { + b.Errorf("error reading Body: error %v", err) + } + if string(br) != bb { + b.Fail() + } + err = r.Body.Close() + if err != nil { + b.Errorf("error closing the Body: error %v", err) + } + } else { + b.Errorf("got nil response") + } + } +} diff --git a/tests/plugins/http/http_plugin_test.go b/tests/plugins/http/http_plugin_test.go new file mode 100644 index 00000000..88857df5 --- /dev/null +++ b/tests/plugins/http/http_plugin_test.go @@ -0,0 +1,1247 @@ +package http + +import ( + "bytes" + "crypto/tls" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "net/rpc" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/spiral/endure" + goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" + "github.com/spiral/roadrunner/v2/interfaces/events" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/informer" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/resetter" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/spiral/roadrunner/v2/tests/mocks" + "github.com/spiral/roadrunner/v2/tools" + "github.com/yookoala/gofast" + + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/stretchr/testify/assert" +) + +var sslClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, //nolint:gosec + }, + }, +} + +func TestHTTPInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + rIn := makeConfig("6001", "15395", "7921", "8892", "false", "false", "php ../../http/client.php echo pipes") + cfg := &config.Viper{ + ReadInCfg: rIn, + Type: "yaml", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + stopCh <- struct{}{} + wg.Wait() +} + +func TestHTTPInformerReset(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-resetter.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &informer.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("HTTPInformerTest", informerTest) + t.Run("HTTPEchoTestBefore", echoHTTP) + t.Run("HTTPResetTest", resetTest) + t.Run("HTTPEchoTestAfter", echoHTTP) + + stopCh <- struct{}{} + + wg.Wait() +} + +func echoHTTP(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:10084?hello=world", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func resetTest(t *testing.T) { + conn, err := net.Dial("tcp", "127.0.0.1:6001") + assert.NoError(t, err) + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + // WorkerList contains list of workers. + + var ret bool + err = client.Call("resetter.Reset", "http", &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + var services []string + err = client.Call("resetter.List", nil, &services) + assert.NoError(t, err) + if services[0] != "http" { + t.Fatal("no enough services") + } +} + +func informerTest(t *testing.T) { + conn, err := net.Dial("tcp", "127.0.0.1:6001") + assert.NoError(t, err) + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + // WorkerList contains list of workers. + list := struct { + // Workers is list of workers. + Workers []tools.ProcessState `json:"workers"` + }{} + + err = client.Call("informer.Workers", "http", &list) + assert.NoError(t, err) + assert.Len(t, list.Workers, 2) +} + +func TestSSL(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-ssl.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("SSLEcho", sslEcho) + t.Run("SSLNoRedirect", sslNoRedirect) + t.Run("fCGIecho", fcgiEcho) + + stopCh <- struct{}{} + wg.Wait() +} + +func sslNoRedirect(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:8085?hello=world", nil) + assert.NoError(t, err) + + r, err := sslClient.Do(req) + assert.NoError(t, err) + + assert.Nil(t, r.TLS) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) + + err2 := r.Body.Close() + if err2 != nil { + t.Errorf("fail to close the Body: error %v", err2) + } +} + +func sslEcho(t *testing.T) { + req, err := http.NewRequest("GET", "https://localhost:8893?hello=world", nil) + assert.NoError(t, err) + + r, err := sslClient.Do(req) + assert.NoError(t, err) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) + + err2 := r.Body.Close() + if err2 != nil { + t.Errorf("fail to close the Body: error %v", err2) + } +} + +func fcgiEcho(t *testing.T) { + fcgiConnFactory := gofast.SimpleConnFactory("tcp", "0.0.0.0:16920") + + fcgiHandler := gofast.NewHandler( + gofast.BasicParamsMap(gofast.BasicSession), + gofast.SimpleClientFactory(fcgiConnFactory, 0), + ) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "http://site.local/?hello=world", nil) + fcgiHandler.ServeHTTP(w, req) + + body, err := ioutil.ReadAll(w.Result().Body) //nolint:bodyclose + + defer func() { + _ = w.Result().Body.Close() + w.Body.Reset() + }() + + assert.NoError(t, err) + assert.Equal(t, 201, w.Result().StatusCode) //nolint:bodyclose + assert.Equal(t, "WORLD", string(body)) +} + +func TestSSLRedirect(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-ssl-redirect.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("SSLRedirect", sslRedirect) + + stopCh <- struct{}{} + wg.Wait() +} + +func sslRedirect(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:8087?hello=world", nil) + assert.NoError(t, err) + + r, err := sslClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, r.TLS) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) + + err2 := r.Body.Close() + if err2 != nil { + t.Errorf("fail to close the Body: error %v", err2) + } +} + +func TestSSLPushPipes(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-ssl-push.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("SSLPush", sslPush) + + stopCh <- struct{}{} + wg.Wait() +} + +func sslPush(t *testing.T) { + req, err := http.NewRequest("GET", "https://localhost:8894?hello=world", nil) + assert.NoError(t, err) + + r, err := sslClient.Do(req) + assert.NoError(t, err) + + assert.NotNil(t, r.TLS) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, "", r.Header.Get("Http2-Push")) + + assert.NoError(t, err) + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) + + err2 := r.Body.Close() + if err2 != nil { + t.Errorf("fail to close the Body: error %v", err2) + } +} + +func TestFastCGI_RequestUri(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-fcgi-reqUri.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("FastCGIServiceRequestUri", fcgiReqURI) + + stopCh <- struct{}{} + wg.Wait() +} + +func fcgiReqURI(t *testing.T) { + time.Sleep(time.Second * 2) + fcgiConnFactory := gofast.SimpleConnFactory("tcp", "127.0.0.1:6921") + + fcgiHandler := gofast.NewHandler( + gofast.BasicParamsMap(gofast.BasicSession), + gofast.SimpleClientFactory(fcgiConnFactory, 0), + ) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "http://site.local/hello-world", nil) + fcgiHandler.ServeHTTP(w, req) + + body, err := ioutil.ReadAll(w.Result().Body) //nolint:bodyclose + assert.NoError(t, err) + assert.Equal(t, 200, w.Result().StatusCode) //nolint:bodyclose + assert.Equal(t, "http://site.local/hello-world", string(body)) +} + +func TestH2CUpgrade(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-h2c.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("H2cUpgrade", h2cUpgrade) + + stopCh <- struct{}{} + wg.Wait() +} + +func h2cUpgrade(t *testing.T) { + req, err := http.NewRequest("PRI", "http://localhost:8083?hello=world", nil) + if err != nil { + t.Fatal(err) + } + + req.Header.Add("Upgrade", "h2c") + req.Header.Add("Connection", "HTTP2-Settings") + req.Header.Add("HTTP2-Settings", "") + + r, err2 := http.DefaultClient.Do(req) + if err2 != nil { + t.Fatal(err) + } + + assert.Equal(t, "101 Switching Protocols", r.Status) + + err3 := r.Body.Close() + if err3 != nil { + t.Fatal(err) + } +} + +func TestH2C(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-h2c.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("H2c", h2c) + + stopCh <- struct{}{} + wg.Wait() +} + +func h2c(t *testing.T) { + req, err := http.NewRequest("PRI", "http://localhost:8083?hello=world", nil) + if err != nil { + t.Fatal(err) + } + + req.Header.Add("Connection", "HTTP2-Settings") + req.Header.Add("HTTP2-Settings", "") + + r, err2 := http.DefaultClient.Do(req) + if err2 != nil { + t.Fatal(err) + } + + assert.Equal(t, "201 Created", r.Status) + + err3 := r.Body.Close() + if err3 != nil { + t.Fatal(err) + } +} + +func TestHttpMiddleware(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &PluginMiddleware{}, + &PluginMiddleware2{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("MiddlewareTest", middleware) + + stopCh <- struct{}{} + wg.Wait() +} + +func middleware(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:18903?hello=world", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) + + req, err = http.NewRequest("GET", "http://localhost:18903/halt", nil) + assert.NoError(t, err) + + r, err = http.DefaultClient.Do(req) + assert.NoError(t, err) + b, err = ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, 500, r.StatusCode) + assert.Equal(t, "halted", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestHttpEchoErr(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + rIn := ` +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +server: + command: "php ../../http/client.php echoerr pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:34999 + maxRequestSize: 1024 + middleware: [ "pluginMiddleware", "pluginMiddleware2" ] + uploads: + forbid: [ "" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error +` + + cfg := &config.Viper{ + Path: "", + Prefix: "", + Type: "yaml", + ReadInCfg: []byte(rIn), + } + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Info("worker destructed", "pid", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").MinTimes(1) + mockLogger.EXPECT().Info("WORLD", "pid", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Debug("worker event received", "event", events.EventWorkerLog, "worker state", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &PluginMiddleware{}, + &PluginMiddleware2{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("HttpEchoError", echoError) + + stopCh <- struct{}{} + wg.Wait() +} + +func echoError(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:34999?hello=world", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestHttpEnvVariables(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-env.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &PluginMiddleware{}, + &PluginMiddleware2{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second * 1) + t.Run("EnvVariablesTest", envVarsTest) + + stopCh <- struct{}{} + wg.Wait() +} + +func envVarsTest(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:12084", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "ENV_VALUE", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestHttpBrokenPipes(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-broken-pipes.yaml", + Prefix: "rr", + Type: "yaml", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &PluginMiddleware{}, + &PluginMiddleware2{}, + ) + assert.NoError(t, err) + + err = cont.Init() + assert.Error(t, err) + + _, err = cont.Serve() + assert.Error(t, err) +} + +func get(url string) (string, *http.Response, error) { + r, err := http.Get(url) //nolint:gosec + if err != nil { + return "", nil, err + } + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return "", nil, err + } + defer func() { + _ = r.Body.Close() + }() + return string(b), r, err +} + +// get request and return body +func getHeader(url string, h map[string]string) (string, *http.Response, error) { + req, err := http.NewRequest("GET", url, bytes.NewBuffer(nil)) + if err != nil { + return "", nil, err + } + + for k, v := range h { + req.Header.Set(k, v) + } + + r, err := http.DefaultClient.Do(req) + if err != nil { + return "", nil, err + } + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return "", nil, err + } + + err = r.Body.Close() + if err != nil { + return "", nil, err + } + return string(b), r, err +} + +func makeConfig(rpcPort, httpPort, fcgiPort, sslPort, redirect, http2Enabled, command string) []byte { + return []byte(fmt.Sprintf(` +rpc: + listen: tcp://127.0.0.1:%s + disabled: false + +server: + command: "%s" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + address: 127.0.0.1:%s + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s + + ssl: + port: %s + redirect: %s + cert: fixtures/server.crt + key: fixtures/server.key + # rootCa: root.crt + fcgi: + address: tcp://0.0.0.0:%s + http2: + enabled: %s + h2c: false + maxConcurrentStreams: 128 +logs: + mode: development + level: error +`, rpcPort, command, httpPort, sslPort, redirect, fcgiPort, http2Enabled)) +} diff --git a/tests/plugins/http/parse_test.go b/tests/plugins/http/parse_test.go new file mode 100644 index 00000000..5cc1ce32 --- /dev/null +++ b/tests/plugins/http/parse_test.go @@ -0,0 +1,54 @@ +package http + +import ( + "testing" + + "github.com/spiral/roadrunner/v2/plugins/http" +) + +var samples = []struct { + in string + out []string +}{ + {"key", []string{"key"}}, + {"key[subkey]", []string{"key", "subkey"}}, + {"key[subkey]value", []string{"key", "subkey", "value"}}, + {"key[subkey][value]", []string{"key", "subkey", "value"}}, + {"key[subkey][value][]", []string{"key", "subkey", "value", ""}}, + {"key[subkey] [value][]", []string{"key", "subkey", "value", ""}}, + {"key [ subkey ] [ value ] [ ]", []string{"key", "subkey", "value", ""}}, +} + +func Test_FetchIndexes(t *testing.T) { + for i := 0; i < len(samples); i++ { + r := http.FetchIndexes(samples[i].in) + if !same(r, samples[i].out) { + t.Errorf("got %q, want %q", r, samples[i].out) + } + } +} + +func BenchmarkConfig_FetchIndexes(b *testing.B) { + for _, tt := range samples { + for n := 0; n < b.N; n++ { + r := http.FetchIndexes(tt.in) + if !same(r, tt.out) { + b.Fail() + } + } + } +} + +func same(in, out []string) bool { + if len(in) != len(out) { + return false + } + + for i, v := range in { + if v != out[i] { + return false + } + } + + return true +} diff --git a/tests/plugins/http/plugin1.go b/tests/plugins/http/plugin1.go new file mode 100644 index 00000000..0ec31211 --- /dev/null +++ b/tests/plugins/http/plugin1.go @@ -0,0 +1,27 @@ +package http + +import ( + "github.com/spiral/roadrunner/v2/plugins/config" +) + +type Plugin1 struct { + config config.Configurer +} + +func (p1 *Plugin1) Init(cfg config.Configurer) error { + p1.config = cfg + return nil +} + +func (p1 *Plugin1) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (p1 *Plugin1) Stop() error { + return nil +} + +func (p1 *Plugin1) Name() string { + return "http_test.plugin1" +} diff --git a/tests/plugins/http/plugin_middleware.go b/tests/plugins/http/plugin_middleware.go new file mode 100644 index 00000000..8d02524d --- /dev/null +++ b/tests/plugins/http/plugin_middleware.go @@ -0,0 +1,61 @@ +package http + +import ( + "net/http" + + "github.com/spiral/roadrunner/v2/plugins/config" +) + +type PluginMiddleware struct { + config config.Configurer +} + +func (p *PluginMiddleware) Init(cfg config.Configurer) error { + p.config = cfg + return nil +} + +func (p *PluginMiddleware) Middleware(next http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/halt" { + w.WriteHeader(500) + _, err := w.Write([]byte("halted")) + if err != nil { + panic("error writing the data to the http reply") + } + } else { + next.ServeHTTP(w, r) + } + } +} + +func (p *PluginMiddleware) Name() string { + return "pluginMiddleware" +} + +type PluginMiddleware2 struct { + config config.Configurer +} + +func (p *PluginMiddleware2) Init(cfg config.Configurer) error { + p.config = cfg + return nil +} + +func (p *PluginMiddleware2) Middleware(next http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/boom" { + w.WriteHeader(555) + _, err := w.Write([]byte("boom")) + if err != nil { + panic("error writing the data to the http reply") + } + } else { + next.ServeHTTP(w, r) + } + } +} + +func (p *PluginMiddleware2) Name() string { + return "pluginMiddleware2" +} diff --git a/tests/plugins/http/response_test.go b/tests/plugins/http/response_test.go new file mode 100644 index 00000000..a9cbf91a --- /dev/null +++ b/tests/plugins/http/response_test.go @@ -0,0 +1,163 @@ +package http + +import ( + "bytes" + "errors" + "net/http" + "testing" + + "github.com/spiral/roadrunner/v2/pkg/payload" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/stretchr/testify/assert" +) + +type testWriter struct { + h http.Header + buf bytes.Buffer + wroteHeader bool + code int + err error + pushErr error + pushes []string +} + +func (tw *testWriter) Header() http.Header { return tw.h } + +func (tw *testWriter) Write(p []byte) (int, error) { + if !tw.wroteHeader { + tw.WriteHeader(http.StatusOK) + } + + n, e := tw.buf.Write(p) + if e == nil { + e = tw.err + } + + return n, e +} + +func (tw *testWriter) WriteHeader(code int) { tw.wroteHeader = true; tw.code = code } + +func (tw *testWriter) Push(target string, opts *http.PushOptions) error { + tw.pushes = append(tw.pushes, target) + + return tw.pushErr +} + +func TestNewResponse_Error(t *testing.T) { + r, err := httpPlugin.NewResponse(payload.Payload{Context: []byte(`invalid payload`)}) + assert.Error(t, err) + assert.Nil(t, r) +} + +func TestNewResponse_Write(t *testing.T) { + r, err := httpPlugin.NewResponse(payload.Payload{ + Context: []byte(`{"headers":{"key":["value"]},"status": 301}`), + Body: []byte(`sample body`), + }) + + assert.NoError(t, err) + assert.NotNil(t, r) + + w := &testWriter{h: http.Header(make(map[string][]string))} + assert.NoError(t, r.Write(w)) + + assert.Equal(t, 301, w.code) + assert.Equal(t, "value", w.h.Get("key")) + assert.Equal(t, "sample body", w.buf.String()) +} + +func TestNewResponse_Stream(t *testing.T) { + r, err := httpPlugin.NewResponse(payload.Payload{ + Context: []byte(`{"headers":{"key":["value"]},"status": 301}`), + }) + + // r is pointer, so, it might be nil + if r == nil { + t.Fatal("response is nil") + } + + r.Body = &bytes.Buffer{} + r.Body.(*bytes.Buffer).WriteString("hello world") + + assert.NoError(t, err) + assert.NotNil(t, r) + + w := &testWriter{h: http.Header(make(map[string][]string))} + assert.NoError(t, r.Write(w)) + + assert.Equal(t, 301, w.code) + assert.Equal(t, "value", w.h.Get("key")) + assert.Equal(t, "hello world", w.buf.String()) +} + +func TestNewResponse_StreamError(t *testing.T) { + r, err := httpPlugin.NewResponse(payload.Payload{ + Context: []byte(`{"headers":{"key":["value"]},"status": 301}`), + }) + + // r is pointer, so, it might be nil + if r == nil { + t.Fatal("response is nil") + } + + r.Body = &bytes.Buffer{} + r.Body.(*bytes.Buffer).WriteString("hello world") + + assert.NoError(t, err) + assert.NotNil(t, r) + + w := &testWriter{h: http.Header(make(map[string][]string)), err: errors.New("error")} + assert.Error(t, r.Write(w)) +} + +func TestWrite_HandlesPush(t *testing.T) { + r, err := httpPlugin.NewResponse(payload.Payload{ + Context: []byte(`{"headers":{"Http2-Push":["/test.js"],"content-type":["text/html"]},"status": 200}`), + }) + + assert.NoError(t, err) + assert.NotNil(t, r) + + w := &testWriter{h: http.Header(make(map[string][]string))} + assert.NoError(t, r.Write(w)) + + assert.Nil(t, w.h["Http2-Push"]) + assert.Equal(t, []string{"/test.js"}, w.pushes) +} + +func TestWrite_HandlesTrailers(t *testing.T) { + r, err := httpPlugin.NewResponse(payload.Payload{ + Context: []byte(`{"headers":{"Trailer":["foo, bar", "baz"],"foo":["test"],"bar":["demo"]},"status": 200}`), + }) + + assert.NoError(t, err) + assert.NotNil(t, r) + + w := &testWriter{h: http.Header(make(map[string][]string))} + assert.NoError(t, r.Write(w)) + + assert.Nil(t, w.h[httpPlugin.TrailerHeaderKey]) + assert.Nil(t, w.h["foo"]) //nolint:golint,staticcheck + assert.Nil(t, w.h["baz"]) //nolint:golint,staticcheck + + assert.Equal(t, "test", w.h.Get("Trailer:foo")) + assert.Equal(t, "demo", w.h.Get("Trailer:bar")) +} + +func TestWrite_HandlesHandlesWhitespacesInTrailer(t *testing.T) { + r, err := httpPlugin.NewResponse(payload.Payload{ + Context: []byte( + `{"headers":{"Trailer":["foo\t,bar , baz"],"foo":["a"],"bar":["b"],"baz":["c"]},"status": 200}`), + }) + + assert.NoError(t, err) + assert.NotNil(t, r) + + w := &testWriter{h: http.Header(make(map[string][]string))} + assert.NoError(t, r.Write(w)) + + assert.Equal(t, "a", w.h.Get("Trailer:foo")) + assert.Equal(t, "b", w.h.Get("Trailer:bar")) + assert.Equal(t, "c", w.h.Get("Trailer:baz")) +} diff --git a/tests/plugins/http/uploads_config_test.go b/tests/plugins/http/uploads_config_test.go new file mode 100644 index 00000000..e76078ee --- /dev/null +++ b/tests/plugins/http/uploads_config_test.go @@ -0,0 +1,26 @@ +package http + +import ( + "os" + "testing" + + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/stretchr/testify/assert" +) + +func TestFsConfig_Forbids(t *testing.T) { + cfg := httpPlugin.UploadsConfig{Forbid: []string{".php"}} + + assert.True(t, cfg.Forbids("index.php")) + assert.True(t, cfg.Forbids("index.PHP")) + assert.True(t, cfg.Forbids("phpadmin/index.bak.php")) + assert.False(t, cfg.Forbids("index.html")) +} + +func TestFsConfig_TmpFallback(t *testing.T) { + cfg := httpPlugin.UploadsConfig{Dir: "test"} + assert.Equal(t, "test", cfg.TmpDir()) + + cfg = httpPlugin.UploadsConfig{Dir: ""} + assert.Equal(t, os.TempDir(), cfg.TmpDir()) +} diff --git a/tests/plugins/http/uploads_test.go b/tests/plugins/http/uploads_test.go new file mode 100644 index 00000000..7bb25cbf --- /dev/null +++ b/tests/plugins/http/uploads_test.go @@ -0,0 +1,432 @@ +package http + +import ( + "bytes" + "context" + "crypto/sha512" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "os" + "os/exec" + "testing" + "time" + + j "github.com/json-iterator/go" + "github.com/spiral/roadrunner/v2/pkg/pipe" + poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/stretchr/testify/assert" +) + +var json = j.ConfigCompatibleWithStandardLibrary + +const testFile = "uploads_test.go" + +func TestHandler_Upload_File(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "upload", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8021", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + var mb bytes.Buffer + w := multipart.NewWriter(&mb) + + f := mustOpen(testFile) + defer func() { + err := f.Close() + if err != nil { + t.Errorf("failed to close a file: error %v", err) + } + }() + fw, err := w.CreateFormFile("upload", f.Name()) + assert.NotNil(t, fw) + assert.NoError(t, err) + _, err = io.Copy(fw, f) + if err != nil { + t.Errorf("error copying the file: error %v", err) + } + + err = w.Close() + if err != nil { + t.Errorf("error closing the file: error %v", err) + } + + req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) + assert.NoError(t, err) + + req.Header.Set("Content-Type", w.FormDataContentType()) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error closing the Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + fs := fileString(testFile, 0, "application/octet-stream") + + assert.Equal(t, `{"upload":`+fs+`}`, string(b)) +} + +func TestHandler_Upload_NestedFile(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "upload", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8021", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + var mb bytes.Buffer + w := multipart.NewWriter(&mb) + + f := mustOpen(testFile) + defer func() { + err := f.Close() + if err != nil { + t.Errorf("failed to close a file: error %v", err) + } + }() + fw, err := w.CreateFormFile("upload[x][y][z][]", f.Name()) + assert.NotNil(t, fw) + assert.NoError(t, err) + _, err = io.Copy(fw, f) + if err != nil { + t.Errorf("error copying the file: error %v", err) + } + + err = w.Close() + if err != nil { + t.Errorf("error closing the file: error %v", err) + } + + req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) + assert.NoError(t, err) + + req.Header.Set("Content-Type", w.FormDataContentType()) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error closing the Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + fs := fileString(testFile, 0, "application/octet-stream") + + assert.Equal(t, `{"upload":{"x":{"y":{"z":[`+fs+`]}}}}`, string(b)) +} + +func TestHandler_Upload_File_NoTmpDir(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "upload", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: "-------", + Forbid: []string{}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8021", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + var mb bytes.Buffer + w := multipart.NewWriter(&mb) + + f := mustOpen(testFile) + defer func() { + err := f.Close() + if err != nil { + t.Errorf("failed to close a file: error %v", err) + } + }() + fw, err := w.CreateFormFile("upload", f.Name()) + assert.NotNil(t, fw) + assert.NoError(t, err) + _, err = io.Copy(fw, f) + if err != nil { + t.Errorf("error copying the file: error %v", err) + } + + err = w.Close() + if err != nil { + t.Errorf("error closing the file: error %v", err) + } + + req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) + assert.NoError(t, err) + + req.Header.Set("Content-Type", w.FormDataContentType()) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error closing the Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + fs := fileString(testFile, 6, "application/octet-stream") + + assert.Equal(t, `{"upload":`+fs+`}`, string(b)) +} + +func TestHandler_Upload_File_Forbids(t *testing.T) { + pool, err := poolImpl.Initialize(context.Background(), + func() *exec.Cmd { return exec.Command("php", "../../http/client.php", "upload", "pipes") }, + pipe.NewPipeFactory(), + poolImpl.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second * 1000, + DestroyTimeout: time.Second * 1000, + }) + if err != nil { + t.Fatal(err) + } + + h, err := httpPlugin.NewHandler(1024, httpPlugin.UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{".go"}, + }, nil, pool) + assert.NoError(t, err) + + hs := &http.Server{Addr: ":8021", Handler: h} + defer func() { + err := hs.Shutdown(context.Background()) + if err != nil { + t.Errorf("error during the shutdown: error %v", err) + } + }() + + go func() { + err := hs.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + t.Errorf("error listening the interface: error %v", err) + } + }() + time.Sleep(time.Millisecond * 10) + + var mb bytes.Buffer + w := multipart.NewWriter(&mb) + + f := mustOpen(testFile) + defer func() { + err := f.Close() + if err != nil { + t.Errorf("failed to close a file: error %v", err) + } + }() + fw, err := w.CreateFormFile("upload", f.Name()) + assert.NotNil(t, fw) + assert.NoError(t, err) + _, err = io.Copy(fw, f) + if err != nil { + t.Errorf("error copying the file: error %v", err) + } + + err = w.Close() + if err != nil { + t.Errorf("error closing the file: error %v", err) + } + + req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) + assert.NoError(t, err) + + req.Header.Set("Content-Type", w.FormDataContentType()) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer func() { + err := r.Body.Close() + if err != nil { + t.Errorf("error closing the Body: error %v", err) + } + }() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + fs := fileString(testFile, 8, "application/octet-stream") + + assert.Equal(t, `{"upload":`+fs+`}`, string(b)) +} + +func Test_FileExists(t *testing.T) { + assert.True(t, exists(testFile)) + assert.False(t, exists("uploads_test.")) +} + +func mustOpen(f string) *os.File { + r, err := os.Open(f) + if err != nil { + panic(err) + } + return r +} + +type fInfo struct { + Name string `json:"name"` + Size int64 `json:"size"` + Mime string `json:"mime"` + Error int `json:"error"` + Sha512 string `json:"sha512,omitempty"` +} + +func fileString(f string, errNo int, mime string) string { + s, err := os.Stat(f) + if err != nil { + fmt.Println(fmt.Errorf("error stat the file, error: %v", err)) + } + + ff, err := os.Open(f) + if err != nil { + fmt.Println(fmt.Errorf("error opening the file, error: %v", err)) + } + + defer func() { + er := ff.Close() + if er != nil { + fmt.Println(fmt.Errorf("error closing the file, error: %v", er)) + } + }() + + h := sha512.New() + _, err = io.Copy(h, ff) + if err != nil { + fmt.Println(fmt.Errorf("error copying the file, error: %v", err)) + } + + v := &fInfo{ + Name: s.Name(), + Size: s.Size(), + Error: errNo, + Mime: mime, + Sha512: hex.EncodeToString(h.Sum(nil)), + } + + if errNo != 0 { + v.Sha512 = "" + v.Size = 0 + } + + r, err := json.Marshal(v) + if err != nil { + fmt.Println(fmt.Errorf("error marshalling fInfo, error: %v", err)) + } + return string(r) +} + +// exists if file exists. +func exists(path string) bool { + if _, err := os.Stat(path); os.IsNotExist(err) { + return false + } + return true +} diff --git a/tests/plugins/informer/.rr-informer.yaml b/tests/plugins/informer/.rr-informer.yaml new file mode 100644 index 00000000..e50ca9c9 --- /dev/null +++ b/tests/plugins/informer/.rr-informer.yaml @@ -0,0 +1,16 @@ +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_CONFIG": "/some/place/on/the/C134" + "RR_CONFIG2": "C138" + relay: "pipes" + relayTimeout: "20s" + +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/informer/informer_test.go b/tests/plugins/informer/informer_test.go new file mode 100644 index 00000000..d9fc2143 --- /dev/null +++ b/tests/plugins/informer/informer_test.go @@ -0,0 +1,108 @@ +package informer + +import ( + "net" + "net/rpc" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/informer" + "github.com/spiral/roadrunner/v2/plugins/logger" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/spiral/roadrunner/v2/tools" + "github.com/stretchr/testify/assert" +) + +func TestInformerInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Viper{ + Path: ".rr-informer.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &server.Plugin{}, + &logger.ZapLogger{}, + &informer.Plugin{}, + &rpcPlugin.Plugin{}, + &Plugin1{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + stopCh := make(chan struct{}, 1) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("InformerRpcTest", informerRPCTest) + + stopCh <- struct{}{} + wg.Wait() +} + +func informerRPCTest(t *testing.T) { + conn, err := net.Dial("tcp", "127.0.0.1:6001") + assert.NoError(t, err) + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + // WorkerList contains list of workers. + list := struct { + // Workers is list of workers. + Workers []tools.ProcessState `json:"workers"` + }{} + + err = client.Call("informer.Workers", "informer.plugin1", &list) + assert.NoError(t, err) + assert.Len(t, list.Workers, 10) +} diff --git a/tests/plugins/informer/test_plugin.go b/tests/plugins/informer/test_plugin.go new file mode 100644 index 00000000..ba281d02 --- /dev/null +++ b/tests/plugins/informer/test_plugin.go @@ -0,0 +1,59 @@ +package informer + +import ( + "context" + "time" + + "github.com/spiral/roadrunner/v2/interfaces/worker" + poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/server" +) + +var testPoolConfig = poolImpl.Config{ + NumWorkers: 10, + MaxJobs: 100, + AllocateTimeout: time.Second * 10, + DestroyTimeout: time.Second * 10, + Supervisor: &poolImpl.SupervisorConfig{ + WatchTick: 60, + TTL: 1000, + IdleTTL: 10, + ExecTTL: 10, + MaxWorkerMemory: 1000, + }, +} + +// Gauge ////////////// +type Plugin1 struct { + config config.Configurer + server server.Server +} + +func (p1 *Plugin1) Init(cfg config.Configurer, server server.Server) error { + p1.config = cfg + p1.server = server + return nil +} + +func (p1 *Plugin1) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (p1 *Plugin1) Stop() error { + return nil +} + +func (p1 *Plugin1) Name() string { + return "informer.plugin1" +} + +func (p1 *Plugin1) Workers() []worker.BaseProcess { + pool, err := p1.server.NewWorkerPool(context.Background(), testPoolConfig, nil) + if err != nil { + panic(err) + } + + return pool.Workers() +} diff --git a/tests/plugins/logger/.rr.yaml b/tests/plugins/logger/.rr.yaml new file mode 100644 index 00000000..cb555ec3 --- /dev/null +++ b/tests/plugins/logger/.rr.yaml @@ -0,0 +1,3 @@ +logs: + mode: development + level: debug
\ No newline at end of file diff --git a/tests/plugins/logger/logger_test.go b/tests/plugins/logger/logger_test.go new file mode 100644 index 00000000..cc788be3 --- /dev/null +++ b/tests/plugins/logger/logger_test.go @@ -0,0 +1,79 @@ +package logger + +import ( + "os" + "os/signal" + "sync" + "testing" + + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/stretchr/testify/assert" +) + +func TestLogger(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = ".rr.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = container.Init() + if err != nil { + t.Fatal(err) + } + + errCh, err := container.Serve() + if err != nil { + t.Fatal(err) + } + + // stop by CTRL+C + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + stopCh := make(chan struct{}, 1) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + defer wg.Done() + for { + select { + case e := <-errCh: + assert.NoError(t, e.Error) + assert.NoError(t, container.Stop()) + return + case <-c: + err = container.Stop() + assert.NoError(t, err) + return + case <-stopCh: + assert.NoError(t, container.Stop()) + return + } + } + }() + + stopCh <- struct{}{} + wg.Wait() +} diff --git a/tests/plugins/logger/plugin.go b/tests/plugins/logger/plugin.go new file mode 100644 index 00000000..9ddf9ec9 --- /dev/null +++ b/tests/plugins/logger/plugin.go @@ -0,0 +1,40 @@ +package logger + +import ( + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +type Plugin struct { + config config.Configurer + log logger.Logger +} + +func (p1 *Plugin) Init(cfg config.Configurer, log logger.Logger) error { + p1.config = cfg + p1.log = log + return nil +} + +func (p1 *Plugin) Serve() chan error { + errCh := make(chan error, 1) + p1.log.Error("error", "test", errors.E(errors.Str("test"))) + p1.log.Info("error", "test", errors.E(errors.Str("test"))) + p1.log.Debug("error", "test", errors.E(errors.Str("test"))) + p1.log.Warn("error", "test", errors.E(errors.Str("test"))) + + p1.log.Error("error", "test") + p1.log.Info("error", "test") + p1.log.Debug("error", "test") + p1.log.Warn("error", "test") + return errCh +} + +func (p1 *Plugin) Stop() error { + return nil +} + +func (p1 *Plugin) Name() string { + return "logger_plugin" +} diff --git a/tests/plugins/metrics/.rr-test.yaml b/tests/plugins/metrics/.rr-test.yaml new file mode 100644 index 00000000..37c50395 --- /dev/null +++ b/tests/plugins/metrics/.rr-test.yaml @@ -0,0 +1,16 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +metrics: + # prometheus client address (path /metrics added automatically) + address: localhost:2112 + collect: + app_metric: + type: histogram + help: "Custom application metric" + labels: [ "type" ] + buckets: [ 0.1, 0.2, 0.3, 1.0 ] +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/metrics/docker-compose.yml b/tests/plugins/metrics/docker-compose.yml new file mode 100644 index 00000000..610633b4 --- /dev/null +++ b/tests/plugins/metrics/docker-compose.yml @@ -0,0 +1,7 @@ +version: '3.7' + +services: + prometheus: + image: prom/prometheus + ports: + - 9090:9090 diff --git a/tests/plugins/metrics/metrics_test.go b/tests/plugins/metrics/metrics_test.go new file mode 100644 index 00000000..c94d51bc --- /dev/null +++ b/tests/plugins/metrics/metrics_test.go @@ -0,0 +1,739 @@ +package metrics + +import ( + "io/ioutil" + "net" + "net/http" + "net/rpc" + "os" + "os/signal" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/metrics" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/stretchr/testify/assert" +) + +const dialAddr = "127.0.0.1:6001" +const dialNetwork = "tcp" +const getAddr = "http://localhost:2112/metrics" + +// get request and return body +func get() (string, error) { + r, err := http.Get(getAddr) + if err != nil { + return "", err + } + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return "", err + } + + err = r.Body.Close() + if err != nil { + return "", err + } + // unsafe + return string(b), err +} + +func TestMetricsInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Viper{} + cfg.Prefix = "rr" + cfg.Path = ".rr-test.yaml" + + err = cont.RegisterAll( + cfg, + &metrics.Plugin{}, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &Plugin1{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + tt := time.NewTimer(time.Second * 5) + + out, err := get() + assert.NoError(t, err) + + assert.Contains(t, out, "go_gc_duration_seconds") + + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } +} + +func TestMetricsGaugeCollector(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Viper{} + cfg.Prefix = "rr" + cfg.Path = ".rr-test.yaml" + + err = cont.RegisterAll( + cfg, + &metrics.Plugin{}, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &Plugin1{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + time.Sleep(time.Second) + tt := time.NewTimer(time.Second * 5) + + out, err := get() + assert.NoError(t, err) + assert.Contains(t, out, "my_gauge 100") + assert.Contains(t, out, "my_gauge2 100") + + out, err = get() + assert.NoError(t, err) + assert.Contains(t, out, "go_gc_duration_seconds") + + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } +} + +func TestMetricsDifferentRPCCalls(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Viper{} + cfg.Prefix = "rr" + cfg.Path = ".rr-test.yaml" + + err = cont.RegisterAll( + cfg, + &metrics.Plugin{}, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + go func() { + tt := time.NewTimer(time.Minute * 3) + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("DeclareMetric", declareMetricsTest) + genericOut, err := get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "test_metrics_named_collector") + + t.Run("AddMetric", addMetricsTest) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "test_metrics_named_collector 10000") + + t.Run("SetMetric", setMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "user_gauge_collector 100") + + t.Run("VectorMetric", vectorMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "gauge_2_collector{section=\"first\",type=\"core\"} 100") + + t.Run("MissingSection", missingSection) + t.Run("SetWithoutLabels", setWithoutLabels) + t.Run("SetOnHistogram", setOnHistogram) + t.Run("MetricSub", subMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "sub_gauge_subMetric 1") + + t.Run("SubVector", subVector) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "sub_gauge_subVector{section=\"first\",type=\"core\"} 1") + + t.Run("RegisterHistogram", registerHistogram) + + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, `TYPE histogram_registerHistogram`) + + // check buckets + assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="0.1"} 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="0.2"} 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="0.5"} 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="+Inf"} 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_sum 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_count 0`) + + t.Run("CounterMetric", counterMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "HELP default_default_counter_CounterMetric test_counter") + assert.Contains(t, genericOut, `default_default_counter_CounterMetric{section="section2",type="type2"}`) + + t.Run("ObserveMetric", observeMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "observe_observeMetric") + + t.Run("ObserveMetricNotEnoughLabels", observeMetricNotEnoughLabels) + + close(sig) +} + +func observeMetricNotEnoughLabels(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "observe_observeMetricNotEnoughLabels", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Help: "test_observe", + Type: metrics.Histogram, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + assert.Error(t, client.Call("metrics.Observe", metrics.Metric{ + Name: "observe_observeMetric", + Value: 100.0, + Labels: []string{"test"}, + }, &ret)) + assert.False(t, ret) +} + +func observeMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "observe_observeMetric", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Help: "test_observe", + Type: metrics.Histogram, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + assert.NoError(t, client.Call("metrics.Observe", metrics.Metric{ + Name: "observe_observeMetric", + Value: 100.0, + Labels: []string{"test", "test2"}, + }, &ret)) + assert.True(t, ret) +} + +func counterMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "counter_CounterMetric", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Help: "test_counter", + Type: metrics.Counter, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + assert.NoError(t, client.Call("metrics.Add", metrics.Metric{ + Name: "counter_CounterMetric", + Value: 100.0, + Labels: []string{"type2", "section2"}, + }, &ret)) + assert.True(t, ret) +} + +func registerHistogram(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "histogram_registerHistogram", + Collector: metrics.Collector{ + Help: "test_histogram", + Type: metrics.Histogram, + Buckets: []float64{0.1, 0.2, 0.5}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "histogram_registerHistogram", + Value: 10000, + Labels: nil, + } + + err = client.Call("metrics.Add", m, &ret) + assert.Error(t, err) + assert.False(t, ret) +} + +func subVector(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "sub_gauge_subVector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m := metrics.Metric{ + Name: "sub_gauge_subVector", + Value: 100000, + Labels: []string{"core", "first"}, + } + + err = client.Call("metrics.Add", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m = metrics.Metric{ + Name: "sub_gauge_subVector", + Value: 99999, + Labels: []string{"core", "first"}, + } + + err = client.Call("metrics.Sub", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func subMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "sub_gauge_subMetric", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m := metrics.Metric{ + Name: "sub_gauge_subMetric", + Value: 100000, + } + + err = client.Call("metrics.Add", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m = metrics.Metric{ + Name: "sub_gauge_subMetric", + Value: 99999, + } + + err = client.Call("metrics.Sub", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func setOnHistogram(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "histogram_setOnHistogram", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Histogram, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "gauge_setOnHistogram", + Value: 100.0, + } + + err = client.Call("metrics.Set", m, &ret) // expected 2 label values but got 1 in []string{"missing"} + assert.Error(t, err) + assert.False(t, ret) +} + +func setWithoutLabels(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "gauge_setWithoutLabels", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "gauge_setWithoutLabels", + Value: 100.0, + } + + err = client.Call("metrics.Set", m, &ret) // expected 2 label values but got 1 in []string{"missing"} + assert.Error(t, err) + assert.False(t, ret) +} + +func missingSection(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "gauge_missing_section_collector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "gauge_missing_section_collector", + Value: 100.0, + Labels: []string{"missing"}, + } + + err = client.Call("metrics.Set", m, &ret) // expected 2 label values but got 1 in []string{"missing"} + assert.Error(t, err) + assert.False(t, ret) +} + +func vectorMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "gauge_2_collector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "gauge_2_collector", + Value: 100.0, + Labels: []string{"core", "first"}, + } + + err = client.Call("metrics.Set", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func setMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "user_gauge_collector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m := metrics.Metric{ + Name: "user_gauge_collector", + Value: 100.0, + } + + err = client.Call("metrics.Set", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func addMetricsTest(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + m := metrics.Metric{ + Name: "test_metrics_named_collector", + Value: 10000, + Labels: nil, + } + + err = client.Call("metrics.Add", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func declareMetricsTest(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "test_metrics_named_collector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Counter, + Help: "NO HELP!", + Labels: nil, + Buckets: nil, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} diff --git a/tests/plugins/metrics/plugin1.go b/tests/plugins/metrics/plugin1.go new file mode 100644 index 00000000..ae024a8a --- /dev/null +++ b/tests/plugins/metrics/plugin1.go @@ -0,0 +1,46 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/spiral/roadrunner/v2/plugins/config" +) + +// Gauge ////////////// +type Plugin1 struct { + config config.Configurer +} + +func (p1 *Plugin1) Init(cfg config.Configurer) error { + p1.config = cfg + return nil +} + +func (p1 *Plugin1) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (p1 *Plugin1) Stop() error { + return nil +} + +func (p1 *Plugin1) Name() string { + return "metrics_test.plugin1" +} + +func (p1 *Plugin1) MetricsCollector() []prometheus.Collector { + collector := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "my_gauge", + Help: "My gauge value", + }) + + collector.Set(100) + + collector2 := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "my_gauge2", + Help: "My gauge2 value", + }) + + collector2.Set(100) + return []prometheus.Collector{collector, collector2} +} diff --git a/tests/plugins/mocks/mock_log.go b/tests/plugins/mocks/mock_log.go new file mode 100644 index 00000000..e9631805 --- /dev/null +++ b/tests/plugins/mocks/mock_log.go @@ -0,0 +1,150 @@ +package mocks + +import ( + "reflect" + + "github.com/golang/mock/gomock" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +// MockLogger is a mock of Logger interface. +type MockLogger struct { + ctrl *gomock.Controller + recorder *MockLoggerMockRecorder +} + +// MockLoggerMockRecorder is the mock recorder for MockLogger. +type MockLoggerMockRecorder struct { + mock *MockLogger +} + +// NewMockLogger creates a new mock instance. +func NewMockLogger(ctrl *gomock.Controller) *MockLogger { + mock := &MockLogger{ctrl: ctrl} + mock.recorder = &MockLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLogger) EXPECT() *MockLoggerMockRecorder { + return m.recorder +} + +func (m *MockLogger) Init() error { + mock := &MockLogger{ctrl: m.ctrl} + mock.recorder = &MockLoggerMockRecorder{mock} + return nil +} + +// Debug mocks base method. +func (m *MockLogger) Debug(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debug", varargs...) +} + +// Warn mocks base method. +func (m *MockLogger) Warn(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Warn", varargs...) +} + +// Info mocks base method. +func (m *MockLogger) Info(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Info", varargs...) +} + +// Error mocks base method. +func (m *MockLogger) Error(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Error", varargs...) +} + +// Warn indicates an expected call of Warn. +func (mr *MockLoggerMockRecorder) Warn(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), varargs...) +} + +// Debug indicates an expected call of Debug. +func (mr *MockLoggerMockRecorder) Debug(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), varargs...) +} + +// Error indicates an expected call of Error. +func (mr *MockLoggerMockRecorder) Error(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), varargs...) +} + +func (mr *MockLoggerMockRecorder) Init() error { + return nil +} + +// Info indicates an expected call of Info. +func (mr *MockLoggerMockRecorder) Info(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), varargs...) +} + +// MockWithLogger is a mock of WithLogger interface. +type MockWithLogger struct { + ctrl *gomock.Controller + recorder *MockWithLoggerMockRecorder +} + +// MockWithLoggerMockRecorder is the mock recorder for MockWithLogger. +type MockWithLoggerMockRecorder struct { + mock *MockWithLogger +} + +// NewMockWithLogger creates a new mock instance. +func NewMockWithLogger(ctrl *gomock.Controller) *MockWithLogger { + mock := &MockWithLogger{ctrl: ctrl} + mock.recorder = &MockWithLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockWithLogger) EXPECT() *MockWithLoggerMockRecorder { + return m.recorder +} + +// With mocks base method. +func (m *MockWithLogger) With(keyvals ...interface{}) logger.Logger { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range keyvals { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "With", varargs...) + ret0, _ := ret[0].(logger.Logger) + return ret0 +} + +// With indicates an expected call of With. +func (mr *MockWithLoggerMockRecorder) With(keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "With", reflect.TypeOf((*MockWithLogger)(nil).With), keyvals...) +} diff --git a/tests/plugins/redis/plugin1.go b/tests/plugins/redis/plugin1.go new file mode 100644 index 00000000..e50213e5 --- /dev/null +++ b/tests/plugins/redis/plugin1.go @@ -0,0 +1,43 @@ +package redis + +import ( + "context" + "time" + + "github.com/go-redis/redis/v8" + "github.com/spiral/errors" + redisPlugin "github.com/spiral/roadrunner/v2/plugins/redis" +) + +type Plugin1 struct { + redisClient redis.UniversalClient +} + +func (p *Plugin1) Init(redis redisPlugin.Redis) error { + p.redisClient = redis.GetClient() + return nil +} + +func (p *Plugin1) Serve() chan error { + const op = errors.Op("plugin1 serve") + errCh := make(chan error, 1) + p.redisClient.Set(context.Background(), "foo", "bar", time.Minute) + + stringCmd := p.redisClient.Get(context.Background(), "foo") + data, err := stringCmd.Result() + if err != nil { + errCh <- errors.E(op, err) + return errCh + } + + if data != "bar" { + errCh <- errors.E(op, errors.Str("no such key")) + return errCh + } + + return errCh +} + +func (p *Plugin1) Stop() error { + return nil +} diff --git a/tests/plugins/redis/redis_plugin_test.go b/tests/plugins/redis/redis_plugin_test.go new file mode 100644 index 00000000..eba05752 --- /dev/null +++ b/tests/plugins/redis/redis_plugin_test.go @@ -0,0 +1,120 @@ +package redis + +import ( + "fmt" + "os" + "os/signal" + "sync" + "syscall" + "testing" + + "github.com/alicebob/miniredis/v2" + "github.com/golang/mock/gomock" + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/redis" + "github.com/spiral/roadrunner/v2/tests/mocks" + "github.com/stretchr/testify/assert" +) + +func redisConfig(port string) string { + cfg := ` +redis: + addrs: + - 'localhost:%s' + master_name: '' + username: '' + password: '' + db: 0 + sentinel_password: '' + route_by_latency: false + route_randomly: false + dial_timeout: 0 + max_retries: 1 + min_retry_backoff: 0 + max_retry_backoff: 0 + pool_size: 0 + min_idle_conns: 0 + max_conn_age: 0 + read_timeout: 0 + write_timeout: 0 + pool_timeout: 0 + idle_timeout: 0 + idle_check_freq: 0 + read_only: false +` + return fmt.Sprintf(cfg, port) +} + +func TestRedisInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + s, err := miniredis.Run() + assert.NoError(t, err) + + c := redisConfig(s.Port()) + + cfg := &config.Viper{} + cfg.Type = "yaml" + cfg.ReadInCfg = []byte(c) + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + err = cont.RegisterAll( + cfg, + mockLogger, + &redis.Plugin{}, + &Plugin1{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + stopCh <- struct{}{} + wg.Wait() +} diff --git a/tests/plugins/reload/config_test.go b/tests/plugins/reload/config_test.go new file mode 100644 index 00000000..72c11070 --- /dev/null +++ b/tests/plugins/reload/config_test.go @@ -0,0 +1,63 @@ +package reload + +import ( + "testing" + "time" + + "github.com/spiral/roadrunner/v2/plugins/reload" + "github.com/stretchr/testify/assert" +) + +func Test_Config_Valid(t *testing.T) { + services := make(map[string]reload.ServiceConfig) + services["test"] = reload.ServiceConfig{ + Recursive: false, + Patterns: nil, + Dirs: nil, + Ignore: nil, + } + + cfg := &reload.Config{ + Interval: time.Second, + Patterns: nil, + Services: services, + } + assert.NoError(t, cfg.Valid()) +} + +func Test_Fake_ServiceConfig(t *testing.T) { + services := make(map[string]reload.ServiceConfig) + cfg := &reload.Config{ + Interval: time.Microsecond, + Patterns: nil, + Services: services, + } + assert.Error(t, cfg.Valid()) +} + +func Test_Interval(t *testing.T) { + services := make(map[string]reload.ServiceConfig) + services["test"] = reload.ServiceConfig{ + Enabled: false, + Recursive: false, + Patterns: nil, + Dirs: nil, + Ignore: nil, + } + + cfg := &reload.Config{ + Interval: time.Millisecond, // should crash here + Patterns: nil, + Services: services, + } + assert.Error(t, cfg.Valid()) +} + +func Test_NoServiceConfig(t *testing.T) { + cfg := &reload.Config{ + Interval: time.Second, + Patterns: nil, + Services: nil, + } + assert.Error(t, cfg.Valid()) +} diff --git a/tests/plugins/reload/configs/.rr-reload-2.yaml b/tests/plugins/reload/configs/.rr-reload-2.yaml new file mode 100644 index 00000000..ab32b2d1 --- /dev/null +++ b/tests/plugins/reload/configs/.rr-reload-2.yaml @@ -0,0 +1,44 @@ +server: + command: php ../../psr-worker-bench.php + user: '' + group: '' + env: + RR_HTTP: 'true' + relay: pipes + relayTimeout: 20s +http: + debug: true + address: '127.0.0.1:27388' + maxRequestSize: 1024 + middleware: + - '' + uploads: + forbid: + - .php + - .exe + - .bat + trustedSubnets: + - 10.0.0.0/8 + - 127.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - '::1/128' + - 'fc00::/7' + - 'fe80::/10' + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error +reload: + interval: 2s + patterns: + - .txt + services: + http: + dirs: + - './unit_tests' + recursive: true diff --git a/tests/plugins/reload/configs/.rr-reload-3.yaml b/tests/plugins/reload/configs/.rr-reload-3.yaml new file mode 100644 index 00000000..881d9b88 --- /dev/null +++ b/tests/plugins/reload/configs/.rr-reload-3.yaml @@ -0,0 +1,46 @@ +server: + command: php ../../psr-worker-bench.php + user: '' + group: '' + env: + RR_HTTP: 'true' + relay: pipes + relayTimeout: 20s +http: + debug: true + address: '127.0.0.1:37388' + maxRequestSize: 1024 + middleware: + - '' + uploads: + forbid: + - .php + - .exe + - .bat + trustedSubnets: + - 10.0.0.0/8 + - 127.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - '::1/128' + - 'fc00::/7' + - 'fe80::/10' + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error +reload: + interval: 2s + patterns: + - .txt + services: + http: + dirs: + - './unit_tests' + - './unit_tests_copied' + - './dir1' + recursive: true diff --git a/tests/plugins/reload/configs/.rr-reload-4.yaml b/tests/plugins/reload/configs/.rr-reload-4.yaml new file mode 100644 index 00000000..d47df558 --- /dev/null +++ b/tests/plugins/reload/configs/.rr-reload-4.yaml @@ -0,0 +1,46 @@ +server: + command: php ../../psr-worker-bench.php + user: '' + group: '' + env: + RR_HTTP: 'true' + relay: pipes + relayTimeout: 20s +http: + debug: true + address: '127.0.0.1:22766' + maxRequestSize: 1024 + middleware: + - '' + uploads: + forbid: + - .php + - .exe + - .bat + trustedSubnets: + - 10.0.0.0/8 + - 127.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - '::1/128' + - 'fc00::/7' + - 'fe80::/10' + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error +reload: + interval: 2s + patterns: + - .aaa + services: + http: + dirs: + - './unit_tests' + - './unit_tests_copied' + - './dir1' + recursive: false diff --git a/tests/plugins/reload/configs/.rr-reload.yaml b/tests/plugins/reload/configs/.rr-reload.yaml new file mode 100644 index 00000000..794c41f2 --- /dev/null +++ b/tests/plugins/reload/configs/.rr-reload.yaml @@ -0,0 +1,44 @@ +server: + command: php ../../psr-worker-bench.php + user: '' + group: '' + env: + RR_HTTP: 'true' + relay: pipes + relayTimeout: 20s +http: + debug: true + address: '127.0.0.1:22388' + maxRequestSize: 1024 + middleware: + - '' + uploads: + forbid: + - .php + - .exe + - .bat + trustedSubnets: + - 10.0.0.0/8 + - 127.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - '::1/128' + - 'fc00::/7' + - 'fe80::/10' + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error +reload: + interval: 1s + patterns: + - .txt + services: + http: + dirs: + - './unit_tests' + recursive: true diff --git a/tests/plugins/reload/reload_plugin_test.go b/tests/plugins/reload/reload_plugin_test.go new file mode 100644 index 00000000..c83d4787 --- /dev/null +++ b/tests/plugins/reload/reload_plugin_test.go @@ -0,0 +1,827 @@ +package reload + +import ( + "io" + "io/ioutil" + "math/rand" + "os" + "os/signal" + "path/filepath" + "strconv" + "sync" + "syscall" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/reload" + "github.com/spiral/roadrunner/v2/plugins/resetter" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/spiral/roadrunner/v2/tests/mocks" + "github.com/stretchr/testify/assert" +) + +const testDir string = "unit_tests" +const testCopyToDir string = "unit_tests_copied" +const dir1 string = "dir1" +const hugeNumberOfFiles uint = 500 + +func TestReloadInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + err = os.Mkdir(testDir, 0755) + assert.NoError(t, err) + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1) + mockLogger.EXPECT().Debug("file was created", "path", gomock.Any(), "name", "file.txt", "size", gomock.Any()).Times(2) + mockLogger.EXPECT().Debug("file was added to watcher", "path", gomock.Any(), "name", "file.txt", "size", gomock.Any()).Times(2) + mockLogger.EXPECT().Info("HTTP plugin got restart request. Restarting...").Times(1) + mockLogger.EXPECT().Info("HTTP workers Pool successfully restarted").Times(1) + mockLogger.EXPECT().Info("HTTP listeners successfully re-added").Times(1) + mockLogger.EXPECT().Info("HTTP plugin successfully restarted").Times(1) + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + assert.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("ReloadTestInit", reloadTestInit) + + stopCh <- struct{}{} + wg.Wait() + assert.NoError(t, freeResources(testDir)) +} + +func reloadTestInit(t *testing.T) { + err := ioutil.WriteFile(filepath.Join(testDir, "file.txt"), //nolint:gosec + []byte{}, 0755) + assert.NoError(t, err) +} + +func TestReloadHugeNumberOfFiles(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + + assert.NoError(t, os.Mkdir(testDir, 0755)) + assert.NoError(t, os.Mkdir(testCopyToDir, 0755)) + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("file added to the list of removed files", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1) + mockLogger.EXPECT().Debug("file was created", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Debug("file was updated", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Debug("file was added to watcher", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Info("HTTP plugin got restart request. Restarting...").MinTimes(1) + mockLogger.EXPECT().Info("HTTP workers Pool successfully restarted").MinTimes(1) + mockLogger.EXPECT().Info("HTTP listeners successfully re-added").MinTimes(1) + mockLogger.EXPECT().Info("HTTP plugin successfully restarted").MinTimes(1) + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + assert.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("ReloadTestHugeNumberOfFiles", reloadHugeNumberOfFiles) + t.Run("ReloadRandomlyChangeFile", randomlyChangeFile) + + stopCh <- struct{}{} + wg.Wait() + + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) +} + +func randomlyChangeFile(t *testing.T) { + // we know, that directory contains 500 files (0-499) + // let's try to randomly change it + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(500) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(hugeNumberOfFiles)) // nolint:gosec + err := ioutil.WriteFile(filepath.Join(testDir, "file_"+strconv.Itoa(int(rNum))+".txt"), []byte("Hello, Gophers!"), 0755) // nolint:gosec + assert.NoError(t, err) + } +} + +func reloadHugeNumberOfFiles(t *testing.T) { + for i := uint(0); i < hugeNumberOfFiles; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt")) + } +} + +// Should be events only about creating files with txt ext +func TestReloadFilterFileExt(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload-2.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, os.Mkdir(testDir, 0755)) + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1) + mockLogger.EXPECT().Debug("file was created", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(100) + mockLogger.EXPECT().Debug("file was added to watcher", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Debug("file added to the list of removed files", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Info("HTTP plugin got restart request. Restarting...").Times(1) + mockLogger.EXPECT().Info("HTTP workers Pool successfully restarted").Times(1) + mockLogger.EXPECT().Info("HTTP listeners successfully re-added").Times(1) + mockLogger.EXPECT().Info("HTTP plugin successfully restarted").Times(1) + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + assert.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("ReloadMakeFiles", reloadMakeFiles) + t.Run("ReloadFilteredExt", reloadFilteredExt) + + stopCh <- struct{}{} + wg.Wait() + + assert.NoError(t, freeResources(testDir)) +} + +func reloadMakeFiles(t *testing.T) { + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".abc")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".def")) + } +} + +func reloadFilteredExt(t *testing.T) { + // change files with abc extension + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(1000) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(hugeNumberOfFiles)) // nolint:gosec + err := ioutil.WriteFile(filepath.Join(testDir, "file_"+strconv.Itoa(int(rNum))+".abc"), []byte("Hello, Gophers!"), 0755) // nolint:gosec + assert.NoError(t, err) + } + + // change files with def extension + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(1000) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(hugeNumberOfFiles)) // nolint:gosec + err := ioutil.WriteFile(filepath.Join(testDir, "file_"+strconv.Itoa(int(rNum))+".def"), []byte("Hello, Gophers!"), 0755) // nolint:gosec + assert.NoError(t, err) + } +} + +// Should be events only about creating files with txt ext +func TestReloadCopy500(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload-3.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + assert.NoError(t, freeResources(dir1)) + + assert.NoError(t, os.Mkdir(testDir, 0755)) + assert.NoError(t, os.Mkdir(testCopyToDir, 0755)) + assert.NoError(t, os.Mkdir(dir1, 0755)) + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + // + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1) + mockLogger.EXPECT().Debug("file was created", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50) + mockLogger.EXPECT().Debug("file was added to watcher", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50) + mockLogger.EXPECT().Debug("file added to the list of removed files", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50) + mockLogger.EXPECT().Debug("file was removed from watcher", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50) + mockLogger.EXPECT().Debug("file was updated", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50) + mockLogger.EXPECT().Info("HTTP plugin got restart request. Restarting...").MinTimes(1) + mockLogger.EXPECT().Info("HTTP workers Pool successfully restarted").MinTimes(1) + mockLogger.EXPECT().Info("HTTP listeners successfully re-added").MinTimes(1) + mockLogger.EXPECT().Info("HTTP plugin successfully restarted").MinTimes(1) + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + assert.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + // Scenario + // 1 + // Create 3k files with txt, abc, def extensions + // Copy files to the unit_tests_copy dir + // 2 + // Delete both dirs, recreate + // Create 3k files with txt, abc, def extensions + // Move files to the unit_tests_copy dir + // 3 + // Recursive + + t.Run("ReloadMake300Files", reloadMake300Files) + t.Run("ReloadCopyFiles", reloadCopyFiles) + t.Run("ReloadRecursiveDirsSupport", copyFilesRecursive) + t.Run("RandomChangesInRecursiveDirs", randomChangesInRecursiveDirs) + t.Run("RemoveFilesSupport", removeFilesSupport) + t.Run("ReloadMoveSupport", reloadMoveSupport) + + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + assert.NoError(t, freeResources(dir1)) + + stopCh <- struct{}{} + wg.Wait() +} + +func reloadMoveSupport(t *testing.T) { + t.Run("MoveSupportCopy", copyFilesRecursive) + // move some files + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(500) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(100)) // nolint:gosec + rDir := rand.Int63n(9) // nolint:gosec + rExt := rand.Int63n(3) // nolint:gosec + + ext := []string{ + ".txt", + ".abc", + ".def", + } + + // change files with def extension + dirs := []string{ + "dir1", + "dir1/dir2", + "dir1/dir2/dir3", + "dir1/dir2/dir3/dir4", + "dir1/dir2/dir3/dir4/dir5", + "dir1/dir2/dir3/dir4/dir5/dir6", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10", + } + + // move file + err := os.Rename(filepath.Join(dirs[rDir], "file_"+strconv.Itoa(int(rNum))+ext[rExt]), filepath.Join(dirs[rDir+1], "file_"+strconv.Itoa(int(rNum))+ext[rExt])) + assert.NoError(t, err) + } +} + +func removeFilesSupport(t *testing.T) { + // remove some files + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(500) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(100)) // nolint:gosec + rDir := rand.Int63n(10) // nolint:gosec + rExt := rand.Int63n(3) // nolint:gosec + + ext := []string{ + ".txt", + ".abc", + ".def", + } + + // change files with def extension + dirs := []string{ + "dir1", + "dir1/dir2", + "dir1/dir2/dir3", + "dir1/dir2/dir3/dir4", + "dir1/dir2/dir3/dir4/dir5", + "dir1/dir2/dir3/dir4/dir5/dir6", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10", + } + // here can be a situation, when file already deleted + _ = os.Remove(filepath.Join(dirs[rDir], "file_"+strconv.Itoa(int(rNum))+ext[rExt])) + } +} + +func randomChangesInRecursiveDirs(t *testing.T) { + // change files with def extension + dirs := []string{ + "dir1", + "dir1/dir2", + "dir1/dir2/dir3", + "dir1/dir2/dir3/dir4", + "dir1/dir2/dir3/dir4/dir5", + "dir1/dir2/dir3/dir4/dir5/dir6", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10", + } + + ext := []string{ + ".txt", + ".abc", + ".def", + } + + filenames := []string{ + "file_", // should be update + "foo_", // should be created + "bar_", // should be created + } + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(500) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(100)) // nolint:gosec + rDir := rand.Int63n(10) // nolint:gosec + rExt := rand.Int63n(3) // nolint:gosec + rName := rand.Int63n(3) // nolint:gosec + + err := ioutil.WriteFile(filepath.Join(dirs[rDir], filenames[rName]+strconv.Itoa(int(rNum))+ext[rExt]), []byte("Hello, Gophers!"), 0755) // nolint:gosec + assert.NoError(t, err) + } +} + +func copyFilesRecursive(t *testing.T) { + err := copyDir(testDir, "dir1") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10") + assert.NoError(t, err) +} + +func reloadCopyFiles(t *testing.T) { + err := copyDir(testDir, testCopyToDir) + assert.NoError(t, err) + + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + + assert.NoError(t, os.Mkdir(testDir, 0755)) + assert.NoError(t, os.Mkdir(testCopyToDir, 0755)) + + // recreate files + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".abc")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".def")) + } + + err = copyDir(testDir, testCopyToDir) + assert.NoError(t, err) +} + +func reloadMake300Files(t *testing.T) { + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".abc")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".def")) + } +} + +func TestReloadNoRecursion(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload-4.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + assert.NoError(t, freeResources(dir1)) + + assert.NoError(t, os.Mkdir(testDir, 0755)) + assert.NoError(t, os.Mkdir(dir1, 0755)) + assert.NoError(t, os.Mkdir(testCopyToDir, 0755)) + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + // http server should not be restarted. all event from wrong file extensions should be skipped + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1) + mockLogger.EXPECT().Debug("file added to the list of removed files", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + assert.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("ReloadMakeFiles", reloadMakeFiles) // make files in the testDir + t.Run("ReloadCopyFilesRecursive", reloadCopyFiles) + + stopCh <- struct{}{} + wg.Wait() + + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + assert.NoError(t, freeResources(dir1)) +} + +// ======================================================================== + +func freeResources(path string) error { + return os.RemoveAll(path) +} + +func makeFile(filename string) error { + return ioutil.WriteFile(filepath.Join(testDir, filename), []byte{}, 0755) //nolint:gosec +} + +func copyDir(src string, dst string) error { + src = filepath.Clean(src) + dst = filepath.Clean(dst) + + si, err := os.Stat(src) + if err != nil { + return err + } + if !si.IsDir() { + return errors.E(errors.Str("source is not a directory")) + } + + _, err = os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + return err + } + + err = os.MkdirAll(dst, si.Mode()) + if err != nil { + return err + } + + entries, err := ioutil.ReadDir(src) + if err != nil { + return err + } + + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + err = copyDir(srcPath, dstPath) + if err != nil { + return err + } + } else { + // Skip symlinks. + if entry.Mode()&os.ModeSymlink != 0 { + continue + } + + err = copyFile(srcPath, dstPath) + if err != nil { + return err + } + } + } + return nil +} + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return errors.E(err) + } + defer func() { + _ = in.Close() + }() + + out, err := os.Create(dst) + if err != nil { + return errors.E(err) + } + defer func() { + _ = out.Close() + }() + + _, err = io.Copy(out, in) + if err != nil { + return errors.E(err) + } + + err = out.Sync() + if err != nil { + return errors.E(err) + } + + si, err := os.Stat(src) + if err != nil { + return errors.E(err) + } + err = os.Chmod(dst, si.Mode()) + if err != nil { + return errors.E(err) + } + return nil +} diff --git a/tests/plugins/resetter/.rr-resetter.yaml b/tests/plugins/resetter/.rr-resetter.yaml new file mode 100644 index 00000000..e50ca9c9 --- /dev/null +++ b/tests/plugins/resetter/.rr-resetter.yaml @@ -0,0 +1,16 @@ +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_CONFIG": "/some/place/on/the/C134" + "RR_CONFIG2": "C138" + relay: "pipes" + relayTimeout: "20s" + +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/resetter/resetter_test.go b/tests/plugins/resetter/resetter_test.go new file mode 100644 index 00000000..89dd43c7 --- /dev/null +++ b/tests/plugins/resetter/resetter_test.go @@ -0,0 +1,113 @@ +package resetter + +import ( + "net" + "net/rpc" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/resetter" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +func TestResetterInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Viper{ + Path: ".rr-resetter.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &server.Plugin{}, + &logger.ZapLogger{}, + &resetter.Plugin{}, + &rpcPlugin.Plugin{}, + &Plugin1{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + stopCh := make(chan struct{}, 1) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + + t.Run("ResetterRpcTest", resetterRPCTest) + stopCh <- struct{}{} + wg.Wait() +} + +func resetterRPCTest(t *testing.T) { + conn, err := net.Dial("tcp", "127.0.0.1:6001") + assert.NoError(t, err) + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + // WorkerList contains list of workers. + + var ret bool + err = client.Call("resetter.Reset", "resetter.plugin1", &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + var services []string + err = client.Call("resetter.List", nil, &services) + assert.NotNil(t, services) + assert.NoError(t, err) + if services[0] != "resetter.plugin1" { + t.Fatal("no enough services") + } +} diff --git a/tests/plugins/resetter/test_plugin.go b/tests/plugins/resetter/test_plugin.go new file mode 100644 index 00000000..7d53bca0 --- /dev/null +++ b/tests/plugins/resetter/test_plugin.go @@ -0,0 +1,66 @@ +package resetter + +import ( + "context" + "time" + + poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/server" +) + +var testPoolConfig = poolImpl.Config{ + NumWorkers: 10, + MaxJobs: 100, + AllocateTimeout: time.Second * 10, + DestroyTimeout: time.Second * 10, + Supervisor: &poolImpl.SupervisorConfig{ + WatchTick: 60, + TTL: 1000, + IdleTTL: 10, + ExecTTL: 10, + MaxWorkerMemory: 1000, + }, +} + +// Gauge ////////////// +type Plugin1 struct { + config config.Configurer + server server.Server +} + +func (p1 *Plugin1) Init(cfg config.Configurer, server server.Server) error { + p1.config = cfg + p1.server = server + return nil +} + +func (p1 *Plugin1) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (p1 *Plugin1) Stop() error { + return nil +} + +func (p1 *Plugin1) Name() string { + return "resetter.plugin1" +} + +func (p1 *Plugin1) Reset() error { + pool, err := p1.server.NewWorkerPool(context.Background(), testPoolConfig, nil) + if err != nil { + panic(err) + } + pool.Destroy(context.Background()) + + pool, err = p1.server.NewWorkerPool(context.Background(), testPoolConfig, nil) + if err != nil { + panic(err) + } + + _ = pool + + return nil +} diff --git a/tests/plugins/rpc/config_test.go b/tests/plugins/rpc/config_test.go new file mode 100755 index 00000000..df5fa391 --- /dev/null +++ b/tests/plugins/rpc/config_test.go @@ -0,0 +1,135 @@ +package rpc + +import ( + "runtime" + "testing" + + "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/stretchr/testify/assert" +) + +func TestConfig_Listener(t *testing.T) { + cfg := &rpc.Config{Listen: "tcp://:18001"} + + ln, err := cfg.Listener() + assert.NoError(t, err) + assert.NotNil(t, ln) + defer func() { + err := ln.Close() + if err != nil { + t.Errorf("error closing the listener: error %v", err) + } + }() + + assert.Equal(t, "tcp", ln.Addr().Network()) + if runtime.GOOS == "windows" { + assert.Equal(t, "[::]:18001", ln.Addr().String()) + } else { + assert.Equal(t, "0.0.0.0:18001", ln.Addr().String()) + } +} + +func TestConfig_ListenerUnix(t *testing.T) { + cfg := &rpc.Config{Listen: "unix://file.sock"} + + ln, err := cfg.Listener() + assert.NoError(t, err) + assert.NotNil(t, ln) + defer func() { + err := ln.Close() + if err != nil { + t.Errorf("error closing the listener: error %v", err) + } + }() + + assert.Equal(t, "unix", ln.Addr().Network()) + assert.Equal(t, "file.sock", ln.Addr().String()) +} + +func Test_Config_Error(t *testing.T) { + cfg := &rpc.Config{Listen: "uni:unix.sock"} + ln, err := cfg.Listener() + assert.Nil(t, ln) + assert.Error(t, err) + assert.Equal(t, "invalid DSN (tcp://:6001, unix://file.sock)", err.Error()) +} + +func Test_Config_ErrorMethod(t *testing.T) { + cfg := &rpc.Config{Listen: "xinu://unix.sock"} + + ln, err := cfg.Listener() + assert.Nil(t, ln) + assert.Error(t, err) +} + +func TestConfig_Dialer(t *testing.T) { + cfg := &rpc.Config{Listen: "tcp://:18001"} + + ln, _ := cfg.Listener() + defer func() { + err := ln.Close() + if err != nil { + t.Errorf("error closing the listener: error %v", err) + } + }() + + conn, err := cfg.Dialer() + assert.NoError(t, err) + assert.NotNil(t, conn) + defer func() { + err := conn.Close() + if err != nil { + t.Errorf("error closing the connection: error %v", err) + } + }() + + assert.Equal(t, "tcp", conn.RemoteAddr().Network()) + assert.Equal(t, "127.0.0.1:18001", conn.RemoteAddr().String()) +} + +func TestConfig_DialerUnix(t *testing.T) { + cfg := &rpc.Config{Listen: "unix://file.sock"} + + ln, _ := cfg.Listener() + defer func() { + err := ln.Close() + if err != nil { + t.Errorf("error closing the listener: error %v", err) + } + }() + + conn, err := cfg.Dialer() + assert.NoError(t, err) + assert.NotNil(t, conn) + defer func() { + err := conn.Close() + if err != nil { + t.Errorf("error closing the connection: error %v", err) + } + }() + + assert.Equal(t, "unix", conn.RemoteAddr().Network()) + assert.Equal(t, "file.sock", conn.RemoteAddr().String()) +} + +func Test_Config_DialerError(t *testing.T) { + cfg := &rpc.Config{Listen: "uni:unix.sock"} + ln, err := cfg.Dialer() + assert.Nil(t, ln) + assert.Error(t, err) + assert.Equal(t, "invalid socket DSN (tcp://:6001, unix://file.sock)", err.Error()) +} + +func Test_Config_DialerErrorMethod(t *testing.T) { + cfg := &rpc.Config{Listen: "xinu://unix.sock"} + + ln, err := cfg.Dialer() + assert.Nil(t, ln) + assert.Error(t, err) +} + +func Test_Config_Defaults(t *testing.T) { + c := &rpc.Config{} + c.InitDefaults() + assert.Equal(t, "tcp://127.0.0.1:6001", c.Listen) +} diff --git a/tests/plugins/rpc/configs/.rr-rpc-disabled.yaml b/tests/plugins/rpc/configs/.rr-rpc-disabled.yaml new file mode 100644 index 00000000..d5c185e7 --- /dev/null +++ b/tests/plugins/rpc/configs/.rr-rpc-disabled.yaml @@ -0,0 +1,6 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: true +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/rpc/configs/.rr.yaml b/tests/plugins/rpc/configs/.rr.yaml new file mode 100644 index 00000000..d2cb6c70 --- /dev/null +++ b/tests/plugins/rpc/configs/.rr.yaml @@ -0,0 +1,6 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/rpc/plugin1.go b/tests/plugins/rpc/plugin1.go new file mode 100644 index 00000000..6843b396 --- /dev/null +++ b/tests/plugins/rpc/plugin1.go @@ -0,0 +1,42 @@ +package rpc + +import ( + "fmt" + + "github.com/spiral/roadrunner/v2/plugins/config" +) + +type Plugin1 struct { + config config.Configurer +} + +func (p1 *Plugin1) Init(cfg config.Configurer) error { + p1.config = cfg + return nil +} + +func (p1 *Plugin1) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (p1 *Plugin1) Stop() error { + return nil +} + +func (p1 *Plugin1) Name() string { + return "rpc_test.plugin1" +} + +func (p1 *Plugin1) RPC() interface{} { + return &PluginRPC{srv: p1} +} + +type PluginRPC struct { + srv *Plugin1 +} + +func (r *PluginRPC) Hello(in string, out *string) error { + *out = fmt.Sprintf("Hello, username: %s", in) + return nil +} diff --git a/tests/plugins/rpc/plugin2.go b/tests/plugins/rpc/plugin2.go new file mode 100644 index 00000000..2c47158f --- /dev/null +++ b/tests/plugins/rpc/plugin2.go @@ -0,0 +1,53 @@ +package rpc + +import ( + "net" + "net/rpc" + "time" + + "github.com/spiral/errors" + goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" +) + +// plugin2 makes a call to the plugin1 via RPC +// this is just a simulation of external call FOR TEST +// you don't need to do such things :) +type Plugin2 struct { +} + +func (p2 *Plugin2) Init() error { + return nil +} + +func (p2 *Plugin2) Serve() chan error { + errCh := make(chan error, 1) + + go func() { + time.Sleep(time.Second * 3) + + conn, err := net.Dial("tcp", "127.0.0.1:6001") + if err != nil { + errCh <- errors.E(errors.Serve, err) + return + } + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret string + err = client.Call("rpc_test.plugin1.Hello", "Valery", &ret) + if err != nil { + errCh <- err + return + } + if ret != "Hello, username: Valery" { + errCh <- errors.E("wrong response") + return + } + // to stop exec + errCh <- errors.E(errors.Disabled) + }() + + return errCh +} + +func (p2 *Plugin2) Stop() error { + return nil +} diff --git a/tests/plugins/rpc/rpc_test.go b/tests/plugins/rpc/rpc_test.go new file mode 100644 index 00000000..98959b28 --- /dev/null +++ b/tests/plugins/rpc/rpc_test.go @@ -0,0 +1,188 @@ +package rpc + +import ( + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/stretchr/testify/assert" +) + +// graph https://bit.ly/3ensdNb +func TestRpcInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&Plugin1{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&Plugin2{}) + if err != nil { + t.Fatal(err) + } + + v := &config.Viper{} + v.Path = "configs/.rr.yaml" + v.Prefix = "rr" + err = cont.Register(v) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&rpc.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + if err != nil { + t.Fatal(err) + } + + sig := make(chan os.Signal, 1) + + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 10) + + go func() { + defer wg.Done() + defer tt.Stop() + for { + select { + case e := <-ch: + // just stop, this is ok + if errors.Is(errors.Disabled, e.Error) { + return + } + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + assert.Fail(t, "timeout") + } + } + }() + + wg.Wait() +} + +// graph https://bit.ly/3ensdNb +func TestRpcDisabled(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&Plugin1{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&Plugin2{}) + if err != nil { + t.Fatal(err) + } + + v := &config.Viper{} + v.Path = "configs/.rr-rpc-disabled.yaml" + v.Prefix = "rr" + err = cont.Register(v) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&rpc.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + if err != nil { + t.Fatal(err) + } + + sig := make(chan os.Signal, 1) + + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + tt := time.NewTimer(time.Second * 20) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + defer wg.Done() + defer tt.Stop() + for { + select { + case e := <-ch: + // RPC is turned off, should be and dial error + if errors.Is(errors.Disabled, e.Error) { + assert.FailNow(t, "should not be disabled error") + } + assert.Error(t, e.Error) + err = cont.Stop() + assert.Error(t, err) + return + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + return + } + } + }() + + wg.Wait() +} diff --git a/tests/plugins/server/configs/.rr-no-app-section.yaml b/tests/plugins/server/configs/.rr-no-app-section.yaml new file mode 100644 index 00000000..4888d249 --- /dev/null +++ b/tests/plugins/server/configs/.rr-no-app-section.yaml @@ -0,0 +1,12 @@ +server: + command: "php ../../client.php echo pipes" + user: "" + group: "" + env: + "RR_CONFIG": "/some/place/on/the/C134" + "RR_CONFIG2": "C138" + relay: "pipes" + relayTimeout: "20s" +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/server/configs/.rr-sockets.yaml b/tests/plugins/server/configs/.rr-sockets.yaml new file mode 100644 index 00000000..6b5b6bf5 --- /dev/null +++ b/tests/plugins/server/configs/.rr-sockets.yaml @@ -0,0 +1,12 @@ +server: + command: "php socket.php" + user: "" + group: "" + env: + "RR_CONFIG": "/some/place/on/the/C134" + "RR_CONFIG2": "C138" + relay: "unix://unix.sock" + relayTimeout: "20s" +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/server/configs/.rr-tcp.yaml b/tests/plugins/server/configs/.rr-tcp.yaml new file mode 100644 index 00000000..ee1d450a --- /dev/null +++ b/tests/plugins/server/configs/.rr-tcp.yaml @@ -0,0 +1,12 @@ +server: + command: "php tcp.php" + user: "" + group: "" + env: + "RR_CONFIG": "/some/place/on/the/C134" + "RR_CONFIG2": "C138" + relay: "tcp://localhost:9999" + relayTimeout: "20s" +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/server/configs/.rr-wrong-command.yaml b/tests/plugins/server/configs/.rr-wrong-command.yaml new file mode 100644 index 00000000..e66349dd --- /dev/null +++ b/tests/plugins/server/configs/.rr-wrong-command.yaml @@ -0,0 +1,12 @@ +server: + command: "php some_absent_file.php" + user: "" + group: "" + env: + "RR_CONFIG": "/some/place/on/the/C134" + "RR_CONFIG2": "C138" + relay: "pipes" + relayTimeout: "20s" +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/server/configs/.rr-wrong-relay.yaml b/tests/plugins/server/configs/.rr-wrong-relay.yaml new file mode 100644 index 00000000..6f532c3f --- /dev/null +++ b/tests/plugins/server/configs/.rr-wrong-relay.yaml @@ -0,0 +1,12 @@ +server: + command: "php ../../client.php echo pipes" + user: "" + group: "" + env: + "RR_CONFIG": "/some/place/on/the/C134" + "RR_CONFIG2": "C138" + relay: "pupes" + relayTimeout: "20s" +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/server/configs/.rr.yaml b/tests/plugins/server/configs/.rr.yaml new file mode 100644 index 00000000..4888d249 --- /dev/null +++ b/tests/plugins/server/configs/.rr.yaml @@ -0,0 +1,12 @@ +server: + command: "php ../../client.php echo pipes" + user: "" + group: "" + env: + "RR_CONFIG": "/some/place/on/the/C134" + "RR_CONFIG2": "C138" + relay: "pipes" + relayTimeout: "20s" +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/server/plugin_pipes.go b/tests/plugins/server/plugin_pipes.go new file mode 100644 index 00000000..5eb2fed1 --- /dev/null +++ b/tests/plugins/server/plugin_pipes.go @@ -0,0 +1,133 @@ +package server + +import ( + "context" + "time" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/pool" + "github.com/spiral/roadrunner/v2/pkg/payload" + poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" + "github.com/spiral/roadrunner/v2/pkg/worker" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/server" +) + +const ConfigSection = "server" +const Response = "test" + +var testPoolConfig = poolImpl.Config{ + NumWorkers: 10, + MaxJobs: 100, + AllocateTimeout: time.Second * 10, + DestroyTimeout: time.Second * 10, + Supervisor: &poolImpl.SupervisorConfig{ + WatchTick: 60, + TTL: 1000, + IdleTTL: 10, + ExecTTL: 10, + MaxWorkerMemory: 1000, + }, +} + +type Foo struct { + configProvider config.Configurer + wf server.Server + pool pool.Pool +} + +func (f *Foo) Init(p config.Configurer, workerFactory server.Server) error { + f.configProvider = p + f.wf = workerFactory + return nil +} + +func (f *Foo) Serve() chan error { + const op = errors.Op("serve") + + // test payload for echo + r := payload.Payload{ + Context: nil, + Body: []byte(Response), + } + + errCh := make(chan error, 1) + + conf := &server.Config{} + var err error + err = f.configProvider.UnmarshalKey(ConfigSection, conf) + if err != nil { + errCh <- err + return errCh + } + + // test CMDFactory + cmd, err := f.wf.CmdFactory(nil) + if err != nil { + errCh <- err + return errCh + } + if cmd == nil { + errCh <- errors.E(op, "command is nil") + return errCh + } + + // test worker creation + w, err := f.wf.NewWorker(context.Background(), nil) + if err != nil { + errCh <- err + return errCh + } + + // test that our worker is functional + sw, err := worker.From(w) + if err != nil { + errCh <- err + return errCh + } + + rsp, err := sw.Exec(r) + if err != nil { + errCh <- err + return errCh + } + + if string(rsp.Body) != Response { + errCh <- errors.E("response from worker is wrong", errors.Errorf("response: %s", rsp.Body)) + return errCh + } + + // should not be errors + err = sw.Stop() + if err != nil { + errCh <- err + return errCh + } + + // test pool + f.pool, err = f.wf.NewWorkerPool(context.Background(), testPoolConfig, nil) + if err != nil { + errCh <- err + return errCh + } + + // test pool execution + rsp, err = f.pool.Exec(r) + if err != nil { + errCh <- err + return errCh + } + + // echo of the "test" should be -> test + if string(rsp.Body) != Response { + errCh <- errors.E("response from worker is wrong", errors.Errorf("response: %s", rsp.Body)) + return errCh + } + + return errCh +} + +func (f *Foo) Stop() error { + f.pool.Destroy(context.Background()) + return nil +} diff --git a/tests/plugins/server/plugin_sockets.go b/tests/plugins/server/plugin_sockets.go new file mode 100644 index 00000000..ede67ded --- /dev/null +++ b/tests/plugins/server/plugin_sockets.go @@ -0,0 +1,113 @@ +package server + +import ( + "context" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/pool" + "github.com/spiral/roadrunner/v2/pkg/payload" + "github.com/spiral/roadrunner/v2/pkg/worker" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/server" +) + +type Foo2 struct { + configProvider config.Configurer + wf server.Server + pool pool.Pool +} + +func (f *Foo2) Init(p config.Configurer, workerFactory server.Server) error { + f.configProvider = p + f.wf = workerFactory + return nil +} + +func (f *Foo2) Serve() chan error { + const op = errors.Op("serve") + var err error + errCh := make(chan error, 1) + conf := &server.Config{} + + // test payload for echo + r := payload.Payload{ + Context: nil, + Body: []byte(Response), + } + + err = f.configProvider.UnmarshalKey(ConfigSection, conf) + if err != nil { + errCh <- err + return errCh + } + + // test CMDFactory + cmd, err := f.wf.CmdFactory(nil) + if err != nil { + errCh <- err + return errCh + } + if cmd == nil { + errCh <- errors.E(op, "command is nil") + return errCh + } + + // test worker creation + w, err := f.wf.NewWorker(context.Background(), nil) + if err != nil { + errCh <- err + return errCh + } + + // test that our worker is functional + sw, err := worker.From(w) + if err != nil { + errCh <- err + return errCh + } + + rsp, err := sw.Exec(r) + if err != nil { + errCh <- err + return errCh + } + + if string(rsp.Body) != Response { + errCh <- errors.E("response from worker is wrong", errors.Errorf("response: %s", rsp.Body)) + return errCh + } + + // should not be errors + err = sw.Stop() + if err != nil { + errCh <- err + return errCh + } + + // test pool + f.pool, err = f.wf.NewWorkerPool(context.Background(), testPoolConfig, nil) + if err != nil { + errCh <- err + return errCh + } + + // test pool execution + rsp, err = f.pool.Exec(r) + if err != nil { + errCh <- err + return errCh + } + + // echo of the "test" should be -> test + if string(rsp.Body) != Response { + errCh <- errors.E("response from worker is wrong", errors.Errorf("response: %s", rsp.Body)) + return errCh + } + + return errCh +} + +func (f *Foo2) Stop() error { + f.pool.Destroy(context.Background()) + return nil +} diff --git a/tests/plugins/server/plugin_tcp.go b/tests/plugins/server/plugin_tcp.go new file mode 100644 index 00000000..98c13b2b --- /dev/null +++ b/tests/plugins/server/plugin_tcp.go @@ -0,0 +1,113 @@ +package server + +import ( + "context" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/interfaces/pool" + "github.com/spiral/roadrunner/v2/pkg/payload" + "github.com/spiral/roadrunner/v2/pkg/worker" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/server" +) + +type Foo3 struct { + configProvider config.Configurer + wf server.Server + pool pool.Pool +} + +func (f *Foo3) Init(p config.Configurer, workerFactory server.Server) error { + f.configProvider = p + f.wf = workerFactory + return nil +} + +func (f *Foo3) Serve() chan error { + const op = errors.Op("serve") + var err error + errCh := make(chan error, 1) + conf := &server.Config{} + + // test payload for echo + r := payload.Payload{ + Context: nil, + Body: []byte(Response), + } + + err = f.configProvider.UnmarshalKey(ConfigSection, conf) + if err != nil { + errCh <- err + return errCh + } + + // test CMDFactory + cmd, err := f.wf.CmdFactory(nil) + if err != nil { + errCh <- err + return errCh + } + if cmd == nil { + errCh <- errors.E(op, "command is nil") + return errCh + } + + // test worker creation + w, err := f.wf.NewWorker(context.Background(), nil) + if err != nil { + errCh <- err + return errCh + } + + // test that our worker is functional + sw, err := worker.From(w) + if err != nil { + errCh <- err + return errCh + } + + rsp, err := sw.Exec(r) + if err != nil { + errCh <- err + return errCh + } + + if string(rsp.Body) != Response { + errCh <- errors.E("response from worker is wrong", errors.Errorf("response: %s", rsp.Body)) + return errCh + } + + // should not be errors + err = sw.Stop() + if err != nil { + errCh <- err + return errCh + } + + // test pool + f.pool, err = f.wf.NewWorkerPool(context.Background(), testPoolConfig, nil) + if err != nil { + errCh <- err + return errCh + } + + // test pool execution + rsp, err = f.pool.Exec(r) + if err != nil { + errCh <- err + return errCh + } + + // echo of the "test" should be -> test + if string(rsp.Body) != Response { + errCh <- errors.E("response from worker is wrong", errors.Errorf("response: %s", rsp.Body)) + return errCh + } + + return errCh +} + +func (f *Foo3) Stop() error { + f.pool.Destroy(context.Background()) + return nil +} diff --git a/tests/plugins/server/server_plugin_test.go b/tests/plugins/server/server_plugin_test.go new file mode 100644 index 00000000..d63b0ccd --- /dev/null +++ b/tests/plugins/server/server_plugin_test.go @@ -0,0 +1,361 @@ +package server + +import ( + "os" + "os/signal" + "sync" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +func TestAppPipes(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = "configs/.rr.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&server.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Foo{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = container.Init() + if err != nil { + t.Fatal(err) + } + + errCh, err := container.Serve() + if err != nil { + t.Fatal(err) + } + + // stop by CTRL+C + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + tt := time.NewTimer(time.Second * 10) + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + defer wg.Done() + defer tt.Stop() + for { + select { + case e := <-errCh: + assert.NoError(t, e.Error) + assert.NoError(t, container.Stop()) + return + case <-c: + er := container.Stop() + assert.NoError(t, er) + return + case <-tt.C: + assert.NoError(t, container.Stop()) + return + } + } + }() + + wg.Wait() +} + +func TestAppSockets(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = "configs/.rr-sockets.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&server.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Foo2{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = container.Init() + if err != nil { + t.Fatal(err) + } + + errCh, err := container.Serve() + if err != nil { + t.Fatal(err) + } + + // stop by CTRL+C + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + // stop after 10 seconds + tt := time.NewTicker(time.Second * 10) + + for { + select { + case e := <-errCh: + assert.NoError(t, e.Error) + assert.NoError(t, container.Stop()) + return + case <-c: + er := container.Stop() + if er != nil { + panic(er) + } + return + case <-tt.C: + tt.Stop() + assert.NoError(t, container.Stop()) + return + } + } +} + +func TestAppTCP(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = "configs/.rr-tcp.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&server.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Foo3{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = container.Init() + if err != nil { + t.Fatal(err) + } + + errCh, err := container.Serve() + if err != nil { + t.Fatal(err) + } + + // stop by CTRL+C + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + // stop after 10 seconds + tt := time.NewTicker(time.Second * 10) + + for { + select { + case e := <-errCh: + assert.NoError(t, e.Error) + assert.NoError(t, container.Stop()) + return + case <-c: + er := container.Stop() + if er != nil { + panic(er) + } + return + case <-tt.C: + tt.Stop() + assert.NoError(t, container.Stop()) + return + } + } +} + +func TestAppWrongConfig(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = "configs/.rrrrrrrrrr.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&server.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Foo3{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + assert.Error(t, container.Init()) +} + +func TestAppWrongRelay(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = "configs/.rr-wrong-relay.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&server.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Foo3{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = container.Init() + assert.Error(t, err) + + _, err = container.Serve() + assert.Error(t, err) +} + +func TestAppWrongCommand(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = "configs/.rr-wrong-command.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&server.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Foo3{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = container.Init() + if err != nil { + t.Fatal(err) + } + + _, err = container.Serve() + assert.Error(t, err) +} + +func TestAppNoAppSectionInConfig(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = "configs/.rr-wrong-command.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&server.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Foo3{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = container.Init() + if err != nil { + t.Fatal(err) + } + + _, err = container.Serve() + assert.Error(t, err) +} diff --git a/tests/plugins/server/socket.php b/tests/plugins/server/socket.php new file mode 100644 index 00000000..3159c445 --- /dev/null +++ b/tests/plugins/server/socket.php @@ -0,0 +1,25 @@ +<?php +/** + * @var Goridge\RelayInterface $relay + */ + +use Spiral\Goridge; +use Spiral\RoadRunner; + +require dirname(__DIR__) . "/../vendor/autoload.php"; + +$relay = new Goridge\SocketRelay( + "unix.sock", + null, + Goridge\SocketRelay::SOCK_UNIX + ); + +$rr = new RoadRunner\Worker($relay); + +while ($in = $rr->waitPayload()) { + try { + $rr->send((string)$in->body); + } catch (\Throwable $e) { + $rr->error((string)$e); + } +} diff --git a/tests/plugins/server/tcp.php b/tests/plugins/server/tcp.php new file mode 100644 index 00000000..88c49848 --- /dev/null +++ b/tests/plugins/server/tcp.php @@ -0,0 +1,20 @@ +<?php +/** + * @var Goridge\RelayInterface $relay + */ + +use Spiral\Goridge; +use Spiral\RoadRunner; + +require dirname(__DIR__) . "/../vendor/autoload.php"; + +$relay = new Goridge\SocketRelay("localhost", 9999); +$rr = new RoadRunner\Worker($relay); + +while ($in = $rr->waitPayload()) { + try { + $rr->send((string)$in->body); + } catch (\Throwable $e) { + $rr->error((string)$e); + } +} diff --git a/tests/plugins/static/config_test.go b/tests/plugins/static/config_test.go new file mode 100644 index 00000000..f458eed3 --- /dev/null +++ b/tests/plugins/static/config_test.go @@ -0,0 +1,49 @@ +package static + +import ( + "testing" + + "github.com/spiral/roadrunner/v2/plugins/static" + "github.com/stretchr/testify/assert" +) + +func TestConfig_Forbids(t *testing.T) { + cfg := static.Config{Static: struct { + Dir string + Forbid []string + Always []string + Request map[string]string + Response map[string]string + }{Dir: "", Forbid: []string{".php"}, Always: nil, Request: nil, Response: nil}} + + assert.True(t, cfg.AlwaysForbid("index.php")) + assert.True(t, cfg.AlwaysForbid("index.PHP")) + assert.True(t, cfg.AlwaysForbid("phpadmin/index.bak.php")) + assert.False(t, cfg.AlwaysForbid("index.html")) +} + +func TestConfig_Valid(t *testing.T) { + assert.NoError(t, (&static.Config{Static: struct { + Dir string + Forbid []string + Always []string + Request map[string]string + Response map[string]string + }{Dir: "./"}}).Valid()) + + assert.Error(t, (&static.Config{Static: struct { + Dir string + Forbid []string + Always []string + Request map[string]string + Response map[string]string + }{Dir: "./config.go"}}).Valid()) + + assert.Error(t, (&static.Config{Static: struct { + Dir string + Forbid []string + Always []string + Request map[string]string + Response map[string]string + }{Dir: "./dir/"}}).Valid()) +} diff --git a/tests/plugins/static/configs/.rr-http-static-disabled.yaml b/tests/plugins/static/configs/.rr-http-static-disabled.yaml new file mode 100644 index 00000000..d8ee15e0 --- /dev/null +++ b/tests/plugins/static/configs/.rr-http-static-disabled.yaml @@ -0,0 +1,33 @@ +server: + command: "php ../../http/client.php pid pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:21234 + maxRequestSize: 1024 + middleware: [ "gzip", "static" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + static: + dir: "abc" #not exists + forbid: [ ".php", ".htaccess" ] + request: + "Example-Request-Header": "Value" + # Automatically add headers to every response. + response: + "X-Powered-By": "RoadRunner" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/static/configs/.rr-http-static-files-disable.yaml b/tests/plugins/static/configs/.rr-http-static-files-disable.yaml new file mode 100644 index 00000000..563d95cf --- /dev/null +++ b/tests/plugins/static/configs/.rr-http-static-files-disable.yaml @@ -0,0 +1,33 @@ +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:45877 + maxRequestSize: 1024 + middleware: [ "gzip", "static" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + static: + dir: "../../../tests" + forbid: [ ".php" ] + request: + "Example-Request-Header": "Value" + # Automatically add headers to every response. + response: + "X-Powered-By": "RoadRunner" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/static/configs/.rr-http-static-files.yaml b/tests/plugins/static/configs/.rr-http-static-files.yaml new file mode 100644 index 00000000..8961c6f4 --- /dev/null +++ b/tests/plugins/static/configs/.rr-http-static-files.yaml @@ -0,0 +1,34 @@ +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:34653 + maxRequestSize: 1024 + middleware: [ "gzip", "static" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + static: + dir: "../../../tests" + forbid: [ ".php", ".htaccess" ] + always: [ ".ico" ] + request: + "Example-Request-Header": "Value" + # Automatically add headers to every response. + response: + "X-Powered-By": "RoadRunner" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/static/configs/.rr-http-static.yaml b/tests/plugins/static/configs/.rr-http-static.yaml new file mode 100644 index 00000000..0a1f5df4 --- /dev/null +++ b/tests/plugins/static/configs/.rr-http-static.yaml @@ -0,0 +1,31 @@ +server: + command: "php ../../http/client.php pid pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + address: 127.0.0.1:21603 + maxRequestSize: 1024 + middleware: [ "gzip", "static" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + static: + dir: "../../../tests" + forbid: [ "" ] + request: + "input": "custom-header" + response: + "output": "output-header" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/static/static_plugin_test.go b/tests/plugins/static/static_plugin_test.go new file mode 100644 index 00000000..74daaa16 --- /dev/null +++ b/tests/plugins/static/static_plugin_test.go @@ -0,0 +1,437 @@ +package static + +import ( + "bytes" + "io" + "io/ioutil" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/gzip" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/spiral/roadrunner/v2/plugins/static" + "github.com/spiral/roadrunner/v2/tests/mocks" + "github.com/stretchr/testify/assert" +) + +func TestStaticPlugin(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + &static.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("ServeSample", serveStaticSample) + t.Run("StaticNotForbid", staticNotForbid) + t.Run("StaticHeaders", staticHeaders) + + stopCh <- struct{}{} + wg.Wait() +} + +func staticHeaders(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:21603/client.php", nil) + if err != nil { + t.Fatal(err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + + if resp.Header.Get("Output") != "output-header" { + t.Fatal("can't find output header in response") + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + + defer func() { + _ = resp.Body.Close() + }() + + assert.Equal(t, all("../../../tests/client.php"), string(b)) + assert.Equal(t, all("../../../tests/client.php"), string(b)) +} + +func staticNotForbid(t *testing.T) { + b, r, err := get("http://localhost:21603/client.php") + assert.NoError(t, err) + assert.Equal(t, all("../../../tests/client.php"), b) + assert.Equal(t, all("../../../tests/client.php"), b) + _ = r.Body.Close() +} + +func serveStaticSample(t *testing.T) { + b, r, err := get("http://localhost:21603/sample.txt") + assert.NoError(t, err) + assert.Equal(t, "sample", b) + _ = r.Body.Close() +} + +func TestStaticDisabled(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static-disabled.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + &static.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("StaticDisabled", staticDisabled) + + stopCh <- struct{}{} + wg.Wait() +} + +func staticDisabled(t *testing.T) { + _, r, err := get("http://localhost:21234/sample.txt") //nolint:bodyclose + assert.Error(t, err) + assert.Nil(t, r) +} + +func TestStaticFilesDisabled(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static-files-disable.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + &static.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("StaticFilesDisabled", staticFilesDisabled) + + stopCh <- struct{}{} + wg.Wait() +} + +func staticFilesDisabled(t *testing.T) { + b, r, err := get("http://localhost:45877/client.php?hello=world") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "WORLD", b) + _ = r.Body.Close() +} + +func TestStaticFilesForbid(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static-files.yaml", + Prefix: "rr", + } + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").AnyTimes() + mockLogger.EXPECT().Error("file open error", "error", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + &static.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("StaticTestFilesDir", staticTestFilesDir) + t.Run("StaticNotFound", staticNotFound) + t.Run("StaticFilesForbid", staticFilesForbid) + t.Run("StaticFilesAlways", staticFilesAlways) + + stopCh <- struct{}{} + wg.Wait() +} + +func staticTestFilesDir(t *testing.T) { + b, r, err := get("http://localhost:34653/http?hello=world") + assert.NoError(t, err) + assert.Equal(t, "WORLD", b) + _ = r.Body.Close() +} + +func staticNotFound(t *testing.T) { + b, _, _ := get("http://localhost:34653/client.XXX?hello=world") //nolint:bodyclose + assert.Equal(t, "WORLD", b) +} + +func staticFilesAlways(t *testing.T) { + _, r, err := get("http://localhost:34653/favicon.ico") + assert.NoError(t, err) + assert.Equal(t, 404, r.StatusCode) + _ = r.Body.Close() +} + +func staticFilesForbid(t *testing.T) { + b, r, err := get("http://localhost:34653/client.php?hello=world") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "WORLD", b) + _ = r.Body.Close() +} + +// HELPERS +func get(url string) (string, *http.Response, error) { + r, err := http.Get(url) //nolint:gosec + if err != nil { + return "", nil, err + } + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return "", nil, err + } + + err = r.Body.Close() + if err != nil { + return "", nil, err + } + + return string(b), r, err +} + +func all(fn string) string { + f, _ := os.Open(fn) + + b := new(bytes.Buffer) + _, err := io.Copy(b, f) + if err != nil { + return "" + } + + err = f.Close() + if err != nil { + return "" + } + + return b.String() +} |