summaryrefslogtreecommitdiff
path: root/contrib/cgi/cgi.go
diff options
context:
space:
mode:
authortjpcc <tjp@ctrl-c.club>2023-01-28 14:52:35 -0700
committertjpcc <tjp@ctrl-c.club>2023-01-28 15:01:41 -0700
commit66a1b1f39a1e1d5499b548b36d18c8daa872d7da (patch)
tree96471dbd5486ede1a908790ac23e0c55b226dfad /contrib/cgi/cgi.go
parenta27b879accb191b6a6c6e76a6251ed751967f73a (diff)
gopher support.
Some of the contrib packages were originally built gemini-specific and had to be refactored into generic core functionality and thin protocol-specific wrappers for each of gemini and gopher.
Diffstat (limited to 'contrib/cgi/cgi.go')
-rw-r--r--contrib/cgi/cgi.go105
1 files changed, 46 insertions, 59 deletions
diff --git a/contrib/cgi/cgi.go b/contrib/cgi/cgi.go
index 71743a0..e57f2d0 100644
--- a/contrib/cgi/cgi.go
+++ b/contrib/cgi/cgi.go
@@ -6,7 +6,7 @@ import (
"crypto/sha256"
"encoding/hex"
"errors"
- "fmt"
+ "io"
"io/fs"
"net"
"os"
@@ -14,52 +14,45 @@ import (
"strings"
"tildegit.org/tjp/gus"
- "tildegit.org/tjp/gus/gemini"
)
-// CGIDirectory runs any executable files relative to a root directory on the file system.
+// ResolveCGI finds a CGI program corresponding to a request path.
//
-// 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.
-func CGIDirectory(pathRoot, fsRoot string) gus.Handler {
- fsRoot = strings.TrimRight(fsRoot, "/")
-
- return func(ctx context.Context, req *gus.Request) *gus.Response {
- if !strings.HasPrefix(req.Path, pathRoot) {
- return nil
+// 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
}
- path := req.Path[len(pathRoot):]
- segments := strings.Split(strings.TrimLeft(path, "/"), "/")
- for i := range append(segments, "") {
- path := strings.Join(append([]string{fsRoot}, segments[:i]...), "/")
- path = strings.TrimRight(path, "/")
- isDir, isExecutable, err := executableFile(path)
- if err != nil {
- return gemini.Failure(err)
- }
-
- if isExecutable {
- pathInfo := "/"
- if len(segments) > i+1 {
- pathInfo = strings.Join(segments[i:], "/")
- }
- return RunCGI(ctx, req, path, pathInfo)
- }
-
- if !isDir {
- break
+ if isExecutable {
+ pathinfo := "/"
+ if len(segments) > i+1 {
+ pathinfo = strings.Join(segments[i:], "/")
}
+ return filepath, pathinfo, nil
}
- return nil
+ if !isDir {
+ break
+ }
}
+
+ return "", "", nil
}
-func executableFile(path string) (bool, bool, error) {
- file, err := os.Open(path)
+func executableFile(filepath string) (bool, bool, error) {
+ file, err := os.Open(filepath)
if isNotExistError(err) {
return false, false, nil
}
@@ -98,10 +91,10 @@ func isNotExistError(err error) bool {
// RunCGI runs a specific program as a CGI script.
func RunCGI(
ctx context.Context,
- req *gus.Request,
+ request *gus.Request,
executable string,
pathInfo string,
-) *gus.Response {
+) (io.Reader, int, error) {
pathSegments := strings.Split(executable, "/")
dirPath := "."
@@ -115,40 +108,34 @@ func RunCGI(
infoLen -= 1
}
- scriptName := req.Path[:len(req.Path)-infoLen]
+ scriptName := request.Path[:len(request.Path)-infoLen]
scriptName = strings.TrimSuffix(scriptName, "/")
cmd := exec.CommandContext(ctx, "./"+basename)
- cmd.Env = prepareCGIEnv(ctx, req, scriptName, pathInfo)
+ cmd.Env = prepareCGIEnv(ctx, request, scriptName, pathInfo)
cmd.Dir = dirPath
responseBuffer := &bytes.Buffer{}
cmd.Stdout = responseBuffer
- if err := cmd.Run(); err != nil {
+ err := cmd.Run()
+ if err != nil {
var exErr *exec.ExitError
if errors.As(err, &exErr) {
- errMsg := fmt.Sprintf("CGI returned exit code %d", exErr.ExitCode())
- return gemini.CGIError(errMsg)
+ return responseBuffer, exErr.ExitCode(), nil
}
- return gemini.Failure(err)
- }
-
- response, err := gemini.ParseResponse(responseBuffer)
- if err != nil {
- return gemini.Failure(err)
}
- return response
+ return responseBuffer, cmd.ProcessState.ExitCode(), err
}
func prepareCGIEnv(
ctx context.Context,
- req *gus.Request,
+ request *gus.Request,
scriptName string,
pathInfo string,
) []string {
var authType string
- if len(req.TLSState.PeerCertificates) > 0 {
+ if request.TLSState != nil && len(request.TLSState.PeerCertificates) > 0 {
authType = "Certificate"
}
environ := []string{
@@ -158,10 +145,10 @@ func prepareCGIEnv(
"GATEWAY_INTERFACE=CGI/1.1",
"PATH_INFO=" + pathInfo,
"PATH_TRANSLATED=",
- "QUERY_STRING=" + req.RawQuery,
+ "QUERY_STRING=" + request.RawQuery,
}
- host, _, _ := net.SplitHostPort(req.RemoteAddr.String())
+ host, _, _ := net.SplitHostPort(request.RemoteAddr.String())
environ = append(environ, "REMOTE_ADDR="+host)
environ = append(
@@ -169,14 +156,14 @@ func prepareCGIEnv(
"REMOTE_HOST=",
"REMOTE_IDENT=",
"SCRIPT_NAME="+scriptName,
- "SERVER_NAME="+req.Server.Hostname(),
- "SERVER_PORT="+req.Server.Port(),
- "SERVER_PROTOCOL=GEMINI",
+ "SERVER_NAME="+request.Server.Hostname(),
+ "SERVER_PORT="+request.Server.Port(),
+ "SERVER_PROTOCOL="+request.Server.Protocol(),
"SERVER_SOFTWARE=GUS",
)
- if len(req.TLSState.PeerCertificates) > 0 {
- cert := req.TLSState.PeerCertificates[0]
+ if request.TLSState != nil && len(request.TLSState.PeerCertificates) > 0 {
+ cert := request.TLSState.PeerCertificates[0]
environ = append(
environ,
"TLS_CLIENT_HASH="+fingerprint(cert.Raw),