gosl.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  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/op/go-logging"
  8. "gopkg.in/natefinch/lumberjack.v2"
  9. "net/http"
  10. "net/http/fcgi"
  11. "os"
  12. "path/filepath"
  13. // "regexp"
  14. "runtime"
  15. // "strings"
  16. )
  17. // Logging setup
  18. var log = logging.MustGetLogger("gosl") // configuration for the go-logging logger, must be available everywhere
  19. var logFormat logging.Formatter
  20. /*
  21. .__
  22. _____ _____ |__| ____
  23. / \\__ \ | |/ \
  24. | Y Y \/ __ \| | | \
  25. |__|_| (____ /__|___| /
  26. \/ \/ \/
  27. */
  28. // main() starts here.
  29. func main() {
  30. // Flag setup
  31. var myPort = flag.String("port", "3000", "Server port")
  32. var isServer = flag.Bool("server", false, "Run as server on port " + *myPort)
  33. var isShell = flag.Bool("shell", false, "Run as an interactive shell")
  34. // default is FastCGI
  35. flag.Parse()
  36. // We cannot write to stdout if we're running as FastCGI, only to logs!
  37. if *isServer || *isShell {
  38. fmt.Println("gosl is starting...")
  39. }
  40. // Setup the lumberjack rotating logger. This is because we need it for the go-logging logger when writing to files. (20170813)
  41. rotatingLogger := &lumberjack.Logger{
  42. Filename: "gosl.log",
  43. MaxSize: 10, // megabytes
  44. MaxBackups: 3,
  45. MaxAge: 28, //days
  46. }
  47. // Set formatting for stderr and file (basically the same).
  48. 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
  49. // Setup the go-logging Logger. Do **not** log to stderr if running as FastCGI!
  50. backendFile := logging.NewLogBackend(rotatingLogger, "", 0)
  51. backendFileFormatter := logging.NewBackendFormatter(backendFile, logFormat)
  52. backendFileLeveled := logging.AddModuleLevel(backendFileFormatter)
  53. backendFileLeveled.SetLevel(logging.INFO, "gosl") // we just send debug data to logs if we run as shell
  54. if *isServer || *isShell {
  55. backendStderr := logging.NewLogBackend(os.Stderr, "", 0)
  56. backendStderrFormatter := logging.NewBackendFormatter(backendStderr, logFormat)
  57. backendStderrLeveled := logging.AddModuleLevel(backendStderrFormatter)
  58. if *isShell {
  59. backendStderrLeveled.SetLevel(logging.DEBUG, "gosl") // shell is meant to be for debugging mostly
  60. } else {
  61. backendStderrLeveled.SetLevel(logging.INFO, "gosl")
  62. }
  63. logging.SetBackend(backendStderrLeveled, backendFileLeveled)
  64. } else {
  65. logging.SetBackend(backendFileLeveled) // FastCGI only logs to file
  66. }
  67. log.Info("gosl started and logging is set up. Proceeding to configuration.")
  68. if (*isShell) {
  69. log.Info("Starting to run as interactive shell")
  70. reader := bufio.NewReader(os.Stdin)
  71. fmt.Println("Ctrl-C to quit.")
  72. var err error // to avoid assigning text in a different scope (this is a bit awkward, but that's the problem with bi-assignment)
  73. var text string
  74. for {
  75. // Prompt and read
  76. fmt.Print("Enter avatar name: ")
  77. text, err = reader.ReadString('\n')
  78. checkErr(err)
  79. fmt.Println("You typed:", text, "which has", len(text), "character(s).")
  80. }
  81. // never leaves until Ctrl-C
  82. }
  83. // set up routing
  84. http.HandleFunc("/", handlerQuery)
  85. http.HandleFunc("/touch", handlerTouch)
  86. if (*isServer) {
  87. log.Info("Starting to run as web server on port " + *myPort)
  88. http.HandleFunc("/", handlerQuery)
  89. err := http.ListenAndServe(":" + *myPort, nil) // set listen port
  90. checkErrPanic(err) // if it can't listen to all the above, then it has to abort anyway
  91. } else {
  92. // default is to run as FastCGI!
  93. // works like a charm thanks to http://www.dav-muz.net/blog/2013/09/how-to-use-go-and-fastcgi/
  94. log.Info("Starting to run as FastCGI")
  95. if err := fcgi.Serve(nil, nil); err != nil {
  96. checkErrPanic(err)
  97. }
  98. }
  99. // we should never have reached this point!
  100. log.Error("Unknown usage! This application may run as a standalone server, as FastCGI application, or as an interactive shell")
  101. if *isServer || *isShell {
  102. flag.PrintDefaults()
  103. }
  104. }
  105. // handlerQuery deals with queries
  106. func handlerQuery(w http.ResponseWriter, r *http.Request) {
  107. // get all parameters in array (error if no parameters are passed)
  108. if err := r.ParseForm(); err != nil {
  109. logErrHTTP(w, http.StatusNotFound, "No query string received")
  110. return
  111. }
  112. // test first if this comes from Second Life or OpenSimulator
  113. if r.Header.Get("X-Secondlife-Region") == "" {
  114. logErrHTTP(w, http.StatusForbidden, "Sorry, this application only works inside Second Life.")
  115. return
  116. }
  117. // someone sent us an avatar name to look up, cool!
  118. queryName := r.Form.Get("name")
  119. log.Debug("%s - Looking up '%s'", funcName(), queryName)
  120. if queryName == "" {
  121. logErrHTTP(w, http.StatusNotFound, "Empty avatar name received; did you use ...?name=avatar_name")
  122. return
  123. }
  124. w.WriteHeader(http.StatusOK)
  125. w.Header().Set("Content-type", "text/plain; charset=utf-8")
  126. fmt.Fprintf(w, "You called me with: %s and your avatar name is %s\n", r.URL.Path[1:], queryName)
  127. }
  128. // handlerTouch associates avatar names with keys
  129. func handlerTouch(w http.ResponseWriter, r *http.Request) {
  130. if err := r.ParseForm(); err != nil {
  131. logErrHTTP(w, http.StatusNotFound, "No avatar and/or UUID received")
  132. return
  133. }
  134. // test first if this comes from Second Life or OpenSimulator
  135. if r.Header.Get("X-Secondlife-Region") == "" {
  136. logErrHTTP(w, http.StatusForbidden, "Sorry, this application only works inside Second Life.")
  137. return
  138. }
  139. registerName := r.Form.Get("name")
  140. registerKey := r.Form.Get("key")
  141. if registerName == "" {
  142. logErrHTTP(w, http.StatusNotFound, "Empty avatar name received; did you use ...?name=avatar_name")
  143. return
  144. }
  145. if registerKey == "" {
  146. logErrHTTP(w, http.StatusNotFound, "Empty avatar UUID key received; did you use ...?key=avatar_uuid")
  147. return
  148. }
  149. w.WriteHeader(http.StatusOK)
  150. w.Header().Set("Content-type", "text/plain; charset=utf-8")
  151. fmt.Fprintf(w, "You called me with: %s and your avatar name is '%s' and your UUID key is '%s'", registerName, registerKey, r.URL.Path[1:])
  152. }
  153. // NOTE(gwyneth):Auxiliary functions which I'm always using...
  154. // checkErrPanic logs a fatal error and panics.
  155. func checkErrPanic(err error) {
  156. if err != nil {
  157. pc, file, line, ok := runtime.Caller(1)
  158. log.Panic(filepath.Base(file), ":", line, ":", pc, ok, " - panic:", err)
  159. }
  160. }
  161. // checkErr checks if there is an error, and if yes, it logs it out and continues.
  162. // this is for 'normal' situations when we want to get a log if something goes wrong but do not need to panic
  163. func checkErr(err error) {
  164. if err != nil {
  165. pc, file, line, ok := runtime.Caller(1)
  166. log.Error(filepath.Base(file), ":", line, ":", pc, ok, " - error:", err)
  167. }
  168. }
  169. // Auxiliary functions for HTTP handling
  170. // checkErrHTTP returns an error via HTTP and also logs the error.
  171. func checkErrHTTP(w http.ResponseWriter, httpStatus int, errorMessage string, err error) {
  172. if err != nil {
  173. http.Error(w, fmt.Sprintf(errorMessage, err), httpStatus)
  174. pc, file, line, ok := runtime.Caller(1)
  175. log.Error("(", http.StatusText(httpStatus), ") ", filepath.Base(file), ":", line, ":", pc, ok, " - error:", errorMessage, err)
  176. }
  177. }
  178. // checkErrPanicHTTP returns an error via HTTP and logs the error with a panic.
  179. func checkErrPanicHTTP(w http.ResponseWriter, httpStatus int, errorMessage string, err error) {
  180. if err != nil {
  181. http.Error(w, fmt.Sprintf(errorMessage, err), httpStatus)
  182. pc, file, line, ok := runtime.Caller(1)
  183. log.Panic("(", http.StatusText(httpStatus), ") ", filepath.Base(file), ":", line, ":", pc, ok, " - panic:", errorMessage, err)
  184. }
  185. }
  186. // logErrHTTP assumes that the error message was already composed and writes it to HTTP and logs it.
  187. // this is mostly to avoid code duplication and make sure that all entries are written similarly
  188. func logErrHTTP(w http.ResponseWriter, httpStatus int, errorMessage string) {
  189. http.Error(w, errorMessage, httpStatus)
  190. log.Error("(" + http.StatusText(httpStatus) + ") " + errorMessage)
  191. }
  192. // funcName is @Sonia's solution to get the name of the function that Go is currently running.
  193. // This will be extensively used to deal with figuring out where in the code the errors are!
  194. // Source: https://stackoverflow.com/a/10743805/1035977 (20170708)
  195. func funcName() string {
  196. pc, _, _, _ := runtime.Caller(1)
  197. return runtime.FuncForPC(pc).Name()
  198. }