|
|
@@ -6,6 +6,7 @@ import (
|
|
|
"io"
|
|
|
"mime/multipart"
|
|
|
"os"
|
|
|
+ "path"
|
|
|
"path/filepath"
|
|
|
"strconv"
|
|
|
"strings"
|
|
|
@@ -285,14 +286,36 @@ func (h *TgAccountHandler) Import(c *gin.Context) {
|
|
|
|
|
|
res, err := sessionimport.Import(ctx, h.sessionsDir, staged)
|
|
|
if err != nil {
|
|
|
- Fail(c, 422, err.Error())
|
|
|
+ detail := gin.H{"error": err.Error()}
|
|
|
+ if res != nil {
|
|
|
+ detail["pyrogram_err"] = res.PyrogramErr
|
|
|
+ detail["tdata_err"] = res.TdataErr
|
|
|
+ detail["tmp_dir"] = stageDir
|
|
|
+ }
|
|
|
+ c.JSON(422, gin.H{
|
|
|
+ "code": 422,
|
|
|
+ "message": err.Error(),
|
|
|
+ "data": detail,
|
|
|
+ })
|
|
|
return
|
|
|
}
|
|
|
|
|
|
+ cleanupOnFail := func() {
|
|
|
+ if res != nil {
|
|
|
+ if res.SessionFile != "" {
|
|
|
+ _ = os.Remove(res.SessionFile)
|
|
|
+ }
|
|
|
+ if res.OriginDir != "" {
|
|
|
+ _ = os.RemoveAll(res.OriginDir)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// Duplicate check.
|
|
|
var count int64
|
|
|
h.store.DB.Model(&model.TgAccount{}).Where("phone = ?", res.Meta.Phone).Count(&count)
|
|
|
if count > 0 {
|
|
|
+ cleanupOnFail()
|
|
|
Fail(c, 409, "该手机号已存在")
|
|
|
return
|
|
|
}
|
|
|
@@ -302,6 +325,7 @@ func (h *TgAccountHandler) Import(c *gin.Context) {
|
|
|
if res.TwoFAPlaintext != "" && h.crypto != nil {
|
|
|
twoFAEnc, err = h.crypto.Encrypt(res.TwoFAPlaintext)
|
|
|
if err != nil {
|
|
|
+ cleanupOnFail()
|
|
|
Fail(c, 500, "encrypt 2fa: "+err.Error())
|
|
|
return
|
|
|
}
|
|
|
@@ -330,9 +354,11 @@ func (h *TgAccountHandler) Import(c *gin.Context) {
|
|
|
}
|
|
|
if err := h.store.DB.Create(&acc).Error; err != nil {
|
|
|
if strings.Contains(strings.ToLower(err.Error()), "duplicate") {
|
|
|
+ cleanupOnFail()
|
|
|
Fail(c, 409, "该手机号已存在")
|
|
|
return
|
|
|
}
|
|
|
+ cleanupOnFail()
|
|
|
Fail(c, 500, "db create: "+err.Error())
|
|
|
return
|
|
|
}
|
|
|
@@ -340,8 +366,11 @@ func (h *TgAccountHandler) Import(c *gin.Context) {
|
|
|
// Rebuild account manager so new account is Acquireable.
|
|
|
h.reloadAccounts()
|
|
|
|
|
|
- // Auto-test.
|
|
|
- testResult := h.runTest(c.Request.Context(), &acc)
|
|
|
+ // Auto-test — detach from the HTTP request context so a client disconnect
|
|
|
+ // cannot falsely mark a freshly imported account as dead.
|
|
|
+ bgCtx, bgCancel := context.WithTimeout(context.Background(), 45*time.Second)
|
|
|
+ defer bgCancel()
|
|
|
+ testResult := h.runTest(bgCtx, &acc)
|
|
|
if testResult["status"] == "fail" {
|
|
|
h.store.DB.Model(&acc).Updates(map[string]any{
|
|
|
"enabled": false,
|
|
|
@@ -409,11 +438,19 @@ func normalizeRel(filename, topPrefix string) string {
|
|
|
if topPrefix != "" && strings.HasPrefix(p, topPrefix+"/") {
|
|
|
p = strings.TrimPrefix(p, topPrefix+"/")
|
|
|
}
|
|
|
- // Reject any path that tries to escape.
|
|
|
- if strings.Contains(p, "../") || strings.HasPrefix(p, "/") {
|
|
|
+ // Use path.Clean (forward-slash aware) to resolve . and .. components,
|
|
|
+ // then reject any result that escapes the staging root.
|
|
|
+ cleaned := path.Clean(p)
|
|
|
+ if cleaned == "." || cleaned == ".." {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+ if strings.HasPrefix(cleaned, "../") {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+ if filepath.IsAbs(cleaned) || strings.HasPrefix(cleaned, "/") {
|
|
|
return ""
|
|
|
}
|
|
|
- return p
|
|
|
+ return cleaned
|
|
|
}
|
|
|
|
|
|
func saveUpload(fh *multipart.FileHeader, dst string) error {
|