Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
package subscription
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/prism/proxy/internal/store"
|
||||
)
|
||||
|
||||
// ParseLinkLines parses share links (ss/socks5/http/trojan) into manual proxy nodes.
|
||||
func ParseLinkLines(text string) ([]store.ProxyNode, []string) {
|
||||
lines := strings.Split(text, "\n")
|
||||
var out []store.ProxyNode
|
||||
var errs []string
|
||||
idx := 0
|
||||
for i, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
n, err := parseLink(line, 0, idx)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("第 %d 行: %v", i+1, err))
|
||||
continue
|
||||
}
|
||||
n.Source = "manual"
|
||||
n.RuntimeName = store.RuntimeName("manual", 0, n.Name)
|
||||
out = append(out, n)
|
||||
idx++
|
||||
}
|
||||
return out, errs
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package subscription
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseLinkLines(t *testing.T) {
|
||||
nodes, errs := ParseLinkLines("socks5://1.2.3.4:1080#test-node\n")
|
||||
if len(errs) != 0 {
|
||||
t.Fatal(errs)
|
||||
}
|
||||
if len(nodes) != 1 || nodes[0].Name != "test-node" || nodes[0].Source != "manual" {
|
||||
t.Fatalf("unexpected %+v", nodes[0])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
package subscription
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/prism/proxy/internal/country"
|
||||
"github.com/prism/proxy/internal/store"
|
||||
)
|
||||
|
||||
const subscriptionUserAgent = "clash.meta/1.19.0"
|
||||
|
||||
type Service struct {
|
||||
store *store.Store
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewService(s *store.Store) *Service {
|
||||
return &Service{
|
||||
store: s,
|
||||
client: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 10 {
|
||||
return fmt.Errorf("too many redirects")
|
||||
}
|
||||
req.Header.Set("User-Agent", subscriptionUserAgent)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type ParsedData struct {
|
||||
Nodes []store.ProxyNode
|
||||
Groups []store.ProxyGroup
|
||||
Rules []store.Rule
|
||||
}
|
||||
|
||||
func (s *Service) FetchAndStore(ctx context.Context, subID int64) error {
|
||||
sub, err := s.store.GetSubscription(ctx, subID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := s.fetchURL(sub.URL)
|
||||
if err != nil {
|
||||
_ = s.store.UpdateSubscriptionFetch(ctx, subID, 0, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
parsed, err := s.parseSubscription(body, subID, sub.Type)
|
||||
if err != nil {
|
||||
_ = s.store.UpdateSubscriptionFetch(ctx, subID, 0, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.store.ReplaceSubscriptionData(ctx, subID, parsed.Nodes, parsed.Groups, parsed.Rules); err != nil {
|
||||
_ = s.store.UpdateSubscriptionFetch(ctx, subID, 0, err.Error())
|
||||
return err
|
||||
}
|
||||
return s.store.UpdateSubscriptionFetch(ctx, subID, len(parsed.Nodes), "")
|
||||
}
|
||||
|
||||
func (s *Service) ParseSubscription(body []byte, subID int64, subType string) (ParsedData, error) {
|
||||
return s.parseSubscription(body, subID, subType)
|
||||
}
|
||||
|
||||
func (s *Service) parseSubscription(body []byte, subID int64, subType string) (ParsedData, error) {
|
||||
content := decodeMaybeBase64(body)
|
||||
text := strings.TrimSpace(string(content))
|
||||
|
||||
format := subType
|
||||
if format == "" || format == "auto" {
|
||||
format = detectFormat(text)
|
||||
}
|
||||
|
||||
switch format {
|
||||
case "links":
|
||||
return s.parseLinks(body, subID)
|
||||
default:
|
||||
parsed, err := s.parseClash(body, subID)
|
||||
if err != nil && looksLikeLinks(text) {
|
||||
return s.parseLinks(body, subID)
|
||||
}
|
||||
return parsed, err
|
||||
}
|
||||
}
|
||||
|
||||
func detectFormat(text string) string {
|
||||
if strings.HasPrefix(text, "port:") || strings.HasPrefix(text, "mixed-port:") ||
|
||||
strings.HasPrefix(text, "proxies:") || strings.Contains(text, "\nproxies:") {
|
||||
return "clash"
|
||||
}
|
||||
if looksLikeLinks(text) {
|
||||
return "links"
|
||||
}
|
||||
return "clash"
|
||||
}
|
||||
|
||||
func looksLikeLinks(text string) bool {
|
||||
lower := strings.ToLower(text)
|
||||
for _, prefix := range []string{"ss://", "vmess://", "trojan://", "socks5://", "http://", "https://"} {
|
||||
if strings.Contains(lower, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Service) fetchURL(rawURL string) ([]byte, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, rawURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", subscriptionUserAgent)
|
||||
req.Header.Set("Accept", "*/*")
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf("fetch failed: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func (s *Service) parseClash(body []byte, subID int64) (ParsedData, error) {
|
||||
content := decodeMaybeBase64(body)
|
||||
|
||||
var raw map[string]any
|
||||
if err := yaml.Unmarshal(content, &raw); err != nil {
|
||||
return ParsedData{}, fmt.Errorf("invalid clash yaml: %w", err)
|
||||
}
|
||||
if _, ok := raw["proxies"]; !ok {
|
||||
return ParsedData{}, fmt.Errorf("clash config missing proxies")
|
||||
}
|
||||
|
||||
var out ParsedData
|
||||
|
||||
if proxies, ok := raw["proxies"].([]any); ok {
|
||||
for i, p := range proxies {
|
||||
pm, ok := p.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name, _ := pm["name"].(string)
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("node%d", i+1)
|
||||
}
|
||||
rawJSON, _ := json.Marshal(pm)
|
||||
out.Nodes = append(out.Nodes, store.ProxyNode{
|
||||
Name: name,
|
||||
RuntimeName: store.RuntimeName("sub", subID, fmt.Sprintf("%d_%s", i, name)),
|
||||
Type: fmt.Sprint(pm["type"]),
|
||||
RawConfig: string(rawJSON),
|
||||
Country: country.Detect(name),
|
||||
Enabled: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if groups, ok := raw["proxy-groups"].([]any); ok {
|
||||
for i, g := range groups {
|
||||
gm, ok := g.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name, _ := gm["name"].(string)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
gtype, _ := gm["type"].(string)
|
||||
var members []string
|
||||
if arr, ok := gm["proxies"].([]any); ok {
|
||||
for _, m := range arr {
|
||||
members = append(members, fmt.Sprint(m))
|
||||
}
|
||||
}
|
||||
membersJSON, _ := json.Marshal(members)
|
||||
out.Groups = append(out.Groups, store.ProxyGroup{
|
||||
Name: name,
|
||||
RuntimeName: store.RuntimeName("grp", subID, fmt.Sprintf("%d_%s", i, name)),
|
||||
Type: gtype,
|
||||
Proxies: string(membersJSON),
|
||||
Enabled: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if rules, ok := raw["rules"].([]any); ok {
|
||||
for _, r := range rules {
|
||||
line := fmt.Sprint(r)
|
||||
parts := strings.Split(line, ",")
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
rule := store.Rule{
|
||||
RuleType: parts[0],
|
||||
Target: parts[len(parts)-1],
|
||||
Enabled: true,
|
||||
}
|
||||
if len(parts) > 2 {
|
||||
rule.Payload = strings.Join(parts[1:len(parts)-1], ",")
|
||||
}
|
||||
out.Rules = append(out.Rules, rule)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Service) parseLinks(body []byte, subID int64) (ParsedData, error) {
|
||||
text := string(decodeMaybeBase64(body))
|
||||
lines := strings.Split(text, "\n")
|
||||
var out ParsedData
|
||||
idx := 0
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
node, err := parseLink(line, subID, idx)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out.Nodes = append(out.Nodes, node)
|
||||
idx++
|
||||
}
|
||||
if len(out.Nodes) == 0 {
|
||||
return ParsedData{}, fmt.Errorf("no valid proxy links found")
|
||||
}
|
||||
names := make([]string, 0, len(out.Nodes))
|
||||
for _, n := range out.Nodes {
|
||||
names = append(names, n.RuntimeName)
|
||||
}
|
||||
namesJSON, _ := json.Marshal(names)
|
||||
out.Groups = append(out.Groups, store.ProxyGroup{
|
||||
Name: fmt.Sprintf("sub%d", subID),
|
||||
RuntimeName: store.RuntimeName("grp", subID, "auto"),
|
||||
Type: "url-test",
|
||||
Proxies: string(namesJSON),
|
||||
Enabled: true,
|
||||
})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func parseLink(link string, subID int64, idx int) (store.ProxyNode, error) {
|
||||
lower := strings.ToLower(link)
|
||||
var proxy map[string]any
|
||||
var name string
|
||||
var ptype string
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(lower, "ss://"):
|
||||
proxy, name, ptype = parseSS(link)
|
||||
case strings.HasPrefix(lower, "socks5://"):
|
||||
proxy, name, ptype = parseSocks5(link)
|
||||
case strings.HasPrefix(lower, "http://"), strings.HasPrefix(lower, "https://"):
|
||||
proxy, name, ptype = parseHTTP(link)
|
||||
case strings.HasPrefix(lower, "trojan://"):
|
||||
proxy, name, ptype = parseTrojan(link)
|
||||
default:
|
||||
return store.ProxyNode{}, fmt.Errorf("unsupported link")
|
||||
}
|
||||
if proxy == nil {
|
||||
return store.ProxyNode{}, fmt.Errorf("failed to parse link")
|
||||
}
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("node%d", idx+1)
|
||||
}
|
||||
raw, _ := json.Marshal(proxy)
|
||||
return store.ProxyNode{
|
||||
Name: name,
|
||||
RuntimeName: store.RuntimeName("sub", subID, name),
|
||||
Type: ptype,
|
||||
RawConfig: string(raw),
|
||||
Country: country.Detect(name),
|
||||
Enabled: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseSS(link string) (map[string]any, string, string) {
|
||||
name, payload := splitLinkFragment(link, "ss://")
|
||||
if payload == "" {
|
||||
return nil, name, "ss"
|
||||
}
|
||||
|
||||
// SIP002: ss://BASE64(method:password@host:port)#name
|
||||
if proxy, ok := parseSIP002(payload); ok {
|
||||
return proxy, name, "ss"
|
||||
}
|
||||
|
||||
// 传统格式: ss://BASE64(method:password)@host:port#name
|
||||
// 或 ss://BASE64(method:password)@BASE64(host:port)#name(部分机场订阅)
|
||||
parts := strings.SplitN(payload, "@", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, name, "ss"
|
||||
}
|
||||
userinfo := string(decodeBase64Flexible(parts[0]))
|
||||
if userinfo == "" {
|
||||
userinfo = parts[0]
|
||||
}
|
||||
hostport := string(decodeBase64Flexible(parts[1]))
|
||||
if hostport == "" {
|
||||
hostport = parts[1]
|
||||
}
|
||||
server, portStr, ok := strings.Cut(hostport, ":")
|
||||
if !ok {
|
||||
return nil, name, "ss"
|
||||
}
|
||||
var port int
|
||||
fmt.Sscanf(portStr, "%d", &port)
|
||||
cipher, password, _ := strings.Cut(userinfo, ":")
|
||||
if cipher == "" {
|
||||
cipher = "aes-256-gcm"
|
||||
}
|
||||
return map[string]any{
|
||||
"type": "ss",
|
||||
"server": server,
|
||||
"port": port,
|
||||
"cipher": cipher,
|
||||
"password": password,
|
||||
}, name, "ss"
|
||||
}
|
||||
|
||||
func parseSIP002(payload string) (map[string]any, bool) {
|
||||
decoded := string(decodeBase64Flexible(payload))
|
||||
at := strings.LastIndex(decoded, "@")
|
||||
if at <= 0 || !strings.Contains(decoded[:at], ":") {
|
||||
return nil, false
|
||||
}
|
||||
userinfo := decoded[:at]
|
||||
hostport := decoded[at+1:]
|
||||
server, portStr, ok := strings.Cut(hostport, ":")
|
||||
if !ok || server == "" {
|
||||
return nil, false
|
||||
}
|
||||
var port int
|
||||
if _, err := fmt.Sscanf(portStr, "%d", &port); err != nil || port <= 0 {
|
||||
return nil, false
|
||||
}
|
||||
cipher, password, ok := strings.Cut(userinfo, ":")
|
||||
if !ok || cipher == "" {
|
||||
return nil, false
|
||||
}
|
||||
return map[string]any{
|
||||
"type": "ss",
|
||||
"server": server,
|
||||
"port": port,
|
||||
"cipher": cipher,
|
||||
"password": password,
|
||||
}, true
|
||||
}
|
||||
|
||||
func splitLinkFragment(link, scheme string) (name, payload string) {
|
||||
rest := strings.TrimPrefix(link, scheme)
|
||||
if i := strings.Index(rest, "#"); i >= 0 {
|
||||
name, _ = url.QueryUnescape(rest[i+1:])
|
||||
rest = rest[:i]
|
||||
}
|
||||
return name, rest
|
||||
}
|
||||
|
||||
func decodeBase64Flexible(s string) []byte {
|
||||
decoders := []func(string) ([]byte, error){
|
||||
base64.StdEncoding.DecodeString,
|
||||
base64.RawStdEncoding.DecodeString,
|
||||
base64.URLEncoding.DecodeString,
|
||||
base64.RawURLEncoding.DecodeString,
|
||||
}
|
||||
for _, dec := range decoders {
|
||||
if b, err := dec(s); err == nil && len(b) > 0 {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseSocks5(link string) (map[string]any, string, string) {
|
||||
name, payload := splitLinkFragment(link, "socks5://")
|
||||
user, pass := "", ""
|
||||
if at := strings.LastIndex(payload, "@"); at >= 0 {
|
||||
auth := payload[:at]
|
||||
payload = payload[at+1:]
|
||||
if c := strings.Index(auth, ":"); c >= 0 {
|
||||
user, pass = auth[:c], auth[c+1:]
|
||||
}
|
||||
}
|
||||
server, portStr, _ := strings.Cut(payload, ":")
|
||||
var port int
|
||||
fmt.Sscanf(portStr, "%d", &port)
|
||||
m := map[string]any{"type": "socks5", "server": server, "port": port}
|
||||
if user != "" {
|
||||
m["username"] = user
|
||||
m["password"] = pass
|
||||
}
|
||||
return m, name, "socks5"
|
||||
}
|
||||
|
||||
func parseHTTP(link string) (map[string]any, string, string) {
|
||||
isTLS := strings.HasPrefix(strings.ToLower(link), "https://")
|
||||
scheme := "http://"
|
||||
if isTLS {
|
||||
scheme = "https://"
|
||||
}
|
||||
name, payload := splitLinkFragment(link, scheme)
|
||||
user, pass := "", ""
|
||||
if at := strings.LastIndex(payload, "@"); at >= 0 {
|
||||
auth := payload[:at]
|
||||
payload = payload[at+1:]
|
||||
if c := strings.Index(auth, ":"); c >= 0 {
|
||||
user, pass = auth[:c], auth[c+1:]
|
||||
}
|
||||
}
|
||||
server, portStr, _ := strings.Cut(payload, ":")
|
||||
var port int
|
||||
fmt.Sscanf(portStr, "%d", &port)
|
||||
m := map[string]any{"type": "http", "server": server, "port": port, "tls": isTLS}
|
||||
if user != "" {
|
||||
m["username"] = user
|
||||
m["password"] = pass
|
||||
}
|
||||
return m, name, "http"
|
||||
}
|
||||
|
||||
func parseTrojan(link string) (map[string]any, string, string) {
|
||||
name, payload := splitLinkFragment(link, "trojan://")
|
||||
pass, rest, _ := strings.Cut(payload, "@")
|
||||
server, portStr, _ := strings.Cut(rest, ":")
|
||||
var port int
|
||||
fmt.Sscanf(portStr, "%d", &port)
|
||||
if q := strings.Index(server, "?"); q >= 0 {
|
||||
server = server[:q]
|
||||
}
|
||||
return map[string]any{
|
||||
"type": "trojan",
|
||||
"server": server,
|
||||
"port": port,
|
||||
"password": pass,
|
||||
"sni": server,
|
||||
}, name, "trojan"
|
||||
}
|
||||
|
||||
func decodeMaybeBase64(body []byte) []byte {
|
||||
trim := strings.TrimSpace(string(body))
|
||||
if strings.HasPrefix(trim, "proxies:") || strings.HasPrefix(trim, "{") || strings.HasPrefix(trim, "ss://") {
|
||||
return body
|
||||
}
|
||||
if decoded := decodeBase64Flexible(trim); len(decoded) > 0 {
|
||||
return decoded
|
||||
}
|
||||
return body
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package subscription
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const sampleBase64Sub = `c3M6Ly9ZV1Z6TFRFeU9DMW5ZMjA2Wm1Fd1lqZ3laamN0Tm1VelppMWhZakZoTFdJMk5qZ3RZamhqTlRjeU5ERTJNakpqUUdWNGNHbHlaUzEwYVcxbExtTjVZbVZ5Ym05a1pTNTBiM0E2TVRrNE5ERT0jJUU1JThCJUJGJUU5JTgwJTg5LSVFNSU4OCVCMCVFNiU5QyU5RiVFNiU5NyVCNiVFOSU5NyVCNCVFRiVCQyU5QTIwMjYtMDktMDMKc3M6Ly9ZV1Z6TFRFeU9DMW5ZMjA2Wm1Fd1lqZ3laamN0Tm1VelppMWhZakZoTFdJMk5qZ3RZamhqTlRjeU5ERTJNakpqUUhKbGJXRnBiaTEwY21GbVptbGpMbU41WW1WeWJtOWtaUzUwYjNBNk1UazROREk9IyVFNSU4QiVCRiVFOSU4MCU4OS0lRTUlQkQlOTMlRTYlOUMlODglRTUlODklQTklRTQlQkQlOTklRTYlQjUlODElRTklODclOEYlRUYlQkMlOUEyNi4wOUdCCnNzOi8vWVdWekxURXlPQzFuWTIwNlptRXdZamd5WmpjdE5tVXpaaTFoWWpGaExXSTJOamd0WWpoak5UY3lOREUyTWpKalFERXVNUzR4TGpFNk5UazFNREk9IyVFMyU4MCU5MCVFOSU4MCU5QSVFNyU5RiVBNSVFMyU4MCU5MSVFNSVBNiU4MiVFOSU5QyU4MCVFNiU5QiVCNCVFNSVBNCU5QSVFOCU4QSU4MiVFNyU4MiVCOSVFOCVBRiVCNyVFOSU4NyU4RCVFOCVBMyU4NUFwcApzczovL1lXVnpMVEV5T0MxblkyMDZabUV3WWpneVpqY3RObVV6WmkxaFlqRmhMV0kyTmpndFlqaGpOVGN5TkRFMk1qSmpRREV1TVM0eExqRTZOVGsxTURNPSMlRTglODElOTQlRTclQjMlQkIlRTklODIlQUUlRTclQUUlQjElM0FsaWJjeWJlcjd4MjQlNDBnbWFpbC5jb20Kc3M6Ly9ZV1Z6TFRFeU9DMW5ZMjA2Wm1Fd1lqZ3laamN0Tm1VelppMWhZakZoTFdJMk5qZ3RZamhqTlRjeU5ERTJNakpqUURFd015NHhPREV1TVRZMExqRXdORG8yTVRFd01RPT0jJUU5JUFCJTk4JUU5JTgwJTlGJUU4JUFGJTk1JUU3JTk0JUE4LSVFNiVCMyU5NSVFNSU5QiVCRDEKc3M6Ly9ZV1Z6TFRFeU9DMW5ZMjA2Wm1Fd1lqZ3laamN0Tm1VelppMWhZakZoTFdJMk5qZ3RZamhqTlRjeU5ERTJNakpqUURFd015NHhPREV1TVRZMExqRXdORG8yTURBNE9BPT0jJUU5JUFCJTk4JUU5JTgwJTlGJUU1JUE0JTg3JUU3JTk0JUE4LSVFNCVCQiU4NSVFOSU5OSU5MCVFNCVCOCVCNCVFNiU5NyVCNiVFNCVCRCVCRiVFNyU5NCVBOAo=`
|
||||
|
||||
func TestParseXHSCacheBase64Subscription(t *testing.T) {
|
||||
svc := NewService(nil)
|
||||
parsed, err := svc.parseSubscription([]byte(sampleBase64Sub), 1, "auto")
|
||||
if err != nil {
|
||||
t.Fatalf("parse failed: %v", err)
|
||||
}
|
||||
if len(parsed.Nodes) != 6 {
|
||||
t.Fatalf("expected 6 nodes, got %d", len(parsed.Nodes))
|
||||
}
|
||||
if parsed.Nodes[0].Type != "ss" {
|
||||
t.Fatalf("expected ss type, got %s", parsed.Nodes[0].Type)
|
||||
}
|
||||
var cfg map[string]any
|
||||
if err := json.Unmarshal([]byte(parsed.Nodes[0].RawConfig), &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg["server"] != "expire-time.cybernode.top" {
|
||||
t.Fatalf("unexpected server: %v", cfg["server"])
|
||||
}
|
||||
if cfg["cipher"] != "aes-128-gcm" {
|
||||
t.Fatalf("unexpected cipher: %v", cfg["cipher"])
|
||||
}
|
||||
if parsed.Nodes[0].Name == "" {
|
||||
t.Fatal("expected decoded node name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFormatBase64Links(t *testing.T) {
|
||||
if got := detectFormat(string(decodeMaybeBase64([]byte(sampleBase64Sub)))); got != "links" {
|
||||
t.Fatalf("expected links, got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchURLFollowsRequest(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("User-Agent") == "" {
|
||||
t.Fatal("missing user agent")
|
||||
}
|
||||
_, _ = io.WriteString(w, sampleBase64Sub)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
svc := NewService(nil)
|
||||
body, err := svc.fetchURL(srv.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(body) == 0 {
|
||||
t.Fatal("empty body")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user