clash - stash subscription #373

This commit is contained in:
Alireza Ahmadi
2025-06-01 23:50:45 +02:00
parent 3f7657c080
commit d186875ab7
7 changed files with 382 additions and 21 deletions
+5
View File
@@ -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
+332
View File
@@ -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)
}
+9 -5
View File
@@ -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) {
+15 -8
View File
@@ -11,6 +11,7 @@ type SubHandler struct {
service.SettingService
SubService
JsonService
ClashService
}
func NewSubHandler(g *gin.RouterGroup) {
@@ -23,23 +24,31 @@ 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 {
return
}
}
// Add headers
c.Writer.Header().Set("Subscription-Userinfo", headers[0])
c.Writer.Header().Set("Profile-Update-Interval", headers[1])
@@ -47,5 +56,3 @@ func (s *SubHandler) subs(c *gin.Context) {
c.String(200, *result)
}
}
}
+2 -4
View File
@@ -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 {
+1
View File
@@ -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
+14
View File
@@ -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
}