1
0

gosl.go 12 KB

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