spsrv/dynamic.go

131 lines
3.3 KiB
Go

package main
import (
"bufio"
"context"
"io"
"log"
"net"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"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 {
log.Println(err.Error())
ok = false
return
}
if !(info.Mode().Perm()&0555 == 0555) {
log.Println("File not executable")
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)
// Put input data into stdin pipe
stdin, err := cmd.StdinPipe()
if err != nil {
log.Println("Error creating a stdin pipe:", err.Error())
ok = false
return
}
io.WriteString(stdin, req.data)
stdin.Close()
// Set environment variables
cmd.Env = []string{}
for key, value := range vars {
cmd.Env = append(cmd.Env, key+"="+value)
}
// Manually change the uid/gid for the command
// 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("5 CGI process timed out!\r\n"))
return
}
if err != nil {
log.Println("Error running CGI program " + path + ": " + err.Error())
if strings.Contains(err.Error(), "permission denied") {
ok = false
return
}
if err, ok := err.(*exec.ExitError); ok {
log.Println("↳ stderr output: " + string(err.Stderr))
}
conn.Write([]byte("5 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("5 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["REQUEST_METHOD"] = ""
vars["SERVER_NAME"] = conf.Hostname
vars["SERVER_PORT"] = strconv.Itoa(conf.Port)
vars["SERVER_PROTOCOL"] = "SPARTAN"
vars["SERVER_SOFTWARE"] = "SPSRV"
vars["DATA_LENGTH"] = strconv.Itoa(req.dataLen)
host, _, _ := net.SplitHostPort((*req.netConn).RemoteAddr().String())
vars["REMOTE_ADDR"] = host
return vars
}