172 lines
3.8 KiB
Go
172 lines
3.8 KiB
Go
package cgi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
sr "tildegit.org/tjp/sliderule"
|
|
)
|
|
|
|
// ResolveCGI finds a CGI program corresponding to a request path.
|
|
//
|
|
// It returns the path to the executable file and the PATH_INFO that should be passed,
|
|
// or an error.
|
|
//
|
|
// It will find executables which are just part way through the path, so for example
|
|
// a request for /foo/bar/baz can run an executable found at /foo or /foo/bar. In such
|
|
// a case the PATH_INFO would include the remaining portion of the URI path.
|
|
func ResolveCGI(requestpath, fsroot string) (string, string, error) {
|
|
segments := append([]string{""}, strings.Split(requestpath, "/")...)
|
|
|
|
fullpath := fsroot
|
|
for i, segment := range segments {
|
|
fullpath = filepath.Join(fullpath, segment)
|
|
|
|
info, err := os.Stat(fullpath)
|
|
if isNotExistError(err) {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
if info.IsDir() {
|
|
continue
|
|
}
|
|
|
|
if info.Mode()&5 != 5 {
|
|
break
|
|
}
|
|
|
|
pathinfo := "/"
|
|
if len(segments) > i+1 {
|
|
pathinfo = path.Join(segments[i:]...)
|
|
}
|
|
return fullpath, pathinfo, nil
|
|
}
|
|
|
|
return "", "", 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
|
|
}
|
|
|
|
// RunCGI runs a specific program as a CGI script.
|
|
func RunCGI(
|
|
ctx context.Context,
|
|
request *sr.Request,
|
|
executable string,
|
|
pathInfo string,
|
|
workdir string,
|
|
stderr io.Writer,
|
|
) (*bytes.Buffer, int, error) {
|
|
infoLen := len(pathInfo)
|
|
if pathInfo == "/" {
|
|
infoLen = 0
|
|
}
|
|
|
|
scriptName := request.Path[:len(request.Path)-infoLen]
|
|
scriptName = strings.TrimSuffix(scriptName, "/")
|
|
|
|
execpath, err := filepath.Abs(executable)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, execpath)
|
|
cmd.Env = prepareCGIEnv(ctx, request, scriptName, pathInfo)
|
|
cmd.Dir = workdir
|
|
|
|
if body, ok := request.Meta.(io.Reader); ok {
|
|
cmd.Stdin = body
|
|
}
|
|
responseBuffer := &bytes.Buffer{}
|
|
cmd.Stdout = responseBuffer
|
|
cmd.Stderr = stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
var exErr *exec.ExitError
|
|
if errors.As(err, &exErr) {
|
|
return responseBuffer, exErr.ExitCode(), nil
|
|
}
|
|
}
|
|
return responseBuffer, cmd.ProcessState.ExitCode(), err
|
|
}
|
|
|
|
func prepareCGIEnv(
|
|
ctx context.Context,
|
|
request *sr.Request,
|
|
scriptName string,
|
|
pathInfo string,
|
|
) []string {
|
|
var authType string
|
|
if request.TLSState != nil && len(request.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=" + request.RawQuery,
|
|
}
|
|
|
|
host, port, _ := net.SplitHostPort(request.RemoteAddr.String())
|
|
environ = append(environ, "REMOTE_ADDR="+host, "REMOTE_PORT="+port)
|
|
|
|
environ = append(
|
|
environ,
|
|
"REMOTE_HOST=",
|
|
"REMOTE_IDENT=",
|
|
"SCRIPT_NAME="+scriptName,
|
|
"SERVER_NAME="+request.Server.Hostname(),
|
|
"SERVER_PORT="+request.Server.Port(),
|
|
"SERVER_PROTOCOL="+request.Server.Protocol(),
|
|
"SERVER_SOFTWARE=SLIDERULE",
|
|
)
|
|
|
|
if request.TLSState != nil && len(request.TLSState.PeerCertificates) > 0 {
|
|
cert := request.TLSState.PeerCertificates[0]
|
|
environ = append(
|
|
environ,
|
|
"TLS_CLIENT_HASH="+fingerprint(cert.Raw),
|
|
"TLS_CLIENT_CERT="+hex.EncodeToString(cert.Raw),
|
|
"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,
|
|
)
|
|
}
|
|
|
|
return environ
|
|
}
|
|
|
|
func fingerprint(raw []byte) string {
|
|
hash := sha256.Sum256(raw)
|
|
return hex.EncodeToString(hash[:])
|
|
}
|