3x-ui/web/job/ldap_sync_job.go
2025-09-26 12:25:08 +03:00

253 lines
7.8 KiB
Go

package job
import (
"time"
"x-ui/database/model"
"x-ui/logger"
ldaputil "x-ui/util/ldap"
"x-ui/web/service"
"strings"
"github.com/google/uuid"
"strconv"
)
type LdapSyncJob struct {
settingService service.SettingService
inboundService service.InboundService
xrayService service.XrayService
}
func NewLdapSyncJob() *LdapSyncJob {
return new(LdapSyncJob)
}
func (j *LdapSyncJob) Run() {
enabled, err := j.settingService.GetLdapEnable()
if err != nil || !enabled {
return
}
host, _ := j.settingService.GetLdapHost()
port, _ := j.settingService.GetLdapPort()
useTLS, _ := j.settingService.GetLdapUseTLS()
bindDN, _ := j.settingService.GetLdapBindDN()
password, _ := j.settingService.GetLdapPassword()
baseDN, _ := j.settingService.GetLdapBaseDN()
userFilter, _ := j.settingService.GetLdapUserFilter()
userAttr, _ := j.settingService.GetLdapUserAttr()
// Generic flag settings
flagField, _ := j.settingService.GetLdapFlagField()
if flagField == "" {
flagField, _ = j.settingService.GetLdapVlessField()
}
truthyCSV, _ := j.settingService.GetLdapTruthyValues()
invert, _ := j.settingService.GetLdapInvertFlag()
cfg := ldaputil.Config{
Host: host,
Port: port,
UseTLS: useTLS,
BindDN: bindDN,
Password: password,
BaseDN: baseDN,
UserFilter: userFilter,
UserAttr: userAttr,
FlagField: flagField,
TruthyVals: splitCsv(truthyCSV),
Invert: invert,
}
flags, err := ldaputil.FetchVlessFlags(cfg)
if err != nil {
logger.Warning("LDAP sync failed:", err)
return
}
// Settings for auto create/delete
inboundTagsCSV, _ := j.settingService.GetLdapInboundTags()
autoCreate, _ := j.settingService.GetLdapAutoCreate()
autoDelete, _ := j.settingService.GetLdapAutoDelete()
defGB, _ := j.settingService.GetLdapDefaultTotalGB()
defExpiryDays, _ := j.settingService.GetLdapDefaultExpiryDays()
defLimitIP, _ := j.settingService.GetLdapDefaultLimitIP()
inboundTags := splitCsv(inboundTagsCSV)
// Build a set of LDAP emails for delete checks
ldapEmails := map[string]struct{}{}
for email := range flags { ldapEmails[email] = struct{}{} }
// Create/enable according to LDAP flags
for email, allowed := range flags {
// ensure exists if allowed and autoCreate
if allowed && autoCreate && len(inboundTags) > 0 {
for _, tag := range inboundTags {
j.ensureClientExists(tag, email, defGB, defExpiryDays, defLimitIP)
}
}
changed, needRestart, err := j.inboundService.SetClientEnableByEmail(email, allowed)
if err != nil {
logger.Debugf("LDAP sync skip email=%s err=%v", email, err)
continue
}
if changed {
logger.Infof("LDAP sync: %s -> enable=%v", email, allowed)
}
if needRestart {
j.xrayService.SetToNeedRestart()
}
}
// Auto delete: find clients in targeted inbounds that are not in LDAP
if autoDelete && len(inboundTags) > 0 {
for _, tag := range inboundTags {
j.deleteClientsNotInLDAP(tag, ldapEmails)
}
}
}
func splitCsv(s string) []string {
if s == "" {
return []string{"true", "1", "yes", "on"}
}
parts := strings.Split(s, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
v := strings.TrimSpace(p)
if v != "" {
out = append(out, v)
}
}
return out
}
// ensureClientExists adds client with defaults to inbound tag if not present
func (j *LdapSyncJob) ensureClientExists(inboundTag string, email string, defGB int, defExpiryDays int, defLimitIP int) {
inbounds, err := j.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("ensureClientExists: get inbounds failed:", err)
return
}
var target *model.Inbound
for _, ib := range inbounds {
if ib.Tag == inboundTag {
target = ib
break
}
}
if target == nil {
logger.Debugf("ensureClientExists: inbound tag %s not found", inboundTag)
return
}
// check if email already exists in this inbound
clients, err := j.inboundService.GetClients(target)
if err == nil {
for _, c := range clients {
if c.Email == email {
return
}
}
}
// build new client according to protocol
newClient := model.Client{
Email: email,
Enable: true,
LimitIP: defLimitIP,
TotalGB: int64(defGB),
}
if defExpiryDays > 0 {
newClient.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()
}
switch target.Protocol {
case model.Trojan:
newClient.Password = uuid.NewString()
case model.Shadowsocks:
newClient.Password = uuid.NewString()
default: // VMESS/VLESS and others using ID
newClient.ID = uuid.NewString()
}
// prepare inbound payload with only the new client
payload := &model.Inbound{Id: target.Id}
payload.Settings = `{"clients":[` + j.clientToJSON(newClient) + `]}`
if _, err := j.inboundService.AddInboundClient(payload); err != nil {
logger.Warning("ensureClientExists: add client failed:", err)
} else {
j.xrayService.SetToNeedRestart()
logger.Infof("LDAP auto-create: %s in %s", email, inboundTag)
}
}
// deleteClientsNotInLDAP removes clients from inbound tag that are not in ldapEmails
func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[string]struct{}) {
inbounds, err := j.inboundService.GetAllInbounds()
if err != nil {
return
}
for _, ib := range inbounds {
if ib.Tag != inboundTag {
continue
}
clients, err := j.inboundService.GetClients(ib)
if err != nil {
continue
}
for _, c := range clients {
if _, ok := ldapEmails[c.Email]; !ok {
// determine clientId per protocol
clientId := c.ID
if ib.Protocol == model.Trojan {
clientId = c.Password
} else if ib.Protocol == model.Shadowsocks {
clientId = c.Email
}
needRestart, err := j.inboundService.DelInboundClient(ib.Id, clientId)
if err == nil {
if needRestart {
j.xrayService.SetToNeedRestart()
}
logger.Infof("LDAP auto-delete: %s from %s", c.Email, inboundTag)
}
}
}
}
}
// clientToJSON serializes minimal client fields to JSON object string without extra deps
func (j *LdapSyncJob) clientToJSON(c model.Client) string {
// construct minimal JSON manually to avoid importing json for simple case
b := strings.Builder{}
b.WriteString("{")
if c.ID != "" {
b.WriteString("\"id\":\"")
b.WriteString(c.ID)
b.WriteString("\",")
}
if c.Password != "" {
b.WriteString("\"password\":\"")
b.WriteString(c.Password)
b.WriteString("\",")
}
b.WriteString("\"email\":\"")
b.WriteString(c.Email)
b.WriteString("\",")
b.WriteString("\"enable\":")
if c.Enable { b.WriteString("true") } else { b.WriteString("false") }
b.WriteString(",")
b.WriteString("\"limitIp\":")
b.WriteString(strconv.Itoa(c.LimitIP))
b.WriteString(",")
b.WriteString("\"totalGB\":")
b.WriteString(strconv.FormatInt(c.TotalGB, 10))
if c.ExpiryTime > 0 {
b.WriteString(",\"expiryTime\":")
b.WriteString(strconv.FormatInt(c.ExpiryTime, 10))
}
b.WriteString("}")
return b.String()
}