first commit
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 andoma
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,26 @@
|
||||
# gin contrib
|
||||
|
||||
gin contrib is middleware utilities for [Gin](https://gitverse.ru/andoma/gin)
|
||||
|
||||
[](https://godoc.org/gitverse.ru/andoma/gin-contrib)
|
||||
[](https://goreportcard.com/report/gitverse.ru/andoma/gin-contrib)
|
||||
|
||||
## Usage
|
||||
|
||||
### Installation
|
||||
|
||||
Use go get.
|
||||
|
||||
```bash
|
||||
go get gitverse.ru/andoma/gin-contrib
|
||||
```
|
||||
|
||||
Then import the package into your own code.
|
||||
|
||||
```go
|
||||
import "gitverse.ru/andoma/gin-contrib"
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is under MIT License. See the [LICENSE](LICENSE) file for the full license text.
|
||||
@@ -0,0 +1,38 @@
|
||||
module gitverse.ru/andoma/gin-contrib/examples
|
||||
|
||||
go 1.21.5
|
||||
|
||||
replace gitverse.ru/andoma/gin-contrib => ../
|
||||
|
||||
require (
|
||||
gitverse.ru/andoma/gin-contrib v0.0.0-00010101000000-000000000000
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.10.2 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.18.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.7.0 // indirect
|
||||
golang.org/x/crypto v0.19.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
"gitverse.ru/andoma/gin-contrib/gzip"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
r.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix()))
|
||||
})
|
||||
|
||||
// Listen and Server in 0.0.0.0:8080
|
||||
if err := r.Run(":8080"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"gitverse.ru/andoma/gin"
|
||||
"gitverse.ru/andoma/gin-contrib/nocache"
|
||||
)
|
||||
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
router.Use(nocache.NoCache())
|
||||
router.Run(":8080")
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
"gitverse.ru/andoma/gin-contrib/pprof"
|
||||
)
|
||||
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
adminGroup := router.Group("/admin", func(c *gin.Context) {
|
||||
if c.Request.Header.Get("Authorization") != "foobar" {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
pprof.RouteRegister(adminGroup, "pprof")
|
||||
router.Run(":8080")
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"gitverse.ru/andoma/gin"
|
||||
"gitverse.ru/andoma/gin-contrib/pprof"
|
||||
)
|
||||
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
pprof.Register(router)
|
||||
router.Run(":8080")
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
"gitverse.ru/andoma/gin-contrib/requestid"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.New()
|
||||
|
||||
r.Use(requestid.New())
|
||||
|
||||
// Example ping request.
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix()))
|
||||
})
|
||||
|
||||
// Example / request.
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "id:"+requestid.Get(c))
|
||||
})
|
||||
|
||||
// Listen and Server in 0.0.0.0:8080
|
||||
if err := r.Run(":8080"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
"gitverse.ru/andoma/gin-contrib/requestid"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.New()
|
||||
|
||||
r.Use(
|
||||
requestid.New(
|
||||
requestid.WithGenerator(func() string {
|
||||
return "test"
|
||||
}),
|
||||
requestid.WithCustomHeaderStrKey("your-customer-key"),
|
||||
requestid.WithHandler(func(c *gin.Context, requestID string) {
|
||||
log.Printf("RequestID: %s", requestID)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
// Example ping request.
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix()))
|
||||
})
|
||||
|
||||
// Example / request.
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "id:"+requestid.Get(c))
|
||||
})
|
||||
|
||||
// Listen and Server in 0.0.0.0:8080
|
||||
if err := r.Run(":8080"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
"gitverse.ru/andoma/gin-contrib/secure"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
r.Use(secure.Secure(secure.Options{
|
||||
AllowedHosts: []string{"example.com", "ssl.example.com"},
|
||||
SSLRedirect: true,
|
||||
SSLHost: "ssl.example.com",
|
||||
SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"},
|
||||
STSSeconds: 315360000,
|
||||
STSIncludeSubdomains: true,
|
||||
FrameDeny: true,
|
||||
ContentTypeNosniff: true,
|
||||
BrowserXssFilter: true,
|
||||
ContentSecurityPolicy: "default-src 'self'",
|
||||
}))
|
||||
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.String(200, "pong "+fmt.Sprint(time.Now().Unix()))
|
||||
})
|
||||
|
||||
// Listen and Server in 0.0.0.0:8080
|
||||
r.Run(":8080")
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
"gitverse.ru/andoma/gin-contrib/static"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
// if Allow DirectoryIndex
|
||||
// r.Use(static.Serve("/", static.LocalFile("/tmp", true)))
|
||||
// set prefix
|
||||
// r.Use(static.Serve("/static", static.LocalFile("/tmp", true)))
|
||||
|
||||
r.Use(static.Serve("/", static.LocalFile("/tmp", false)))
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.String(200, "test")
|
||||
})
|
||||
// Listen and Server in 0.0.0.0:8080
|
||||
if err := r.Run(":8080"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
"gitverse.ru/andoma/gin-contrib/timeout"
|
||||
)
|
||||
|
||||
func emptySuccessResponse(c *gin.Context) {
|
||||
time.Sleep(200 * time.Microsecond)
|
||||
c.String(http.StatusOK, "")
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.New()
|
||||
|
||||
r.GET("/", timeout.New(
|
||||
timeout.WithTimeout(100*time.Microsecond),
|
||||
timeout.WithHandler(emptySuccessResponse),
|
||||
))
|
||||
|
||||
// Listen and Server in 0.0.0.0:8080
|
||||
if err := r.Run(":8080"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
"gitverse.ru/andoma/gin-contrib/timeout"
|
||||
)
|
||||
|
||||
func testResponse(c *gin.Context) {
|
||||
c.String(http.StatusRequestTimeout, "timeout")
|
||||
}
|
||||
|
||||
func timeoutMiddleware() gin.HandlerFunc {
|
||||
return timeout.New(
|
||||
timeout.WithTimeout(500*time.Millisecond),
|
||||
timeout.WithHandler(func(c *gin.Context) {
|
||||
c.Next()
|
||||
}),
|
||||
timeout.WithResponse(testResponse),
|
||||
)
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.New()
|
||||
r.Use(timeoutMiddleware())
|
||||
r.GET("/slow", func(c *gin.Context) {
|
||||
time.Sleep(800 * time.Millisecond)
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
if err := r.Run(":8080"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
module gitverse.ru/andoma/gin-contrib
|
||||
|
||||
go 1.21.5
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
gitverse.ru/andoma/gin v0.0.0-20240329081234-0c04899a8413
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.11.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.18.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.7.0 // indirect
|
||||
golang.org/x/crypto v0.19.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace gitverse.ru/andoma/gin-contrib/static => ./static
|
||||
@@ -0,0 +1,93 @@
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||
github.com/bytedance/sonic v1.11.0 h1:FwNNv6Vu4z2Onf1++LNzxB/QhitD8wuTdpZzMTGITWo=
|
||||
github.com/bytedance/sonic v1.11.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
|
||||
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U=
|
||||
github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
gitverse.ru/andoma/gin v0.0.0-20240329081234-0c04899a8413 h1:enDogauF7weHy3UV3sIw7kaSWqH7p2Jg+oqlc1XKLO4=
|
||||
gitverse.ru/andoma/gin v0.0.0-20240329081234-0c04899a8413/go.mod h1:QJKH9K4t9bNSV2Y4vbqgmHm8lqK8Bm3coDzeM0Qjwgk=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
|
||||
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Gin-Gonic
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
# GZIP gin's middleware
|
||||
|
||||
Gin middleware to enable `GZIP` support.
|
||||
|
||||
Copied from [gin-contrib/gzip](https://github.com/gin-contrib/gzip)
|
||||
|
||||
## Usage
|
||||
|
||||
Canonical example:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitverse.ru/andoma/gin-contrib/gzip"
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
r.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix()))
|
||||
})
|
||||
|
||||
// Listen and Server in 0.0.0.0:8080
|
||||
if err := r.Run(":8080"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Customized Excluded Extensions
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitverse.ru/andoma/gin-contrib/gzip"
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
r.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedExtensions([]string{".pdf", ".mp4"})))
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix()))
|
||||
})
|
||||
|
||||
// Listen and Server in 0.0.0.0:8080
|
||||
if err := r.Run(":8080"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Customized Excluded Paths
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitverse.ru/andoma/gin-contrib/gzip"
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
r.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{"/api/"})))
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix()))
|
||||
})
|
||||
|
||||
// Listen and Server in 0.0.0.0:8080
|
||||
if err := r.Run(":8080"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Customized Excluded Paths
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitverse.ru/andoma/gin-contrib/gzip"
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
r.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPathsRegexs([]string{".*"})))
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix()))
|
||||
})
|
||||
|
||||
// Listen and Server in 0.0.0.0:8080
|
||||
if err := r.Run(":8080"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
BestCompression = gzip.BestCompression
|
||||
BestSpeed = gzip.BestSpeed
|
||||
DefaultCompression = gzip.DefaultCompression
|
||||
NoCompression = gzip.NoCompression
|
||||
)
|
||||
|
||||
func Gzip(level int, options ...Option) gin.HandlerFunc {
|
||||
return newGzipHandler(level, options...).Handle
|
||||
}
|
||||
|
||||
type gzipWriter struct {
|
||||
gin.ResponseWriter
|
||||
writer *gzip.Writer
|
||||
}
|
||||
|
||||
func (g *gzipWriter) WriteString(s string) (int, error) {
|
||||
g.Header().Del("Content-Length")
|
||||
return g.writer.Write([]byte(s))
|
||||
}
|
||||
|
||||
func (g *gzipWriter) Write(data []byte) (int, error) {
|
||||
g.Header().Del("Content-Length")
|
||||
return g.writer.Write(data)
|
||||
}
|
||||
|
||||
// Fix: https://github.com/mholt/caddy/issues/38
|
||||
func (g *gzipWriter) WriteHeader(code int) {
|
||||
g.Header().Del("Content-Length")
|
||||
g.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
testResponse = "Gzip Test Response "
|
||||
testReverseResponse = "Gzip Test Reverse Response "
|
||||
)
|
||||
|
||||
type rServer struct{}
|
||||
|
||||
func (s *rServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
fmt.Fprint(rw, testReverseResponse)
|
||||
}
|
||||
|
||||
type closeNotifyingRecorder struct {
|
||||
*httptest.ResponseRecorder
|
||||
closed chan bool
|
||||
}
|
||||
|
||||
func newCloseNotifyingRecorder() *closeNotifyingRecorder {
|
||||
return &closeNotifyingRecorder{
|
||||
httptest.NewRecorder(),
|
||||
make(chan bool, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *closeNotifyingRecorder) CloseNotify() <-chan bool {
|
||||
return c.closed
|
||||
}
|
||||
|
||||
func newServer() *gin.Engine {
|
||||
// init reverse proxy server
|
||||
rServer := httptest.NewServer(new(rServer))
|
||||
target, _ := url.Parse(rServer.URL)
|
||||
rp := httputil.NewSingleHostReverseProxy(target)
|
||||
|
||||
router := gin.New()
|
||||
router.Use(Gzip(DefaultCompression))
|
||||
router.GET("/", func(c *gin.Context) {
|
||||
c.Header("Content-Length", strconv.Itoa(len(testResponse)))
|
||||
c.String(200, testResponse)
|
||||
})
|
||||
router.Any("/reverse", func(c *gin.Context) {
|
||||
rp.ServeHTTP(c.Writer, c.Request)
|
||||
})
|
||||
return router
|
||||
}
|
||||
|
||||
func TestGzip(t *testing.T) {
|
||||
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
|
||||
req.Header.Add("Accept-Encoding", "gzip")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := newServer()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, w.Code, 200)
|
||||
assert.Equal(t, w.Header().Get("Content-Encoding"), "gzip")
|
||||
assert.Equal(t, w.Header().Get("Vary"), "Accept-Encoding")
|
||||
assert.NotEqual(t, w.Header().Get("Content-Length"), "0")
|
||||
assert.NotEqual(t, w.Body.Len(), 19)
|
||||
assert.Equal(t, fmt.Sprint(w.Body.Len()), w.Header().Get("Content-Length"))
|
||||
|
||||
gr, err := gzip.NewReader(w.Body)
|
||||
assert.NoError(t, err)
|
||||
defer gr.Close()
|
||||
|
||||
body, _ := io.ReadAll(gr)
|
||||
assert.Equal(t, string(body), testResponse)
|
||||
}
|
||||
|
||||
func TestGzipPNG(t *testing.T) {
|
||||
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/image.png", nil)
|
||||
req.Header.Add("Accept-Encoding", "gzip")
|
||||
|
||||
router := gin.New()
|
||||
router.Use(Gzip(DefaultCompression))
|
||||
router.GET("/image.png", func(c *gin.Context) {
|
||||
c.String(200, "this is a PNG!")
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, w.Code, 200)
|
||||
assert.Equal(t, w.Header().Get("Content-Encoding"), "")
|
||||
assert.Equal(t, w.Header().Get("Vary"), "")
|
||||
assert.Equal(t, w.Body.String(), "this is a PNG!")
|
||||
}
|
||||
|
||||
func TestExcludedExtensions(t *testing.T) {
|
||||
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/index.html", nil)
|
||||
req.Header.Add("Accept-Encoding", "gzip")
|
||||
|
||||
router := gin.New()
|
||||
router.Use(Gzip(DefaultCompression, WithExcludedExtensions([]string{".html"})))
|
||||
router.GET("/index.html", func(c *gin.Context) {
|
||||
c.String(200, "this is a HTML!")
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "", w.Header().Get("Content-Encoding"))
|
||||
assert.Equal(t, "", w.Header().Get("Vary"))
|
||||
assert.Equal(t, "this is a HTML!", w.Body.String())
|
||||
assert.Equal(t, "", w.Header().Get("Content-Length"))
|
||||
}
|
||||
|
||||
func TestExcludedPaths(t *testing.T) {
|
||||
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/api/books", nil)
|
||||
req.Header.Add("Accept-Encoding", "gzip")
|
||||
|
||||
router := gin.New()
|
||||
router.Use(Gzip(DefaultCompression, WithExcludedPaths([]string{"/api/"})))
|
||||
router.GET("/api/books", func(c *gin.Context) {
|
||||
c.String(200, "this is books!")
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "", w.Header().Get("Content-Encoding"))
|
||||
assert.Equal(t, "", w.Header().Get("Vary"))
|
||||
assert.Equal(t, "this is books!", w.Body.String())
|
||||
assert.Equal(t, "", w.Header().Get("Content-Length"))
|
||||
}
|
||||
|
||||
func TestNoGzip(t *testing.T) {
|
||||
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := newServer()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, w.Code, 200)
|
||||
assert.Equal(t, w.Header().Get("Content-Encoding"), "")
|
||||
assert.Equal(t, w.Header().Get("Content-Length"), "19")
|
||||
assert.Equal(t, w.Body.String(), testResponse)
|
||||
}
|
||||
|
||||
func TestGzipWithReverseProxy(t *testing.T) {
|
||||
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/reverse", nil)
|
||||
req.Header.Add("Accept-Encoding", "gzip")
|
||||
|
||||
w := newCloseNotifyingRecorder()
|
||||
r := newServer()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, w.Code, 200)
|
||||
assert.Equal(t, w.Header().Get("Content-Encoding"), "gzip")
|
||||
assert.Equal(t, w.Header().Get("Vary"), "Accept-Encoding")
|
||||
assert.NotEqual(t, w.Header().Get("Content-Length"), "0")
|
||||
assert.NotEqual(t, w.Body.Len(), 19)
|
||||
assert.Equal(t, fmt.Sprint(w.Body.Len()), w.Header().Get("Content-Length"))
|
||||
|
||||
gr, err := gzip.NewReader(w.Body)
|
||||
assert.NoError(t, err)
|
||||
defer gr.Close()
|
||||
|
||||
body, _ := io.ReadAll(gr)
|
||||
assert.Equal(t, string(body), testReverseResponse)
|
||||
}
|
||||
|
||||
func TestDecompressGzip(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
gz, _ := gzip.NewWriterLevel(buf, gzip.DefaultCompression)
|
||||
if _, err := gz.Write([]byte(testResponse)); err != nil {
|
||||
gz.Close()
|
||||
t.Fatal(err)
|
||||
}
|
||||
gz.Close()
|
||||
|
||||
req, _ := http.NewRequestWithContext(context.Background(), "POST", "/", buf)
|
||||
req.Header.Add("Content-Encoding", "gzip")
|
||||
|
||||
router := gin.New()
|
||||
router.Use(Gzip(DefaultCompression, WithDecompressFn(DefaultDecompressHandle)))
|
||||
router.POST("/", func(c *gin.Context) {
|
||||
if v := c.Request.Header.Get("Content-Encoding"); v != "" {
|
||||
t.Errorf("unexpected `Content-Encoding`: %s header", v)
|
||||
}
|
||||
if v := c.Request.Header.Get("Content-Length"); v != "" {
|
||||
t.Errorf("unexpected `Content-Length`: %s header", v)
|
||||
}
|
||||
data, err := c.GetRawData()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.Data(200, "text/plain", data)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "", w.Header().Get("Content-Encoding"))
|
||||
assert.Equal(t, "", w.Header().Get("Vary"))
|
||||
assert.Equal(t, testResponse, w.Body.String())
|
||||
assert.Equal(t, "", w.Header().Get("Content-Length"))
|
||||
}
|
||||
|
||||
func TestDecompressGzipWithEmptyBody(t *testing.T) {
|
||||
req, _ := http.NewRequestWithContext(context.Background(), "POST", "/", nil)
|
||||
req.Header.Add("Content-Encoding", "gzip")
|
||||
|
||||
router := gin.New()
|
||||
router.Use(Gzip(DefaultCompression, WithDecompressFn(DefaultDecompressHandle)))
|
||||
router.POST("/", func(c *gin.Context) {
|
||||
c.String(200, "ok")
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "", w.Header().Get("Content-Encoding"))
|
||||
assert.Equal(t, "", w.Header().Get("Vary"))
|
||||
assert.Equal(t, "ok", w.Body.String())
|
||||
assert.Equal(t, "", w.Header().Get("Content-Length"))
|
||||
}
|
||||
|
||||
func TestDecompressGzipWithIncorrectData(t *testing.T) {
|
||||
req, _ := http.NewRequestWithContext(context.Background(), "POST", "/", bytes.NewReader([]byte(testResponse)))
|
||||
req.Header.Add("Content-Encoding", "gzip")
|
||||
|
||||
router := gin.New()
|
||||
router.Use(Gzip(DefaultCompression, WithDecompressFn(DefaultDecompressHandle)))
|
||||
router.POST("/", func(c *gin.Context) {
|
||||
c.String(200, "ok")
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
type gzipHandler struct {
|
||||
*Options
|
||||
gzPool sync.Pool
|
||||
}
|
||||
|
||||
func newGzipHandler(level int, options ...Option) *gzipHandler {
|
||||
handler := &gzipHandler{
|
||||
Options: DefaultOptions,
|
||||
gzPool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
gz, err := gzip.NewWriterLevel(io.Discard, level)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return gz
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, setter := range options {
|
||||
setter(handler.Options)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
func (g *gzipHandler) Handle(c *gin.Context) {
|
||||
if fn := g.DecompressFn; fn != nil && c.Request.Header.Get("Content-Encoding") == "gzip" {
|
||||
fn(c)
|
||||
}
|
||||
|
||||
if !g.shouldCompress(c.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
gz := g.gzPool.Get().(*gzip.Writer)
|
||||
defer g.gzPool.Put(gz)
|
||||
defer gz.Reset(io.Discard)
|
||||
gz.Reset(c.Writer)
|
||||
|
||||
c.Header("Content-Encoding", "gzip")
|
||||
c.Header("Vary", "Accept-Encoding")
|
||||
c.Writer = &gzipWriter{c.Writer, gz}
|
||||
defer func() {
|
||||
gz.Close()
|
||||
c.Header("Content-Length", fmt.Sprint(c.Writer.Size()))
|
||||
}()
|
||||
c.Next()
|
||||
}
|
||||
|
||||
func (g *gzipHandler) shouldCompress(req *http.Request) bool {
|
||||
if !strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") ||
|
||||
strings.Contains(req.Header.Get("Connection"), "Upgrade") ||
|
||||
strings.Contains(req.Header.Get("Accept"), "text/event-stream") {
|
||||
return false
|
||||
}
|
||||
|
||||
extension := filepath.Ext(req.URL.Path)
|
||||
if g.ExcludedExtensions.Contains(extension) {
|
||||
return false
|
||||
}
|
||||
|
||||
if g.ExcludedPaths.Contains(req.URL.Path) {
|
||||
return false
|
||||
}
|
||||
if g.ExcludedPathesRegexs.Contains(req.URL.Path) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultExcludedExtentions = NewExcludedExtensions([]string{
|
||||
".png", ".gif", ".jpeg", ".jpg",
|
||||
})
|
||||
DefaultOptions = &Options{
|
||||
ExcludedExtensions: DefaultExcludedExtentions,
|
||||
}
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
ExcludedExtensions ExcludedExtensions
|
||||
ExcludedPaths ExcludedPaths
|
||||
ExcludedPathesRegexs ExcludedPathesRegexs
|
||||
DecompressFn func(c *gin.Context)
|
||||
}
|
||||
|
||||
type Option func(*Options)
|
||||
|
||||
func WithExcludedExtensions(args []string) Option {
|
||||
return func(o *Options) {
|
||||
o.ExcludedExtensions = NewExcludedExtensions(args)
|
||||
}
|
||||
}
|
||||
|
||||
func WithExcludedPaths(args []string) Option {
|
||||
return func(o *Options) {
|
||||
o.ExcludedPaths = NewExcludedPaths(args)
|
||||
}
|
||||
}
|
||||
|
||||
func WithExcludedPathsRegexs(args []string) Option {
|
||||
return func(o *Options) {
|
||||
o.ExcludedPathesRegexs = NewExcludedPathesRegexs(args)
|
||||
}
|
||||
}
|
||||
|
||||
func WithDecompressFn(decompressFn func(c *gin.Context)) Option {
|
||||
return func(o *Options) {
|
||||
o.DecompressFn = decompressFn
|
||||
}
|
||||
}
|
||||
|
||||
// Using map for better lookup performance
|
||||
type ExcludedExtensions map[string]bool
|
||||
|
||||
func NewExcludedExtensions(extensions []string) ExcludedExtensions {
|
||||
res := make(ExcludedExtensions)
|
||||
for _, e := range extensions {
|
||||
res[e] = true
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (e ExcludedExtensions) Contains(target string) bool {
|
||||
_, ok := e[target]
|
||||
return ok
|
||||
}
|
||||
|
||||
type ExcludedPaths []string
|
||||
|
||||
func NewExcludedPaths(paths []string) ExcludedPaths {
|
||||
return ExcludedPaths(paths)
|
||||
}
|
||||
|
||||
func (e ExcludedPaths) Contains(requestURI string) bool {
|
||||
for _, path := range e {
|
||||
if strings.HasPrefix(requestURI, path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ExcludedPathesRegexs []*regexp.Regexp
|
||||
|
||||
func NewExcludedPathesRegexs(regexs []string) ExcludedPathesRegexs {
|
||||
result := make([]*regexp.Regexp, len(regexs))
|
||||
for i, reg := range regexs {
|
||||
result[i] = regexp.MustCompile(reg)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (e ExcludedPathesRegexs) Contains(requestURI string) bool {
|
||||
for _, reg := range e {
|
||||
if reg.MatchString(requestURI) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func DefaultDecompressHandle(c *gin.Context) {
|
||||
if c.Request.Body == nil {
|
||||
return
|
||||
}
|
||||
r, err := gzip.NewReader(c.Request.Body)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
c.Request.Header.Del("Content-Encoding")
|
||||
c.Request.Header.Del("Content-Length")
|
||||
c.Request.Body = r
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Buwei Chiu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Gin Access Limit Middleware
|
||||
|
||||
Based on [bu/gin-access-limit](https://github.com/bu/gin-access-limit)
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
gin "gitverse.ru/andoma/gin"
|
||||
"gitverse.ru/andoma/gin-contrib/limit"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// create a Gin engine
|
||||
r := gin.Default()
|
||||
|
||||
// this API is only accessible from Docker containers
|
||||
r.Use(limit.New("172.18.0.0/16"))
|
||||
|
||||
// if need to specify serveral range of allowed sources, use comma to concatenate them
|
||||
// r.Use(limit.New("172.18.0.0/16, 127.0.0.1/32"))
|
||||
|
||||
// routes
|
||||
r.GET("/", func (c *gin.Context) {
|
||||
c.String(200, "pong")
|
||||
})
|
||||
|
||||
// listen to request
|
||||
r.Run(":8080")
|
||||
}
|
||||
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
package limit
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
// CIDR is a middleware that check given CIDR rules and return 403 Forbidden when user is not coming from allowed source.
|
||||
// CIDRs accepts a list of CIDRs, separated by comma. (e.g. 127.0.0.1/32, ::1/128 )
|
||||
func New(CIDRs string, opts ...Option) gin.HandlerFunc {
|
||||
|
||||
l := &Limit{
|
||||
handler: func(c *gin.Context, remoteAddr, CIDRs string) bool {
|
||||
log.Println("[LIMIT] Request from [" + remoteAddr + "] is not allow to access `" + c.Request.RequestURI + "`, only allow from: [" + CIDRs + "]")
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(l)
|
||||
}
|
||||
|
||||
return func(c *gin.Context) {
|
||||
remoteAddr := c.ClientIP()
|
||||
|
||||
// parse it into IP type
|
||||
remoteIP := net.ParseIP(remoteAddr)
|
||||
|
||||
// split CIDRs by comma, and we are going check them one by one
|
||||
cidrSlices := strings.Split(CIDRs, ",")
|
||||
|
||||
// under of CIDR we were in
|
||||
var matchCount uint
|
||||
|
||||
// go over each CIDR and do the tests
|
||||
for _, cidr := range cidrSlices {
|
||||
// remove unwanted spaces
|
||||
cidr = strings.TrimSpace(cidr)
|
||||
|
||||
// try to parse the CIDR
|
||||
_, cidrIPNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
|
||||
// This is the core of this middleware, it ask current CIDR network range to test if current IP is in
|
||||
if cidrIPNet.Contains(remoteIP) {
|
||||
matchCount = matchCount + 1
|
||||
}
|
||||
}
|
||||
|
||||
// if no CIDR ranges contains our IP
|
||||
if matchCount == 0 && l.handler(c, remoteAddr, CIDRs) {
|
||||
c.AbortWithStatus(403)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package limit
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
func setupRouter(CIDRs string) *gin.Engine {
|
||||
// no debug mode
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
// create a default
|
||||
r := gin.Default()
|
||||
|
||||
// our middle-ware
|
||||
r.Use(New(CIDRs))
|
||||
|
||||
// routes
|
||||
r.GET("/", testGET)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func TestAllowAccessSource(t *testing.T) {
|
||||
r := setupRouter("127.0.0.1/32")
|
||||
|
||||
// prepare
|
||||
ExpectedResponseStatus := 200
|
||||
|
||||
// run
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = "127.0.0.1:80"
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
// check
|
||||
assert.Equal(t, ExpectedResponseStatus, w.Code)
|
||||
}
|
||||
|
||||
func TestNotAllowAccessSource(t *testing.T) {
|
||||
r := setupRouter("172.18.0.0/16")
|
||||
|
||||
// prepare
|
||||
ExpectedResponseStatus := 403
|
||||
|
||||
// run
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = "127.0.0.1:80"
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
// check
|
||||
assert.Equal(t, ExpectedResponseStatus, w.Code)
|
||||
}
|
||||
|
||||
func TestAllowAccessFromManySource(t *testing.T) {
|
||||
r := setupRouter("172.18.0.0/16, 127.0.0.1/32, ::1/128")
|
||||
|
||||
// prepare
|
||||
ExpectedResponseStatus := 200
|
||||
|
||||
// run
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = "127.0.0.1:80"
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
// check
|
||||
assert.Equal(t, ExpectedResponseStatus, w.Code)
|
||||
}
|
||||
|
||||
func TestNotAllowAccessFromManySource(t *testing.T) {
|
||||
r := setupRouter("172.18.0.0/16, 127.0.0.1/32, ::1/128")
|
||||
|
||||
// prepare
|
||||
ExpectedResponseStatus := 403
|
||||
|
||||
// run
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = "192.168.1.12:80"
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
// check
|
||||
assert.Equal(t, ExpectedResponseStatus, w.Code)
|
||||
}
|
||||
|
||||
func testGET(c *gin.Context) {
|
||||
c.String(200, "pong")
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package limit
|
||||
|
||||
import "gitverse.ru/andoma/gin"
|
||||
|
||||
type Handler func(c *gin.Context, remoteAddr, CIDRs string) bool
|
||||
|
||||
type Limit struct {
|
||||
handler Handler
|
||||
}
|
||||
|
||||
type Option func(*Limit)
|
||||
|
||||
// WithHandler
|
||||
func WithHandler(handler Handler) Option {
|
||||
return func(limit *Limit) {
|
||||
limit.handler = handler
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 thinkgo
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,56 @@
|
||||
package nocache
|
||||
|
||||
// Ported from Goji's middleware, source:
|
||||
// https://github.com/zenazn/goji/tree/master/web/middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
// Unix epoch time
|
||||
var epoch = time.Unix(0, 0).UTC().Format(http.TimeFormat)
|
||||
|
||||
// Taken from https://github.com/mytrile/nocache
|
||||
var noCacheHeaders = map[string]string{
|
||||
"Expires": epoch,
|
||||
"Cache-Control": "no-cache, no-store, no-transform, must-revalidate, private, max-age=0",
|
||||
"Pragma": "no-cache",
|
||||
"X-Accel-Expires": "0",
|
||||
}
|
||||
|
||||
var etagHeaders = []string{
|
||||
"ETag",
|
||||
"If-Modified-Since",
|
||||
"If-Match",
|
||||
"If-None-Match",
|
||||
"If-Range",
|
||||
"If-Unmodified-Since",
|
||||
}
|
||||
|
||||
// NoCache is a simple piece of middleware that sets a number of HTTP headers to prevent
|
||||
// a router (or subrouter) from being cached by an upstream proxy and/or client.
|
||||
//
|
||||
// As per http://wiki.nginx.org/HttpProxyModule - NoCache sets:
|
||||
//
|
||||
// Expires: Thu, 01 Jan 1970 00:00:00 UTC
|
||||
// Cache-Control: no-cache, private, max-age=0
|
||||
// X-Accel-Expires: 0
|
||||
// Pragma: no-cache (for HTTP/1.0 proxies/clients)
|
||||
func NoCache() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Delete any ETag headers that may have been set
|
||||
for _, v := range etagHeaders {
|
||||
if c.Request.Header.Get(v) != "" {
|
||||
c.Request.Header.Del(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Set our NoCache headers
|
||||
for k, v := range noCacheHeaders {
|
||||
c.Writer.Header().Set(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package nocache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
func Test_NoCacheHeaders(t *testing.T) {
|
||||
responseHeaders := map[string]string{
|
||||
"Cache-Control": "no-cache, no-store, no-transform, must-revalidate, private, max-age=0",
|
||||
"Pragma": "no-cache",
|
||||
"Expires": time.Unix(0, 0).UTC().Format(http.TimeFormat),
|
||||
}
|
||||
|
||||
m := gin.New()
|
||||
m.Use(NoCache())
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
r, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
|
||||
for _, k := range etagHeaders {
|
||||
r.Header.Add(k, "value")
|
||||
}
|
||||
m.ServeHTTP(recorder, r)
|
||||
|
||||
for key, value := range responseHeaders {
|
||||
if recorder.Header()[key][0] != value {
|
||||
t.Errorf("Missing header: %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Gin-Gonic
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,98 @@
|
||||
# pprof
|
||||
|
||||
gin pprof middleware
|
||||
|
||||
> Package pprof serves via its HTTP server runtime profiling data in the format expected by the pprof visualization tool.
|
||||
|
||||
Copied from [gin-contrib/pprof](https://github.com/gin-contrib/pprof)
|
||||
|
||||
## Usage
|
||||
|
||||
### Start using it
|
||||
|
||||
Import it in your code:
|
||||
|
||||
```go
|
||||
import "gitverse.ru/andoma/gin-contrib/pprof"
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"gitverse.ru/andoma/gin-contrib/pprof"
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
pprof.Register(router)
|
||||
router.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### change default path prefix
|
||||
|
||||
```go
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
// default is "debug/pprof"
|
||||
pprof.Register(router, "dev/pprof")
|
||||
router.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### custom router group
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitverse.ru/andoma/gin-contrib/pprof"
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
adminGroup := router.Group("/admin", func(c *gin.Context) {
|
||||
if c.Request.Header.Get("Authorization") != "foobar" {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
pprof.RouteRegister(adminGroup, "pprof")
|
||||
router.Run(":8080")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Use the pprof tool
|
||||
|
||||
Then use the pprof tool to look at the heap profile:
|
||||
|
||||
```bash
|
||||
go tool pprof http://localhost:8080/debug/pprof/heap
|
||||
```
|
||||
|
||||
Or to look at a 30-second CPU profile:
|
||||
|
||||
```bash
|
||||
go tool pprof http://localhost:8080/debug/pprof/profile
|
||||
```
|
||||
|
||||
Or to look at the goroutine blocking profile, after calling runtime.SetBlockProfileRate in your program:
|
||||
|
||||
```bash
|
||||
go tool pprof http://localhost:8080/debug/pprof/block
|
||||
```
|
||||
|
||||
Or to collect a 5-second execution trace:
|
||||
|
||||
```bash
|
||||
wget http://localhost:8080/debug/pprof/trace?seconds=5
|
||||
```
|
||||
@@ -0,0 +1,50 @@
|
||||
package pprof
|
||||
|
||||
import (
|
||||
"net/http/pprof"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultPrefix url prefix of pprof
|
||||
DefaultPrefix = "/debug/pprof"
|
||||
)
|
||||
|
||||
func getPrefix(prefixOptions ...string) string {
|
||||
prefix := DefaultPrefix
|
||||
if len(prefixOptions) > 0 {
|
||||
prefix = prefixOptions[0]
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
|
||||
// Register the standard HandlerFuncs from the net/http/pprof package with
|
||||
// the provided gin.Engine. prefixOptions is a optional. If not prefixOptions,
|
||||
// the default path prefix is used, otherwise first prefixOptions will be path prefix.
|
||||
func Register(r *gin.Engine, prefixOptions ...string) {
|
||||
RouteRegister(&(r.RouterGroup), prefixOptions...)
|
||||
}
|
||||
|
||||
// RouteRegister the standard HandlerFuncs from the net/http/pprof package with
|
||||
// the provided gin.GrouterGroup. prefixOptions is a optional. If not prefixOptions,
|
||||
// the default path prefix is used, otherwise first prefixOptions will be path prefix.
|
||||
func RouteRegister(rg *gin.RouterGroup, prefixOptions ...string) {
|
||||
prefix := getPrefix(prefixOptions...)
|
||||
|
||||
prefixRouter := rg.Group(prefix)
|
||||
{
|
||||
prefixRouter.GET("/", gin.WrapF(pprof.Index))
|
||||
prefixRouter.GET("/cmdline", gin.WrapF(pprof.Cmdline))
|
||||
prefixRouter.GET("/profile", gin.WrapF(pprof.Profile))
|
||||
prefixRouter.POST("/symbol", gin.WrapF(pprof.Symbol))
|
||||
prefixRouter.GET("/symbol", gin.WrapF(pprof.Symbol))
|
||||
prefixRouter.GET("/trace", gin.WrapF(pprof.Trace))
|
||||
prefixRouter.GET("/allocs", gin.WrapH(pprof.Handler("allocs")))
|
||||
prefixRouter.GET("/block", gin.WrapH(pprof.Handler("block")))
|
||||
prefixRouter.GET("/goroutine", gin.WrapH(pprof.Handler("goroutine")))
|
||||
prefixRouter.GET("/heap", gin.WrapH(pprof.Handler("heap")))
|
||||
prefixRouter.GET("/mutex", gin.WrapH(pprof.Handler("mutex")))
|
||||
prefixRouter.GET("/threadcreate", gin.WrapH(pprof.Handler("threadcreate")))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package pprof
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
func Test_getPrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{"default value", nil, "/debug/pprof"},
|
||||
{"test user input value", []string{"test/pprof"}, "test/pprof"},
|
||||
{"test user input value", []string{"test/pprof", "pprof"}, "test/pprof"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := getPrefix(tt.args...); got != tt.want {
|
||||
t.Errorf("%q. getPrefix() = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterAndRouteRegister(t *testing.T) {
|
||||
bearerToken := "Bearer token"
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.New()
|
||||
Register(r)
|
||||
adminGroup := r.Group("/admin", func(c *gin.Context) {
|
||||
if c.Request.Header.Get("Authorization") != bearerToken {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
RouteRegister(adminGroup, "pprof")
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, "/debug/pprof/", nil)
|
||||
rw := httptest.NewRecorder()
|
||||
r.ServeHTTP(rw, req)
|
||||
|
||||
if expected, got := http.StatusOK, rw.Code; expected != got {
|
||||
t.Errorf("expected: %d, got: %d", expected, got)
|
||||
}
|
||||
|
||||
req, _ = http.NewRequest(http.MethodGet, "/admin/pprof/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
r.ServeHTTP(rw, req)
|
||||
|
||||
if expected, got := http.StatusForbidden, rw.Code; expected != got {
|
||||
t.Errorf("expected: %d, got: %d", expected, got)
|
||||
}
|
||||
|
||||
req, _ = http.NewRequest(http.MethodGet, "/admin/pprof/", nil)
|
||||
req.Header.Set("Authorization", bearerToken)
|
||||
rw = httptest.NewRecorder()
|
||||
r.ServeHTTP(rw, req)
|
||||
|
||||
if expected, got := http.StatusOK, rw.Code; expected != got {
|
||||
t.Errorf("expected: %d, got: %d", expected, got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Gin-Gonic
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,72 @@
|
||||
# RequestID
|
||||
|
||||
Request ID middleware for Gin Framework. Adds an indentifier to the response using the `X-Request-ID` header. Passes the `X-Request-ID` value back to the caller if it's sent in the request headers.
|
||||
|
||||
Copied from [gin-contrib/requestid](https://github.com/gin-contrib/requestid)
|
||||
|
||||
## Config
|
||||
|
||||
define your custom generator function:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
|
||||
r := gin.New()
|
||||
|
||||
r.Use(
|
||||
requestid.New(
|
||||
requestid.WithGenerator(func() string {
|
||||
return "test"
|
||||
}),
|
||||
requestid.WithCustomHeaderStrKey("your-customer-key"),
|
||||
),
|
||||
)
|
||||
|
||||
// Example ping request.
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix()))
|
||||
})
|
||||
|
||||
// Listen and Server in 0.0.0.0:8080
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitverse.ru/andoma/gin-contrib/requestid"
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
r := gin.New()
|
||||
|
||||
r.Use(requestid.New())
|
||||
|
||||
// Example ping request.
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix()))
|
||||
})
|
||||
|
||||
// Listen and Server in 0.0.0.0:8080
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
How to get the request identifier:
|
||||
|
||||
```go
|
||||
// Example / request.
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "id:"+requestid.Get(c))
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,36 @@
|
||||
package requestid
|
||||
|
||||
import (
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
// Option for queue system
|
||||
type Option func(*config)
|
||||
|
||||
type (
|
||||
Generator func() string
|
||||
Handler func(c *gin.Context, requestID string)
|
||||
)
|
||||
|
||||
type HeaderStrKey string
|
||||
|
||||
// WithGenerator set generator function
|
||||
func WithGenerator(g Generator) Option {
|
||||
return func(cfg *config) {
|
||||
cfg.generator = g
|
||||
}
|
||||
}
|
||||
|
||||
// WithCustomHeaderStrKey set custom header key for request id
|
||||
func WithCustomHeaderStrKey(s HeaderStrKey) Option {
|
||||
return func(cfg *config) {
|
||||
cfg.headerKey = s
|
||||
}
|
||||
}
|
||||
|
||||
// WithHandler set handler function for request id with context
|
||||
func WithHandler(handler Handler) Option {
|
||||
return func(cfg *config) {
|
||||
cfg.handler = handler
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package requestid
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
const defaultHeaderKey = "X-Request-ID"
|
||||
|
||||
var headerXRequestID string
|
||||
|
||||
// Config defines the config for RequestID middleware
|
||||
type config struct {
|
||||
// Generator defines a function to generate an ID.
|
||||
// Optional. Default: func() string {
|
||||
// return uuid.New().String()
|
||||
// }
|
||||
generator Generator
|
||||
headerKey HeaderStrKey
|
||||
handler Handler
|
||||
}
|
||||
|
||||
// New initializes the RequestID middleware.
|
||||
func New(opts ...Option) gin.HandlerFunc {
|
||||
cfg := &config{
|
||||
generator: func() string {
|
||||
return uuid.New().String()
|
||||
},
|
||||
headerKey: defaultHeaderKey,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(cfg)
|
||||
}
|
||||
|
||||
headerXRequestID = string(cfg.headerKey)
|
||||
|
||||
return func(c *gin.Context) {
|
||||
// Get id from request
|
||||
rid := c.GetHeader(headerXRequestID)
|
||||
if rid == "" {
|
||||
rid = cfg.generator()
|
||||
c.Request.Header.Add(headerXRequestID, rid)
|
||||
}
|
||||
if cfg.handler != nil {
|
||||
cfg.handler(c, rid)
|
||||
}
|
||||
// Set the id to ensure that the requestid is in the response
|
||||
c.Header(headerXRequestID, rid)
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns the request identifier
|
||||
func Get(c *gin.Context) string {
|
||||
return c.GetHeader(headerXRequestID)
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package requestid
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
const testXRequestID = "test-request-id"
|
||||
|
||||
func emptySuccessResponse(c *gin.Context) {
|
||||
c.String(http.StatusOK, "")
|
||||
}
|
||||
|
||||
func Test_RequestID_CreateNew(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.Use(New())
|
||||
r.GET("/", emptySuccessResponse)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.NotEmpty(t, w.Header().Get(headerXRequestID))
|
||||
}
|
||||
|
||||
func Test_RequestID_PassThru(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.Use(New())
|
||||
r.GET("/", emptySuccessResponse)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
|
||||
req.Header.Set(headerXRequestID, testXRequestID)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, testXRequestID, w.Header().Get(headerXRequestID))
|
||||
}
|
||||
|
||||
func TestRequestIDWithCustomID(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.Use(
|
||||
New(
|
||||
WithGenerator(func() string {
|
||||
return testXRequestID
|
||||
}),
|
||||
),
|
||||
)
|
||||
r.GET("/", emptySuccessResponse)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, testXRequestID, w.Header().Get(headerXRequestID))
|
||||
}
|
||||
|
||||
func TestRequestIDWithCustomHeaderKey(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.Use(
|
||||
New(
|
||||
WithCustomHeaderStrKey("customKey"),
|
||||
),
|
||||
)
|
||||
r.GET("/", emptySuccessResponse)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
|
||||
req.Header.Set("customKey", testXRequestID)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, testXRequestID, w.Header().Get("customKey"))
|
||||
}
|
||||
|
||||
func TestRequestIDWithHandler(t *testing.T) {
|
||||
r := gin.New()
|
||||
called := false
|
||||
r.Use(
|
||||
New(
|
||||
WithHandler(func(c *gin.Context, requestID string) {
|
||||
called = true
|
||||
assert.Equal(t, testXRequestID, requestID)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
|
||||
req.Header.Set("X-Request-ID", testXRequestID)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.True(t, called)
|
||||
}
|
||||
|
||||
func TestRequestIDIsAttachedToRequestHeaders(t *testing.T) {
|
||||
r := gin.New()
|
||||
|
||||
r.Use(New())
|
||||
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
result := c.GetHeader(defaultHeaderKey)
|
||||
assert.NotEmpty(t, result)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func TestRequestIDNotNilAfterGinCopy(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.Use(New())
|
||||
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
copy := c.Copy()
|
||||
result := Get(copy)
|
||||
assert.NotEmpty(t, result)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Gin-Gonic
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,5 @@
|
||||
# secure
|
||||
|
||||
gin secure middleware
|
||||
|
||||
Copied from [gin-gonic/contrib](https://github.com/gin-gonic/contrib)
|
||||
@@ -0,0 +1,179 @@
|
||||
package secure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
stsHeader = "Strict-Transport-Security"
|
||||
stsSubdomainString = "; includeSubdomains"
|
||||
frameOptionsHeader = "X-Frame-Options"
|
||||
frameOptionsValue = "DENY"
|
||||
contentTypeHeader = "X-Content-Type-Options"
|
||||
contentTypeValue = "nosniff"
|
||||
xssProtectionHeader = "X-XSS-Protection"
|
||||
xssProtectionValue = "1; mode=block"
|
||||
cspHeader = "Content-Security-Policy"
|
||||
)
|
||||
|
||||
func defaultBadHostHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Bad Host", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Options is a struct for specifying configuration options for the secure.Secure middleware.
|
||||
type Options struct {
|
||||
// AllowedHosts is a list of fully qualified domain names that are allowed. Default is empty list, which allows any and all host names.
|
||||
AllowedHosts []string
|
||||
// If SSLRedirect is set to true, then only allow https requests. Default is false.
|
||||
SSLRedirect bool
|
||||
// If SSLTemporaryRedirect is true, the a 302 will be used while redirecting. Default is false (301).
|
||||
SSLTemporaryRedirect bool
|
||||
// SSLHost is the host name that is used to redirect http requests to https. Default is "", which indicates to use the same host.
|
||||
SSLHost string
|
||||
// SSLProxyHeaders is set of header keys with associated values that would indicate a valid https request. Useful when using Nginx: `map[string]string{"X-Forwarded-Proto": "https"}`. Default is blank map.
|
||||
SSLProxyHeaders map[string]string
|
||||
// STSSeconds is the max-age of the Strict-Transport-Security header. Default is 0, which would NOT include the header.
|
||||
STSSeconds int64
|
||||
// If STSIncludeSubdomains is set to true, the `includeSubdomains` will be appended to the Strict-Transport-Security header. Default is false.
|
||||
STSIncludeSubdomains bool
|
||||
// If FrameDeny is set to true, adds the X-Frame-Options header with the value of `DENY`. Default is false.
|
||||
FrameDeny bool
|
||||
// CustomFrameOptionsValue allows the X-Frame-Options header value to be set with a custom value. This overrides the FrameDeny option.
|
||||
CustomFrameOptionsValue string
|
||||
// If ContentTypeNosniff is true, adds the X-Content-Type-Options header with the value `nosniff`. Default is false.
|
||||
ContentTypeNosniff bool
|
||||
// If BrowserXssFilter is true, adds the X-XSS-Protection header with the value `1; mode=block`. Default is false.
|
||||
BrowserXssFilter bool
|
||||
// ContentSecurityPolicy allows the Content-Security-Policy header value to be set with a custom value. Default is "".
|
||||
ContentSecurityPolicy string
|
||||
// When developing, the AllowedHosts, SSL, and STS options can cause some unwanted effects. Usually testing happens on http, not https, and on localhost, not your production domain... so set this to true for dev environment.
|
||||
// If you would like your development environment to mimic production with complete Host blocking, SSL redirects, and STS headers, leave this as false. Default if false.
|
||||
IsDevelopment bool
|
||||
|
||||
// Handlers for when an error occurs (ie bad host).
|
||||
BadHostHandler http.Handler
|
||||
}
|
||||
|
||||
// Secure is a middleware that helps setup a few basic security features. A single secure.Options struct can be
|
||||
// provided to configure which features should be enabled, and the ability to override a few of the default values.
|
||||
type secure struct {
|
||||
// Customize Secure with an Options struct.
|
||||
opt Options
|
||||
}
|
||||
|
||||
// Constructs a new Secure instance with supplied options.
|
||||
func New(options Options) *secure {
|
||||
if options.BadHostHandler == nil {
|
||||
options.BadHostHandler = http.HandlerFunc(defaultBadHostHandler)
|
||||
}
|
||||
|
||||
return &secure{
|
||||
opt: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *secure) process(w http.ResponseWriter, r *http.Request) error {
|
||||
// Allowed hosts check.
|
||||
if len(s.opt.AllowedHosts) > 0 && !s.opt.IsDevelopment {
|
||||
isGoodHost := false
|
||||
for _, allowedHost := range s.opt.AllowedHosts {
|
||||
if strings.EqualFold(allowedHost, r.Host) {
|
||||
isGoodHost = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isGoodHost {
|
||||
s.opt.BadHostHandler.ServeHTTP(w, r)
|
||||
return fmt.Errorf("bad host name: %s", r.Host)
|
||||
}
|
||||
}
|
||||
|
||||
// SSL check.
|
||||
if s.opt.SSLRedirect && !s.opt.IsDevelopment {
|
||||
isSSL := false
|
||||
if strings.EqualFold(r.URL.Scheme, "https") || r.TLS != nil {
|
||||
isSSL = true
|
||||
} else {
|
||||
for k, v := range s.opt.SSLProxyHeaders {
|
||||
if r.Header.Get(k) == v {
|
||||
isSSL = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !isSSL {
|
||||
url := r.URL
|
||||
url.Scheme = "https"
|
||||
url.Host = r.Host
|
||||
|
||||
if len(s.opt.SSLHost) > 0 {
|
||||
url.Host = s.opt.SSLHost
|
||||
}
|
||||
|
||||
status := http.StatusMovedPermanently
|
||||
if s.opt.SSLTemporaryRedirect {
|
||||
status = http.StatusTemporaryRedirect
|
||||
}
|
||||
|
||||
http.Redirect(w, r, url.String(), status)
|
||||
return fmt.Errorf("redirecting to HTTPS")
|
||||
}
|
||||
}
|
||||
|
||||
// Strict Transport Security header.
|
||||
if s.opt.STSSeconds != 0 && !s.opt.IsDevelopment {
|
||||
stsSub := ""
|
||||
if s.opt.STSIncludeSubdomains {
|
||||
stsSub = stsSubdomainString
|
||||
}
|
||||
|
||||
w.Header().Add(stsHeader, fmt.Sprintf("max-age=%d%s", s.opt.STSSeconds, stsSub))
|
||||
}
|
||||
|
||||
// Frame Options header.
|
||||
if len(s.opt.CustomFrameOptionsValue) > 0 {
|
||||
w.Header().Add(frameOptionsHeader, s.opt.CustomFrameOptionsValue)
|
||||
} else if s.opt.FrameDeny {
|
||||
w.Header().Add(frameOptionsHeader, frameOptionsValue)
|
||||
}
|
||||
|
||||
// Content Type Options header.
|
||||
if s.opt.ContentTypeNosniff {
|
||||
w.Header().Add(contentTypeHeader, contentTypeValue)
|
||||
}
|
||||
|
||||
// XSS Protection header.
|
||||
if s.opt.BrowserXssFilter {
|
||||
w.Header().Add(xssProtectionHeader, xssProtectionValue)
|
||||
}
|
||||
|
||||
// Content Security Policy header.
|
||||
if len(s.opt.ContentSecurityPolicy) > 0 {
|
||||
w.Header().Add(cspHeader, s.opt.ContentSecurityPolicy)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func Secure(options Options) gin.HandlerFunc {
|
||||
s := New(options)
|
||||
|
||||
return func(c *gin.Context) {
|
||||
err := s.process(c.Writer, c.Request)
|
||||
if err != nil {
|
||||
if c.Writer.Written() {
|
||||
c.AbortWithStatus(c.Writer.Status())
|
||||
} else {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
package secure
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
testResponse = "bar"
|
||||
)
|
||||
|
||||
func newServer(options Options) *gin.Engine {
|
||||
r := gin.Default()
|
||||
r.Use(Secure(options))
|
||||
r.GET("/foo", func(c *gin.Context) {
|
||||
c.String(200, testResponse)
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
func TestNoConfig(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
// Intentionally left blank.
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "http://example.com/foo", nil)
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
expect(t, res.Body.String(), "bar")
|
||||
}
|
||||
|
||||
func TestNoAllowHosts(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
AllowedHosts: []string{},
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
req.Host = "www.example.com"
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
expect(t, res.Body.String(), `bar`)
|
||||
}
|
||||
|
||||
func TestGoodSingleAllowHosts(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
AllowedHosts: []string{"www.example.com"},
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
req.Host = "www.example.com"
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
expect(t, res.Body.String(), `bar`)
|
||||
}
|
||||
|
||||
func TestBadSingleAllowHosts(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
AllowedHosts: []string{"sub.example.com"},
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
req.Host = "www.example.com"
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func TestGoodMultipleAllowHosts(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
AllowedHosts: []string{"www.example.com", "sub.example.com"},
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
req.Host = "sub.example.com"
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
expect(t, res.Body.String(), `bar`)
|
||||
}
|
||||
|
||||
func TestBadMultipleAllowHosts(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
AllowedHosts: []string{"www.example.com", "sub.example.com"},
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
req.Host = "www3.example.com"
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func TestAllowHostsInDevMode(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
AllowedHosts: []string{"www.example.com", "sub.example.com"},
|
||||
IsDevelopment: true,
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
req.Host = "www3.example.com"
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestBadHostHandler(t *testing.T) {
|
||||
|
||||
badHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "BadHost", http.StatusInternalServerError)
|
||||
})
|
||||
|
||||
s := newServer(Options{
|
||||
AllowedHosts: []string{"www.example.com", "sub.example.com"},
|
||||
BadHostHandler: badHandler,
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
req.Host = "www3.example.com"
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusInternalServerError)
|
||||
|
||||
// http.Error outputs a new line character with the response.
|
||||
expect(t, res.Body.String(), "BadHost\n")
|
||||
}
|
||||
|
||||
func TestSSL(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
SSLRedirect: true,
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
req.Host = "www.example.com"
|
||||
req.URL.Scheme = "https"
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestSSLInDevMode(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
SSLRedirect: true,
|
||||
IsDevelopment: true,
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
req.Host = "www.example.com"
|
||||
req.URL.Scheme = "http"
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestBasicSSL(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
SSLRedirect: true,
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
req.Host = "www.example.com"
|
||||
req.URL.Scheme = "http"
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusMovedPermanently)
|
||||
expect(t, res.Header().Get("Location"), "https://www.example.com/foo")
|
||||
}
|
||||
|
||||
func TestBasicSSLWithHost(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
SSLRedirect: true,
|
||||
SSLHost: "secure.example.com",
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
req.Host = "www.example.com"
|
||||
req.URL.Scheme = "http"
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusMovedPermanently)
|
||||
expect(t, res.Header().Get("Location"), "https://secure.example.com/foo")
|
||||
}
|
||||
|
||||
func TestBadProxySSL(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
SSLRedirect: true,
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
req.Host = "www.example.com"
|
||||
req.URL.Scheme = "http"
|
||||
req.Header.Add("X-Forwarded-Proto", "https")
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusMovedPermanently)
|
||||
expect(t, res.Header().Get("Location"), "https://www.example.com/foo")
|
||||
}
|
||||
|
||||
func TestCustomProxySSL(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
SSLRedirect: true,
|
||||
SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"},
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
req.Host = "www.example.com"
|
||||
req.URL.Scheme = "http"
|
||||
req.Header.Add("X-Forwarded-Proto", "https")
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestCustomProxySSLInDevMode(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
SSLRedirect: true,
|
||||
SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"},
|
||||
IsDevelopment: true,
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
req.Host = "www.example.com"
|
||||
req.URL.Scheme = "http"
|
||||
req.Header.Add("X-Forwarded-Proto", "http")
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestCustomProxyAndHostSSL(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
SSLRedirect: true,
|
||||
SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"},
|
||||
SSLHost: "secure.example.com",
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
req.Host = "www.example.com"
|
||||
req.URL.Scheme = "http"
|
||||
req.Header.Add("X-Forwarded-Proto", "https")
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestCustomBadProxyAndHostSSL(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
SSLRedirect: true,
|
||||
SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "superman"},
|
||||
SSLHost: "secure.example.com",
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
req.Host = "www.example.com"
|
||||
req.URL.Scheme = "http"
|
||||
req.Header.Add("X-Forwarded-Proto", "https")
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusMovedPermanently)
|
||||
expect(t, res.Header().Get("Location"), "https://secure.example.com/foo")
|
||||
}
|
||||
|
||||
func TestCustomBadProxyAndHostSSLWithTempRedirect(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
SSLRedirect: true,
|
||||
SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "superman"},
|
||||
SSLHost: "secure.example.com",
|
||||
SSLTemporaryRedirect: true,
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
req.Host = "www.example.com"
|
||||
req.URL.Scheme = "http"
|
||||
req.Header.Add("X-Forwarded-Proto", "https")
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusTemporaryRedirect)
|
||||
expect(t, res.Header().Get("Location"), "https://secure.example.com/foo")
|
||||
}
|
||||
|
||||
func TestStsHeader(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
STSSeconds: 315360000,
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
expect(t, res.Header().Get("Strict-Transport-Security"), "max-age=315360000")
|
||||
}
|
||||
|
||||
func TestStsHeaderInDevMode(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
STSSeconds: 315360000,
|
||||
IsDevelopment: true,
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
expect(t, res.Header().Get("Strict-Transport-Security"), "")
|
||||
}
|
||||
|
||||
func TestStsHeaderWithSubdomain(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
STSSeconds: 315360000,
|
||||
STSIncludeSubdomains: true,
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
expect(t, res.Header().Get("Strict-Transport-Security"), "max-age=315360000; includeSubdomains")
|
||||
}
|
||||
|
||||
func TestFrameDeny(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
FrameDeny: true,
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
expect(t, res.Header().Get("X-Frame-Options"), "DENY")
|
||||
}
|
||||
|
||||
func TestCustomFrameValue(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
CustomFrameOptionsValue: "SAMEORIGIN",
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
expect(t, res.Header().Get("X-Frame-Options"), "SAMEORIGIN")
|
||||
}
|
||||
|
||||
func TestCustomFrameValueWithDeny(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
FrameDeny: true,
|
||||
CustomFrameOptionsValue: "SAMEORIGIN",
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
expect(t, res.Header().Get("X-Frame-Options"), "SAMEORIGIN")
|
||||
}
|
||||
|
||||
func TestContentNosniff(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
ContentTypeNosniff: true,
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
expect(t, res.Header().Get("X-Content-Type-Options"), "nosniff")
|
||||
}
|
||||
|
||||
func TestXSSProtection(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
BrowserXssFilter: true,
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
expect(t, res.Header().Get("X-XSS-Protection"), "1; mode=block")
|
||||
}
|
||||
|
||||
func TestCsp(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
ContentSecurityPolicy: "default-src 'self'",
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
expect(t, res.Header().Get("Content-Security-Policy"), "default-src 'self'")
|
||||
}
|
||||
|
||||
func TestInlineSecure(t *testing.T) {
|
||||
s := newServer(Options{
|
||||
FrameDeny: true,
|
||||
})
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/foo", nil)
|
||||
|
||||
s.ServeHTTP(res, req)
|
||||
|
||||
expect(t, res.Code, http.StatusOK)
|
||||
expect(t, res.Header().Get("X-Frame-Options"), "DENY")
|
||||
}
|
||||
|
||||
/* Test Helpers */
|
||||
func expect(t *testing.T, a interface{}, b interface{}) {
|
||||
if a != b {
|
||||
t.Errorf("Expected [%v] (type %v) - Got [%v] (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 gin-contrib
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,113 @@
|
||||
# Gin Static Middleware
|
||||
|
||||
Static middleware, support both local files and embed filesystem.
|
||||
|
||||
Copied from [gin-contrib/static](https://github.com/gin-contrib/static) and [soulteary/gin-static](https://github.com/soulteary/gin-static)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Download and Import
|
||||
|
||||
Download and install it:
|
||||
|
||||
```bash
|
||||
go get gitverse.ru/andoma/gin-contrib/static
|
||||
```
|
||||
|
||||
Import it in your code:
|
||||
|
||||
```go
|
||||
import "gitverse.ru/andoma/gin-contrib/static"
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
See the [example](../examples/static/)
|
||||
|
||||
### Serve Local Files
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"gitverse.ru/andoma/gin-contrib/static"
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
// if Allow DirectoryIndex
|
||||
// r.Use(static.Serve("/", static.LocalFile("./public", true)))
|
||||
// set prefix
|
||||
// r.Use(static.Serve("/static", static.LocalFile("./public", true)))
|
||||
|
||||
r.Use(static.Serve("/", static.LocalFile("./public", false)))
|
||||
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.String(200, "test")
|
||||
})
|
||||
|
||||
// Listen and Server in 0.0.0.0:8080
|
||||
if err := r.Run(":8080"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Serve Embed folder
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitverse.ru/andoma/gin-contrib/static"
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
//go:embed public
|
||||
var EmbedFS embed.FS
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
// method 1: use as Gin Router
|
||||
// trim embedfs path `public/page`, and use it as url path `/`
|
||||
// r.GET("/", static.ServeEmbed("public/page", EmbedFS))
|
||||
|
||||
// method 2: use as middleware
|
||||
// trim embedfs path `public/page`, the embedfs path start with `/`
|
||||
// r.Use(static.ServeEmbed("public/page", EmbedFS))
|
||||
|
||||
// method 2.1: use as middleware
|
||||
// trim embedfs path `public/page`, the embedfs path start with `/public/page`
|
||||
// r.Use(static.ServeEmbed("", EmbedFS))
|
||||
|
||||
// method 3: use as manual
|
||||
// trim embedfs path `public/page`, the embedfs path start with `/public/page`
|
||||
// staticFiles, err := static.EmbedFolder(EmbedFS, "public/page")
|
||||
// if err != nil {
|
||||
// log.Fatalln("initialization of embed folder failed:", err)
|
||||
// } else {
|
||||
// r.Use(static.Serve("/", staticFiles))
|
||||
// }
|
||||
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.String(200, "test")
|
||||
})
|
||||
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
fmt.Printf("%s doesn't exists, redirect on /\n", c.Request.URL.Path)
|
||||
c.Redirect(http.StatusMovedPermanently, "/")
|
||||
})
|
||||
|
||||
// Listen and Server in 0.0.0.0:8080
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,62 @@
|
||||
package static
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
type embedFileSystem struct {
|
||||
http.FileSystem
|
||||
overrides []Override
|
||||
}
|
||||
|
||||
func (e embedFileSystem) Exists(prefix string, path string) bool {
|
||||
_, err := e.Open(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (e embedFileSystem) Override(c *gin.Context) bool {
|
||||
if len(e.overrides) > 0 {
|
||||
fn := e.overrides[0]
|
||||
return fn(c)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func EmbedFolder(fsEmbed embed.FS, reqPath string, overrides ...Override) (ServeFileSystem, error) {
|
||||
targetPath := strings.TrimSpace(reqPath)
|
||||
if targetPath == "" {
|
||||
return embedFileSystem{
|
||||
FileSystem: http.FS(fsEmbed),
|
||||
overrides: overrides,
|
||||
}, nil
|
||||
}
|
||||
|
||||
fsys, _ := fs.Sub(fsEmbed, targetPath)
|
||||
_, err := fsEmbed.Open(targetPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return embedFileSystem{
|
||||
FileSystem: http.FS(fsys),
|
||||
overrides: overrides,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ServeEmbed(reqPath string, fsEmbed embed.FS) gin.HandlerFunc {
|
||||
embedFS, err := EmbedFolder(fsEmbed, reqPath)
|
||||
if err != nil {
|
||||
return func(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"message": "initialization of embed folder failed",
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
return gin.WrapH(http.FileServer(embedFS))
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package static_test
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gitverse.ru/andoma/gin"
|
||||
"gitverse.ru/andoma/gin-contrib/static"
|
||||
)
|
||||
|
||||
//go:embed test/data/server
|
||||
var testFS embed.FS
|
||||
|
||||
func TestEmbedFolderWithRedir(t *testing.T) {
|
||||
var tests = []struct {
|
||||
targetURL string // input
|
||||
httpCode int // expected http code
|
||||
httpBody string // expected http body
|
||||
name string // test name
|
||||
}{
|
||||
{"/404.html", 301, "<a href=\"/\">Moved Permanently</a>.\n\n", "Unknown file"},
|
||||
{"/", 200, "<h1>Hello Embed</h1>", "Root"},
|
||||
{"/index.html", 301, "", "Root by file name automatic redirect"},
|
||||
{"/static.html", 200, "<h1>Hello Gin Static</h1>", "Other file"},
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
|
||||
staticFiles, err := static.EmbedFolder(testFS, "test/data/server")
|
||||
if err != nil {
|
||||
log.Fatalln("initialization of embed folder failed:", err)
|
||||
} else {
|
||||
router.Use(static.Serve("/", staticFiles))
|
||||
}
|
||||
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
fmt.Printf("%s doesn't exists, redirect on /\n", c.Request.URL.Path)
|
||||
c.Redirect(301, "/")
|
||||
})
|
||||
|
||||
for _, tt := range tests {
|
||||
w := PerformRequest(router, "GET", tt.targetURL)
|
||||
assert.Equal(t, tt.httpCode, w.Code, tt.name)
|
||||
assert.Equal(t, tt.httpBody, w.Body.String(), tt.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbedFolderWithoutRedir(t *testing.T) {
|
||||
var tests = []struct {
|
||||
targetURL string // input
|
||||
httpCode int // expected http code
|
||||
httpBody string // expected http body
|
||||
name string // test name
|
||||
}{
|
||||
{"/404.html", http.StatusNotFound, "404 page not found", "Unknown file"},
|
||||
{"/", http.StatusOK, "<h1>Hello Embed</h1>", "Root"},
|
||||
{"/index.html", http.StatusMovedPermanently, "", "Root by file name automatic redirect"},
|
||||
{"/static.html", http.StatusOK, "<h1>Hello Gin Static</h1>", "Other file"},
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
staticFiles, err := static.EmbedFolder(testFS, "test/data/server")
|
||||
if err != nil {
|
||||
log.Fatalln("initialization of embed folder failed:", err)
|
||||
} else {
|
||||
router.Use(static.Serve("/", staticFiles))
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
w := PerformRequest(router, "GET", tt.targetURL)
|
||||
assert.Equal(t, tt.httpCode, w.Code, tt.name)
|
||||
assert.Equal(t, tt.httpBody, w.Body.String(), tt.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbedInitErrorPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
targetPath string
|
||||
haveErr bool
|
||||
fs embed.FS
|
||||
}{
|
||||
{
|
||||
name: "ValidPath",
|
||||
targetPath: "test/data/server",
|
||||
haveErr: false,
|
||||
fs: testFS,
|
||||
},
|
||||
{
|
||||
name: "InvalidPath",
|
||||
targetPath: "nonexistingdirectory/nonexistingdirectory",
|
||||
haveErr: true,
|
||||
fs: testFS,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := static.EmbedFolder(tt.fs, tt.targetPath)
|
||||
assert.Equal(t, (err != nil), tt.haveErr, tt.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateEmbed(t *testing.T) {
|
||||
_, err := static.EmbedFolder(testFS, "test/data/server")
|
||||
if err != nil {
|
||||
log.Fatalln("initialization of embed folder failed:", err)
|
||||
}
|
||||
_, err2 := static.EmbedFolder(testFS, "")
|
||||
if err2 != nil {
|
||||
log.Fatalln("initialization of embed folder failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServEmbed(t *testing.T) {
|
||||
var tests = []struct {
|
||||
targetURL string // input
|
||||
httpCode int // expected http code
|
||||
httpBody string // expected http body
|
||||
name string // test name
|
||||
}{
|
||||
{"/404.html", 404, "404 page not found\n", "Unknown file"},
|
||||
{"/", 200, "<h1>Hello Embed</h1>", "Root"},
|
||||
{"/index.html", 301, "<a href=\"/\">Moved Permanently</a>.\n\n", "Root by file name automatic redirect"},
|
||||
{"/static.html", 200, "<h1>Hello Gin Static</h1>", "Other file"},
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
|
||||
router.Use(static.ServeEmbed("test/data/server", testFS))
|
||||
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
fmt.Printf("%s doesn't exists, redirect on /\n", c.Request.URL.Path)
|
||||
c.Redirect(301, "/")
|
||||
})
|
||||
|
||||
for _, tt := range tests {
|
||||
w := PerformRequest(router, "GET", tt.targetURL)
|
||||
assert.Equal(t, tt.httpCode, w.Code, tt.name)
|
||||
assert.Equal(t, tt.httpBody, w.Body.String(), tt.name)
|
||||
}
|
||||
|
||||
router2 := gin.New()
|
||||
|
||||
router2.Use(static.ServeEmbed("", testFS))
|
||||
|
||||
router2.NoRoute(func(c *gin.Context) {
|
||||
fmt.Printf("%s doesn't exists, redirect on /\n", c.Request.URL.Path)
|
||||
c.Redirect(301, "/")
|
||||
})
|
||||
|
||||
for _, tt := range tests {
|
||||
w := PerformRequest(router2, "GET", "/test/data/server"+tt.targetURL)
|
||||
assert.Equal(t, tt.httpCode, w.Code, tt.name)
|
||||
assert.Equal(t, tt.httpBody, w.Body.String(), tt.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServEmbedErr(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
URL string
|
||||
Code int
|
||||
Result string
|
||||
}{
|
||||
{
|
||||
name: "Invalid Path",
|
||||
URL: "/test/data/server/nonexistingdirectory/nonexistingdirectory",
|
||||
Code: http.StatusInternalServerError,
|
||||
Result: "{\"error\":\"open 111111test/data/server: file does not exist\",\"message\":\"initialization of embed folder failed\"}",
|
||||
},
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.Use(static.ServeEmbed("111111test/data/server", testFS))
|
||||
for _, tt := range tests {
|
||||
w := PerformRequest(router, "GET", tt.URL)
|
||||
assert.Equal(t, w.Code, tt.Code)
|
||||
assert.Equal(t, w.Body.String(), tt.Result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package static
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
type localFileSystem struct {
|
||||
http.FileSystem
|
||||
root string
|
||||
indexes bool
|
||||
}
|
||||
|
||||
func (l *localFileSystem) Exists(prefix string, file string) bool {
|
||||
if p := strings.TrimPrefix(file, prefix); len(p) < len(file) {
|
||||
name := path.Join(l.root, path.Clean(p))
|
||||
stats, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if stats.IsDir() {
|
||||
if !l.indexes {
|
||||
_, err := os.Stat(path.Join(name, "index.html"))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Dummy
|
||||
func (l *localFileSystem) Override(*gin.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func LocalFile(root string, indexes bool) *localFileSystem {
|
||||
return &localFileSystem{
|
||||
FileSystem: gin.Dir(root, indexes),
|
||||
root: root,
|
||||
indexes: indexes,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package static_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gitverse.ru/andoma/gin"
|
||||
"gitverse.ru/andoma/gin-contrib/static"
|
||||
)
|
||||
|
||||
func TestLocalFile(t *testing.T) {
|
||||
// SETUP file
|
||||
testRoot, _ := os.Getwd()
|
||||
f, err := os.CreateTemp(testRoot, "")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
_, _ = f.WriteString("Gin Web Framework")
|
||||
f.Close()
|
||||
|
||||
dir, filename := filepath.Split(f.Name())
|
||||
router := gin.New()
|
||||
router.Use(static.Serve("/", static.LocalFile(dir, true)))
|
||||
|
||||
w := PerformRequest(router, "GET", "/"+filename)
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
assert.Equal(t, w.Body.String(), "Gin Web Framework")
|
||||
|
||||
w = PerformRequest(router, "GET", "/")
|
||||
assert.Contains(t, w.Body.String(), `<a href="`+filename)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package static
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
type Override func(*gin.Context) bool
|
||||
|
||||
type ServeFileSystem interface {
|
||||
http.FileSystem
|
||||
Exists(prefix string, path string) bool
|
||||
Override(*gin.Context) bool
|
||||
}
|
||||
|
||||
func ServeRoot(urlPrefix, root string) gin.HandlerFunc {
|
||||
return Serve(urlPrefix, LocalFile(root, false))
|
||||
}
|
||||
|
||||
// Serve returns a middleware handler that serves static files in the given directory.
|
||||
func Serve(urlPrefix string, fs ServeFileSystem) gin.HandlerFunc {
|
||||
return ServeCached(urlPrefix, fs, 0)
|
||||
}
|
||||
|
||||
// ServeCached returns a middleware handler that similar as Serve but with the Cache-Control Header set as passed in the cacheAge parameter
|
||||
func ServeCached(urlPrefix string, fs ServeFileSystem, cacheAge uint) gin.HandlerFunc {
|
||||
fileserver := http.FileServer(fs)
|
||||
if urlPrefix != "" {
|
||||
fileserver = http.StripPrefix(urlPrefix, fileserver)
|
||||
}
|
||||
return func(c *gin.Context) {
|
||||
if fs.Exists(urlPrefix, c.Request.URL.Path) && !fs.Override(c) {
|
||||
if cacheAge != 0 {
|
||||
c.Writer.Header().Add("Cache-Control", fmt.Sprintf("max-age=%d", cacheAge))
|
||||
}
|
||||
fileserver.ServeHTTP(c.Writer, c.Request)
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package static_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gitverse.ru/andoma/gin"
|
||||
"gitverse.ru/andoma/gin-contrib/static"
|
||||
)
|
||||
|
||||
// nolint:unparam
|
||||
func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
|
||||
req, _ := http.NewRequestWithContext(context.Background(), method, path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
func TestEmptyDirectory(t *testing.T) {
|
||||
// SETUP file
|
||||
testRoot, _ := os.Getwd()
|
||||
f, err := os.CreateTemp(testRoot, "")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
_, _ = f.WriteString("Gin Web Framework")
|
||||
f.Close()
|
||||
|
||||
dir, filename := filepath.Split(f.Name())
|
||||
|
||||
router := gin.New()
|
||||
router.Use(static.ServeRoot("/", dir))
|
||||
router.GET("/", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "index")
|
||||
})
|
||||
router.GET("/a", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "a")
|
||||
})
|
||||
router.GET("/"+filename, func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "this is not printed")
|
||||
})
|
||||
w := PerformRequest(router, "GET", "/")
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
assert.Equal(t, w.Body.String(), "index")
|
||||
|
||||
w = PerformRequest(router, "GET", "/"+filename)
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
assert.Equal(t, w.Body.String(), "Gin Web Framework")
|
||||
|
||||
w = PerformRequest(router, "GET", "/"+filename+"a")
|
||||
assert.Equal(t, w.Code, http.StatusNotFound)
|
||||
|
||||
w = PerformRequest(router, "GET", "/a")
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
assert.Equal(t, w.Body.String(), "a")
|
||||
|
||||
router2 := gin.New()
|
||||
router2.Use(static.ServeRoot("/static", dir))
|
||||
router2.GET("/"+filename, func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "this is printed")
|
||||
})
|
||||
|
||||
w = PerformRequest(router2, "GET", "/")
|
||||
assert.Equal(t, w.Code, http.StatusNotFound)
|
||||
|
||||
w = PerformRequest(router2, "GET", "/static")
|
||||
assert.Equal(t, w.Code, http.StatusNotFound)
|
||||
router2.GET("/static", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "index")
|
||||
})
|
||||
|
||||
w = PerformRequest(router2, "GET", "/static")
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
|
||||
w = PerformRequest(router2, "GET", "/"+filename)
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
assert.Equal(t, w.Body.String(), "this is printed")
|
||||
|
||||
w = PerformRequest(router2, "GET", "/static/"+filename)
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
assert.Equal(t, w.Body.String(), "Gin Web Framework")
|
||||
}
|
||||
|
||||
func TestIndex(t *testing.T) {
|
||||
// SETUP file
|
||||
testRoot, _ := os.Getwd()
|
||||
f, err := os.Create(path.Join(testRoot, "index.html"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
_, _ = f.WriteString("index")
|
||||
f.Close()
|
||||
|
||||
dir, filename := filepath.Split(f.Name())
|
||||
|
||||
router := gin.New()
|
||||
router.Use(static.ServeRoot("/", dir))
|
||||
|
||||
w := PerformRequest(router, "GET", "/"+filename)
|
||||
assert.Equal(t, w.Code, http.StatusMovedPermanently)
|
||||
|
||||
w = PerformRequest(router, "GET", "/")
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
assert.Equal(t, w.Body.String(), "index")
|
||||
}
|
||||
|
||||
func TestListIndex(t *testing.T) {
|
||||
// SETUP file
|
||||
testRoot, _ := os.Getwd()
|
||||
f, err := os.CreateTemp(testRoot, "")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
_, _ = f.WriteString("Gin Web Framework")
|
||||
f.Close()
|
||||
|
||||
dir, filename := filepath.Split(f.Name())
|
||||
router := gin.New()
|
||||
router.Use(static.Serve("/", static.LocalFile(dir, true)))
|
||||
|
||||
w := PerformRequest(router, "GET", "/"+filename)
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
assert.Equal(t, w.Body.String(), "Gin Web Framework")
|
||||
|
||||
w = PerformRequest(router, "GET", "/")
|
||||
assert.Contains(t, w.Body.String(), `<a href="`+filename)
|
||||
}
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
// SETUP file
|
||||
testRoot, _ := os.Getwd()
|
||||
f, err := os.CreateTemp(testRoot, "")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
_, _ = f.WriteString("Gin Web Framework")
|
||||
f.Close()
|
||||
|
||||
dir, filename := filepath.Split(f.Name())
|
||||
|
||||
router := gin.New()
|
||||
router.Use(static.ServeCached("/", static.LocalFile(dir, true), 3600))
|
||||
|
||||
w := PerformRequest(router, "GET", "/"+filename)
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
assert.Equal(t, w.Header().Get("Cache-Control"), "max-age=3600")
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<h1>Hello Embed</h1>
|
||||
@@ -0,0 +1 @@
|
||||
<h1>Hello Gin Static</h1>
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Gin-Gonic
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,101 @@
|
||||
# Timeout
|
||||
|
||||
Timeout wraps a handler and aborts the process of the handler if the timeout is reached.
|
||||
|
||||
Copied from [gin-contrib/timeout](https://github.com/gin-contrib/timeout)
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitverse.ru/andoma/gin-contrib/timeout"
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
func emptySuccessResponse(c *gin.Context) {
|
||||
time.Sleep(200 * time.Microsecond)
|
||||
c.String(http.StatusOK, "")
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.New()
|
||||
|
||||
r.GET("/", timeout.New(
|
||||
timeout.WithTimeout(100*time.Microsecond),
|
||||
timeout.WithHandler(emptySuccessResponse),
|
||||
))
|
||||
|
||||
// Listen and Server in 0.0.0.0:8080
|
||||
if err := r.Run(":8080"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### custom error response
|
||||
|
||||
Add new error response func:
|
||||
|
||||
```go
|
||||
func testResponse(c *gin.Context) {
|
||||
c.String(http.StatusRequestTimeout, "test response")
|
||||
}
|
||||
```
|
||||
|
||||
Add `WithResponse` option.
|
||||
|
||||
```go
|
||||
r.GET("/", timeout.New(
|
||||
timeout.WithTimeout(100*time.Microsecond),
|
||||
timeout.WithHandler(emptySuccessResponse),
|
||||
timeout.WithResponse(testResponse),
|
||||
))
|
||||
```
|
||||
|
||||
### custom middleware
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitverse.ru/andoma/gin-contrib/timeout"
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
func testResponse(c *gin.Context) {
|
||||
c.String(http.StatusRequestTimeout, "timeout")
|
||||
}
|
||||
|
||||
func timeoutMiddleware() gin.HandlerFunc {
|
||||
return timeout.New(
|
||||
timeout.WithTimeout(500*time.Millisecond),
|
||||
timeout.WithHandler(func(c *gin.Context) {
|
||||
c.Next()
|
||||
}),
|
||||
timeout.WithResponse(testResponse),
|
||||
)
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.New()
|
||||
r.Use(timeoutMiddleware())
|
||||
r.GET("/slow", func(c *gin.Context) {
|
||||
time.Sleep(800 * time.Millisecond)
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
if err := r.Run(":8080"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
package timeout
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// BufferPool represents a pool of buffers.
|
||||
type BufferPool struct {
|
||||
pool sync.Pool
|
||||
}
|
||||
|
||||
// Get returns a buffer from the buffer pool.
|
||||
// If the pool is empty, a new buffer is created and returned.
|
||||
func (p *BufferPool) Get() *bytes.Buffer {
|
||||
buf := p.pool.Get()
|
||||
if buf == nil {
|
||||
return &bytes.Buffer{}
|
||||
}
|
||||
return buf.(*bytes.Buffer)
|
||||
}
|
||||
|
||||
// Put adds a buffer back to the pool.
|
||||
func (p *BufferPool) Put(buf *bytes.Buffer) {
|
||||
p.pool.Put(buf)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package timeout
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetBuffer(t *testing.T) {
|
||||
pool := &BufferPool{}
|
||||
buf := pool.Get()
|
||||
assert.NotEqual(t, nil, buf)
|
||||
pool.Put(buf)
|
||||
buf2 := pool.Get()
|
||||
assert.NotEqual(t, nil, buf2)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package timeout
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
// Option for timeout
|
||||
type Option func(*Timeout)
|
||||
|
||||
// WithTimeout set timeout
|
||||
func WithTimeout(timeout time.Duration) Option {
|
||||
return func(t *Timeout) {
|
||||
t.timeout = timeout
|
||||
}
|
||||
}
|
||||
|
||||
// WithHandler add gin handler
|
||||
func WithHandler(h gin.HandlerFunc) Option {
|
||||
return func(t *Timeout) {
|
||||
t.handler = h
|
||||
}
|
||||
}
|
||||
|
||||
// WithResponse add gin handler
|
||||
func WithResponse(h gin.HandlerFunc) Option {
|
||||
return func(t *Timeout) {
|
||||
t.response = h
|
||||
}
|
||||
}
|
||||
|
||||
func defaultResponse(c *gin.Context) {
|
||||
c.String(http.StatusRequestTimeout, http.StatusText(http.StatusRequestTimeout))
|
||||
}
|
||||
|
||||
// Timeout struct
|
||||
type Timeout struct {
|
||||
timeout time.Duration
|
||||
handler gin.HandlerFunc
|
||||
response gin.HandlerFunc
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package timeout
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
var bufPool *BufferPool
|
||||
|
||||
const (
|
||||
defaultTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// New wraps a handler and aborts the process of the handler if the timeout is reached
|
||||
func New(opts ...Option) gin.HandlerFunc {
|
||||
t := &Timeout{
|
||||
timeout: defaultTimeout,
|
||||
handler: nil,
|
||||
response: defaultResponse,
|
||||
}
|
||||
|
||||
// Loop through each option
|
||||
for _, opt := range opts {
|
||||
if opt == nil {
|
||||
panic("timeout Option not be nil")
|
||||
}
|
||||
|
||||
// Call the option giving the instantiated
|
||||
opt(t)
|
||||
}
|
||||
|
||||
if t.timeout <= 0 {
|
||||
return t.handler
|
||||
}
|
||||
|
||||
bufPool = &BufferPool{}
|
||||
|
||||
return func(c *gin.Context) {
|
||||
finish := make(chan struct{}, 1)
|
||||
panicChan := make(chan interface{}, 1)
|
||||
|
||||
w := c.Writer
|
||||
buffer := bufPool.Get()
|
||||
tw := NewWriter(w, buffer)
|
||||
c.Writer = tw
|
||||
buffer.Reset()
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
panicChan <- p
|
||||
}
|
||||
}()
|
||||
t.handler(c)
|
||||
finish <- struct{}{}
|
||||
}()
|
||||
|
||||
select {
|
||||
case p := <-panicChan:
|
||||
tw.FreeBuffer()
|
||||
c.Writer = w
|
||||
panic(p)
|
||||
|
||||
case <-finish:
|
||||
c.Next()
|
||||
tw.mu.Lock()
|
||||
defer tw.mu.Unlock()
|
||||
dst := tw.ResponseWriter.Header()
|
||||
for k, vv := range tw.Header() {
|
||||
dst[k] = vv
|
||||
}
|
||||
|
||||
if _, err := tw.ResponseWriter.Write(buffer.Bytes()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tw.FreeBuffer()
|
||||
bufPool.Put(buffer)
|
||||
|
||||
case <-time.After(t.timeout):
|
||||
c.Abort()
|
||||
tw.mu.Lock()
|
||||
defer tw.mu.Unlock()
|
||||
tw.timeout = true
|
||||
tw.FreeBuffer()
|
||||
bufPool.Put(buffer)
|
||||
|
||||
c.Writer = w
|
||||
t.response(c)
|
||||
c.Writer = tw
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package timeout
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
// nolint:unparam
|
||||
func performRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
|
||||
req, _ := http.NewRequestWithContext(context.Background(), method, path, nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
func emptySuccessResponse(c *gin.Context) {
|
||||
time.Sleep(200 * time.Microsecond)
|
||||
c.String(http.StatusOK, "")
|
||||
}
|
||||
|
||||
func TestTimeout(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.GET("/", New(WithTimeout(50*time.Microsecond), WithHandler(emptySuccessResponse)))
|
||||
|
||||
w := performRequest(r, "GET", "/")
|
||||
assert.Equal(t, http.StatusRequestTimeout, w.Code)
|
||||
assert.Equal(t, http.StatusText(http.StatusRequestTimeout), w.Body.String())
|
||||
}
|
||||
|
||||
func TestWithoutTimeout(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.GET("/", New(WithTimeout(-1*time.Microsecond), WithHandler(emptySuccessResponse)))
|
||||
|
||||
w := performRequest(r, "GET", "/")
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "", w.Body.String())
|
||||
}
|
||||
|
||||
func testResponse(c *gin.Context) {
|
||||
c.String(http.StatusRequestTimeout, "test response")
|
||||
}
|
||||
|
||||
func TestCustomResponse(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.GET("/", New(
|
||||
WithTimeout(100*time.Microsecond),
|
||||
WithHandler(emptySuccessResponse),
|
||||
WithResponse(testResponse),
|
||||
))
|
||||
|
||||
w := performRequest(r, "GET", "/")
|
||||
assert.Equal(t, http.StatusRequestTimeout, w.Code)
|
||||
assert.Equal(t, "test response", w.Body.String())
|
||||
}
|
||||
|
||||
func emptySuccessResponse2(c *gin.Context) {
|
||||
time.Sleep(50 * time.Microsecond)
|
||||
c.String(http.StatusOK, "")
|
||||
}
|
||||
|
||||
func TestSuccess(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.GET("/", New(
|
||||
WithTimeout(1*time.Second),
|
||||
WithHandler(emptySuccessResponse2),
|
||||
WithResponse(testResponse),
|
||||
))
|
||||
|
||||
w := performRequest(r, "GET", "/")
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "", w.Body.String())
|
||||
}
|
||||
|
||||
func TestPanic(t *testing.T) {
|
||||
buffer := new(strings.Builder)
|
||||
r := gin.New()
|
||||
r.Use(gin.RecoveryWithWriter(buffer))
|
||||
r.GET("/", New(
|
||||
WithTimeout(1*time.Second),
|
||||
WithHandler(func(_ *gin.Context) {
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
}),
|
||||
))
|
||||
|
||||
w := performRequest(r, "GET", "/")
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
assert.Contains(t, buffer.String(), "panic recovered")
|
||||
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
||||
assert.Contains(t, buffer.String(), t.Name())
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package timeout
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
// Writer is a writer with memory buffer
|
||||
type Writer struct {
|
||||
gin.ResponseWriter
|
||||
body *bytes.Buffer
|
||||
headers http.Header
|
||||
mu sync.Mutex
|
||||
timeout bool
|
||||
wroteHeaders bool
|
||||
code int
|
||||
}
|
||||
|
||||
// NewWriter will return a timeout.Writer pointer
|
||||
func NewWriter(w gin.ResponseWriter, buf *bytes.Buffer) *Writer {
|
||||
return &Writer{ResponseWriter: w, body: buf, headers: make(http.Header)}
|
||||
}
|
||||
|
||||
// Write will write data to response body
|
||||
func (w *Writer) Write(data []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.timeout || w.body == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return w.body.Write(data)
|
||||
}
|
||||
|
||||
// WriteHeader sends an HTTP response header with the provided status code.
|
||||
// If the response writer has already written headers or if a timeout has occurred,
|
||||
// this method does nothing.
|
||||
func (w *Writer) WriteHeader(code int) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.timeout || w.wroteHeaders {
|
||||
return
|
||||
}
|
||||
|
||||
// gin is using -1 to skip writing the status code
|
||||
// see https://github.com/gin-gonic/gin/blob/a0acf1df2814fcd828cb2d7128f2f4e2136d3fac/response_writer.go#L61
|
||||
if code == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
checkWriteHeaderCode(code)
|
||||
|
||||
w.writeHeader(code)
|
||||
w.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func (w *Writer) writeHeader(code int) {
|
||||
w.wroteHeaders = true
|
||||
w.code = code
|
||||
}
|
||||
|
||||
// Header will get response headers
|
||||
func (w *Writer) Header() http.Header {
|
||||
return w.headers
|
||||
}
|
||||
|
||||
// WriteString will write string to response body
|
||||
func (w *Writer) WriteString(s string) (int, error) {
|
||||
return w.Write([]byte(s))
|
||||
}
|
||||
|
||||
// FreeBuffer will release buffer pointer
|
||||
func (w *Writer) FreeBuffer() {
|
||||
// if not reset body,old bytes will put in bufPool
|
||||
w.body.Reset()
|
||||
w.body = nil
|
||||
}
|
||||
|
||||
// Status we must override Status func here,
|
||||
// or the http status code returned by gin.Context.Writer.Status()
|
||||
// will always be 200 in other custom gin middlewares.
|
||||
func (w *Writer) Status() int {
|
||||
if w.code == 0 || w.timeout {
|
||||
return w.ResponseWriter.Status()
|
||||
}
|
||||
return w.code
|
||||
}
|
||||
|
||||
func checkWriteHeaderCode(code int) {
|
||||
if code < 100 || code > 999 {
|
||||
panic(fmt.Sprintf("invalid http status code: %d", code))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package timeout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gitverse.ru/andoma/gin"
|
||||
)
|
||||
|
||||
func TestWriteHeader(t *testing.T) {
|
||||
code1 := 99
|
||||
errmsg1 := fmt.Sprintf("invalid http status code: %d", code1)
|
||||
code2 := 1000
|
||||
errmsg2 := fmt.Sprintf("invalid http status code: %d", code2)
|
||||
|
||||
writer := Writer{}
|
||||
assert.PanicsWithValue(t, errmsg1, func() {
|
||||
writer.WriteHeader(code1)
|
||||
})
|
||||
assert.PanicsWithValue(t, errmsg2, func() {
|
||||
writer.WriteHeader(code2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWriteHeader_SkipMinusOne(t *testing.T) {
|
||||
code := -1
|
||||
|
||||
writer := Writer{}
|
||||
assert.NotPanics(t, func() {
|
||||
writer.WriteHeader(code)
|
||||
assert.False(t, writer.wroteHeaders)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWriter_Status(t *testing.T) {
|
||||
r := gin.New()
|
||||
|
||||
r.Use(New(
|
||||
WithTimeout(1*time.Second),
|
||||
WithHandler(func(c *gin.Context) {
|
||||
c.Next()
|
||||
}),
|
||||
WithResponse(testResponse),
|
||||
))
|
||||
|
||||
r.Use(func(c *gin.Context) {
|
||||
c.Next()
|
||||
statusInMW := c.Writer.Status()
|
||||
c.Request.Header.Set("X-Status-Code-MW-Set", strconv.Itoa(statusInMW))
|
||||
t.Logf("[%s] %s %s %d\n", time.Now().Format(time.RFC3339), c.Request.Method, c.Request.URL, statusInMW)
|
||||
})
|
||||
|
||||
r.GET("/test", func(c *gin.Context) {
|
||||
c.Writer.WriteHeader(http.StatusInternalServerError)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
assert.Equal(t, strconv.Itoa(http.StatusInternalServerError), req.Header.Get("X-Status-Code-MW-Set"))
|
||||
}
|
||||
|
||||
// testNew is a copy of New() with a small change to the timeoutHandler() function.
|
||||
// ref: https://github.com/gin-contrib/timeout/issues/31
|
||||
func testNew(duration time.Duration) gin.HandlerFunc {
|
||||
return New(
|
||||
WithTimeout(duration),
|
||||
WithHandler(func(c *gin.Context) { c.Next() }),
|
||||
WithResponse(timeoutHandler()),
|
||||
)
|
||||
}
|
||||
|
||||
// timeoutHandler returns a handler that returns a 504 Gateway Timeout error.
|
||||
func timeoutHandler() gin.HandlerFunc {
|
||||
gatewayTimeoutErr := struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: "Timed out.",
|
||||
}
|
||||
|
||||
return func(c *gin.Context) {
|
||||
log.Printf("request timed out: [method=%s,path=%s]",
|
||||
c.Request.Method, c.Request.URL.Path)
|
||||
c.JSON(http.StatusGatewayTimeout, gatewayTimeoutErr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHTTPStatusCode tests the HTTP status code of the response.
|
||||
func TestHTTPStatusCode(t *testing.T) {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
type testCase struct {
|
||||
Name string
|
||||
Method string
|
||||
Path string
|
||||
ExpStatusCode int
|
||||
Handler gin.HandlerFunc
|
||||
}
|
||||
|
||||
var (
|
||||
cases = []testCase{
|
||||
{
|
||||
Name: "Plain text (200)",
|
||||
Method: http.MethodGet,
|
||||
Path: "/me",
|
||||
ExpStatusCode: http.StatusOK,
|
||||
Handler: func(ctx *gin.Context) {
|
||||
ctx.String(http.StatusOK, "I'm text!")
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Plain text (201)",
|
||||
Method: http.MethodGet,
|
||||
Path: "/me",
|
||||
ExpStatusCode: http.StatusCreated,
|
||||
Handler: func(ctx *gin.Context) {
|
||||
ctx.String(http.StatusCreated, "I'm created!")
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Plain text (204)",
|
||||
Method: http.MethodGet,
|
||||
Path: "/me",
|
||||
ExpStatusCode: http.StatusNoContent,
|
||||
Handler: func(ctx *gin.Context) {
|
||||
ctx.String(http.StatusNoContent, "")
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Plain text (400)",
|
||||
Method: http.MethodGet,
|
||||
Path: "/me",
|
||||
ExpStatusCode: http.StatusBadRequest,
|
||||
Handler: func(ctx *gin.Context) {
|
||||
ctx.String(http.StatusBadRequest, "")
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "JSON (200)",
|
||||
Method: http.MethodGet,
|
||||
Path: "/me",
|
||||
ExpStatusCode: http.StatusOK,
|
||||
Handler: func(ctx *gin.Context) {
|
||||
ctx.JSON(http.StatusOK, gin.H{"field": "value"})
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "JSON (201)",
|
||||
Method: http.MethodGet,
|
||||
Path: "/me",
|
||||
ExpStatusCode: http.StatusCreated,
|
||||
Handler: func(ctx *gin.Context) {
|
||||
ctx.JSON(http.StatusCreated, gin.H{"field": "value"})
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "JSON (204)",
|
||||
Method: http.MethodGet,
|
||||
Path: "/me",
|
||||
ExpStatusCode: http.StatusNoContent,
|
||||
Handler: func(ctx *gin.Context) {
|
||||
ctx.JSON(http.StatusNoContent, nil)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "JSON (400)",
|
||||
Method: http.MethodGet,
|
||||
Path: "/me",
|
||||
ExpStatusCode: http.StatusBadRequest,
|
||||
Handler: func(ctx *gin.Context) {
|
||||
ctx.JSON(http.StatusBadRequest, nil)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "No reply",
|
||||
Method: http.MethodGet,
|
||||
Path: "/me",
|
||||
ExpStatusCode: http.StatusOK,
|
||||
Handler: func(ctx *gin.Context) {},
|
||||
},
|
||||
}
|
||||
|
||||
initCase = func(c testCase) (*http.Request, *httptest.ResponseRecorder) {
|
||||
return httptest.NewRequest(c.Method, c.Path, nil), httptest.NewRecorder()
|
||||
}
|
||||
)
|
||||
|
||||
for i := range cases {
|
||||
t.Run(cases[i].Name, func(tt *testing.T) {
|
||||
tt.Logf("Test case [%s]", cases[i].Name)
|
||||
|
||||
router := gin.Default()
|
||||
|
||||
router.Use(testNew(1 * time.Second))
|
||||
router.GET("/*root", cases[i].Handler)
|
||||
|
||||
req, resp := initCase(cases[i])
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
if resp.Code != cases[i].ExpStatusCode {
|
||||
tt.Errorf("response is different from expected:\nexp: >>>%d<<<\ngot: >>>%d<<<",
|
||||
cases[i].ExpStatusCode, resp.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user