Ctrl K
ComunidadGuías…

engine/basic.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package engine

import (
	"bytes"
	"encoding/json"
	"fmt"
	"html"
	"io"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

	"github.com/gomarkdown/markdown"
	"github.com/gomarkdown/markdown/parser"
	"github.com/microcosm-cc/bluemonday"
	"github.com/sirupsen/logrus"
	"github.com/xyproto/algernon/lua/convert"
	"github.com/xyproto/algernon/lua/run3"
	"github.com/xyproto/algernon/utils"
	lua "github.com/xyproto/gopher-lua"
	"github.com/xyproto/splash"
)

// FutureStatus is useful when redirecting in combination with writing to a
// buffer before writing to a client. May contain more fields in the future.
type FutureStatus struct {
	code   int  // Buffered HTTP status code
	closed bool // The server has stopped writing output to this client
}

// LoadBasicSystemFunctions loads functions related to logging, markdown and the
// current server directory into the given Lua state
func (ac *Config) LoadBasicSystemFunctions(L *lua.LState) {
	// Return the version string
	L.SetGlobal("version", L.NewFunction(func(L *lua.LState) int {
		L.Push(lua.LString(ac.versionString))
		return 1 // number of results
	}))

	// Log text with the "Info" log type
	L.SetGlobal("log", L.NewFunction(func(L *lua.LState) int {
		buf := convert.Arguments2buffer(L, false)
		// Log the combined text
		logrus.Info(buf.String())
		return 0 // number of results
	}))

	// Log text with the "Warn" log type
	L.SetGlobal("warn", L.NewFunction(func(L *lua.LState) int {
		buf := convert.Arguments2buffer(L, false)
		// Log the combined text
		logrus.Warn(buf.String())
		return 0 // number of results
	}))

	// Log text with the "Error" log type
	L.SetGlobal("err", L.NewFunction(func(L *lua.LState) int {
		buf := convert.Arguments2buffer(L, false)
		// Log the combined text
		logrus.Error(buf.String())
		return 0 // number of results
	}))

	// Sleep for the given number of seconds (can be a float)
	L.SetGlobal("sleep", L.NewFunction(func(L *lua.LState) int {
		// Extract the correct number of nanoseconds
		duration := time.Duration(float64(L.ToNumber(1)) * 1000000000.0)
		// Wait and block the current thread of execution.
		time.Sleep(duration)
		return 0
	}))

	// Return the current unixtime, with an attempt at nanosecond resolution
	L.SetGlobal("unixnano", L.NewFunction(func(L *lua.LState) int {
		// Extract the correct number of nanoseconds
		L.Push(lua.LNumber(time.Now().UnixNano()))
		return 1 // number of results
	}))

	// Convert Markdown to HTML
	L.SetGlobal("markdown", L.NewFunction(func(L *lua.LState) int {
		// Retrieve all the function arguments as a bytes.Buffer
		buf := convert.Arguments2buffer(L, true)
		// Create a Markdown parser with the desired extensions
		mdParser := parser.NewWithExtensions(enabledMarkdownExtensions)
		mdContent := buf.Bytes()
		// Convert the buffer to markdown
		htmlData := markdown.ToHTML(mdContent, mdParser, nil)

		// Apply syntax highlighting
		if highlightedHTML, err := splash.Splash(htmlData, markdownCodeStyle); err == nil { // success
			htmlData = highlightedHTML
		}

		// Add a script for rendering MathJax, but only if at least one mathematical formula is present,
		// and if it hasn't been added already.
		if containsFormula(mdContent) {
			js := append([]byte(`<script id="MathJax-script">`), []byte(mathJaxScript)...)
			htmlData = InsertScriptTag(htmlData, js) // also adds the closing </script> tag
		}

		htmlString := strings.TrimSpace(string(htmlData))
		L.Push(lua.LString(htmlString))
		return 1 // number of results
	}))

	// Sanitize HTML
	L.SetGlobal("sanhtml", L.NewFunction(func(L *lua.LState) int {
		// Retrieve the HTML content to be sanitized
		htmlContent := L.ToString(1)
		// Sanitize the HTML content
		policy := bluemonday.UGCPolicy()
		sanitizedHTML := policy.Sanitize(htmlContent)
		// Return the sanitized HTML
		L.Push(lua.LString(sanitizedHTML))
		return 1 // number of results
	}))

	// Escape HTML special characters, for including text in an HTML page
	L.SetGlobal("htmlescape", L.NewFunction(func(L *lua.LState) int {
		L.Push(lua.LString(html.EscapeString(L.ToString(1))))
		return 1 // number of results
	}))

	// Get the full filename of a given file that is in the directory
	// where the server is running (root directory for the server).
	// If no filename is given, the directory where the server is
	// currently running is returned.
	L.SetGlobal("serverdir", L.NewFunction(func(L *lua.LState) int {
		serverdir, err := os.Getwd()
		if err != nil {
			// Could not retrieve a directory
			serverdir = ""
		} else if L.GetTop() == 1 {
			// Also include a separator and a filename
			fn := L.ToString(1)
			serverdir = filepath.Join(serverdir, fn)
		}
		L.Push(lua.LString(serverdir))
		return 1 // number of results
	}))

	// run3 returns stdout and stderr as tables of lines, and also the exit code
	L.SetGlobal("run3", L.NewFunction(func(L *lua.LState) int {
		command := L.ToString(1)
		workingDir, err := os.Getwd()
		if err != nil {
			workingDir = ""
		}
		return run3.ShellHelper(L, command, workingDir)
	}))
}

// LoadBasicWeb loads functions related to handling requests, outputting data to
// the browser, setting headers, pretty printing and dealing with the directory
// where files are being served, into the given Lua state.
func (ac *Config) LoadBasicWeb(w http.ResponseWriter, req *http.Request, L *lua.LState, filename string, flushFunc func(), httpStatus *FutureStatus) {

	// Always have a place to track whether output has been closed for this request
	if httpStatus == nil {
		httpStatus = &FutureStatus{}
	}

	// Print the given arguments to the web page that is being served. Add a newline.
	L.SetGlobal("print", L.NewFunction(func(L *lua.LState) int {
		if httpStatus.closed {
			return 0 // number of results
		}

		var buf bytes.Buffer
		top := L.GetTop()
		for i := 1; i <= top; i++ {
			buf.WriteString(L.Get(i).String())
			if i != top {
				buf.WriteString("\t")
			}
		}
		// Final newline
		buf.WriteString("\n")
		// Write the combined text to the http.ResponseWriter
		if _, err := buf.WriteTo(w); err != nil && ac.debugMode {
			logrus.Error("print: could not write to buffer: " + err.Error())
		}
		return 0 // number of results
	}))

	// Print the given arguments to the web page that is being served. Do not add a newline.
	L.SetGlobal("print_nonl", L.NewFunction(func(L *lua.LState) int {
		if httpStatus.closed {
			return 0 // number of results
		}

		var buf bytes.Buffer
		top := L.GetTop()
		for i := 1; i <= top; i++ {
			buf.WriteString(L.Get(i).String())
			if i != top {
				buf.WriteString("\t")
			}
		}
		// Write the combined text to the http.ResponseWriter
		if _, err := buf.WriteTo(w); err != nil && ac.debugMode {
			logrus.Error("print_nonl: could not write to buffer: " + err.Error())
		}
		return 0 // number of results
	}))

	// Pretty print text to the web page that is being served. Add a newline.
	L.SetGlobal("pprint", L.NewFunction(func(L *lua.LState) int {
		if httpStatus.closed {
			return 0 // number of results
		}

		var buf bytes.Buffer
		top := L.GetTop()
		for i := 1; i <= top; i++ {
			convert.PprintToWriter(&buf, L.Get(i))
			if i != top {
				buf.WriteString("\t")
			}
		}

		// Final newline
		buf.WriteString("\n")

		// Write the combined text to the http.ResponseWriter
		if _, err := buf.WriteTo(w); err != nil && ac.debugMode {
			logrus.Error("pprint: could not write to buffer: " + err.Error())
		}

		return 0 // number of results
	}))

	// Pretty print to string
	L.SetGlobal("ppstr", L.NewFunction(func(L *lua.LState) int {
		var buf bytes.Buffer
		top := L.GetTop()
		for i := 1; i <= top; i++ {
			convert.PprintToWriter(&buf, L.Get(i))
			if i != top {
				buf.WriteString("\t")
			}
		}

		// Return the string
		L.Push(lua.LString(buf.String()))

		return 1 // number of results
	}))

	// Flush the ResponseWriter.
	// Needed in debug mode, where ResponseWriter is buffered.
	L.SetGlobal("flush", L.NewFunction(func(_ *lua.LState) int {
		if httpStatus.closed {
			L.Push(lua.LBool(false)) // not a success, the connection has been closed
			return 1                 // number of results
		}
		if flushFunc == nil {
			logrus.Warn("cannot flush(): flushFunc is nil")
			L.Push(lua.LBool(true)) // not a failure, ignore the missing flashFunc
			return 1                // number of results
		}
		flushFunc()
		L.Push(lua.LBool(true)) // success
		return 1                // number of results
	}))

	// Close the communication with the client by setting a "Connection: close" header,
	// flushing and marking the output as closed.
	L.SetGlobal("close", L.NewFunction(func(_ *lua.LState) int {
		// Close the connection.
		// Works for both HTTP and HTTP/2 now, ref: https://github.com/golang/go/issues/20977
		w.Header().Add("Connection", "close")

		// Flush, if a flush function is available
		if flushFunc != nil {
			// TODO: If flush() was called, and no further writing to w was done by now, then don't flush twice
			flushFunc()
		}

		// Stop Lua functions from writing more to this client
		httpStatus.closed = true

		// TODO: Set up the HTTP/QUIC/HTTP/2 Server structs with a ConnContext
		//       field and then fetch the connection from the req.Context()
		//       and use it here for closing the connection.

		// Hijack the connection (does not work with fasthttp!)
		//if hijacker, ok := w.(http.Hijacker); ok { // success
		//  if conn, buf, err := hijacker.Hijack(); err == nil { // success
		//    buf.Flush()
		//    conn.Close() // Close the hijacked connection
		//  }
		//}

		return 0 // number of results
	}))

	// Set the Content-Type for the page
	L.SetGlobal("content", L.NewFunction(func(L *lua.LState) int {
		if httpStatus.closed {
			return 0 // number of results
		}

		lv := L.ToString(1)
		w.Header().Add(contentType, lv)
		return 0 // number of results
	}))

	// Return the current URL Path
	L.SetGlobal("urlpath", L.NewFunction(func(L *lua.LState) int {
		L.Push(lua.LString(req.URL.Path))
		return 1 // number of results
	}))

	// Return the current HTTP method (GET, POST etc)
	L.SetGlobal("method", L.NewFunction(func(L *lua.LState) int {
		L.Push(lua.LString(req.Method))
		return 1 // number of results
	}))

	// Return the remote address ("host:port") of the connected client, see issue #114
	L.SetGlobal("remoteaddr", L.NewFunction(func(L *lua.LState) int {
		L.Push(lua.LString(req.RemoteAddr))
		return 1 // number of results
	}))

	// Return the HTTP headers as a table
	L.SetGlobal("headers", L.NewFunction(func(L *lua.LState) int {
		luaTable := L.NewTable()
		for key := range req.Header {
			L.RawSet(luaTable, lua.LString(key), lua.LString(req.Header.Get(key)))
		}
		if req.Host != "" {
			L.RawSet(luaTable, lua.LString("Host"), lua.LString(req.Host))
		}
		L.Push(luaTable)
		return 1 // number of results
	}))

	// Return the HTTP header in the request, for a given key/string
	L.SetGlobal("header", L.NewFunction(func(L *lua.LState) int {
		key := L.ToString(1)
		value := req.Header.Get(key)
		L.Push(lua.LString(value))
		return 1 // number of results
	}))

	// Set the HTTP header in the request, for a given key and value
	L.SetGlobal("setheader", L.NewFunction(func(L *lua.LState) int {
		if httpStatus.closed {
			return 0 // number of results
		}

		key := L.ToString(1)
		value := L.ToString(2)
		w.Header().Set(key, value)
		return 0 // number of results
	}))

	// Return the HTTP body in the request
	L.SetGlobal("body", L.NewFunction(func(L *lua.LState) int {
		body, err := io.ReadAll(req.Body)
		var result lua.LString
		if err != nil {
			result = lua.LString("")
		} else {
			result = lua.LString(string(body))
		}
		L.Push(result)
		return 1 // number of results
	}))

	// Set the HTTP status code (must come before print)
	L.SetGlobal("status", L.NewFunction(func(L *lua.LState) int {
		if httpStatus.closed {
			return 0 // number of results
		}

		code := int(L.ToNumber(1))
		if httpStatus != nil {
			httpStatus.code = code
		}
		w.WriteHeader(code)
		return 0 // number of results
	}))

	// Throw an error/exception in Lua
	L.SetGlobal("throw", L.GetGlobal("error"))

	// Set a HTTP status code and print a message (optional)
	L.SetGlobal("error", L.NewFunction(func(L *lua.LState) int {
		if httpStatus.closed {
			return 0 // number of results
		}

		code := int(L.ToNumber(1))
		if httpStatus != nil {
			httpStatus.code = code
		}
		w.WriteHeader(code)
		if L.GetTop() == 2 {
			message := L.ToString(2)
			fmt.Fprint(w, message)
		}
		return 0 // number of results
	}))

	// Get the full filename of a given file that is in the directory
	// of the script that is about to be run. If no filename is given,
	// the directory of the script is returned.
	L.SetGlobal("scriptdir", L.NewFunction(func(L *lua.LState) int {
		scriptpath, err := filepath.Abs(filename)
		if err != nil {
			scriptpath = filename
		}
		scriptdir := filepath.Dir(scriptpath)
		scriptpath = scriptdir
		top := L.GetTop()
		if top == 1 {
			// Also include a separator and a filename
			fn := L.ToString(1)
			scriptpath = filepath.Join(scriptdir, fn)
		}
		// Now have the correct absolute scriptpath
		L.Push(lua.LString(scriptpath))
		return 1 // number of results
	}))

	// Given a glob, like "md/*.md", read the files with the scriptdir() as the starting point.
	// Then return all the contents of the files as a table.
	L.SetGlobal("readglob", L.NewFunction(func(L *lua.LState) int {
		pattern := L.ToString(1)
		var basepath string
		if L.GetTop() == 2 {
			basepath = L.ToString(2)
		} else {
			scriptpath, err := filepath.Abs(filename)
			if err != nil {
				scriptpath = filename
			}
			basepath = filepath.Dir(scriptpath)
		}
		matches, err := filepath.Glob(filepath.Join(basepath, pattern))
		if err != nil {
			L.Push(lua.LNil)
			return 1
		}
		results := L.NewTable()
		for _, match := range matches {
			content, err := os.ReadFile(match)
			if err != nil {
				L.Push(lua.LNil)
				return 1
			}
			results.Append(lua.LString(content))
		}
		L.Push(results)
		return 1
	}))

	// Given a filename, return the URL path
	L.SetGlobal("file2url", L.NewFunction(func(L *lua.LState) int {
		fn := L.ToString(1)
		targetpath := strings.TrimPrefix(filepath.Join(filepath.Dir(filename), fn), ac.serverDirOrFilename)
		if utils.Pathsep != "/" {
			// For operating systems that use another path separator for files than for URLs
			targetpath = strings.ReplaceAll(targetpath, utils.Pathsep, "/")
		}
		withSlashPrefix := path.Join("/", targetpath)
		L.Push(lua.LString(withSlashPrefix))
		return 1 // number of results
	}))

	// Retrieve a table with keys and values from the form in the request
	L.SetGlobal("formdata", L.NewFunction(func(L *lua.LState) int {
		// Place the form data in a map
		m := make(map[string]string)
		req.ParseForm()
		for key, values := range req.Form {
			m[key] = values[0]
		}
		// Convert the map to a table and return it
		L.Push(convert.Map2table(L, m))
		return 1 // number of results
	}))

	// Read a JSON request body and return a table with the decoded fields
	L.SetGlobal("formjson", L.NewFunction(func(L *lua.LState) int {
		body, err := io.ReadAll(req.Body)
		if err != nil {
			L.Push(L.NewTable())
			return 1 // number of results
		}
		m := make(map[string]any)
		if err := json.Unmarshal(body, &m); err != nil {
			L.Push(L.NewTable())
			return 1 // number of results
		}
		table := L.NewTable()
		for key, value := range m {
			L.RawSet(table, lua.LString(key), lua.LString(fmt.Sprintf("%v", value)))
		}
		L.Push(table)
		return 1 // number of results
	}))

	// Retrieve a table with keys and values from the URL in the request
	L.SetGlobal("urldata", L.NewFunction(func(L *lua.LState) int {
		var (
			valueMap url.Values
			err      error
		)
		if L.GetTop() == 1 {
			// If given an argument
			rawurl := L.ToString(1)
			valueMap, err = url.ParseQuery(rawurl)
			// Log error as warning if there are issues.
			// An empty Value map will then be used.
			if err != nil {
				logrus.Error(err)
				// return 0
			}
		} else {
			// If not given an argument
			valueMap = req.URL.Query() // map[string][]string
		}

		// Place the Value data in a map, using the first values
		// if there are many values for a given key.
		m := make(map[string]string)
		for key, values := range valueMap {
			m[key] = values[0]
		}
		// Convert the map to a table and return it
		L.Push(convert.Map2table(L, m))
		return 1 // number of results
	}))

	// Redirect a request (as found, by default)
	L.SetGlobal("redirect", L.NewFunction(func(L *lua.LState) int {
		if httpStatus.closed {
			L.Push(lua.LBool(false)) // can not redirect, since the connection is closed
			return 1                 // number of results
		}

		newurl := L.ToString(1)
		httpStatusCode := http.StatusFound
		if L.GetTop() == 2 {
			httpStatusCode = int(L.ToNumber(2))
		}
		if httpStatus != nil {
			httpStatus.code = httpStatusCode
		}
		http.Redirect(w, req, newurl, httpStatusCode)
		L.Push(lua.LBool(true)) // success
		return 1                // number of results
	}))

	// Permanently redirect a request, which is the same as redirect(url, 301)
	L.SetGlobal("permanent_redirect", L.NewFunction(func(L *lua.LState) int {
		if httpStatus.closed {
			L.Push(lua.LBool(false)) // not a success, the connection has been closed
			return 1                 // number of results
		}

		newurl := L.ToString(1)
		httpStatusCode := http.StatusMovedPermanently
		if httpStatus != nil {
			httpStatus.code = httpStatusCode
		}
		http.Redirect(w, req, newurl, httpStatusCode)
		L.Push(lua.LBool(true)) // success
		return 1                // number of results
	}))

	// Run the given Lua file (replacement for the built-in dofile, to look in the right directory)
	// Returns whatever the Lua file returns when it is being run.
	L.SetGlobal("dofile", L.NewFunction(func(L *lua.LState) int {
		givenFilename := L.ToString(1)
		luaFilename := filepath.Join(filepath.Dir(filename), givenFilename)
		if !ac.fs.Exists(luaFilename) {
			logrus.Error("Could not find:", luaFilename)
			return 0 // number of results
		}
		if err := L.DoFile(luaFilename); err != nil {
			logrus.Errorf("Error running %s: %s\n", luaFilename, err)
			return 0 // number of results
		}
		// Retrieve the returned value from the script
		retval := L.Get(-1)
		L.Pop(1)
		// Return the value returned from the script
		L.Push(retval)
		return 1 // number of results
	}))

	// run3 returns stdout and stderr as tables of lines, and also the exit code
	L.SetGlobal("run3", L.NewFunction(func(L *lua.LState) int {
		command := L.ToString(1)
		workingDir := filepath.Dir(filename)
		return run3.ShellHelper(L, command, workingDir)
	}))

	// Truncate a string to at most n bytes
	L.SetGlobal("maxlen", L.NewFunction(func(L *lua.LState) int {
		s := L.ToString(1)
		n := max(int(L.ToNumber(2)), 0)
		if len(s) > n {
			s = s[:n]
		}
		L.Push(lua.LString(s))
		return 1 // number of results
	}))

	// Return true if the request method is POST, set JSON content type,
	// and send 405 Method Not Allowed for methods other than GET and POST.
	L.SetGlobal("methodPOST", L.NewFunction(func(L *lua.LState) int {
		switch req.Method {
		case "POST":
			w.Header().Set(contentType, "application/json; charset=utf-8")
			L.Push(lua.LBool(true))
		case "GET", "HEAD":
			L.Push(lua.LBool(false))
		default:
			w.Header().Set("Allow", "GET, POST")
			http.Error(w, "405 method not allowed", http.StatusMethodNotAllowed)
			L.Push(lua.LBool(false))
		}
		return 1 // number of results
	}))

	// Embed a raw JSON value within a JSON object.
	// jsonembed(key, rawjson) returns {"key":rawjson}
	L.SetGlobal("jsonembed", L.NewFunction(func(L *lua.LState) int {
		key := L.ToString(1)
		raw := L.ToString(2)
		keyJSON, err := json.Marshal(key)
		if err != nil {
			L.Push(lua.LString("{}"))
			return 1 // number of results
		}
		L.Push(lua.LString("{" + string(keyJSON) + ":" + raw + "}"))
		return 1 // number of results
	}))

}