chore: sub-store Go 重写项目初始化

This commit is contained in:
2026-07-27 14:38:13 +08:00
commit 90e53aa754
85 changed files with 15271 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
package util
import (
"regexp"
"strings"
)
// Flag detection rules: [regex, emoji].
// Per review-resolution #16: Go RE2 supports \p{Regional_Indicator} and \uFE0F.
var flagRules []flagRule
type flagRule struct {
re *regexp.Regexp
emoji string
}
func init() {
rules := []struct {
pattern string
emoji string
}{
{`香港|港|hong\s*kong|\bhk\b`, "🇭🇰"},
{`台湾|台灣|taiwan|\btw\b`, "🇹🇼"},
{`新加坡|狮城|獅城|singapore|\bsg\b`, "🇸🇬"},
{`日本|东京|東京|大阪|japan|tokyo|osaka|\bjp\b`, "🇯🇵"},
{`美国|美國|洛杉矶|洛杉磯|纽约|紐約|united\s*states|los\s*angeles|new\s*york|\bus\b|\busa\b`, "🇺🇸"},
{`英国|英國|伦敦|倫敦|united\s*kingdom|london|\buk\b`, "🇬🇧"},
{`德国|德國|法兰克福|法蘭克福|germany|frankfurt|\bde\b`, "🇩🇪"},
{`韩国|韓國|首尔|首爾|korea|seoul|\bkr\b`, "🇰🇷"},
}
for _, r := range rules {
re, err := regexp.Compile("(?i)" + r.pattern)
if err != nil {
continue
}
flagRules = append(flagRules, flagRule{re: re, emoji: r.emoji})
}
}
// DetectFlag returns the flag emoji for a name, or 🏳️ if no match.
func DetectFlag(name string) string {
text := strings.ToLower(name)
for _, rule := range flagRules {
if rule.re.MatchString(text) {
return rule.emoji
}
}
return "🏳️"
}
// removeFlagRe matches leading flag emoji sequences and whitespace.
// Regional Indicator letters are U+1F1E6–U+1F1FF; ZWJ is U+200D; VS16 is U+FE0F.
var removeFlagRe = regexp.MustCompile(`^[\x{1F1E6}-\x{1F1FF}\x{FE0F}\x{200D}\s]+`)
// RemoveFlag strips leading flag emoji and whitespace from a name.
// Per review-resolution #16: handles ZWJ (\u200D) and variation selector (\uFE0F).
func RemoveFlag(name string) string {
cleaned := removeFlagRe.ReplaceAllString(name, "")
// Also strip leading 🏳️
cleaned = strings.TrimLeft(cleaned, "🏳️ ")
return strings.TrimSpace(cleaned)
}
// NormalizeTaiwanFlag maps the Taiwan flag to different flags based on `tw` mode.
// Per review-resolution #19: three modes — ws/tw/default.
func NormalizeTaiwanFlag(flag, mode string) string {
if flag != "🇹🇼" {
return flag
}
switch mode {
case "ws":
return "🇼🇸"
case "tw":
return "🇹🇼"
default:
return "🇨🇳"
}
}
// IsASCII checks if a string contains only ASCII characters.
func IsASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] > 127 {
return false
}
}
return true
}
+61
View File
@@ -0,0 +1,61 @@
package util
import "strings"
// IsIPv4 checks if a string is a valid IPv4 address.
func IsIPv4(value string) bool {
parts := strings.Split(value, ".")
if len(parts) != 4 {
return false
}
for _, part := range parts {
if len(part) == 0 || len(part) > 3 {
return false
}
for _, c := range part {
if c < '0' || c > '9' {
return false
}
}
var n int
for _, c := range part {
n = n*10 + int(c-'0')
}
if n > 255 {
return false
}
}
return true
}
// IsIPv6 checks if a string is a valid IPv6 address (simplified).
func IsIPv6(value string) bool {
if !strings.Contains(value, ":") {
return false
}
for _, c := range value {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') || c == ':') {
return false
}
}
return true
}
// IsIPAddress checks if a string is any IP address.
func IsIPAddress(value string) bool {
return IsIPv4(value) || IsIPv6(value)
}
// ShouldResolveServer returns true if the server value is a hostname that should be DNS-resolved.
func ShouldResolveServer(server string) bool {
s := strings.TrimSpace(server)
if s == "" || IsIPAddress(s) {
return false
}
for _, c := range s {
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '.' || c == '-') {
return false
}
}
return strings.Contains(s, ".")
}
+13
View File
@@ -0,0 +1,13 @@
package util
// Byte/quantity limit constants — mirror the original project's limits.ts.
const (
MaxAPIBodyBytes = 4 * 1024 * 1024
MaxRemoteSourceUrls = 8
MaxRemoteSourceRespBytes = 2 * 1024 * 1024
MaxRemoteSourceTotalBytes = 12 * 1024 * 1024
MaxFlowRespBytes = 64 * 1024
MaxDoHRespBytes = 64 * 1024
MaxRecycleEntries = 50
MaxCustomRules = 32 // review-resolution #37
)
+164
View File
@@ -0,0 +1,164 @@
package util
import (
"encoding/json"
"fmt"
"strings"
)
// GetByPath retrieves a value from a nested map using dot-notation path.
// e.g. GetByPath(node, "ws-opts.headers.Host")
func GetByPath(input map[string]any, path string) any {
keys := strings.Split(path, ".")
var current any = input
for _, key := range keys {
if current == nil {
return nil
}
m, ok := current.(map[string]any)
if !ok {
return nil
}
current = m[key]
}
return current
}
// SetByPath sets a value in a nested map using dot-notation path,
// creating intermediate maps as needed.
func SetByPath(input map[string]any, path string, value any) map[string]any {
keys := strings.Split(path, ".")
current := input
for i := 0; i < len(keys)-1; i++ {
key := keys[i]
next, ok := current[key].(map[string]any)
if !ok {
next = map[string]any{}
current[key] = next
}
current = next
}
current[keys[len(keys)-1]] = value
return input
}
func GetString(input map[string]any, path string) string {
v := GetByPath(input, path)
if s, ok := v.(string); ok {
return s
}
return ""
}
func GetInt(input map[string]any, path string) int {
v := GetByPath(input, path)
switch n := v.(type) {
case int:
return n
case int64:
return int(n)
case float64:
return int(n)
case string:
var i int
fmt.Sscanf(n, "%d", &i)
return i
}
return 0
}
func GetBool(input map[string]any, path string) bool {
v := GetByPath(input, path)
if b, ok := v.(bool); ok {
return b
}
return false
}
// DeepCopy produces a deep copy of a ProxyNode via JSON round-trip.
func DeepCopy(node map[string]any) map[string]any {
data, err := json.Marshal(node)
if err != nil {
return map[string]any{}
}
var copy map[string]any
if err := json.Unmarshal(data, &copy); err != nil {
return map[string]any{}
}
return copy
}
// StripUndefined removes keys with nil or empty-string values from a map.
func StripUndefined(input map[string]any) map[string]any {
result := make(map[string]any, len(input))
for k, v := range input {
if v == nil {
continue
}
if s, ok := v.(string); ok && s == "" {
continue
}
result[k] = v
}
return result
}
// MergeDeep recursively merges `next` into `base`, returning a new map.
func MergeDeep(base, next map[string]any) map[string]any {
result := make(map[string]any, len(base))
for k, v := range base {
result[k] = v
}
for k, v := range next {
if v == nil {
continue
}
if isPlainObject(v) && isPlainObject(result[k]) {
result[k] = MergeDeep(result[k].(map[string]any), v.(map[string]any))
} else {
result[k] = v
}
}
return result
}
func isPlainObject(v any) bool {
_, ok := v.(map[string]any)
return ok
}
// ToId normalizes a string into a valid record ID: lowercase, non-alphanum → dash, max 64.
func ToId(input string) string {
s := strings.TrimSpace(input)
b := make([]byte, 0, len(s))
prevDash := false
for i := 0; i < len(s); i++ {
c := s[i]
if c >= 'A' && c <= 'Z' {
c += 32
}
if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' {
b = append(b, c)
prevDash = false
} else if c == '-' {
if !prevDash {
b = append(b, '-')
prevDash = true
}
} else {
if !prevDash && len(b) > 0 {
b = append(b, '-')
prevDash = true
}
}
}
// trim leading/trailing dashes
result := strings.Trim(string(b), "-")
if len(result) > 64 {
result = result[:64]
}
if result == "" {
return "item"
}
return result
}
+8
View File
@@ -0,0 +1,8 @@
package util
import "crypto/rand"
// readRandom fills b with cryptographically secure random bytes.
func readRandom(b []byte) (int, error) {
return rand.Read(b)
}
+112
View File
@@ -0,0 +1,112 @@
package util
import (
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"fmt"
"strings"
)
// SHA256Hex returns the hex-encoded SHA-256 hash of a string.
func SHA256Hex(input string) string {
h := sha256.Sum256([]byte(input))
return hex.EncodeToString(h[:])
}
// IsTokenValid checks a plaintext input against a plaintext secret.
// Per review-resolution #24: SHA-256 both sides, then constant-time compare.
func IsTokenValid(input, secret string) bool {
if secret == "" || input == "" {
return false
}
inputHash := SHA256Hex(input)
secretHash := SHA256Hex(secret)
return subtle.ConstantTimeCompare([]byte(inputHash), []byte(secretHash)) == 1
}
// IsGrantTokenValid checks a plaintext input against a stored SHA-256 hash.
// Per review-resolution #24: hash the input, then constant-time compare with stored hash.
func IsGrantTokenValid(input, storedHash string) bool {
if input == "" || storedHash == "" {
return false
}
inputHash := SHA256Hex(input)
return subtle.ConstantTimeCompare([]byte(inputHash), []byte(storedHash)) == 1
}
// RandomToken generates a 24-byte random base64url token (no padding).
func RandomToken() (string, error) {
var buf [24]byte
if _, err := readRandom(buf[:]); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(buf[:]), nil
}
// EncodeBase64URL encodes to standard base64 URL encoding with padding.
func EncodeBase64URL(input string) string {
return base64.URLEncoding.EncodeToString([]byte(input))
}
// EncodeBase64RawURL encodes to base64 URL encoding without padding.
func EncodeBase64RawURL(input string) string {
return base64.RawURLEncoding.EncodeToString([]byte(input))
}
// EncodeBase64Std encodes to standard base64 encoding.
func EncodeBase64Std(input string) string {
return base64.StdEncoding.EncodeToString([]byte(input))
}
// DecodeBase64Std decodes standard base64.
func DecodeBase64Std(s string) (string, error) {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return "", err
}
return string(b), nil
}
// DecodeBase64URL decodes URL-safe base64 with padding.
func DecodeBase64URL(s string) (string, error) {
b, err := base64.URLEncoding.DecodeString(s)
if err != nil {
return "", err
}
return string(b), nil
}
// DecodeBase64RawURL decodes URL-safe base64 without padding.
// Per review-resolution #15: used for SSR.
func DecodeBase64RawURL(s string) (string, error) {
b, err := base64.RawURLEncoding.DecodeString(s)
if err != nil {
return "", err
}
return string(b), nil
}
// DecodeBase64Auto tries Std first, then RawURL — per review-resolution #15.
func DecodeBase64Auto(s string) (string, error) {
s = strings.TrimSpace(s)
// Try standard base64 first
if b, err := base64.StdEncoding.DecodeString(s); err == nil {
return string(b), nil
}
// Try raw URL encoding (no padding)
if b, err := base64.RawURLEncoding.DecodeString(s); err == nil {
return string(b), nil
}
// Try URL encoding with padding
if b, err := base64.URLEncoding.DecodeString(s); err == nil {
return string(b), nil
}
return "", fmt.Errorf("invalid base64")
}
// Base64Utf8 encodes a UTF-8 string to standard base64.
func Base64Utf8(input string) string {
return base64.StdEncoding.EncodeToString([]byte(input))
}