handlers.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. package engine
  2. import (
  3. "fmt"
  4. "html/template"
  5. "io"
  6. "net/http"
  7. "net/http/httptest"
  8. "os"
  9. "path"
  10. "path/filepath"
  11. "strings"
  12. "time"
  13. "github.com/didip/tollbooth"
  14. log "github.com/sirupsen/logrus"
  15. "github.com/xyproto/algernon/themes"
  16. "github.com/xyproto/algernon/utils"
  17. "github.com/xyproto/datablock"
  18. "github.com/xyproto/recwatch"
  19. "github.com/xyproto/sheepcounter"
  20. "github.com/xyproto/simpleform"
  21. "github.com/xyproto/unzip"
  22. )
  23. const (
  24. // Gzip content over this size
  25. gzipThreshold = 4096
  26. // Used for deciding how long to wait before quitting when only serving a single file and starting a browser
  27. defaultSoonDuration = time.Second * 3
  28. )
  29. // ClientCanGzip checks if the client supports gzip compressed responses
  30. func (ac *Config) ClientCanGzip(req *http.Request) bool {
  31. // Curl does not use --compressed by default. This causes problems when
  32. // serving gzipped contents when curl is run without --compressed!
  33. // The wrong data, of the same size, will be downloaded. Beware!
  34. if ac.curlSupport {
  35. return strings.Contains(req.Header.Get("Accept-Encoding"), "gzip")
  36. }
  37. // Modern browsers support gzip
  38. return true
  39. }
  40. // PongoHandler renders and serves a Pongo2 template
  41. func (ac *Config) PongoHandler(w http.ResponseWriter, req *http.Request, filename, ext string) {
  42. w.Header().Add("Content-Type", "text/html;charset=utf-8")
  43. pongoblock, err := ac.cache.Read(filename, ac.shouldCache(ext))
  44. if err != nil {
  45. if ac.debugMode {
  46. fmt.Fprintf(w, "Unable to read %s: %s", filename, err)
  47. } else {
  48. log.Errorf("Unable to read %s: %s", filename, err)
  49. }
  50. return
  51. }
  52. // Make the functions in luaDataFilename available for the Pongo2 template
  53. luafilename := filepath.Join(filepath.Dir(filename), ac.defaultLuaDataFilename)
  54. if ac.fs.Exists(ac.defaultLuaDataFilename) {
  55. luafilename = ac.defaultLuaDataFilename
  56. }
  57. if ac.fs.Exists(luafilename) {
  58. // Extract the function map from luaDataFilenname in a goroutine
  59. errChan := make(chan error)
  60. funcMapChan := make(chan template.FuncMap)
  61. go ac.Lua2funcMap(w, req, filename, luafilename, ext, errChan, funcMapChan)
  62. funcs := <-funcMapChan
  63. err = <-errChan
  64. if err != nil {
  65. if ac.debugMode {
  66. // Try reading luaDataFilename as well, if possible
  67. luablock, luablockErr := ac.cache.Read(luafilename, ac.shouldCache(ext))
  68. if luablockErr != nil {
  69. // Could not find and/or read luaDataFilename
  70. luablock = datablock.EmptyDataBlock
  71. }
  72. // Use the Lua filename as the title
  73. ac.PrettyError(w, req, luafilename, luablock.Bytes(), err.Error(), "lua")
  74. } else {
  75. log.Error(err)
  76. }
  77. return
  78. }
  79. // Render the Pongo2 page, using functions from luaDataFilename, if available
  80. ac.pongomutex.Lock()
  81. ac.PongoPage(w, req, filename, pongoblock.Bytes(), funcs)
  82. ac.pongomutex.Unlock()
  83. return
  84. }
  85. // Output a warning if something different from default has been given
  86. if !strings.HasSuffix(luafilename, ac.defaultLuaDataFilename) {
  87. log.Warn("Could not read ", luafilename)
  88. }
  89. // Use the Pongo2 template without any Lua functions
  90. ac.pongomutex.Lock()
  91. funcs := make(template.FuncMap)
  92. ac.PongoPage(w, req, filename, pongoblock.Bytes(), funcs)
  93. ac.pongomutex.Unlock()
  94. }
  95. // ReadAndLogErrors tries to read a file, and logs an error if it could not be read
  96. func (ac *Config) ReadAndLogErrors(w http.ResponseWriter, filename, ext string) (*datablock.DataBlock, error) {
  97. byteblock, err := ac.cache.Read(filename, ac.shouldCache(ext))
  98. if err != nil {
  99. if ac.debugMode {
  100. fmt.Fprintf(w, "Unable to read %s: %s", filename, err)
  101. } else {
  102. log.Errorf("Unable to read %s: %s", filename, err)
  103. }
  104. }
  105. return byteblock, err
  106. }
  107. // FilePage tries to serve a single file. The file must exist. Must be given a full filename.
  108. func (ac *Config) FilePage(w http.ResponseWriter, req *http.Request, filename, _ string) {
  109. if ac.quitAfterFirstRequest {
  110. go ac.quitSoon("Quit after first request", defaultSoonDuration)
  111. }
  112. // Use the file extension for setting the mimetype
  113. lowercaseFilename := strings.ToLower(filename)
  114. ext := filepath.Ext(lowercaseFilename)
  115. // Filenames ending with .hyper.js or .hyper.jsx are special cases
  116. if strings.HasSuffix(lowercaseFilename, ".hyper.js") {
  117. ext = ".hyper.js"
  118. } else if strings.HasSuffix(lowercaseFilename, ".hyper.jsx") {
  119. ext = ".hyper.jsx"
  120. }
  121. // Serve the file in different ways based on the filename extension
  122. switch ext {
  123. // HTML pages are handled differently, if auto-refresh has been enabled
  124. case ".html", ".htm":
  125. w.Header().Add("Content-Type", "text/html;charset=utf-8")
  126. // Read the file (possibly in compressed format, straight from the cache)
  127. htmlblock, err := ac.ReadAndLogErrors(w, filename, ext)
  128. if err != nil {
  129. return
  130. }
  131. // If the auto-refresh feature has been enabled
  132. if ac.autoRefresh {
  133. // Get the bytes from the datablock
  134. htmldata := htmlblock.Bytes()
  135. // Insert JavaScript for refreshing the page, into the HTML
  136. htmldata = ac.InsertAutoRefresh(req, htmldata)
  137. // Write the data to the client
  138. ac.DataToClient(w, req, filename, htmldata)
  139. } else {
  140. // Serve the file
  141. htmlblock.ToClient(w, req, filename, ac.ClientCanGzip(req), gzipThreshold)
  142. }
  143. return
  144. case ".md", ".markdown":
  145. w.Header().Add("Content-Type", "text/html;charset=utf-8")
  146. if markdownblock, err := ac.ReadAndLogErrors(w, filename, ext); err == nil { // if no error
  147. // Render the markdown page
  148. ac.MarkdownPage(w, req, markdownblock.Bytes(), filename)
  149. }
  150. return
  151. case ".frm", ".form":
  152. w.Header().Add("Content-Type", "text/html;charset=utf-8")
  153. formblock, err := ac.cache.Read(filename, ac.shouldCache(ext))
  154. if err != nil {
  155. return
  156. }
  157. // Render the form file as just the HTML body, not the surrounding document
  158. // (between <body> and </body>)
  159. html, err := simpleform.HTML(formblock.String(), false, "en")
  160. if err != nil {
  161. return
  162. }
  163. w.Write([]byte(html))
  164. return
  165. case ".amber", ".amb":
  166. w.Header().Add("Content-Type", "text/html;charset=utf-8")
  167. amberblock, err := ac.ReadAndLogErrors(w, filename, ext)
  168. if err != nil {
  169. return
  170. }
  171. // Try reading luaDataFilename as well, if possible
  172. luafilename := filepath.Join(filepath.Dir(filename), ac.defaultLuaDataFilename)
  173. luablock, err := ac.cache.Read(luafilename, ac.shouldCache(ext))
  174. if err != nil {
  175. // Could not find and/or read luaDataFilename
  176. luablock = datablock.EmptyDataBlock
  177. }
  178. // Make functions from the given Lua data available
  179. funcs := make(template.FuncMap)
  180. // luablock can be empty if there was an error or if the file was empty
  181. if luablock.HasData() {
  182. // There was Lua code available. Now make the functions and
  183. // variables available for the template.
  184. funcs, err = ac.LuaFunctionMap(w, req, luablock.Bytes(), luafilename)
  185. if err != nil {
  186. if ac.debugMode {
  187. // Use the Lua filename as the title
  188. ac.PrettyError(w, req, luafilename, luablock.Bytes(), err.Error(), "lua")
  189. } else {
  190. log.Error(err)
  191. }
  192. return
  193. }
  194. if ac.debugMode && ac.verboseMode {
  195. s := "These functions from " + luafilename
  196. s += " are useable for " + filename + ": "
  197. // Create a comma separated list of the available functions
  198. for key := range funcs {
  199. s += key + ", "
  200. }
  201. // Remove the final comma
  202. s = strings.TrimSuffix(s, ", ")
  203. // Output the message
  204. log.Info(s)
  205. }
  206. }
  207. // Render the Amber page, using functions from luaDataFilename, if available
  208. ac.AmberPage(w, req, filename, amberblock.Bytes(), funcs)
  209. return
  210. case ".po2", ".pongo2", ".tpl", ".tmpl":
  211. ac.PongoHandler(w, req, filename, ext)
  212. return
  213. case ".alg":
  214. // Assume this to be a compressed Algernon application
  215. webApplicationExtractionDir := "/dev/shm" // extract to memory, if possible
  216. testfile := filepath.Join(webApplicationExtractionDir, "canary")
  217. if _, err := os.Create(testfile); err == nil { // success
  218. os.Remove(testfile)
  219. } else {
  220. // Could not create the test file
  221. // Use the server temp dir (typically /tmp) instead of /dev/shm
  222. webApplicationExtractionDir = ac.serverTempDir
  223. }
  224. if extractErr := unzip.Extract(filename, webApplicationExtractionDir); extractErr == nil { // no error
  225. firstname := path.Base(filename)
  226. if strings.HasSuffix(filename, ".alg") {
  227. firstname = path.Base(filename[:len(filename)-4])
  228. }
  229. serveDir := path.Join(webApplicationExtractionDir, firstname)
  230. log.Warn(".alg web applications must be given as an argument to algernon to be served correctly")
  231. ac.DirPage(w, req, serveDir, serveDir, ac.defaultTheme)
  232. }
  233. return
  234. case ".lua", ".tl":
  235. // If in debug mode, let the Lua script print to a buffer first, in
  236. // case there are errors that should be displayed instead.
  237. // If debug mode is enabled
  238. if ac.debugMode {
  239. // Use a buffered ResponseWriter for delaying the output
  240. recorder := httptest.NewRecorder()
  241. // Create a new struct for keeping an optional http header status
  242. httpStatus := &FutureStatus{}
  243. // The flush function writes the ResponseRecorder to the ResponseWriter
  244. flushFunc := func() {
  245. utils.WriteRecorder(w, recorder)
  246. recwatch.Flush(w)
  247. }
  248. // Run the lua script, without the possibility to flush
  249. if err := ac.RunLua(recorder, req, filename, flushFunc, httpStatus); err != nil {
  250. errortext := err.Error()
  251. fileblock, err := ac.cache.Read(filename, ac.shouldCache(ext))
  252. if err != nil {
  253. // If the file could not be read, use the error message as the data
  254. // Use the error as the file contents when displaying the error message
  255. // if reading the file failed.
  256. fileblock = datablock.NewDataBlock([]byte(err.Error()), true)
  257. }
  258. // If there were errors, display an error page
  259. ac.PrettyError(w, req, filename, fileblock.Bytes(), errortext, "lua")
  260. } else {
  261. // If things went well, check if there is a status code we should write first
  262. // (especially for the case of a redirect)
  263. if httpStatus.code != 0 {
  264. recorder.WriteHeader(httpStatus.code)
  265. }
  266. // Then write to the ResponseWriter
  267. utils.WriteRecorder(w, recorder)
  268. }
  269. } else {
  270. // The flush function just flushes the ResponseWriter
  271. flushFunc := func() {
  272. recwatch.Flush(w)
  273. }
  274. // Run the lua script, with the flush feature
  275. if err := ac.RunLua(w, req, filename, flushFunc, nil); err != nil {
  276. // Output the non-fatal error message to the log
  277. if strings.HasPrefix(err.Error(), filename) {
  278. log.Error("Error at " + err.Error())
  279. } else {
  280. log.Error("Error in " + filename + ": " + err.Error())
  281. }
  282. }
  283. }
  284. return
  285. case ".gcss":
  286. if gcssblock, err := ac.ReadAndLogErrors(w, filename, ext); err == nil { // if no error
  287. w.Header().Add("Content-Type", "text/css;charset=utf-8")
  288. // Render the GCSS page as CSS
  289. ac.GCSSPage(w, req, filename, gcssblock.Bytes())
  290. }
  291. return
  292. case ".scss":
  293. if scssblock, err := ac.ReadAndLogErrors(w, filename, ext); err == nil { // if no error
  294. // Render the SASS page (with .scss extension) as CSS
  295. w.Header().Add("Content-Type", "text/css;charset=utf-8")
  296. ac.SCSSPage(w, req, filename, scssblock.Bytes())
  297. }
  298. return
  299. case ".happ", ".hyper", ".hyper.jsx", ".hyper.js": // hyperApp JSX -> JS, wrapped in HTML
  300. if jsxblock, err := ac.ReadAndLogErrors(w, filename, ext); err == nil { // if no error
  301. // Render the JSX page as HTML with embedded JavaScript
  302. w.Header().Add("Content-Type", "text/html;charset=utf-8")
  303. ac.HyperAppPage(w, req, filename, jsxblock.Bytes())
  304. } else {
  305. log.Error("Error when serving " + filename + ":" + err.Error())
  306. }
  307. return
  308. // This case must come after the .hyper.jsx case
  309. case ".jsx":
  310. if jsxblock, err := ac.ReadAndLogErrors(w, filename, ext); err == nil { // if no error
  311. // Render the JSX page as JavaScript
  312. w.Header().Add("Content-Type", "text/javascript;charset=utf-8")
  313. ac.JSXPage(w, req, filename, jsxblock.Bytes())
  314. }
  315. return
  316. // --- End of special handlers that returns early ---
  317. // Text and configuration files (most likely)
  318. case "", ".asciidoc", ".conf", ".config", ".diz", ".example", ".gitignore", ".gitmodules", ".ini", ".log", ".lst", ".me", ".nfo", ".pem", ".readme", ".sub", ".sum", ".tml", ".toml", ".txt", ".yaml", ".yml":
  319. // Set headers for displaying it in the browser.
  320. w.Header().Set("Content-Type", "text/plain;charset=utf-8")
  321. // Source files that may be used by web pages
  322. case ".js":
  323. w.Header().Add("Content-Type", "text/javascript;charset=utf-8")
  324. // JSON
  325. case ".json":
  326. w.Header().Add("Content-Type", "application/json;charset=utf-8")
  327. // Source code files for viewing
  328. case ".S", ".ada", ".asm", ".bash", ".bat", ".c", ".c++", ".cc", ".cl", ".clj", ".cpp", ".cs", ".cxx", ".el", ".elm", ".erl", ".fish", ".go", ".h", ".h++", ".hpp", ".hs", ".java", ".kt", ".lisp", ".mk", ".ml", ".pas", ".pl", ".py", ".r", ".rb", ".rs", ".scm", ".sh", ".ts", ".tsx":
  329. // Set headers for displaying it in the browser.
  330. w.Header().Set("Content-Type", "text/plain;charset=utf-8")
  331. // Common binary file extensions
  332. case ".7z", ".arj", ".bin", ".com", ".dat", ".db", ".elf", ".exe", ".gz", ".iso", ".lz", ".rar", ".tar.bz", ".tar.bz2", ".tar.gz", ".tar.xz", ".tbz", ".tbz2", ".tgz", ".txz", ".xz", ".zip":
  333. // Set headers for downloading the file instead of displaying it in the browser.
  334. w.Header().Set("Content-Disposition", "attachment")
  335. default:
  336. // If the filename starts with a ".", assume it's a plain text configuration file
  337. if strings.HasPrefix(filepath.Base(lowercaseFilename), ".") {
  338. w.Header().Set("Content-Type", "text/plain;charset=utf-8")
  339. } else {
  340. // Set the correct Content-Type
  341. if ac.mimereader != nil {
  342. ac.mimereader.SetHeader(w, ext)
  343. } else {
  344. log.Error("Uninitialized mimereader!")
  345. }
  346. }
  347. }
  348. // TODO Add support for "prettifying"/HTML-ifying some file extensions:
  349. // movies, music, source code etc. Wrap videos in the right html tags for playback, etc.
  350. // This should be placed in a separate Go module.
  351. // TODO: Modify ac.fs to also cache .Size(), .Name() and .ModTime()
  352. // Check the size of the file
  353. f, err := os.Open(filename)
  354. if err != nil {
  355. log.Error("Could not open " + filename + "! " + err.Error())
  356. return
  357. }
  358. defer f.Close()
  359. fInfo, err := f.Stat()
  360. if err != nil {
  361. log.Error("Could not stat " + filename + "! " + err.Error())
  362. return
  363. }
  364. // Check if the file is so large that it needs to be streamed directly
  365. fileSize := uint64(fInfo.Size())
  366. // Cache size can be set to a low number to trigger this behavior
  367. if fileSize > ac.largeFileSize {
  368. // log.Info("Streaming " + filename + " directly...")
  369. // http.ServeContent will first seek to the end of the file, then
  370. // serve the file. The alternative here is to use io.Copy(w, f),
  371. // but io.Copy does not support ranges.
  372. http.ServeContent(w, req, fInfo.Name(), fInfo.ModTime(), f)
  373. return
  374. }
  375. // Read the file (possibly in compressed format, straight from the cache)
  376. if dataBlock, err := ac.ReadAndLogErrors(w, filename, ext); err == nil { // if no error
  377. // Serve the file
  378. dataBlock.ToClient(w, req, filename, ac.ClientCanGzip(req), gzipThreshold)
  379. } else {
  380. log.Error("Could not serve " + filename + " with datablock.ToClient: " + err.Error())
  381. return
  382. }
  383. }
  384. // ServerHeaders sets the HTTP headers that are set before anything else
  385. func (ac *Config) ServerHeaders(w http.ResponseWriter) {
  386. w.Header().Set("Server", ac.serverHeaderName)
  387. if !ac.autoRefresh {
  388. w.Header().Set("X-XSS-Protection", "1; mode=block")
  389. w.Header().Set("X-Content-Type-Options", "nosniff")
  390. w.Header().Set("X-Frame-Options", "SAMEORIGIN")
  391. }
  392. if !ac.autoRefresh && ac.stricterHeaders {
  393. w.Header().Set("Content-Security-Policy",
  394. "connect-src 'self'; object-src 'self'; form-action 'self'")
  395. }
  396. // w.Header().Set("X-Powered-By", name+"/"+version)
  397. }
  398. // RegisterHandlers configures the given mutex and request limiter to handle
  399. // HTTP requests
  400. func (ac *Config) RegisterHandlers(mux *http.ServeMux, handlePath, servedir string, addDomain bool) {
  401. theme := ac.defaultTheme
  402. // Theme aliases. Use a map if there are more than 2 aliases in the future.
  403. if theme == "light" {
  404. // The "light" theme is the "gray" theme
  405. theme = "gray"
  406. }
  407. // Handle all requests with this function
  408. allRequests := func(w http.ResponseWriter, req *http.Request) {
  409. // Rejecting requests is handled by the permission system, which
  410. // in turn requires a database backend.
  411. if ac.perm != nil {
  412. if ac.perm.Rejected(w, req) {
  413. // Prepare to count bytes written
  414. sc := sheepcounter.New(w)
  415. // Get and call the Permission Denied function
  416. ac.perm.DenyFunction()(sc, req)
  417. // Log the response
  418. ac.LogAccess(req, http.StatusForbidden, sc.Counter())
  419. // Reject the request by just returning
  420. return
  421. }
  422. }
  423. // Local to this function
  424. servedir := servedir
  425. // Look for the directory that is named the same as the host
  426. if addDomain {
  427. servedir = filepath.Join(servedir, utils.GetDomain(req))
  428. }
  429. urlpath := req.URL.Path
  430. //log.Debugln("Checking reverse proxy", urlpath, ac.reverseProxyConfig)
  431. if ac.reverseProxyConfig != nil {
  432. if rproxy := ac.reverseProxyConfig.FindMatchingReverseProxy(urlpath); rproxy != nil {
  433. //log.Debugf("Querying reverse proxy %+v, %+v\n", rproxy, req)
  434. res, err := rproxy.DoProxyPass(*req)
  435. if err != nil {
  436. w.WriteHeader(http.StatusBadGateway)
  437. w.Write([]byte("reverse proxy error, please check your server config for AddReverseProxy calls\n"))
  438. return
  439. }
  440. data, err := io.ReadAll(res.Body)
  441. if err != nil {
  442. w.WriteHeader(http.StatusInternalServerError)
  443. return
  444. }
  445. res.Body.Close()
  446. for k, vals := range res.Header {
  447. for _, v := range vals {
  448. w.Header().Set(k, v)
  449. }
  450. }
  451. w.WriteHeader(res.StatusCode)
  452. w.Write(data)
  453. return
  454. }
  455. }
  456. filename := utils.URL2filename(servedir, urlpath)
  457. // Remove the trailing slash from the filename, if any
  458. noslash := filename
  459. if strings.HasSuffix(filename, utils.Pathsep) {
  460. noslash = filename[:len(filename)-1]
  461. }
  462. hasdir := ac.fs.Exists(filename) && ac.fs.IsDir(filename)
  463. dirname := filename
  464. hasfile := ac.fs.Exists(noslash)
  465. // Set the server headers, if not disabled
  466. if !ac.noHeaders {
  467. ac.ServerHeaders(w)
  468. }
  469. // Share the directory or file
  470. if hasdir {
  471. // Prepare to count bytes written
  472. sc := sheepcounter.New(w)
  473. // Get the directory page
  474. ac.DirPage(sc, req, servedir, dirname, theme)
  475. // Log the access
  476. ac.LogAccess(req, http.StatusOK, sc.Counter())
  477. return
  478. } else if !hasdir && hasfile {
  479. // Prepare to count bytes written
  480. sc := sheepcounter.New(w)
  481. // Share a single file instead of a directory
  482. ac.FilePage(sc, req, noslash, ac.defaultLuaDataFilename)
  483. // Log the access
  484. ac.LogAccess(req, http.StatusOK, sc.Counter())
  485. return
  486. }
  487. // Not found
  488. w.WriteHeader(http.StatusNotFound)
  489. data := themes.NoPage(filename, theme)
  490. ac.LogAccess(req, http.StatusNotFound, int64(len(data)))
  491. w.Write(data)
  492. }
  493. // Handle requests differently depending on rate limiting being enabled or not
  494. if ac.disableRateLimiting {
  495. mux.HandleFunc(handlePath, allRequests)
  496. } else {
  497. limiter := tollbooth.NewLimiter(float64(ac.limitRequests), nil)
  498. limiter.SetMessage(themes.MessagePage("Rate-limit exceeded", "<div style='color:red'>You have reached the maximum request limit.</div>", theme))
  499. limiter.SetMessageContentType("text/html;charset=utf-8")
  500. mux.Handle(handlePath, tollbooth.LimitFuncHandler(limiter, allRequests))
  501. }
  502. }