├── docs ├── book.json ├── router_params.md ├── group.md ├── server_timing.md ├── custom_compress.md ├── performances.md ├── custom_body_parser.md └── body_parse_validate.md ├── hooks └── pre-commit ├── .data ├── koa.png └── server-timing.png ├── .gitignore ├── middleware ├── README.md ├── prefix_url.go ├── response_size_limiter_test.go ├── prefix_url_test.go ├── lru_store_test.go ├── static_embed_test.go ├── response_size_limiter.go ├── fresh.go ├── recover.go ├── lru_store.go ├── etag.go ├── error.go ├── recover_test.go ├── cache_compressor_test.go ├── stats_test.go ├── brotli.go ├── gzip.go ├── zstd.go ├── tracker_test.go ├── renderer.go ├── stats.go ├── basic_auth.go ├── error_test.go ├── router_concurrent_limiter.go ├── renderer_test.go ├── router_concurrent_limiter_test.go ├── responder.go ├── tracker.go ├── basic_auth_test.go ├── etag_test.go ├── fresh_test.go ├── compressor_test.go ├── concurrent_limiter_test.go ├── static_embed.go ├── http_header_test.go ├── cache_compressor.go └── logger_test.go ├── Makefile ├── SUMMARY.md ├── go.mod ├── .github └── workflows │ └── test.yml ├── LICENSE ├── route_params_test.go ├── route_params.go ├── multipart_form_test.go ├── template_test.go ├── go.sum ├── bench_test.go ├── signed_keys.go ├── util_test.go ├── signed_keys_test.go ├── util.go ├── template.go ├── fresh.go ├── multipart_form.go ├── trace_test.go ├── df.go └── trace.go /docs/book.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "elton" 3 | } -------------------------------------------------------------------------------- /hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | make lint && make test -------------------------------------------------------------------------------- /.data/koa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vicanso/elton/HEAD/.data/koa.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | tmp 3 | *.out 4 | examples/*/go.sum 5 | _book 6 | .vscode -------------------------------------------------------------------------------- /.data/server-timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vicanso/elton/HEAD/.data/server-timing.png -------------------------------------------------------------------------------- /middleware/README.md: -------------------------------------------------------------------------------- 1 | # Middlewares 2 | 3 | elton中将常用的中间件已经在middleware中实现,详细使用方法可参考[middlewares](../docs/middlewares.md) 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export GO111MODULE = on 2 | 3 | .PHONY: default test test-cover dev hooks 4 | 5 | 6 | # for test 7 | test: 8 | go test -race -cover ./... 9 | 10 | test-cover: 11 | go test -race -coverprofile=test.out ./... && go tool cover --html=test.out 12 | 13 | bench: 14 | go test --benchmem -bench=. ./... 15 | 16 | lint: 17 | golangci-lint run 18 | 19 | hooks: 20 | cp hooks/* .git/hooks/ -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [Introduction](./docs/introduction.md) 4 | 5 | * [Application](./docs/application.md) 6 | 7 | * [Context](./docs/context.md) 8 | 9 | * [Group](./docs/group.md) 10 | 11 | * [Middlewares](./docs/middlewares.md) 12 | 13 | ## More 14 | 15 | * [Body parse and validate](./docs/body_parse_validate.md) 16 | 17 | * [HTTP2 and HTTP3](./docs/http2_http3.md) 18 | 19 | * [Server Timing](./docs/server_timing.md) 20 | 21 | * [Performances](./docs/performances.md) 22 | 23 | * [Custom body parser](./docs/custom_body_parser.md) 24 | 25 | * [Custom compress](./docs/custom_compress.md) 26 | 27 | * [Router params](./docs/router_params.md) 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vicanso/elton 2 | 3 | go 1.22 4 | 5 | toolchain go1.23.4 6 | 7 | require ( 8 | github.com/andybalholm/brotli v1.1.1 9 | github.com/hashicorp/golang-lru/v2 v2.0.7 10 | github.com/klauspost/compress v1.18.0 11 | github.com/stretchr/testify v1.10.0 12 | github.com/tidwall/gjson v1.18.0 13 | github.com/vicanso/hes v0.7.1 14 | github.com/vicanso/intranet-ip v0.2.0 15 | github.com/vicanso/keygrip v1.3.0 16 | ) 17 | 18 | require ( 19 | github.com/davecgh/go-spew v1.1.1 // indirect 20 | github.com/pmezard/go-difflib v1.0.0 // indirect 21 | github.com/tidwall/match v1.1.1 // indirect 22 | github.com/tidwall/pretty v1.2.0 // indirect 23 | gopkg.in/yaml.v3 v3.0.1 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /docs/router_params.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 路由参数 3 | --- 4 | 5 | Elton支持各种不同种类的路由参数配置形式,正则表达式或*等。需要注意的是,如果路由参数使用正则,在参数不匹配时是无法获取对应的路由,导致接口404。 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "github.com/vicanso/elton" 12 | "github.com/vicanso/elton/middleware" 13 | ) 14 | 15 | func main() { 16 | e := elton.New() 17 | e.Use(middleware.NewDefaultResponder()) 18 | fn := func(c *elton.Context) (err error) { 19 | c.Body = c.Params.ToMap() 20 | return 21 | } 22 | e.GET("/books/{bookID:^[1-9][0-9]{0,3}$}", fn) 23 | e.GET("/books/{bookID:^[1-9][0-9]{0,3}$}/detail", fn) 24 | e.GET("/books/summary/*", fn) 25 | e.GET("/books/trending/{year}/{month}/{day}", fn) 26 | err := e.ListenAndServe(":3000") 27 | if err != nil { 28 | panic(err) 29 | } 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | go: 17 | - '1.24' 18 | - '1.23' 19 | - '1.22' 20 | steps: 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v4 24 | - name: Go ${{ matrix.go }} test 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: ${{ matrix.go }} 28 | 29 | - name: Get dependencies 30 | run: 31 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin latest 32 | 33 | - name: Lint 34 | run: make lint 35 | 36 | - name: Test 37 | run: make test 38 | 39 | - name: Bench 40 | run: make bench 41 | -------------------------------------------------------------------------------- /docs/group.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Group的相关方法说明 3 | --- 4 | 5 | # Group 6 | 7 | ## NewGroup 8 | 9 | 创建一个组,它包括Path的前缀以及组内公共中间件(非全局),适用于创建有相同前置校验条件的路由处理,如用户相关的操作。返回的Group对象包括`GET`,`POST`,`PUT`等方法,与Elton类似,之后可以通过`AddGroup`将所有路由处理添加至Elton实例。 10 | 11 | **Example** 12 | ```go 13 | package main 14 | 15 | import ( 16 | "github.com/vicanso/elton" 17 | "github.com/vicanso/elton/middleware" 18 | ) 19 | 20 | func main() { 21 | e := elton.New() 22 | 23 | e.Use(middleware.NewDefaultResponder()) 24 | // user相关的公共中间件 25 | noop := func(c *elton.Context) error { 26 | return c.Next() 27 | } 28 | 29 | userGroup := elton.NewGroup("/users", noop) 30 | userGroup.GET("/me", func(c *elton.Context) (err error) { 31 | // 从session中读取用户信息... 32 | c.Body = "user info" 33 | return 34 | }) 35 | userGroup.POST("/login", func(c *elton.Context) (err error) { 36 | // 登录验证处理... 37 | c.Body = "login success" 38 | return 39 | }) 40 | e.AddGroup(userGroup) 41 | 42 | err := e.ListenAndServe(":3000") 43 | if err != nil { 44 | panic(err) 45 | } 46 | } 47 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tree Xie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /route_params_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package elton 24 | 25 | import ( 26 | "testing" 27 | 28 | "github.com/stretchr/testify/assert" 29 | ) 30 | 31 | func TestRouteParams(t *testing.T) { 32 | assert := assert.New(t) 33 | params := new(RouteParams) 34 | params.methodNotAllowed = true 35 | params.Add("id", "1") 36 | assert.Equal("1", params.Get("id")) 37 | assert.Equal(map[string]string{ 38 | "id": "1", 39 | }, params.ToMap()) 40 | 41 | params.Reset() 42 | 43 | assert.False(params.methodNotAllowed) 44 | assert.Empty(params.Keys) 45 | assert.Empty(params.Values) 46 | } 47 | -------------------------------------------------------------------------------- /middleware/prefix_url.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2022 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "fmt" 27 | "net/http" 28 | "regexp" 29 | "strings" 30 | 31 | "github.com/vicanso/elton" 32 | ) 33 | 34 | // NewPrefixURL returns a new prefix url handler, it will replace the prefix of url as "" 35 | func NewPrefixURL(prefix ...string) elton.PreHandler { 36 | var reg *regexp.Regexp 37 | if len(prefix) != 0 { 38 | reg = regexp.MustCompile(fmt.Sprintf(`^(%s)`, strings.Join(prefix, "|"))) 39 | } 40 | return func(req *http.Request) { 41 | if reg == nil { 42 | return 43 | } 44 | req.URL.Path = reg.ReplaceAllString(req.URL.Path, "") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /middleware/response_size_limiter_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2021 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "bytes" 27 | "testing" 28 | 29 | "github.com/stretchr/testify/assert" 30 | "github.com/vicanso/elton" 31 | ) 32 | 33 | func TestResponseSizeLimiter(t *testing.T) { 34 | assert := assert.New(t) 35 | 36 | fn := NewResponseSizeLimiter(ResponseSizeLimiterConfig{ 37 | MaxSize: 5, 38 | }) 39 | 40 | c := elton.NewContext(nil, nil) 41 | c.BodyBuffer = bytes.NewBufferString("test") 42 | c.Next = func() error { 43 | return nil 44 | } 45 | err := fn(c) 46 | assert.Nil(err) 47 | 48 | c.BodyBuffer = bytes.NewBufferString("Hello World!") 49 | err = fn(c) 50 | assert.Equal(ErrResponseTooLarge, err) 51 | } 52 | -------------------------------------------------------------------------------- /middleware/prefix_url_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2022 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "net/http/httptest" 27 | "testing" 28 | 29 | "github.com/stretchr/testify/assert" 30 | ) 31 | 32 | func TestNewPrefixURL(t *testing.T) { 33 | assert := assert.New(t) 34 | 35 | fn := NewPrefixURL("/api", `/\d{2,5}`) 36 | 37 | tests := []struct { 38 | url string 39 | result string 40 | }{ 41 | { 42 | url: "/api/users/v1/me", 43 | result: "/users/v1/me", 44 | }, 45 | { 46 | url: "/rest/api/users/v1/me", 47 | result: "/rest/api/users/v1/me", 48 | }, 49 | { 50 | url: "/100/users/v1/me", 51 | result: "/users/v1/me", 52 | }, 53 | { 54 | url: "/1/users/v1/me", 55 | result: "/1/users/v1/me", 56 | }, 57 | } 58 | for _, tt := range tests { 59 | req := httptest.NewRequest("GET", tt.url, nil) 60 | fn(req) 61 | assert.Equal(tt.result, req.URL.Path) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /docs/server_timing.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: HTTP Server Timing 3 | --- 4 | 5 | elton可以非常方便的获取各中间件的处理时长,获取统计时长之后,则可方便的写入相关的统计数据或HTTP响应的Server-Timing了。 6 | 7 | 如图所示在chrome中network面板所能看得到Server-Timing展示: 8 | 9 | ![](https://raw.githubusercontent.com/vicanso/elton/master/.data/server-timing.png) 10 | 11 | 12 | ```go 13 | package main 14 | 15 | import ( 16 | "github.com/vicanso/elton" 17 | "github.com/vicanso/elton/middleware" 18 | ) 19 | 20 | func main() { 21 | 22 | e := elton.New() 23 | e.EnableTrace = true 24 | e.OnTrace(func(c *elton.Context, traceInfos elton.TraceInfos) { 25 | serverTiming := string(traceInfos.ServerTiming("elton-")) 26 | c.SetHeader(elton.HeaderServerTiming, serverTiming) 27 | }) 28 | 29 | entry := func(c *elton.Context) (err error) { 30 | c.ID = "random id" 31 | c.NoCache() 32 | return c.Next() 33 | } 34 | e.Use(entry) 35 | // 设置中间件的名称,若不设置从runtime中获取 36 | // 对于公共的中间件,建议指定名称 37 | e.SetFunctionName(entry, "entry") 38 | 39 | fn := middleware.NewDefaultResponder() 40 | e.Use(fn) 41 | e.SetFunctionName(fn, "responder") 42 | 43 | e.GET("/", func(c *elton.Context) (err error) { 44 | c.Body = &struct { 45 | Name string `json:"name,omitempty"` 46 | Content string `json:"content,omitempty"` 47 | }{ 48 | "tree.xie", 49 | "Hello, World!", 50 | } 51 | return 52 | }) 53 | 54 | err := e.ListenAndServe(":3000") 55 | if err != nil { 56 | panic(err) 57 | } 58 | } 59 | ``` 60 | 61 | ``` 62 | curl -v 'http://127.0.0.1:3000/' 63 | * Trying 127.0.0.1... 64 | * TCP_NODELAY set 65 | * Connected to 127.0.0.1 (127.0.0.1) port 3000 (#0) 66 | > GET / HTTP/1.1 67 | > Host: 127.0.0.1:3000 68 | > User-Agent: curl/7.54.0 69 | > Accept: */* 70 | > 71 | < HTTP/1.1 200 OK 72 | < Cache-Control: no-cache 73 | < Content-Length: 44 74 | < Content-Type: application/json; charset=utf-8 75 | < Server-Timing: elton-0;dur=0;desc="entry",elton-1;dur=0.03;desc="responder",elton-2;dur=0;desc="main.main.func3" 76 | < Date: Fri, 03 Jan 2020 13:08:50 GMT 77 | < 78 | * Connection #0 to host 127.0.0.1 left intact 79 | {"name":"tree.xie","content":"Hello, World!"} 80 | ``` -------------------------------------------------------------------------------- /middleware/lru_store_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2023 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "context" 27 | "testing" 28 | "time" 29 | 30 | "github.com/stretchr/testify/assert" 31 | ) 32 | 33 | func TestLruStore(t *testing.T) { 34 | assert := assert.New(t) 35 | 36 | store := NewLruStore(128) 37 | 38 | ctx := context.Background() 39 | key1 := "1" 40 | buf, _ := store.Get(ctx, key1) 41 | assert.Empty(buf) 42 | 43 | _ = store.Set(ctx, key1, []byte("Hello world!"), time.Second) 44 | buf, _ = store.Get(ctx, key1) 45 | assert.Equal([]byte("Hello world!"), buf) 46 | 47 | time.Sleep(2 * time.Second) 48 | buf, _ = store.Get(ctx, key1) 49 | assert.Empty(buf) 50 | } 51 | 52 | func BenchmarkLruStore(b *testing.B) { 53 | store := NewLruStore(128) 54 | ctx := context.Background() 55 | for i := 0; i < b.N; i++ { 56 | _ = store.Set(ctx, "key", []byte("Hello world!"), time.Second) 57 | _, _ = store.Get(ctx, "key") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /middleware/static_embed_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.16 2 | 3 | // Copyright (c) 2021 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "embed" 27 | "testing" 28 | 29 | "github.com/stretchr/testify/assert" 30 | ) 31 | 32 | //go:embed * 33 | var assetFS embed.FS 34 | 35 | func TestEmbedGetFile(t *testing.T) { 36 | assert := assert.New(t) 37 | es := embedStaticFS{ 38 | Prefix: "web", 39 | } 40 | file := es.getFile("abc\\test.txt") 41 | assert.Equal("web/abc/test.txt", file) 42 | file = es.getFile("abc/test.txt") 43 | assert.Equal("web/abc/test.txt", file) 44 | 45 | } 46 | func TestEmbedStaticFS(t *testing.T) { 47 | assert := assert.New(t) 48 | file := "static_embed.go" 49 | 50 | fs := NewEmbedStaticFS(assetFS, "") 51 | 52 | assert.True(fs.Exists(file)) 53 | 54 | fileInfo := fs.Stat(file) 55 | assert.Nil(fileInfo) 56 | 57 | buf, err := fs.Get(file) 58 | assert.Nil(err) 59 | assert.NotEmpty(buf) 60 | 61 | r, err := fs.NewReader(file) 62 | assert.Nil(err) 63 | assert.NotEmpty(r) 64 | } 65 | -------------------------------------------------------------------------------- /route_params.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package elton 24 | 25 | // RouteParams is a structure to track URL routing parameters efficiently. 26 | type RouteParams struct { 27 | Keys, Values []string 28 | methodNotAllowed bool 29 | } 30 | 31 | // Add a URL parameter to the end of the route param 32 | func (s *RouteParams) Add(key, value string) { 33 | s.Keys = append(s.Keys, key) 34 | s.Values = append(s.Values, value) 35 | } 36 | 37 | // Reset the params 38 | func (s *RouteParams) Reset() { 39 | s.Keys = s.Keys[:0] 40 | s.Values = s.Values[:0] 41 | s.methodNotAllowed = false 42 | } 43 | 44 | // Get value from params 45 | func (s *RouteParams) Get(key string) string { 46 | index := -1 47 | for i, k := range s.Keys { 48 | if key == k { 49 | index = i 50 | break 51 | } 52 | } 53 | if index != -1 { 54 | return s.Values[index] 55 | } 56 | return "" 57 | } 58 | 59 | // ToMap converts route params to map[string]string 60 | func (s *RouteParams) ToMap() map[string]string { 61 | m := make(map[string]string, len(s.Keys)) 62 | for index, key := range s.Keys { 63 | m[key] = s.Values[index] 64 | } 65 | return m 66 | } 67 | -------------------------------------------------------------------------------- /middleware/response_size_limiter.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2021 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "errors" 27 | "net/http" 28 | 29 | "github.com/vicanso/elton" 30 | "github.com/vicanso/hes" 31 | ) 32 | 33 | const ( 34 | ErrResponseSizeLimiterCategory = "elton-response-size-limiter" 35 | ) 36 | 37 | type ( 38 | ResponseSizeLimiterConfig struct { 39 | Skipper elton.Skipper 40 | MaxSize int 41 | } 42 | ) 43 | 44 | var ErrResponseTooLarge = &hes.Error{ 45 | Category: ErrResponseSizeLimiterCategory, 46 | Message: "body of response is too large", 47 | StatusCode: http.StatusInternalServerError, 48 | } 49 | 50 | // NewResponseSizeLimiter returns a new response size limiter 51 | func NewResponseSizeLimiter(config ResponseSizeLimiterConfig) elton.Handler { 52 | skipper := config.Skipper 53 | if skipper == nil { 54 | skipper = elton.DefaultSkipper 55 | } 56 | if config.MaxSize <= 0 { 57 | panic(errors.New("max size should be > 0")) 58 | } 59 | return func(c *elton.Context) error { 60 | if skipper(c) { 61 | return c.Next() 62 | } 63 | err := c.Next() 64 | if err != nil { 65 | return err 66 | } 67 | if c.BodyBuffer != nil && c.BodyBuffer.Len() > config.MaxSize { 68 | return ErrResponseTooLarge 69 | } 70 | return nil 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /multipart_form_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2022 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package elton 24 | 25 | import ( 26 | "bytes" 27 | "io" 28 | "strings" 29 | "testing" 30 | 31 | "github.com/stretchr/testify/assert" 32 | ) 33 | 34 | func TestMultipartForm(t *testing.T) { 35 | assert := assert.New(t) 36 | 37 | form := NewMultipartForm() 38 | defer func() { 39 | _ = form.Destroy() 40 | }() 41 | err := form.AddField("a", "b") 42 | assert.Nil(err) 43 | 44 | err = form.AddFile("file", "test.txt", bytes.NewBufferString("Hello world!")) 45 | assert.Nil(err) 46 | 47 | err = form.AddFile("source", "./multipart_form_test.go") 48 | assert.Nil(err) 49 | 50 | assert.True(strings.HasPrefix(form.ContentType(), "multipart/form-data; boundary=")) 51 | r, err := form.Reader() 52 | assert.Nil(err) 53 | buf, err := io.ReadAll(r) 54 | assert.Nil(err) 55 | str := string(buf) 56 | assert.True(strings.Contains(str, "Hello world!")) 57 | assert.True(strings.Contains(str, `Content-Disposition: form-data; name="file"; filename="test.txt"`)) 58 | assert.True(strings.Contains(str, `Content-Type: application/octet-stream`)) 59 | assert.True(strings.Contains(str, `Content-Disposition: form-data; name="a"`)) 60 | } 61 | 62 | func TestTestMultipartFormDestroy(t *testing.T) { 63 | assert := assert.New(t) 64 | form := NewMultipartForm() 65 | err := form.AddField("a", "1") 66 | assert.Nil(err) 67 | 68 | err = form.Destroy() 69 | assert.Nil(err) 70 | } 71 | -------------------------------------------------------------------------------- /template_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2021 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package elton 24 | 25 | import ( 26 | "context" 27 | "os" 28 | "testing" 29 | 30 | "github.com/stretchr/testify/assert" 31 | ) 32 | 33 | func TestHTMLTemplate(t *testing.T) { 34 | assert := assert.New(t) 35 | ht := HTMLTemplate{} 36 | 37 | type Data struct { 38 | ID int 39 | Name string 40 | } 41 | 42 | text := "

{{.ID}}{{.Name}}

" 43 | 44 | t.Run("render text", func(t *testing.T) { 45 | // render text 46 | html, err := ht.Render(context.Background(), text, &Data{ 47 | ID: 1, 48 | Name: "tree.xie", 49 | }) 50 | assert.Nil(err) 51 | assert.Equal("

1tree.xie

", html) 52 | }) 53 | 54 | t.Run("render file", func(t *testing.T) { 55 | // render file 56 | f, err := os.CreateTemp("", "") 57 | assert.Nil(err) 58 | filename := f.Name() 59 | defer func() { 60 | _ = os.Remove(filename) 61 | }() 62 | _, err = f.WriteString(text) 63 | assert.Nil(err) 64 | err = f.Close() 65 | assert.Nil(err) 66 | html, err := ht.RenderFile(context.Background(), filename, &Data{ 67 | ID: 2, 68 | Name: "tree", 69 | }) 70 | assert.Nil(err) 71 | assert.Equal("

2tree

", html) 72 | }) 73 | } 74 | 75 | func TestTemplates(t *testing.T) { 76 | assert := assert.New(t) 77 | 78 | assert.NotNil(DefaultTemplateParsers.Get("html")) 79 | assert.NotNil(DefaultTemplateParsers.Get("tmpl")) 80 | } 81 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 2 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 6 | github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 7 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 8 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 12 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 13 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 14 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 15 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 16 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 17 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 18 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 19 | github.com/vicanso/hes v0.7.1 h1:JCj8MTHCPI5Noilghvp6N09+DHCpPi4bDgZNOd9hB4w= 20 | github.com/vicanso/hes v0.7.1/go.mod h1:zQL+oh4pprYJcT8HN0QUqk/vp21rcfs3kYAlAFi42hM= 21 | github.com/vicanso/intranet-ip v0.2.0 h1:NcrSKXd2/1a+iqo11umPMErt5OpJfY98thwCCpXojOM= 22 | github.com/vicanso/intranet-ip v0.2.0/go.mod h1:TbLL1dR66ACrln7Krf8p6iHlDw+XBnSN/nau4rLPoi0= 23 | github.com/vicanso/keygrip v1.3.0 h1:HwzPb9Kqrew08wYdvs2VJyJi3E49H8Qi0Gx9ixN4+JI= 24 | github.com/vicanso/keygrip v1.3.0/go.mod h1:aatcaEwiC3k8Eu524qeA+sdEraCZSPTqdIYHwtJxFoo= 25 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 26 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 28 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 29 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 30 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 31 | -------------------------------------------------------------------------------- /middleware/fresh.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "net/http" 27 | 28 | "github.com/vicanso/elton" 29 | ) 30 | 31 | type ( 32 | // FreshConfig fresh config 33 | FreshConfig struct { 34 | Skipper elton.Skipper 35 | } 36 | ) 37 | 38 | // NewDefaultFresh returns a default fresh middleware, it will return 304 modified if the data is not modified. 39 | func NewDefaultFresh() elton.Handler { 40 | return NewFresh(FreshConfig{}) 41 | } 42 | 43 | // NewFresh returns a fresh middleware. 44 | func NewFresh(config FreshConfig) elton.Handler { 45 | skipper := config.Skipper 46 | if skipper == nil { 47 | skipper = elton.DefaultSkipper 48 | } 49 | return func(c *elton.Context) error { 50 | if skipper(c) { 51 | return c.Next() 52 | } 53 | err := c.Next() 54 | if err != nil { 55 | return err 56 | } 57 | // 如果空数据或者已经是304,则跳过 58 | bodyBuf := c.BodyBuffer 59 | if bodyBuf == nil || bodyBuf.Len() == 0 || c.StatusCode == http.StatusNotModified { 60 | return nil 61 | } 62 | 63 | // 如果非GET HEAD请求,则跳过 64 | method := c.Request.Method 65 | if method != http.MethodGet && method != http.MethodHead { 66 | return nil 67 | } 68 | 69 | // 如果响应状态码不为0 而且( < 200 或者 >= 300),则跳过 70 | // 如果未设置状态码,最终则为200 71 | statusCode := c.StatusCode 72 | if statusCode != 0 && 73 | (statusCode < http.StatusOK || 74 | statusCode >= http.StatusMultipleChoices) { 75 | return nil 76 | } 77 | 78 | // 304的处理 79 | if elton.Fresh(c.Request.Header, c.Header()) { 80 | c.NotModified() 81 | } 82 | return nil 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package elton 24 | 25 | import ( 26 | "net/http/httptest" 27 | "strings" 28 | "sync/atomic" 29 | "testing" 30 | "time" 31 | ) 32 | 33 | func BenchmarkRoutes(b *testing.B) { 34 | e := NewWithoutServer() 35 | e.GET("/", func(c *Context) error { 36 | return nil 37 | }) 38 | b.ReportAllocs() 39 | req := httptest.NewRequest("GET", "/", nil) 40 | resp := httptest.NewRecorder() 41 | for i := 0; i < b.N; i++ { 42 | e.ServeHTTP(resp, req) 43 | } 44 | } 45 | 46 | func BenchmarkGetFunctionName(b *testing.B) { 47 | b.ReportAllocs() 48 | fn := func() {} 49 | e := New() 50 | e.SetFunctionName(fn, "test-fn") 51 | for i := 0; i < b.N; i++ { 52 | e.GetFunctionName(fn) 53 | } 54 | } 55 | 56 | func BenchmarkContextGet(b *testing.B) { 57 | b.ReportAllocs() 58 | key := "id" 59 | c := NewContext(nil, nil) 60 | 61 | for i := 0; i < b.N; i++ { 62 | c.Set(key, "abc") 63 | _, _ = c.Get(key) 64 | } 65 | } 66 | 67 | func BenchmarkContextNewMap(b *testing.B) { 68 | b.ReportAllocs() 69 | for i := 0; i < b.N; i++ { 70 | _ = make(map[string]interface{}) 71 | } 72 | } 73 | 74 | func BenchmarkConvertServerTiming(b *testing.B) { 75 | b.ReportAllocs() 76 | traceInfos := make(TraceInfos, 0) 77 | for _, name := range strings.Split("0123456789", "") { 78 | traceInfos = append(traceInfos, &TraceInfo{ 79 | Name: name, 80 | Duration: time.Microsecond * 100, 81 | }) 82 | } 83 | for i := 0; i < b.N; i++ { 84 | traceInfos.ServerTiming("elton-") 85 | } 86 | } 87 | 88 | func BenchmarkGetStatus(b *testing.B) { 89 | b.ReportAllocs() 90 | var v int32 91 | for i := 0; i < b.N; i++ { 92 | atomic.LoadInt32(&v) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /middleware/recover.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "fmt" 27 | "net/http" 28 | "strings" 29 | 30 | "github.com/vicanso/elton" 31 | "github.com/vicanso/hes" 32 | ) 33 | 34 | const ( 35 | // ErrRecoverCategory recover error category 36 | ErrRecoverCategory = "elton-recover" 37 | ) 38 | 39 | // NewRecover return a recover middleware, it can recover from panic, 40 | // and then emit an `elton-recover` error. 41 | // Suggest to graceful close the elton instance for recover error. 42 | func NewRecover() elton.Handler { 43 | return func(c *elton.Context) error { 44 | defer func() { 45 | // 可针对实际需求调整,如对于每个recover增加邮件通知等 46 | if r := recover(); r != nil { 47 | err, ok := r.(error) 48 | if !ok { 49 | err = fmt.Errorf("%v", r) 50 | } 51 | 52 | he := hes.Wrap(err) 53 | he.Category = ErrRecoverCategory 54 | he.StatusCode = http.StatusInternalServerError 55 | err = he 56 | c.Elton().EmitError(c, err) 57 | // 出错时清除部分响应头 58 | for _, key := range []string{ 59 | elton.HeaderETag, 60 | elton.HeaderLastModified, 61 | elton.HeaderContentEncoding, 62 | elton.HeaderContentLength, 63 | } { 64 | c.SetHeader(key, "") 65 | } 66 | // 直接对Response写入数据,则将 Committed设置为 true 67 | c.Committed = true 68 | resp := c.Response 69 | buf := []byte(err.Error()) 70 | if strings.Contains(c.GetRequestHeader("Accept"), "application/json") { 71 | c.SetHeader(elton.HeaderContentType, elton.MIMEApplicationJSON) 72 | buf = he.ToJSON() 73 | } 74 | resp.WriteHeader(he.StatusCode) 75 | _, err = resp.Write(buf) 76 | if err != nil { 77 | c.Elton().EmitError(c, err) 78 | } 79 | } 80 | }() 81 | return c.Next() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /middleware/lru_store.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2023 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | package middleware 23 | 24 | import ( 25 | "context" 26 | "encoding/binary" 27 | "time" 28 | 29 | lru "github.com/hashicorp/golang-lru/v2" 30 | ) 31 | 32 | var _ CacheStore = (*lruStore)(nil) 33 | 34 | type lruStore struct { 35 | usePeek bool 36 | store *lru.Cache[string, []byte] 37 | } 38 | 39 | const expiredByteSize = 4 40 | 41 | func now_seconds() uint32 { 42 | return uint32(time.Now().Unix()) 43 | } 44 | 45 | func (s *lruStore) Get(ctx context.Context, key string) ([]byte, error) { 46 | var value []byte 47 | var ok bool 48 | // 使用peek更高性能 49 | if s.usePeek { 50 | value, ok = s.store.Peek(key) 51 | } else { 52 | value, ok = s.store.Get(key) 53 | } 54 | if !ok || len(value) < expiredByteSize { 55 | return nil, nil 56 | } 57 | expired := binary.BigEndian.Uint32(value) 58 | if now_seconds() > expired { 59 | return nil, nil 60 | } 61 | return value[expiredByteSize:], nil 62 | } 63 | 64 | func (s *lruStore) Set(ctx context.Context, key string, data []byte, ttl time.Duration) error { 65 | buf := make([]byte, len(data)+expiredByteSize) 66 | expired := now_seconds() + uint32(ttl/time.Second) 67 | binary.BigEndian.PutUint32(buf, expired) 68 | copy(buf[expiredByteSize:], data) 69 | s.store.Add(key, buf) 70 | return nil 71 | } 72 | 73 | func newLruStore(size int, usePeek bool) *lruStore { 74 | if size <= 0 { 75 | size = 128 76 | } 77 | // 只要size > 0则不会出错 78 | s, _ := lru.New[string, []byte](size) 79 | return &lruStore{ 80 | usePeek: usePeek, 81 | store: s, 82 | } 83 | } 84 | 85 | // NewPeekLruStore creates a lru store use peek 86 | func NewPeekLruStore(size int) *lruStore { 87 | return newLruStore(size, true) 88 | } 89 | 90 | // NewLruStore creates a lru store 91 | func NewLruStore(size int) *lruStore { 92 | return newLruStore(size, false) 93 | } 94 | -------------------------------------------------------------------------------- /middleware/etag.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "crypto/sha1" 27 | "encoding/base64" 28 | "fmt" 29 | "net/http" 30 | 31 | "github.com/vicanso/elton" 32 | ) 33 | 34 | type ( 35 | // ETagConfig ETag config 36 | ETagConfig struct { 37 | Skipper elton.Skipper 38 | } 39 | ) 40 | 41 | // gen generate eTag 42 | func genETag(buf []byte) (string, error) { 43 | size := len(buf) 44 | if size == 0 { 45 | return `"0-2jmj7l5rSw0yVb_vlWAYkK_YBwk="`, nil 46 | } 47 | h := sha1.New() 48 | _, err := h.Write(buf) 49 | if err != nil { 50 | return "", err 51 | } 52 | hash := base64.URLEncoding.EncodeToString(h.Sum(nil)) 53 | return fmt.Sprintf(`"%x-%s"`, size, hash), nil 54 | } 55 | 56 | // NewDefaultETag returns a default ETag middleware, it will use sha1 to generate etag. 57 | func NewDefaultETag() elton.Handler { 58 | return NewETag(ETagConfig{}) 59 | } 60 | 61 | // NewETag returns a default ETag middleware. 62 | func NewETag(config ETagConfig) elton.Handler { 63 | skipper := config.Skipper 64 | if skipper == nil { 65 | skipper = elton.DefaultSkipper 66 | } 67 | return func(c *elton.Context) error { 68 | if skipper(c) { 69 | return c.Next() 70 | } 71 | err := c.Next() 72 | if err != nil { 73 | return err 74 | } 75 | bodyBuf := c.BodyBuffer 76 | // 如果无内容或已设置 ETag ,则跳过 77 | // 因为没有内容生不生成ETag意义不大 78 | if bodyBuf == nil || bodyBuf.Len() == 0 || 79 | c.GetHeader(elton.HeaderETag) != "" { 80 | return nil 81 | } 82 | // 如果响应状态码不为0 而且( < 200 或者 >= 300),则跳过 83 | // 如果未设置状态码,最终为200 84 | statusCode := c.StatusCode 85 | if statusCode != 0 && 86 | (statusCode < http.StatusOK || 87 | statusCode >= http.StatusMultipleChoices) { 88 | return nil 89 | } 90 | eTag, _ := genETag(bodyBuf.Bytes()) 91 | if eTag != "" { 92 | c.SetHeader(elton.HeaderETag, eTag) 93 | } 94 | return nil 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /middleware/error.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "bytes" 27 | "net/http" 28 | "strings" 29 | 30 | "github.com/vicanso/elton" 31 | "github.com/vicanso/hes" 32 | ) 33 | 34 | type ( 35 | // ErrorConfig error handler config 36 | ErrorConfig struct { 37 | Skipper elton.Skipper 38 | ResponseType string 39 | } 40 | ) 41 | 42 | const ( 43 | // ErrErrorCategory error category of error handler 44 | ErrErrorCategory = "elton-error" 45 | ) 46 | 47 | // NewDefaultError return a new error handler, it will convert the error to hes.Error and response. 48 | // JSON will be used is client's request accept header support application/json, otherwise text will be used. 49 | func NewDefaultError() elton.Handler { 50 | return NewError(ErrorConfig{}) 51 | } 52 | 53 | // NewError return a new error handler. 54 | func NewError(config ErrorConfig) elton.Handler { 55 | skipper := config.Skipper 56 | if skipper == nil { 57 | skipper = elton.DefaultSkipper 58 | } 59 | return func(c *elton.Context) error { 60 | if skipper(c) { 61 | return c.Next() 62 | } 63 | err := c.Next() 64 | // 如果没有出错,直接返回 65 | if err == nil { 66 | return nil 67 | } 68 | he, ok := err.(*hes.Error) 69 | if !ok { 70 | he = hes.Wrap(err) 71 | // 非hes的error,则都认为是500出错异常 72 | he.StatusCode = http.StatusInternalServerError 73 | he.Exception = true 74 | he.Category = ErrErrorCategory 75 | } 76 | c.StatusCode = he.StatusCode 77 | if config.ResponseType == "json" || 78 | strings.Contains(c.GetRequestHeader("Accept"), "application/json") { 79 | buf := he.ToJSON() 80 | c.BodyBuffer = bytes.NewBuffer(buf) 81 | c.SetHeader(elton.HeaderContentType, elton.MIMEApplicationJSON) 82 | } else { 83 | c.BodyBuffer = bytes.NewBufferString(he.Error()) 84 | c.SetHeader(elton.HeaderContentType, elton.MIMETextPlain) 85 | } 86 | 87 | return nil 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /middleware/recover_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "net/http" 27 | "net/http/httptest" 28 | "testing" 29 | 30 | "github.com/stretchr/testify/assert" 31 | "github.com/vicanso/elton" 32 | ) 33 | 34 | func TestRecoverResponseText(t *testing.T) { 35 | // panic响应返回text 36 | assert := assert.New(t) 37 | var ctx *elton.Context 38 | e := elton.New() 39 | e.Use(NewRecover()) 40 | e.GET("/", func(c *elton.Context) error { 41 | ctx = c 42 | panic("abc") 43 | }) 44 | req := httptest.NewRequest("GET", "https://aslant.site/", nil) 45 | resp := httptest.NewRecorder() 46 | keys := []string{ 47 | elton.HeaderETag, 48 | elton.HeaderLastModified, 49 | elton.HeaderContentEncoding, 50 | elton.HeaderContentLength, 51 | } 52 | for _, key := range keys { 53 | resp.Header().Set(key, "a") 54 | } 55 | 56 | catchError := false 57 | e.OnError(func(_ *elton.Context, _ error) { 58 | catchError = true 59 | }) 60 | 61 | e.ServeHTTP(resp, req) 62 | assert.Equal(http.StatusInternalServerError, resp.Code) 63 | assert.Equal("statusCode=500, category=elton-recover, message=abc", resp.Body.String()) 64 | assert.True(ctx.Committed) 65 | assert.True(catchError) 66 | for _, key := range keys { 67 | assert.Empty(ctx.GetHeader(key), "header should be resetted") 68 | } 69 | } 70 | func TestRecoverResponseJSON(t *testing.T) { 71 | // 响应返回json 72 | assert := assert.New(t) 73 | e := elton.New() 74 | e.Use(NewRecover()) 75 | e.GET("/", func(c *elton.Context) error { 76 | panic("abc") 77 | }) 78 | req := httptest.NewRequest("GET", "https://aslant.site/", nil) 79 | req.Header.Set("Accept", "application/json, text/plain, */*") 80 | resp := httptest.NewRecorder() 81 | e.ServeHTTP(resp, req) 82 | assert.Equal(500, resp.Code) 83 | assert.Equal(elton.MIMEApplicationJSON, resp.Header().Get(elton.HeaderContentType)) 84 | assert.NotEmpty(resp.Body.Bytes()) 85 | } 86 | -------------------------------------------------------------------------------- /signed_keys.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package elton 24 | 25 | import ( 26 | "sync" 27 | "sync/atomic" 28 | ) 29 | 30 | type ( 31 | // SignedKeysGenerator signed keys generator 32 | SignedKeysGenerator interface { 33 | GetKeys() []string 34 | SetKeys([]string) 35 | } 36 | // SimpleSignedKeys simple sigined key 37 | SimpleSignedKeys struct { 38 | keys []string 39 | } 40 | // RWMutexSignedKeys read/write mutex signed key 41 | RWMutexSignedKeys struct { 42 | sync.RWMutex 43 | keys []string 44 | } 45 | // AtomicSignedKeys atomic toggle signed keys 46 | AtomicSignedKeys struct { 47 | value atomic.Value 48 | } 49 | ) 50 | 51 | // GetKeys returns the key list of simple signed keys 52 | func (sk *SimpleSignedKeys) GetKeys() []string { 53 | return sk.keys 54 | } 55 | 56 | // SetKeys sets the key list of simple signed keys 57 | func (sk *SimpleSignedKeys) SetKeys(values []string) { 58 | keys := make([]string, len(values)) 59 | copy(keys, values) 60 | sk.keys = keys 61 | } 62 | 63 | // GetKeys returns the key list of rwmutex signed keys 64 | func (rwSk *RWMutexSignedKeys) GetKeys() []string { 65 | rwSk.RLock() 66 | defer rwSk.RUnlock() 67 | return rwSk.keys 68 | } 69 | 70 | // SetKeys sets the key list of rwmutex signed keys 71 | func (rwSk *RWMutexSignedKeys) SetKeys(values []string) { 72 | keys := make([]string, len(values)) 73 | copy(keys, values) 74 | rwSk.Lock() 75 | defer rwSk.Unlock() 76 | rwSk.keys = keys 77 | } 78 | 79 | // GetKeys returns the key list of atomic signed keys 80 | func (atSk *AtomicSignedKeys) GetKeys() []string { 81 | if value := atSk.value.Load(); value != nil { 82 | // atomic value只能相同的类型,因此只要值存在,转换时直接转换 83 | return *value.(*[]string) 84 | } 85 | return nil 86 | } 87 | 88 | // SetKeys sets the key list of atomic signed keys 89 | func (atSk *AtomicSignedKeys) SetKeys(values []string) { 90 | keys := make([]string, len(values)) 91 | copy(keys, values) 92 | atSk.value.Store(&keys) 93 | } 94 | -------------------------------------------------------------------------------- /middleware/cache_compressor_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2021 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "bytes" 27 | "testing" 28 | 29 | "github.com/stretchr/testify/assert" 30 | ) 31 | 32 | func TestNewBrotliCompress(t *testing.T) { 33 | assert := assert.New(t) 34 | compressor := NewCacheBrCompressor() 35 | compressor.MinLength = 20 36 | 37 | assert.False(compressor.IsValid("text", 1)) 38 | assert.True(compressor.IsValid("text", 100)) 39 | 40 | data := bytes.NewBufferString("hello world!hello world!hello world!") 41 | result, compressionType, err := compressor.Compress(data) 42 | assert.Nil(err) 43 | assert.Equal(CompressionBr, compressionType) 44 | assert.NotEqual(data, result) 45 | result, _ = BrotliDecompress(result.Bytes()) 46 | assert.Equal(data, result) 47 | } 48 | 49 | func TestNewGzipCompress(t *testing.T) { 50 | assert := assert.New(t) 51 | compressor := NewCacheGzipCompressor() 52 | compressor.MinLength = 20 53 | 54 | assert.False(compressor.IsValid("text", 1)) 55 | assert.True(compressor.IsValid("text", 100)) 56 | 57 | data := bytes.NewBufferString("hello world!hello world!hello world!") 58 | result, compressionType, err := compressor.Compress(data) 59 | assert.Equal(CompressionGzip, compressionType) 60 | assert.Nil(err) 61 | assert.NotEqual(data, result) 62 | result, _ = GzipDecompress(result.Bytes()) 63 | assert.Equal(data, result) 64 | } 65 | 66 | func TestNewZstdCompress(t *testing.T) { 67 | assert := assert.New(t) 68 | compressor := NewCacheZstdCompressor() 69 | compressor.MinLength = 20 70 | 71 | assert.False(compressor.IsValid("text", 1)) 72 | assert.True(compressor.IsValid("text", 100)) 73 | 74 | data := bytes.NewBufferString("hello world!hello world!hello world!") 75 | result, compressionType, err := compressor.Compress(data) 76 | assert.Equal(CompressionZstd, compressionType) 77 | assert.Nil(err) 78 | assert.NotEqual(data, result) 79 | result, _ = ZstdDecompress(result.Bytes()) 80 | assert.Equal(data, result) 81 | } 82 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2022 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package elton 24 | 25 | import ( 26 | "bytes" 27 | "errors" 28 | "testing" 29 | 30 | "github.com/stretchr/testify/assert" 31 | ) 32 | 33 | func TestBufferPool(t *testing.T) { 34 | assert := assert.New(t) 35 | 36 | bp := NewBufferPool(512) 37 | 38 | buf := bp.Get() 39 | assert.NotNil(buf) 40 | assert.Equal(0, buf.Len()) 41 | buf.WriteString("abc") 42 | bp.Put(buf) 43 | 44 | buf = bp.Get() 45 | assert.NotNil(buf) 46 | } 47 | 48 | func TestReadAllInitCap(t *testing.T) { 49 | assert := assert.New(t) 50 | 51 | buf := &bytes.Buffer{} 52 | for i := 0; i < 1024*1024; i++ { 53 | buf.Write([]byte("hello world!")) 54 | } 55 | result := buf.Bytes() 56 | 57 | data, err := ReadAllInitCap(buf, 1024*100) 58 | assert.Nil(err) 59 | assert.Equal(result, data) 60 | 61 | data, err = ReadAllInitCap(bytes.NewBufferString("hello world!"), 1024*100) 62 | assert.Nil(err) 63 | assert.Equal([]byte("hello world!"), data) 64 | } 65 | 66 | func TestReadAllToBuffer(t *testing.T) { 67 | assert := assert.New(t) 68 | 69 | source := &bytes.Buffer{} 70 | for i := 0; i < 1024*1024; i++ { 71 | source.Write([]byte("hello world!")) 72 | } 73 | sourceBytes := source.Bytes() 74 | 75 | buf := bytes.Buffer{} 76 | err := ReadAllToBuffer(source, &buf) 77 | assert.Nil(err) 78 | assert.Equal(sourceBytes, buf.Bytes()) 79 | 80 | buf.Reset() 81 | err = ReadAllToBuffer(bytes.NewBufferString("hello world!"), &buf) 82 | assert.Nil(err) 83 | assert.Equal([]byte("hello world!"), buf.Bytes()) 84 | } 85 | 86 | func BenchmarkReadAllInitCap(b *testing.B) { 87 | buf := &bytes.Buffer{} 88 | for i := 0; i < 1024*1024; i++ { 89 | buf.Write([]byte("hello world!")) 90 | } 91 | result := buf.Bytes() 92 | size := buf.Len() 93 | for i := 0; i < b.N; i++ { 94 | data, err := ReadAllInitCap(bytes.NewBuffer(result), 1024*1024) 95 | if err != nil { 96 | panic(err) 97 | } 98 | if len(data) != size { 99 | panic(errors.New("data is invalid")) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /signed_keys_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package elton 24 | 25 | import ( 26 | "testing" 27 | 28 | "github.com/stretchr/testify/assert" 29 | ) 30 | 31 | func TestSimpleSignedKeys(t *testing.T) { 32 | assert := assert.New(t) 33 | var sk SignedKeysGenerator 34 | keys := []string{ 35 | "a", 36 | "b", 37 | } 38 | sk = new(SimpleSignedKeys) 39 | sk.SetKeys(keys) 40 | assert.Equal(sk.GetKeys(), keys) 41 | } 42 | 43 | func TestRWMutexSignedKeys(t *testing.T) { 44 | assert := assert.New(t) 45 | var sk SignedKeysGenerator 46 | keys := []string{ 47 | "a", 48 | "b", 49 | } 50 | sk = new(RWMutexSignedKeys) 51 | sk.SetKeys(keys) 52 | assert.Equal(sk.GetKeys(), keys) 53 | done := make(chan bool) 54 | max := 10000 55 | go func() { 56 | for index := 0; index < max; index++ { 57 | sk.SetKeys([]string{"a"}) 58 | } 59 | done <- true 60 | }() 61 | for index := 0; index < max; index++ { 62 | sk.GetKeys() 63 | } 64 | <-done 65 | } 66 | 67 | func TestAtomicSignedKeys(t *testing.T) { 68 | assert := assert.New(t) 69 | var sk SignedKeysGenerator 70 | keys := []string{ 71 | "a", 72 | "b", 73 | } 74 | sk = new(AtomicSignedKeys) 75 | sk.SetKeys(keys) 76 | assert.Equal(sk.GetKeys(), keys) 77 | done := make(chan bool) 78 | max := 10000 79 | go func() { 80 | for index := 0; index < max; index++ { 81 | sk.SetKeys([]string{"a"}) 82 | } 83 | done <- true 84 | }() 85 | for index := 0; index < max; index++ { 86 | keys := sk.GetKeys() 87 | if len(keys) == 2 { 88 | assert.Equal([]string{"a", "b"}, keys) 89 | } else { 90 | assert.Equal([]string{"a"}, keys) 91 | } 92 | } 93 | <-done 94 | } 95 | 96 | func BenchmarkRWMutexSignedKeys(b *testing.B) { 97 | sk := new(RWMutexSignedKeys) 98 | sk.SetKeys([]string{"a"}) 99 | for i := 0; i < b.N; i++ { 100 | sk.GetKeys() 101 | } 102 | } 103 | 104 | func BenchmarkAtomicSignedKeys(b *testing.B) { 105 | sk := new(AtomicSignedKeys) 106 | sk.SetKeys([]string{"a"}) 107 | for i := 0; i < b.N; i++ { 108 | sk.GetKeys() 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2022 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package elton 24 | 25 | import ( 26 | "bytes" 27 | "io" 28 | "sync" 29 | ) 30 | 31 | // A BufferPool is an interface for getting and 32 | // returning temporary buffer 33 | type BufferPool interface { 34 | Get() *bytes.Buffer 35 | Put(*bytes.Buffer) 36 | } 37 | 38 | type simpleBufferPool struct { 39 | pool sync.Pool 40 | } 41 | 42 | // NewBufferPool creates a buffer pool, if the init cap gt 0, 43 | // the buffer will be init with cap size 44 | func NewBufferPool(initCap int) BufferPool { 45 | p := &simpleBufferPool{} 46 | p.pool.New = func() interface{} { 47 | if initCap > 0 { 48 | return bytes.NewBuffer(make([]byte, 0, initCap)) 49 | } 50 | return &bytes.Buffer{} 51 | } 52 | return p 53 | } 54 | 55 | func (sp *simpleBufferPool) Get() *bytes.Buffer { 56 | return sp.pool.Get().(*bytes.Buffer) 57 | } 58 | 59 | func (sp *simpleBufferPool) Put(buf *bytes.Buffer) { 60 | sp.pool.Put(buf) 61 | } 62 | 63 | // copy from io.ReadAll 64 | // ReadAll reads from r until an error or EOF and returns the data it read. 65 | // A successful call returns err == nil, not err == EOF. Because ReadAll is 66 | // defined to read from src until EOF, it does not treat an EOF from Read 67 | // as an error to be reported. 68 | func ReadAllInitCap(r io.Reader, initCap int) ([]byte, error) { 69 | if initCap <= 0 { 70 | initCap = 512 71 | } 72 | b := make([]byte, 0, initCap) 73 | for { 74 | if len(b) == cap(b) { 75 | // Add more capacity (let append pick how much). 76 | b = append(b, 0)[:len(b)] 77 | } 78 | n, err := r.Read(b[len(b):cap(b)]) 79 | b = b[:len(b)+n] 80 | if err != nil { 81 | if err == io.EOF { 82 | err = nil 83 | } 84 | return b, err 85 | } 86 | } 87 | } 88 | 89 | // ReadAllToBuffer reader from r util an error or EOF and write data to buffer. 90 | // A successful call returns err == nil, not err == EOF. Because ReadAll is 91 | // defined to read from src until EOF, it does not treat an EOF from Read 92 | // as an error to be reported. 93 | func ReadAllToBuffer(r io.Reader, buffer *bytes.Buffer) error { 94 | b := make([]byte, 512) 95 | for { 96 | n, err := r.Read(b) 97 | // 先将读取数据写入 98 | buffer.Write(b[0:n]) 99 | if err != nil { 100 | if err == io.EOF { 101 | err = nil 102 | } 103 | return err 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /middleware/stats_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "bytes" 27 | "errors" 28 | "net/http/httptest" 29 | "testing" 30 | 31 | "github.com/stretchr/testify/assert" 32 | "github.com/vicanso/elton" 33 | "github.com/vicanso/hes" 34 | ) 35 | 36 | func TestNoStatsPanic(t *testing.T) { 37 | assert := assert.New(t) 38 | done := false 39 | defer func() { 40 | r := recover() 41 | assert.Equal(r.(error), ErrStatsNoFunction) 42 | done = true 43 | }() 44 | NewStats(StatsConfig{}) 45 | assert.True(done) 46 | } 47 | 48 | func TestStats(t *testing.T) { 49 | assert := assert.New(t) 50 | 51 | statsKey := "_stats" 52 | defaultStats := NewStats(StatsConfig{ 53 | OnStats: func(info *StatsInfo, c *elton.Context) { 54 | c.Set(statsKey, info) 55 | }, 56 | }) 57 | 58 | tests := []struct { 59 | newContext func() *elton.Context 60 | err error 61 | statusCode int 62 | }{ 63 | // http error 64 | { 65 | newContext: func() *elton.Context { 66 | req := httptest.NewRequest("GET", "/", nil) 67 | resp := httptest.NewRecorder() 68 | c := elton.NewContext(resp, req) 69 | c.Next = func() error { 70 | return hes.New("abc") 71 | } 72 | return c 73 | }, 74 | err: hes.New("abc"), 75 | statusCode: 400, 76 | }, 77 | // error 78 | { 79 | newContext: func() *elton.Context { 80 | req := httptest.NewRequest("GET", "/", nil) 81 | resp := httptest.NewRecorder() 82 | c := elton.NewContext(resp, req) 83 | c.Next = func() error { 84 | return errors.New("abc") 85 | } 86 | return c 87 | }, 88 | err: errors.New("abc"), 89 | statusCode: 500, 90 | }, 91 | { 92 | newContext: func() *elton.Context { 93 | req := httptest.NewRequest("GET", "/", nil) 94 | resp := httptest.NewRecorder() 95 | c := elton.NewContext(resp, req) 96 | c.BodyBuffer = bytes.NewBufferString("abcd") 97 | c.Next = func() error { 98 | return nil 99 | } 100 | return c 101 | }, 102 | statusCode: 200, 103 | }, 104 | } 105 | 106 | for _, tt := range tests { 107 | c := tt.newContext() 108 | err := defaultStats(c) 109 | assert.Equal(tt.err, err) 110 | v, ok := c.Get(statsKey) 111 | assert.True(ok) 112 | info, ok := v.(*StatsInfo) 113 | assert.True(ok) 114 | assert.Equal(tt.statusCode, info.Status) 115 | 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /template.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2021 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package elton 24 | 25 | import ( 26 | "bytes" 27 | "context" 28 | "html/template" 29 | "os" 30 | ) 31 | 32 | type TemplateParser interface { 33 | Render(ctx context.Context, text string, data interface{}) (string, error) 34 | RenderFile(ctx context.Context, filename string, data interface{}) (string, error) 35 | } 36 | type TemplateParsers map[string]TemplateParser 37 | 38 | // ReadFile defines how to read file 39 | type ReadFile func(filename string) ([]byte, error) 40 | 41 | func (tps TemplateParsers) Add(template string, parser TemplateParser) { 42 | if template == "" || parser == nil { 43 | panic("template and parser can not be nil") 44 | } 45 | tps[template] = parser 46 | } 47 | func (tps TemplateParsers) Get(template string) TemplateParser { 48 | if tps == nil || template == "" { 49 | return nil 50 | } 51 | return tps[template] 52 | } 53 | func NewTemplateParsers() TemplateParsers { 54 | return make(TemplateParsers) 55 | } 56 | 57 | var _ TemplateParser = (*HTMLTemplate)(nil) 58 | var DefaultTemplateParsers = NewTemplateParsers() 59 | 60 | func init() { 61 | DefaultTemplateParsers.Add("tmpl", NewHTMLTemplate(nil)) 62 | DefaultTemplateParsers.Add("html", NewHTMLTemplate(nil)) 63 | } 64 | 65 | func NewHTMLTemplate(read ReadFile) *HTMLTemplate { 66 | return &HTMLTemplate{ 67 | readFile: read, 68 | } 69 | } 70 | 71 | type HTMLTemplate struct { 72 | readFile ReadFile 73 | } 74 | 75 | func (ht *HTMLTemplate) render(name, text string, data interface{}) (string, error) { 76 | tpl, err := template.New(name).Parse(text) 77 | if err != nil { 78 | return "", err 79 | } 80 | b := bytes.Buffer{} 81 | err = tpl.Execute(&b, data) 82 | if err != nil { 83 | return "", err 84 | } 85 | return b.String(), nil 86 | } 87 | 88 | // Render renders the text using text/template 89 | func (ht *HTMLTemplate) Render(ctx context.Context, text string, data interface{}) (string, error) { 90 | return ht.render("", text, data) 91 | } 92 | 93 | // Render renders the text of file using text/template 94 | func (ht *HTMLTemplate) RenderFile(ctx context.Context, filename string, data interface{}) (string, error) { 95 | read := ht.readFile 96 | if read == nil { 97 | read = os.ReadFile 98 | } 99 | buf, err := read(filename) 100 | if err != nil { 101 | return "", err 102 | } 103 | return ht.render(filename, string(buf), data) 104 | } 105 | -------------------------------------------------------------------------------- /middleware/brotli.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2021 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "bytes" 27 | "io" 28 | 29 | "github.com/andybalholm/brotli" 30 | "github.com/vicanso/elton" 31 | ) 32 | 33 | const ( 34 | // BrEncoding br encoding 35 | maxBrQuality = 11 36 | defaultBrQuality = 6 37 | ) 38 | 39 | type ( 40 | // BrCompressor brotli compress 41 | BrCompressor struct { 42 | Level int 43 | MinLength int 44 | } 45 | ) 46 | 47 | func (b *BrCompressor) getLevel() int { 48 | level := b.Level 49 | if level <= 0 { 50 | level = defaultBrQuality 51 | } 52 | if level > maxBrQuality { 53 | level = maxBrQuality 54 | } 55 | return level 56 | } 57 | 58 | func (b *BrCompressor) getMinLength() int { 59 | if b.MinLength == 0 { 60 | return DefaultCompressMinLength 61 | } 62 | return b.MinLength 63 | } 64 | 65 | // Accept check accept econding 66 | func (b *BrCompressor) Accept(c *elton.Context, bodySize int) (acceptable bool, encoding string) { 67 | // 如果数据少于最低压缩长度,则不压缩 68 | if bodySize >= 0 && bodySize < b.getMinLength() { 69 | return 70 | } 71 | return AcceptEncoding(c, elton.Br) 72 | } 73 | 74 | // BrotliCompress compress data by brotli 75 | func BrotliCompress(buf []byte, level int) (*bytes.Buffer, error) { 76 | buffer := new(bytes.Buffer) 77 | w := brotli.NewWriterLevel(buffer, level) 78 | _, err := w.Write(buf) 79 | if err != nil { 80 | return nil, err 81 | } 82 | // 直接调用close触发数据的flush 83 | err = w.Close() 84 | if err != nil { 85 | return nil, err 86 | } 87 | return buffer, nil 88 | } 89 | 90 | // BrotliDecompress decompress data of brotli 91 | func BrotliDecompress(buf []byte) (*bytes.Buffer, error) { 92 | r := brotli.NewReader(bytes.NewBuffer(buf)) 93 | data, err := io.ReadAll(r) 94 | if err != nil { 95 | return nil, err 96 | } 97 | return bytes.NewBuffer(data), nil 98 | } 99 | 100 | // Compress brotli compress 101 | func (b *BrCompressor) Compress(buf []byte, levels ...int) (*bytes.Buffer, error) { 102 | level := b.getLevel() 103 | if len(levels) != 0 && levels[0] != IgnoreCompression { 104 | level = levels[0] 105 | } 106 | return BrotliCompress(buf, level) 107 | } 108 | 109 | // Pipe brotli pipe 110 | func (b *BrCompressor) Pipe(c *elton.Context) (err error) { 111 | r := c.Body.(io.Reader) 112 | closer, ok := c.Body.(io.Closer) 113 | if ok { 114 | defer func() { 115 | _ = closer.Close() 116 | }() 117 | } 118 | w := brotli.NewWriterLevel(c.Response, b.getLevel()) 119 | 120 | defer func() { 121 | _ = w.Close() 122 | }() 123 | _, err = io.Copy(w, r) 124 | return 125 | } 126 | -------------------------------------------------------------------------------- /middleware/gzip.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "bytes" 27 | "compress/gzip" 28 | "io" 29 | 30 | "github.com/vicanso/elton" 31 | ) 32 | 33 | type ( 34 | // GzipCompressor gzip compress 35 | GzipCompressor struct { 36 | Level int 37 | MinLength int 38 | } 39 | ) 40 | 41 | // Accept accept gzip encoding 42 | func (g *GzipCompressor) Accept(c *elton.Context, bodySize int) (bool, string) { 43 | // 如果数据少于最低压缩长度,则不压缩(因为reader中的bodySize会设置为1,因此需要判断>=0) 44 | if bodySize >= 0 && bodySize < g.getMinLength() { 45 | return false, "" 46 | } 47 | return AcceptEncoding(c, elton.Gzip) 48 | } 49 | 50 | // GzipCompress compress data by gzip 51 | func GzipCompress(buf []byte, level int) (*bytes.Buffer, error) { 52 | buffer := new(bytes.Buffer) 53 | 54 | w, err := gzip.NewWriterLevel(buffer, level) 55 | if err != nil { 56 | return nil, err 57 | } 58 | _, err = w.Write(buf) 59 | if err != nil { 60 | return nil, err 61 | } 62 | err = w.Close() 63 | if err != nil { 64 | return nil, err 65 | } 66 | return buffer, nil 67 | } 68 | 69 | // GzipDecompress decompress data of gzip 70 | func GzipDecompress(buf []byte) (*bytes.Buffer, error) { 71 | r, err := gzip.NewReader(bytes.NewBuffer(buf)) 72 | if err != nil { 73 | return nil, err 74 | } 75 | defer func() { 76 | _ = r.Close() 77 | }() 78 | data, err := io.ReadAll(r) 79 | if err != nil { 80 | return nil, err 81 | } 82 | return bytes.NewBuffer(data), nil 83 | } 84 | 85 | // Compress compress data by gzip 86 | func (g *GzipCompressor) Compress(buf []byte, levels ...int) (*bytes.Buffer, error) { 87 | level := g.getLevel() 88 | if len(levels) != 0 && levels[0] != IgnoreCompression { 89 | level = levels[0] 90 | } 91 | return GzipCompress(buf, level) 92 | } 93 | 94 | func (g *GzipCompressor) getLevel() int { 95 | level := g.Level 96 | if level <= 0 { 97 | level = gzip.DefaultCompression 98 | } 99 | if level > gzip.BestCompression { 100 | level = gzip.BestCompression 101 | } 102 | return level 103 | } 104 | 105 | func (g *GzipCompressor) getMinLength() int { 106 | if g.MinLength == 0 { 107 | return DefaultCompressMinLength 108 | } 109 | return g.MinLength 110 | } 111 | 112 | // Pipe compress by pipe 113 | func (g *GzipCompressor) Pipe(c *elton.Context) error { 114 | r := c.Body.(io.Reader) 115 | closer, ok := c.Body.(io.Closer) 116 | if ok { 117 | defer func() { 118 | _ = closer.Close() 119 | }() 120 | } 121 | w, _ := gzip.NewWriterLevel(c.Response, g.getLevel()) 122 | defer func() { 123 | _ = w.Close() 124 | }() 125 | _, err := io.Copy(w, r) 126 | return err 127 | } 128 | -------------------------------------------------------------------------------- /middleware/zstd.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2023 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "bytes" 27 | "io" 28 | 29 | "github.com/klauspost/compress/zstd" 30 | "github.com/vicanso/elton" 31 | ) 32 | 33 | type ( 34 | // ZstdCompressor zstd compress 35 | ZstdCompressor struct { 36 | Level int 37 | MinLength int 38 | } 39 | ) 40 | 41 | // Accept accept zstd encoding 42 | func (z *ZstdCompressor) Accept(c *elton.Context, bodySize int) (bool, string) { 43 | 44 | // 如果数据少于最低压缩长度,则不压缩(因为reader中的bodySize会设置为1,因此需要判断>=0) 45 | if bodySize >= 0 && bodySize < z.getMinLength() { 46 | return false, "" 47 | } 48 | return AcceptEncoding(c, elton.Zstd) 49 | } 50 | 51 | func (z *ZstdCompressor) getLevel() int { 52 | level := z.Level 53 | if level <= 0 { 54 | level = int(zstd.SpeedBetterCompression) 55 | } 56 | if level > int(zstd.SpeedBestCompression) { 57 | level = int(zstd.SpeedBestCompression) 58 | } 59 | return level 60 | } 61 | 62 | func (z *ZstdCompressor) getMinLength() int { 63 | if z.MinLength == 0 { 64 | return DefaultCompressMinLength 65 | } 66 | return z.MinLength 67 | } 68 | 69 | // Compress compress data by zstd 70 | func (z *ZstdCompressor) Compress(buf []byte, levels ...int) (*bytes.Buffer, error) { 71 | level := z.getLevel() 72 | if len(levels) != 0 && levels[0] != IgnoreCompression { 73 | level = levels[0] 74 | } 75 | return ZstdCompress(buf, level) 76 | } 77 | 78 | // Pipe compress by pipe 79 | func (z *ZstdCompressor) Pipe(c *elton.Context) error { 80 | r := c.Body.(io.Reader) 81 | closer, ok := c.Body.(io.Closer) 82 | if ok { 83 | defer func() { 84 | _ = closer.Close() 85 | }() 86 | } 87 | enc, err := zstd.NewWriter(c.Response, zstd.WithEncoderLevel(zstd.EncoderLevel(z.getLevel()))) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | _, err = io.Copy(enc, r) 93 | if err != nil { 94 | _ = enc.Close() 95 | return err 96 | } 97 | return enc.Close() 98 | } 99 | 100 | // ZstdCompressor compress data by zstd 101 | func ZstdCompress(buf []byte, level int) (*bytes.Buffer, error) { 102 | encoder, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.EncoderLevel(level))) 103 | if err != nil { 104 | return nil, err 105 | } 106 | dst := encoder.EncodeAll(buf, make([]byte, 0, len(buf))) 107 | return bytes.NewBuffer(dst), nil 108 | } 109 | 110 | // ZstdDecompress decompress data of zstd 111 | func ZstdDecompress(buf []byte) (*bytes.Buffer, error) { 112 | r, err := zstd.NewReader(bytes.NewBuffer(buf)) 113 | if err != nil { 114 | return nil, err 115 | } 116 | defer r.Close() 117 | data, err := io.ReadAll(r) 118 | if err != nil { 119 | return nil, err 120 | } 121 | return bytes.NewBuffer(data), nil 122 | } 123 | -------------------------------------------------------------------------------- /middleware/tracker_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "errors" 27 | "net/http/httptest" 28 | "testing" 29 | 30 | "github.com/stretchr/testify/assert" 31 | "github.com/vicanso/elton" 32 | ) 33 | 34 | func TestNoTrackPanic(t *testing.T) { 35 | assert := assert.New(t) 36 | done := false 37 | defer func() { 38 | r := recover() 39 | assert.Equal(ErrTrackerNoFunction, r.(error)) 40 | done = true 41 | }() 42 | 43 | NewTracker(TrackerConfig{}) 44 | assert.True(done) 45 | } 46 | 47 | func TestConvertMap(t *testing.T) { 48 | assert := assert.New(t) 49 | assert.Nil(convertMap(nil, nil, 0)) 50 | assert.Equal(map[string]string{ 51 | "foo": "b ... (2 more)", 52 | "password": "***", 53 | }, convertMap(map[string]string{ 54 | "password": "123", 55 | "foo": "bar", 56 | }, defaultTrackerMaskFields, 1)) 57 | } 58 | 59 | func TestTracker(t *testing.T) { 60 | assert := assert.New(t) 61 | 62 | skipErr := errors.New("skip error") 63 | // next直接返回skip error,用于判断是否执行了next 64 | next := func() error { 65 | return skipErr 66 | } 67 | 68 | trackerInfoKey := "_trackerInfo" 69 | defaultTracker := NewTracker(TrackerConfig{ 70 | OnTrack: func(info *TrackerInfo, c *elton.Context) { 71 | c.Set(trackerInfoKey, info) 72 | }, 73 | }) 74 | tests := []struct { 75 | newContext func() *elton.Context 76 | err error 77 | info *TrackerInfo 78 | }{ 79 | { 80 | newContext: func() *elton.Context { 81 | req := httptest.NewRequest("POST", "/users/login?type=1&passwordType=2", nil) 82 | c := elton.NewContext(nil, req) 83 | c.RequestBody = []byte(`{ 84 | "account": "tree.xie tree.xie tree.xie", 85 | "password": "password" 86 | }`) 87 | c.Params = new(elton.RouteParams) 88 | c.Params.Add("category", "login") 89 | c.Next = next 90 | return c 91 | }, 92 | err: skipErr, 93 | info: &TrackerInfo{ 94 | Result: HandleFail, 95 | Query: map[string]string{ 96 | "type": "1", 97 | "passwordType": "***", 98 | }, 99 | Params: map[string]string{ 100 | "category": "login", 101 | }, 102 | Form: map[string]interface{}{ 103 | "account": "tree.xie tree.xie tr ... (6 more)", 104 | "password": "***", 105 | }, 106 | Err: skipErr, 107 | }, 108 | }, 109 | } 110 | 111 | for _, tt := range tests { 112 | c := tt.newContext() 113 | err := defaultTracker(c) 114 | assert.Equal(tt.err, err) 115 | v, ok := c.Get(trackerInfoKey) 116 | assert.True(ok) 117 | info, ok := v.(*TrackerInfo) 118 | assert.True(ok) 119 | assert.NotEmpty(info.Latency) 120 | // 重置耗时 121 | info.Latency = 0 122 | assert.Equal(tt.info, info) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /docs/custom_compress.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 自定义压缩 3 | --- 4 | 5 | 绝大多数客户端都支持多种压缩方式,需要不同的场景选择适合的压缩算法,微服务之间的调用更是可以选择一些高性能的压缩方式,下面来介绍如何编写自定义的压缩中间件,主要的要点如下: 6 | 7 | - 根据请求头`Accept-Encoding`判断客户端支持的压缩算法 8 | - 设定最小压缩长度,避免对较小数据的压缩浪费性能 9 | - 根据响应头`Content-Type`判断只压缩文本类的响应数据 10 | - 根据场景平衡压缩率与性能的选择,如内网的可以选择snappy,lz4等高效压缩算法 11 | 12 | [elton-compress](https://github.com/vicanso/elton-compress)中间件提供了其它几种常用的压缩方式,包括`zstd`以及`snappy`等。如果要增加压缩方式,只需要实现`Compressor`的三个函数则可。 13 | 14 | ```go 15 | // Compressor compressor interface 16 | Compressor interface { 17 | // Accept accept check function 18 | Accept(c *elton.Context, bodySize int) (acceptable bool, encoding string) 19 | // Compress compress function 20 | Compress([]byte) (*bytes.Buffer, error) 21 | // Pipe pipe function 22 | Pipe(*elton.Context) error 23 | } 24 | ``` 25 | 26 | 下面是elton-compress中间件lz4压缩的实现代码: 27 | 28 | ```go 29 | package compress 30 | 31 | import ( 32 | "bytes" 33 | "io" 34 | 35 | "github.com/pierrec/lz4" 36 | "github.com/vicanso/elton" 37 | ) 38 | 39 | const ( 40 | // Lz4Encoding lz4 encoding 41 | Lz4Encoding = "lz4" 42 | ) 43 | 44 | type ( 45 | // Lz4Compressor lz4 compress 46 | Lz4Compressor struct { 47 | Level int 48 | MinLength int 49 | } 50 | ) 51 | 52 | func (l *Lz4Compressor) getMinLength() int { 53 | if l.MinLength == 0 { 54 | return defaultCompressMinLength 55 | } 56 | return l.MinLength 57 | } 58 | 59 | // Accept check accept encoding 60 | func (l *Lz4Compressor) Accept(c *elton.Context, bodySize int) (acceptable bool, encoding string) { 61 | // 如果数据少于最低压缩长度,则不压缩 62 | if bodySize >= 0 && bodySize < l.getMinLength() { 63 | return 64 | } 65 | return AcceptEncoding(c, Lz4Encoding) 66 | } 67 | 68 | // Compress lz4 compress 69 | func (l *Lz4Compressor) Compress(buf []byte) (*bytes.Buffer, error) { 70 | buffer := new(bytes.Buffer) 71 | w := lz4.NewWriter(buffer) 72 | defer w.Close() 73 | w.Header.CompressionLevel = l.Level 74 | _, err := w.Write(buf) 75 | if err != nil { 76 | return nil, err 77 | } 78 | return buffer, nil 79 | } 80 | 81 | // Pipe lz4 pipe compress 82 | func (l *Lz4Compressor) Pipe(c *elton.Context) (err error) { 83 | r := c.Body.(io.Reader) 84 | closer, ok := c.Body.(io.Closer) 85 | if ok { 86 | defer closer.Close() 87 | } 88 | w := lz4.NewWriter(c.Response) 89 | w.Header.CompressionLevel = l.Level 90 | defer w.Close() 91 | _, err = io.Copy(w, r) 92 | return 93 | } 94 | ``` 95 | 96 | 下面调用示例: 97 | 98 | ```go 99 | package main 100 | 101 | import ( 102 | "bytes" 103 | 104 | "github.com/vicanso/elton" 105 | compress "github.com/vicanso/elton-compress" 106 | "github.com/vicanso/elton/middleware" 107 | ) 108 | 109 | func main() { 110 | d := elton.New() 111 | 112 | conf := middleware.CompressConfig{} 113 | lz4 := &compress.Lz4Compressor{ 114 | Level: 2, 115 | MinLength: 1024, 116 | } 117 | conf.AddCompressor(lz4) 118 | d.Use(middleware.NewCompress(conf)) 119 | 120 | d.GET("/", func(c *elton.Context) (err error) { 121 | b := new(bytes.Buffer) 122 | for i := 0; i < 1000; i++ { 123 | b.WriteString("Hello, World!\n") 124 | } 125 | c.SetHeader(elton.HeaderContentType, "text/plain; charset=utf-8") 126 | c.BodyBuffer = b 127 | return 128 | }) 129 | 130 | err := d.ListenAndServe(":3000") 131 | if err != nil { 132 | panic(err) 133 | } 134 | } 135 | ``` 136 | 137 | 138 | ``` 139 | curl -H 'Accept-Encoding:lz4' -v 'http://127.0.0.1:3000' 140 | * Rebuilt URL to: http://127.0.0.1:3000/ 141 | * Trying 127.0.0.1... 142 | * TCP_NODELAY set 143 | * Connected to 127.0.0.1 (127.0.0.1) port 3000 (#0) 144 | > GET / HTTP/1.1 145 | > Host: 127.0.0.1:3000 146 | > User-Agent: curl/7.54.0 147 | > Accept: */* 148 | > Accept-Encoding:lz4 149 | > 150 | < HTTP/1.1 200 OK 151 | < Content-Encoding: lz4 152 | < Content-Length: 103 153 | < Content-Type: text/plain; charset=utf-8 154 | < Vary: Accept-Encoding 155 | ... 156 | ``` 157 | 158 | 从响应头中可以看出,数据已经压缩为`lz4`的格式,数据长度仅为`103`字节,节约了带宽。 159 | -------------------------------------------------------------------------------- /middleware/renderer.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2021 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "bytes" 27 | "path/filepath" 28 | 29 | "github.com/vicanso/elton" 30 | "github.com/vicanso/hes" 31 | ) 32 | 33 | type RenderData struct { 34 | File string 35 | Text string 36 | TemplateType string 37 | Data interface{} 38 | } 39 | 40 | type RendererConfig struct { 41 | Skipper elton.Skipper 42 | ViewPath string 43 | Parsers elton.TemplateParsers 44 | } 45 | 46 | func (data *RenderData) getTemplateType() string { 47 | // 获取模板类型 48 | templateType := data.TemplateType 49 | if templateType == "" && data.File != "" { 50 | ext := filepath.Ext(data.File) 51 | if ext != "" { 52 | templateType = ext[1:] 53 | } 54 | } 55 | // 默认为html模板 56 | if templateType == "" { 57 | templateType = "html" 58 | } 59 | return templateType 60 | } 61 | 62 | const ErrRendererCategory = "elton-renderer" 63 | 64 | var ( 65 | ErrTemplateTypeInvalid = &hes.Error{ 66 | Exception: true, 67 | StatusCode: 500, 68 | Message: "template type is invalid", 69 | Category: ErrRendererCategory, 70 | } 71 | ErrFileAndTextNil = &hes.Error{ 72 | Exception: true, 73 | StatusCode: 500, 74 | Message: "file and text can not be nil", 75 | Category: ErrRendererCategory, 76 | } 77 | ) 78 | 79 | // NewRenderer returns a new renderer middleware. 80 | // It will render the template with data, 81 | // and set response data as html. 82 | func NewRenderer(config RendererConfig) elton.Handler { 83 | skipper := config.Skipper 84 | if skipper == nil { 85 | skipper = elton.DefaultSkipper 86 | } 87 | parsers := config.Parsers 88 | if parsers == nil { 89 | parsers = elton.DefaultTemplateParsers 90 | } 91 | return func(c *elton.Context) error { 92 | err := c.Next() 93 | if skipper(c) { 94 | return err 95 | } 96 | if err != nil { 97 | return err 98 | } 99 | valid := false 100 | var data *RenderData 101 | switch d := c.Body.(type) { 102 | case *RenderData: 103 | valid = true 104 | data = d 105 | case RenderData: 106 | valid = true 107 | data = &d 108 | default: 109 | valid = false 110 | } 111 | // 如果设置的数据非render data 112 | // 则直接返回 113 | if !valid { 114 | return nil 115 | } 116 | // 如果文件和模板均为空 117 | if data.File == "" && data.Text == "" { 118 | return ErrFileAndTextNil 119 | } 120 | // 获取模板类型 121 | templateType := data.getTemplateType() 122 | 123 | parser := parsers.Get(templateType) 124 | if parser == nil { 125 | return ErrTemplateTypeInvalid 126 | } 127 | var html string 128 | file := filepath.Join(config.ViewPath, data.File) 129 | if file != "" { 130 | html, err = parser.RenderFile(c.Context(), file, data.Data) 131 | } else { 132 | html, err = parser.Render(c.Context(), data.Text, data.Data) 133 | } 134 | if err != nil { 135 | return err 136 | } 137 | c.SetContentTypeByExt(".html") 138 | c.BodyBuffer = bytes.NewBufferString(html) 139 | 140 | return nil 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /fresh.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2021 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package elton 24 | 25 | import ( 26 | "bytes" 27 | "net/http" 28 | "regexp" 29 | "time" 30 | ) 31 | 32 | var noCacheReg = regexp.MustCompile(`(?:^|,)\s*?no-cache\s*?(?:,|$)`) 33 | 34 | var weekTagPrefix = []byte("W/") 35 | 36 | func parseTokenList(buf []byte) [][]byte { 37 | end := 0 38 | start := 0 39 | count := len(buf) 40 | list := make([][]byte, 0) 41 | for index := 0; index < count; index++ { 42 | switch int(buf[index]) { 43 | // 空格 44 | case 0x20: 45 | if start == end { 46 | end = index + 1 47 | start = end 48 | } 49 | // , 号 50 | case 0x2c: 51 | list = append(list, buf[start:end]) 52 | end = index + 1 53 | start = end 54 | default: 55 | end = index + 1 56 | } 57 | } 58 | list = append(list, buf[start:end]) 59 | return list 60 | } 61 | 62 | func parseHTTPDate(date string) int64 { 63 | t, err := time.Parse(time.RFC1123, date) 64 | if err != nil { 65 | return 0 66 | } 67 | return t.Unix() 68 | } 69 | 70 | // isFresh returns true if the data is fresh 71 | func isFresh(modifiedSince, noneMatch, cacheControl, lastModified, etag []byte) bool { 72 | if len(modifiedSince) == 0 && len(noneMatch) == 0 { 73 | return false 74 | } 75 | if len(cacheControl) != 0 && noCacheReg.Match(cacheControl) { 76 | return false 77 | } 78 | // if none match 79 | if len(noneMatch) != 0 && (len(noneMatch) != 1 || noneMatch[0] != byte('*')) { 80 | if len(etag) == 0 { 81 | return false 82 | } 83 | matches := parseTokenList(noneMatch) 84 | etagStale := true 85 | for _, match := range matches { 86 | if bytes.Equal(match, etag) { 87 | etagStale = false 88 | break 89 | } 90 | if bytes.HasPrefix(match, weekTagPrefix) && bytes.Equal(match[2:], etag) { 91 | etagStale = false 92 | break 93 | } 94 | if bytes.HasPrefix(etag, weekTagPrefix) && bytes.Equal(etag[2:], match) { 95 | etagStale = false 96 | break 97 | } 98 | } 99 | if etagStale { 100 | return false 101 | } 102 | } 103 | // if modified since 104 | if len(modifiedSince) != 0 { 105 | if len(lastModified) == 0 { 106 | return false 107 | } 108 | lastModifiedUnix := parseHTTPDate(string(lastModified)) 109 | modifiedSinceUnix := parseHTTPDate(string(modifiedSince)) 110 | if lastModifiedUnix == 0 || modifiedSinceUnix == 0 { 111 | return false 112 | } 113 | if modifiedSinceUnix < lastModifiedUnix { 114 | return false 115 | } 116 | } 117 | return true 118 | } 119 | 120 | // Fresh returns fresh status by judget request header and response header 121 | func Fresh(reqHeader http.Header, resHeader http.Header) bool { 122 | modifiedSince := []byte(reqHeader.Get(HeaderIfModifiedSince)) 123 | noneMatch := []byte(reqHeader.Get(HeaderIfNoneMatch)) 124 | cacheControl := []byte(reqHeader.Get(HeaderCacheControl)) 125 | 126 | lastModified := []byte(resHeader.Get(HeaderLastModified)) 127 | etag := []byte(resHeader.Get(HeaderETag)) 128 | 129 | return isFresh(modifiedSince, noneMatch, cacheControl, lastModified, etag) 130 | } 131 | -------------------------------------------------------------------------------- /middleware/stats.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "errors" 27 | "net/http" 28 | "net/url" 29 | "sync/atomic" 30 | "time" 31 | 32 | "github.com/vicanso/elton" 33 | "github.com/vicanso/hes" 34 | ) 35 | 36 | var ( 37 | ErrStatsNoFunction = errors.New("require on stats function") 38 | ) 39 | 40 | type ( 41 | // OnStats on stats function 42 | OnStats func(*StatsInfo, *elton.Context) 43 | // StatsConfig stats config 44 | StatsConfig struct { 45 | OnStats OnStats 46 | Skipper elton.Skipper 47 | } 48 | // StatsInfo stats's info 49 | StatsInfo struct { 50 | // ctx id 51 | CID string `json:"cid,omitempty"` 52 | // real ip 53 | IP string `json:"ip,omitempty"` 54 | // http request method 55 | Method string `json:"method,omitempty"` 56 | // route of elton 57 | Route string `json:"route,omitempty"` 58 | // http request uri 59 | URI string `json:"uri,omitempty"` 60 | // http status code 61 | Status int `json:"status,omitempty"` 62 | // latency of processing 63 | Latency time.Duration `json:"latency,omitempty"` 64 | Type int `json:"type,omitempty"` 65 | // bytes of request body 66 | RequestBodySize int `json:"requestBodySize"` 67 | // bytes of response body 68 | Size int `json:"size,omitempty"` 69 | // request connecting count of elton 70 | Connecting uint32 `json:"connecting,omitempty"` 71 | } 72 | ) 73 | 74 | // NewStats returns a new stats middleware, 75 | // it will throw a panic if the OnStats is nil. 76 | func NewStats(config StatsConfig) elton.Handler { 77 | if config.OnStats == nil { 78 | panic(ErrStatsNoFunction) 79 | } 80 | var connectingCount uint32 81 | skipper := config.Skipper 82 | if skipper == nil { 83 | skipper = elton.DefaultSkipper 84 | } 85 | return func(c *elton.Context) error { 86 | if skipper(c) { 87 | return c.Next() 88 | } 89 | connecting := atomic.AddUint32(&connectingCount, 1) 90 | defer atomic.AddUint32(&connectingCount, ^uint32(0)) 91 | 92 | startedAt := time.Now() 93 | 94 | req := c.Request 95 | uri, _ := url.QueryUnescape(req.RequestURI) 96 | if uri == "" { 97 | uri = req.RequestURI 98 | } 99 | info := &StatsInfo{ 100 | CID: c.ID, 101 | Method: req.Method, 102 | Route: c.Route, 103 | URI: uri, 104 | Connecting: connecting, 105 | IP: c.RealIP(), 106 | RequestBodySize: len(c.RequestBody), 107 | } 108 | 109 | err := c.Next() 110 | 111 | info.Latency = time.Since(startedAt) 112 | status := c.StatusCode 113 | if err != nil { 114 | he, ok := err.(*hes.Error) 115 | if ok { 116 | status = he.StatusCode 117 | } else { 118 | status = http.StatusInternalServerError 119 | } 120 | } 121 | if status == 0 { 122 | status = http.StatusOK 123 | } 124 | info.Status = status 125 | info.Type = status / 100 126 | size := 0 127 | if c.BodyBuffer != nil { 128 | size = c.BodyBuffer.Len() 129 | } 130 | info.Size = size 131 | 132 | config.OnStats(info, c) 133 | return err 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /middleware/basic_auth.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "errors" 27 | "net/http" 28 | 29 | "github.com/vicanso/elton" 30 | "github.com/vicanso/hes" 31 | ) 32 | 33 | const ( 34 | defaultBasicAuthRealm = "basic auth" 35 | // ErrBasicAuthCategory basic auth error category 36 | ErrBasicAuthCategory = "elton-basic-auth" 37 | ) 38 | 39 | type ( 40 | // BasicAuthValidate validate function 41 | BasicAuthValidate func(username string, password string, c *elton.Context) (bool, error) 42 | // BasicAuthConfig basic auth config 43 | BasicAuthConfig struct { 44 | Realm string 45 | Validate BasicAuthValidate 46 | Skipper elton.Skipper 47 | } 48 | ) 49 | 50 | var ( 51 | // ErrBasicAuthUnauthorized unauthorized err 52 | ErrBasicAuthUnauthorized = getBasicAuthError(errors.New("unAuthorized"), http.StatusUnauthorized) 53 | // ErrBasicAuthRequireValidateFunction require validate function 54 | ErrBasicAuthRequireValidateFunction = errors.New("require validate function") 55 | ) 56 | 57 | func getBasicAuthError(err error, statusCode int) *hes.Error { 58 | he := hes.Wrap(err) 59 | he.StatusCode = statusCode 60 | he.Category = ErrBasicAuthCategory 61 | return he 62 | } 63 | 64 | // NewDefaultBasicAuth returns a new basic auth middleware, 65 | // it will check the account and password, it will returns an error if check failed 66 | func NewDefaultBasicAuth(account, password string) elton.Handler { 67 | return NewBasicAuth(BasicAuthConfig{ 68 | Validate: func(acc, pwd string, _ *elton.Context) (bool, error) { 69 | if acc == account && pwd == password { 70 | return true, nil 71 | } 72 | return false, nil 73 | }, 74 | }) 75 | } 76 | 77 | // NewBasicAuth create a basic auth middleware, it will throw an error if the the validate function is nil. 78 | func NewBasicAuth(config BasicAuthConfig) elton.Handler { 79 | if config.Validate == nil { 80 | panic(ErrBasicAuthRequireValidateFunction) 81 | } 82 | basic := "basic" 83 | realm := defaultBasicAuthRealm 84 | if config.Realm != "" { 85 | realm = config.Realm 86 | } 87 | wwwAuthenticate := basic + ` realm="` + realm + `"` 88 | skipper := config.Skipper 89 | if skipper == nil { 90 | skipper = elton.DefaultSkipper 91 | } 92 | return func(c *elton.Context) error { 93 | if skipper(c) || c.Request.Method == http.MethodOptions { 94 | return c.Next() 95 | } 96 | 97 | user, password, hasAuth := c.Request.BasicAuth() 98 | // 如果请求头无认证头,则返回出错 99 | if !hasAuth { 100 | c.SetHeader(elton.HeaderWWWAuthenticate, wwwAuthenticate) 101 | return ErrBasicAuthUnauthorized 102 | } 103 | 104 | valid, e := config.Validate(user, password, c) 105 | 106 | // 如果返回出错,则输出出错信息 107 | if e != nil { 108 | err, ok := e.(*hes.Error) 109 | if !ok { 110 | err = getBasicAuthError(e, http.StatusBadRequest) 111 | } 112 | return err 113 | } 114 | 115 | // 如果校验失败,设置认证头,客户重新输入 116 | if !valid { 117 | c.SetHeader(elton.HeaderWWWAuthenticate, wwwAuthenticate) 118 | return ErrBasicAuthUnauthorized 119 | } 120 | return c.Next() 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /middleware/error_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "bytes" 27 | "errors" 28 | "net/http/httptest" 29 | "testing" 30 | "time" 31 | 32 | "github.com/stretchr/testify/assert" 33 | "github.com/vicanso/elton" 34 | ) 35 | 36 | func TestErrorHandler(t *testing.T) { 37 | assert := assert.New(t) 38 | defaultErrorHandler := NewDefaultError() 39 | skipErr := errors.New("skip error") 40 | // next直接返回skip error,用于判断是否执行了next 41 | next := func() error { 42 | return skipErr 43 | } 44 | 45 | tests := []struct { 46 | newContext func() *elton.Context 47 | fn elton.Handler 48 | result *bytes.Buffer 49 | cacheControl string 50 | contentType string 51 | err error 52 | }{ 53 | // skip 54 | { 55 | newContext: func() *elton.Context { 56 | req := httptest.NewRequest("GET", "/users/me", nil) 57 | resp := httptest.NewRecorder() 58 | c := elton.NewContext(resp, req) 59 | c.Committed = true 60 | c.Next = next 61 | return c 62 | }, 63 | fn: defaultErrorHandler, 64 | err: skipErr, 65 | }, 66 | // no error 67 | { 68 | newContext: func() *elton.Context { 69 | req := httptest.NewRequest("GET", "/users/me", nil) 70 | resp := httptest.NewRecorder() 71 | c := elton.NewContext(resp, req) 72 | c.Next = func() error { 73 | return nil 74 | } 75 | return c 76 | }, 77 | fn: defaultErrorHandler, 78 | }, 79 | // error(json) 80 | { 81 | newContext: func() *elton.Context { 82 | req := httptest.NewRequest("GET", "/users/me", nil) 83 | req.Header.Set("Accept", "application/json, text/plain, */*") 84 | resp := httptest.NewRecorder() 85 | c := elton.NewContext(resp, req) 86 | c.Next = func() error { 87 | return errors.New("abcd") 88 | } 89 | c.CacheMaxAge(5 * time.Minute) 90 | return c 91 | }, 92 | fn: defaultErrorHandler, 93 | result: bytes.NewBufferString(`{"statusCode":500,"category":"elton-error","message":"abcd","exception":true}`), 94 | cacheControl: "public, max-age=300", 95 | contentType: "application/json; charset=utf-8", 96 | }, 97 | // error(text) 98 | { 99 | newContext: func() *elton.Context { 100 | req := httptest.NewRequest("GET", "/users/me", nil) 101 | resp := httptest.NewRecorder() 102 | c := elton.NewContext(resp, req) 103 | c.Next = func() error { 104 | return errors.New("abcd") 105 | } 106 | c.CacheMaxAge(5 * time.Minute) 107 | return c 108 | }, 109 | fn: defaultErrorHandler, 110 | result: bytes.NewBufferString(`statusCode=500, category=elton-error, message=abcd`), 111 | cacheControl: "public, max-age=300", 112 | contentType: "text/plain; charset=utf-8", 113 | }, 114 | } 115 | 116 | for _, tt := range tests { 117 | c := tt.newContext() 118 | err := tt.fn(c) 119 | assert.Equal(tt.err, err) 120 | assert.Equal(tt.result, c.BodyBuffer) 121 | assert.Equal(tt.cacheControl, c.GetHeader(elton.HeaderCacheControl)) 122 | assert.Equal(tt.contentType, c.GetHeader(elton.HeaderContentType)) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /middleware/router_concurrent_limiter.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "errors" 27 | "fmt" 28 | "net/http" 29 | "sync/atomic" 30 | 31 | "github.com/vicanso/elton" 32 | "github.com/vicanso/hes" 33 | ) 34 | 35 | const ( 36 | // ErrRCLCategory router concurrent limiter error category 37 | ErrRCLCategory = "elton-router-concurrent-limiter" 38 | ) 39 | 40 | var ( 41 | ErrRCLRequireLimiter = errors.New("require limiter") 42 | ) 43 | 44 | type ( 45 | // Config router concurrent limiter config 46 | RCLConfig struct { 47 | Skipper elton.Skipper 48 | Limiter RCLLimiter 49 | } 50 | rclConcurrency struct { 51 | max uint32 52 | current uint32 53 | } 54 | // RCLLimiter limiter interface 55 | RCLLimiter interface { 56 | IncConcurrency(route string) (current uint32, max uint32) 57 | DecConcurrency(route string) 58 | GetConcurrency(route string) (current uint32) 59 | } 60 | // LocalLimiter local limiter 61 | RCLLocalLimiter struct { 62 | m map[string]*rclConcurrency 63 | } 64 | ) 65 | 66 | // NewLocalLimiter returns a new local limiter, it's useful for limit concurrency for process. 67 | func NewLocalLimiter(data map[string]uint32) *RCLLocalLimiter { 68 | m := make(map[string]*rclConcurrency, len(data)) 69 | for route, max := range data { 70 | m[route] = &rclConcurrency{ 71 | max: max, 72 | current: 0, 73 | } 74 | } 75 | return &RCLLocalLimiter{ 76 | m: m, 77 | } 78 | } 79 | 80 | // IncConcurrency inc 1 81 | func (l *RCLLocalLimiter) IncConcurrency(key string) (uint32, uint32) { 82 | concur, ok := l.m[key] 83 | if !ok { 84 | return 0, 0 85 | } 86 | v := atomic.AddUint32(&concur.current, 1) 87 | return v, concur.max 88 | } 89 | 90 | // DecConcurrency dec 1 91 | func (l *RCLLocalLimiter) DecConcurrency(key string) { 92 | concur, ok := l.m[key] 93 | if !ok { 94 | return 95 | } 96 | atomic.AddUint32(&concur.current, ^uint32(0)) 97 | } 98 | 99 | // GetConcurrency value 100 | func (l *RCLLocalLimiter) GetConcurrency(key string) uint32 { 101 | concur, ok := l.m[key] 102 | if !ok { 103 | return 0 104 | } 105 | return atomic.LoadUint32(&concur.current) 106 | } 107 | 108 | func createRCLError(current, max uint32) error { 109 | he := hes.New(fmt.Sprintf("too many request, current:%d, max:%d", current, max)) 110 | he.Category = ErrRCLCategory 111 | he.StatusCode = http.StatusTooManyRequests 112 | return he 113 | } 114 | 115 | // NewRCL returns a router concurrent limiter middleware. 116 | // It will throw panic if Limiter is nil. 117 | func NewRCL(config RCLConfig) elton.Handler { 118 | skipper := config.Skipper 119 | if skipper == nil { 120 | skipper = elton.DefaultSkipper 121 | } 122 | if config.Limiter == nil { 123 | panic(ErrRCLRequireLimiter) 124 | } 125 | limiter := config.Limiter 126 | return func(c *elton.Context) error { 127 | if skipper(c) { 128 | return c.Next() 129 | } 130 | key := c.Request.Method + " " + c.Route 131 | current, max := limiter.IncConcurrency(key) 132 | defer limiter.DecConcurrency(key) 133 | if max != 0 && current > max { 134 | return createRCLError(current, max) 135 | } 136 | return c.Next() 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /middleware/renderer_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2021 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "net/http/httptest" 27 | "os" 28 | "testing" 29 | 30 | "github.com/stretchr/testify/assert" 31 | "github.com/vicanso/elton" 32 | ) 33 | 34 | func TestGetTemplateType(t *testing.T) { 35 | assert := assert.New(t) 36 | data := RenderData{} 37 | 38 | // 默认值 39 | assert.Equal("html", data.getTemplateType()) 40 | 41 | // 从文件后缀获取 42 | data.File = "test.ejs" 43 | assert.Equal("ejs", data.getTemplateType()) 44 | 45 | // 指定类型 46 | data.TemplateType = "pug" 47 | assert.Equal("pug", data.getTemplateType()) 48 | } 49 | 50 | func TestRenderer(t *testing.T) { 51 | assert := assert.New(t) 52 | type Data struct { 53 | ID int 54 | Name string 55 | } 56 | 57 | renderer := NewRenderer(RendererConfig{}) 58 | text := "

{{.ID}}{{.Name}}

" 59 | 60 | t.Run("not set render data", func(t *testing.T) { 61 | c := elton.NewContext(nil, nil) 62 | c.Next = func() error { 63 | return nil 64 | } 65 | c.Body = "hello world!" 66 | err := renderer(c) 67 | assert.Nil(err) 68 | assert.Empty(c.BodyBuffer) 69 | }) 70 | 71 | t.Run("file and text is nil", func(t *testing.T) { 72 | c := elton.NewContext(nil, nil) 73 | c.Next = func() error { 74 | return nil 75 | } 76 | c.Body = RenderData{} 77 | err := renderer(c) 78 | assert.Equal(ErrFileAndTextNil, err) 79 | }) 80 | 81 | t.Run("tempate is not support", func(t *testing.T) { 82 | c := elton.NewContext(nil, nil) 83 | c.Next = func() error { 84 | return nil 85 | } 86 | c.Body = RenderData{ 87 | Text: text, 88 | TemplateType: "pug", 89 | } 90 | err := renderer(c) 91 | assert.Equal(ErrTemplateTypeInvalid, err) 92 | }) 93 | 94 | t.Run("render html from text", func(t *testing.T) { 95 | resp := httptest.NewRecorder() 96 | c := elton.NewContext(resp, httptest.NewRequest("GET", "/", nil)) 97 | c.Next = func() error { 98 | return nil 99 | } 100 | c.Body = RenderData{ 101 | Text: text, 102 | Data: &Data{ 103 | ID: 1, 104 | Name: "tree.xie", 105 | }, 106 | } 107 | err := renderer(c) 108 | assert.Nil(err) 109 | assert.Equal("

1tree.xie

", c.BodyBuffer.String()) 110 | assert.Equal("text/html; charset=utf-8", resp.Header().Get(elton.HeaderContentType)) 111 | }) 112 | 113 | t.Run("render html from file", func(t *testing.T) { 114 | // render file 115 | f, err := os.CreateTemp("", "") 116 | assert.Nil(err) 117 | filename := f.Name() 118 | defer func() { 119 | _ = os.Remove(filename) 120 | }() 121 | _, err = f.WriteString(text) 122 | assert.Nil(err) 123 | err = f.Close() 124 | assert.Nil(err) 125 | 126 | resp := httptest.NewRecorder() 127 | c := elton.NewContext(resp, httptest.NewRequest("GET", "/", nil)) 128 | c.Next = func() error { 129 | return nil 130 | } 131 | c.Body = &RenderData{ 132 | File: filename, 133 | Data: &Data{ 134 | ID: 2, 135 | Name: "tree", 136 | }, 137 | } 138 | err = renderer(c) 139 | assert.Nil(err) 140 | assert.Equal("

2tree

", c.BodyBuffer.String()) 141 | assert.Equal("text/html; charset=utf-8", resp.Header().Get(elton.HeaderContentType)) 142 | }) 143 | } 144 | -------------------------------------------------------------------------------- /middleware/router_concurrent_limiter_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "errors" 27 | "net/http/httptest" 28 | "testing" 29 | "time" 30 | 31 | "github.com/stretchr/testify/assert" 32 | "github.com/vicanso/elton" 33 | ) 34 | 35 | func TestRCLLimiter(t *testing.T) { 36 | 37 | assert := assert.New(t) 38 | limiter := NewLocalLimiter(map[string]uint32{ 39 | "/users/login": 10, 40 | "/books/:id": 100, 41 | }) 42 | 43 | cur, max := limiter.IncConcurrency("/not-match-route") 44 | assert.Equal(uint32(0), max) 45 | assert.Equal(uint32(0), cur) 46 | 47 | cur, max = limiter.IncConcurrency("/users/login") 48 | assert.Equal(uint32(10), max) 49 | assert.Equal(uint32(1), cur) 50 | 51 | limiter.DecConcurrency("/not-match-route") 52 | assert.Equal(uint32(0), limiter.GetConcurrency("/not-match-route")) 53 | 54 | limiter.DecConcurrency("/users/login") 55 | assert.Equal(uint32(0), limiter.GetConcurrency("/users/login")) 56 | } 57 | 58 | func TestRCLNoLimiterPanic(t *testing.T) { 59 | assert := assert.New(t) 60 | defer func() { 61 | r := recover() 62 | assert.NotNil(r) 63 | assert.Equal(r.(error), ErrRCLRequireLimiter) 64 | }() 65 | 66 | NewRCL(RCLConfig{}) 67 | } 68 | 69 | func TestRouterConcurrentLimiter(t *testing.T) { 70 | assert := assert.New(t) 71 | skipErr := errors.New("skip error") 72 | // next直接返回skip error,用于判断是否执行了next 73 | next := func() error { 74 | return skipErr 75 | } 76 | 77 | defaultLimiter := NewRCL(RCLConfig{ 78 | Limiter: NewLocalLimiter(map[string]uint32{ 79 | "POST /users/login": 1, 80 | "GET /books/:id": 100, 81 | }), 82 | }) 83 | 84 | tests := []struct { 85 | newContext func() *elton.Context 86 | err error 87 | }{ 88 | // skip 89 | { 90 | newContext: func() *elton.Context { 91 | req := httptest.NewRequest("GET", "/", nil) 92 | c := elton.NewContext(nil, req) 93 | c.Committed = true 94 | c.Next = next 95 | return c 96 | }, 97 | err: skipErr, 98 | }, 99 | // over limit 100 | { 101 | newContext: func() *elton.Context { 102 | go func() { 103 | req := httptest.NewRequest("POST", "/users/login", nil) 104 | c := elton.NewContext(nil, req) 105 | c.Route = "/users/login" 106 | c.Next = func() error { 107 | // 该请求在处理,但延时完成 108 | time.Sleep(10 * time.Millisecond) 109 | return nil 110 | } 111 | _ = defaultLimiter(c) 112 | }() 113 | // 延时,保证第一个请求已进入 114 | time.Sleep(2 * time.Millisecond) 115 | req := httptest.NewRequest("POST", "/users/login", nil) 116 | c := elton.NewContext(nil, req) 117 | c.Route = "/users/login" 118 | c.Next = func() error { 119 | time.Sleep(10 * time.Millisecond) 120 | return nil 121 | } 122 | return c 123 | }, 124 | err: createRCLError(2, 1), 125 | }, 126 | { 127 | newContext: func() *elton.Context { 128 | req := httptest.NewRequest("GET", "/books/1", nil) 129 | c := elton.NewContext(nil, req) 130 | c.Route = "/books/:id" 131 | c.Next = next 132 | return c 133 | }, 134 | err: skipErr, 135 | }, 136 | } 137 | 138 | for _, tt := range tests { 139 | c := tt.newContext() 140 | err := defaultLimiter(c) 141 | assert.Equal(tt.err, err) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /multipart_form.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2022 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package elton 24 | 25 | import ( 26 | "errors" 27 | "fmt" 28 | "io" 29 | "mime" 30 | "mime/multipart" 31 | "net/textproto" 32 | "os" 33 | "path/filepath" 34 | "strings" 35 | ) 36 | 37 | type multipartForm struct { 38 | w *multipart.Writer 39 | tmpfile string 40 | contentType string 41 | } 42 | 43 | // NewMultipartForm returns a new multipart form, 44 | // the form data will be saved as tmp file for less memory. 45 | func NewMultipartForm() *multipartForm { 46 | return &multipartForm{} 47 | } 48 | 49 | func (f *multipartForm) newFileBuffer() error { 50 | if f.w != nil { 51 | return nil 52 | } 53 | file, err := os.CreateTemp("", "multipart-form-") 54 | if err != nil { 55 | return err 56 | } 57 | f.tmpfile = file.Name() 58 | f.w = multipart.NewWriter(file) 59 | f.contentType = f.w.FormDataContentType() 60 | return nil 61 | } 62 | 63 | // AddField adds a field to form 64 | func (f *multipartForm) AddField(name, value string) error { 65 | err := f.newFileBuffer() 66 | if err != nil { 67 | return err 68 | } 69 | return f.w.WriteField(name, value) 70 | } 71 | 72 | var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") 73 | 74 | func escapeQuotes(s string) string { 75 | return quoteEscaper.Replace(s) 76 | } 77 | 78 | // AddFile add a file to form, if the reader is nil, the filename will be used to open as reader 79 | func (f *multipartForm) AddFile(name, filename string, reader ...io.Reader) error { 80 | err := f.newFileBuffer() 81 | if err != nil { 82 | return err 83 | } 84 | var r io.Reader 85 | if len(reader) != 0 { 86 | r = reader[0] 87 | } else { 88 | file, err := os.Open(filename) 89 | if err != nil { 90 | return err 91 | } 92 | // 调整filename 93 | filename = filepath.Base(filename) 94 | defer func() { 95 | _ = file.Close() 96 | }() 97 | r = file 98 | } 99 | h := make(textproto.MIMEHeader) 100 | h.Set("Content-Disposition", 101 | fmt.Sprintf(`form-data; name="%s"; filename="%s"`, 102 | escapeQuotes(name), escapeQuotes(filename))) 103 | ext := filepath.Ext(filename) 104 | contentType := mime.TypeByExtension(ext) 105 | if contentType == "" { 106 | contentType = "application/octet-stream" 107 | } 108 | h.Set("Content-Type", contentType) 109 | fw, err := f.w.CreatePart(h) 110 | if err != nil { 111 | return err 112 | } 113 | _, err = io.Copy(fw, r) 114 | if err != nil { 115 | return err 116 | } 117 | return nil 118 | } 119 | 120 | // Reader returns a render of form 121 | func (f *multipartForm) Reader() (io.Reader, error) { 122 | if f.w == nil { 123 | return nil, errors.New("multi part is nil") 124 | } 125 | err := f.w.Close() 126 | if err != nil { 127 | return nil, err 128 | } 129 | f.w = nil 130 | return os.Open(f.tmpfile) 131 | } 132 | 133 | // Destroy closes the writer and removes the tmpfile 134 | func (f *multipartForm) Destroy() error { 135 | if f.w != nil { 136 | err := f.w.Close() 137 | if err != nil { 138 | return err 139 | } 140 | } 141 | if f.tmpfile != "" { 142 | return os.Remove(f.tmpfile) 143 | } 144 | return nil 145 | } 146 | 147 | // ContentType returns the content type of form 148 | func (f *multipartForm) ContentType() string { 149 | return f.contentType 150 | } 151 | -------------------------------------------------------------------------------- /middleware/responder.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "bytes" 27 | "encoding/json" 28 | "net/http" 29 | 30 | "github.com/vicanso/elton" 31 | "github.com/vicanso/hes" 32 | ) 33 | 34 | type ( 35 | // Config responder config 36 | ResponderConfig struct { 37 | Skipper elton.Skipper 38 | // Marshal custom marshal function 39 | Marshal func(v interface{}) ([]byte, error) 40 | // ContentType response's content type 41 | ContentType string 42 | } 43 | ) 44 | 45 | const ( 46 | // ErrResponderCategory responder error category 47 | ErrResponderCategory = "elton-responder" 48 | ) 49 | 50 | var ( 51 | // ErrInvalidResponse invalid response(body an status is nil) 52 | ErrInvalidResponse = &hes.Error{ 53 | Exception: true, 54 | StatusCode: 500, 55 | Message: "invalid response", 56 | Category: ErrResponderCategory, 57 | } 58 | ) 59 | 60 | // NewDefaultResponder returns a new default responder middleware, it will use json.Marshal and application/json for response. 61 | func NewDefaultResponder() elton.Handler { 62 | return NewResponder(ResponderConfig{}) 63 | } 64 | 65 | // NewResponder returns a new responder middleware. 66 | // If will use json.Marshal as default marshal function. 67 | // If will use application/json as default content type. 68 | func NewResponder(config ResponderConfig) elton.Handler { 69 | skipper := config.Skipper 70 | if skipper == nil { 71 | skipper = elton.DefaultSkipper 72 | } 73 | marshal := config.Marshal 74 | // 如果未定义marshal 75 | if marshal == nil { 76 | marshal = json.Marshal 77 | } 78 | contentType := config.ContentType 79 | if contentType == "" { 80 | contentType = elton.MIMEApplicationJSON 81 | } 82 | 83 | return func(c *elton.Context) error { 84 | if skipper(c) { 85 | return c.Next() 86 | } 87 | err := c.Next() 88 | if err != nil { 89 | return err 90 | } 91 | // 如果已设置了BodyBuffer,则已生成好响应数据,跳过 92 | // 如果设置为commit,则表示其响应数据已处理 93 | if c.BodyBuffer != nil || c.Committed { 94 | return nil 95 | } 96 | 97 | if c.StatusCode == 0 && c.Body == nil { 98 | // 如果status code 与 body 都为空,则为非法响应 99 | return ErrInvalidResponse 100 | } 101 | // 如果body是reader,则跳过 102 | if c.IsReaderBody() { 103 | return nil 104 | } 105 | 106 | // 判断是否已设置响应头的Content-Type 107 | hadContentType := c.GetHeader(elton.HeaderContentType) != "" 108 | 109 | var body []byte 110 | if c.Body != nil { 111 | switch data := c.Body.(type) { 112 | case string: 113 | if !hadContentType { 114 | c.SetHeader(elton.HeaderContentType, elton.MIMETextPlain) 115 | } 116 | body = []byte(data) 117 | case []byte: 118 | if !hadContentType { 119 | c.SetHeader(elton.HeaderContentType, elton.MIMEBinary) 120 | } 121 | body = data 122 | default: 123 | // 使用marshal转换(默认为转换为json) 124 | buf, e := marshal(data) 125 | if e != nil { 126 | he := hes.NewWithErrorStatusCode(e, http.StatusInternalServerError) 127 | he.Category = ErrResponderCategory 128 | he.Exception = true 129 | return he 130 | } 131 | if !hadContentType { 132 | c.SetHeader(elton.HeaderContentType, contentType) 133 | } 134 | body = buf 135 | } 136 | } 137 | 138 | statusCode := c.StatusCode 139 | if statusCode == 0 { 140 | statusCode = http.StatusOK 141 | } 142 | if len(body) != 0 { 143 | c.BodyBuffer = bytes.NewBuffer(body) 144 | } 145 | c.StatusCode = statusCode 146 | return nil 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /trace_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2021 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package elton 24 | 25 | import ( 26 | "context" 27 | "testing" 28 | "time" 29 | 30 | "github.com/stretchr/testify/assert" 31 | ) 32 | 33 | func TestTrace(t *testing.T) { 34 | assert := assert.New(t) 35 | 36 | trace := NewTrace() 37 | 38 | trace.Add(&TraceInfo{ 39 | Name: "1", 40 | Middleware: true, 41 | Duration: 5 * time.Millisecond, 42 | }) 43 | 44 | trace.Add(&TraceInfo{ 45 | Name: "2", 46 | Middleware: true, 47 | Duration: 3 * time.Millisecond, 48 | }) 49 | 50 | trace.Add(&TraceInfo{ 51 | Name: "3", 52 | Middleware: true, 53 | Duration: 2 * time.Millisecond, 54 | }) 55 | trace.Add(&TraceInfo{ 56 | Name: "3", 57 | Duration: 1 * time.Millisecond, 58 | }) 59 | trace.Calculate() 60 | 61 | assert.Equal(2*time.Millisecond, trace.Infos[0].Duration) 62 | assert.Equal(time.Millisecond, trace.Infos[1].Duration) 63 | assert.Equal(2*time.Millisecond, trace.Infos[2].Duration) 64 | assert.Equal(time.Millisecond, trace.Infos[3].Duration) 65 | } 66 | 67 | func TestTraceStart(t *testing.T) { 68 | assert := assert.New(t) 69 | 70 | trace := NewTrace() 71 | done := trace.Start("test") 72 | time.Sleep(2 * time.Millisecond) 73 | done() 74 | assert.Equal(1, len(trace.Infos)) 75 | assert.Equal("test", trace.Infos[0].Name) 76 | assert.True(trace.Infos[0].Duration > 1) 77 | } 78 | 79 | func TestConvertToServerTiming(t *testing.T) { 80 | assert := assert.New(t) 81 | traceInfos := make(TraceInfos, 0) 82 | 83 | t.Run("get ms", func(t *testing.T) { 84 | assert.Equal("0", getMs(10)) 85 | assert.Equal("0.10", getMs(100000)) 86 | }) 87 | 88 | t.Run("empty trace infos", func(t *testing.T) { 89 | assert.Empty(traceInfos.ServerTiming(""), "no trace should return nil") 90 | }) 91 | t.Run("server timing", func(t *testing.T) { 92 | traceInfos = append(traceInfos, &TraceInfo{ 93 | Name: "a", 94 | Duration: time.Microsecond * 10, 95 | }) 96 | traceInfos = append(traceInfos, &TraceInfo{ 97 | Name: "b", 98 | Duration: time.Millisecond + time.Microsecond, 99 | }) 100 | assert.Equal(`elton-0;dur=0.01;desc="a",elton-1;dur=1;desc="b"`, string(traceInfos.ServerTiming("elton-"))) 101 | }) 102 | } 103 | 104 | func TestGetTrace(t *testing.T) { 105 | assert := assert.New(t) 106 | ctx := context.Background() 107 | 108 | // 第一次无则创建 109 | t1 := GetTrace(ctx) 110 | assert.NotNil(t1) 111 | t1.Add(&TraceInfo{}) 112 | 113 | // 第二次创建时与第一次非同一个值 114 | t2 := GetTrace(ctx) 115 | assert.NotNil(t2) 116 | assert.NotEqual(t1, t2) 117 | 118 | ctx = context.WithValue(ctx, ContextTraceKey, "a") 119 | t3 := GetTrace(ctx) 120 | assert.NotNil(t3) 121 | 122 | ctx = context.WithValue(ctx, ContextTraceKey, t2) 123 | t4 := GetTrace(ctx) 124 | assert.Equal(t2, t4) 125 | } 126 | 127 | func TestTraceFilter(t *testing.T) { 128 | assert := assert.New(t) 129 | 130 | traceInfos := TraceInfos{ 131 | { 132 | Name: "a", 133 | Duration: time.Millisecond, 134 | }, 135 | { 136 | Name: "b", 137 | Duration: 10 * time.Millisecond, 138 | }, 139 | } 140 | assert.Equal(TraceInfos{ 141 | { 142 | Name: "a", 143 | Duration: time.Millisecond, 144 | }, 145 | }, traceInfos.Filter(func(ti *TraceInfo) bool { 146 | return ti.Name == "a" 147 | })) 148 | 149 | assert.Equal(TraceInfos{ 150 | { 151 | Name: "b", 152 | Duration: 10 * time.Millisecond, 153 | }, 154 | }, traceInfos.FilterDurationGT(5*time.Millisecond)) 155 | } 156 | -------------------------------------------------------------------------------- /df.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package elton 24 | 25 | import ( 26 | "net/http" 27 | 28 | "github.com/vicanso/hes" 29 | ) 30 | 31 | type ContextKey string 32 | 33 | const ContextTraceKey ContextKey = "contextTrace" 34 | 35 | var ( 36 | methods = []string{ 37 | http.MethodGet, 38 | http.MethodPost, 39 | http.MethodPut, 40 | http.MethodPatch, 41 | http.MethodDelete, 42 | http.MethodHead, 43 | http.MethodOptions, 44 | http.MethodTrace, 45 | } 46 | 47 | // ErrInvalidRedirect invalid redirect 48 | ErrInvalidRedirect = &hes.Error{ 49 | StatusCode: 400, 50 | Message: "invalid redirect", 51 | Category: ErrCategory, 52 | } 53 | 54 | // ErrNilResponse nil response 55 | ErrNilResponse = &hes.Error{ 56 | StatusCode: 500, 57 | Message: "nil response", 58 | Category: ErrCategory, 59 | } 60 | // ErrNotSupportPush not support http push 61 | ErrNotSupportPush = &hes.Error{ 62 | StatusCode: 500, 63 | Message: "not support http push", 64 | Category: ErrCategory, 65 | } 66 | // ErrFileNotFound file not found 67 | ErrFileNotFound = &hes.Error{ 68 | StatusCode: 404, 69 | Message: "file not found", 70 | Category: ErrCategory, 71 | } 72 | ) 73 | 74 | const ( 75 | // ErrCategory elton category 76 | ErrCategory = "elton" 77 | // HeaderXForwardedFor x-forwarded-for 78 | HeaderXForwardedFor = "X-Forwarded-For" 79 | // HeaderXRealIP x-real-ip 80 | HeaderXRealIP = "X-Real-Ip" 81 | // HeaderSetCookie Set-Cookie 82 | HeaderSetCookie = "Set-Cookie" 83 | // HeaderLocation Location 84 | HeaderLocation = "Location" 85 | // HeaderContentType Content-Type 86 | HeaderContentType = "Content-Type" 87 | // HeaderAuthorization Authorization 88 | HeaderAuthorization = "Authorization" 89 | // HeaderWWWAuthenticate WWW-Authenticate 90 | HeaderWWWAuthenticate = "WWW-Authenticate" 91 | // HeaderCacheControl Cache-Control 92 | HeaderCacheControl = "Cache-Control" 93 | // HeaderETag ETag 94 | HeaderETag = "ETag" 95 | // HeaderLastModified last modified 96 | HeaderLastModified = "Last-Modified" 97 | // HeaderContentEncoding content encoding 98 | HeaderContentEncoding = "Content-Encoding" 99 | // HeaderContentLength content length 100 | HeaderContentLength = "Content-Length" 101 | // HeaderIfModifiedSince if modified since 102 | HeaderIfModifiedSince = "If-Modified-Since" 103 | // HeaderIfNoneMatch if none match 104 | HeaderIfNoneMatch = "If-None-Match" 105 | // HeaderAcceptEncoding accept encoding 106 | HeaderAcceptEncoding = "Accept-Encoding" 107 | // HeaderServerTiming server timing 108 | HeaderServerTiming = "Server-Timing" 109 | // HeaderTransferEncoding transfer encoding 110 | HeaderTransferEncoding = "Transfer-Encoding" 111 | 112 | // MinRedirectCode min redirect code 113 | MinRedirectCode = 300 114 | // MaxRedirectCode max redirect code 115 | MaxRedirectCode = 308 116 | 117 | // MIMETextPlain text plain 118 | MIMETextPlain = "text/plain; charset=utf-8" 119 | // MIMEApplicationJSON application json 120 | MIMEApplicationJSON = "application/json; charset=utf-8" 121 | // MIMEBinary binary data 122 | MIMEBinary = "application/octet-stream" 123 | 124 | // Gzip gzip compress 125 | Gzip = "gzip" 126 | // Br brotli compress 127 | Br = "br" 128 | // Zstd zstd compress 129 | Zstd = "zstd" 130 | ) 131 | 132 | var ( 133 | // ServerTimingDur server timing dur 134 | ServerTimingDur = []byte(";dur=") 135 | // ServerTimingDesc server timing desc 136 | ServerTimingDesc = []byte(`;desc="`) 137 | // ServerTimingEnd server timing end 138 | ServerTimingEnd = []byte(`"`) 139 | ) 140 | -------------------------------------------------------------------------------- /middleware/tracker.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "encoding/json" 27 | "errors" 28 | "fmt" 29 | "regexp" 30 | "time" 31 | 32 | "github.com/vicanso/elton" 33 | ) 34 | 35 | const ( 36 | // HandleSuccess handle success 37 | HandleSuccess = iota 38 | // HandleFail handle fail 39 | HandleFail 40 | ) 41 | 42 | var ( 43 | defaultTrackerMaskFields = regexp.MustCompile(`(?i)password`) 44 | ErrTrackerNoFunction = errors.New("require on track function") 45 | ) 46 | 47 | type ( 48 | // TrackerInfo tracker info 49 | TrackerInfo struct { 50 | CID string `json:"cid,omitempty"` 51 | Query map[string]string `json:"query,omitempty"` 52 | Params map[string]string `json:"params,omitempty"` 53 | Form map[string]interface{} `json:"form,omitempty"` 54 | Latency time.Duration `json:"latency,omitempty"` 55 | Result int `json:"result,omitempty"` 56 | Err error `json:"err,omitempty"` 57 | } 58 | // OnTrack on track function 59 | OnTrack func(*TrackerInfo, *elton.Context) 60 | // TrackerConfig tracker config 61 | TrackerConfig struct { 62 | // On Track function 63 | OnTrack OnTrack 64 | // mask regexp 65 | Mask *regexp.Regexp 66 | // max length for filed 67 | MaxLength int 68 | Skipper elton.Skipper 69 | } 70 | ) 71 | 72 | func convertMap(data map[string]string, mask *regexp.Regexp, maxLength int) map[string]string { 73 | size := len(data) 74 | if size == 0 { 75 | return nil 76 | } 77 | m := make(map[string]string, size) 78 | for k, v := range data { 79 | if mask.MatchString(k) { 80 | v = "***" 81 | m[k] = "***" 82 | } else if maxLength > 0 && len(v) > maxLength { 83 | v = fmt.Sprintf("%s ... (%d more)", v[:maxLength], len(v)-maxLength) 84 | } 85 | m[k] = v 86 | } 87 | return m 88 | } 89 | 90 | // NewTracker returns a new tracker middleware, 91 | // it will throw a panic if OnTrack function is nil. 92 | func NewTracker(config TrackerConfig) elton.Handler { 93 | mask := config.Mask 94 | if mask == nil { 95 | mask = defaultTrackerMaskFields 96 | } 97 | if config.OnTrack == nil { 98 | panic(ErrTrackerNoFunction) 99 | } 100 | skipper := config.Skipper 101 | if skipper == nil { 102 | skipper = elton.DefaultSkipper 103 | } 104 | maxLength := config.MaxLength 105 | if maxLength <= 0 { 106 | maxLength = 20 107 | } 108 | return func(c *elton.Context) error { 109 | if skipper(c) { 110 | return c.Next() 111 | } 112 | startedAt := time.Now() 113 | result := HandleSuccess 114 | query := convertMap(c.Query(), mask, maxLength) 115 | params := convertMap(c.Params.ToMap(), mask, maxLength) 116 | var form map[string]interface{} 117 | if len(c.RequestBody) != 0 { 118 | form = make(map[string]interface{}) 119 | _ = json.Unmarshal(c.RequestBody, &form) 120 | for k := range form { 121 | if mask.MatchString(k) { 122 | form[k] = "***" 123 | } else { 124 | str, ok := form[k].(string) 125 | if ok && len(str) > maxLength { 126 | str = fmt.Sprintf("%s ... (%d more)", str[:maxLength], len(str)-maxLength) 127 | form[k] = str 128 | } 129 | } 130 | } 131 | } 132 | err := c.Next() 133 | if err != nil { 134 | result = HandleFail 135 | } 136 | config.OnTrack(&TrackerInfo{ 137 | CID: c.ID, 138 | Query: query, 139 | Params: params, 140 | Form: form, 141 | Result: result, 142 | Err: err, 143 | Latency: time.Since(startedAt), 144 | }, c) 145 | return err 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /middleware/basic_auth_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "errors" 27 | "net/http" 28 | "net/http/httptest" 29 | "testing" 30 | 31 | "github.com/stretchr/testify/assert" 32 | "github.com/vicanso/elton" 33 | ) 34 | 35 | func TestNoVildatePanic(t *testing.T) { 36 | assert := assert.New(t) 37 | defer func() { 38 | r := recover() 39 | assert.NotNil(r) 40 | assert.Equal(ErrBasicAuthRequireValidateFunction, r.(error)) 41 | }() 42 | 43 | NewBasicAuth(BasicAuthConfig{}) 44 | } 45 | 46 | func TestBasicAuthSkip(t *testing.T) { 47 | assert := assert.New(t) 48 | skipErr := errors.New("skip error") 49 | // next直接返回skip error,用于判断是否执行了next 50 | next := func() error { 51 | return skipErr 52 | } 53 | defaultAuth := NewBasicAuth(BasicAuthConfig{ 54 | Validate: func(acccount, pwd string, c *elton.Context) (bool, error) { 55 | return true, nil 56 | }, 57 | }) 58 | tests := []struct { 59 | newContext func() *elton.Context 60 | err error 61 | fn elton.Handler 62 | headerAuth string 63 | }{ 64 | // committed: true 65 | { 66 | newContext: func() *elton.Context { 67 | c := elton.NewContext(httptest.NewRecorder(), nil) 68 | c.Committed = true 69 | c.Next = next 70 | return c 71 | }, 72 | err: skipErr, 73 | fn: defaultAuth, 74 | }, 75 | // options method 76 | { 77 | newContext: func() *elton.Context { 78 | req := httptest.NewRequest("OPTIONS", "/", nil) 79 | c := elton.NewContext(httptest.NewRecorder(), req) 80 | c.Next = next 81 | return c 82 | }, 83 | err: skipErr, 84 | fn: defaultAuth, 85 | }, 86 | // not set auth header 87 | { 88 | newContext: func() *elton.Context { 89 | c := elton.NewContext(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil)) 90 | return c 91 | }, 92 | err: ErrBasicAuthUnauthorized, 93 | fn: defaultAuth, 94 | headerAuth: `basic realm="basic auth"`, 95 | }, 96 | // validate return error 97 | { 98 | newContext: func() *elton.Context { 99 | c := elton.NewContext(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil)) 100 | c.Request.SetBasicAuth("account", "password") 101 | return c 102 | }, 103 | err: getBasicAuthError(errors.New("custom error"), http.StatusBadRequest), 104 | fn: NewBasicAuth(BasicAuthConfig{ 105 | Validate: func(account, password string, _ *elton.Context) (bool, error) { 106 | return false, errors.New("custom error") 107 | }, 108 | }), 109 | }, 110 | // validate fail 111 | { 112 | newContext: func() *elton.Context { 113 | c := elton.NewContext(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil)) 114 | c.Request.SetBasicAuth("account", "pass") 115 | return c 116 | }, 117 | err: ErrBasicAuthUnauthorized, 118 | headerAuth: `basic realm="custom realm"`, 119 | fn: NewBasicAuth(BasicAuthConfig{ 120 | Validate: func(account, password string, _ *elton.Context) (bool, error) { 121 | return false, nil 122 | }, 123 | Realm: "custom realm", 124 | }), 125 | }, 126 | // success 127 | { 128 | newContext: func() *elton.Context { 129 | c := elton.NewContext(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil)) 130 | c.Request.SetBasicAuth("account", "password") 131 | c.Next = next 132 | return c 133 | }, 134 | fn: NewDefaultBasicAuth("account", "password"), 135 | err: skipErr, 136 | }, 137 | } 138 | 139 | for _, tt := range tests { 140 | c := tt.newContext() 141 | err := tt.fn(c) 142 | assert.Equal(tt.err, err) 143 | assert.Equal(tt.headerAuth, c.GetHeader(elton.HeaderWWWAuthenticate)) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /middleware/etag_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "bytes" 27 | "crypto/md5" 28 | "encoding/base64" 29 | "errors" 30 | "fmt" 31 | "math/rand" 32 | "net/http/httptest" 33 | "testing" 34 | 35 | "github.com/stretchr/testify/assert" 36 | "github.com/vicanso/elton" 37 | ) 38 | 39 | var testData []byte 40 | 41 | func init() { 42 | var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 43 | 44 | fn := func(n int) string { 45 | b := make([]rune, n) 46 | for i := range b { 47 | b[i] = letterRunes[rand.Intn(len(letterRunes))] 48 | } 49 | return string(b) 50 | } 51 | testData = []byte(fn(4096)) 52 | } 53 | 54 | func TestGen(t *testing.T) { 55 | assert := assert.New(t) 56 | value, err := genETag([]byte("")) 57 | assert.Nil(err) 58 | assert.Equal(value, `"0-2jmj7l5rSw0yVb_vlWAYkK_YBwk="`) 59 | } 60 | 61 | func TestETag(t *testing.T) { 62 | assert := assert.New(t) 63 | skipErr := errors.New("skip error") 64 | // next直接返回skip error,用于判断是否执行了next 65 | next := func() error { 66 | return skipErr 67 | } 68 | defaultETag := NewDefaultETag() 69 | 70 | tests := []struct { 71 | newContext func() *elton.Context 72 | eTag string 73 | err error 74 | }{ 75 | // skip 76 | { 77 | newContext: func() *elton.Context { 78 | c := elton.NewContext(httptest.NewRecorder(), nil) 79 | c.Committed = true 80 | c.Next = next 81 | return c 82 | }, 83 | err: skipErr, 84 | }, 85 | // response error, not generate etag 86 | { 87 | newContext: func() *elton.Context { 88 | c := elton.NewContext(httptest.NewRecorder(), nil) 89 | c.Next = next 90 | return c 91 | }, 92 | err: skipErr, 93 | }, 94 | // empty response 95 | { 96 | newContext: func() *elton.Context { 97 | c := elton.NewContext(httptest.NewRecorder(), nil) 98 | c.Next = func() error { 99 | return nil 100 | } 101 | return c 102 | }, 103 | }, 104 | // status <200 or >=300, not generate etag 105 | { 106 | newContext: func() *elton.Context { 107 | resp := httptest.NewRecorder() 108 | c := elton.NewContext(resp, nil) 109 | c.Next = func() error { 110 | c.Body = map[string]string{ 111 | "name": "tree.xie", 112 | } 113 | c.StatusCode = 400 114 | c.BodyBuffer = bytes.NewBufferString(`{"name":"tree.xie"}`) 115 | return nil 116 | } 117 | return c 118 | }, 119 | }, 120 | // generate etag 121 | { 122 | newContext: func() *elton.Context { 123 | resp := httptest.NewRecorder() 124 | c := elton.NewContext(resp, nil) 125 | c.Next = func() error { 126 | c.Body = map[string]string{ 127 | "name": "tree.xie", 128 | } 129 | c.BodyBuffer = bytes.NewBufferString(`{"name":"tree.xie"}`) 130 | return nil 131 | } 132 | return c 133 | }, 134 | eTag: `"13-yo9YroUOjW1obRvVoXfrCiL2JGE="`, 135 | }, 136 | } 137 | 138 | for _, tt := range tests { 139 | c := tt.newContext() 140 | err := defaultETag(c) 141 | assert.Equal(tt.err, err) 142 | assert.Equal(tt.eTag, c.GetHeader(elton.HeaderETag)) 143 | } 144 | } 145 | 146 | func BenchmarkGenETag(b *testing.B) { 147 | b.ReportAllocs() 148 | for i := 0; i < b.N; i++ { 149 | _, err := genETag(testData) 150 | if err != nil { 151 | panic(err) 152 | } 153 | } 154 | } 155 | 156 | func BenchmarkMd5(b *testing.B) { 157 | b.ReportAllocs() 158 | fn := func(buf []byte) string { 159 | size := len(buf) 160 | if size == 0 { 161 | return `"0-2jmj7l5rSw0yVb_vlWAYkK_YBwk="` 162 | } 163 | h := md5.New() 164 | _, _ = h.Write(buf) 165 | hash := base64.URLEncoding.EncodeToString(h.Sum(nil)) 166 | return fmt.Sprintf(`"%x-%s"`, size, hash) 167 | } 168 | for i := 0; i < b.N; i++ { 169 | fn(testData) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /middleware/fresh_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "bytes" 27 | "errors" 28 | "net/http" 29 | "net/http/httptest" 30 | "testing" 31 | 32 | "github.com/stretchr/testify/assert" 33 | "github.com/vicanso/elton" 34 | ) 35 | 36 | func TestFresh(t *testing.T) { 37 | assert := assert.New(t) 38 | skipErr := errors.New("skip error") 39 | // next直接返回skip error,用于判断是否执行了next 40 | next := func() error { 41 | return skipErr 42 | } 43 | defaultFresh := NewDefaultFresh() 44 | tests := []struct { 45 | newContext func() *elton.Context 46 | err error 47 | statusCode int 48 | body interface{} 49 | result *bytes.Buffer 50 | }{ 51 | // skip 52 | { 53 | newContext: func() *elton.Context { 54 | c := elton.NewContext(nil, nil) 55 | c.Committed = true 56 | c.Next = next 57 | return c 58 | 59 | }, 60 | err: skipErr, 61 | }, 62 | // error 63 | { 64 | newContext: func() *elton.Context { 65 | c := elton.NewContext(nil, nil) 66 | c.Next = next 67 | return c 68 | 69 | }, 70 | err: skipErr, 71 | }, 72 | // pass method 73 | { 74 | newContext: func() *elton.Context { 75 | modifiedAt := "Tue, 25 Dec 2018 00:02:22 GMT" 76 | 77 | req := httptest.NewRequest("POST", "/users/me", nil) 78 | req.Header.Set(elton.HeaderIfModifiedSince, modifiedAt) 79 | resp := httptest.NewRecorder() 80 | resp.Header().Set(elton.HeaderLastModified, modifiedAt) 81 | 82 | c := elton.NewContext(resp, req) 83 | c.Next = func() error { 84 | c.StatusCode = http.StatusOK 85 | c.Body = map[string]string{ 86 | "name": "tree.xie", 87 | } 88 | c.BodyBuffer = bytes.NewBufferString(`{"name":"tree.xie"}`) 89 | return nil 90 | } 91 | return c 92 | }, 93 | statusCode: 200, 94 | body: map[string]string{ 95 | "name": "tree.xie", 96 | }, 97 | result: bytes.NewBufferString(`{"name":"tree.xie"}`), 98 | }, 99 | // status code >= 300 100 | { 101 | newContext: func() *elton.Context { 102 | modifiedAt := "Tue, 25 Dec 2018 00:02:22 GMT" 103 | 104 | req := httptest.NewRequest("GET", "/users/me", nil) 105 | req.Header.Set(elton.HeaderIfModifiedSince, modifiedAt) 106 | resp := httptest.NewRecorder() 107 | resp.Header().Set(elton.HeaderLastModified, modifiedAt) 108 | 109 | c := elton.NewContext(resp, req) 110 | c.Next = func() error { 111 | c.StatusCode = http.StatusBadRequest 112 | c.Body = map[string]string{ 113 | "name": "tree.xie", 114 | } 115 | c.BodyBuffer = bytes.NewBufferString(`{"name":"tree.xie"}`) 116 | return nil 117 | } 118 | return c 119 | }, 120 | statusCode: http.StatusBadRequest, 121 | body: map[string]string{ 122 | "name": "tree.xie", 123 | }, 124 | result: bytes.NewBufferString(`{"name":"tree.xie"}`), 125 | }, 126 | // 304 127 | { 128 | newContext: func() *elton.Context { 129 | modifiedAt := "Tue, 25 Dec 2018 00:02:22 GMT" 130 | 131 | req := httptest.NewRequest("GET", "/users/me", nil) 132 | req.Header.Set(elton.HeaderIfModifiedSince, modifiedAt) 133 | resp := httptest.NewRecorder() 134 | resp.Header().Set(elton.HeaderLastModified, modifiedAt) 135 | 136 | c := elton.NewContext(resp, req) 137 | c.Next = func() error { 138 | c.Body = map[string]string{ 139 | "name": "tree.xie", 140 | } 141 | c.BodyBuffer = bytes.NewBufferString(`{"name":"tree.xie"}`) 142 | return nil 143 | } 144 | return c 145 | }, 146 | statusCode: 304, 147 | }, 148 | } 149 | 150 | for _, tt := range tests { 151 | c := tt.newContext() 152 | err := defaultFresh(c) 153 | assert.Equal(tt.err, err) 154 | assert.Equal(tt.result, c.BodyBuffer) 155 | assert.Equal(tt.body, c.Body) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /trace.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2021 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package elton 24 | 25 | import ( 26 | "context" 27 | "strconv" 28 | "strings" 29 | "time" 30 | ) 31 | 32 | type ( 33 | // TraceInfo trace's info 34 | TraceInfo struct { 35 | Middleware bool `json:"-"` 36 | Name string `json:"name,omitempty"` 37 | Duration time.Duration `json:"duration,omitempty"` 38 | } 39 | // TraceInfos trace infos 40 | TraceInfos []*TraceInfo 41 | 42 | Trace struct { 43 | calculateDone bool 44 | Infos TraceInfos 45 | } 46 | ) 47 | 48 | // NewTrace returns a new trace 49 | func NewTrace() *Trace { 50 | return &Trace{ 51 | Infos: make(TraceInfos, 0), 52 | } 53 | } 54 | 55 | // Start starts a sub trace and return done function 56 | // for sub trace. 57 | func (t *Trace) Start(name string) func() { 58 | startedAt := time.Now() 59 | info := &TraceInfo{ 60 | Name: name, 61 | } 62 | t.Add(info) 63 | return func() { 64 | info.Duration = time.Since(startedAt) 65 | } 66 | } 67 | 68 | // Add adds trace info to trace 69 | func (t *Trace) Add(info *TraceInfo) *Trace { 70 | t.Infos = append(t.Infos, info) 71 | return t 72 | } 73 | 74 | // Calculate calculates the duration of middleware 75 | func (t *Trace) Calculate() { 76 | if t.calculateDone { 77 | return 78 | } 79 | // middleware需要减去后面middleware的处理时长 80 | var cur *TraceInfo 81 | for _, item := range t.Infos { 82 | if !item.Middleware { 83 | continue 84 | } 85 | if cur != nil { 86 | cur.Duration -= item.Duration 87 | } 88 | cur = item 89 | } 90 | t.calculateDone = true 91 | } 92 | 93 | func getMs(ns int) string { 94 | microSecond := int(time.Microsecond) 95 | milliSecond := int(time.Millisecond) 96 | if ns < microSecond { 97 | return "0" 98 | } 99 | 100 | // 计算ms的位 101 | ms := ns / milliSecond 102 | prefix := strconv.Itoa(ms) 103 | 104 | // 计算micro seconds 105 | offset := (ns % milliSecond) / microSecond 106 | // 如果小于10,不展示小数点(取小数点两位) 107 | unit := 10 108 | if offset < unit { 109 | return prefix 110 | } 111 | // 如果小于100,补一位0 112 | if offset < 100 { 113 | return prefix + ".0" + strconv.Itoa(offset/unit) 114 | } 115 | return prefix + "." + strconv.Itoa(offset/unit) 116 | } 117 | 118 | // ServerTiming return server timing with prefix 119 | func (traceInfos TraceInfos) ServerTiming(prefix string) string { 120 | size := len(traceInfos) 121 | if size == 0 { 122 | return "" 123 | } 124 | 125 | // 转换为 http server timing 126 | s := new(strings.Builder) 127 | // 每一个server timing长度预估为30 128 | s.Grow(30 * size) 129 | for i, traceInfo := range traceInfos { 130 | v := traceInfo.Duration.Nanoseconds() 131 | s.WriteString(prefix) 132 | s.WriteString(strconv.Itoa(i)) 133 | s.Write(ServerTimingDur) 134 | s.WriteString(getMs(int(v))) 135 | s.Write(ServerTimingDesc) 136 | s.WriteString(traceInfo.Name) 137 | s.Write(ServerTimingEnd) 138 | if i != size-1 { 139 | s.WriteRune(',') 140 | } 141 | 142 | } 143 | return s.String() 144 | } 145 | 146 | // Filter filters the trace info, the new trace infos will be returned. 147 | func (traceInfos TraceInfos) Filter(fn func(*TraceInfo) bool) TraceInfos { 148 | infos := make(TraceInfos, 0, len(traceInfos)) 149 | for _, info := range traceInfos { 150 | if fn(info) { 151 | infos = append(infos, info) 152 | } 153 | } 154 | return infos 155 | } 156 | 157 | // FilterDurationGT flters the duration of trace is gt than d. 158 | func (traceInfos TraceInfos) FilterDurationGT(d time.Duration) TraceInfos { 159 | return traceInfos.Filter(func(ti *TraceInfo) bool { 160 | return ti.Duration > d 161 | }) 162 | } 163 | 164 | // GetTrace get trace from context, if context without trace, new trace will be created. 165 | func GetTrace(ctx context.Context) *Trace { 166 | value := ctx.Value(ContextTraceKey) 167 | if value == nil { 168 | return NewTrace() 169 | } 170 | trace, ok := value.(*Trace) 171 | if !ok { 172 | return NewTrace() 173 | } 174 | return trace 175 | } 176 | -------------------------------------------------------------------------------- /middleware/compressor_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "bytes" 27 | "compress/gzip" 28 | "io" 29 | "net/http/httptest" 30 | "testing" 31 | 32 | "github.com/andybalholm/brotli" 33 | "github.com/stretchr/testify/assert" 34 | "github.com/vicanso/elton" 35 | ) 36 | 37 | func TestCompressor(t *testing.T) { 38 | tests := []struct { 39 | compressor Compressor 40 | encoding string 41 | uncompress func([]byte) ([]byte, error) 42 | }{ 43 | { 44 | compressor: new(GzipCompressor), 45 | encoding: elton.Gzip, 46 | uncompress: func(b []byte) ([]byte, error) { 47 | buffer, err := GzipDecompress(b) 48 | if err != nil { 49 | return nil, err 50 | } 51 | return buffer.Bytes(), nil 52 | }, 53 | }, 54 | { 55 | compressor: new(BrCompressor), 56 | encoding: elton.Br, 57 | uncompress: func(b []byte) ([]byte, error) { 58 | buffer, err := BrotliDecompress(b) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return buffer.Bytes(), nil 63 | }, 64 | }, 65 | } 66 | assert := assert.New(t) 67 | for _, tt := range tests { 68 | originalData := randomString(1024) 69 | req := httptest.NewRequest("GET", "/users/me", nil) 70 | req.Header.Set("Accept-Encoding", "gzip, deflate, br") 71 | c := elton.NewContext(nil, req) 72 | 73 | acceptable, encoding := tt.compressor.Accept(c, 0) 74 | assert.False(acceptable) 75 | assert.Empty(encoding) 76 | 77 | acceptable, encoding = tt.compressor.Accept(c, len(originalData)) 78 | assert.True(acceptable) 79 | assert.Equal(tt.encoding, encoding) 80 | 81 | _, err := tt.compressor.Compress([]byte(originalData), 1) 82 | assert.Nil(err) 83 | buffer, err := tt.compressor.Compress([]byte(originalData), IgnoreCompression) 84 | assert.Nil(err) 85 | assert.Nil(err) 86 | 87 | uncompressBuf, _ := tt.uncompress(buffer.Bytes()) 88 | assert.Equal([]byte(originalData), uncompressBuf) 89 | } 90 | } 91 | 92 | func TestCompressorPipe(t *testing.T) { 93 | assert := assert.New(t) 94 | tests := []struct { 95 | compressor Compressor 96 | encoding string 97 | uncompress func(io.Reader) ([]byte, error) 98 | }{ 99 | { 100 | compressor: new(GzipCompressor), 101 | uncompress: func(r io.Reader) ([]byte, error) { 102 | gzipReader, err := gzip.NewReader(r) 103 | if err != nil { 104 | return nil, err 105 | } 106 | defer func() { 107 | _ = gzipReader.Close() 108 | }() 109 | return io.ReadAll(gzipReader) 110 | }, 111 | }, 112 | { 113 | compressor: new(BrCompressor), 114 | uncompress: func(r io.Reader) ([]byte, error) { 115 | return io.ReadAll(brotli.NewReader(r)) 116 | }, 117 | }, 118 | } 119 | for _, tt := range tests { 120 | resp := httptest.NewRecorder() 121 | originalData := randomString(1024) 122 | c := elton.NewContext(resp, nil) 123 | 124 | c.Body = bytes.NewReader([]byte(originalData)) 125 | 126 | err := tt.compressor.Pipe(c) 127 | assert.Nil(err) 128 | buf, _ := tt.uncompress(resp.Body) 129 | assert.Equal([]byte(originalData), buf) 130 | } 131 | 132 | } 133 | 134 | func TestCompressorGetLevel(t *testing.T) { 135 | assert := assert.New(t) 136 | g := GzipCompressor{ 137 | Level: 1000, 138 | } 139 | assert.Equal(gzip.BestCompression, g.getLevel()) 140 | g.Level = 0 141 | assert.Equal(gzip.DefaultCompression, g.getLevel()) 142 | g.Level = 1 143 | assert.Equal(1, g.getLevel()) 144 | 145 | br := BrCompressor{ 146 | Level: 1000, 147 | } 148 | assert.Equal(maxBrQuality, br.getLevel()) 149 | br.Level = 0 150 | assert.Equal(defaultBrQuality, br.getLevel()) 151 | br.Level = 1 152 | assert.Equal(1, br.getLevel()) 153 | } 154 | 155 | func TestCompressorGetMinLength(t *testing.T) { 156 | assert := assert.New(t) 157 | 158 | g := GzipCompressor{} 159 | assert.Equal(DefaultCompressMinLength, g.getMinLength()) 160 | g.MinLength = 1 161 | assert.Equal(1, g.getMinLength()) 162 | 163 | br := BrCompressor{} 164 | assert.Equal(DefaultCompressMinLength, br.getMinLength()) 165 | br.MinLength = 1 166 | assert.Equal(1, br.getMinLength()) 167 | } 168 | -------------------------------------------------------------------------------- /docs/performances.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 性能测试 3 | --- 4 | 5 | `elton`的性能如何是大家都会关心的重点,下面是使用测试服务器(4U8线程,8G内存)的几个测试场景,go版本为1.14: 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "bytes" 12 | 13 | "github.com/vicanso/elton" 14 | ) 15 | 16 | func main() { 17 | d := elton.New() 18 | 19 | d.GET("/", func(c *elton.Context) (err error) { 20 | c.BodyBuffer = bytes.NewBufferString("Hello, World!") 21 | return 22 | }) 23 | err := d.ListenAndServe(":3000") 24 | if err != nil { 25 | panic(err) 26 | } 27 | } 28 | ``` 29 | 30 | ```bash 31 | wrk -c 1000 -t 10 --latency 'http://127.0.0.1:3000/' 32 | Running 10s test @ http://127.0.0.1:3000/ 33 | 10 threads and 1000 connections 34 | Thread Stats Avg Stdev Max +/- Stdev 35 | Latency 11.24ms 12.07ms 127.54ms 87.12% 36 | Req/Sec 11.39k 2.60k 33.20k 74.42% 37 | Latency Distribution 38 | 50% 7.24ms 39 | 75% 13.95ms 40 | 90% 26.98ms 41 | 99% 56.30ms 42 | 1129086 requests in 10.09s, 139.98MB read 43 | Requests/sec: 111881.19 44 | Transfer/sec: 13.87MB 45 | ``` 46 | 47 | 从上面的测试可以看出,每秒可以处理110K的请求数,这看着性能是好高,但实际上这种测试的意义不太大,不过总可以让大家放心不至于拖后腿。 48 | 49 | `elton`的亮点是在响应数据中间件的处理,以简单的方式返回正常或出错的响应数据,下面我们来测试一下这两种场景的性能表现。 50 | 51 | 52 | ```go 53 | package main 54 | 55 | import ( 56 | "strings" 57 | 58 | "github.com/vicanso/elton" 59 | "github.com/vicanso/elton/middleware" 60 | "github.com/vicanso/hes" 61 | ) 62 | 63 | type ( 64 | HelloWord struct { 65 | Content string `json:"content,omitempty"` 66 | Size int `json:"size,omitempty"` 67 | Price float32 `json:"price,omitempty"` 68 | VIP bool `json:"vip,omitempty"` 69 | } 70 | ) 71 | 72 | func main() { 73 | d := elton.New() 74 | 75 | arr := make([]string, 0) 76 | for i := 0; i < 100; i++ { 77 | arr = append(arr, "花褪残红青杏小。燕子飞时,绿水人家绕。枝上柳绵吹又少,天涯何处无芳草!") 78 | } 79 | content := strings.Join(arr, "\n") 80 | 81 | d.Use(middleware.NewDefaultError()) 82 | d.Use(middleware.NewDefaultResponder()) 83 | 84 | d.GET("/", func(c *elton.Context) (err error) { 85 | c.Body = &HelloWord{ 86 | Content: content, 87 | Size: 100, 88 | Price: 10.12, 89 | VIP: true, 90 | } 91 | return 92 | }) 93 | 94 | d.GET("/error", func(c *elton.Context) (err error) { 95 | err = hes.New("abcd") 96 | return 97 | }) 98 | err := d.ListenAndServe(":3000") 99 | if err != nil { 100 | panic(err) 101 | } 102 | } 103 | ``` 104 | 105 | ```bash 106 | wrk -c 1000 -t 10 --latency 'http://127.0.0.1:3000/' 107 | Running 10s test @ http://127.0.0.1:3000/ 108 | 10 threads and 1000 connections 109 | Thread Stats Avg Stdev Max +/- Stdev 110 | Latency 46.41ms 58.15ms 606.12ms 83.56% 111 | Req/Sec 4.22k 798.75 7.18k 69.90% 112 | Latency Distribution 113 | 50% 15.31ms 114 | 75% 79.23ms 115 | 90% 129.41ms 116 | 99% 240.98ms 117 | 420454 requests in 10.07s, 4.26GB read 118 | Requests/sec: 41734.70 119 | Transfer/sec: 432.80MB 120 | ``` 121 | 122 | ```bash 123 | wrk -c 1000 -t 10 --latency 'http://127.0.0.1:3000/error' 124 | Running 10s test @ http://127.0.0.1:3000/error 125 | 10 threads and 1000 connections 126 | Thread Stats Avg Stdev Max +/- Stdev 127 | Latency 11.29ms 11.36ms 146.95ms 86.59% 128 | Req/Sec 10.91k 2.37k 21.86k 70.08% 129 | Latency Distribution 130 | 50% 7.62ms 131 | 75% 14.23ms 132 | 90% 26.56ms 133 | 99% 53.32ms 134 | 1083752 requests in 10.10s, 142.63MB read 135 | Non-2xx or 3xx responses: 1083752 136 | Requests/sec: 107344.19 137 | Transfer/sec: 14.13MB 138 | ``` 139 | 140 | 对于正常返回(数据量为10KB)的struct做序列化时,性能会有所降低,从测试结果可以看出,每秒还是可以处理41K的请求,出错的转换处理效率更高,每秒能处理107K的请求。 141 | 142 | 143 | 144 | 下面是`gin`的测试结果: 145 | 146 | ```go 147 | package main 148 | 149 | import ( 150 | "strings" 151 | 152 | "github.com/gin-gonic/gin" 153 | ) 154 | 155 | type ( 156 | HelloWord struct { 157 | Content string `json:"content,omitempty"` 158 | Size int `json:"size,omitempty"` 159 | Price float32 `json:"price,omitempty"` 160 | VIP bool `json:"vip,omitempty"` 161 | } 162 | ) 163 | 164 | func main() { 165 | 166 | arr := make([]string, 0) 167 | for i := 0; i < 100; i++ { 168 | arr = append(arr, "花褪残红青杏小。燕子飞时,绿水人家绕。枝上柳绵吹又少,天涯何处无芳草!") 169 | } 170 | content := strings.Join(arr, "\n") 171 | 172 | router := gin.New() 173 | 174 | router.GET("/", func(c *gin.Context) { 175 | c.JSON(200, &HelloWord{ 176 | Content: content, 177 | Size: 100, 178 | Price: 10.12, 179 | VIP: true, 180 | }) 181 | }) 182 | router.Run(":3000") 183 | } 184 | ``` 185 | 186 | ```bash 187 | wrk -c 1000 -t 10 --latency 'http://127.0.0.1:3000/' 188 | Running 10s test @ http://127.0.0.1:3000/ 189 | 10 threads and 1000 connections 190 | Thread Stats Avg Stdev Max +/- Stdev 191 | Latency 52.17ms 66.58ms 629.46ms 83.46% 192 | Req/Sec 3.97k 0.91k 13.91k 72.16% 193 | Latency Distribution 194 | 50% 16.37ms 195 | 75% 89.93ms 196 | 90% 145.96ms 197 | 99% 277.75ms 198 | 394628 requests in 10.10s, 4.00GB read 199 | Requests/sec: 39075.49 200 | Transfer/sec: 405.90MB 201 | ``` 202 | 203 | 从上面的测试数据可以看出`elton`的性能与`gin`整体上基本一致,无需太过担忧性能问题,需要注意的是elton不再使用httprouter来处理路由,路由参数支持更多自定义的处理,因此路由的查询性能有所下降。 -------------------------------------------------------------------------------- /middleware/concurrent_limiter_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "errors" 27 | "net/http/httptest" 28 | "sync" 29 | "testing" 30 | 31 | "github.com/stretchr/testify/assert" 32 | "github.com/vicanso/elton" 33 | "github.com/vicanso/hes" 34 | ) 35 | 36 | func TestNoLockFunction(t *testing.T) { 37 | assert := assert.New(t) 38 | defer func() { 39 | r := recover() 40 | assert.Equal(r.(error), ErrRequireLockFunction) 41 | }() 42 | 43 | NewConcurrentLimiter(ConcurrentLimiterConfig{}) 44 | } 45 | 46 | func TestConcurrentLimiter(t *testing.T) { 47 | assert := assert.New(t) 48 | m := new(sync.Map) 49 | concurrentLimiter := NewConcurrentLimiter(ConcurrentLimiterConfig{ 50 | Keys: []string{ 51 | ":ip", 52 | "h:X-Token", 53 | "q:type", 54 | "p:id", 55 | "account", 56 | }, 57 | KeyGenerator: func(c *elton.Context) (string, error) { 58 | return "abc", nil 59 | }, 60 | Lock: func(key string, _ *elton.Context) (success bool, unlock func(), err error) { 61 | value := "192.0.2.1,xyz,1,123,tree.xie,abc" 62 | assert.Equal(value, key) 63 | if key != value { 64 | err = errors.New("key is invalid") 65 | return 66 | } 67 | _, loaded := m.LoadOrStore(key, 1) 68 | // 如果已存在,则获取锁失败 69 | if loaded { 70 | return 71 | } 72 | success = true 73 | // 删除锁 74 | unlock = func() { 75 | m.Delete(key) 76 | } 77 | return 78 | }, 79 | }) 80 | 81 | skipErr := errors.New("skip error") 82 | // next直接返回skip error,用于判断是否执行了next 83 | next := func() error { 84 | return skipErr 85 | } 86 | tests := []struct { 87 | newContext func() *elton.Context 88 | fn elton.Handler 89 | err error 90 | }{ 91 | // not allow empty 92 | { 93 | newContext: func() *elton.Context { 94 | return elton.NewContext(nil, httptest.NewRequest("GET", "/", nil)) 95 | }, 96 | fn: NewConcurrentLimiter(ConcurrentLimiterConfig{ 97 | NotAllowEmpty: true, 98 | Keys: []string{ 99 | "p:id", 100 | }, 101 | Lock: func(key string, c *elton.Context) (success bool, unlock func(), err error) { 102 | return 103 | }, 104 | }), 105 | err: ErrNotAllowEmpty, 106 | }, 107 | // lock fail 108 | { 109 | newContext: func() *elton.Context { 110 | return elton.NewContext(nil, httptest.NewRequest("GET", "/", nil)) 111 | }, 112 | fn: NewConcurrentLimiter(ConcurrentLimiterConfig{ 113 | Keys: []string{ 114 | "p:id", 115 | }, 116 | Lock: func(key string, c *elton.Context) (success bool, unlock func(), err error) { 117 | return false, nil, errors.New("lock error") 118 | }, 119 | }), 120 | err: hes.NewWithError(errors.New("lock error")), 121 | }, 122 | // global concurrency limit 1(fail) 123 | { 124 | newContext: func() *elton.Context { 125 | req := httptest.NewRequest("POST", "/users/login?type=1", nil) 126 | resp := httptest.NewRecorder() 127 | c := elton.NewContext(resp, req) 128 | return c 129 | }, 130 | fn: NewGlobalConcurrentLimiter(GlobalConcurrentLimiterConfig{ 131 | Max: 1, 132 | }), 133 | err: ErrTooManyRequests, 134 | }, 135 | // global concurrency limit 2(success) 136 | { 137 | newContext: func() *elton.Context { 138 | req := httptest.NewRequest("POST", "/users/login?type=1", nil) 139 | resp := httptest.NewRecorder() 140 | c := elton.NewContext(resp, req) 141 | c.Next = next 142 | return c 143 | }, 144 | fn: NewGlobalConcurrentLimiter(GlobalConcurrentLimiterConfig{ 145 | Max: 2, 146 | }), 147 | err: skipErr, 148 | }, 149 | { 150 | newContext: func() *elton.Context { 151 | req := httptest.NewRequest("POST", "/users/login?type=1", nil) 152 | resp := httptest.NewRecorder() 153 | c := elton.NewContext(resp, req) 154 | req.Header.Set("X-Token", "xyz") 155 | c.RequestBody = []byte(`{ 156 | "account": "tree.xie" 157 | }`) 158 | c.Params = new(elton.RouteParams) 159 | c.Params.Add("id", "123") 160 | c.Next = next 161 | return c 162 | }, 163 | fn: concurrentLimiter, 164 | err: skipErr, 165 | }, 166 | } 167 | 168 | for _, tt := range tests { 169 | c := tt.newContext() 170 | err := tt.fn(c) 171 | assert.Equal(tt.err, err) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /middleware/static_embed.go: -------------------------------------------------------------------------------- 1 | //go:build go1.16 2 | // +build go1.16 3 | 4 | // Copyright (c) 2021 Tree Xie 5 | 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | package middleware 25 | 26 | import ( 27 | "archive/tar" 28 | "bytes" 29 | "embed" 30 | "io" 31 | "io/fs" 32 | "os" 33 | "path/filepath" 34 | "strings" 35 | 36 | "github.com/vicanso/elton" 37 | "github.com/vicanso/hes" 38 | ) 39 | 40 | type embedStaticFS struct { 41 | // prefix of file 42 | Prefix string 43 | FS embed.FS 44 | } 45 | 46 | var _ StaticFile = (*embedStaticFS)(nil) 47 | 48 | // NewEmbedStaticFS returns a new embed static fs 49 | func NewEmbedStaticFS(fs embed.FS, prefix string) *embedStaticFS { 50 | return &embedStaticFS{ 51 | Prefix: prefix, 52 | FS: fs, 53 | } 54 | } 55 | 56 | func getFile(prefix string, file string) string { 57 | windowsPathSeparator := "\\" 58 | return strings.ReplaceAll(filepath.Join(prefix, file), windowsPathSeparator, "/") 59 | } 60 | 61 | func (es *embedStaticFS) getFile(file string) string { 62 | return getFile(es.Prefix, file) 63 | } 64 | 65 | // Exists check the file exists 66 | func (es *embedStaticFS) Exists(file string) bool { 67 | f, err := es.FS.Open(es.getFile(file)) 68 | if err != nil { 69 | return false 70 | } 71 | defer func() { 72 | _ = f.Close() 73 | }() 74 | return true 75 | } 76 | 77 | // Get returns content of file 78 | func (es *embedStaticFS) Get(file string) ([]byte, error) { 79 | return es.FS.ReadFile(es.getFile(file)) 80 | } 81 | 82 | // Stat return nil for file stat 83 | func (es *embedStaticFS) Stat(file string) os.FileInfo { 84 | // 文件打包至程序中,因此无file info 85 | return nil 86 | } 87 | 88 | // NewReader returns a reader of file 89 | func (es *embedStaticFS) NewReader(file string) (io.Reader, error) { 90 | buf, err := es.Get(file) 91 | if err != nil { 92 | return nil, err 93 | } 94 | return bytes.NewReader(buf), nil 95 | } 96 | 97 | // SendFile sends file to http response and set content type 98 | func (es *embedStaticFS) SendFile(c *elton.Context, file string) error { 99 | // 因为静态文件打包至程序中,直接读取 100 | buf, err := es.Get(file) 101 | if err != nil { 102 | return err 103 | } 104 | // 根据文件后续设置类型 105 | c.SetContentTypeByExt(file) 106 | c.BodyBuffer = bytes.NewBuffer(buf) 107 | return nil 108 | } 109 | 110 | type tarFS struct { 111 | // prefix of file 112 | Prefix string 113 | // tar file 114 | File string 115 | // embed fs 116 | Embed *embed.FS 117 | } 118 | 119 | var _ StaticFile = (*tarFS)(nil) 120 | 121 | // NewTarFS returns a new tar static fs 122 | func NewTarFS(file string) *tarFS { 123 | return &tarFS{ 124 | File: file, 125 | } 126 | } 127 | 128 | func (t *tarFS) get(file string, includeContent bool) (bool, []byte, error) { 129 | var f fs.File 130 | var err error 131 | if t.Embed != nil { 132 | f, err = t.Embed.Open(t.File) 133 | } else { 134 | f, err = os.Open(t.File) 135 | } 136 | if err != nil { 137 | return false, nil, err 138 | } 139 | defer func() { 140 | _ = f.Close() 141 | }() 142 | tr := tar.NewReader(f) 143 | var data []byte 144 | found := false 145 | file = getFile(t.Prefix, file) 146 | for { 147 | hdr, err := tr.Next() 148 | if err == io.EOF { 149 | break 150 | } 151 | if err != nil { 152 | return false, nil, err 153 | } 154 | if hdr.Name == file { 155 | found = true 156 | if includeContent { 157 | buf, err := io.ReadAll(tr) 158 | if err != nil { 159 | return false, nil, err 160 | } 161 | data = buf 162 | } 163 | break 164 | } 165 | } 166 | return found, data, nil 167 | } 168 | 169 | // Exists check the file exists 170 | func (t *tarFS) Exists(file string) bool { 171 | found, _, _ := t.get(file, false) 172 | return found 173 | } 174 | 175 | // Get returns content of file 176 | func (t *tarFS) Get(file string) ([]byte, error) { 177 | found, data, err := t.get(file, true) 178 | if err != nil { 179 | return nil, err 180 | } 181 | if !found { 182 | return nil, hes.NewWithStatusCode("Not Found", 404) 183 | } 184 | return data, nil 185 | } 186 | 187 | // Stat return nil for file stat 188 | func (t *tarFS) Stat(file string) os.FileInfo { 189 | return nil 190 | } 191 | 192 | // NewReader returns a reader of file 193 | func (t *tarFS) NewReader(file string) (io.Reader, error) { 194 | buf, err := t.Get(file) 195 | if err != nil { 196 | return nil, err 197 | } 198 | return bytes.NewReader(buf), nil 199 | } 200 | -------------------------------------------------------------------------------- /middleware/http_header_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2021 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "encoding/json" 27 | "net/http" 28 | "testing" 29 | 30 | "github.com/stretchr/testify/assert" 31 | ) 32 | 33 | func TestShortHeaderIndexes(t *testing.T) { 34 | assert := assert.New(t) 35 | 36 | name := shortHeaderIndexes.getName(1) 37 | assert.Equal("accept-charset", name) 38 | name = shortHeaderIndexes.getName(3) 39 | assert.Equal("accept-language", name) 40 | assert.Empty(shortHeaderIndexes.getName(int(MaxShortHeader))) 41 | name = shortHeaderIndexes.getName(30) 42 | assert.Equal("last-modified", name) 43 | 44 | // 故意大写了O 45 | index, ok := shortHeaderIndexes.getIndex("Cache-COntrol") 46 | assert.True(ok) 47 | assert.Equal(index, uint8(10)) 48 | 49 | _, ok = shortHeaderIndexes.getIndex("abc") 50 | assert.False(ok) 51 | } 52 | 53 | func TestHTTPHeader(t *testing.T) { 54 | assert := assert.New(t) 55 | 56 | // 压缩的header 57 | h := NewHTTPHeader("Cache-Control", []string{"no-cache"}) 58 | assert.Equal(uint8(10), h[0]) 59 | assert.Equal("no-cache", string(h[1:])) 60 | 61 | name, values := h.Header() 62 | assert.Equal("cache-control", name) 63 | assert.Equal([]string{ 64 | "no-cache", 65 | }, values) 66 | 67 | // 非压缩的header 68 | h = NewHTTPHeader("X-Test", []string{"my name", "my job"}) 69 | assert.Equal(uint8(NoneMatchHeader), h[0]) 70 | assert.Equal("X-Test:my name\nmy job", string(h[1:])) 71 | name, values = h.Header() 72 | assert.Equal("X-Test", name) 73 | assert.Equal([]string{ 74 | "my name", 75 | "my job", 76 | }, values) 77 | } 78 | 79 | func TestHTTPHeaders(t *testing.T) { 80 | assert := assert.New(t) 81 | header := http.Header{ 82 | "Cache-Control": []string{ 83 | "max-age=0, private, must-revalidate", 84 | }, 85 | "Content-Encoding": []string{ 86 | "gzip", 87 | }, 88 | "Content-Type": []string{ 89 | "text/html; charset=utf-8", 90 | }, 91 | "Date": []string{ 92 | "Mon, 08 Nov 2021 23:48:55 GMT", 93 | }, 94 | "Etag": []string{ 95 | `W/"e232d5a706265f21a7019b5ab453e14a"`, 96 | }, 97 | "X-Referrer-Policy": []string{ 98 | "origin-when-cross-origin, strict-origin-when-cross-origin", 99 | }, 100 | "X-Trace-Id": []string{ 101 | "83C9:30C1:9C96A:146FC2:61888203", 102 | }, 103 | HeaderXCache: []string{ 104 | "ABCD", 105 | }, 106 | } 107 | hs := NewHTTPHeaders(header, "x-cache") 108 | assert.Equal(258, len(hs)) 109 | 110 | header.Del(HeaderXCache) 111 | assert.Equal(header, hs.Header()) 112 | } 113 | 114 | func BenchmarkNewShortHTTPHeader(b *testing.B) { 115 | for i := 0; i < b.N; i++ { 116 | _ = NewHTTPHeader("Cache-Control", []string{"no-cache"}) 117 | } 118 | } 119 | 120 | func BenchmarkNewHTTPHeader(b *testing.B) { 121 | for i := 0; i < b.N; i++ { 122 | _ = NewHTTPHeader("X-Test", []string{"my name", "my job"}) 123 | } 124 | } 125 | 126 | func getTestHTTPHeader() http.Header { 127 | return http.Header{ 128 | "Cache-Control": []string{ 129 | "max-age=0, private, must-revalidate", 130 | }, 131 | "Content-Encoding": []string{ 132 | "gzip", 133 | }, 134 | "Content-Type": []string{ 135 | "text/html; charset=utf-8", 136 | }, 137 | "Date": []string{ 138 | "Mon, 08 Nov 2021 23:48:55 GMT", 139 | }, 140 | "Etag": []string{ 141 | `W/"e232d5a706265f21a7019b5ab453e14a"`, 142 | }, 143 | "X-Referrer-Policy": []string{ 144 | "origin-when-cross-origin, strict-origin-when-cross-origin", 145 | }, 146 | "X-Trace-Id": []string{ 147 | "83C9:30C1:9C96A:146FC2:61888203", 148 | }, 149 | } 150 | } 151 | 152 | func BenchmarkNewHTTPHeaders(b *testing.B) { 153 | header := getTestHTTPHeader() 154 | 155 | for i := 0; i < b.N; i++ { 156 | _ = NewHTTPHeaders(header, "Date") 157 | } 158 | } 159 | 160 | func BenchmarkHTTPHeaderMarshal(b *testing.B) { 161 | header := getTestHTTPHeader() 162 | for i := 0; i < b.N; i++ { 163 | _, _ = json.Marshal(header) 164 | } 165 | } 166 | 167 | func BenchmarkToHTTPHeader(b *testing.B) { 168 | hs := NewHTTPHeaders(getTestHTTPHeader()) 169 | for i := 0; i < b.N; i++ { 170 | _ = hs.Header() 171 | } 172 | } 173 | 174 | func BenchmarkHTTPHeaderUnmarshal(b *testing.B) { 175 | buf, _ := json.Marshal(getTestHTTPHeader()) 176 | if len(buf) == 0 { 177 | panic("marshal fail") 178 | } 179 | for i := 0; i < b.N; i++ { 180 | header := make(http.Header) 181 | _ = json.Unmarshal(buf, &header) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /docs/custom_body_parser.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: 自定义Body Parser 3 | --- 4 | 5 | `elton-body-parser`只提供对`application/json`以及`application/x-www-form-urlencoded`转换为json字节的处理,在实际使用中还存在一些其它的场景。如`xml`,自定义数据结构等。 6 | 7 | 在实际项目中,统计数据一般记录至influxdb,为了性能的考虑,统计数据是批量提交(如每1000个统计点提交一次)。数据提交的时候,重复的字符比较多,为了减少带宽的占用,所以先做压缩处理。考虑到性能的原因,采用了`snappy`压缩处理。下面是抽取出来的示例代码: 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "bytes" 14 | "fmt" 15 | "io" 16 | "net/http" 17 | "strconv" 18 | "strings" 19 | "time" 20 | 21 | "github.com/golang/snappy" 22 | "github.com/vicanso/elton" 23 | ) 24 | 25 | // 仅示例,对于出错直接panic 26 | func post() { 27 | // weather,location=us-midwest temperature=82 1465839830100400200 28 | max := 1000 29 | arr := make([]string, max) 30 | for i := 0; i < max; i++ { 31 | arr[i] = "weather,location=us-midwest temperature=82 " + strconv.FormatInt(time.Now().UnixNano(), 10) 32 | } 33 | var dst []byte 34 | data := snappy.Encode(dst, []byte(strings.Join(arr, "\n"))) 35 | 36 | req, err := http.NewRequest("POST", "http://127.0.0.1:3000/influx", bytes.NewReader(data)) 37 | req.Header.Set(elton.HeaderContentType, ContentTypeIfx) 38 | if err != nil { 39 | panic(err) 40 | } 41 | resp, err := http.DefaultClient.Do(req) 42 | if err != nil { 43 | panic(err) 44 | } 45 | result, _ := io.ReadAll(resp.Body) 46 | fmt.Println(string(result)) 47 | } 48 | 49 | const ( 50 | // ContentTypeIfx influx data type 51 | ContentTypeIfx = "application/ifx" 52 | ) 53 | 54 | // NewInfluxParser influx parser 55 | func NewInfluxParser() elton.Handler { 56 | return func(c *elton.Context) (err error) { 57 | // 对于非POST请求,以及数据类型不匹配的,则跳过 58 | if c.Request.Method != http.MethodPost || 59 | c.GetRequestHeader(elton.HeaderContentType) != ContentTypeIfx { 60 | return c.Next() 61 | } 62 | body, err := io.ReadAll(c.Request.Body) 63 | // 如果读取数据时出错,直接返回 64 | if err != nil { 65 | return 66 | } 67 | var dst []byte 68 | data, err := snappy.Decode(dst, body) 69 | // 如果解压出错,直接返回(也可再自定义出错类型,方便排查) 70 | if err != nil { 71 | return 72 | } 73 | // 至此则解压生成提交的数据了 74 | c.RequestBody = data 75 | return c.Next() 76 | } 77 | } 78 | 79 | func main() { 80 | e := elton.New() 81 | go func() { 82 | // 等待一秒让elton启动(仅为了测试方便,直接客户端服务端同一份代码) 83 | time.Sleep(time.Second) 84 | post() 85 | }() 86 | 87 | e.Use(NewInfluxParser()) 88 | 89 | e.POST("/influx", func(c *elton.Context) (err error) { 90 | points := strings.SplitN(string(c.RequestBody), "\n", -1) 91 | c.BodyBuffer = bytes.NewBufferString("add " + strconv.Itoa(len(points)) + " points to influxdb done") 92 | return 93 | }) 94 | 95 | err := e.ListenAndServe(":3000") 96 | if err != nil { 97 | panic(err) 98 | } 99 | } 100 | ``` 101 | 102 | 通过各类自定义的中间件,可以实现各种不同的提交数据的解析,只要将解析结果保存至`Context.RequestBody`中,后续则由处理函数再将字节转换为相对应的结构,简单易用。 103 | 104 | `elton-body-parser`提供自定义Decoder方式,可以按实际使用添加Decoder,上面的实现可以简化为: 105 | 106 | ```go 107 | package main 108 | 109 | import ( 110 | "bytes" 111 | "fmt" 112 | "io" 113 | "net/http" 114 | "regexp" 115 | "strconv" 116 | "strings" 117 | "time" 118 | 119 | "github.com/golang/snappy" 120 | "github.com/vicanso/elton" 121 | "github.com/vicanso/elton/middleware" 122 | ) 123 | 124 | // 仅示例,对于出错直接panic 125 | func post() { 126 | // weather,location=us-midwest temperature=82 1465839830100400200 127 | max := 1000 128 | arr := make([]string, max) 129 | for i := 0; i < max; i++ { 130 | arr[i] = "weather,location=us-midwest temperature=82 " + strconv.FormatInt(time.Now().UnixNano(), 10) 131 | } 132 | var dst []byte 133 | data := snappy.Encode(dst, []byte(strings.Join(arr, "\n"))) 134 | 135 | req, err := http.NewRequest("POST", "http://127.0.0.1:3000/influx", bytes.NewReader(data)) 136 | req.Header.Set(elton.HeaderContentType, ContentTypeIfx) 137 | if err != nil { 138 | panic(err) 139 | } 140 | resp, err := http.DefaultClient.Do(req) 141 | if err != nil { 142 | panic(err) 143 | } 144 | result, _ := io.ReadAll(resp.Body) 145 | fmt.Println(string(result)) 146 | } 147 | 148 | const ( 149 | // ContentTypeIfx influx data type 150 | ContentTypeIfx = "application/ifx" 151 | ) 152 | 153 | func main() { 154 | e := elton.New() 155 | go func() { 156 | // 等待一秒让elton启动(仅为了测试方便,直接客户端服务端同一份代码) 157 | time.Sleep(time.Second) 158 | post() 159 | }() 160 | 161 | conf := middleware.BodyParserConfig{ 162 | // 设置对哪些content type处理,默认只处理application/json 163 | ContentTypeValidate: func(c *elton.Context) bool { 164 | ct := c.GetRequestHeader(elton.HeaderContentType) 165 | return regexp.MustCompile("application/json|" + ContentTypeIfx).MatchString(ct) 166 | }, 167 | } 168 | // gzip解压 169 | conf.AddDecoder(middleware.NewGzipDecoder()) 170 | // json decoder 171 | conf.AddDecoder(middleware.NewJSONDecoder()) 172 | // 添加自定义influx的decoder 173 | conf.AddDecoder(&middleware.BodyDecoder{ 174 | // 判断是否符合该decoder 175 | Validate: func(c *elton.Context) bool { 176 | return c.GetRequestHeader(elton.HeaderContentType) == ContentTypeIfx 177 | }, 178 | // 解压snappy 179 | Decode: func(c *elton.Context, orginalData []byte) (data []byte, err error) { 180 | var dst []byte 181 | data, err = snappy.Decode(dst, orginalData) 182 | return 183 | }, 184 | }) 185 | 186 | e.Use(middleware.NewBodyParser(conf)) 187 | 188 | e.POST("/influx", func(c *elton.Context) (err error) { 189 | points := strings.SplitN(string(c.RequestBody), "\n", -1) 190 | c.BodyBuffer = bytes.NewBufferString("add " + strconv.Itoa(len(points)) + " points to influxdb done") 191 | return 192 | }) 193 | 194 | err := e.ListenAndServe(":3000") 195 | if err != nil { 196 | panic(err) 197 | } 198 | } 199 | ``` -------------------------------------------------------------------------------- /docs/body_parse_validate.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: body反序列化与校验 3 | --- 4 | 5 | elton中`body-parser`中间件只将数据读取为字节,并没有做反序列化以及参数的校验。使用`json`来反序列化时,只能简单的对参数类型做校验,下面介绍如何使用[validator](https://github.com/go-playground/validator)与[govalidator](https://github.com/asaskevich/govalidator)增强参数校验,可以按自己喜好选择合格的校验库。 6 | 7 | 下面的例子是用户登录功能,参数为账号与密码,两个参数的限制如下: 8 | 9 | - 账号:只允许为数字与字母,而且长度不能超过20位 10 | - 密码:只允许为数字与字母,而且长度不能小于6位,不能超过20位 11 | 12 | ## validator 13 | 14 | ```go 15 | package main 16 | 17 | import ( 18 | "encoding/json" 19 | "reflect" 20 | "regexp" 21 | 22 | "github.com/go-playground/validator/v10" 23 | "github.com/vicanso/elton" 24 | "github.com/vicanso/elton/middleware" 25 | ) 26 | 27 | var ( 28 | validate = validator.New() 29 | ) 30 | 31 | func toString(value reflect.Value) (string, bool) { 32 | if value.Kind() != reflect.String { 33 | return "", false 34 | } 35 | return value.String(), true 36 | } 37 | 38 | func init() { 39 | rxAlphanumeric := regexp.MustCompile("^[a-zA-Z0-9]+$") 40 | 41 | // 添加自定义参数校验,如果返回false则表示参数不符合 42 | validate.RegisterAlias("xAccount", "alphanum,max=20") 43 | 44 | _ = validate.RegisterValidation("xPassword", func(fl validator.FieldLevel) bool { 45 | value, ok := toString(fl.Field()) 46 | if !ok { 47 | return false 48 | } 49 | if value == "" { 50 | return false 51 | } 52 | // 如果不是字母与数字 53 | if !rxAlphanumeric.MatchString(value) { 54 | return false 55 | } 56 | // 密码<=20而且>=6 57 | return len(value) <= 20 && len(value) >= 6 58 | }) 59 | } 60 | 61 | type ( 62 | loginParams struct { 63 | Account string `json:"account,omitempty" validate:"xAccount"` 64 | Password string `json:"password,omitempty" validate:"xPassword"` 65 | } 66 | ) 67 | 68 | func doValidate(s interface{}, data interface{}) (err error) { 69 | // 如果有数据则做反序列化 70 | if data != nil { 71 | switch data := data.(type) { 72 | case []byte: 73 | err = json.Unmarshal(data, s) 74 | if err != nil { 75 | return 76 | } 77 | default: 78 | // 如果数据不是字节,则先序列化(有可能是map) 79 | buf, err := json.Marshal(data) 80 | if err != nil { 81 | return err 82 | } 83 | err = json.Unmarshal(buf, s) 84 | if err != nil { 85 | return err 86 | } 87 | } 88 | } 89 | err = validate.Struct(s) 90 | return 91 | } 92 | 93 | func main() { 94 | e := elton.New() 95 | 96 | e.Use(middleware.NewError(middleware.ErrorConfig{ 97 | ResponseType: "json", 98 | })) 99 | e.Use(middleware.NewDefaultBodyParser()) 100 | e.Use(middleware.NewDefaultResponder()) 101 | e.POST("/users/login", func(c *elton.Context) (err error) { 102 | params := &loginParams{} 103 | err = doValidate(params, c.RequestBody) 104 | if err != nil { 105 | return 106 | } 107 | c.Body = params 108 | return 109 | }) 110 | err := e.ListenAndServe(":3000") 111 | if err != nil { 112 | panic(err) 113 | } 114 | } 115 | 116 | ``` 117 | ## govalidator 118 | 119 | ```go 120 | package main 121 | 122 | import ( 123 | "encoding/json" 124 | 125 | "github.com/asaskevich/govalidator" 126 | "github.com/vicanso/elton" 127 | "github.com/vicanso/elton/middleware" 128 | ) 129 | 130 | var ( 131 | customTypeTagMap = govalidator.CustomTypeTagMap 132 | ) 133 | 134 | func init() { 135 | // 添加自定义参数校验,如果返回false则表示参数不符合 136 | customTypeTagMap.Set("xAccount", func(i interface{}, _ interface{}) bool { 137 | v, ok := i.(string) 138 | if !ok || v == "" { 139 | return false 140 | } 141 | // 如果不是字母与数字 142 | if !govalidator.IsAlphanumeric(v) { 143 | return false 144 | } 145 | // 账号长度不能大于20 146 | if len(v) > 20 { 147 | return false 148 | } 149 | return true 150 | }) 151 | customTypeTagMap.Set("xPassword", func(i interface{}, _ interface{}) bool { 152 | v, ok := i.(string) 153 | if !ok || v == "" { 154 | return false 155 | } 156 | // 如果不是字母与数字 157 | if !govalidator.IsAlphanumeric(v) { 158 | return false 159 | } 160 | // 密码长度不能大于20小于6 161 | if len(v) > 20 || len(v) < 6 { 162 | return false 163 | } 164 | return true 165 | }) 166 | } 167 | 168 | type ( 169 | loginParams struct { 170 | Account string `json:"account,omitempty" valid:"xAccount~账号只允许数字与字母且不能超过20位"` 171 | Password string `json:"password,omitempty" valid:"xPassword~密码只允许数字与字母且不能少于6位超过20位"` 172 | } 173 | ) 174 | 175 | func doValidate(s interface{}, data interface{}) (err error) { 176 | // 如果有数据则做反序列化 177 | if data != nil { 178 | switch data := data.(type) { 179 | case []byte: 180 | err = json.Unmarshal(data, s) 181 | if err != nil { 182 | return 183 | } 184 | default: 185 | // 如果数据不是字节,则先序列化(有可能是map) 186 | buf, err := json.Marshal(data) 187 | if err != nil { 188 | return err 189 | } 190 | err = json.Unmarshal(buf, s) 191 | if err != nil { 192 | return err 193 | } 194 | } 195 | } 196 | _, err = govalidator.ValidateStruct(s) 197 | return 198 | } 199 | 200 | func main() { 201 | e := elton.New() 202 | 203 | e.Use(middleware.NewError(middleware.ErrorConfig{ 204 | ResponseType: "json", 205 | })) 206 | e.Use(middleware.NewDefaultBodyParser()) 207 | e.Use(middleware.NewDefaultResponder()) 208 | e.POST("/users/login", func(c *elton.Context) (err error) { 209 | params := &loginParams{} 210 | err = doValidate(params, c.RequestBody) 211 | if err != nil { 212 | return 213 | } 214 | c.Body = params 215 | return 216 | }) 217 | err := e.ListenAndServe(":3000") 218 | if err != nil { 219 | panic(err) 220 | } 221 | } 222 | 223 | ``` 224 | 225 | ## 调用示例 226 | 227 | ``` 228 | curl -XPOST -H 'Content-Type:application/json' -d '{"account":"treexie", "password": "123"}' 'http://127.0.0.1:3000/users/login' 229 | ``` 230 | 231 | 从上面的代码中可以看到,可以自通过定义校验标签进行值校验(一般都是长度,大小,符合性的校验),而且大部分的校验都可复用常规校验函数,实现简单便捷。建议在实际项目中,针对每个不同的参数都自定义校验,尽可能保证参数的合法性。 -------------------------------------------------------------------------------- /middleware/cache_compressor.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2021 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "bytes" 27 | "compress/gzip" 28 | "regexp" 29 | 30 | "github.com/klauspost/compress/zstd" 31 | "github.com/vicanso/elton" 32 | ) 33 | 34 | type CompressionType uint8 35 | 36 | const ( 37 | // not compress 38 | CompressionNone CompressionType = iota 39 | // gzip compress 40 | CompressionGzip 41 | // br compress 42 | CompressionBr 43 | // zstd compress 44 | CompressionZstd 45 | ) 46 | 47 | type CacheCompressor interface { 48 | // decompress function 49 | Decompress(buffer *bytes.Buffer) (data *bytes.Buffer, err error) 50 | // get encoding of compressor 51 | GetEncoding() (encoding string) 52 | // is valid for compress 53 | IsValid(contentType string, length int) (valid bool) 54 | // compress function 55 | Compress(buffer *bytes.Buffer) (data *bytes.Buffer, compressionType CompressionType, err error) 56 | // get compression type 57 | GetCompression() CompressionType 58 | } 59 | 60 | type CacheBrCompressor struct { 61 | Level int 62 | MinLength int 63 | ContentRegexp *regexp.Regexp 64 | } 65 | 66 | func isValidForCompress(reg *regexp.Regexp, minLength int, contentType string, length int) bool { 67 | if minLength == 0 { 68 | minLength = DefaultCompressMinLength 69 | } 70 | if length < minLength { 71 | return false 72 | } 73 | if reg == nil { 74 | reg = DefaultCompressRegexp 75 | } 76 | return reg.MatchString(contentType) 77 | } 78 | 79 | func NewCacheBrCompressor() *CacheBrCompressor { 80 | return &CacheBrCompressor{ 81 | Level: defaultBrQuality, 82 | } 83 | } 84 | 85 | func (br *CacheBrCompressor) Decompress(data *bytes.Buffer) (*bytes.Buffer, error) { 86 | return BrotliDecompress(data.Bytes()) 87 | } 88 | func (br *CacheBrCompressor) GetEncoding() string { 89 | return elton.Br 90 | } 91 | func (br *CacheBrCompressor) IsValid(contentType string, length int) bool { 92 | return isValidForCompress(br.ContentRegexp, br.MinLength, contentType, length) 93 | } 94 | func (br *CacheBrCompressor) Compress(buffer *bytes.Buffer) (*bytes.Buffer, CompressionType, error) { 95 | data, err := BrotliCompress(buffer.Bytes(), br.Level) 96 | if err != nil { 97 | return nil, CompressionNone, err 98 | } 99 | return data, br.GetCompression(), nil 100 | } 101 | func (br *CacheBrCompressor) GetCompression() CompressionType { 102 | return CompressionBr 103 | } 104 | 105 | type CacheGzipCompressor struct { 106 | Level int 107 | MinLength int 108 | ContentRegexp *regexp.Regexp 109 | } 110 | 111 | func NewCacheGzipCompressor() *CacheGzipCompressor { 112 | return &CacheGzipCompressor{ 113 | Level: gzip.DefaultCompression, 114 | } 115 | } 116 | 117 | func (g *CacheGzipCompressor) Decompress(data *bytes.Buffer) (*bytes.Buffer, error) { 118 | return GzipDecompress(data.Bytes()) 119 | } 120 | func (g *CacheGzipCompressor) GetEncoding() string { 121 | return elton.Gzip 122 | } 123 | func (g *CacheGzipCompressor) IsValid(contentType string, length int) bool { 124 | return isValidForCompress(g.ContentRegexp, g.MinLength, contentType, length) 125 | } 126 | func (g *CacheGzipCompressor) Compress(buffer *bytes.Buffer) (*bytes.Buffer, CompressionType, error) { 127 | data, err := GzipCompress(buffer.Bytes(), g.Level) 128 | if err != nil { 129 | return nil, CompressionNone, err 130 | } 131 | return data, g.GetCompression(), nil 132 | } 133 | func (g *CacheGzipCompressor) GetCompression() CompressionType { 134 | return CompressionGzip 135 | } 136 | 137 | type CacheZstdCompressor struct { 138 | Level int 139 | MinLength int 140 | ContentRegexp *regexp.Regexp 141 | } 142 | 143 | func NewCacheZstdCompressor() *CacheZstdCompressor { 144 | return &CacheZstdCompressor{ 145 | Level: int(zstd.SpeedBetterCompression), 146 | } 147 | } 148 | func (z *CacheZstdCompressor) Decompress(data *bytes.Buffer) (*bytes.Buffer, error) { 149 | return ZstdDecompress(data.Bytes()) 150 | } 151 | func (z *CacheZstdCompressor) GetEncoding() string { 152 | return elton.Zstd 153 | } 154 | func (z *CacheZstdCompressor) IsValid(contentType string, length int) bool { 155 | return isValidForCompress(z.ContentRegexp, z.MinLength, contentType, length) 156 | } 157 | func (z *CacheZstdCompressor) Compress(buffer *bytes.Buffer) (*bytes.Buffer, CompressionType, error) { 158 | data, err := ZstdCompress(buffer.Bytes(), z.Level) 159 | if err != nil { 160 | return nil, CompressionNone, err 161 | } 162 | return data, z.GetCompression(), nil 163 | } 164 | func (z *CacheZstdCompressor) GetCompression() CompressionType { 165 | return CompressionZstd 166 | } 167 | -------------------------------------------------------------------------------- /middleware/logger_test.go: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2020 Tree Xie 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | package middleware 24 | 25 | import ( 26 | "bytes" 27 | "net/http" 28 | "net/http/httptest" 29 | "os" 30 | "strings" 31 | "testing" 32 | "time" 33 | 34 | "github.com/stretchr/testify/assert" 35 | "github.com/vicanso/elton" 36 | ) 37 | 38 | func TestGetHumanReadableSize(t *testing.T) { 39 | assert := assert.New(t) 40 | assert.Equal("1MB", getHumanReadableSize(1024*1024)) 41 | assert.Equal("1.49MB", getHumanReadableSize(1024*1024+500*1024)) 42 | assert.Equal("1KB", getHumanReadableSize(1024)) 43 | assert.Equal("1.49KB", getHumanReadableSize(1024+500)) 44 | assert.Equal("500B", getHumanReadableSize(500)) 45 | } 46 | 47 | func TestLogger(t *testing.T) { 48 | assert := assert.New(t) 49 | t.Run("normal", func(t *testing.T) { 50 | _ = os.Setenv("__LOGGER__", "LOGGER") 51 | config := LoggerConfig{ 52 | DefaultFill: "-", 53 | Format: "{host} {remote} {real-ip} {client-ip} {method} {path} {proto} {query} {scheme} {uri} {referer} {userAgent} {size} {size-human} {status} {payload-size} {payload-size-human} {X-Token} {