gosl.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  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. "compress/bzip2"
  6. "encoding/csv"
  7. "encoding/json"
  8. "flag"
  9. "fmt"
  10. "github.com/dgraph-io/badger"
  11. "github.com/op/go-logging"
  12. "gopkg.in/natefinch/lumberjack.v2"
  13. "io"
  14. // "io/ioutil"
  15. "net/http"
  16. "net/http/fcgi"
  17. "os"
  18. "path/filepath"
  19. // "regexp"
  20. "runtime"
  21. "strings"
  22. "time"
  23. )
  24. const NullUUID = "00000000-0000-0000-0000-000000000000" // always useful when we deal with SL/OpenSimulator...
  25. // Logging setup.
  26. var log = logging.MustGetLogger("gosl") // configuration for the go-logging logger, must be available everywhere
  27. var logFormat logging.Formatter
  28. // Opt is used for KV database setup.
  29. var Opt badger.Options
  30. // AvatarUUID is the type that we store in the database; we keep a record from which grid it came from.
  31. type avatarUUID struct {
  32. UUID string // needs to be capitalised for JSON marshalling (it has to do with the way it works)
  33. Grid string
  34. }
  35. /*
  36. .__
  37. _____ _____ |__| ____
  38. / \\__ \ | |/ \
  39. | Y Y \/ __ \| | | \
  40. |__|_| (____ /__|___| /
  41. \/ \/ \/
  42. */
  43. // main() starts here.
  44. func main() {
  45. // Flag setup
  46. var myPort = flag.String("port", "3000", "Server port")
  47. var myDir = flag.String("dir", "slkvdb", "Directory where database files are stored")
  48. var isServer = flag.Bool("server", false, "Run as server on port " + *myPort)
  49. var isShell = flag.Bool("shell", false, "Run as an interactive shell")
  50. var importFilename = flag.String("import", "", "Import database from W-Hat (use the csv.bz2 version)")
  51. // default is FastCGI
  52. flag.Parse()
  53. // We cannot write to stdout if we're running as FastCGI, only to logs!
  54. if *isServer || *isShell {
  55. fmt.Println("gosl is starting...")
  56. }
  57. // Setup the lumberjack rotating logger. This is because we need it for the go-logging logger when writing to files. (20170813)
  58. rotatingLogger := &lumberjack.Logger{
  59. Filename: "gosl.log",
  60. MaxSize: 10, // megabytes
  61. MaxBackups: 3,
  62. MaxAge: 28, //days
  63. }
  64. // Set formatting for stderr and file (basically the same).
  65. 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
  66. // Setup the go-logging Logger. Do **not** log to stderr if running as FastCGI!
  67. backendFile := logging.NewLogBackend(rotatingLogger, "", 0)
  68. backendFileFormatter := logging.NewBackendFormatter(backendFile, logFormat)
  69. backendFileLeveled := logging.AddModuleLevel(backendFileFormatter)
  70. backendFileLeveled.SetLevel(logging.INFO, "gosl") // we just send debug data to logs if we run as shell
  71. if *isServer || *isShell {
  72. backendStderr := logging.NewLogBackend(os.Stderr, "", 0)
  73. backendStderrFormatter := logging.NewBackendFormatter(backendStderr, logFormat)
  74. backendStderrLeveled := logging.AddModuleLevel(backendStderrFormatter)
  75. if *isShell {
  76. backendStderrLeveled.SetLevel(logging.DEBUG, "gosl") // shell is meant to be for debugging mostly
  77. } else {
  78. backendStderrLeveled.SetLevel(logging.INFO, "gosl")
  79. }
  80. logging.SetBackend(backendStderrLeveled, backendFileLeveled)
  81. } else {
  82. logging.SetBackend(backendFileLeveled) // FastCGI only logs to file
  83. }
  84. log.Info("gosl started and logging is set up. Proceeding to test KV database.")
  85. const testAvatarName = "Nobody Here"
  86. var err error
  87. Opt = badger.DefaultOptions
  88. // 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
  89. if stat, err := os.Stat(*myDir); err == nil && stat.IsDir() {
  90. // path is a valid directory
  91. log.Debugf("Valid directory: %s\n", *myDir)
  92. } else {
  93. // try to create directory
  94. err = os.Mkdir(*myDir, 0700)
  95. checkErrPanic(err) // cannot make directory, panic and exit logging what went wrong
  96. log.Debugf("Created new directory: %s\n", *myDir)
  97. }
  98. Opt.Dir = *myDir
  99. Opt.ValueDir = Opt.Dir
  100. kv, err := badger.NewKV(&Opt)
  101. checkErrPanic(err) // should probably panic, cannot prep new database
  102. var testValue = avatarUUID{ NullUUID, "all grids" }
  103. jsonTestValue, err := json.Marshal(testValue)
  104. checkErrPanic(err) // something went VERY wrong
  105. kv.Set([]byte(testAvatarName), jsonTestValue, 0x00)
  106. log.Debugf("SET %+v (json: %v)\n", testValue, string(jsonTestValue))
  107. kv.Close()
  108. key, grid := searchKVname(testAvatarName)
  109. log.Debugf("GET '%s' returned '%s' [grid '%s']\n", testAvatarName, key, grid)
  110. log.Info("KV database seems fine.")
  111. if *importFilename != "" {
  112. log.Info("Attempting to import", *importFilename, "...")
  113. importDatabase(*importFilename)
  114. log.Info("Database finished import.")
  115. }
  116. if (*isShell) {
  117. log.Info("Starting to run as interactive shell")
  118. reader := bufio.NewReader(os.Stdin)
  119. fmt.Println("Ctrl-C to quit.")
  120. var err error // to avoid assigning text in a different scope (this is a bit awkward, but that's the problem with bi-assignment)
  121. var avatarName, avatarKey, gridName string
  122. for {
  123. // Prompt and read
  124. fmt.Print("Enter avatar name: ")
  125. avatarName, err = reader.ReadString('\n')
  126. checkErr(err)
  127. avatarName = strings.TrimRight(avatarName, "\r\n")
  128. avatarKey, gridName = searchKVname(avatarName)
  129. if avatarKey != NullUUID {
  130. fmt.Println("You typed:", avatarName, "which has UUID:", avatarKey, "and comes from grid:", gridName)
  131. } else {
  132. fmt.Println("Sorry, unknown avatar ", avatarName)
  133. }
  134. }
  135. // never leaves until Ctrl-C
  136. }
  137. // set up routing.
  138. // NOTE(gwyneth): one function only because FastCGI seems to have problems with multiple handlers.
  139. http.HandleFunc("/", handler)
  140. if (*isServer) {
  141. log.Info("Starting to run as web server on port " + *myPort)
  142. err := http.ListenAndServe(":" + *myPort, nil) // set listen port
  143. checkErrPanic(err) // if it can't listen to all the above, then it has to abort anyway
  144. } else {
  145. // default is to run as FastCGI!
  146. // works like a charm thanks to http://www.dav-muz.net/blog/2013/09/how-to-use-go-and-fastcgi/
  147. log.Info("Starting to run as FastCGI")
  148. log.Info("http.DefaultServeMux is", http.DefaultServeMux)
  149. if err := fcgi.Serve(nil, nil); err != nil {
  150. checkErrPanic(err)
  151. }
  152. }
  153. // we should never have reached this point!
  154. log.Error("Unknown usage! This application may run as a standalone server, as FastCGI application, or as an interactive shell")
  155. if *isServer || *isShell {
  156. flag.PrintDefaults()
  157. }
  158. }
  159. // handler deals with incoming queries and/or associates avatar names with keys depending on parameters.
  160. // Basically we check if both an avatar name and a UUID key has been received: if yes, this means a new entry;
  161. // if just the avatar name was received, it means looking up its key;
  162. // if just the key was received, it means looking up the name (not necessary since llKey2Name does that, but it's just to illustrate);
  163. // if nothing is received, then return an error
  164. func handler(w http.ResponseWriter, r *http.Request) {
  165. if err := r.ParseForm(); err != nil {
  166. logErrHTTP(w, http.StatusNotFound, "No avatar and/or UUID received")
  167. return
  168. }
  169. // test first if this comes from Second Life or OpenSimulator
  170. if r.Header.Get("X-Secondlife-Region") == "" {
  171. logErrHTTP(w, http.StatusForbidden, "Sorry, this application only works inside Second Life.")
  172. return
  173. }
  174. name := r.Form.Get("name") // can be empty
  175. key := r.Form.Get("key") // can be empty
  176. compat := r.Form.Get("compat") // compatibility mode with W-Hat
  177. var valueToInsert avatarUUID
  178. messageToSL := "" // this is what we send back to SL - defined here due to scope issues.
  179. if name != "" {
  180. if key != "" {
  181. // we received both: add a new entry
  182. kv, err := badger.NewKV(&Opt)
  183. checkErrPanic(err) // should probably panic
  184. valueToInsert.UUID = key
  185. valueToInsert.Grid = r.Header.Get("X-Secondlife-Shard")
  186. jsonValueToInsert, err := json.Marshal(valueToInsert)
  187. checkErr(err)
  188. kv.Set([]byte(name), jsonValueToInsert, 0x00)
  189. kv.Close()
  190. messageToSL += "Added new entry for '" + name + "' which is: " + valueToInsert.UUID + " from grid: '" + valueToInsert.Grid + "'"
  191. } else {
  192. // we received a name: look up its UUID key and grid.
  193. key, grid := searchKVname(name)
  194. if compat == "false" {
  195. messageToSL += "UUID for '" + name + "' is: " + key + " from grid: '" + grid + "'"
  196. } else { // empty also means true!
  197. messageToSL += key
  198. }
  199. }
  200. } else if key != "" {
  201. // in this scenario, we have the UUID key but no avatar name: do the equivalent of a llKey2Name (slow)
  202. name, grid := searchKVUUID(key)
  203. if compat == "false" {
  204. messageToSL += "Avatar name for " + key + "' is '" + name + "' on grid: '" + grid + "'"
  205. } else { // empty also means true!
  206. messageToSL += name
  207. }
  208. } else {
  209. // neither UUID key nor avatar received, this is an error
  210. logErrHTTP(w, http.StatusNotFound, "Empty avatar name and UUID key received, cannot proceed")
  211. return
  212. }
  213. w.WriteHeader(http.StatusOK)
  214. w.Header().Set("Content-type", "text/plain; charset=utf-8")
  215. fmt.Fprintf(w, messageToSL)
  216. }
  217. // searchKVname searches the KV database for an avatar name.
  218. func searchKVname(avatarName string) (UUID string, grid string) {
  219. kv, err := badger.NewKV(&Opt)
  220. defer kv.Close()
  221. var item badger.KVItem
  222. if err := kv.Get([]byte(avatarName), &item); err != nil {
  223. log.Errorf("Error while getting name: %s - %v\n", avatarName, err)
  224. return NullUUID, ""
  225. }
  226. var val avatarUUID
  227. if err = json.Unmarshal(item.Value(), &val); err != nil {
  228. log.Errorf("Error while unparsing UUID for name: %s - %v\n", avatarName, err)
  229. return NullUUID, ""
  230. }
  231. return val.UUID, val.Grid
  232. }
  233. // searchKVUUID searches the KV database for an avatar key.
  234. func searchKVUUID(avatarKey string) (name string, grid string) {
  235. kv, err := badger.NewKV(&Opt)
  236. checkErr(err) // should probably panic
  237. itOpt := badger.DefaultIteratorOptions
  238. itr := kv.NewIterator(itOpt)
  239. var val = avatarUUID{ NullUUID, "" }
  240. var found string
  241. checks := 0
  242. time_start := time.Now()
  243. for itr.Rewind(); itr.Valid(); itr.Next() {
  244. item := itr.Item()
  245. if err = json.Unmarshal(item.Value(), &val); err == nil {
  246. checks++ //Just to see how many
  247. if avatarKey == val.UUID { // are these pointers?
  248. found = string(item.Key())
  249. break
  250. }
  251. }
  252. }
  253. time_end := time.Now()
  254. diffTime := time_end.Sub(time_start)
  255. log.Debugf("Made %d checks for '%s' in %v\n", checks, avatarKey, diffTime)
  256. itr.Close()
  257. kv.Close()
  258. return found, val.Grid
  259. }
  260. // importDatabase is essentially reading a bzip2'ed CSV file with UUID,AvatarName downloaded from http://w-hat.com/#name2key .
  261. // One could theoretically set a cron job to get this file, save it on disk periodically, and keep the database up-to-date
  262. // see https://stackoverflow.com/questions/24673335/how-do-i-read-a-gzipped-csv-file for the actual usage of these complicated things!
  263. func importDatabase(filename string) {
  264. f, err := os.Open(filename)
  265. if err != nil {
  266. log.Fatal(err)
  267. }
  268. defer f.Close()
  269. gr := bzip2.NewReader(f) // open bzip2 reader
  270. cr := csv.NewReader(gr) // open csv reader and feed the bzip2 reader into it
  271. limit := 0
  272. kv, err := badger.NewKV(&Opt)
  273. checkErrPanic(err) // should probably panic
  274. defer kv.Close()
  275. time_start := time.Now()
  276. for {
  277. record, err := cr.Read()
  278. if err == io.EOF {
  279. break
  280. }
  281. if err != nil {
  282. log.Fatal(err)
  283. }
  284. //fmt.Println("Key:", record[0], "Name:", record[1])
  285. jsonNewEntry, err := json.Marshal(avatarUUID{ record[0], "Production" }) // W-Hat keys come all from the main LL grid, known as 'Production'
  286. if err != nil {
  287. log.Warning(err)
  288. } else {
  289. kv.Set([]byte(record[1]), []byte(jsonNewEntry), 0x00)
  290. }
  291. limit++
  292. if limit % 1000000 == 0 {
  293. time_end := time.Now()
  294. diffTime := time_end.Sub(time_start)
  295. log.Info("Read", limit, "records (or thereabouts) in", diffTime)
  296. }
  297. }
  298. time_end := time.Now()
  299. diffTime := time_end.Sub(time_start)
  300. log.Info("Total read", limit, "records (or thereabouts) in", diffTime)
  301. }
  302. // NOTE(gwyneth):Auxiliary functions which I'm always using...
  303. // checkErrPanic logs a fatal error and panics.
  304. func checkErrPanic(err error) {
  305. if err != nil {
  306. pc, file, line, ok := runtime.Caller(1)
  307. log.Panic(filepath.Base(file), ":", line, ":", pc, ok, " - panic:", err)
  308. }
  309. }
  310. // checkErr checks if there is an error, and if yes, it logs it out and continues.
  311. // this is for 'normal' situations when we want to get a log if something goes wrong but do not need to panic
  312. func checkErr(err error) {
  313. if err != nil {
  314. pc, file, line, ok := runtime.Caller(1)
  315. log.Error(filepath.Base(file), ":", line, ":", pc, ok, " - error:", err)
  316. }
  317. }
  318. // Auxiliary functions for HTTP handling
  319. // checkErrHTTP returns an error via HTTP and also logs the error.
  320. func checkErrHTTP(w http.ResponseWriter, httpStatus int, errorMessage string, err error) {
  321. if err != nil {
  322. http.Error(w, fmt.Sprintf(errorMessage, err), httpStatus)
  323. pc, file, line, ok := runtime.Caller(1)
  324. log.Error("(", http.StatusText(httpStatus), ") ", filepath.Base(file), ":", line, ":", pc, ok, " - error:", errorMessage, err)
  325. }
  326. }
  327. // checkErrPanicHTTP returns an error via HTTP and logs the error with a panic.
  328. func checkErrPanicHTTP(w http.ResponseWriter, httpStatus int, errorMessage string, err error) {
  329. if err != nil {
  330. http.Error(w, fmt.Sprintf(errorMessage, err), httpStatus)
  331. pc, file, line, ok := runtime.Caller(1)
  332. log.Panic("(", http.StatusText(httpStatus), ") ", filepath.Base(file), ":", line, ":", pc, ok, " - panic:", errorMessage, err)
  333. }
  334. }
  335. // logErrHTTP assumes that the error message was already composed and writes it to HTTP and logs it.
  336. // this is mostly to avoid code duplication and make sure that all entries are written similarly
  337. func logErrHTTP(w http.ResponseWriter, httpStatus int, errorMessage string) {
  338. http.Error(w, errorMessage, httpStatus)
  339. log.Error("(" + http.StatusText(httpStatus) + ") " + errorMessage)
  340. }
  341. // funcName is @Sonia's solution to get the name of the function that Go is currently running.
  342. // This will be extensively used to deal with figuring out where in the code the errors are!
  343. // Source: https://stackoverflow.com/a/10743805/1035977 (20170708)
  344. func funcName() string {
  345. pc, _, _, _ := runtime.Caller(1)
  346. return runtime.FuncForPC(pc).Name()
  347. }