bot TG
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user