fix(turnstile): route CAPTCHA verification through the configured proxy (#38412) (#38420)

Backport #38412

Fixes #38217

Co-authored-by: Lovepreet Singh <lovepreet.singh61182@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Giteabot
2026-07-12 05:39:14 -07:00
committed by GitHub
parent 6815d1646c
commit d4250bafd9
4 changed files with 150 additions and 1 deletions

View File

@@ -12,9 +12,20 @@ import (
"strings"
"gitea.dev/modules/json"
"gitea.dev/modules/proxy"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
)
// httpClient returns an HTTP client that honors Gitea's proxy configuration.
var httpClient = util.OnceValue[*http.Client]{
Func: func() *http.Client {
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.Proxy = proxy.Proxy()
return &http.Client{Transport: transport}
},
}
// Response is the structure of JSON returned from API
type Response struct {
Success bool `json:"success"`
@@ -40,7 +51,7 @@ func Verify(ctx context.Context, response string) (bool, error) {
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
resp, err := httpClient.Value().Do(req)
if err != nil {
return false, fmt.Errorf("Failed to send CAPTCHA response: %w", err)
}

View File

@@ -0,0 +1,38 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package turnstile
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHTTPClientHonorsProxy(t *testing.T) {
proxyURL, err := url.Parse("http://proxy.example.com:3128")
require.NoError(t, err)
defer test.MockVariableValue(&setting.Proxy.Enabled, true)()
defer test.MockVariableValue(&setting.Proxy.ProxyURL, proxyURL.String())()
defer test.MockVariableValue(&setting.Proxy.ProxyURLFixed, proxyURL)()
defer test.MockVariableValue(&setting.Proxy.ProxyHosts, []string{"**"})()
httpClient.Reset()
transport, ok := httpClient.Value().Transport.(*http.Transport)
require.True(t, ok)
require.NotNil(t, transport.Proxy)
// The Turnstile verification request must be routed through the configured proxy.
req := httptest.NewRequest(http.MethodPost, "https://any.example.com", nil)
got, err := transport.Proxy(req)
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, proxyURL.String(), got.String())
}

51
modules/util/sync.go Normal file
View File

@@ -0,0 +1,51 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package util
import (
"sync"
"sync/atomic"
)
type onceValueResult[T any] struct {
value T
panic any
}
// OnceValue is similar to Golang's "sync.OnceValue", but can be reset.
type OnceValue[T any] struct {
Func func() T
mu sync.Mutex
res atomic.Pointer[onceValueResult[T]]
}
func (o *OnceValue[T]) Value() T {
res := o.res.Load()
if res == nil {
o.mu.Lock()
defer o.mu.Unlock()
res = o.res.Load()
if res == nil {
res = &onceValueResult[T]{}
defer func() {
res.panic = recover()
o.res.Store(res)
if res.panic != nil {
panic(res.panic)
}
}()
res.value = o.Func()
}
}
if res.panic != nil {
panic(res.panic)
}
return res.value
}
func (o *OnceValue[T]) Reset() {
o.mu.Lock()
defer o.mu.Unlock()
o.res.Store(nil)
}

49
modules/util/sync_test.go Normal file
View File

@@ -0,0 +1,49 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package util
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestOnceValue(t *testing.T) {
t.Run("RepeatCall", func(t *testing.T) {
callCount := 0
o := OnceValue[int]{Func: func() int {
callCount++
return 42
}}
assert.Equal(t, 42, o.Value())
assert.Equal(t, 42, o.Value())
assert.Equal(t, 1, callCount)
o.Reset()
assert.Equal(t, 42, o.Value())
assert.Equal(t, 2, callCount)
assert.Equal(t, 42, o.Value())
assert.Equal(t, 2, callCount)
})
t.Run("Panic", func(t *testing.T) {
callCount := 0
doPanic := true
o := OnceValue[int]{Func: func() int {
callCount++
if doPanic {
panic("some error")
}
return 42
}}
assert.PanicsWithValue(t, "some error", func() { o.Value() })
assert.PanicsWithValue(t, "some error", func() { o.Value() })
assert.Equal(t, 1, callCount)
doPanic = false
o.Reset()
assert.Equal(t, 42, o.Value())
assert.Equal(t, 2, callCount)
assert.Equal(t, 42, o.Value())
assert.Equal(t, 2, callCount)
})
}