Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/MHSanaei/3x-ui.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'web/service')
-rw-r--r--web/service/inbound.go203
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 {