mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-10-14 12:09:12 +00:00
253 lines
7.8 KiB
Go
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()
|
|
}
|
|
|
|
|