mirror of
https://github.com/gin-gonic/gin.git
synced 2026-07-06 10:21:17 +08:00
Compare commits
7 Commits
196e45a40e
...
cf01a2ef5a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf01a2ef5a | ||
|
|
c3d5a28ed6 | ||
|
|
acc55e049e | ||
|
|
0c0e99d253 | ||
|
|
dceb61e6e7 | ||
|
|
d661e32716 | ||
|
|
ee1fd5609e |
2
.github/workflows/trivy-scan.yml
vendored
2
.github/workflows/trivy-scan.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
||||
ignore-unfixed: true
|
||||
|
||||
- name: Upload Trivy results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
if: always()
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
|
||||
11
README.md
11
README.md
@ -3,6 +3,7 @@
|
||||
<img align="right" width="159px" src="https://raw.githubusercontent.com/gin-gonic/logo/master/color.png">
|
||||
|
||||
[](https://github.com/gin-gonic/gin/actions/workflows/gin.yml)
|
||||
[](https://github.com/gin-gonic/gin/actions/workflows/trivy-scan.yml)
|
||||
[](https://codecov.io/gh/gin-gonic/gin)
|
||||
[](https://goreportcard.com/report/github.com/gin-gonic/gin)
|
||||
[](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc)
|
||||
@ -62,6 +63,7 @@ Here's a complete example that demonstrates Gin's simplicity:
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@ -70,7 +72,7 @@ import (
|
||||
func main() {
|
||||
// Create a Gin router with default middleware (logger and recovery)
|
||||
r := gin.Default()
|
||||
|
||||
|
||||
// Define a simple GET endpoint
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
// Return JSON response
|
||||
@ -78,10 +80,12 @@ func main() {
|
||||
"message": "pong",
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
// Start server on port 8080 (default)
|
||||
// Server will listen on 0.0.0.0:8080 (localhost:8080 on Windows)
|
||||
r.Run()
|
||||
if err := r.Run(); err != nil {
|
||||
log.Fatalf("failed to run server: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -190,7 +194,6 @@ Gin has a rich ecosystem of middleware for common web development needs. Explore
|
||||
- CORS, Rate limiting, Compression
|
||||
- Logging, Metrics, Tracing
|
||||
- Static file serving, Template engines
|
||||
|
||||
- **[gin-gonic/contrib](https://github.com/gin-gonic/contrib)** - Additional community middleware
|
||||
|
||||
## 🏢 Production Usage
|
||||
|
||||
22
context.go
22
context.go
@ -39,6 +39,7 @@ const (
|
||||
MIMEYAML = binding.MIMEYAML
|
||||
MIMEYAML2 = binding.MIMEYAML2
|
||||
MIMETOML = binding.MIMETOML
|
||||
MIMEPROTOBUF = binding.MIMEPROTOBUF
|
||||
)
|
||||
|
||||
// BodyBytesKey indicates a default body bytes key.
|
||||
@ -1280,14 +1281,15 @@ func (c *Context) Stream(step func(w io.Writer) bool) bool {
|
||||
|
||||
// Negotiate contains all negotiations data.
|
||||
type Negotiate struct {
|
||||
Offered []string
|
||||
HTMLName string
|
||||
HTMLData any
|
||||
JSONData any
|
||||
XMLData any
|
||||
YAMLData any
|
||||
Data any
|
||||
TOMLData any
|
||||
Offered []string
|
||||
HTMLName string
|
||||
HTMLData any
|
||||
JSONData any
|
||||
XMLData any
|
||||
YAMLData any
|
||||
Data any
|
||||
TOMLData any
|
||||
PROTOBUFData any
|
||||
}
|
||||
|
||||
// Negotiate calls different Render according to acceptable Accept format.
|
||||
@ -1313,6 +1315,10 @@ func (c *Context) Negotiate(code int, config Negotiate) {
|
||||
data := chooseData(config.TOMLData, config.Data)
|
||||
c.TOML(code, data)
|
||||
|
||||
case binding.MIMEPROTOBUF:
|
||||
data := chooseData(config.PROTOBUFData, config.Data)
|
||||
c.ProtoBuf(code, data)
|
||||
|
||||
default:
|
||||
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck
|
||||
}
|
||||
|
||||
@ -1628,6 +1628,32 @@ func TestContextNegotiationWithHTML(t *testing.T) {
|
||||
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
func TestContextNegotiationWithPROTOBUF(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
|
||||
reps := []int64{int64(1), int64(2)}
|
||||
label := "test"
|
||||
data := &testdata.Test{
|
||||
Label: &label,
|
||||
Reps: reps,
|
||||
}
|
||||
|
||||
c.Negotiate(http.StatusCreated, Negotiate{
|
||||
Offered: []string{MIMEPROTOBUF, MIMEJSON, MIMEXML},
|
||||
Data: data,
|
||||
})
|
||||
|
||||
// Marshal original data for comparison
|
||||
protoData, err := proto.Marshal(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Equal(t, string(protoData), w.Body.String())
|
||||
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
func TestContextNegotiationNotSupport(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
20
gin.go
20
gin.go
@ -11,7 +11,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@ -46,9 +45,6 @@ var defaultTrustedCIDRs = []*net.IPNet{
|
||||
},
|
||||
}
|
||||
|
||||
var regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
|
||||
var regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
|
||||
|
||||
// HandlerFunc defines the handler used by gin middleware as return value.
|
||||
type HandlerFunc func(*Context)
|
||||
|
||||
@ -593,6 +589,7 @@ func (engine *Engine) RunFd(fd int) (err error) {
|
||||
}
|
||||
|
||||
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
|
||||
defer f.Close()
|
||||
listener, err := net.FileListener(f)
|
||||
if err != nil {
|
||||
return
|
||||
@ -749,8 +746,8 @@ func redirectTrailingSlash(c *Context) {
|
||||
req := c.Request
|
||||
p := req.URL.Path
|
||||
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
|
||||
prefix = regSafePrefix.ReplaceAllString(prefix, "")
|
||||
prefix = regRemoveRepeatedChar.ReplaceAllString(prefix, "/")
|
||||
prefix = sanitizePathChars(prefix)
|
||||
prefix = removeRepeatedChar(prefix, '/')
|
||||
|
||||
p = prefix + "/" + req.URL.Path
|
||||
}
|
||||
@ -761,6 +758,17 @@ func redirectTrailingSlash(c *Context) {
|
||||
redirectRequest(c)
|
||||
}
|
||||
|
||||
// sanitizePathChars removes unsafe characters from path strings,
|
||||
// keeping only ASCII letters, ASCII numbers, forward slashes, and hyphens.
|
||||
func sanitizePathChars(s string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '/' || r == '-' {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, s)
|
||||
}
|
||||
|
||||
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
|
||||
req := c.Request
|
||||
rPath := req.URL.Path
|
||||
|
||||
55
path.go
55
path.go
@ -5,6 +5,8 @@
|
||||
|
||||
package gin
|
||||
|
||||
const stackBufSize = 128
|
||||
|
||||
// cleanPath is the URL version of path.Clean, it returns a canonical URL path
|
||||
// for p, eliminating . and .. elements.
|
||||
//
|
||||
@ -19,7 +21,6 @@ package gin
|
||||
//
|
||||
// If the result of this process is an empty string, "/" is returned.
|
||||
func cleanPath(p string) string {
|
||||
const stackBufSize = 128
|
||||
// Turn empty string into "/"
|
||||
if p == "" {
|
||||
return "/"
|
||||
@ -148,3 +149,55 @@ func bufApp(buf *[]byte, s string, w int, c byte) {
|
||||
}
|
||||
b[w] = c
|
||||
}
|
||||
|
||||
// removeRepeatedChar removes multiple consecutive 'char's from a string.
|
||||
// if s == "/a//b///c////" && char == '/', it returns "/a/b/c/"
|
||||
func removeRepeatedChar(s string, char byte) string {
|
||||
// Check if there are any consecutive chars
|
||||
hasRepeatedChar := false
|
||||
for i := 1; i < len(s); i++ {
|
||||
if s[i] == char && s[i-1] == char {
|
||||
hasRepeatedChar = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasRepeatedChar {
|
||||
return s
|
||||
}
|
||||
|
||||
// Reasonably sized buffer on stack to avoid allocations in the common case.
|
||||
buf := make([]byte, 0, stackBufSize)
|
||||
|
||||
// Invariants:
|
||||
// reading from s; r is index of next byte to process.
|
||||
// writing to buf; w is index of next byte to write.
|
||||
r := 0
|
||||
w := 0
|
||||
|
||||
for n := len(s); r < n; {
|
||||
if s[r] == char {
|
||||
// Write the first char
|
||||
bufApp(&buf, s, w, char)
|
||||
w++
|
||||
r++
|
||||
|
||||
// Skip all consecutive chars
|
||||
for r < n && s[r] == char {
|
||||
r++
|
||||
}
|
||||
} else {
|
||||
// Copy non-char character
|
||||
bufApp(&buf, s, w, s[r])
|
||||
w++
|
||||
r++
|
||||
}
|
||||
}
|
||||
|
||||
// If the original string was not modified (or only shortened at the end),
|
||||
// return the respective substring of the original string.
|
||||
// Otherwise, return a new string from the buffer.
|
||||
if len(buf) == 0 {
|
||||
return s[:w]
|
||||
}
|
||||
return string(buf[:w])
|
||||
}
|
||||
|
||||
47
path_test.go
47
path_test.go
@ -143,3 +143,50 @@ func BenchmarkPathCleanLong(b *testing.B) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveRepeatedChar(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
str string
|
||||
char byte
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
str: "",
|
||||
char: 'a',
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "noSlash",
|
||||
str: "abc",
|
||||
char: ',',
|
||||
want: "abc",
|
||||
},
|
||||
{
|
||||
name: "withSlash",
|
||||
str: "/a/b/c/",
|
||||
char: '/',
|
||||
want: "/a/b/c/",
|
||||
},
|
||||
{
|
||||
name: "withRepeatedSlashes",
|
||||
str: "/a//b///c////",
|
||||
char: '/',
|
||||
want: "/a/b/c/",
|
||||
},
|
||||
{
|
||||
name: "threeSlashes",
|
||||
str: "///",
|
||||
char: '/',
|
||||
want: "/",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res := removeRepeatedChar(tc.str, tc.char)
|
||||
assert.Equal(t, tc.want, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user