Compare commits

...

13 Commits

Author SHA1 Message Date
Pratham Gadkari
c26314d8a8
Merge 527f128f96bf4d9e412b8e7f01323c33910d069e into 9914178584e42458ff7d23891463a880f58c9d86 2026-01-09 10:35:51 +08:00
Nurysso
9914178584
fix(context): ClientIP handling for multiple X-Forwarded-For header values (#4472)
* Fix ClientIP calculation by concatenating all RemoteIPHeaders values

* test: used http.MethodGet instead constants and fix lints

* lint error fixed

* Refactor ClientIP X-Forwarded-For tests

---------

Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-01-02 10:15:27 +08:00
Paulo Henrique
915e4c90d2
refactor(context): replace hardcoded localhost IPs with constants (#4481) 2025-12-27 19:25:17 +08:00
Twacqwq
26c3a62865
chore(response): prevent Flush() panic when http.Flusher (#4479) 2025-12-24 18:35:20 +08:00
dependabot[bot]
22c274c84b
chore(deps): bump actions/cache from 4 to 5 in the actions group (#4469)
Bumps the actions group with 1 update: [actions/cache](https://github.com/actions/cache).


Updates `actions/cache` from 4 to 5
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  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-12-24 18:33:46 +08:00
Pratham Gadkari
527f128f96 combining and adding tests 2025-11-30 16:35:13 +05:30
Pratham Gadkari
b6e5603530 more tests because coverage is failing 2025-11-30 16:26:33 +05:30
Pratham Gadkari
822cf17bf9 modifying setArray and setSlice 2025-11-30 16:10:58 +05:30
Pratham Gadkari
6b6ac46ae3 using errors instead of fmt 2025-11-30 16:01:27 +05:30
Pratham Gadkari
ee1f501d4a more tests for code coverage 2025-11-30 15:58:56 +05:30
Bo-Yi Wu
ef09093976
Merge branch 'master' into fix-binding-custom-unmarhsal-4296 2025-11-30 15:41:08 +08:00
Pratham Gadkari
513a86e17f added tests for feature and coverage 2025-11-15 22:28:32 +05:30
Pratham Gadkari
b4d09053db fix(binding): support custom unmarshaler for slices & arrays 2025-11-14 23:44:50 +05:30
8 changed files with 169 additions and 10 deletions

View File

@ -65,7 +65,7 @@ jobs:
with:
ref: ${{ github.ref }}
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: |
${{ matrix.go-build }}

View File

@ -186,7 +186,10 @@ type BindUnmarshaler interface {
func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
switch v := value.Addr().Interface().(type) {
case BindUnmarshaler:
return true, v.UnmarshalParam(val)
if err := v.UnmarshalParam(val); err != nil {
return true, fmt.Errorf("invalid value %q: %w", val, err)
}
return true, nil
}
return false, nil
}
@ -245,7 +248,10 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
}
if ok, err = trySetCustom(vs[0], value); ok {
return ok, err
if err != nil {
return true, fmt.Errorf("field %q: %w", field.Name, err)
}
return true, nil
}
if vs, err = trySplit(vs, field); err != nil {
@ -268,7 +274,10 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
}
if ok, err = trySetCustom(vs[0], value); ok {
return ok, err
if err != nil {
return true, fmt.Errorf("field %q: %w", field.Name, err)
}
return true, nil
}
if vs, err = trySplit(vs, field); err != nil {
@ -293,7 +302,10 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
}
}
if ok, err := trySetCustom(val, value); ok {
return ok, err
if err != nil {
return true, fmt.Errorf("field %q: %w", field.Name, err)
}
return true, nil
}
return true, setWithProperType(val, value, field)
}
@ -461,6 +473,12 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
func setArray(vals []string, value reflect.Value, field reflect.StructField) error {
for i, s := range vals {
if ok, err := trySetCustom(s, value.Index(i)); ok {
if err != nil {
return fmt.Errorf("field %q: %w", field.Name, err)
}
continue
}
err := setWithProperType(s, value.Index(i), field)
if err != nil {
return err

View File

@ -754,3 +754,104 @@ func TestMappingEmptyValues(t *testing.T) {
assert.Equal(t, []int{1, 2, 3}, s.SliceCsv)
})
}
type testCustom struct {
Value string
}
func (t *testCustom) UnmarshalParam(param string) error {
t.Value = "prefix_" + param
return nil
}
func TestTrySetCustomIntegration(t *testing.T) {
var s struct {
F testCustom `form:"f"`
}
err := mappingByPtr(&s, formSource{"f": {"hello"}}, "form")
require.NoError(t, err)
assert.Equal(t, "prefix_hello", s.F.Value)
}
type badCustom struct{}
func (b *badCustom) UnmarshalParam(s string) error {
return errors.New("boom")
}
func TestTrySetCustom_SingleValues(t *testing.T) {
t.Run("Error case", func(t *testing.T) {
var s struct {
F badCustom `form:"f"`
}
err := mappingByPtr(&s, formSource{"f": {"hello"}}, "form")
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid value")
})
t.Run("Integration success", func(t *testing.T) {
var s struct {
F testCustom `form:"f"`
}
err := mappingByPtr(&s, formSource{"f": {"hello"}}, "form")
require.NoError(t, err)
assert.Equal(t, "prefix_hello", s.F.Value)
})
t.Run("Not applicable type", func(t *testing.T) {
var s struct {
N int `form:"n"`
}
err := mappingByPtr(&s, formSource{"n": {"42"}}, "form")
require.NoError(t, err)
assert.Equal(t, 42, s.N)
})
}
func TestTrySetCustom_Collections(t *testing.T) {
t.Run("Slice success", func(t *testing.T) {
var s struct {
F []testCustom `form:"f"`
}
err := mappingByPtr(&s, formSource{"f": {"one", "two"}}, "form")
require.NoError(t, err)
assert.Equal(t, "prefix_one", s.F[0].Value)
assert.Equal(t, "prefix_two", s.F[1].Value)
})
t.Run("Array success", func(t *testing.T) {
var s struct {
F [2]testCustom `form:"f"`
}
err := mappingByPtr(&s, formSource{"f": {"hello", "world"}}, "form")
require.NoError(t, err)
assert.Equal(t, "prefix_hello", s.F[0].Value)
assert.Equal(t, "prefix_world", s.F[1].Value)
})
t.Run("Slice error", func(t *testing.T) {
var s struct {
F []badCustom `form:"f"`
}
err := mappingByPtr(&s, formSource{"f": {"oops1", "oops2"}}, "form")
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid value")
})
t.Run("Array error", func(t *testing.T) {
var s struct {
F [2]badCustom `form:"f"`
}
err := mappingByPtr(&s, formSource{"f": {"fail1", "fail2"}}, "form")
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid value")
})
}

View File

@ -989,7 +989,8 @@ func (c *Context) ClientIP() string {
if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
for _, headerName := range c.engine.RemoteIPHeaders {
ip, valid := c.engine.validateHeader(c.requestHeader(headerName))
headerValue := strings.Join(c.Request.Header.Values(headerName), ",")
ip, valid := c.engine.validateHeader(headerValue)
if valid {
return ip
}

View File

@ -1143,6 +1143,37 @@ func TestContextRenderNoContentIndentedJSON(t *testing.T) {
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestContextClientIPWithMultipleHeaders(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/test", nil)
// Multiple X-Forwarded-For headers
c.Request.Header.Add("X-Forwarded-For", "1.2.3.4, "+localhostIP)
c.Request.Header.Add("X-Forwarded-For", "5.6.7.8")
c.Request.RemoteAddr = localhostIP + ":1234"
c.engine.ForwardedByClientIP = true
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
_ = c.engine.SetTrustedProxies([]string{localhostIP})
// Should return 5.6.7.8 (last non-trusted IP)
assert.Equal(t, "5.6.7.8", c.ClientIP())
}
func TestContextClientIPWithSingleHeader(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/test", nil)
c.Request.Header.Set("X-Forwarded-For", "1.2.3.4, "+localhostIP)
c.Request.RemoteAddr = localhostIP + ":1234"
c.engine.ForwardedByClientIP = true
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
_ = c.engine.SetTrustedProxies([]string{localhostIP})
// Should return 1.2.3.4
assert.Equal(t, "1.2.3.4", c.ClientIP())
}
// Tests that the response is serialized as Secure JSON
// and Content-Type is set to application/json
func TestContextRenderSecureJSON(t *testing.T) {
@ -1910,7 +1941,7 @@ func TestContextClientIP(t *testing.T) {
resetContextForClientIPTests(c)
// IPv6 support
c.Request.RemoteAddr = "[::1]:12345"
c.Request.RemoteAddr = fmt.Sprintf("[%s]:12345", localhostIPv6)
assert.Equal(t, "20.20.20.20", c.ClientIP())
resetContextForClientIPTests(c)
@ -3212,7 +3243,7 @@ func TestContextCopyShouldNotCancel(t *testing.T) {
}()
addr := strings.Split(l.Addr().String(), ":")
res, err := http.Get(fmt.Sprintf("http://127.0.0.1:%s/", addr[len(addr)-1]))
res, err := http.Get(fmt.Sprintf("http://%s:%s/", localhostIP, addr[len(addr)-1]))
if err != nil {
t.Error(fmt.Errorf("request error: %w", err))
return

View File

@ -83,7 +83,7 @@ func TestLoadHTMLGlobDebugMode(t *testing.T) {
}
func TestH2c(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
ln, err := net.Listen("tcp", localhostIP+":0")
if err != nil {
t.Error(err)
}

View File

@ -128,7 +128,9 @@ func (w *responseWriter) CloseNotify() <-chan bool {
// Flush implements the http.Flusher interface.
func (w *responseWriter) Flush() {
w.WriteHeaderNow()
w.ResponseWriter.(http.Flusher).Flush()
if f, ok := w.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
func (w *responseWriter) Pusher() (pusher http.Pusher) {

View File

@ -19,6 +19,12 @@ import (
// BindKey indicates a default bind key.
const BindKey = "_gin-gonic/gin/bindkey"
// localhostIP indicates the default localhost IP address.
const localhostIP = "127.0.0.1"
// localhostIPv6 indicates the default localhost IPv6 address.
const localhostIPv6 = "::1"
// Bind is a helper function for given interface object and returns a Gin middleware.
func Bind(val any) HandlerFunc {
value := reflect.ValueOf(val)