gosl.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. // gosl implements the name2key/key2name functionality for about
  2. // ten million avatar names (≈⅙ of the total database).
  3. package main
  4. import (
  5. // "bufio" // replaced by the more sophisticated readline (gwyneth 20211106)
  6. "encoding/json"
  7. "fmt"
  8. "net/http"
  9. "net/http/fcgi"
  10. "os"
  11. "path/filepath"
  12. // "regexp"
  13. "strings"
  14. // "time"
  15. "github.com/dgraph-io/badger/v3"
  16. // "github.com/dgraph-io/badger/options"
  17. // "github.com/fsnotify/fsnotify"
  18. "github.com/google/uuid"
  19. "github.com/op/go-logging"
  20. flag "github.com/spf13/pflag"
  21. "github.com/spf13/viper"
  22. "github.com/syndtr/goleveldb/leveldb"
  23. "github.com/tidwall/buntdb"
  24. "gitlab.com/cznic/readline"
  25. // "gopkg.in/go-playground/validator.v9" // to validate UUIDs... and a lot of thinks
  26. "gopkg.in/natefinch/lumberjack.v2"
  27. )
  28. // NullUUID is the "all zeros" UUID. We assign it to a string for analogy with LSL, where
  29. // keys are also strings.
  30. var NullUUID = uuid.Nil.String()
  31. // Logging setup.
  32. var log = logging.MustGetLogger("gosl") // configuration for the go-logging logger, must be available everywhere
  33. // Sets up type of log.
  34. var logFormat logging.Formatter
  35. // Opt is used for Badger database setup.
  36. var Opt badger.Options
  37. // Set to the program name, which is the first entry in os.Args[].
  38. // Do some cleanup as well. This is just for avoiding redundancy and having
  39. // a nice-to-remember name! (gwyneth 20231203)
  40. var programName = filepath.Clean(os.Args[0])
  41. // AvatarUUID is the type that we store in the database; we keep a record from which grid it came from.
  42. // Field names need to be capitalised for JSON marshalling (it has to do with the way it works)
  43. // Note that we will store both UUID -> AvatarName *and* AvatarName -> UUID on the same database,
  44. //
  45. // thus the apparent redundancy in fields! (gwyneth 20211030)
  46. //
  47. // The 'validate' decorator is for usage with the go-playground validator, currently unused (gwyneth 20211031)
  48. type avatarUUID struct {
  49. AvatarName string `json:"name" form:"name" binding:"required" validate:"omitempty,alphanum"`
  50. UUID string `json:"key" form:"key" binding:"required" validate:"omitempty,uuid4_rfc4122"`
  51. Grid string `json:"grid" form:"grid" validate:"omitempty,alphanum"` // Grid name, if retrieved; "Production" is for SL Aditi.
  52. }
  53. /*
  54. .__
  55. _____ _____ |__| ____
  56. / \\__ \ | |/ \
  57. | Y Y \/ __ \| | | \
  58. |__|_| (____ /__|___| /
  59. \/ \/ \/
  60. */
  61. // Configuration options.
  62. type goslConfigOptions struct {
  63. BATCH_BLOCK int // how many entries to write to the database as a block; the bigger, the faster, but the more memory it consumes.
  64. loopBatch int // how many entries to skip when emitting debug messages in a tight loop.
  65. noMemory, isServer, isShell bool // !isServer && !isShell => FastCGI!
  66. myDir, myPort, importFilename, database string
  67. databaseName string // Name of the database, as placed on disk. Defaults to "gosl-database.db".
  68. configFilename string // name (+ path?) of the configuratio file.
  69. dbNamePath string // for BuntDB.
  70. logLevel, logFilename string // for logs.
  71. maxSize, maxBackups, maxAge int // logs configuration options.
  72. }
  73. var goslConfig goslConfigOptions // list of all configuration options.
  74. var kv *badger.DB // current KV store being used (Badger).
  75. // loadConfiguration reads our configuration from a `config.ini` file,
  76. func loadConfiguration() {
  77. fmt.Println("Reading ", programName, " configuration:") // note that we might not have go-logging active as yet, so we use fmt and write to stdout
  78. // Open our config file and extract relevant data from there
  79. // Find and read the config file
  80. if err := viper.ReadInConfig(); err != nil {
  81. fmt.Printf("error reading config file %q, falling back to defaults - error was: %s\n", goslConfig.configFilename, err)
  82. // we fall back to what we have
  83. }
  84. // NOTE(gwyneth): the authors of say that 100000 is way too much for Badger.
  85. // Let's see what happens with BuntDB
  86. viper.SetDefault("config.BATCH_BLOCK", 100000)
  87. goslConfig.BATCH_BLOCK = viper.GetInt("config.BATCH_BLOCK")
  88. viper.SetDefault("config.loopBatch", 1000)
  89. goslConfig.loopBatch = viper.GetInt("config.loopBatch")
  90. viper.SetDefault("config.myPort", 3000)
  91. goslConfig.myPort = viper.GetString("config.myPort")
  92. viper.SetDefault("config.myDir", "slkvdb")
  93. goslConfig.myDir = viper.GetString("config.myDir")
  94. viper.SetDefault("config.isServer", false)
  95. goslConfig.isServer = viper.GetBool("config.isServer")
  96. viper.SetDefault("config.isShell", false)
  97. goslConfig.isShell = viper.GetBool("config.isShell")
  98. viper.SetDefault("config.database", "badger") // currently, badger, boltdb, leveldb.
  99. goslConfig.database = viper.GetString("config.database")
  100. viper.SetDefault("config.databaseName", "badger") // currently, badger, boltdb, leveldb.
  101. goslConfig.databaseName = viper.GetString("config.databaseName")
  102. viper.SetDefault("options.importFilename", "") // must be empty by default.
  103. goslConfig.importFilename = viper.GetString("options.importFilename")
  104. viper.SetDefault("options.noMemory", false)
  105. goslConfig.noMemory = viper.GetBool("options.noMemory")
  106. // Logging options
  107. viper.SetDefault("log.Filename", "gosl.log")
  108. goslConfig.logFilename = viper.GetString("log.Filename")
  109. viper.SetDefault("log.logLevel", "ERROR")
  110. goslConfig.logLevel = viper.GetString("log.logLevel")
  111. viper.SetDefault("log.MaxSize", 10)
  112. goslConfig.maxSize = viper.GetInt("log.MaxSize")
  113. viper.SetDefault("log.MaxBackups", 3)
  114. goslConfig.maxBackups = viper.GetInt("log.MaxBackups")
  115. viper.SetDefault("log.MaxAge", 28)
  116. goslConfig.maxAge = viper.GetInt("log.MaxAge")
  117. }
  118. // main() starts here.
  119. func main() {
  120. // Config viper, which reads in the configuration file every time it's needed.
  121. // Note that we need some hard-coded variables for the path and config file name.
  122. viper.SetDefault(goslConfig.configFilename, "config.ini")
  123. viper.SetConfigName(goslConfig.configFilename)
  124. // just to make sure; it's the same format as OpenSimulator (or MySQL) config files.
  125. viper.SetConfigType("ini")
  126. // optionally, look for config in the working directory.
  127. viper.AddConfigPath(".")
  128. // this is also a great place to put standard configurations:
  129. // NOTE:
  130. viper.AddConfigPath(filepath.Join("$HOME/.config/", programName))
  131. // last chance — check on the usual place for Go source.
  132. viper.AddConfigPath("$HOME/go/src/git.gwynethllewelyn.net/GwynethLlewelyn/gosl-basics/")
  133. loadConfiguration()
  134. // Flag setup; can be overridden by config file.
  135. flag.StringVarP(&goslConfig.myPort, "port", "p", "3000", "Server port")
  136. flag.StringVar( &goslConfig.myDir, "dir", "slkvdb", "Directory where database files are stored")
  137. flag.BoolVar( &goslConfig.isServer, "server", false, "Run as server on port " + goslConfig.myPort)
  138. flag.BoolVar( &goslConfig.isShell, "shell", false, "Run as an interactive shell")
  139. flag.StringVarP(&goslConfig.importFilename, "import", "i", "", "Import database from W-Hat (use the csv.bz2 versions)")
  140. flag.StringVar( &goslConfig.configFilename, "config", "config.ini", "Configuration filename [extension defines type, INI by default]")
  141. flag.StringVar( &goslConfig.database, "database", "badger", "Database type [badger | buntdb | leveldb]")
  142. flag.StringVarP(&goslConfig.databaseName, "databaseName", "n", "gosl-database.db", "Database file name")
  143. flag.BoolVar( &goslConfig.noMemory, "nomemory", true, "Attempt to use only disk to save memory on Badger (important for shared webservers)")
  144. flag.StringVarP(&goslConfig.logLevel, "debug", "d", "ERROR", "Logging level, e.g. one of [DEBUG | ERROR | NOTICE | INFO]")
  145. flag.IntVarP( &goslConfig.loopBatch, "loopbatch", "l", 1000, "How many entries to skip when emitting debug messages in a tight loop. Only useful when importing huge databases with high logging levels. Set to 1 if you wish to see logs for all entries.")
  146. flag.IntVarP( &goslConfig.BATCH_BLOCK, "batchblock", "b", 100000, "How many entries to write to the database as a block; the bigger, the faster, but the more memory it consumes.")
  147. // default is FastCGI
  148. flag.Parse()
  149. if err := viper.BindPFlags(flag.CommandLine); err != nil {
  150. fmt.Printf("error parsing/binding flags: %s\n", err)
  151. }
  152. if goslConfig.configFilename != "config.ini" {
  153. viper.SetConfigName(goslConfig.configFilename)
  154. // we can switch filetypes here
  155. ext := filepath.Ext(goslConfig.configFilename)[1:]
  156. viper.SetConfigType(ext)
  157. // Find and read the config fil
  158. if err := viper.ReadInConfig(); err != nil {
  159. fmt.Printf("error reading config file %q [type %s], falling back to defaults - error was: %s\n", goslConfig.configFilename, ext, err)
  160. // we fall back to what we have
  161. }
  162. }
  163. // Avoid division by zero...
  164. if goslConfig.BATCH_BLOCK < 1 {
  165. goslConfig.BATCH_BLOCK = 1
  166. }
  167. if goslConfig.loopBatch < 1 {
  168. goslConfig.loopBatch = 1
  169. }
  170. // this will allow our configuration file to be 'read on demand'
  171. // TODO(gwyneth): There is something broken with this, no reason why... (gwyneth 20211026)
  172. // viper.WatchConfig()
  173. // viper.OnConfigChange(func(e fsnotify.Event) {
  174. // if goslConfig.isServer || goslConfig.isShell {
  175. // fmt.Println("Config file changed:", e.Name) // BUG(gwyneth): FastCGI cannot write to output
  176. // }
  177. // loadConfiguration()
  178. // })
  179. // NOTE(gwyneth): We cannot write to stdout if we're running as FastCGI, only to logs!
  180. if goslConfig.isServer || goslConfig.isShell {
  181. fmt.Println(programName, " is starting...")
  182. }
  183. // This is mostly to deal with scoping issues below. (gwyneth 20211106)
  184. var err error
  185. // Setup the lumberjack rotating logger. This is because we need it for the go-logging logger when writing to files. (20170813)
  186. rotatingLogger := &lumberjack.Logger{
  187. Filename: goslConfig.logFilename,
  188. MaxSize: goslConfig.maxSize, // megabytes
  189. MaxBackups: goslConfig.maxBackups,
  190. MaxAge: goslConfig.maxAge, //days
  191. }
  192. // Set formatting for stderr and file (basically the same).
  193. logFormat := logging.MustStringFormatter(`%{color}%{time:2006/01/02 15:04:05.0} %{shortfile} - %{shortfunc} ▶ %{level:.4s}%{color:reset} %{message}`) // must be initialised or all hell breaks loose
  194. // Setup the go-logging Logger. Do **not** log to stderr if running as FastCGI!
  195. backendFile := logging.NewLogBackend(rotatingLogger, "", 0)
  196. backendFileFormatter := logging.NewBackendFormatter(backendFile, logFormat)
  197. backendFileLeveled := logging.AddModuleLevel(backendFileFormatter)
  198. theLogLevel, err := logging.LogLevel(goslConfig.logLevel)
  199. if err != nil {
  200. log.Warningf("could not set log level to %q — invalid?\nlogging.LogLevel() returned error %q\n", goslConfig.logLevel, err)
  201. } else {
  202. log.Debugf("requested file log level: %q\n", theLogLevel.String())
  203. backendFileLeveled.SetLevel(theLogLevel, "gosl") // we just send debug data to logs if we run asshell
  204. log.Debugf("file log level set to: %v\n", backendFileLeveled.GetLevel("gosl"))
  205. }
  206. if goslConfig.isServer || goslConfig.isShell {
  207. backendStderr := logging.NewLogBackend(os.Stderr, "", 0)
  208. backendStderrFormatter := logging.NewBackendFormatter(backendStderr, logFormat)
  209. backendStderrLeveled := logging.AddModuleLevel(backendStderrFormatter)
  210. log.Debugf("requested stderr log level: %q\n", theLogLevel.String())
  211. backendStderrLeveled.SetLevel(theLogLevel, "gosl")
  212. log.Debugf("stderr log level set to: %v\n", backendStderrLeveled.GetLevel("gosl"))
  213. }
  214. /*
  215. // deprecated, now we set it explicitly if desired
  216. if goslConfig.isShell {
  217. backendStderrLeveled.SetLevel(logging.DEBUG, "gosl") // shell is meant to be for debugging mostly
  218. } else {
  219. backendStderrLeveled.SetLevel(logging.INFO, "gosl")
  220. }
  221. logging.SetBackend(backendStderrLeveled, backendFileLeveled)
  222. } else {
  223. logging.SetBackend(backendFileLeveled) // FastCGI only logs to file
  224. }
  225. */
  226. goslConfig.dbNamePath = filepath.Join(goslConfig.myDir, goslConfig.databaseName)
  227. log.Debugf("Full config: %+v\n", goslConfig)
  228. // Check if this directory actually exists; if not, create it. Panic if something wrong happens (we cannot proceed without a valid directory for the database to be written)
  229. if stat, err := os.Stat(goslConfig.myDir); err == nil && stat.IsDir() {
  230. // path is a valid directory
  231. log.Debugf("valid directory: %q\n", goslConfig.myDir)
  232. } else {
  233. // try to create directory
  234. if err = os.Mkdir(goslConfig.myDir, 0700); err != nil {
  235. if !os.IsExist(err) {
  236. checkErr(err)
  237. } else {
  238. log.Debugf("directory %q exists, no need to create it\n", goslConfig.myDir)
  239. }
  240. }
  241. log.Debugf("created new directory: %q\n", goslConfig.myDir)
  242. }
  243. // Special options configuration.
  244. // Currently, this is only needed for Badger v3, the others have much simpler configurations.
  245. // (gwyneth 20211106)
  246. switch goslConfig.database {
  247. case "badger":
  248. // Badger v3 - fully rewritten configuration (much simpler!!) (gwyneth 20211026)
  249. if goslConfig.noMemory {
  250. // use disk; note that unlike the others, Badger generates its own filenames,
  251. // we can only pass a _directory_... (gwyneth 20211027)
  252. // goslConfig.dbNamePath = filepath.Join(goslConfig.myDir, goslConfig.databaseName)
  253. // try to create directory
  254. if err = os.Mkdir(goslConfig.dbNamePath, 0700); err != nil {
  255. if !os.IsExist(err) {
  256. checkErr(err)
  257. } else {
  258. log.Debugf("directory %q exists, no need to create it\n", goslConfig.dbNamePath)
  259. }
  260. } else {
  261. log.Debugf("created new directory: %q\n", goslConfig.dbNamePath)
  262. }
  263. Opt = badger.DefaultOptions(goslConfig.dbNamePath)
  264. log.Debugf("entering disk mode, Opt is %+v\n", Opt)
  265. } else {
  266. // Use only memory
  267. Opt = badger.LSMOnlyOptions("").WithInMemory(true)
  268. Opt.WithLevelSizeMultiplier(1)
  269. Opt.WithNumMemtables(1)
  270. Opt.WithValueDir(Opt.Dir) // probably not needed
  271. log.Debugf("entering memory-only mode, Opt is %+v\n", Opt)
  272. }
  273. // common config
  274. Opt.WithLogger(log) // set the internal logger to our own rotating logger
  275. Opt.WithLoggingLevel(badger.ERROR)
  276. goslConfig.BATCH_BLOCK = 1000 // try to import less at each time, it will take longer but hopefully work
  277. log.Info("trying to avoid too much memory consumption")
  278. // the other databases do not require any special configuration (for now)
  279. } // /switch
  280. // if importFilename isn't empty, this means we potentially have something to import.
  281. if goslConfig.importFilename != "" {
  282. log.Info("attempting to import", goslConfig.importFilename, "...")
  283. importDatabase(goslConfig.importFilename)
  284. log.Info("database finished import.")
  285. } else {
  286. // it's not an error if there is no name2key database available for import (gwyneth 20211027)
  287. log.Debug("no database configured for import — 🆗")
  288. }
  289. // Prepare testing data! (common to all database types)
  290. // Note: this only works for shell/server; for FastCGI it's definitely overkill (gwyneth 20211106),
  291. // so we do it only for server/shell mode.
  292. if goslConfig.isServer || goslConfig.isShell {
  293. const testAvatarName = "Nobody Here"
  294. log.Infof("%s started and logging is set up. Proceeding to test database (%s) at %q\n", programName, goslConfig.database, goslConfig.myDir)
  295. // generate a random UUID (gwyneth2021103) (gwyneth 20211031)
  296. var (
  297. testUUID = uuid.New().String() // Random UUID (gwyneth 20211031 — from )
  298. testValue = avatarUUID{testAvatarName, testUUID, "all grids"}
  299. )
  300. jsonTestValue, err := json.Marshal(testValue)
  301. checkErrPanic(err) // something went VERY wrong
  302. // KVDB Initialisation & Tests
  303. // Each case is different
  304. switch goslConfig.database {
  305. case "badger":
  306. // Opt has already been set earlier. (gwyneth 20211106)
  307. kv, err := badger.Open(Opt)
  308. checkErrPanic(err) // should probably panic, cannot prep new database
  309. txn := kv.NewTransaction(true)
  310. err = txn.Set([]byte(testAvatarName), jsonTestValue)
  311. checkErrPanic(err)
  312. err = txn.Set([]byte(testUUID), jsonTestValue)
  313. checkErrPanic(err)
  314. err = txn.Commit()
  315. checkErrPanic(err)
  316. log.Debugf("badger SET %+v (json: %v)\n", testValue, string(jsonTestValue))
  317. kv.Close()
  318. case "buntdb":
  319. // goslConfig.dbNamePath = filepath.Join(goslConfig.myDir, goslConfig.databaseName)
  320. db, err := buntdb.Open(goslConfig.dbNamePath)
  321. checkErrPanic(err)
  322. err = db.Update(func(tx *buntdb.Tx) error {
  323. _, _, err := tx.Set(testAvatarName, string(jsonTestValue), nil)
  324. return err
  325. })
  326. checkErr(err)
  327. log.Debugf("buntdb SET %+v (json: %v)\n", testValue, string(jsonTestValue))
  328. db.Close()
  329. case "leveldb":
  330. // goslConfig.dbNamePath = filepath.Join(goslConfig.myDir, goslConfig.databaseName)
  331. db, err := leveldb.OpenFile(goslConfig.dbNamePath, nil)
  332. checkErrPanic(err)
  333. err = db.Put([]byte(testAvatarName), jsonTestValue, nil)
  334. checkErrPanic(err)
  335. log.Debugf("leveldb SET %+v (json: %v)\n", testValue, string(jsonTestValue))
  336. db.Close()
  337. } // /switch
  338. // common to all databases:
  339. key, grid := searchKVname(testAvatarName)
  340. log.Debugf("GET %q returned %q [grid %q]\n", testAvatarName, key, grid)
  341. log.Info("KV database seems fine.")
  342. if goslConfig.importFilename != "" {
  343. log.Info("attempting to import", goslConfig.importFilename, "...")
  344. importDatabase(goslConfig.importFilename)
  345. log.Info("database finished import.")
  346. } else {
  347. // it's not an error if there is no name2key database available for import (gwyneth 20211027)
  348. log.Debug("no database configured for import")
  349. }
  350. }
  351. if goslConfig.isShell {
  352. log.Info("starting to run as interactive shell")
  353. fmt.Println("Ctrl-C to quit, or just type \"quit\".")
  354. var err error // to avoid assigning text in a different scope (this is a bit awkward, but that's the problem with bi-assignment)
  355. var avatarName, avatarKey, gridName string
  356. rl, err := readline.New("enter avatar name or UUID: ")
  357. if err != nil {
  358. log.Criticalf("major readline issue preventing normal functioning; error was %q\n", err)
  359. }
  360. defer rl.Close()
  361. for {
  362. checkInput, err := rl.Readline()
  363. if err != nil || checkInput == "quit" { // io.EOF
  364. break
  365. }
  366. // It's better to also trim spaces at the beginning, too.
  367. checkInput = strings.TrimSpace(checkInput)
  368. // fmt.Printf("Ok, got %s length is %d and UUID is %v\n", checkInput, len(checkInput), isValidUUID(checkInput))
  369. if (len(checkInput) == 36) && isValidUUID(checkInput) {
  370. avatarName, gridName = searchKVUUID(checkInput)
  371. avatarKey = checkInput
  372. } else {
  373. avatarKey, gridName = searchKVname(checkInput)
  374. avatarName = checkInput
  375. }
  376. if avatarName != "" && avatarKey != NullUUID {
  377. fmt.Println(avatarName, "which has UUID:", avatarKey, "comes from grid:", gridName)
  378. } else {
  379. fmt.Println("sorry, unknown input", checkInput)
  380. }
  381. }
  382. // never leaves until Ctrl-C or by typing `quit`. (gwyneth 20211106)
  383. log.Debug("interactive session finished.")
  384. os.Exit(0) // normal exit
  385. } else if goslConfig.isServer {
  386. // set up routing.
  387. // NOTE(gwyneth): one function only because FastCGI seems to have problems with multiple handlers.
  388. http.HandleFunc("/", handler)
  389. log.Debug("directory for database:", goslConfig.myDir)
  390. log.Info("starting to run as web server on port :" + goslConfig.myPort)
  391. err := http.ListenAndServe(":"+goslConfig.myPort, nil) // set listen port
  392. checkErrPanic(err) // if it can't listen to all the above, then it has to abort anyway
  393. } else {
  394. // default is to run as FastCGI!
  395. // works like a charm thanks to http://www.dav-muz.net/blog/2013/09/how-to-use-go-and-fastcgi/
  396. log.Debug("http.DefaultServeMux is", http.DefaultServeMux)
  397. log.Info("Starting to run as FastCGI")
  398. if err := fcgi.Serve(nil, http.HandlerFunc(handler)); err != nil {
  399. log.Errorf("seems that we got an error from FCGI: %q\n", err)
  400. checkErrPanic(err)
  401. }
  402. }
  403. // we should never have reached this point!
  404. log.Error("unknown usage — this application may run as a standalone server, as a FastCGI application, or as an interactive shell")
  405. if goslConfig.isServer || goslConfig.isShell {
  406. flag.PrintDefaults()
  407. }
  408. }
  409. // handler deals with incoming queries and/or associates avatar names with keys depending on parameters.
  410. // Basically we check if both an avatar name and a UUID key has been received: if yes, this means a new entry;
  411. // - if just the avatar name was received, it means looking up its key;
  412. // - if just the key was received, it means looking up the name (not necessary since llKey2Name does that, but it's just to illustrate);
  413. // - if nothing is received, then return an error.
  414. //
  415. // Note: to ensure quick lookups, we actually set *two* key/value pairs, one with avatar name/UUID,
  416. // the other with UUID/name — that way, we can efficiently search for *both* in the same database!
  417. // Theoretically, we could even have *two* KV databases, but that's too much trouble for the
  418. // sake of some extra efficiency. (gwyneth 20211030)
  419. func handler(w http.ResponseWriter, r *http.Request) {
  420. if err := r.ParseForm(); err != nil {
  421. logErrHTTP(w, http.StatusNotFound, "no avatar and/or UUID received")
  422. return
  423. }
  424. // test first if this comes from Second Life or OpenSimulator
  425. /*
  426. if r.Header.Get("X-Secondlife-Region") == "" {
  427. logErrHTTP(w, http.StatusForbidden, "Sorry, this application only works inside Second Life.")
  428. return
  429. }
  430. */
  431. name := r.Form.Get("name") // can be empty.
  432. key := r.Form.Get("key") // can be empty.
  433. compat := r.Form.Get("compat") // compatibility mode with W-Hat,
  434. var uuidToInsert avatarUUID
  435. messageToSL := "" // this is what we send back to SL - defined here due to scope issues.
  436. if name != "" {
  437. if key != "" {
  438. // be stricter!
  439. if len(key) != 36 || !isValidUUID(key) {
  440. logErrHTTP(w, http.StatusBadRequest, fmt.Sprintf("invalid key %q", key))
  441. return
  442. }
  443. // we received both: add a new entry.
  444. uuidToInsert.UUID = key
  445. uuidToInsert.Grid = r.Header.Get("X-Secondlife-Shard")
  446. jsonUUIDToInsert, err := json.Marshal(uuidToInsert)
  447. checkErr(err)
  448. switch goslConfig.database {
  449. case "badger":
  450. kv, err := badger.Open(Opt)
  451. checkErrPanic(err) // should probably panic.
  452. txn := kv.NewTransaction(true)
  453. defer txn.Discard()
  454. err = txn.Set([]byte(name), jsonUUIDToInsert)
  455. checkErrPanic(err)
  456. err = txn.Commit()
  457. checkErrPanic(err)
  458. kv.Close()
  459. case "buntdb":
  460. db, err := buntdb.Open(goslConfig.dbNamePath)
  461. checkErrPanic(err)
  462. defer db.Close()
  463. err = db.Update(func(tx *buntdb.Tx) error {
  464. _, _, err := tx.Set(name, string(jsonUUIDToInsert), nil)
  465. return err
  466. })
  467. checkErr(err)
  468. case "leveldb":
  469. db, err := leveldb.OpenFile(goslConfig.dbNamePath, nil)
  470. checkErrPanic(err)
  471. err = db.Put([]byte(name), jsonUUIDToInsert, nil)
  472. checkErrPanic(err)
  473. db.Close()
  474. }
  475. messageToSL += "Added new entry for '" + name + "' which is: " + uuidToInsert.UUID + " from grid: '" + uuidToInsert.Grid + "'"
  476. } else {
  477. // we received a name: look up its UUID key and grid.
  478. key, grid := searchKVname(name)
  479. if len(key) != 36 || !isValidUUID(key) { // this is to prevent stupid mistakes!
  480. key = NullUUID
  481. }
  482. if compat == "false" {
  483. messageToSL += "UUID for '" + name + "' is: " + key + " from grid: '" + grid + "'"
  484. } else { // empty also means true!
  485. messageToSL += key
  486. }
  487. }
  488. } else if key != "" {
  489. // in this scenario, we have the UUID key but no avatar name: do the equivalent of a llKey2Name
  490. name, grid := searchKVUUID(key)
  491. if compat == "false" {
  492. messageToSL += "avatar name for '" + key + "' is '" + name + "' on grid: '" + grid + "'"
  493. } else { // empty also means true!
  494. messageToSL += name
  495. }
  496. } else {
  497. // neither UUID key nor avatar received, this is an error
  498. logErrHTTP(w, http.StatusNotFound, "empty avatar name and UUID key received, cannot proceed")
  499. return
  500. }
  501. w.WriteHeader(http.StatusOK)
  502. w.Header().Set("Content-Type", "text/plain; charset=utf-8")
  503. fmt.Fprint(w, messageToSL)
  504. }