package cgi import ( "bytes" "context" "crypto/sha256" "encoding/hex" "errors" "io" "io/fs" "net" "os" "os/exec" "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 := strings.Split(strings.TrimLeft(requestPath, "/"), "/") for i := range append(segments, "") { filepath := strings.Join(append([]string{fsRoot}, segments[:i]...), "/") filepath = strings.TrimRight(filepath, "/") isDir, isExecutable, err := executableFile(filepath) if err != nil { return "", "", err } if isExecutable { pathinfo := "/" if len(segments) > i+1 { pathinfo = strings.Join(segments[i:], "/") } return filepath, pathinfo, nil } if !isDir { break } } return "", "", nil } func executableFile(filepath string) (bool, bool, error) { file, err := os.Open(filepath) 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 } // RunCGI runs a specific program as a CGI script. func RunCGI( ctx context.Context, request *sr.Request, executable string, pathInfo string, ) (io.Reader, int, error) { pathSegments := strings.Split(executable, "/") dirPath := "." if len(pathSegments) > 1 { dirPath = strings.Join(pathSegments[:len(pathSegments)-1], "/") } basename := pathSegments[len(pathSegments)-1] infoLen := len(pathInfo) if pathInfo == "/" { infoLen -= 1 } scriptName := request.Path[:len(request.Path)-infoLen] scriptName = strings.TrimSuffix(scriptName, "/") cmd := exec.CommandContext(ctx, "./"+basename) cmd.Env = prepareCGIEnv(ctx, request, scriptName, pathInfo) cmd.Dir = dirPath if body, ok := request.Meta.(io.Reader); ok { cmd.Stdin = body } responseBuffer := &bytes.Buffer{} cmd.Stdout = responseBuffer err := cmd.Run() if 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, _, _ := net.SplitHostPort(request.RemoteAddr.String()) environ = append(environ, "REMOTE_ADDR="+host) 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[:]) }