CGI support

This commit is contained in:
Hedy Li 2021-08-02 12:45:48 +08:00
parent 6f87d2eb6b
commit c34697e1cc
Signed by: hedy
GPG Key ID: B51B5A8D1B176372
3 changed files with 147 additions and 3 deletions

View File

@ -18,6 +18,7 @@ type Config struct {
DirlistSort string
DirlistTitles bool
RestrictHostname string
CGIPaths []string
}
var defaultConf = &Config{
@ -31,6 +32,7 @@ var defaultConf = &Config{
UserDirEnable: true,
UserDir: "public_spartan",
RestrictHostname: "",
CGIPaths: []string{"cgi/"},
}
func LoadConfig(path string) (*Config, error) {

112
dynamic.go Normal file
View File

@ -0,0 +1,112 @@
package main
import (
"bufio"
"context"
"log"
"net"
"os"
"os/exec"
"os/user"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
)
func handleCGI(conf *Config, req *Request, cgiPath string) (ok bool){
ok = true
path := req.filePath
conn := req.conn
scriptPath := filepath.Join(conf.RootDir, req.filePath)
if req.user != "" {
scriptPath = filepath.Join("/home", req.user, conf.UserDir, req.filePath)
}
info, err := os.Stat(scriptPath)
if err != nil {
ok = false
return
}
if !(info.Mode().Perm()&0555 == 0555) {
ok = false
return
}
// Prepare environment variables
vars := prepareCGIVariables(conf, req, scriptPath)
log.Println("Running script:", scriptPath)
// Spawn process
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, scriptPath)
cmd.Env = []string{}
for key, value := range vars {
cmd.Env = append(cmd.Env, key+"="+value)
}
// Manually change the uid/gid for the command, rather than the calling goroutine
// Fetch user info
user, err := user.Lookup(req.user)
if err == nil {
tmp, _ := strconv.ParseUint(user.Uid, 10, 32)
uid := uint32(tmp)
tmp, _ = strconv.ParseUint(user.Gid, 10, 32)
gid := uint32(tmp)
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid}
}
// Fetch and check output
response, err := cmd.Output()
if ctx.Err() == context.DeadlineExceeded {
log.Println("Terminating CGI process " + path + " due to exceeding 10 second runtime limit.")
conn.Write([]byte("42 CGI process timed out!\r\n"))
return
}
if err != nil {
log.Println("Error running CGI program " + path + ": " + err.Error())
if err, ok := err.(*exec.ExitError); ok {
log.Println("↳ stderr output: " + string(err.Stderr))
}
conn.Write([]byte("42 CGI error\r\n"))
return
}
// Extract response header
header, _, err := bufio.NewReader(strings.NewReader(string(response))).ReadLine()
_, err2 := strconv.Atoi(strings.Fields(string(header))[0])
if err != nil || err2 != nil {
log.Println("Unable to parse first line of output from CGI process " + path + " as valid Gemini response header. Line was: " + string(header))
conn.Write([]byte("42 CGI error\r\n"))
return
}
log.Println("Returning CGI output")
// Write response
conn.Write(response)
return
}
func prepareCGIVariables(conf *Config, req *Request, script_path string) map[string]string {
vars := prepareGatewayVariables(conf, req)
vars["GATEWAY_INTERFACE"] = "CGI/1.1"
vars["SCRIPT_PATH"] = script_path
return vars
}
func prepareGatewayVariables(conf *Config, req *Request) map[string]string {
vars := make(map[string]string)
// vars["QUERY_STRING"] = URL.RawQuery
vars["REQUEST_METHOD"] = ""
vars["SERVER_NAME"] = conf.Hostname
vars["SERVER_PORT"] = strconv.Itoa(conf.Port)
vars["SERVER_PROTOCOL"] = "SPARTAN"
vars["SERVER_SOFTWARE"] = "SPSRV"
host, _, _ := net.SplitHostPort((*req.netConn).RemoteAddr().String())
vars["REMOTE_ADDR"] = host
return vars
}

View File

@ -17,6 +17,14 @@ import (
flag "github.com/spf13/pflag"
)
type Request struct {
conn io.ReadWriteCloser
netConn *net.Conn
user string
path string // Requested path
filePath string // Actual file path that does not include the content dir name
}
const (
statusSuccess = 2
statusRedirect = 3
@ -76,7 +84,8 @@ func serveSpartan(listener net.Listener, conf *Config) {
}
// handleConnection handles a request and does the response
func handleConnection(conn io.ReadWriteCloser, conf *Config) {
func handleConnection(netConn net.Conn, conf *Config) {
conn := io.ReadWriteCloser(netConn)
defer conn.Close()
// Check the size of the request buffer.
@ -114,15 +123,33 @@ func handleConnection(conn io.ReadWriteCloser, conf *Config) {
return
}
req := &Request{path: reqPath, netConn: &netConn, conn: conn}
// Time to fetch the files!
path := resolvePath(reqPath, conf)
path := resolvePath(reqPath, conf, req)
// Check for CGI
for _, cgiPath := range conf.CGIPaths {
if strings.HasPrefix(req.filePath, cgiPath) {
log.Println("Attempting CGI:", req.filePath)
ok := handleCGI(conf, req, cgiPath)
if ok {
log.Println("Closed connection")
return
}
break // CGI failed. just handle the request as if it's a static file.
}
}
serveFile(conn, reqPath, path, conf)
log.Println("Closed connection")
}
// resolvePath takes in teh request path and returns the cleaned filepath that needs to be fetched.
// It also handles user directories paths /~user/ and /~user if user directories is enabled in the config.
func resolvePath(reqPath string, conf *Config) (path string) {
func resolvePath(reqPath string, conf *Config, req *Request) (path string) {
// Handle tildes
if conf.UserDirEnable && strings.HasPrefix(reqPath, "/~") {
bits := strings.Split(reqPath, "/")
@ -136,7 +163,9 @@ func resolvePath(reqPath string, conf *Config) (path string) {
// /~user/ and have duplicate results, although in that case the search should handle
// omitting duplicates...
}
req.filePath = strings.TrimPrefix(filepath.Clean(strings.TrimPrefix(reqPath, "/~" + username)), "/")
new_prefix := filepath.Join("/home/", username, conf.UserDir)
req.user = username
path = filepath.Clean(strings.Replace(reqPath, bits[1], new_prefix, 1))
if strings.HasSuffix(reqPath, "/") {
path = filepath.Join(path, "index.gmi")
@ -148,6 +177,7 @@ func resolvePath(reqPath string, conf *Config) (path string) {
if strings.HasSuffix(reqPath, "/") || reqPath == "" {
path = filepath.Join(reqPath, "index.gmi")
}
req.filePath = filepath.Clean(strings.TrimPrefix(path, "/"))
path = filepath.Clean(filepath.Join(conf.RootDir, path))
return
}