mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-09 21:00:58 +00:00
c56f6447a8
Frontend deps: @hookform/resolvers 5.4.3 -> 5.5.7, Storybook 10.5.4 -> 10.5.5 across the four packages we declare, globals 17.7.0 -> 17.8.0, and jsdom 29.1.1 -> 30.0.1. The jsdom major replaces its CSS and selector stack -- @asamuzakjp/css-color 5 -> 6, @asamuzakjp/dom-selector 7 -> 8, undici 7 -> 8, nwsapi and generational-cache folded into their parents, whatwg-url 17 nested underneath. Nothing in the Vitest suites reaches those directly and the whole frontend gate (typecheck, lint, tests, build, Storybook compile) is green. Panel frontend version to 0.6.0. Backend deps: mattn/go-sqlite3 1.14.48 -> 1.14.49 and valyala/fasthttp 1.72.0 -> 1.73.0, plus the golang.org/x/exp and genproto/googleapis/rpc indirect bumps that came with them. Go tests: modernize -fix output, covering range-over-int, sync.WaitGroup.Go in place of manual Add/Done pairs, maps.Copy, and Go 1.26 new(expr) for pointer-to-value in the forwarded-trust table. The storedAs helper is deleted instead of being left behind a //go:fix inline directive -- keeping it that way fails govet on the one call site the rewrite did not reach, and every caller now takes new(...) directly. Behaviour is unchanged. DnsTab: the hosts-sync effect tested dns while declaring dnsEnabled in its dependency array. Both carry the same truth value, so this is exhaustive-deps hygiene rather than a behaviour change.
202 lines
6.4 KiB
Go
202 lines
6.4 KiB
Go
package sub
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
|
)
|
|
|
|
func requestFrom(t *testing.T, remoteAddr string, headers map[string]string) *gin.Context {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodGet, "/sub/abc", nil)
|
|
req.Host = "panel.example.com:2096"
|
|
req.RemoteAddr = remoteAddr
|
|
for k, v := range headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
c.Request = req
|
|
return c
|
|
}
|
|
|
|
func setTrustedProxyCIDRs(t *testing.T, value string) {
|
|
t.Helper()
|
|
if err := database.GetDB().Create(&model.Setting{Key: "trustedProxyCIDRs", Value: value}).Error; err != nil {
|
|
t.Fatalf("set trustedProxyCIDRs: %v", err)
|
|
}
|
|
settingService := service.SettingService{}
|
|
stored, err := settingService.GetTrustedProxyCIDRs()
|
|
if err != nil {
|
|
t.Fatalf("read trustedProxyCIDRs back through SettingService: %v", err)
|
|
}
|
|
if stored != value {
|
|
t.Fatalf("SettingService reads trustedProxyCIDRs as %q, want %q — the key this helper writes has drifted", stored, value)
|
|
}
|
|
}
|
|
|
|
func TestResolveRequest_ForwardedHeaderTrust(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
stored *string
|
|
remoteAddr string
|
|
wantScheme string
|
|
wantHost string
|
|
wantHostWithPort string
|
|
wantHostHeader string
|
|
}{
|
|
{
|
|
name: "no stored row keeps trusting forwarded headers",
|
|
stored: nil,
|
|
remoteAddr: "203.0.113.9:51000",
|
|
wantScheme: "https",
|
|
wantHost: "sub.example.net",
|
|
wantHostWithPort: "sub.example.net",
|
|
wantHostHeader: "sub.example.net",
|
|
},
|
|
{
|
|
name: "empty stored value keeps trusting forwarded headers",
|
|
stored: new(""),
|
|
remoteAddr: "203.0.113.9:51000",
|
|
wantScheme: "https",
|
|
wantHost: "sub.example.net",
|
|
wantHostWithPort: "sub.example.net",
|
|
wantHostHeader: "sub.example.net",
|
|
},
|
|
{
|
|
name: "stored shipped default keeps trusting forwarded headers",
|
|
stored: new(service.DefaultTrustedProxyCIDRs),
|
|
remoteAddr: "203.0.113.9:51000",
|
|
wantScheme: "https",
|
|
wantHost: "sub.example.net",
|
|
wantHostWithPort: "sub.example.net",
|
|
wantHostHeader: "sub.example.net",
|
|
},
|
|
{
|
|
name: "declared boundary ignores an origin outside it",
|
|
stored: new("10.0.0.0/8"),
|
|
remoteAddr: "203.0.113.9:51000",
|
|
wantScheme: "http",
|
|
wantHost: "panel.example.com",
|
|
wantHostWithPort: "panel.example.com:2096",
|
|
wantHostHeader: "panel.example.com",
|
|
},
|
|
{
|
|
name: "declared boundary trusts an origin inside it",
|
|
stored: new("10.0.0.0/8"),
|
|
remoteAddr: "10.1.2.3:44000",
|
|
wantScheme: "https",
|
|
wantHost: "sub.example.net",
|
|
wantHostWithPort: "sub.example.net",
|
|
wantHostHeader: "sub.example.net",
|
|
},
|
|
{
|
|
name: "declared boundary ignores an unparsable origin",
|
|
stored: new("10.0.0.0/8"),
|
|
remoteAddr: "not-an-ip",
|
|
wantScheme: "http",
|
|
wantHost: "panel.example.com",
|
|
wantHostWithPort: "panel.example.com:2096",
|
|
wantHostHeader: "panel.example.com",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
initSubDB(t)
|
|
if tc.stored != nil {
|
|
setTrustedProxyCIDRs(t, *tc.stored)
|
|
}
|
|
s := &SubService{}
|
|
|
|
c := requestFrom(t, tc.remoteAddr, map[string]string{
|
|
"X-Forwarded-Host": "sub.example.net",
|
|
"X-Forwarded-Proto": "https",
|
|
})
|
|
scheme, host, hostWithPort, hostHeader := s.ResolveRequest(c)
|
|
|
|
if scheme != tc.wantScheme {
|
|
t.Errorf("scheme = %q, want %q", scheme, tc.wantScheme)
|
|
}
|
|
if host != tc.wantHost {
|
|
t.Errorf("host = %q, want %q", host, tc.wantHost)
|
|
}
|
|
if hostWithPort != tc.wantHostWithPort {
|
|
t.Errorf("hostWithPort = %q, want %q", hostWithPort, tc.wantHostWithPort)
|
|
}
|
|
if hostHeader != tc.wantHostHeader {
|
|
t.Errorf("hostHeader = %q, want %q", hostHeader, tc.wantHostHeader)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveRequest_GatesRealIPFallback(t *testing.T) {
|
|
initSubDB(t)
|
|
setTrustedProxyCIDRs(t, "10.0.0.0/8")
|
|
s := &SubService{}
|
|
|
|
c := requestFrom(t, "203.0.113.9:51000", map[string]string{
|
|
"X-Real-IP": "198.51.100.7",
|
|
})
|
|
_, host, _, hostHeader := s.ResolveRequest(c)
|
|
|
|
if host != "panel.example.com" {
|
|
t.Errorf("host = %q, want the request host — X-Real-IP from an untrusted origin must be ignored", host)
|
|
}
|
|
if hostHeader != "panel.example.com" {
|
|
t.Errorf("hostHeader = %q, want the request host", hostHeader)
|
|
}
|
|
}
|
|
|
|
func TestHasForwardedHeaders(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
headers map[string]string
|
|
want bool
|
|
}{
|
|
{name: "no forwarded headers", want: false},
|
|
{name: "forwarded host", headers: map[string]string{"X-Forwarded-Host": "sub.example.net"}, want: true},
|
|
{name: "forwarded proto", headers: map[string]string{"X-Forwarded-Proto": "https"}, want: true},
|
|
{name: "real ip", headers: map[string]string{"X-Real-IP": "10.1.2.3"}, want: true},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := hasForwardedHeaders(requestFrom(t, "10.1.2.3:1234", tc.headers)); got != tc.want {
|
|
t.Errorf("hasForwardedHeaders() = %v, want %v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRemoteAddrInCIDRs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
remoteAddr string
|
|
cidrs string
|
|
want bool
|
|
}{
|
|
{name: "inside cidr", remoteAddr: "10.1.2.3:1234", cidrs: "10.0.0.0/8", want: true},
|
|
{name: "ipv4 mapped address inside cidr", remoteAddr: "[::ffff:10.1.2.3]:1234", cidrs: "10.0.0.0/8", want: true},
|
|
{name: "outside cidr", remoteAddr: "203.0.113.9:1234", cidrs: "10.0.0.0/8", want: false},
|
|
{name: "bare address entry", remoteAddr: "192.168.1.5:80", cidrs: "192.168.1.5", want: true},
|
|
{name: "ipv6 loopback", remoteAddr: "[::1]:8080", cidrs: "::1/128", want: true},
|
|
{name: "no port", remoteAddr: "10.1.2.3", cidrs: "10.0.0.0/8", want: true},
|
|
{name: "unparsable origin", remoteAddr: "not-an-ip", cidrs: "10.0.0.0/8", want: false},
|
|
{name: "empty entries skipped", remoteAddr: "10.1.2.3:1", cidrs: " , 10.0.0.0/8 , ", want: true},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := remoteAddrInCIDRs(tc.remoteAddr, tc.cidrs); got != tc.want {
|
|
t.Errorf("remoteAddrInCIDRs(%q, %q) = %v, want %v", tc.remoteAddr, tc.cidrs, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|