diff options
Diffstat (limited to 'web/service/inbound.go')
| -rw-r--r-- | web/service/inbound.go | 203 |
1 files changed, 203 insertions, 0 deletions
diff --git a/web/service/inbound.go b/web/service/inbound.go index 19c3d80c..7d5d8932 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/google/uuid" "github.com/mhsanaei/3x-ui/v2/database" "github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/logger" @@ -26,6 +27,12 @@ type InboundService struct { xrayApi xray.XrayAPI } +type CopyClientsResult struct { + Added []string `json:"added"` + Skipped []string `json:"skipped"` + Errors []string `json:"errors"` +} + // GetInbounds retrieves all inbounds for a specific user. // Returns a slice of inbound models with their associated client statistics. func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) { @@ -750,6 +757,202 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) { return needRestart, tx.Save(oldInbound).Error } +func (s *InboundService) getClientPrimaryKey(protocol model.Protocol, client model.Client) string { + switch protocol { + case model.Trojan: + return client.Password + case model.Shadowsocks: + return client.Email + case model.Hysteria: + return client.Auth + default: + return client.ID + } +} + +func (s *InboundService) writeBackClientSubID(sourceInboundID int, sourceProtocol model.Protocol, client model.Client, subID string) (bool, error) { + client.SubID = subID + client.UpdatedAt = time.Now().UnixMilli() + clientID := s.getClientPrimaryKey(sourceProtocol, client) + if clientID == "" { + return false, common.NewError("empty client ID") + } + + settingsBytes, err := json.Marshal(map[string][]model.Client{ + "clients": []model.Client{client}, + }) + if err != nil { + return false, err + } + + updatePayload := &model.Inbound{ + Id: sourceInboundID, + Settings: string(settingsBytes), + } + return s.UpdateInboundClient(updatePayload, clientID) +} + +func (s *InboundService) generateRandomCredential(targetProtocol model.Protocol) string { + switch targetProtocol { + case model.VMESS, model.VLESS: + return uuid.NewString() + default: + return strings.ReplaceAll(uuid.NewString(), "-", "") + } +} + +func (s *InboundService) buildTargetClientFromSource(source model.Client, targetProtocol model.Protocol, email string, flow string) (model.Client, error) { + nowTs := time.Now().UnixMilli() + target := source + target.Email = email + target.CreatedAt = nowTs + target.UpdatedAt = nowTs + + target.ID = "" + target.Password = "" + target.Auth = "" + target.Flow = "" + + switch targetProtocol { + case model.VMESS: + target.ID = s.generateRandomCredential(targetProtocol) + case model.VLESS: + target.ID = s.generateRandomCredential(targetProtocol) + if flow == "xtls-rprx-vision" || flow == "xtls-rprx-vision-udp443" { + target.Flow = flow + } + case model.Trojan, model.Shadowsocks: + target.Password = s.generateRandomCredential(targetProtocol) + case model.Hysteria: + target.Auth = s.generateRandomCredential(targetProtocol) + default: + target.ID = s.generateRandomCredential(targetProtocol) + } + + return target, nil +} + +func (s *InboundService) nextAvailableCopiedEmail(originalEmail string, targetID int, occupied map[string]struct{}) string { + base := fmt.Sprintf("%s_%d", originalEmail, targetID) + candidate := base + suffix := 0 + for { + if _, exists := occupied[strings.ToLower(candidate)]; !exists { + occupied[strings.ToLower(candidate)] = struct{}{} + return candidate + } + suffix++ + candidate = fmt.Sprintf("%s_%d", base, suffix) + } +} + +func (s *InboundService) CopyInboundClients(targetInboundID int, sourceInboundID int, clientEmails []string, flow string) (*CopyClientsResult, bool, error) { + result := &CopyClientsResult{ + Added: []string{}, + Skipped: []string{}, + Errors: []string{}, + } + if targetInboundID == sourceInboundID { + return result, false, common.NewError("source and target inbounds must be different") + } + + targetInbound, err := s.GetInbound(targetInboundID) + if err != nil { + return result, false, err + } + sourceInbound, err := s.GetInbound(sourceInboundID) + if err != nil { + return result, false, err + } + + sourceClients, err := s.GetClients(sourceInbound) + if err != nil { + return result, false, err + } + if len(sourceClients) == 0 { + return result, false, nil + } + + allowedEmails := map[string]struct{}{} + if len(clientEmails) > 0 { + for _, email := range clientEmails { + allowedEmails[strings.ToLower(strings.TrimSpace(email))] = struct{}{} + } + } + + occupiedEmails := map[string]struct{}{} + allEmails, err := s.getAllEmails() + if err != nil { + return result, false, err + } + for _, email := range allEmails { + clean := strings.Trim(email, "\"") + if clean != "" { + occupiedEmails[strings.ToLower(clean)] = struct{}{} + } + } + + newClients := make([]model.Client, 0) + needRestart := false + for _, sourceClient := range sourceClients { + originalEmail := strings.TrimSpace(sourceClient.Email) + if originalEmail == "" { + continue + } + if len(allowedEmails) > 0 { + if _, ok := allowedEmails[strings.ToLower(originalEmail)]; !ok { + continue + } + } + + if sourceClient.SubID == "" { + newSubID := uuid.NewString() + subNeedRestart, subErr := s.writeBackClientSubID(sourceInbound.Id, sourceInbound.Protocol, sourceClient, newSubID) + if subErr != nil { + result.Errors = append(result.Errors, fmt.Sprintf("%s: failed to write source subId: %v", originalEmail, subErr)) + continue + } + if subNeedRestart { + needRestart = true + } + sourceClient.SubID = newSubID + } + + targetEmail := s.nextAvailableCopiedEmail(originalEmail, targetInboundID, occupiedEmails) + targetClient, buildErr := s.buildTargetClientFromSource(sourceClient, targetInbound.Protocol, targetEmail, flow) + if buildErr != nil { + result.Errors = append(result.Errors, fmt.Sprintf("%s: %v", originalEmail, buildErr)) + continue + } + newClients = append(newClients, targetClient) + result.Added = append(result.Added, targetEmail) + } + + if len(newClients) == 0 { + return result, needRestart, nil + } + + settingsPayload, err := json.Marshal(map[string][]model.Client{ + "clients": newClients, + }) + if err != nil { + return result, needRestart, err + } + + addNeedRestart, err := s.AddInboundClient(&model.Inbound{ + Id: targetInboundID, + Settings: string(settingsPayload), + }) + if err != nil { + return result, needRestart, err + } + if addNeedRestart { + needRestart = true + } + + return result, needRestart, nil +} + func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool, error) { oldInbound, err := s.GetInbound(inboundId) if err != nil { |
