gosl.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. // gosl is a basic example of how to develop external web services for Second Life/OpenSimulator using the Go programming language.
  2. package main
  3. import (
  4. "bufio"
  5. "encoding/json"
  6. "fmt"
  7. "net/http"
  8. "net/http/fcgi"
  9. "os"
  10. "path/filepath"
  11. // "regexp"
  12. "strings"
  13. // "time"
  14. "github.com/dgraph-io/badger/v3"
  15. // "github.com/dgraph-io/badger/options"
  16. // "github.com/fsnotify/fsnotify"
  17. "github.com/google/uuid"
  18. "github.com/op/go-logging"
  19. flag "github.com/spf13/pflag"
  20. "github.com/spf13/viper"
  21. "github.com/syndtr/goleveldb/leveldb"
  22. "github.com/tidwall/buntdb"
  23. // "gopkg.in/go-playground/validator.v9" // to validate UUIDs... and a lot of thinks
  24. "gopkg.in/natefinch/lumberjack.v2"
  25. )
  26. const NullUUID = "00000000-0000-0000-0000-000000000000" // always useful when we deal with SL/OpenSimulator...
  27. const databaseName = "gosl-database.db" // for BuntDB
  28. // Logging setup.
  29. var log = logging.MustGetLogger("gosl") // configuration for the go-logging logger, must be available everywhere
  30. var logFormat logging.Formatter
  31. // Opt is used for Badger database setup.
  32. var Opt badger.Options
  33. // AvatarUUID is the type that we store in the database; we keep a record from which grid it came from.
  34. // Field names need to be capitalised for JSON marshalling (it has to do with the way it works)
  35. // Note that we will store both UUID -> AvatarName *and* AvatarName -> UUID on the same database,
  36. // thus the apparent redundancy in fields! (gwyneth 20211030)
  37. // The 'validate' decorator is for usage with the go-playground validator, currently unused (gwyneth 20211031)
  38. type avatarUUID struct {
  39. AvatarName string `json:"name" form:"name" binding:"required" validate:"omitempty,alphanum"`
  40. UUID string `json:"key" form:"key" binding:"required" validate:"omitempty,uuid4_rfc4122"`
  41. Grid string `json:"grid" form:"grid" validate:"omitempty,alphanum"`
  42. }
  43. /*
  44. .__
  45. _____ _____ |__| ____
  46. / \\__ \ | |/ \
  47. | Y Y \/ __ \| | | \
  48. |__|_| (____ /__|___| /
  49. \/ \/ \/
  50. */
  51. // Configuration options
  52. type goslConfigOptions struct {
  53. BATCH_BLOCK int // how many entries to write to the database as a block; the bigger, the faster, but the more memory it consumes
  54. noMemory, isServer, isShell bool
  55. myDir, myPort, importFilename, database string
  56. dbNamePath string // for BuntDB
  57. logLevel, logFilename string // for logs
  58. maxSize, maxBackups, maxAge int // logs configuration option
  59. }
  60. var goslConfig goslConfigOptions
  61. var kv *badger.DB
  62. // loadConfiguration reads our configuration from a config.toml file
  63. func loadConfiguration() {
  64. fmt.Print("Reading gosl-basic configuration:") // note that we might not have go-logging active as yet, so we use fmt
  65. // Open our config file and extract relevant data from there
  66. err := viper.ReadInConfig() // Find and read the config file
  67. if err != nil {
  68. fmt.Println("error reading config file:", err)
  69. return // we might still get away with this!
  70. }
  71. viper.SetDefault("config.BATCH_BLOCK", 100000) // NOTE(gwyneth): the authors of say that 100000 is way too much for Badger // NOTE(gwyneth): let's see what happens with BuntDB
  72. goslConfig.BATCH_BLOCK = viper.GetInt("config.BATCH_BLOCK")
  73. viper.SetDefault("config.myPort", 3000)
  74. goslConfig.myPort = viper.GetString("config.myPort")
  75. viper.SetDefault("config.myDir", "slkvdb")
  76. goslConfig.myDir = viper.GetString("config.myDir")
  77. viper.SetDefault("config.isServer", false)
  78. goslConfig.isServer = viper.GetBool("config.isServer")
  79. viper.SetDefault("config.isShell", false)
  80. goslConfig.isShell = viper.GetBool("config.isShell")
  81. viper.SetDefault("config.database", "badger") // currently, badger, boltdb, leveldb
  82. goslConfig.database = viper.GetString("config.database")
  83. viper.SetDefault("options.importFilename", "") // must be empty by default
  84. goslConfig.importFilename = viper.GetString("options.importFilename")
  85. viper.SetDefault("options.noMemory", false)
  86. goslConfig.noMemory = viper.GetBool("options.noMemory")
  87. // Logging options
  88. viper.SetDefault("log.Filename", "gosl.log")
  89. goslConfig.logFilename = viper.GetString("log.Filename")
  90. viper.SetDefault("log.logLevel", "ERROR")
  91. goslConfig.logLevel = viper.GetString("log.logLevel")
  92. viper.SetDefault("log.MaxSize", 10)
  93. goslConfig.maxSize = viper.GetInt("log.MaxSize")
  94. viper.SetDefault("log.MaxBackups", 3)
  95. goslConfig.maxBackups = viper.GetInt("log.MaxBackups")
  96. viper.SetDefault("log.MaxAge", 28)
  97. goslConfig.maxAge = viper.GetInt("log.MaxAge")
  98. }
  99. // main() starts here.
  100. func main() {
  101. // Flag setup; can be overridden by config file.
  102. // TODO(gwyneth): I need to fix this to be the oher way round).
  103. goslConfig.myPort = *flag.String("port", "3000", "Server port")
  104. goslConfig.myDir = *flag.String("dir", "slkvdb", "Directory where database files are stored")
  105. goslConfig.isServer = *flag.Bool("server", false, "Run as server on port " + goslConfig.myPort)
  106. goslConfig.isShell = *flag.Bool("shell", false, "Run as an interactive shell")
  107. goslConfig.importFilename = *flag.String("import", "", "Import database from W-Hat (use the csv.bz2 versions)")
  108. goslConfig.database = *flag.String("database", "badger", "Database type (badger, buntdb, leveldb)")
  109. goslConfig.noMemory = *flag.Bool("nomemory", true, "Attempt to use only disk to save memory on Badger (important for shared webservers)")
  110. // Config viper, which reads in the configuration file every time it's needed.
  111. // Note that we need some hard-coded variables for the path and config file name.
  112. viper.SetConfigName("config")
  113. viper.SetConfigType("toml") // just to make sure; it's the same format as OpenSimulator (or MySQL) config files
  114. viper.AddConfigPath(".") // optionally look for config in the working directory
  115. viper.AddConfigPath("$HOME/go/src/git.gwynethllewelyn.net/GwynethLlewelyn/gosl-basics/") // that's how you'll have it
  116. loadConfiguration()
  117. // default is FastCGI
  118. flag.Parse()
  119. viper.BindPFlags(flag.CommandLine)
  120. // this will allow our configuration file to be 'read on demand'
  121. // TODO(gwyneth): There is something broken with this, no reason why... (gwyneth 20211026)
  122. // viper.WatchConfig()
  123. // viper.OnConfigChange(func(e fsnotify.Event) {
  124. // if goslConfig.isServer || goslConfig.isShell {
  125. // fmt.Println("Config file changed:", e.Name) // BUG(gwyneth): FastCGI cannot write to output
  126. // }
  127. // loadConfiguration()
  128. // })
  129. // NOTE(gwyneth): We cannot write to stdout if we're running as FastCGI, only to logs!
  130. if goslConfig.isServer || goslConfig.isShell {
  131. fmt.Println("gosl is starting...")
  132. }
  133. // Setup the lumberjack rotating logger. This is because we need it for the go-logging logger when writing to files. (20170813)
  134. rotatingLogger := &lumberjack.Logger{
  135. Filename: goslConfig.logFilename,
  136. MaxSize: goslConfig.maxSize, // megabytes
  137. MaxBackups: goslConfig.maxBackups,
  138. MaxAge: goslConfig.maxAge, //days
  139. }
  140. // Set formatting for stderr and file (basically the same).
  141. 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
  142. // Setup the go-logging Logger. Do **not** log to stderr if running as FastCGI!
  143. backendFile := logging.NewLogBackend(rotatingLogger, "", 0)
  144. backendFileFormatter := logging.NewBackendFormatter(backendFile, logFormat)
  145. backendFileLeveled := logging.AddModuleLevel(backendFileFormatter)
  146. backendFileLeveled.SetLevel(logging.INFO, "gosl") // we just send debug data to logs if we run as shell
  147. if goslConfig.isServer || goslConfig.isShell {
  148. backendStderr := logging.NewLogBackend(os.Stderr, "", 0)
  149. backendStderrFormatter := logging.NewBackendFormatter(backendStderr, logFormat)
  150. backendStderrLeveled := logging.AddModuleLevel(backendStderrFormatter)
  151. theLogLevel, err := logging.LogLevel(goslConfig.logLevel)
  152. if err != nil {
  153. fmt.Printf("could not set log level to %q — invalid?\nlogging.LogLevel() returned error %q\n", goslConfig.logLevel, err)
  154. } else {
  155. fmt.Printf("requested log level: %q \n", theLogLevel.String())
  156. }
  157. backendStderrLeveled.SetLevel(theLogLevel, "gosl")
  158. log.Debugf("level set to: %v\n", backendStderrLeveled.GetLevel("gosl"))
  159. }
  160. /*
  161. // deprecated, now we set it explicitly if desired
  162. if goslConfig.isShell {
  163. backendStderrLeveled.SetLevel(logging.DEBUG, "gosl") // shell is meant to be for debugging mostly
  164. } else {
  165. backendStderrLeveled.SetLevel(logging.INFO, "gosl")
  166. }
  167. logging.SetBackend(backendStderrLeveled, backendFileLeveled)
  168. } else {
  169. logging.SetBackend(backendFileLeveled) // FastCGI only logs to file
  170. }
  171. */
  172. // 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)
  173. if stat, err := os.Stat(goslConfig.myDir); err == nil && stat.IsDir() {
  174. // path is a valid directory
  175. log.Infof("valid directory: %q\n", goslConfig.myDir)
  176. } else {
  177. // try to create directory
  178. if err = os.Mkdir(goslConfig.myDir, 0700); err != nil {
  179. if err != os.ErrExist {
  180. checkErr(err)
  181. } else {
  182. log.Debugf("directory %q exists, no need to create it\n", goslConfig.myDir)
  183. }
  184. }
  185. log.Debugf("created new directory: %q\n", goslConfig.myDir)
  186. }
  187. // Prepare testing data! (common to all types)
  188. const testAvatarName = "Nobody Here"
  189. var err error
  190. log.Infof("gosl started and logging is set up. Proceeding to test database (%s) at %q\n",goslConfig.database, goslConfig.myDir)
  191. // generate a random UUID (gwyneth2021103) (gwyneth 20211031)
  192. var (
  193. testUUID = uuid.New().String() // Random UUID (gwyneth 20211031 — from )
  194. testValue = avatarUUID{ testAvatarName, testUUID, "all grids" }
  195. )
  196. jsonTestValue, err := json.Marshal(testValue)
  197. checkErrPanic(err) // something went VERY wrong
  198. // KVDB Initialisation & Tests
  199. // Each case is different
  200. switch goslConfig.database {
  201. case "badger":
  202. // Badger v3 - fully rewritten configuration (much simpler!!) (gwyneth 20211026)
  203. if goslConfig.noMemory {
  204. // use disk; note that unlike the others, Badger generates its own filenames,
  205. // we can only pass a _directory_... (gwyneth 20211027)
  206. goslConfig.dbNamePath = filepath.Join(goslConfig.myDir, databaseName)
  207. // try to create directory
  208. if err = os.Mkdir(goslConfig.dbNamePath, 0700); err != nil {
  209. if err != os.ErrExist {
  210. checkErr(err)
  211. } else {
  212. log.Debugf("directory %q exists, no need to create it\n", goslConfig.dbNamePath)
  213. }
  214. } else {
  215. log.Debugf("created new directory: %q\n", goslConfig.dbNamePath)
  216. }
  217. Opt = badger.DefaultOptions(goslConfig.dbNamePath)
  218. log.Debugf("entering disk mode, Opt is %+v\n", Opt)
  219. } else {
  220. // Use only memory
  221. Opt = badger.LSMOnlyOptions("").WithInMemory(true)
  222. Opt.WithLevelSizeMultiplier(1)
  223. Opt.WithNumMemtables(1)
  224. Opt.WithValueDir(Opt.Dir) // probably not needed
  225. log.Debugf("entering memory-only mode, Opt is %+v\n", Opt)
  226. }
  227. // common config
  228. Opt.WithLogger(log) // set the internal logger to our own rotating logger
  229. Opt.WithLoggingLevel(badger.ERROR)
  230. goslConfig.BATCH_BLOCK = 1000 // try to import less at each time, it will take longer but hopefully work
  231. log.Info("trying to avoid too much memory consumption")
  232. // Badger setup is ready, now the rest is similar to the others
  233. kv, err := badger.Open(Opt)
  234. checkErrPanic(err) // should probably panic, cannot prep new database
  235. txn := kv.NewTransaction(true)
  236. err = txn.Set([]byte(testAvatarName), jsonTestValue)
  237. checkErrPanic(err)
  238. err = txn.Set([]byte(testUUID), jsonTestValue)
  239. checkErrPanic(err)
  240. err = txn.Commit()
  241. checkErrPanic(err)
  242. log.Debugf("badger SET %+v (json: %v)\n", testValue, string(jsonTestValue))
  243. kv.Close()
  244. case "buntdb":
  245. goslConfig.dbNamePath = filepath.Join(goslConfig.myDir, databaseName)
  246. db, err := buntdb.Open(goslConfig.dbNamePath)
  247. checkErrPanic(err)
  248. err = db.Update(func(tx *buntdb.Tx) error {
  249. _, _, err := tx.Set(testAvatarName, string(jsonTestValue), nil)
  250. return err
  251. })
  252. checkErr(err)
  253. log.Debugf("buntdb SET %+v (json: %v)\n", testValue, string(jsonTestValue))
  254. db.Close()
  255. case "leveldb":
  256. goslConfig.dbNamePath = filepath.Join(goslConfig.myDir, databaseName)
  257. db, err := leveldb.OpenFile(goslConfig.dbNamePath, nil)
  258. checkErrPanic(err)
  259. err = db.Put([]byte(testAvatarName), jsonTestValue, nil)
  260. checkErrPanic(err)
  261. log.Debugf("leveldb SET %+v (json: %v)\n", testValue, string(jsonTestValue))
  262. db.Close()
  263. } // /switch
  264. // common to all databases:
  265. key, grid := searchKVname(testAvatarName)
  266. log.Debugf("GET %q returned %q [grid %q]\n", testAvatarName, key, grid)
  267. log.Info("KV database seems fine.")
  268. if goslConfig.importFilename != "" {
  269. log.Info("attempting to import", goslConfig.importFilename, "...")
  270. importDatabase(goslConfig.importFilename)
  271. log.Info("database finished import.")
  272. } else {
  273. // it's not an error if there is no nam2key database available for import (gwyneth 20211027)
  274. log.Warning("no database configured for import")
  275. }
  276. if goslConfig.isShell {
  277. log.Info("starting to run as interactive shell")
  278. reader := bufio.NewReader(os.Stdin)
  279. fmt.Println("Ctrl-C to quit.")
  280. var err error // to avoid assigning text in a different scope (this is a bit awkward, but that's the problem with bi-assignment)
  281. var checkInput, avatarName, avatarKey, gridName string
  282. for {
  283. // Prompt and read
  284. fmt.Print("enter avatar name or UUID: ")
  285. checkInput, err = reader.ReadString('\n')
  286. checkErr(err)
  287. checkInput = strings.TrimRight(checkInput, "\r\n")
  288. // fmt.Printf("Ok, got %s length is %d and UUID is %v\n", checkInput, len(checkInput), isValidUUID(checkInput))
  289. if (len(checkInput) == 36) && isValidUUID(checkInput) {
  290. avatarName, gridName = searchKVUUID(checkInput)
  291. avatarKey = checkInput
  292. } else {
  293. avatarKey, gridName = searchKVname(checkInput)
  294. avatarName = checkInput
  295. }
  296. if avatarName != NullUUID && avatarKey != NullUUID {
  297. fmt.Println(avatarName, "which has UUID:", avatarKey, "comes from grid:", gridName)
  298. } else {
  299. fmt.Println("sorry, unknown input", checkInput)
  300. }
  301. }
  302. // never leaves until Ctrl-C
  303. }
  304. // set up routing.
  305. // NOTE(gwyneth): one function only because FastCGI seems to have problems with multiple handlers.
  306. http.HandleFunc("/", handler)
  307. log.Info("directory for database:", goslConfig.myDir)
  308. if (goslConfig.isServer) {
  309. log.Info("starting to run as web server on port :" + goslConfig.myPort)
  310. err := http.ListenAndServe(":" + goslConfig.myPort, nil) // set listen port
  311. checkErrPanic(err) // if it can't listen to all the above, then it has to abort anyway
  312. } else {
  313. // default is to run as FastCGI!
  314. // works like a charm thanks to http://www.dav-muz.net/blog/2013/09/how-to-use-go-and-fastcgi/
  315. log.Debug("http.DefaultServeMux is", http.DefaultServeMux)
  316. if err := fcgi.Serve(nil, nil); err != nil {
  317. checkErrPanic(err)
  318. }
  319. }
  320. // we should never have reached this point!
  321. log.Error("unknown usage — this application may run as a standalone server, as a FastCGI application, or as an interactive shell")
  322. if goslConfig.isServer || goslConfig.isShell {
  323. flag.PrintDefaults()
  324. }
  325. }
  326. // handler deals with incoming queries and/or associates avatar names with keys depending on parameters.
  327. // Basically we check if both an avatar name and a UUID key has been received: if yes, this means a new entry;
  328. // - if just the avatar name was received, it means looking up its key;
  329. // - if just the key was received, it means looking up the name (not necessary since llKey2Name does that, but it's just to illustrate);
  330. // - if nothing is received, then return an error
  331. // Note: to ensure quick lookups, we actually set *two* key/value pairs, one with avatar name/UUID,
  332. // the other with UUID/name — that way, we can efficiently search for *both* in the same database!
  333. // Theoretically, we could even have *two* KV databases, but that's too much trouble for the
  334. // sake of some extra efficiency (gwyneth 20211030)
  335. func handler(w http.ResponseWriter, r *http.Request) {
  336. if err := r.ParseForm(); err != nil {
  337. logErrHTTP(w, http.StatusNotFound, "no avatar and/or UUID received")
  338. return
  339. }
  340. // test first if this comes from Second Life or OpenSimulator
  341. /*
  342. if r.Header.Get("X-Secondlife-Region") == "" {
  343. logErrHTTP(w, http.StatusForbidden, "Sorry, this application only works inside Second Life.")
  344. return
  345. }
  346. */
  347. name := r.Form.Get("name") // can be empty
  348. key := r.Form.Get("key") // can be empty
  349. compat := r.Form.Get("compat") // compatibility mode with W-Hat
  350. var uuidToInsert avatarUUID
  351. messageToSL := "" // this is what we send back to SL - defined here due to scope issues.
  352. if name != "" {
  353. if key != "" {
  354. // we received both: add a new entry
  355. uuidToInsert.UUID = key
  356. uuidToInsert.Grid = r.Header.Get("X-Secondlife-Shard")
  357. jsonUUIDToInsert, err := json.Marshal(uuidToInsert)
  358. checkErr(err)
  359. switch goslConfig.database {
  360. case "badger":
  361. kv, err := badger.Open(Opt)
  362. checkErrPanic(err) // should probably panic
  363. txn := kv.NewTransaction(true)
  364. defer txn.Discard()
  365. err = txn.Set([]byte(name), jsonUUIDToInsert)
  366. checkErrPanic(err)
  367. err = txn.Commit()
  368. checkErrPanic(err)
  369. kv.Close()
  370. case "buntdb":
  371. db, err := buntdb.Open(goslConfig.dbNamePath)
  372. checkErrPanic(err)
  373. defer db.Close()
  374. err = db.Update(func(tx *buntdb.Tx) error {
  375. _, _, err := tx.Set(name, string(jsonUUIDToInsert), nil)
  376. return err
  377. })
  378. checkErr(err)
  379. case "leveldb":
  380. db, err := leveldb.OpenFile(goslConfig.dbNamePath, nil)
  381. checkErrPanic(err)
  382. err = db.Put([]byte(name), jsonUUIDToInsert, nil)
  383. checkErrPanic(err)
  384. db.Close()
  385. }
  386. messageToSL += "Added new entry for '" + name + "' which is: " + uuidToInsert.UUID + " from grid: '" + uuidToInsert.Grid + "'"
  387. } else {
  388. // we received a name: look up its UUID key and grid.
  389. key, grid := searchKVname(name)
  390. if compat == "false" {
  391. messageToSL += "UUID for '" + name + "' is: " + key + " from grid: '" + grid + "'"
  392. } else { // empty also means true!
  393. messageToSL += key
  394. }
  395. }
  396. } else if key != "" {
  397. // in this scenario, we have the UUID key but no avatar name: do the equivalent of a llKey2Name
  398. name, grid := searchKVUUID(key)
  399. if compat == "false" {
  400. messageToSL += "avatar name for " + key + "' is '" + name + "' on grid: '" + grid + "'"
  401. } else { // empty also means true!
  402. messageToSL += name
  403. }
  404. } else {
  405. // neither UUID key nor avatar received, this is an error
  406. logErrHTTP(w, http.StatusNotFound, "empty avatar name and UUID key received, cannot proceed")
  407. return
  408. }
  409. w.WriteHeader(http.StatusOK)
  410. w.Header().Set("Content-Type", "text/plain; charset=utf-8")
  411. fmt.Fprint(w, messageToSL)
  412. }