Fix VLESS share links to Remnawave format and default inbound to VLESS.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+138
-39
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -272,66 +273,164 @@ ORDER BY i.sort_order ASC, i.tag ASC, n.name ASC NULLS LAST`, client.ID)
|
||||
seen[key] = true
|
||||
|
||||
if h.Available && h.Address != "" {
|
||||
h.Link = buildShareLink(uid, client.Username+"-"+h.Tag, &h)
|
||||
h.Link = buildShareLink(uid, hostDisplayName(client.Username, &h), &h)
|
||||
}
|
||||
hosts = append(hosts, h)
|
||||
}
|
||||
return hosts, rows.Err()
|
||||
}
|
||||
|
||||
func hostDisplayName(username string, h *models.SubHost) string {
|
||||
if h.Remark != "" {
|
||||
return h.Remark
|
||||
}
|
||||
if h.NodeName != "" {
|
||||
return h.NodeName
|
||||
}
|
||||
if h.Tag != "" {
|
||||
return h.Tag
|
||||
}
|
||||
return username
|
||||
}
|
||||
|
||||
// buildShareLink builds Remnawave-compatible share URIs.
|
||||
// Query keys are intentionally ordered (not alphabetically).
|
||||
func buildShareLink(uid, name string, h *models.SubHost) string {
|
||||
name = url.QueryEscape(name)
|
||||
q := url.Values{}
|
||||
q.Set("type", h.Network)
|
||||
q.Set("security", h.Security)
|
||||
if h.Path != "" {
|
||||
q.Set("path", h.Path)
|
||||
}
|
||||
if h.HostHeader != "" {
|
||||
q.Set("host", h.HostHeader)
|
||||
}
|
||||
if h.SNI != "" {
|
||||
q.Set("sni", h.SNI)
|
||||
}
|
||||
if h.Fingerprint != "" {
|
||||
q.Set("fp", h.Fingerprint)
|
||||
}
|
||||
if h.Flow != "" {
|
||||
q.Set("flow", h.Flow)
|
||||
}
|
||||
if h.ALPN != "" {
|
||||
q.Set("alpn", h.ALPN)
|
||||
}
|
||||
if h.PublicKey != "" {
|
||||
q.Set("pbk", h.PublicKey)
|
||||
}
|
||||
if h.ShortID != "" {
|
||||
q.Set("sid", h.ShortID)
|
||||
}
|
||||
switch h.ProtocolCode {
|
||||
switch strings.ToLower(h.ProtocolCode) {
|
||||
case "vless":
|
||||
q.Set("encryption", "none")
|
||||
return fmt.Sprintf("vless://%s@%s:%d?%s#%s", uid, h.Address, h.Port, q.Encode(), name)
|
||||
return buildVLESSLink(uid, name, h)
|
||||
case "vmess":
|
||||
return fmt.Sprintf("vmess://%s@%s:%d?%s#%s", uid, h.Address, h.Port, q.Encode(), name)
|
||||
return buildVMessLink(uid, name, h)
|
||||
case "trojan":
|
||||
pass := uid
|
||||
if h.Password != "" {
|
||||
pass = h.Password
|
||||
}
|
||||
return fmt.Sprintf("trojan://%s@%s:%d?%s#%s", pass, h.Address, h.Port, q.Encode(), name)
|
||||
return buildTrojanLink(uid, name, h)
|
||||
case "shadowsocks":
|
||||
method := h.SSMethod
|
||||
if method == "" {
|
||||
method = "aes-128-gcm"
|
||||
}
|
||||
userInfo := base64.RawURLEncoding.EncodeToString([]byte(method + ":" + uid))
|
||||
userInfo := base64.StdEncoding.EncodeToString([]byte(method + ":" + uid))
|
||||
return fmt.Sprintf("ss://%s@%s:%d#%s", userInfo, h.Address, h.Port, name)
|
||||
default:
|
||||
return fmt.Sprintf("%s://%s@%s:%d?%s#%s", h.ProtocolCode, uid, h.Address, h.Port, q.Encode(), name)
|
||||
// Non-Xray protocols are not shareable as vless-style URIs.
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func buildVLESSLink(uid, name string, h *models.SubHost) string {
|
||||
network := h.Network
|
||||
if network == "" {
|
||||
network = "tcp"
|
||||
}
|
||||
security := h.Security
|
||||
if security == "" {
|
||||
security = "none"
|
||||
}
|
||||
flow := h.Flow
|
||||
if flow == "" && security == "reality" && network == "tcp" {
|
||||
flow = "xtls-rprx-vision"
|
||||
}
|
||||
fp := h.Fingerprint
|
||||
if fp == "" && (security == "reality" || security == "tls") {
|
||||
fp = "chrome"
|
||||
}
|
||||
|
||||
var parts []string
|
||||
parts = append(parts, "encryption=none")
|
||||
if flow != "" {
|
||||
parts = append(parts, "flow="+url.QueryEscape(flow))
|
||||
}
|
||||
parts = append(parts, "type="+url.QueryEscape(network))
|
||||
parts = append(parts, "security="+url.QueryEscape(security))
|
||||
if h.Path != "" {
|
||||
parts = append(parts, "path="+url.QueryEscape(h.Path))
|
||||
}
|
||||
if h.HostHeader != "" {
|
||||
parts = append(parts, "host="+url.QueryEscape(h.HostHeader))
|
||||
}
|
||||
if h.SNI != "" {
|
||||
parts = append(parts, "sni="+url.QueryEscape(h.SNI))
|
||||
}
|
||||
if fp != "" {
|
||||
parts = append(parts, "fp="+url.QueryEscape(fp))
|
||||
}
|
||||
if h.ALPN != "" {
|
||||
parts = append(parts, "alpn="+url.QueryEscape(h.ALPN))
|
||||
}
|
||||
if h.PublicKey != "" {
|
||||
parts = append(parts, "pbk="+url.QueryEscape(h.PublicKey))
|
||||
}
|
||||
if h.ShortID != "" {
|
||||
parts = append(parts, "sid="+url.QueryEscape(h.ShortID))
|
||||
}
|
||||
return fmt.Sprintf("vless://%s@%s:%d?%s#%s", uid, h.Address, h.Port, strings.Join(parts, "&"), name)
|
||||
}
|
||||
|
||||
func buildVMessLink(uid, name string, h *models.SubHost) string {
|
||||
network := orDefault(h.Network, "tcp")
|
||||
security := orDefault(h.Security, "none")
|
||||
var parts []string
|
||||
parts = append(parts, "type="+url.QueryEscape(network))
|
||||
parts = append(parts, "security="+url.QueryEscape(security))
|
||||
if h.Path != "" {
|
||||
parts = append(parts, "path="+url.QueryEscape(h.Path))
|
||||
}
|
||||
if h.HostHeader != "" {
|
||||
parts = append(parts, "host="+url.QueryEscape(h.HostHeader))
|
||||
}
|
||||
if h.SNI != "" {
|
||||
parts = append(parts, "sni="+url.QueryEscape(h.SNI))
|
||||
}
|
||||
if h.Fingerprint != "" {
|
||||
parts = append(parts, "fp="+url.QueryEscape(h.Fingerprint))
|
||||
}
|
||||
if h.PublicKey != "" {
|
||||
parts = append(parts, "pbk="+url.QueryEscape(h.PublicKey))
|
||||
}
|
||||
if h.ShortID != "" {
|
||||
parts = append(parts, "sid="+url.QueryEscape(h.ShortID))
|
||||
}
|
||||
return fmt.Sprintf("vmess://%s@%s:%d?%s#%s", uid, h.Address, h.Port, strings.Join(parts, "&"), name)
|
||||
}
|
||||
|
||||
func buildTrojanLink(uid, name string, h *models.SubHost) string {
|
||||
pass := uid
|
||||
if h.Password != "" {
|
||||
pass = h.Password
|
||||
}
|
||||
network := orDefault(h.Network, "tcp")
|
||||
security := orDefault(h.Security, "tls")
|
||||
var parts []string
|
||||
parts = append(parts, "type="+url.QueryEscape(network))
|
||||
parts = append(parts, "security="+url.QueryEscape(security))
|
||||
if h.Path != "" {
|
||||
parts = append(parts, "path="+url.QueryEscape(h.Path))
|
||||
}
|
||||
if h.HostHeader != "" {
|
||||
parts = append(parts, "host="+url.QueryEscape(h.HostHeader))
|
||||
}
|
||||
if h.SNI != "" {
|
||||
parts = append(parts, "sni="+url.QueryEscape(h.SNI))
|
||||
}
|
||||
if h.Fingerprint != "" {
|
||||
parts = append(parts, "fp="+url.QueryEscape(h.Fingerprint))
|
||||
}
|
||||
if h.PublicKey != "" {
|
||||
parts = append(parts, "pbk="+url.QueryEscape(h.PublicKey))
|
||||
}
|
||||
if h.ShortID != "" {
|
||||
parts = append(parts, "sid="+url.QueryEscape(h.ShortID))
|
||||
}
|
||||
return fmt.Sprintf("trojan://%s@%s:%d?%s#%s", pass, h.Address, h.Port, strings.Join(parts, "&"), name)
|
||||
}
|
||||
|
||||
func orDefault(v, def string) string {
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ClientSubscriptionLinks builds share links for available hosts only.
|
||||
func ClientSubscriptionLinks(db *sql.DB, client *models.Client) ([]string, error) {
|
||||
hosts, err := ClientSubscriptionHosts(db, client)
|
||||
|
||||
@@ -78,7 +78,7 @@ func (s *Server) profileView(w http.ResponseWriter, r *http.Request) {
|
||||
"UserName": s.Auth.CurrentName(r),
|
||||
"Active": "profiles",
|
||||
"Profile": p,
|
||||
"Protocols": protocols,
|
||||
"Protocols": inboundProtocols(protocols),
|
||||
"Flash": r.URL.Query().Get("ok"),
|
||||
"Error": r.URL.Query().Get("err"),
|
||||
})
|
||||
@@ -226,6 +226,12 @@ func (s *Server) inboundCreate(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?err=protocol", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
switch proto.Code {
|
||||
case "vless", "vmess", "trojan", "shadowsocks":
|
||||
default:
|
||||
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?err=protocol", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
in, err := parseInboundForm(r, profileID, protocolID, proto)
|
||||
if err != nil {
|
||||
log.Printf("inbound form: %v", err)
|
||||
@@ -278,12 +284,23 @@ func (s *Server) inboundEditGet(w http.ResponseWriter, r *http.Request) {
|
||||
"Active": "profiles",
|
||||
"Profile": p,
|
||||
"Inbound": in,
|
||||
"Protocols": protocols,
|
||||
"Protocols": inboundProtocols(protocols),
|
||||
"Flash": r.URL.Query().Get("ok"),
|
||||
"Error": r.URL.Query().Get("err"),
|
||||
})
|
||||
}
|
||||
|
||||
func inboundProtocols(all []models.Protocol) []models.Protocol {
|
||||
var out []models.Protocol
|
||||
for _, p := range all {
|
||||
switch p.Code {
|
||||
case "vless", "vmess", "trojan", "shadowsocks":
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Server) inboundUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
profileID, err := uuid.Parse(mux.Vars(r)["id"])
|
||||
if err != nil {
|
||||
@@ -311,6 +328,12 @@ func (s *Server) inboundUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.String()+"?err=protocol", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
switch proto.Code {
|
||||
case "vless", "vmess", "trojan", "shadowsocks":
|
||||
default:
|
||||
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.String()+"?err=protocol", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
in, err := parseInboundForm(r, profileID, protocolID, proto)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.String()+"?err=keys", http.StatusSeeOther)
|
||||
|
||||
Reference in New Issue
Block a user