importer.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. package sessionimport
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "os"
  9. "path/filepath"
  10. "github.com/gotd/td/session"
  11. )
  12. // PyrogramMeta is the parsed content of <phone>.json inside a 协议号 folder.
  13. // Unknown fields are ignored.
  14. type PyrogramMeta struct {
  15. Phone string `json:"phone"`
  16. APIID int `json:"api_id"`
  17. APIHash string `json:"api_hash"`
  18. AppID int `json:"app_id"` // some generators use app_id/app_hash instead
  19. AppHash string `json:"app_hash"`
  20. Device string `json:"device"`
  21. AppVersion string `json:"app_version"`
  22. SDK string `json:"sdk"`
  23. LangPack string `json:"lang_pack"`
  24. LangCode string `json:"lang_code"`
  25. SystemLangCode string `json:"system_lang_code"`
  26. FirstName string `json:"first_name"`
  27. LastName string `json:"last_name"`
  28. Username string `json:"username"`
  29. TwoFA string `json:"twoFA"`
  30. }
  31. // Normalize resolves API credential aliasing. 协议号 JSON files use api_id/api_hash
  32. // or app_id/app_hash interchangeably; we coalesce them.
  33. func (m *PyrogramMeta) Normalize() {
  34. if m.APIID == 0 && m.AppID != 0 {
  35. m.APIID = m.AppID
  36. }
  37. if m.APIHash == "" && m.AppHash != "" {
  38. m.APIHash = m.AppHash
  39. }
  40. }
  41. // Result is the outcome of importing one protocol-number folder.
  42. type Result struct {
  43. Meta PyrogramMeta
  44. TwoFAPlaintext string // from 2fa_password.txt if Meta.TwoFA empty
  45. SessionData *session.Data
  46. UsedSource string // "pyrogram" or "tdata"
  47. SessionFile string // sessions/<phone>.json
  48. OriginDir string // sessions/<phone>.origin/
  49. PyrogramErr string // empty on success
  50. TdataErr string // empty on success or if tdata absent
  51. }
  52. // StagedFile is one file extracted from a multipart upload, already written to a
  53. // temp directory. RelPath preserves the browser's webkitRelativePath minus the
  54. // top-level folder name (so e.g. "13252753163/tdata/key_datas" becomes
  55. // "tdata/key_datas").
  56. type StagedFile struct {
  57. RelPath string
  58. AbsPath string
  59. }
  60. // Import parses staged files from a 协议号 folder, converts the session, and
  61. // materializes sessions/<phone>.json + origin/ backup under sessionsDir.
  62. // The caller (HTTP handler) persists the returned metadata to the DB.
  63. //
  64. // Precondition: all StagedFile.AbsPath must exist on disk. stagedFiles is the
  65. // output of the handler's multipart parser.
  66. func Import(ctx context.Context, sessionsDir string, stagedFiles []StagedFile) (*Result, error) {
  67. if len(stagedFiles) == 0 {
  68. return nil, errors.New("no files uploaded")
  69. }
  70. var metaPath, sessionPath, twoFAPath, tdataDir string
  71. for _, f := range stagedFiles {
  72. switch {
  73. case filepath.Ext(f.RelPath) == ".json" && !hasSep(f.RelPath):
  74. metaPath = f.AbsPath
  75. case filepath.Ext(f.RelPath) == ".session":
  76. sessionPath = f.AbsPath
  77. case filepath.Base(f.RelPath) == "2fa_password.txt":
  78. twoFAPath = f.AbsPath
  79. case hasTdataPrefix(f.RelPath):
  80. // Remember the parent dir containing tdata/
  81. dir := filepath.Dir(f.AbsPath)
  82. // Walk up until we find a folder literally named "tdata".
  83. for dir != "" && filepath.Base(dir) != "tdata" {
  84. dir = filepath.Dir(dir)
  85. }
  86. if dir != "" {
  87. tdataDir = dir
  88. }
  89. }
  90. }
  91. if metaPath == "" {
  92. return nil, errors.New("missing <phone>.json in upload")
  93. }
  94. if sessionPath == "" && tdataDir == "" {
  95. return nil, errors.New("upload has neither .session nor tdata/")
  96. }
  97. meta, err := readMeta(metaPath)
  98. if err != nil {
  99. return nil, err
  100. }
  101. meta.Normalize()
  102. if meta.Phone == "" {
  103. return nil, errors.New("<phone>.json missing phone field")
  104. }
  105. if meta.APIID == 0 || meta.APIHash == "" {
  106. return nil, errors.New("<phone>.json missing api_id / api_hash")
  107. }
  108. res := &Result{Meta: meta}
  109. // Prefer 2FA from JSON; fall back to 2fa_password.txt.
  110. if meta.TwoFA == "" && twoFAPath != "" {
  111. b, err := os.ReadFile(twoFAPath)
  112. if err == nil {
  113. res.TwoFAPlaintext = sanitizeTwoFA(string(b))
  114. }
  115. } else {
  116. res.TwoFAPlaintext = meta.TwoFA
  117. }
  118. // Try Pyrogram first.
  119. if sessionPath != "" {
  120. data, err := ConvertPyrogramSession(sessionPath)
  121. if err == nil {
  122. res.SessionData = data
  123. res.UsedSource = "pyrogram"
  124. } else {
  125. res.PyrogramErr = err.Error()
  126. }
  127. }
  128. // Fallback to tdata.
  129. if res.SessionData == nil && tdataDir != "" {
  130. data, err := ConvertTdataSession(tdataDir, res.TwoFAPlaintext)
  131. if err == nil {
  132. res.SessionData = data
  133. res.UsedSource = "tdata"
  134. } else {
  135. res.TdataErr = err.Error()
  136. }
  137. }
  138. if res.SessionData == nil {
  139. return res, fmt.Errorf("session conversion failed (pyrogram: %s; tdata: %s)",
  140. nonempty(res.PyrogramErr, "n/a"), nonempty(res.TdataErr, "n/a"))
  141. }
  142. // Materialize session JSON.
  143. if err := os.MkdirAll(sessionsDir, 0o755); err != nil {
  144. return res, fmt.Errorf("create sessions dir: %w", err)
  145. }
  146. res.SessionFile = filepath.Join(sessionsDir, meta.Phone+".json")
  147. storage := &session.FileStorage{Path: res.SessionFile}
  148. loader := session.Loader{Storage: storage}
  149. if err := loader.Save(ctx, res.SessionData); err != nil {
  150. return res, fmt.Errorf("save gotd session: %w", err)
  151. }
  152. // Materialize origin backup (best-effort; non-fatal).
  153. res.OriginDir = filepath.Join(sessionsDir, meta.Phone+".origin")
  154. _ = os.RemoveAll(res.OriginDir)
  155. if err := os.MkdirAll(res.OriginDir, 0o755); err == nil {
  156. for _, f := range stagedFiles {
  157. dst := filepath.Join(res.OriginDir, f.RelPath)
  158. if err := os.MkdirAll(filepath.Dir(dst), 0o755); err == nil {
  159. _ = copyFile(f.AbsPath, dst)
  160. }
  161. }
  162. }
  163. return res, nil
  164. }
  165. func readMeta(path string) (PyrogramMeta, error) {
  166. var m PyrogramMeta
  167. b, err := os.ReadFile(path)
  168. if err != nil {
  169. return m, fmt.Errorf("read meta json: %w", err)
  170. }
  171. if err := json.Unmarshal(b, &m); err != nil {
  172. return m, fmt.Errorf("parse meta json: %w", err)
  173. }
  174. return m, nil
  175. }
  176. func sanitizeTwoFA(s string) string {
  177. // Trim CR/LF/space — 2fa_password.txt often has a trailing newline.
  178. for len(s) > 0 && (s[len(s)-1] == '\n' || s[len(s)-1] == '\r' || s[len(s)-1] == ' ') {
  179. s = s[:len(s)-1]
  180. }
  181. return s
  182. }
  183. func hasSep(p string) bool {
  184. for i := 0; i < len(p); i++ {
  185. if p[i] == '/' || p[i] == '\\' {
  186. return true
  187. }
  188. }
  189. return false
  190. }
  191. func hasTdataPrefix(p string) bool {
  192. // "tdata/..." or "tdata\..."
  193. if len(p) < 6 {
  194. return false
  195. }
  196. return (p[:5] == "tdata" && (p[5] == '/' || p[5] == '\\'))
  197. }
  198. func nonempty(s, alt string) string {
  199. if s == "" {
  200. return alt
  201. }
  202. return s
  203. }
  204. func copyFile(src, dst string) error {
  205. in, err := os.Open(src)
  206. if err != nil {
  207. return err
  208. }
  209. defer in.Close()
  210. out, err := os.Create(dst)
  211. if err != nil {
  212. return err
  213. }
  214. defer out.Close()
  215. _, err = io.Copy(out, in)
  216. return err
  217. }