// Serve all files in the current directory, or only a few select filetypes (html, css, js, png and txt) func registerHandlers(mux *http.ServeMux, handlePath, servedir string, perm pinterface.IPermissions, luapool *lStatePool, cache *FileCache, addDomain bool) { // Handle all requests with this function allRequests := func(w http.ResponseWriter, req *http.Request) { if perm.Rejected(w, req) { // Get and call the Permission Denied function perm.DenyFunction()(w, req) // Reject the request by returning return } // Local to this function servedir := servedir // Look for the directory that is named the same as the host if addDomain { servedir = filepath.Join(servedir, getDomain(req)) } urlpath := req.URL.Path filename := url2filename(servedir, urlpath) // Remove the trailing slash from the filename, if any noslash := filename if strings.HasSuffix(filename, pathsep) { noslash = filename[:len(filename)-1] } hasdir := fs.exists(filename) && fs.isDir(filename) dirname := filename hasfile := fs.exists(noslash) // Set the server header. serverHeaders(w) // Share the directory or file if hasdir { dirPage(w, req, servedir, dirname, perm, luapool, cache) return } else if !hasdir && hasfile { // Share a single file instead of a directory filePage(w, req, noslash, perm, luapool, cache) return } // Not found w.WriteHeader(http.StatusNotFound) fmt.Fprint(w, noPage(filename)) } // Handle requests differently depending on if rate limiting is enabled or not if disableRateLimiting { mux.HandleFunc(handlePath, allRequests) } else { limiter := tollbooth.NewLimiter(limitRequests, time.Second) limiter.MessageContentType = "text/html; charset=utf-8" limiter.Message = easyPage("Rate-limit exceeded", "<div style='color:red'>You have reached the maximum request limit.</div>") mux.Handle(handlePath, tollbooth.LimitFuncHandler(limiter, allRequests)) } }
// Run a Lua file as a configuration script. Also has access to the userstate and permissions. // Returns an error if there was a problem with running the lua script, otherwise nil. func runConfiguration(filename string, perm pinterface.IPermissions, luapool *lStatePool, cache *FileCache, mux *http.ServeMux, singleFileMode bool) error { // Retrieve a Lua state L := luapool.Get() // Retrieve the userstate userstate := perm.UserState() // Server configuration functions exportServerConfigFunctions(L, perm, filename, luapool) // Other basic system functions, like log() exportBasicSystemFunctions(L) // Simpleredis data structures (could be used for storing server stats) exportList(L, userstate) exportSet(L, userstate) exportHash(L, userstate) exportKeyValue(L, userstate) // For handling JSON data exportJSONFunctions(L) exportJFile(L, filepath.Dir(filename)) // For saving and loading Lua functions exportCodeLibrary(L, userstate) // Plugins exportPluginFunctions(L, nil) // Cache exportCacheFunctions(L, cache) if singleFileMode { // Lua HTTP handlers exportLuaHandlerFunctions(L, filename, perm, luapool, cache, mux, false) } // Run the script if err := L.DoFile(filename); err != nil { // Close the Lua state L.Close() // Logging and/or HTTP response is handled elsewhere return err } // Only put the Lua state back if there were no errors luapool.Put(L) return nil }
// Return a *lua.LState object that contains several exposed functions func exportCommonFunctions(w http.ResponseWriter, req *http.Request, filename string, perm pinterface.IPermissions, L *lua.LState, luapool *lStatePool, flushFunc func(), cache *FileCache) { // Retrieve the userstate userstate := perm.UserState() // Make basic functions, like print, available to the Lua script. // Only exports functions that can relate to HTTP responses or requests. exportBasicWeb(w, req, L, filename, flushFunc) // Functions for serving files in the same directory as a script exportServeFile(w, req, L, filename, perm, luapool, cache) // Make other basic functions available exportBasicSystemFunctions(L) // Functions for rendering markdown or amber exportRenderFunctions(w, req, L) // Make the functions related to userstate available to the Lua script exportUserstate(w, req, L, userstate) // Simpleredis data structures exportList(L, userstate) exportSet(L, userstate) exportHash(L, userstate) exportKeyValue(L, userstate) // For handling JSON data exportJSONFunctions(L) exportJFile(L, filepath.Dir(filename)) // For saving and loading Lua functions exportCodeLibrary(L, userstate) // pprint //exportREPL(L) // Plugins exportPluginFunctions(L, nil) // Cache exportCacheFunctions(L, cache) // File uploads exportUploadedFile(L, w, req, filepath.Dir(filename)) }
// REPL provides a "Read Eveal Print" loop for interacting with Lua. // A variety of functions are exposed to the Lua state. func REPL(perm pinterface.IPermissions, luapool *lStatePool, cache *FileCache, ready, done chan bool) error { var ( historyFilename string err error ) historydir, err := homedir.Dir() if err != nil { log.Error("Could not find a user directory to store the REPL history.") historydir = "." } if runtime.GOOS == "windows" { historyFilename = filepath.Join(historydir, "algernon", "repl.txt") } else { historyFilename = filepath.Join(historydir, ".algernon_history") } // Retrieve the userstate userstate := perm.UserState() // Retrieve a Lua state L := luapool.Get() // Don't re-use the Lua state defer L.Close() // Server configuration functions exportServerConfigFunctions(L, perm, "", luapool) // Other basic system functions, like log() exportBasicSystemFunctions(L) // Simpleredis data structures exportList(L, userstate) exportSet(L, userstate) exportHash(L, userstate) exportKeyValue(L, userstate) // For handling JSON data exportJSONFunctions(L) exportJFile(L, serverDir) // For saving and loading Lua functions exportCodeLibrary(L, userstate) // Pretty printing exportREPL(L) // Colors and input enableColors := runtime.GOOS != "windows" o := term.NewTextOutput(enableColors, true) // Plugin functionality exportPluginFunctions(L, o) // Cache exportCacheFunctions(L, cache) // Getting ready o.Println(o.LightBlue(versionString)) <-ready // Wait for the server to be ready // Tell the user that the server is ready o.Println(o.LightGreen("Ready")) // Start the read, eval, print loop var ( line string prompt = o.LightGreen("lua> ") EOF bool EOFcount int ) if loadHistory(historyFilename) != nil && fs.exists(historyFilename) { log.Error("Could not load REPL history:", historyFilename) } // To be run at server shutdown atShutdown(func() { // Verbose mode has different log output at shutdown if !verboseMode { o.Println(o.LightBlue(exitMessage)) } }) for { // Retrieve user input EOF = false if line, err = getInput(prompt); err != nil { if err.Error() == "EOF" { if debugMode { o.Println(o.LightPurple(err.Error())) } EOF = true } else { log.Error("Error reading line(" + err.Error() + ").") continue } } else { addHistory(line) // Save the REPL history at every line. // This proved to be safer than only saving the history at shutdown // (due to how ctrl-c was handled on some systems) // and it's hard to imagine performance issues with this. saveHistory(historyFilename) } if EOF { switch EOFcount { case 0: o.Err("Press ctrl-d again to exit.") EOFcount++ continue default: done <- true return nil } } line = strings.TrimSpace(line) if line == "" { continue } switch line { case "help": outputHelp(o, generalHelpText) continue case "webhelp": outputHelp(o, webHelpText) continue case "confighelp": outputHelp(o, configHelpText) continue case "quit", "exit", "shutdown", "halt": done <- true return nil case "zalgo": // Easter egg o.ErrExit("Ḫ̷̲̫̰̯̭̀̂̑̈ͅĚ̥̖̩̘̱͔͈͈ͬ̚ ̦̦͖̲̀ͦ͂C̜͓̲̹͐̔ͭ̏Oͭ͛͂̋ͭͬͬ͆͏̺͓̰͚͠ͅM̢͉̼̖͍̊̕Ḛ̭̭͗̉̀̆ͬ̐ͪ̒S͉̪͂͌̄") } // If the line starts with print, don't touch it if strings.HasPrefix(line, "print(") { if err = L.DoString(line); err != nil { // Output the error message o.Err(err.Error()) } } else { // Wrap the line in "pprint" if err = L.DoString("pprint(" + line + ")"); err != nil { // If there was a syntax error, try again without pprint if strings.Contains(err.Error(), "syntax error") { if err = L.DoString(line); err != nil { // Output the error message o.Err(err.Error()) } // For other kinds of errors, output the error } else { // Output the error message o.Err(err.Error()) } } } } }
// Make functions related to server configuration and permissions available func exportServerConfigFunctions(L *lua.LState, perm pinterface.IPermissions, filename string, luapool *lStatePool) { // Set a default host and port. Maybe useful for alg applications. L.SetGlobal("SetAddr", L.NewFunction(func(L *lua.LState) int { serverAddrLua = L.ToString(1) return 0 // number of results })) // Clear the default path prefixes. This makes everything public. L.SetGlobal("ClearPermissions", L.NewFunction(func(L *lua.LState) int { perm.Clear() return 0 // number of results })) // Registers a path prefix, for instance "/secret", // as having *user* rights. L.SetGlobal("AddUserPrefix", L.NewFunction(func(L *lua.LState) int { path := L.ToString(1) perm.AddUserPath(path) return 0 // number of results })) // Registers a path prefix, for instance "/secret", // as having *admin* rights. L.SetGlobal("AddAdminPrefix", L.NewFunction(func(L *lua.LState) int { path := L.ToString(1) perm.AddAdminPath(path) return 0 // number of results })) // Sets a Lua function as a custom "permissions denied" page handler. L.SetGlobal("DenyHandler", L.NewFunction(func(L *lua.LState) int { luaDenyFunc := L.ToFunction(1) // Custom handler for when permissions are denied perm.SetDenyFunction(func(w http.ResponseWriter, req *http.Request) { // Set up a new Lua state with the current http.ResponseWriter and *http.Request, without caching exportCommonFunctions(w, req, filename, perm, L, luapool, nil, nil) // Then run the given Lua function L.Push(luaDenyFunc) if err := L.PCall(0, lua.MultRet, nil); err != nil { // Non-fatal error log.Error("Permission denied handler failed:", err) // Use the default permission handler from now on if the lua function fails perm.SetDenyFunction(permissions.PermissionDenied) perm.DenyFunction()(w, req) } }) return 0 // number of results })) // Sets a Lua function to be run once the server is done parsing configuration and arguments. L.SetGlobal("OnReady", L.NewFunction(func(L *lua.LState) int { luaReadyFunc := L.ToFunction(1) // Custom handler for when permissions are denied. // Put the *lua.LState in a closure. serverReadyFunctionLua = func() { // Run the given Lua function L.Push(luaReadyFunc) if err := L.PCall(0, lua.MultRet, nil); err != nil { // Non-fatal error log.Error("The OnReady function failed:", err) } } return 0 // number of results })) // Set a access log filename. If blank, the log will go to the console (or browser, if debug mode is set). L.SetGlobal("LogTo", L.NewFunction(func(L *lua.LState) int { filename := L.ToString(1) serverLogFile = filename // Log as JSON by default log.SetFormatter(&log.JSONFormatter{}) // Log to stderr if an empty filename is given if filename == "" { log.SetOutput(os.Stderr) L.Push(lua.LBool(true)) return 1 // number of results } // Try opening/creating the given filename, for appending f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, defaultPermissions) if err != nil { log.Error(err) L.Push(lua.LBool(false)) return 1 // number of results } // Set the file to log to and return log.SetOutput(f) L.Push(lua.LBool(true)) return 1 // number of results })) // Use a single Lua file as the server, instead of directory structure L.SetGlobal("ServerFile", L.NewFunction(func(L *lua.LState) int { givenFilename := L.ToString(1) serverfilename := filepath.Join(filepath.Dir(filename), givenFilename) if !fs.exists(filename) { log.Error("Could not find", serverfilename) L.Push(lua.LBool(false)) return 1 // number of results } luaServerFilename = serverfilename L.Push(lua.LBool(true)) return 1 // number of results })) L.SetGlobal("ServerInfo", L.NewFunction(func(L *lua.LState) int { // Return the string, but drop the final newline L.Push(lua.LString(serverInfo())) return 1 // number of results })) }