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.

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