Introduce internal/export as the single cert/SNI/skip-cert resolver. ClientURIs, DecodeNodeModel, resolveShareTLS and Clash proxy TLS helpers all consume BuildProfileFromConfig so formal IP certs always emit sni and subscriptions no longer diverge from share links on certificate identity.
135 lines
3.8 KiB
Go
135 lines
3.8 KiB
Go
package node
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/kazeyukiro/3m-ui/backend/internal/database/models"
|
|
"github.com/kazeyukiro/3m-ui/backend/internal/export"
|
|
"github.com/kazeyukiro/3m-ui/backend/internal/netutil"
|
|
)
|
|
|
|
func ClientURIs(listener models.Listener, host string) ([]string, error) {
|
|
host = netutil.NormalizeHost(host)
|
|
if host == "" {
|
|
host = netutil.NormalizeHost(listener.PublicHost)
|
|
}
|
|
if host == "" {
|
|
host = normalizeExportHost("", listener.BindAddress, listener.Listen)
|
|
}
|
|
if host == "" {
|
|
return nil, fmt.Errorf("cannot determine public host for listener; set public_host / server.public_url or access via a public hostname (IPv4/IPv6 supported)")
|
|
}
|
|
cfg, err := decodeURIConfig(listener.Config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
profile := export.BuildProfileFromConfig(listener, host, cfg)
|
|
if profile.Host != "" {
|
|
host = profile.Host
|
|
}
|
|
port := profile.Port
|
|
export.ApplyToConfig(cfg, listener, profile)
|
|
if strings.ContainsAny(port, ",-") {
|
|
return nil, fmt.Errorf("URI export requires a single listener port; ranges and port lists are not representable in a share URI")
|
|
}
|
|
if port == "" {
|
|
return nil, fmt.Errorf("URI export requires a listener port")
|
|
}
|
|
switch strings.ToLower(listener.Protocol) {
|
|
case "shadowsocks":
|
|
return shadowsocksURIs(listener.Name, host, port, cfg)
|
|
case "snell":
|
|
return snellURIs(listener.Name, host, port, cfg)
|
|
case "vless":
|
|
return vlessURIs(listener.Name, host, port, cfg)
|
|
case "vmess":
|
|
return vmessURIs(listener.Name, host, port, cfg)
|
|
case "trojan":
|
|
return trojanURIs(listener.Name, host, port, cfg)
|
|
case "hysteria2":
|
|
return hysteria2URIs(listener.Name, host, port, cfg)
|
|
case "tuic", "tuic-v4", "tuic-v5":
|
|
return tuicURIs(listener.Name, host, port, cfg)
|
|
case "shadowquic":
|
|
return shadowQUICURIs(listener.Name, host, port, cfg)
|
|
case "anytls":
|
|
return anytlsURIs(listener.Name, host, port, cfg)
|
|
case "mieru":
|
|
return mieruURIs(listener.Name, host, port, cfg)
|
|
case "sudoku":
|
|
return sudokuURIs(listener.Name, host, port, cfg)
|
|
case "trusttunnel":
|
|
return trustTunnelURIs(listener.Name, host, port, cfg)
|
|
default:
|
|
return nil, fmt.Errorf("URI export is not supported for listener protocol %q", listener.Protocol)
|
|
}
|
|
}
|
|
|
|
// applyURIAccessProfile remains for tests/legacy callers; delegates to export.
|
|
func applyURIAccessProfile(cfg map[string]interface{}, listener models.Listener, connectHost, port string) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
p := export.BuildProfileFromConfig(listener, connectHost, cfg)
|
|
if port != "" {
|
|
p.Port = port
|
|
}
|
|
export.ApplyToConfig(cfg, listener, p)
|
|
}
|
|
|
|
func decodeURIConfig(raw string) (map[string]interface{}, error) {
|
|
if strings.TrimSpace(raw) == "" {
|
|
return map[string]interface{}{}, nil
|
|
}
|
|
var cfg map[string]interface{}
|
|
if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
|
|
return nil, fmt.Errorf("invalid listener configuration: %w", err)
|
|
}
|
|
if cfg == nil {
|
|
return map[string]interface{}{}, nil
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func userMap(cfg map[string]interface{}) map[string]interface{} {
|
|
if users, ok := cfg["users"].(map[string]interface{}); ok {
|
|
return users
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func userRows(cfg map[string]interface{}) []map[string]interface{} {
|
|
users, _ := cfg["users"].([]interface{})
|
|
rows := make([]map[string]interface{}, 0, len(users))
|
|
for _, raw := range users {
|
|
if row, ok := raw.(map[string]interface{}); ok {
|
|
rows = append(rows, row)
|
|
}
|
|
}
|
|
return rows
|
|
}
|
|
|
|
func query(base string, values map[string]string) string {
|
|
q := url.Values{}
|
|
for k, v := range values {
|
|
if v != "" {
|
|
q.Set(k, v)
|
|
}
|
|
}
|
|
if encoded := q.Encode(); encoded != "" {
|
|
return base + "?" + encoded
|
|
}
|
|
return base
|
|
}
|
|
|
|
func addName(uri, name string) string {
|
|
if name == "" {
|
|
return uri
|
|
}
|
|
// Remark fragment; PathEscape matches common panel clients (incl. ).
|
|
return uri + "#" + url.PathEscape(name)
|
|
}
|