CGI support
This commit is contained in:
parent
6f87d2eb6b
commit
c34697e1cc
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
36
spsrv.go
36
spsrv.go
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue