273 lines
7.3 KiB
Go
273 lines
7.3 KiB
Go
package main
|
|
|
|
// bot_provision.go — bridges the bot to the panel's account creation.
|
|
// SSH: Store.UpsertUser. Xray: xrayMgr.AddXrayClient + UpsertXrayClientMeta.
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ---------- credential generation ----------
|
|
|
|
const credAlphabet = "abcdefghijkmnpqrstuvwxyz23456789"
|
|
|
|
func randString(n int) string {
|
|
b := make([]byte, n)
|
|
if _, err := rand.Read(b); err != nil {
|
|
// extremely unlikely; fall back to a fixed-length timestamp-free filler
|
|
for i := range b {
|
|
b[i] = credAlphabet[0]
|
|
}
|
|
return string(b)
|
|
}
|
|
for i := range b {
|
|
b[i] = credAlphabet[int(b[i])%len(credAlphabet)]
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func genUsername(prefix string) string {
|
|
if prefix == "" {
|
|
prefix = "ssh"
|
|
}
|
|
return prefix + randString(6)
|
|
}
|
|
|
|
func genPassword() string { return randString(10) }
|
|
|
|
func uuidV4() string {
|
|
b := make([]byte, 16)
|
|
_, _ = rand.Read(b)
|
|
b[6] = (b[6] & 0x0f) | 0x40
|
|
b[8] = (b[8] & 0x3f) | 0x80
|
|
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
|
}
|
|
|
|
// ---------- SSH ----------
|
|
|
|
func createSSHUser(ctx context.Context, store *Store, username, password string, expiresAt time.Time, maxConns, upMbps, downMbps int, owner string) error {
|
|
cfg := UserConfig{
|
|
Username: username,
|
|
Password: password,
|
|
MaxConnections: maxConns,
|
|
ExpiresAt: expiresAt.UTC().Format(time.RFC3339),
|
|
LimitMbpsUp: upMbps,
|
|
LimitMbpsDown: downMbps,
|
|
OwnerUsername: owner,
|
|
}
|
|
if err := store.UpsertUser(ctx, cfg); err != nil {
|
|
return err
|
|
}
|
|
reloadUsersFromDB(ctx, store)
|
|
return nil
|
|
}
|
|
|
|
// renewSSHUser extends an existing SSH account's expiry, preserving credentials.
|
|
func renewSSHUser(ctx context.Context, store *Store, username string, newExpiry time.Time) error {
|
|
u, ok := userMgr.Get(username)
|
|
if !ok {
|
|
return fmt.Errorf("conta SSH %q não encontrada", username)
|
|
}
|
|
cfg := u.Cfg
|
|
cfg.ExpiresAt = newExpiry.UTC().Format(time.RFC3339)
|
|
if err := store.UpsertUser(ctx, cfg); err != nil {
|
|
return err
|
|
}
|
|
reloadUsersFromDB(ctx, store)
|
|
return nil
|
|
}
|
|
|
|
// ---------- Xray ----------
|
|
|
|
func createXrayClient(ctx context.Context, store *Store, inboundTag, protocol string, expiresAt time.Time, maxConns int, owner, publicHost string) (uuid, link string, err error) {
|
|
if inboundTag == "" {
|
|
return "", "", fmt.Errorf("plano Xray sem inbound configurado")
|
|
}
|
|
uuid = uuidV4()
|
|
email := "bot-" + uuid[:8]
|
|
if err = xrayMgr.AddXrayClient(inboundTag, uuid, email); err != nil {
|
|
return "", "", err
|
|
}
|
|
exp := expiresAt
|
|
meta := XrayClientMeta{
|
|
UUID: uuid,
|
|
Name: email,
|
|
Email: email,
|
|
InboundTag: inboundTag,
|
|
OwnerUsername: owner,
|
|
MaxConns: maxConns,
|
|
ExpiresAt: &exp,
|
|
}
|
|
if e := store.UpsertXrayClientMeta(ctx, meta); e != nil {
|
|
// The client is already live in Xray; a metadata failure must not
|
|
// abort delivery. Log and continue.
|
|
log.Printf("[bot] xray meta save for %s: %v", uuid, e)
|
|
}
|
|
xrayMgr.restartIfExternalRunning()
|
|
link = buildXrayLink(inboundTag, protocol, uuid, publicHost, email)
|
|
return uuid, link, nil
|
|
}
|
|
|
|
func renewXrayClient(ctx context.Context, store *Store, uuid string, newExpiry time.Time) error {
|
|
meta, err := store.GetXrayClientMeta(ctx, uuid)
|
|
if err != nil {
|
|
return fmt.Errorf("cliente Xray não encontrado: %w", err)
|
|
}
|
|
exp := newExpiry
|
|
meta.ExpiresAt = &exp
|
|
return store.UpsertXrayClientMeta(ctx, *meta)
|
|
}
|
|
|
|
// ---------- Xray connection link ----------
|
|
|
|
type xrayInboundDetail struct {
|
|
Protocol string
|
|
Port int
|
|
Network string
|
|
Security string
|
|
Path string
|
|
Host string
|
|
SNI string
|
|
ServiceName string
|
|
}
|
|
|
|
// inboundDetail reads streamSettings for an inbound from the raw Xray config.
|
|
func (m *XrayManager) inboundDetail(tag string) (*xrayInboundDetail, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
data, err := m.readConfigLocked()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var cfg struct {
|
|
Inbounds []json.RawMessage `json:"inbounds"`
|
|
}
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, raw := range cfg.Inbounds {
|
|
var ib struct {
|
|
Tag string `json:"tag"`
|
|
Protocol string `json:"protocol"`
|
|
Port json.RawMessage `json:"port"`
|
|
StreamSettings map[string]interface{} `json:"streamSettings"`
|
|
}
|
|
if err := json.Unmarshal(raw, &ib); err != nil {
|
|
continue
|
|
}
|
|
if ib.Tag != tag {
|
|
continue
|
|
}
|
|
d := &xrayInboundDetail{Protocol: strings.ToLower(ib.Protocol)}
|
|
var pnum int
|
|
if json.Unmarshal(ib.Port, &pnum) == nil {
|
|
d.Port = pnum
|
|
} else {
|
|
var pstr string
|
|
if json.Unmarshal(ib.Port, &pstr) == nil {
|
|
d.Port, _ = strconv.Atoi(pstr)
|
|
}
|
|
}
|
|
if ss := ib.StreamSettings; ss != nil {
|
|
d.Network, _ = ss["network"].(string)
|
|
d.Security, _ = ss["security"].(string)
|
|
d.Path, d.Host, d.ServiceName = extractStreamParams(ss, d.Network)
|
|
if tls, ok := ss["tlsSettings"].(map[string]interface{}); ok {
|
|
d.SNI, _ = tls["serverName"].(string)
|
|
}
|
|
if rl, ok := ss["realitySettings"].(map[string]interface{}); ok {
|
|
if sn, _ := rl["serverNames"].([]interface{}); len(sn) > 0 {
|
|
d.SNI, _ = sn[0].(string)
|
|
}
|
|
}
|
|
}
|
|
return d, nil
|
|
}
|
|
return nil, fmt.Errorf("inbound %q não encontrado", tag)
|
|
}
|
|
|
|
func extractStreamParams(ss map[string]interface{}, network string) (path, host, serviceName string) {
|
|
get := func(key, field string) string {
|
|
if sub, ok := ss[key].(map[string]interface{}); ok {
|
|
v, _ := sub[field].(string)
|
|
return v
|
|
}
|
|
return ""
|
|
}
|
|
switch network {
|
|
case "ws":
|
|
return get("wsSettings", "path"), get("wsSettings", "host"), ""
|
|
case "xhttp":
|
|
return get("xhttpSettings", "path"), get("xhttpSettings", "host"), ""
|
|
case "httpupgrade":
|
|
return get("httpupgradeSettings", "path"), get("httpupgradeSettings", "host"), ""
|
|
case "grpc":
|
|
return "", "", get("grpcSettings", "serviceName")
|
|
case "http", "h2":
|
|
return get("httpSettings", "path"), get("httpSettings", "host"), ""
|
|
}
|
|
return "", "", ""
|
|
}
|
|
|
|
// buildXrayLink assembles a shareable connection URI. Best-effort: if the config
|
|
// can't be read it still returns a minimal link with host/port/uuid.
|
|
func buildXrayLink(inboundTag, protocol, uuid, publicHost, label string) string {
|
|
d, err := xrayMgr.inboundDetail(inboundTag)
|
|
if err != nil || d == nil {
|
|
if protocol == "" {
|
|
protocol = "vless"
|
|
}
|
|
return fmt.Sprintf("%s://%s@%s#%s", protocol, uuid, publicHost, url.QueryEscape(label))
|
|
}
|
|
if protocol == "" {
|
|
protocol = d.Protocol
|
|
}
|
|
host := publicHost
|
|
if host == "" {
|
|
host = d.SNI
|
|
}
|
|
addr := fmt.Sprintf("%s:%d", host, d.Port)
|
|
|
|
q := url.Values{}
|
|
if d.Network != "" {
|
|
q.Set("type", d.Network)
|
|
}
|
|
if d.Security != "" {
|
|
q.Set("security", d.Security)
|
|
}
|
|
if d.Path != "" {
|
|
q.Set("path", d.Path)
|
|
}
|
|
if d.Host != "" {
|
|
q.Set("host", d.Host)
|
|
}
|
|
if d.SNI != "" {
|
|
q.Set("sni", d.SNI)
|
|
}
|
|
if d.ServiceName != "" {
|
|
q.Set("serviceName", d.ServiceName)
|
|
}
|
|
|
|
switch protocol {
|
|
case "vmess":
|
|
conf := map[string]interface{}{
|
|
"v": "2", "ps": label, "add": host, "port": strconv.Itoa(d.Port),
|
|
"id": uuid, "aid": "0", "scy": "auto", "net": d.Network,
|
|
"type": "none", "host": d.Host, "path": d.Path, "tls": d.Security, "sni": d.SNI,
|
|
}
|
|
b, _ := json.Marshal(conf)
|
|
return "vmess://" + base64.StdEncoding.EncodeToString(b)
|
|
default: // vless, trojan
|
|
return fmt.Sprintf("%s://%s@%s?%s#%s", protocol, uuid, addr, q.Encode(), url.QueryEscape(label))
|
|
}
|
|
}
|