gosl.go 10 KB

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