basic.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. package engine
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "net/url"
  8. "os"
  9. "path"
  10. "path/filepath"
  11. "strings"
  12. "time"
  13. "github.com/gomarkdown/markdown"
  14. "github.com/gomarkdown/markdown/parser"
  15. log "github.com/sirupsen/logrus"
  16. "github.com/xyproto/algernon/lua/convert"
  17. "github.com/xyproto/algernon/utils"
  18. lua "github.com/xyproto/gopher-lua"
  19. "github.com/xyproto/splash"
  20. )
  21. // FutureStatus is useful when redirecting in combination with writing to a
  22. // buffer before writing to a client. May contain more fields in the future.
  23. type FutureStatus struct {
  24. code int // Buffered HTTP status code
  25. }
  26. // LoadBasicSystemFunctions loads functions related to logging, markdown and the
  27. // current server directory into the given Lua state
  28. func (ac *Config) LoadBasicSystemFunctions(L *lua.LState) {
  29. // Return the version string
  30. L.SetGlobal("version", L.NewFunction(func(L *lua.LState) int {
  31. L.Push(lua.LString(ac.versionString))
  32. return 1 // number of results
  33. }))
  34. // Log text with the "Info" log type
  35. L.SetGlobal("log", L.NewFunction(func(L *lua.LState) int {
  36. buf := convert.Arguments2buffer(L, false)
  37. // Log the combined text
  38. log.Info(buf.String())
  39. return 0 // number of results
  40. }))
  41. // Log text with the "Warn" log type
  42. L.SetGlobal("warn", L.NewFunction(func(L *lua.LState) int {
  43. buf := convert.Arguments2buffer(L, false)
  44. // Log the combined text
  45. log.Warn(buf.String())
  46. return 0 // number of results
  47. }))
  48. // Log text with the "Error" log type
  49. L.SetGlobal("err", L.NewFunction(func(L *lua.LState) int {
  50. buf := convert.Arguments2buffer(L, false)
  51. // Log the combined text
  52. log.Error(buf.String())
  53. return 0 // number of results
  54. }))
  55. // Sleep for the given number of seconds (can be a float)
  56. L.SetGlobal("sleep", L.NewFunction(func(L *lua.LState) int {
  57. // Extract the correct number of nanoseconds
  58. duration := time.Duration(float64(L.ToNumber(1)) * 1000000000.0)
  59. // Wait and block the current thread of execution.
  60. time.Sleep(duration)
  61. return 0
  62. }))
  63. // Return the current unixtime, with an attempt at nanosecond resolution
  64. L.SetGlobal("unixnano", L.NewFunction(func(L *lua.LState) int {
  65. // Extract the correct number of nanoseconds
  66. L.Push(lua.LNumber(time.Now().UnixNano()))
  67. return 1 // number of results
  68. }))
  69. // Convert Markdown to HTML
  70. L.SetGlobal("markdown", L.NewFunction(func(L *lua.LState) int {
  71. // Retrieve all the function arguments as a bytes.Buffer
  72. buf := convert.Arguments2buffer(L, true)
  73. // Create a Markdown parser with the desired extensions
  74. extensions := parser.CommonExtensions | parser.AutoHeadingIDs
  75. mdParser := parser.NewWithExtensions(extensions)
  76. // Convert the buffer to markdown
  77. htmlData := markdown.ToHTML(buf.Bytes(), mdParser, nil)
  78. codeStyle := "base16-snazzy"
  79. if highlightedHTML, err := splash.Splash(htmlData, codeStyle); err == nil { // success
  80. htmlData = highlightedHTML
  81. }
  82. htmlString := strings.TrimSpace(string(htmlData))
  83. L.Push(lua.LString(htmlString))
  84. return 1 // number of results
  85. }))
  86. // Get the full filename of a given file that is in the directory
  87. // where the server is running (root directory for the server).
  88. // If no filename is given, the directory where the server is
  89. // currently running is returned.
  90. L.SetGlobal("serverdir", L.NewFunction(func(L *lua.LState) int {
  91. serverdir, err := os.Getwd()
  92. if err != nil {
  93. // Could not retrieve a directory
  94. serverdir = ""
  95. } else if L.GetTop() == 1 {
  96. // Also include a separator and a filename
  97. fn := L.ToString(1)
  98. serverdir = filepath.Join(serverdir, fn)
  99. }
  100. L.Push(lua.LString(serverdir))
  101. return 1 // number of results
  102. }))
  103. }
  104. // LoadBasicWeb loads functions related to handling requests, outputting data to
  105. // the browser, setting headers, pretty printing and dealing with the directory
  106. // where files are being served, into the given Lua state.
  107. func (ac *Config) LoadBasicWeb(w http.ResponseWriter, req *http.Request, L *lua.LState, filename string, flushFunc func(), httpStatus *FutureStatus) {
  108. // Print text to the web page that is being served. Add a newline.
  109. L.SetGlobal("print", L.NewFunction(func(L *lua.LState) int {
  110. if req.Close {
  111. if ac.debugMode {
  112. log.Error("call to \"print\" after closing the connection")
  113. }
  114. return 0 // number of results
  115. }
  116. var buf bytes.Buffer
  117. top := L.GetTop()
  118. for i := 1; i <= top; i++ {
  119. buf.WriteString(L.Get(i).String())
  120. if i != top {
  121. buf.WriteString("\t")
  122. }
  123. }
  124. // Final newline
  125. buf.WriteString("\n")
  126. // Write the combined text to the http.ResponseWriter
  127. buf.WriteTo(w)
  128. return 0 // number of results
  129. }))
  130. // Pretty print text to the web page that is being served. Add a newline.
  131. L.SetGlobal("pprint", L.NewFunction(func(L *lua.LState) int {
  132. if req.Close {
  133. if ac.debugMode {
  134. log.Error("call to \"pprint\" after closing the connection")
  135. }
  136. return 0 // number of results
  137. }
  138. var buf bytes.Buffer
  139. top := L.GetTop()
  140. for i := 1; i <= top; i++ {
  141. convert.PprintToWriter(&buf, L.Get(i))
  142. if i != top {
  143. buf.WriteString("\t")
  144. }
  145. }
  146. // Final newline
  147. buf.WriteString("\n")
  148. // Write the combined text to the http.ResponseWriter
  149. buf.WriteTo(w)
  150. return 0 // number of results
  151. }))
  152. // Pretty print to string
  153. L.SetGlobal("ppstr", L.NewFunction(func(L *lua.LState) int {
  154. var buf bytes.Buffer
  155. top := L.GetTop()
  156. for i := 1; i <= top; i++ {
  157. convert.PprintToWriter(&buf, L.Get(i))
  158. if i != top {
  159. buf.WriteString("\t")
  160. }
  161. }
  162. // Return the string
  163. L.Push(lua.LString(buf.String()))
  164. return 1 // number of results
  165. }))
  166. // Flush the ResponseWriter.
  167. // Needed in debug mode, where ResponseWriter is buffered.
  168. L.SetGlobal("flush", L.NewFunction(func(L *lua.LState) int {
  169. if req.Close {
  170. if ac.debugMode {
  171. log.Error("call to \"flush\" after closing the connection")
  172. }
  173. return 0 // number of results
  174. }
  175. if flushFunc != nil {
  176. flushFunc()
  177. }
  178. return 0 // number of results
  179. }))
  180. // Close the communication with the client by setting a "Connection: close" header,
  181. // flushing and setting req.Close to true.
  182. L.SetGlobal("close", L.NewFunction(func(L *lua.LState) int {
  183. // Close the connection.
  184. // Works for both HTTP and HTTP/2 now, ref: https://github.com/golang/go/issues/20977
  185. w.Header().Add("Connection", "close")
  186. // Flush, if possible
  187. if flushFunc != nil {
  188. flushFunc()
  189. }
  190. // Stop Lua functions from writing more to this client
  191. req.Close = true
  192. // TODO: Set up the HTTP/QUIC/HTTP/2 Server structs with a ConnContext
  193. // field and then fetch the connection from the req.Context()
  194. // and use it here for closing the connection.
  195. return 0 // number of results
  196. }))
  197. // Set the Content-Type for the page
  198. L.SetGlobal("content", L.NewFunction(func(L *lua.LState) int {
  199. if req.Close {
  200. if ac.debugMode {
  201. log.Error("call to \"content\" after closing the connection")
  202. }
  203. return 0 // number of results
  204. }
  205. lv := L.ToString(1)
  206. w.Header().Add("Content-Type", lv)
  207. return 0 // number of results
  208. }))
  209. // Return the current URL Path
  210. L.SetGlobal("urlpath", L.NewFunction(func(L *lua.LState) int {
  211. L.Push(lua.LString(req.URL.Path))
  212. return 1 // number of results
  213. }))
  214. // Return the current HTTP method (GET, POST etc)
  215. L.SetGlobal("method", L.NewFunction(func(L *lua.LState) int {
  216. L.Push(lua.LString(req.Method))
  217. return 1 // number of results
  218. }))
  219. // Return the HTTP headers as a table
  220. L.SetGlobal("headers", L.NewFunction(func(L *lua.LState) int {
  221. luaTable := L.NewTable()
  222. for key := range req.Header {
  223. L.RawSet(luaTable, lua.LString(key), lua.LString(req.Header.Get(key)))
  224. }
  225. if req.Host != "" {
  226. L.RawSet(luaTable, lua.LString("Host"), lua.LString(req.Host))
  227. }
  228. L.Push(luaTable)
  229. return 1 // number of results
  230. }))
  231. // Return the HTTP header in the request, for a given key/string
  232. L.SetGlobal("header", L.NewFunction(func(L *lua.LState) int {
  233. key := L.ToString(1)
  234. value := req.Header.Get(key)
  235. L.Push(lua.LString(value))
  236. return 1 // number of results
  237. }))
  238. // Set the HTTP header in the request, for a given key and value
  239. L.SetGlobal("setheader", L.NewFunction(func(L *lua.LState) int {
  240. if req.Close {
  241. if ac.debugMode {
  242. log.Error("call to \"setheader\" after closing the connection")
  243. }
  244. return 0 // number of results
  245. }
  246. key := L.ToString(1)
  247. value := L.ToString(2)
  248. w.Header().Set(key, value)
  249. return 0 // number of results
  250. }))
  251. // Return the HTTP body in the request
  252. L.SetGlobal("body", L.NewFunction(func(L *lua.LState) int {
  253. body, err := io.ReadAll(req.Body)
  254. var result lua.LString
  255. if err != nil {
  256. result = lua.LString("")
  257. } else {
  258. result = lua.LString(string(body))
  259. }
  260. L.Push(result)
  261. return 1 // number of results
  262. }))
  263. // Set the HTTP status code (must come before print)
  264. L.SetGlobal("status", L.NewFunction(func(L *lua.LState) int {
  265. if req.Close {
  266. if ac.debugMode {
  267. log.Error("call to \"status\" after closing the connection")
  268. }
  269. return 0 // number of results
  270. }
  271. code := int(L.ToNumber(1))
  272. if httpStatus != nil {
  273. httpStatus.code = code
  274. }
  275. w.WriteHeader(code)
  276. return 0 // number of results
  277. }))
  278. // Throw an error/exception in Lua
  279. L.SetGlobal("throw", L.GetGlobal("error"))
  280. // Set a HTTP status code and print a message (optional)
  281. L.SetGlobal("error", L.NewFunction(func(L *lua.LState) int {
  282. if req.Close {
  283. if ac.debugMode {
  284. log.Error("call to \"error\" after closing the connection")
  285. }
  286. return 0 // number of results
  287. }
  288. code := int(L.ToNumber(1))
  289. if httpStatus != nil {
  290. httpStatus.code = code
  291. }
  292. w.WriteHeader(code)
  293. if L.GetTop() == 2 {
  294. message := L.ToString(2)
  295. fmt.Fprint(w, message)
  296. }
  297. return 0 // number of results
  298. }))
  299. // Get the full filename of a given file that is in the directory
  300. // of the script that is about to be run. If no filename is given,
  301. // the directory of the script is returned.
  302. L.SetGlobal("scriptdir", L.NewFunction(func(L *lua.LState) int {
  303. scriptpath, err := filepath.Abs(filename)
  304. if err != nil {
  305. scriptpath = filename
  306. }
  307. scriptdir := filepath.Dir(scriptpath)
  308. scriptpath = scriptdir
  309. top := L.GetTop()
  310. if top == 1 {
  311. // Also include a separator and a filename
  312. fn := L.ToString(1)
  313. scriptpath = filepath.Join(scriptdir, fn)
  314. }
  315. // Now have the correct absolute scriptpath
  316. L.Push(lua.LString(scriptpath))
  317. return 1 // number of results
  318. }))
  319. // Given a filename, return the URL path
  320. L.SetGlobal("file2url", L.NewFunction(func(L *lua.LState) int {
  321. fn := L.ToString(1)
  322. targetpath := strings.TrimPrefix(filepath.Join(filepath.Dir(filename), fn), ac.serverDirOrFilename)
  323. if utils.Pathsep != "/" {
  324. // For operating systems that use another path separator for files than for URLs
  325. targetpath = strings.ReplaceAll(targetpath, utils.Pathsep, "/")
  326. }
  327. withSlashPrefix := path.Join("/", targetpath)
  328. L.Push(lua.LString(withSlashPrefix))
  329. return 1 // number of results
  330. }))
  331. // Retrieve a table with keys and values from the form in the request
  332. L.SetGlobal("formdata", L.NewFunction(func(L *lua.LState) int {
  333. // Place the form data in a map
  334. m := make(map[string]string)
  335. req.ParseForm()
  336. for key, values := range req.Form {
  337. m[key] = values[0]
  338. }
  339. // Convert the map to a table and return it
  340. L.Push(convert.Map2table(L, m))
  341. return 1 // number of results
  342. }))
  343. // Retrieve a table with keys and values from the URL in the request
  344. L.SetGlobal("urldata", L.NewFunction(func(L *lua.LState) int {
  345. var (
  346. valueMap url.Values
  347. err error
  348. )
  349. if L.GetTop() == 1 {
  350. // If given an argument
  351. rawurl := L.ToString(1)
  352. valueMap, err = url.ParseQuery(rawurl)
  353. // Log error as warning if there are issues.
  354. // An empty Value map will then be used.
  355. if err != nil {
  356. log.Error(err)
  357. // return 0
  358. }
  359. } else {
  360. // If not given an argument
  361. valueMap = req.URL.Query() // map[string][]string
  362. }
  363. // Place the Value data in a map, using the first values
  364. // if there are many values for a given key.
  365. m := make(map[string]string)
  366. for key, values := range valueMap {
  367. m[key] = values[0]
  368. }
  369. // Convert the map to a table and return it
  370. L.Push(convert.Map2table(L, m))
  371. return 1 // number of results
  372. }))
  373. // Redirect a request (as found, by default)
  374. L.SetGlobal("redirect", L.NewFunction(func(L *lua.LState) int {
  375. if req.Close {
  376. if ac.debugMode {
  377. log.Error("redirect after closing the connection")
  378. }
  379. return 0 // number of results
  380. }
  381. newurl := L.ToString(1)
  382. httpStatusCode := http.StatusFound
  383. if L.GetTop() == 2 {
  384. httpStatusCode = int(L.ToNumber(2))
  385. }
  386. if httpStatus != nil {
  387. httpStatus.code = httpStatusCode
  388. }
  389. http.Redirect(w, req, newurl, httpStatusCode)
  390. return 0 // number of results
  391. }))
  392. // Permanently redirect a request, which is the same as redirect(url, 301)
  393. L.SetGlobal("permanent_redirect", L.NewFunction(func(L *lua.LState) int {
  394. if req.Close {
  395. if ac.debugMode {
  396. log.Error("permanent_redirect after closing the connection")
  397. }
  398. return 0 // number of results
  399. }
  400. newurl := L.ToString(1)
  401. httpStatusCode := http.StatusMovedPermanently
  402. if httpStatus != nil {
  403. httpStatus.code = httpStatusCode
  404. }
  405. http.Redirect(w, req, newurl, httpStatusCode)
  406. return 0 // number of results
  407. }))
  408. // Run the given Lua file (replacement for the built-in dofile, to look in the right directory)
  409. // Returns whatever the Lua file returns when it is being run.
  410. L.SetGlobal("dofile", L.NewFunction(func(L *lua.LState) int {
  411. givenFilename := L.ToString(1)
  412. luaFilename := filepath.Join(filepath.Dir(filename), givenFilename)
  413. if !ac.fs.Exists(luaFilename) {
  414. log.Error("Could not find:", luaFilename)
  415. return 0 // number of results
  416. }
  417. if err := L.DoFile(luaFilename); err != nil {
  418. log.Errorf("Error running %s: %s\n", luaFilename, err)
  419. return 0 // number of results
  420. }
  421. // Retrieve the returned value from the script
  422. retval := L.Get(-1)
  423. L.Pop(1)
  424. // Return the value returned from the script
  425. L.Push(retval)
  426. return 1 // number of results
  427. }))
  428. }