157 lines
4.4 KiB
Go
157 lines
4.4 KiB
Go
package proxy
|
|
|
|
import (
|
|
"encoding/json"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/peterqiu0516/sub-store/internal/model"
|
|
"github.com/peterqiu0516/sub-store/internal/util"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
var (
|
|
// reURIScheme matches a URI scheme like "vless://", "ss://", etc.
|
|
reURIScheme = regexp.MustCompile(`(?im)^[a-z][a-z0-9+.-]*://`)
|
|
// reYamlKeys matches YAML top-level keys: proxies, proxy-groups, rules
|
|
reYamlKeys = regexp.MustCompile(`(?m)^\s*(proxies|proxy-groups|rules)\s*:`)
|
|
// reJSONStart matches the start of a JSON array or object
|
|
reJSONStart = regexp.MustCompile(`^\s*[\[{]`)
|
|
// reQxConfig matches QX-style config lines: "shadowsocks = ...", "vmess = ...", etc.
|
|
reQxConfig = regexp.MustCompile(`(?im)^\s*(shadowsocks|vmess|vless|trojan|http|socks5|anytls)\s*=`)
|
|
// reNamedConfig matches Surge/Loon named config lines
|
|
reNamedConfig = regexp.MustCompile(`(?im)^\s*[^=\n]{1,80}\s*=\s*(ss|shadowsocks|ssr|vmess|vless|trojan|http|https|socks5|socks5-tls|hysteria2|hysteria|anytls|tuic|tuic-v5)\s*,`)
|
|
// reCommentOrSection matches lines to skip: comments (#, ;) and section headers ([...])
|
|
reCommentOrSection = regexp.MustCompile(`^\s*(#|;|\[[^\]]+\])`)
|
|
)
|
|
|
|
// ParseProxies is the main entry point for parsing subscription content.
|
|
// It trims the input, detects JSON/YAML/URI-line format, and dispatches accordingly.
|
|
func ParseProxies(raw string) []model.ProxyNode {
|
|
text := strings.TrimSpace(raw)
|
|
if text == "" {
|
|
return nil
|
|
}
|
|
if reJSONStart.MatchString(text) {
|
|
return ParseJsonProxies(text)
|
|
}
|
|
if reYamlKeys.MatchString(text) {
|
|
return ParseYamlProxies(text)
|
|
}
|
|
return ParseProxyLines(text)
|
|
}
|
|
|
|
// DecodeMaybeBase64 tries structured detection first; if the input doesn't
|
|
// look like a structured subscription, attempts base64 decode.
|
|
func DecodeMaybeBase64(raw string) string {
|
|
text := strings.TrimSpace(raw)
|
|
if LooksLikeStructuredSubscription(text) {
|
|
return raw
|
|
}
|
|
// Try base64 decode (strip all whitespace first)
|
|
cleaned := regexp.MustCompile(`\s+`).ReplaceAllString(text, "")
|
|
decoded, err := util.DecodeBase64Auto(cleaned)
|
|
if err != nil {
|
|
return raw
|
|
}
|
|
if LooksLikeStructuredSubscription(strings.TrimSpace(decoded)) {
|
|
return decoded
|
|
}
|
|
return raw
|
|
}
|
|
|
|
// LooksLikeStructuredSubscription checks if the text looks like structured
|
|
// subscription content (URI scheme, YAML keys, JSON, or client config lines).
|
|
func LooksLikeStructuredSubscription(text string) bool {
|
|
return reURIScheme.MatchString(text) ||
|
|
reYamlKeys.MatchString(text) ||
|
|
reJSONStart.MatchString(text) ||
|
|
reQxConfig.MatchString(text) ||
|
|
reNamedConfig.MatchString(text)
|
|
}
|
|
|
|
// ParseJsonProxies parses a JSON array of proxies or {"proxies": [...]}.
|
|
func ParseJsonProxies(raw string) []model.ProxyNode {
|
|
var payload any
|
|
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
|
return nil
|
|
}
|
|
|
|
var rawList []any
|
|
switch v := payload.(type) {
|
|
case []any:
|
|
rawList = v
|
|
case map[string]any:
|
|
if arr, ok := v["proxies"].([]any); ok {
|
|
rawList = arr
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
var result []model.ProxyNode
|
|
for _, item := range rawList {
|
|
if m, ok := item.(map[string]any); ok {
|
|
normalized := NormalizeProxy(m)
|
|
if IsProxyNode(normalized) {
|
|
result = append(result, normalized)
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ParseYamlProxies parses a YAML document with a "proxies:" key.
|
|
func ParseYamlProxies(raw string) []model.ProxyNode {
|
|
var payload map[string]any
|
|
if err := yaml.Unmarshal([]byte(raw), &payload); err != nil {
|
|
return nil
|
|
}
|
|
|
|
rawList, ok := payload["proxies"].([]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
var result []model.ProxyNode
|
|
for _, item := range rawList {
|
|
if m, ok := item.(map[string]any); ok {
|
|
normalized := NormalizeProxy(m)
|
|
if IsProxyNode(normalized) {
|
|
result = append(result, normalized)
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ParseProxyLines splits raw text into lines, skips comments/blanks/section headers,
|
|
// and parses each line as a URI or client config line.
|
|
func ParseProxyLines(raw string) []model.ProxyNode {
|
|
lines := strings.Split(raw, "\n")
|
|
var result []model.ProxyNode
|
|
index := 0
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
|
|
continue
|
|
}
|
|
if reCommentOrSection.MatchString(line) {
|
|
continue
|
|
}
|
|
|
|
var node model.ProxyNode
|
|
if node = ParseProxyUri(line, index); node == nil {
|
|
node = ParseClientProxyLine(line, index)
|
|
}
|
|
if node != nil {
|
|
result = append(result, node)
|
|
index++
|
|
}
|
|
}
|
|
return result
|
|
}
|