2023-01-10 20:46:35 +00:00
|
|
|
package cgi
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/hex"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io/fs"
|
|
|
|
"net"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"strings"
|
|
|
|
|
2023-01-17 22:59:29 +00:00
|
|
|
"tildegit.org/tjp/gus"
|
2023-01-10 20:46:35 +00:00
|
|
|
"tildegit.org/tjp/gus/gemini"
|
|
|
|
)
|
|
|
|
|
2023-01-11 00:22:13 +00:00
|
|
|
// CGIDirectory runs any executable files relative to a root directory on the file system.
|
|
|
|
//
|
|
|
|
// It will also find and run any executables _part way_ through the path, so for example
|
|
|
|
// a request for /foo/bar/baz can also run an executable found at /foo or /foo/bar. In
|
|
|
|
// such a case the PATH_INFO environment variable will include the remaining portion of
|
|
|
|
// the URI path.
|
2023-01-17 22:59:29 +00:00
|
|
|
func CGIDirectory(pathRoot, fsRoot string) gus.Handler {
|
2023-01-11 00:22:13 +00:00
|
|
|
fsRoot = strings.TrimRight(fsRoot, "/")
|
2023-01-10 20:46:35 +00:00
|
|
|
|
2023-01-17 22:59:29 +00:00
|
|
|
return func(ctx context.Context, req *gus.Request) *gus.Response {
|
2023-01-11 00:22:13 +00:00
|
|
|
if !strings.HasPrefix(req.Path, pathRoot) {
|
2023-01-17 22:59:29 +00:00
|
|
|
return nil
|
2023-01-10 20:46:35 +00:00
|
|
|
}
|
|
|
|
|
2023-01-11 00:22:13 +00:00
|
|
|
path := req.Path[len(pathRoot):]
|
2023-01-10 20:46:35 +00:00
|
|
|
segments := strings.Split(strings.TrimLeft(path, "/"), "/")
|
|
|
|
for i := range append(segments, "") {
|
2023-01-11 00:22:13 +00:00
|
|
|
path := strings.Join(append([]string{fsRoot}, segments[:i]...), "/")
|
2023-01-10 20:46:35 +00:00
|
|
|
path = strings.TrimRight(path, "/")
|
|
|
|
isDir, isExecutable, err := executableFile(path)
|
|
|
|
if err != nil {
|
|
|
|
return gemini.Failure(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if isExecutable {
|
2023-01-11 00:22:13 +00:00
|
|
|
pathInfo := "/"
|
2023-01-10 20:46:35 +00:00
|
|
|
if len(segments) > i+1 {
|
2023-01-11 00:22:13 +00:00
|
|
|
pathInfo = strings.Join(segments[i:], "/")
|
2023-01-10 20:46:35 +00:00
|
|
|
}
|
2023-01-11 00:22:13 +00:00
|
|
|
return RunCGI(ctx, req, path, pathInfo)
|
2023-01-10 20:46:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if !isDir {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-17 22:59:29 +00:00
|
|
|
return nil
|
2023-01-10 20:46:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func executableFile(path string) (bool, bool, error) {
|
|
|
|
file, err := os.Open(path)
|
|
|
|
if isNotExistError(err) {
|
|
|
|
return false, false, nil
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return false, false, err
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
info, err := file.Stat()
|
|
|
|
if err != nil {
|
|
|
|
return false, false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if info.IsDir() {
|
|
|
|
return true, false, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// readable + executable by anyone
|
|
|
|
return false, info.Mode()&0005 == 0005, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func isNotExistError(err error) bool {
|
|
|
|
if err != nil {
|
|
|
|
var pathErr *fs.PathError
|
|
|
|
if errors.As(err, &pathErr) {
|
|
|
|
e := pathErr.Err
|
|
|
|
if errors.Is(e, fs.ErrInvalid) || errors.Is(e, fs.ErrNotExist) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2023-01-11 00:22:13 +00:00
|
|
|
// RunCGI runs a specific program as a CGI script.
|
|
|
|
func RunCGI(
|
2023-01-10 20:46:35 +00:00
|
|
|
ctx context.Context,
|
2023-01-17 22:59:29 +00:00
|
|
|
req *gus.Request,
|
2023-01-11 00:22:13 +00:00
|
|
|
executable string,
|
2023-01-10 20:46:35 +00:00
|
|
|
pathInfo string,
|
2023-01-17 22:59:29 +00:00
|
|
|
) *gus.Response {
|
2023-01-11 00:22:13 +00:00
|
|
|
pathSegments := strings.Split(executable, "/")
|
2023-01-10 20:46:35 +00:00
|
|
|
|
|
|
|
dirPath := "."
|
|
|
|
if len(pathSegments) > 1 {
|
|
|
|
dirPath = strings.Join(pathSegments[:len(pathSegments)-1], "/")
|
|
|
|
}
|
2023-01-11 00:22:13 +00:00
|
|
|
basename := pathSegments[len(pathSegments)-1]
|
2023-01-10 20:46:35 +00:00
|
|
|
|
2023-01-24 05:15:16 +00:00
|
|
|
infoLen := len(pathInfo)
|
|
|
|
if pathInfo == "/" {
|
|
|
|
infoLen -= 1
|
|
|
|
}
|
|
|
|
|
|
|
|
scriptName := req.Path[:len(req.Path)-infoLen]
|
2023-01-25 02:59:47 +00:00
|
|
|
scriptName = strings.TrimSuffix(scriptName, "/")
|
2023-01-11 00:22:13 +00:00
|
|
|
|
2023-01-11 00:50:44 +00:00
|
|
|
cmd := exec.CommandContext(ctx, "./"+basename)
|
2023-01-11 00:22:13 +00:00
|
|
|
cmd.Env = prepareCGIEnv(ctx, req, scriptName, pathInfo)
|
2023-01-10 20:46:35 +00:00
|
|
|
cmd.Dir = dirPath
|
|
|
|
|
|
|
|
responseBuffer := &bytes.Buffer{}
|
|
|
|
cmd.Stdout = responseBuffer
|
|
|
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
var exErr *exec.ExitError
|
|
|
|
if errors.As(err, &exErr) {
|
2023-01-11 00:22:13 +00:00
|
|
|
errMsg := fmt.Sprintf("CGI returned exit code %d", exErr.ExitCode())
|
|
|
|
return gemini.CGIError(errMsg)
|
2023-01-10 20:46:35 +00:00
|
|
|
}
|
|
|
|
return gemini.Failure(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
response, err := gemini.ParseResponse(responseBuffer)
|
|
|
|
if err != nil {
|
|
|
|
return gemini.Failure(err)
|
|
|
|
}
|
|
|
|
return response
|
|
|
|
}
|
|
|
|
|
|
|
|
func prepareCGIEnv(
|
|
|
|
ctx context.Context,
|
2023-01-17 22:59:29 +00:00
|
|
|
req *gus.Request,
|
2023-01-10 20:46:35 +00:00
|
|
|
scriptName string,
|
|
|
|
pathInfo string,
|
|
|
|
) []string {
|
|
|
|
var authType string
|
|
|
|
if len(req.TLSState.PeerCertificates) > 0 {
|
|
|
|
authType = "Certificate"
|
|
|
|
}
|
|
|
|
environ := []string{
|
|
|
|
"AUTH_TYPE=" + authType,
|
|
|
|
"CONTENT_LENGTH=",
|
|
|
|
"CONTENT_TYPE=",
|
|
|
|
"GATEWAY_INTERFACE=CGI/1.1",
|
|
|
|
"PATH_INFO=" + pathInfo,
|
|
|
|
"PATH_TRANSLATED=",
|
|
|
|
"QUERY_STRING=" + req.RawQuery,
|
|
|
|
}
|
|
|
|
|
|
|
|
host, _, _ := net.SplitHostPort(req.RemoteAddr.String())
|
|
|
|
environ = append(environ, "REMOTE_ADDR="+host)
|
|
|
|
|
|
|
|
environ = append(
|
|
|
|
environ,
|
|
|
|
"REMOTE_HOST=",
|
|
|
|
"REMOTE_IDENT=",
|
|
|
|
"SCRIPT_NAME="+scriptName,
|
2023-01-11 00:22:13 +00:00
|
|
|
"SERVER_NAME="+req.Server.Hostname(),
|
|
|
|
"SERVER_PORT="+req.Server.Port(),
|
2023-01-10 20:46:35 +00:00
|
|
|
"SERVER_PROTOCOL=GEMINI",
|
|
|
|
"SERVER_SOFTWARE=GUS",
|
|
|
|
)
|
|
|
|
|
|
|
|
if len(req.TLSState.PeerCertificates) > 0 {
|
|
|
|
cert := req.TLSState.PeerCertificates[0]
|
|
|
|
environ = append(
|
|
|
|
environ,
|
|
|
|
"TLS_CLIENT_HASH="+fingerprint(cert.Raw),
|
2023-01-20 17:58:35 +00:00
|
|
|
"TLS_CLIENT_CERT="+hex.EncodeToString(cert.Raw),
|
2023-01-11 00:22:13 +00:00
|
|
|
"TLS_CLIENT_ISSUER="+cert.Issuer.String(),
|
|
|
|
"TLS_CLIENT_ISSUER_CN="+cert.Issuer.CommonName,
|
|
|
|
"TLS_CLIENT_SUBJECT="+cert.Subject.String(),
|
|
|
|
"TLS_CLIENT_SUBJECT_CN="+cert.Subject.CommonName,
|
2023-01-10 20:46:35 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
return environ
|
|
|
|
}
|
|
|
|
|
|
|
|
func fingerprint(raw []byte) string {
|
|
|
|
hash := sha256.Sum256(raw)
|
|
|
|
return hex.EncodeToString(hash[:])
|
|
|
|
}
|