first commit

This commit is contained in:
2024-03-29 11:40:39 +03:00
commit 1a1ce70a5c
62 changed files with 4080 additions and 0 deletions
+15
View File
@@ -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/
+21
View File
@@ -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.
+26
View File
@@ -0,0 +1,26 @@
# gin contrib
gin contrib is middleware utilities for [Gin](https://gitverse.ru/andoma/gin)
[![GoDoc](https://godoc.org/gitverse.ru/andoma/gin-contrib?status.svg)](https://godoc.org/gitverse.ru/andoma/gin-contrib)
[![Go Report Card](https://goreportcard.com/badge/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.
+38
View File
@@ -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
)
+24
View File
@@ -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)
}
}
+12
View File
@@ -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")
}
+21
View File
@@ -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")
}
+12
View File
@@ -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")
}
+32
View File
@@ -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)
}
}
+42
View File
@@ -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)
}
}
+33
View File
@@ -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")
}
+26
View File
@@ -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)
}
}
+29
View File
@@ -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)
}
}
+36
View File
@@ -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)
}
}
+41
View File
@@ -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
+93
View File
@@ -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=
+21
View File
@@ -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
View File
@@ -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)
}
}
```
+39
View File
@@ -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)
}
+255
View File
@@ -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)
}
+83
View File
@@ -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
View File
@@ -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
}
+21
View File
@@ -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.
+35
View File
@@ -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")
}
```
+61
View File
@@ -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)
}
}
}
+94
View File
@@ -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")
}
+18
View File
@@ -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
}
}
+21
View File
@@ -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.
+56
View File
@@ -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)
}
}
}
+35
View File
@@ -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)
}
}
}
+21
View File
@@ -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.
+98
View File
@@ -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
```
+50
View File
@@ -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")))
}
}
+66
View File
@@ -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)
}
}
+21
View File
@@ -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.
+72
View File
@@ -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))
})
```
+36
View File
@@ -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
}
}
+56
View File
@@ -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)
}
+131
View File
@@ -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)
}
+21
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
# secure
gin secure middleware
Copied from [gin-gonic/contrib](https://github.com/gin-gonic/contrib)
+179
View File
@@ -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)
}
}
}
}
+471
View File
@@ -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))
}
}
+21
View File
@@ -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.
+113
View File
@@ -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")
}
```
+62
View File
@@ -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))
}
+186
View File
@@ -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)
}
}
+49
View File
@@ -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,
}
}
+35
View File
@@ -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)
}
+42
View File
@@ -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()
}
}
}
+157
View File
@@ -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")
}
+1
View File
@@ -0,0 +1 @@
<h1>Hello Embed</h1>
+1
View File
@@ -0,0 +1 @@
<h1>Hello Gin Static</h1>
+21
View File
@@ -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.
+101
View File
@@ -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)
}
}
```
+26
View File
@@ -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)
}
+16
View File
@@ -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)
}
+43
View File
@@ -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
}
+93
View File
@@ -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
}
}
}
+97
View File
@@ -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())
}
+99
View File
@@ -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))
}
}
+215
View File
@@ -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)
}
})
}
}