You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

import.go 7.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. // Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
  2. // released under the MIT license
  3. package irc
  4. import (
  5. "encoding/json"
  6. "fmt"
  7. "log"
  8. "os"
  9. "strconv"
  10. "time"
  11. "github.com/tidwall/buntdb"
  12. "github.com/ergochat/ergo/irc/bunt"
  13. "github.com/ergochat/ergo/irc/datastore"
  14. "github.com/ergochat/ergo/irc/modes"
  15. "github.com/ergochat/ergo/irc/utils"
  16. )
  17. const (
  18. // produce a hardcoded version of the database schema
  19. // XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal
  20. // (to ensure that no matter what code changes happen elsewhere, we're still producing a
  21. // db of the hardcoded version)
  22. importDBSchemaVersion = 23
  23. )
  24. type userImport struct {
  25. Name string
  26. Hash string
  27. Email string
  28. RegisteredAt int64 `json:"registeredAt"`
  29. Vhost string
  30. AdditionalNicks []string `json:"additionalNicks"`
  31. Certfps []string
  32. }
  33. type channelImport struct {
  34. Name string
  35. Founder string
  36. RegisteredAt int64 `json:"registeredAt"`
  37. Topic string
  38. TopicSetBy string `json:"topicSetBy"`
  39. TopicSetAt int64 `json:"topicSetAt"`
  40. Amode map[string]string
  41. Modes string
  42. Key string
  43. Limit int
  44. Forward string
  45. }
  46. type databaseImport struct {
  47. Version int
  48. Source string
  49. Users map[string]userImport
  50. Channels map[string]channelImport
  51. }
  52. func convertAmodes(raw map[string]string, validCfUsernames utils.HashSet[string]) (result map[string]modes.Mode, err error) {
  53. result = make(map[string]modes.Mode)
  54. for accountName, mode := range raw {
  55. if len(mode) != 1 {
  56. return nil, fmt.Errorf("invalid mode %s for account %s", mode, accountName)
  57. }
  58. cfname, err := CasefoldName(accountName)
  59. if err != nil || !validCfUsernames.Has(cfname) {
  60. log.Printf("skipping invalid amode recipient %s\n", accountName)
  61. } else {
  62. result[cfname] = modes.Mode(mode[0])
  63. }
  64. }
  65. return
  66. }
  67. func doImportDBGeneric(config *Config, dbImport databaseImport, credsType CredentialsVersion, tx *buntdb.Tx) (err error) {
  68. requiredVersion := 1
  69. if dbImport.Version != requiredVersion {
  70. return fmt.Errorf("unsupported version of the db for import: version %d is required", requiredVersion)
  71. }
  72. tx.Set(keySchemaVersion, strconv.Itoa(importDBSchemaVersion), nil)
  73. tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil)
  74. cfUsernames := make(utils.HashSet[string])
  75. skeletonToUsername := make(map[string]string)
  76. warnSkeletons := false
  77. for username, userInfo := range dbImport.Users {
  78. cfUsername, err := CasefoldName(username)
  79. skeleton, skErr := Skeleton(username)
  80. if err != nil || skErr != nil {
  81. log.Printf("invalid username %s: %v\n", username, err)
  82. continue
  83. }
  84. if existingSkelUser, ok := skeletonToUsername[skeleton]; ok {
  85. log.Printf("Users %s and %s have confusable nicknames; this may render one or both accounts unusable\n", username, existingSkelUser)
  86. warnSkeletons = true
  87. } else {
  88. skeletonToUsername[skeleton] = username
  89. }
  90. var certfps []string
  91. for _, certfp := range userInfo.Certfps {
  92. normalizedCertfp, err := utils.NormalizeCertfp(certfp)
  93. if err == nil {
  94. certfps = append(certfps, normalizedCertfp)
  95. } else {
  96. log.Printf("invalid certfp %s for %s\n", username, certfp)
  97. }
  98. }
  99. credentials := AccountCredentials{
  100. Version: credsType,
  101. PassphraseHash: []byte(userInfo.Hash),
  102. Certfps: certfps,
  103. }
  104. marshaledCredentials, err := json.Marshal(&credentials)
  105. if err != nil {
  106. log.Printf("invalid credentials for %s: %v\n", username, err)
  107. continue
  108. }
  109. tx.Set(fmt.Sprintf(keyAccountExists, cfUsername), "1", nil)
  110. tx.Set(fmt.Sprintf(keyAccountVerified, cfUsername), "1", nil)
  111. tx.Set(fmt.Sprintf(keyAccountName, cfUsername), userInfo.Name, nil)
  112. settings := AccountSettings{Email: userInfo.Email}
  113. settingsBytes, _ := json.Marshal(settings)
  114. tx.Set(fmt.Sprintf(keyAccountSettings, cfUsername), string(settingsBytes), nil)
  115. tx.Set(fmt.Sprintf(keyAccountCredentials, cfUsername), string(marshaledCredentials), nil)
  116. tx.Set(fmt.Sprintf(keyAccountRegTime, cfUsername), strconv.FormatInt(userInfo.RegisteredAt, 10), nil)
  117. if userInfo.Vhost != "" {
  118. vhinfo := VHostInfo{
  119. Enabled: true,
  120. ApprovedVHost: userInfo.Vhost,
  121. }
  122. vhBytes, err := json.Marshal(vhinfo)
  123. if err == nil {
  124. tx.Set(fmt.Sprintf(keyAccountVHost, cfUsername), string(vhBytes), nil)
  125. } else {
  126. log.Printf("couldn't serialize vhost for %s: %v\n", username, err)
  127. }
  128. }
  129. if len(userInfo.AdditionalNicks) != 0 {
  130. tx.Set(fmt.Sprintf(keyAccountAdditionalNicks, cfUsername), marshalReservedNicks(userInfo.AdditionalNicks), nil)
  131. }
  132. for _, certfp := range certfps {
  133. tx.Set(fmt.Sprintf(keyCertToAccount, certfp), cfUsername, nil)
  134. }
  135. cfUsernames.Add(cfUsername)
  136. }
  137. // TODO fix this:
  138. for chname, chInfo := range dbImport.Channels {
  139. _, err := CasefoldChannel(chname)
  140. if err != nil {
  141. log.Printf("invalid channel name %s: %v", chname, err)
  142. continue
  143. }
  144. cffounder, err := CasefoldName(chInfo.Founder)
  145. if err != nil {
  146. log.Printf("invalid founder %s for channel %s: %v", chInfo.Founder, chname, err)
  147. continue
  148. }
  149. var regInfo RegisteredChannel
  150. regInfo.Name = chname
  151. regInfo.UUID = utils.GenerateUUIDv4()
  152. regInfo.Founder = cffounder
  153. regInfo.RegisteredAt = time.Unix(0, chInfo.RegisteredAt).UTC()
  154. if chInfo.Topic != "" {
  155. regInfo.Topic = chInfo.Topic
  156. regInfo.TopicSetBy = chInfo.TopicSetBy
  157. regInfo.TopicSetTime = time.Unix(0, chInfo.TopicSetAt).UTC()
  158. }
  159. if len(chInfo.Amode) != 0 {
  160. m, err := convertAmodes(chInfo.Amode, cfUsernames)
  161. if err == nil {
  162. regInfo.AccountToUMode = m
  163. } else {
  164. log.Printf("couldn't process amodes for %s: %v", chname, err)
  165. }
  166. }
  167. for _, mode := range chInfo.Modes {
  168. regInfo.Modes = append(regInfo.Modes, modes.Mode(mode))
  169. }
  170. regInfo.Key = chInfo.Key
  171. if chInfo.Limit > 0 {
  172. regInfo.UserLimit = chInfo.Limit
  173. }
  174. if chInfo.Forward != "" {
  175. if _, err := CasefoldChannel(chInfo.Forward); err == nil {
  176. regInfo.Forward = chInfo.Forward
  177. }
  178. }
  179. if j, err := json.Marshal(regInfo); err == nil {
  180. tx.Set(bunt.BuntKey(datastore.TableChannels, regInfo.UUID), string(j), nil)
  181. } else {
  182. log.Printf("couldn't serialize channel %s: %v", chname, err)
  183. }
  184. }
  185. if warnSkeletons {
  186. log.Printf("NOTE: you may be able to avoid confusability issues by changing the server casemapping setting to `ascii`\n")
  187. log.Printf("However, this will prevent the use of non-ASCII Unicode characters in nicknames\n")
  188. }
  189. return nil
  190. }
  191. func doImportDB(config *Config, dbImport databaseImport, tx *buntdb.Tx) (err error) {
  192. switch dbImport.Source {
  193. case "atheme":
  194. return doImportDBGeneric(config, dbImport, CredentialsAtheme, tx)
  195. case "anope":
  196. return doImportDBGeneric(config, dbImport, CredentialsAnope, tx)
  197. default:
  198. return fmt.Errorf("unsupported import source: %s", dbImport.Source)
  199. }
  200. }
  201. func ImportDB(config *Config, infile string) (err error) {
  202. data, err := os.ReadFile(infile)
  203. if err != nil {
  204. return
  205. }
  206. var dbImport databaseImport
  207. err = json.Unmarshal(data, &dbImport)
  208. if err != nil {
  209. return err
  210. }
  211. err = checkDBReadyForInit(config.Datastore.Path)
  212. if err != nil {
  213. return err
  214. }
  215. db, err := buntdb.Open(config.Datastore.Path)
  216. if err != nil {
  217. return err
  218. }
  219. performImport := func(tx *buntdb.Tx) (err error) {
  220. return doImportDB(config, dbImport, tx)
  221. }
  222. err = db.Update(performImport)
  223. db.Close()
  224. return
  225. }