mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
768bbd2a29
* feat(settings): allow customizing Reality scan candidate list Persist a realityScanCandidates panel setting (defaulting to the previous hardcoded list), expose it in General Settings with i18n, and have the Find Targets scanner use it when the search box is empty. Fixes #5847 * style(frontend): oxfmt realityScanCandidates in setting.ts * Fix locale JSON syntax This change removes the malformed duplicate key and missing comma in the Android per-app proxy translations across the bundled locale files. The JSON now parses correctly while preserving the translated labels for each language. * docs(i18n): update Happ translations Localize the remaining Happ subscription settings strings across the translation files and refine the English copy. This aligns the labels and descriptions with the current Happ behavior for notifications, TUN options, HWID enforcement, routing presets, and per-app proxy settings. * refactor(reality): drop test-only scaffolding from the candidate setting TestDefaultRealityScanCandidatesCSV compared the CSV against its own initializer and a defaultValueMap lookup, and TestRealityScanCandidateTokensFallsBackWithoutDB drove a no-database state no production caller reaches (the only caller is the scanRealityTargets handler, served after InitDB). The s != nil && GetDB() != nil guard existed only for that second test. None of them could fail except in lockstep with the code they restate. * docs(api): describe the setting-driven scanRealityTargets fallback An empty targets value now probes the realityScanCandidates setting, but the endpoint summary, parameter description and handler comment still promised the built-in seed list, so API consumers were told the wrong target set. Regenerated openapi.json and synced the docs copy, which also lacked the new AllSetting field in the settings reference. --------- Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
183 lines
5.0 KiB
Go
183 lines
5.0 KiB
Go
package service
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"net"
|
|
"testing"
|
|
)
|
|
|
|
func TestTLSVersionName(t *testing.T) {
|
|
cases := map[uint16]string{
|
|
tls.VersionTLS13: "1.3",
|
|
tls.VersionTLS12: "1.2",
|
|
tls.VersionTLS11: "1.1",
|
|
tls.VersionTLS10: "1.0",
|
|
0: "unknown",
|
|
}
|
|
for in, want := range cases {
|
|
if got := tlsVersionName(in); got != want {
|
|
t.Errorf("tlsVersionName(%d) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRealityCurveName(t *testing.T) {
|
|
cases := map[tls.CurveID]string{
|
|
tls.X25519: "X25519",
|
|
tls.X25519MLKEM768: "X25519MLKEM768",
|
|
tls.CurveP256: "P-256",
|
|
0: "",
|
|
}
|
|
for in, want := range cases {
|
|
if got := realityCurveName(in); got != want {
|
|
t.Errorf("realityCurveName(%d) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFilterUsableSANs(t *testing.T) {
|
|
got := filterUsableSANs([]string{"example.com", "*.example.com", "", " a.com "})
|
|
want := []string{"example.com", "a.com"}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("filterUsableSANs = %v, want %v", got, want)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Errorf("filterUsableSANs[%d] = %q, want %q", i, got[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSplitRealityTarget(t *testing.T) {
|
|
okCases := []struct {
|
|
in string
|
|
wantHost string
|
|
wantPort int
|
|
}{
|
|
{"example.com", "example.com", 443},
|
|
{"example.com:8443", "example.com", 8443},
|
|
{"1.1.1.1:443", "1.1.1.1", 443},
|
|
}
|
|
for _, c := range okCases {
|
|
host, port, err := splitRealityTarget(c.in)
|
|
if err != nil {
|
|
t.Errorf("splitRealityTarget(%q) unexpected error: %v", c.in, err)
|
|
continue
|
|
}
|
|
if host != c.wantHost || port != c.wantPort {
|
|
t.Errorf("splitRealityTarget(%q) = (%q, %d), want (%q, %d)", c.in, host, port, c.wantHost, c.wantPort)
|
|
}
|
|
}
|
|
|
|
badCases := []string{"", " ", "example.com:0", "example.com:70000", "bad host!"}
|
|
for _, in := range badCases {
|
|
if _, _, err := splitRealityTarget(in); err == nil {
|
|
t.Errorf("splitRealityTarget(%q) expected error, got nil", in)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScanRealityTargetInputValidation(t *testing.T) {
|
|
if _, err := (&ServerService{}).ScanRealityTarget("", "", 0, false); err == nil {
|
|
t.Error("ScanRealityTarget(empty) expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestScanRealityTargetBlocksPrivate(t *testing.T) {
|
|
res, err := (&ServerService{}).ScanRealityTarget("127.0.0.1:443", "", 0, false)
|
|
if err != nil {
|
|
t.Fatalf("ScanRealityTarget(loopback) unexpected error: %v", err)
|
|
}
|
|
if res.Feasible {
|
|
t.Error("ScanRealityTarget(loopback) should not be feasible")
|
|
}
|
|
if res.Reason == "" {
|
|
t.Error("ScanRealityTarget(loopback) should set a reason")
|
|
}
|
|
}
|
|
|
|
func TestScanRealityTargetsHandlesPrivateAndBadInput(t *testing.T) {
|
|
results, err := (&ServerService{}).ScanRealityTargets("127.0.0.1:443,10.0.0.1:443,bad host!")
|
|
if err != nil {
|
|
t.Fatalf("ScanRealityTargets unexpected error: %v", err)
|
|
}
|
|
if len(results) != 3 {
|
|
t.Fatalf("ScanRealityTargets returned %d results, want 3", len(results))
|
|
}
|
|
for _, r := range results {
|
|
if r.Feasible {
|
|
t.Errorf("result %q unexpectedly feasible", r.Target)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWriteProxyProtocolV1(t *testing.T) {
|
|
server, client := net.Pipe()
|
|
defer client.Close()
|
|
|
|
local := &net.TCPAddr{IP: net.ParseIP("192.0.2.10"), Port: 51234}
|
|
remote := &net.TCPAddr{IP: net.ParseIP("203.0.113.5"), Port: 443}
|
|
|
|
got := make(chan string, 1)
|
|
go func() {
|
|
buf := make([]byte, 128)
|
|
n, _ := server.Read(buf)
|
|
got <- string(buf[:n])
|
|
}()
|
|
|
|
if err := writeProxyProtocolV1(client, local, remote); err != nil {
|
|
t.Fatalf("writeProxyProtocolV1: %v", err)
|
|
}
|
|
line := <-got
|
|
want := "PROXY TCP4 192.0.2.10 203.0.113.5 51234 443\r\n"
|
|
if line != want {
|
|
t.Fatalf("v1 header = %q, want %q", line, want)
|
|
}
|
|
}
|
|
|
|
func TestWriteProxyProtocolV2Signature(t *testing.T) {
|
|
server, client := net.Pipe()
|
|
defer client.Close()
|
|
|
|
local := &net.TCPAddr{IP: net.ParseIP("192.0.2.10"), Port: 51234}
|
|
remote := &net.TCPAddr{IP: net.ParseIP("203.0.113.5"), Port: 443}
|
|
|
|
got := make(chan []byte, 1)
|
|
go func() {
|
|
buf := make([]byte, 128)
|
|
n, _ := server.Read(buf)
|
|
got <- append([]byte(nil), buf[:n]...)
|
|
}()
|
|
|
|
if err := writeProxyProtocolV2(client, local, remote); err != nil {
|
|
t.Fatalf("writeProxyProtocolV2: %v", err)
|
|
}
|
|
hdr := <-got
|
|
sig := []byte{0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A}
|
|
if len(hdr) < 16 || string(hdr[:12]) != string(sig) {
|
|
t.Fatalf("v2 header missing the protocol signature: %v", hdr)
|
|
}
|
|
if hdr[12] != 0x21 {
|
|
t.Fatalf("v2 version/command byte = 0x%02x, want 0x21", hdr[12])
|
|
}
|
|
if hdr[13] != 0x11 {
|
|
t.Fatalf("v2 family/protocol byte = 0x%02x, want 0x11 (TCP over IPv4)", hdr[13])
|
|
}
|
|
}
|
|
|
|
func TestParseRealityScanCandidateCSV(t *testing.T) {
|
|
got := parseRealityScanCandidateCSV(" a.com:443 , ,b.com:8443 ")
|
|
want := []string{"a.com:443", "b.com:8443"}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("got %v, want %v", got, want)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("got %v, want %v", got, want)
|
|
}
|
|
}
|
|
if tokens := parseRealityScanCandidateCSV(" , "); len(tokens) != 0 {
|
|
t.Fatalf("empty CSV should yield no tokens, got %v", tokens)
|
|
}
|
|
}
|