From d186875ab7e3f0111df372bb99021770540f995e Mon Sep 17 00:00:00 2001 From: Alireza Ahmadi Date: Sun, 1 Jun 2025 23:50:45 +0200 Subject: [PATCH] clash - stash subscription #373 --- service/setting.go | 5 + sub/clashService.go | 332 ++++++++++++++++++++++++++++++++++++++++++++ sub/jsonService.go | 14 +- sub/subHandler.go | 31 +++-- sub/subService.go | 6 +- util/outJson.go | 1 + util/subInfo.go | 14 ++ 7 files changed, 382 insertions(+), 21 deletions(-) create mode 100644 sub/clashService.go create mode 100644 util/subInfo.go diff --git a/service/setting.go b/service/setting.go index e31e5c4..3674237 100644 --- a/service/setting.go +++ b/service/setting.go @@ -56,6 +56,7 @@ var defaultValueMap = map[string]string{ "subShowInfo": "false", "subURI": "", "subJsonExt": "", + "subClashExt": "", "config": defaultConfig, "version": config.GetVersion(), } @@ -392,6 +393,10 @@ func (s *SettingService) GetSubJsonExt() (string, error) { return s.getString("subJsonExt") } +func (s *SettingService) GetSubClashExt() (string, error) { + return s.getString("subClashExt") +} + func (s *SettingService) fileExists(path string) error { _, err := os.Stat(path) return err diff --git a/sub/clashService.go b/sub/clashService.go new file mode 100644 index 0000000..40773db --- /dev/null +++ b/sub/clashService.go @@ -0,0 +1,332 @@ +package sub + +import ( + "s-ui/logger" + "s-ui/service" + "s-ui/util" + "strings" + + "gopkg.in/yaml.v3" +) + +type ClashService struct { + service.SettingService + JsonService + LinkService +} + +const basicClashConfig = `mixed-port: 7890 +allow-lan: false +mode: rule +log-level: info +external-controller: 127.0.0.1:9090 +tun: + enable: true + stack: system + auto-route: true + auto-detect-interface: true + dns-hijack: + - any:53 +dns: + enable: true + ipv6: false + enhanced-mode: fake-ip + fake-ip-range: 198.18.0.1/16 + default-nameserver: + - 8.8.8.8 + - 1.1.1.1 + nameserver: + - https://doh.pub/dns-query + - https://1.0.0.1/dns-query + fallback: + - tcp://9.9.9.9:53 + fake-ip-filter: + - "*.lan" + - localhost + - "*.local" +rules: + - GEOIP,Private,DIRECT + - MATCH,Proxy +` + +const ProxyGroups = `- name: Proxy + type: select + proxies: [] +- name: Auto + type: url-test + proxies: [] + url: http://www.gstatic.com/generate_204 + interval: 300 + tolerance: 50 +` + +func (s *ClashService) GetClash(subId string) (*string, []string, error) { + + client, inDatas, err := s.getData(subId) + if err != nil { + return nil, nil, err + } + + outbounds, outTags, err := s.getOutbounds(client.Config, inDatas) + if err != nil { + return nil, nil, err + } + + links := s.LinkService.GetLinks(&client.Links, "external", "") + for index, link := range links { + json, tag, err := util.GetOutbound(link, index) + if err == nil && len(tag) > 0 { + *outbounds = append(*outbounds, *json) + *outTags = append(*outTags, tag) + } + } + + othersStr, err := s.getClashConfig() + if err != nil || len(othersStr) == 0 { + othersStr = basicClashConfig + } + + result, err := s.ConvertToClashMeta(outbounds) + resultStr := othersStr + "\n" + string(result) + + updateInterval, _ := s.SettingService.GetSubUpdates() + headers := util.GetHeaders(client, updateInterval) + + return &resultStr, headers, nil +} + +func (s *ClashService) getClashConfig() (string, error) { + subClashExt, err := s.SettingService.GetSubClashExt() + if err != nil { + return "", err + } + + return subClashExt, nil +} + +func (s *ClashService) ConvertToClashMeta(outbounds *[]map[string]interface{}) ([]byte, error) { + var proxies []interface{} + proxyTags := make([]string, 0) + for _, obMap := range *outbounds { + + t, _ := obMap["type"].(string) + if t == "selector" || t == "urltest" || t == "direct" { + continue + } + + proxy := make(map[string]interface{}) + proxy["name"] = obMap["tag"] + proxy["type"] = t + proxy["server"] = obMap["server"] + proxy["port"] = obMap["server_port"] + + switch t { + case "vmess", "vless", "tuic": + proxy["uuid"] = obMap["uuid"] + if t == "vmess" { + proxy["alterId"] = obMap["alter_id"] + proxy["cipher"] = "auto" + } + if t == "vless" { + if flow, ok := obMap["flow"].(string); ok { + proxy["flow"] = flow + } + } + case "trojan": + proxy["password"] = obMap["password"] + case "socks", "http": + if t == "socks" { + proxy["type"] = "socks5" + } + proxy["username"] = obMap["username"] + proxy["password"] = obMap["password"] + case "hysteria", "hysteria2": + if _, ok := obMap["up_mbps"].(float64); ok { + proxy["up"] = obMap["up_mbps"] + } else { + proxy["up"] = 1000 + } + if _, ok := obMap["down_mbps"].(float64); ok { + proxy["down"] = obMap["down_mbps"] + } else { + proxy["down"] = 1000 + } + if t == "hysteria" { + proxy["auth-str"] = obMap["auth_str"] + if obfs, ok := obMap["obfs"].(string); ok { + proxy["obfs"] = obfs + } + } else { + proxy["password"] = obMap["password"] + if obfs, ok := obMap["obfs"].(map[string]interface{}); ok { + proxy["obfs"] = obfs["type"] + proxy["obfs-password"] = obfs["password"] + } + if ports, ok := obMap["server_ports"].([]string); ok { + proxy["ports"] = strings.ReplaceAll(strings.Join(ports, ","), ":", "-") + } + } + case "anytls": + proxy["password"] = obMap["password"] + if tls, ok := obMap["tls"].(map[string]interface{}); ok { + proxy["sni"] = tls["server_name"] + proxy["skip-cert-verify"] = tls["insecure"] + } + default: + continue + } + + // TLS params + tls, isTls := obMap["tls"].(map[string]interface{}) + if isTls { + tlsEnabled, ok := tls["enabled"].(bool) + if ok && !tlsEnabled { + isTls = false + } + } + if isTls { + // ignore ech outbounds + if _, ok := tls["ech"].(interface{}); ok { + continue + } + proxy["tls"] = tls["enabled"] + + // ALPN if exists + if alpn, ok := tls["alpn"].([]interface{}); ok { + proxy["alpn"] = alpn + } + + // Add reality if exists + if reality, ok := tls["reality"].(map[string]interface{}); ok && reality["enabled"].(bool) { + reality_opts := make(map[string]interface{}) + if pbk, ok := reality["public_key"].(string); ok { + reality_opts["public-key"] = pbk + } + if sid, ok := reality["short_id"].(string); ok { + reality_opts["short-id"] = sid + } + proxy["reality-opts"] = reality_opts + } + if utls, ok := tls["utls"].(map[string]interface{}); ok { + if enabled, ok := utls["enabled"].(bool); ok && enabled { + if fp, ok := utls["fingerprint"].(string); ok { + proxy["client-fingerprint"] = fp + } + } + } + if sni, ok := tls["server_name"].(string); ok { + if t == "http" { + proxy["sni"] = sni + } else { + proxy["servername"] = sni + } + } + if insecure, ok := tls["insecure"].(bool); ok && insecure { + proxy["skip-cert-verify"] = insecure + } + } + + // Transport if exist + if transport, ok := obMap["transport"].(map[string]interface{}); ok { + tt, _ := transport["type"].(string) + switch tt { + case "http": + httpOpts := make(map[string]interface{}) + if path, ok := transport["path"].([]interface{}); ok { + httpOpts["path"] = path[0] + } else if path, ok := transport["path"].(string); ok { + httpOpts["path"] = path + } + if host, ok := transport["host"].([]interface{}); ok { + httpOpts["host"] = host[0] + } + if isTls { + proxy["network"] = "h2" + proxy["h2-opts"] = httpOpts + } else { + proxy["network"] = "http" + proxy["http-opts"] = httpOpts + } + case "ws", "httpupgrade": + proxy["network"] = "ws" + wsOpts := make(map[string]interface{}) + if path, ok := transport["path"].(string); ok { + wsOpts["path"] = path + } + if headers, ok := transport["headers"].([]interface{}); ok { + wsOpts["headers"] = headers + } + if ed, ok := transport["early_data_header_name"].(string); ok { + wsOpts["early-data-header-name"] = ed + } + if tt == "httpupgrade" { + wsOpts["v2ray-http-upgrade"] = true + } + proxy["ws-opts"] = wsOpts + case "grpc": + proxy["network"] = "grpc" + grpcOpts := make(map[string]interface{}) + if service_name, ok := transport["service_name"].(string); ok { + grpcOpts["grpc-service-name"] = service_name + } + proxy["grpc-opts"] = grpcOpts + } + } + + // Multiplex + if mux, ok := obMap["multiplex"].(map[string]interface{}); ok { + if enabled, ok := mux["enabled"].(bool); ok && enabled { + smux := make(map[string]interface{}) + smux["enabled"] = true + if protocol, ok := mux["protocol"].(string); ok { + smux["protocol"] = protocol + } + if _, ok := mux["max_connections"].(float64); ok { + smux["max-connections"] = mux["max_connections"] + } + if _, ok := mux["min_streams"].(float64); ok { + smux["min-streams"] = mux["min_streams"] + } + if _, ok := mux["max_streams"].(float64); ok { + smux["max-streams"] = mux["max_streams"] + } + if _, ok := mux["padding"].(bool); ok { + smux["padding"] = mux["padding"] + } + if brutal, ok := mux["brutal"].(map[string]interface{}); ok { + if enabled, ok := brutal["enabled"].(bool); ok && enabled { + brutalOpts := make(map[string]interface{}) + brutalOpts["enabled"] = true + if _, ok := brutal["up_mbps"].(float64); ok { + brutalOpts["up"] = brutal["up_mbps"] + } + if _, ok := brutal["down_mbps"].(float64); ok { + brutalOpts["down"] = brutal["down_mbps"] + } + smux["brutal-opts"] = brutalOpts + } + } + proxy["smux"] = smux + } + } + + proxies = append(proxies, proxy) + proxyTags = append(proxyTags, obMap["tag"].(string)) + } + + var proxyGroups []map[string]interface{} + err := yaml.Unmarshal([]byte(ProxyGroups), &proxyGroups) + if err != nil { + logger.Error(err.Error()) + } + + proxyGroups[1]["proxies"] = proxyTags + proxyGroups[0]["proxies"] = append([]string{proxyGroups[1]["name"].(string)}, proxyTags...) + + output := map[string]interface{}{ + "proxies": proxies, + "proxy-groups": proxyGroups, + } + + return yaml.Marshal(output) +} diff --git a/sub/jsonService.go b/sub/jsonService.go index 9a7d245..b8fde0e 100644 --- a/sub/jsonService.go +++ b/sub/jsonService.go @@ -46,17 +46,17 @@ type JsonService struct { LinkService } -func (j *JsonService) GetJson(subId string, format string) (*string, error) { +func (j *JsonService) GetJson(subId string, format string) (*string, []string, error) { var jsonConfig map[string]interface{} client, inDatas, err := j.getData(subId) if err != nil { - return nil, err + return nil, nil, err } outbounds, outTags, err := j.getOutbounds(client.Config, inDatas) if err != nil { - return nil, err + return nil, nil, err } links := j.LinkService.GetLinks(&client.Links, "external", "") @@ -72,7 +72,7 @@ func (j *JsonService) GetJson(subId string, format string) (*string, error) { err = json.Unmarshal([]byte(defaultJson), &jsonConfig) if err != nil { - return nil, err + return nil, nil, err } jsonConfig["outbounds"] = outbounds @@ -82,7 +82,11 @@ func (j *JsonService) GetJson(subId string, format string) (*string, error) { result, _ := json.MarshalIndent(jsonConfig, "", " ") resultStr := string(result) - return &resultStr, nil + + updateInterval, _ := j.SettingService.GetSubUpdates() + headers := util.GetHeaders(client, updateInterval) + + return &resultStr, headers, nil } func (j *JsonService) getData(subId string) (*model.Client, []*model.Inbound, error) { diff --git a/sub/subHandler.go b/sub/subHandler.go index 0ff672f..112ac33 100644 --- a/sub/subHandler.go +++ b/sub/subHandler.go @@ -11,6 +11,7 @@ type SubHandler struct { service.SettingService SubService JsonService + ClashService } func NewSubHandler(g *gin.RouterGroup) { @@ -23,29 +24,35 @@ func (s *SubHandler) initRouter(g *gin.RouterGroup) { } func (s *SubHandler) subs(c *gin.Context) { + var headers []string + var result *string + var err error subId := c.Param("subid") format, isFormat := c.GetQuery("format") if isFormat { - result, err := s.JsonService.GetJson(subId, format) + switch format { + case "json": + result, headers, err = s.JsonService.GetJson(subId, format) + case "clash": + result, headers, err = s.ClashService.GetClash(subId) + } if err != nil || result == nil { logger.Error(err) c.String(400, "Error!") - } else { - c.String(200, *result) + return } } else { - result, headers, err := s.SubService.GetSubs(subId) + result, headers, err = s.SubService.GetSubs(subId) if err != nil || result == nil { logger.Error(err) c.String(400, "Error!") - } else { - - // Add headers - c.Writer.Header().Set("Subscription-Userinfo", headers[0]) - c.Writer.Header().Set("Profile-Update-Interval", headers[1]) - c.Writer.Header().Set("Profile-Title", headers[2]) - - c.String(200, *result) + return } } + // Add headers + c.Writer.Header().Set("Subscription-Userinfo", headers[0]) + c.Writer.Header().Set("Profile-Update-Interval", headers[1]) + c.Writer.Header().Set("Profile-Title", headers[2]) + + c.String(200, *result) } diff --git a/sub/subService.go b/sub/subService.go index 8909d85..716b13d 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -6,6 +6,7 @@ import ( "s-ui/database" "s-ui/database/model" "s-ui/service" + "s-ui/util" "strings" "time" ) @@ -34,11 +35,8 @@ func (s *SubService) GetSubs(subId string) (*string, []string, error) { linksArray := s.LinkService.GetLinks(&client.Links, "all", clientInfo) result := strings.Join(linksArray, "\n") - var headers []string updateInterval, _ := s.SettingService.GetSubUpdates() - headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", client.Up, client.Down, client.Volume, client.Expiry)) - headers = append(headers, fmt.Sprintf("%d", updateInterval)) - headers = append(headers, subId) + headers := util.GetHeaders(client, updateInterval) subEncode, _ := s.SettingService.GetSubEncode() if subEncode { diff --git a/util/outJson.go b/util/outJson.go index 508f250..43982b2 100644 --- a/util/outJson.go +++ b/util/outJson.go @@ -209,6 +209,7 @@ func trojanOut(out *map[string]interface{}, inbound map[string]interface{}) { } func vmessOut(out *map[string]interface{}, inbound map[string]interface{}) { + (*out)["alter_id"] = 0 delete(*out, "transport") if transport, ok := inbound["transport"]; ok { (*out)["transport"] = transport diff --git a/util/subInfo.go b/util/subInfo.go new file mode 100644 index 0000000..8f76cbe --- /dev/null +++ b/util/subInfo.go @@ -0,0 +1,14 @@ +package util + +import ( + "fmt" + "s-ui/database/model" +) + +func GetHeaders(client *model.Client, updateInterval int) []string { + var headers []string + headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", client.Up, client.Down, client.Volume, client.Expiry)) + headers = append(headers, fmt.Sprintf("%d", updateInterval)) + headers = append(headers, client.Name) + return headers +}