chore: sub-store Go 重写项目初始化
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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, ".")
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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, ©); 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user