Compare commits

...

7 Commits

Author SHA1 Message Date
wanghaolong613
0c3e758cb0
Merge 016af753cf0f5a32ae2af9147a5d2e75418e499e into c3d5a28ed6d3849da820195b6774d212bcc038a9 2025-11-07 16:27:26 +07:00
Name
c3d5a28ed6
fix(gin): close os.File in RunFd to prevent resource leak (#4422)
Co-authored-by: 1911860538 <alxps1911@gmail.com>
2025-11-07 12:01:19 +08:00
Name
acc55e049e
feat(context): add Protocol Buffers support to content negotiation (#4423)
Co-authored-by: 1911860538 <alxps1911@gmail.com>
2025-11-07 11:59:58 +08:00
dependabot[bot]
0c0e99d253
chore(deps): bump github/codeql-action from 3 to 4 in the actions group (#4425)
Bumps the actions group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 3 to 4
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-07 11:57:41 +08:00
Bo-Yi Wu
dceb61e6e7
docs(README): add a Trivy security scan badge (#4426)
- Add a Trivy security scan badge to the documentation
- Import the log package in the example code
- Improve error handling for server startup in the example code

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-11-07 11:57:12 +08:00
Bo-Yi Wu
5e5ff3ace4
ci: replace vulnerability scanning workflow with Trivy integration (#4421)
- Remove the vulnerability-scanning job from the gin workflow
- Add a dedicated Trivy security scan workflow with scheduled, push, pull request, and manual triggers
- Improve Trivy scan output by uploading SARIF results to the GitHub Security tab and logging table output

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2025-11-06 14:15:50 +08:00
wanghaolong613
016af753cf optimization cleanPath 2025-10-16 15:13:24 +08:00
8 changed files with 266 additions and 56 deletions

View File

@ -81,19 +81,3 @@ jobs:
uses: codecov/codecov-action@v5
with:
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}
vulnerability-scanning:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Run Trivy vulnerability scanner in repo mode
uses: aquasecurity/trivy-action@0.33.1
with:
scan-type: "fs"
ignore-unfixed: true
format: "table"
exit-code: "1"
severity: "CRITICAL,HIGH,MEDIUM"

57
.github/workflows/trivy-scan.yml vendored Normal file
View File

@ -0,0 +1,57 @@
name: Trivy Security Scan
on:
push:
branches:
- master
pull_request:
branches:
- master
schedule:
# Run every 3 months (quarterly) on the 1st day at 00:00 UTC
# Months: January (1), April (4), July (7), October (10)
- cron: '0 0 1 1,4,7,10 *'
workflow_dispatch: # Allow manual trigger
permissions:
contents: read
security-events: write # Required for uploading SARIF results
jobs:
trivy-scan:
name: Trivy Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Run Trivy vulnerability scanner (source code)
uses: aquasecurity/trivy-action@0.33.1
with:
scan-type: 'fs'
scan-ref: '.'
scanners: 'vuln,secret,misconfig'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH,MEDIUM'
ignore-unfixed: true
- name: Upload Trivy results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: 'trivy-results.sarif'
- name: Run Trivy scanner (table output for logs)
uses: aquasecurity/trivy-action@0.33.1
if: always()
with:
scan-type: 'fs'
scan-ref: '.'
scanners: 'vuln,secret,misconfig'
format: 'table'
severity: 'CRITICAL,HIGH,MEDIUM'
ignore-unfixed: true
exit-code: '1'

View File

@ -3,6 +3,7 @@
<img align="right" width="159px" src="https://raw.githubusercontent.com/gin-gonic/logo/master/color.png">
[![Build Status](https://github.com/gin-gonic/gin/actions/workflows/gin.yml/badge.svg?branch=master)](https://github.com/gin-gonic/gin/actions/workflows/gin.yml)
[![Trivy Security Scan](https://github.com/gin-gonic/gin/actions/workflows/trivy-scan.yml/badge.svg)](https://github.com/gin-gonic/gin/actions/workflows/trivy-scan.yml)
[![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin)
[![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin)
[![Go Reference](https://pkg.go.dev/badge/github.com/gin-gonic/gin?status.svg)](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

View File

@ -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
}

View File

@ -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)

1
gin.go
View File

@ -593,6 +593,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

74
path.go
View File

@ -19,18 +19,29 @@ 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 "/"
}
n := len(p)
// If the path length is 1, handle special cases separately:
// - If it is "/" or ".", return "/".
// - Otherwise, prepend "/" to the path and return it.
if n == 1 {
if p[0] == '/' || p[0] == '.' {
return "/"
}
return "/" + p
}
const stackBufSize = 128
// Reasonably sized buffer on stack to avoid allocations in the common case.
// If a larger buffer is required, it gets allocated dynamically.
buf := make([]byte, 0, stackBufSize)
n := len(p)
// Invariants:
// reading from path; r is index of next byte to process.
// writing to buf; w is index of next byte to write.
@ -50,7 +61,7 @@ func cleanPath(p string) string {
buf[0] = '/'
}
trailing := n > 1 && p[n-1] == '/'
var trailing bool
// A bit more clunky without a 'lazybuf' like the path package, but the loop
// gets completely inlined (bufApp calls).
@ -58,38 +69,44 @@ func cleanPath(p string) string {
// calls (except make, if needed).
for r < n {
switch {
case p[r] == '/':
switch p[r] {
case '/':
// empty path element, trailing slash is added after the end
r++
case p[r] == '.' && r+1 == n:
trailing = true
r++
case '.':
if r+1 == n {
trailing = true
r++
// Reduce one comparison between r and n
goto endOfLoop
}
switch p[r+1] {
case '/':
// . element
r += 2
case p[r] == '.' && p[r+1] == '/':
// . element
r += 2
case '.':
if r+2 == n || p[r+2] == '/' {
// .. element: remove to last /
r += 3
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
// .. element: remove to last /
r += 3
if w > 1 {
// can backtrack
w--
if len(buf) == 0 {
for w > 1 && p[w] != '/' {
w--
}
} else {
for w > 1 && buf[w] != '/' {
if w > 1 {
// can backtrack
w--
if len(buf) == 0 {
for w > 1 && p[w] != '/' {
w--
}
} else {
for w > 1 && buf[w] != '/' {
w--
}
}
}
}
}
default:
// Real path element.
// Add slash if needed
@ -107,8 +124,9 @@ func cleanPath(p string) string {
}
}
endOfLoop:
// Re-append trailing slash
if trailing && w > 1 {
if (trailing || p[n-1] == '/') && w > 1 {
bufApp(&buf, p, w, '/')
w++
}

View File

@ -27,6 +27,7 @@ var cleanTests = []cleanPathTest{
// missing root
{"", "/"},
{"a", "/a"},
{"a/", "/a/"},
{"abc", "/abc"},
{"abc/def", "/abc/def"},
@ -143,3 +144,117 @@ func BenchmarkPathCleanLong(b *testing.B) {
}
}
}
func cleanPathOld(p string) string {
const stackBufSize = 128
// Turn empty string into "/"
if p == "" {
return "/"
}
// Reasonably sized buffer on stack to avoid allocations in the common case.
// If a larger buffer is required, it gets allocated dynamically.
buf := make([]byte, 0, stackBufSize)
n := len(p)
// Invariants:
// reading from path; r is index of next byte to process.
// writing to buf; w is index of next byte to write.
// path must start with '/'
r := 1
w := 1
if p[0] != '/' {
r = 0
if n+1 > stackBufSize {
buf = make([]byte, n+1)
} else {
buf = buf[:n+1]
}
buf[0] = '/'
}
trailing := n > 1 && p[n-1] == '/'
// A bit more clunky without a 'lazybuf' like the path package, but the loop
// gets completely inlined (bufApp calls).
// loop has no expensive function calls (except 1x make) // So in contrast to the path package this loop has no expensive function
// calls (except make, if needed).
for r < n {
switch {
case p[r] == '/':
// empty path element, trailing slash is added after the end
r++
case p[r] == '.' && r+1 == n:
trailing = true
r++
case p[r] == '.' && p[r+1] == '/':
// . element
r += 2
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
// .. element: remove to last /
r += 3
if w > 1 {
// can backtrack
w--
if len(buf) == 0 {
for w > 1 && p[w] != '/' {
w--
}
} else {
for w > 1 && buf[w] != '/' {
w--
}
}
}
default:
// Real path element.
// Add slash if needed
if w > 1 {
bufApp(&buf, p, w, '/')
w++
}
// Copy element
for r < n && p[r] != '/' {
bufApp(&buf, p, w, p[r])
w++
r++
}
}
}
// Re-append trailing slash
if trailing && w > 1 {
bufApp(&buf, p, w, '/')
w++
}
// 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 p[:w]
}
return string(buf[:w])
}
func BenchmarkPathCleanOld(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, test := range cleanTests {
cleanPathOld(test.path)
}
}
}