diff options
author | tjpcc <tjp@ctrl-c.club> | 2023-01-10 17:22:13 -0700 |
---|---|---|
committer | tjpcc <tjp@ctrl-c.club> | 2023-01-10 17:22:13 -0700 |
commit | 474a28663f2871c91ea758c1fa4e0cf9bc7326a5 (patch) | |
tree | a741bea4e5c94237af2e2c7e2f117e770e8635b8 | |
parent | 96f3a7607ffbdb349a4c2eff35efdf11b8d35a4e (diff) |
CGI improvements
-rw-r--r-- | contrib/cgi/cgi.go | 56 | ||||
-rwxr-xr-x | examples/cgi/cgi-bin/cowsay | 3 | ||||
-rwxr-xr-x | examples/cgi/cgi-bin/environ | 8 | ||||
-rw-r--r-- | examples/cgi/main.go | 2 |
4 files changed, 46 insertions, 23 deletions
diff --git a/contrib/cgi/cgi.go b/contrib/cgi/cgi.go index 2e20485..b5dfbdd 100644 --- a/contrib/cgi/cgi.go +++ b/contrib/cgi/cgi.go @@ -16,18 +16,24 @@ import ( "tildegit.org/tjp/gus/gemini" ) -func CGIHandler(pathPrefix, rootDir string) gemini.Handler { - rootDir = strings.TrimRight(rootDir, "/") +// 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. +func CGIDirectory(pathRoot, fsRoot string) gemini.Handler { + fsRoot = strings.TrimRight(fsRoot, "/") return func(ctx context.Context, req *gemini.Request) *gemini.Response { - if !strings.HasPrefix(req.Path, pathPrefix) { + if !strings.HasPrefix(req.Path, pathRoot) { return gemini.NotFound("Resource does not exist.") } - path := req.Path[len(pathPrefix):] + path := req.Path[len(pathRoot):] segments := strings.Split(strings.TrimLeft(path, "/"), "/") for i := range append(segments, "") { - path := strings.Join(append([]string{rootDir}, segments[:i]...), "/") + path := strings.Join(append([]string{fsRoot}, segments[:i]...), "/") path = strings.TrimRight(path, "/") isDir, isExecutable, err := executableFile(path) if err != nil { @@ -35,11 +41,11 @@ func CGIHandler(pathPrefix, rootDir string) gemini.Handler { } if isExecutable { - pathInfo := "" + pathInfo := "/" if len(segments) > i+1 { - pathInfo = strings.Join(segments[i+1:], "/") + pathInfo = strings.Join(segments[i:], "/") } - return runCGI(ctx, req.Server, req, path, pathInfo) + return RunCGI(ctx, req, path, pathInfo) } if !isDir { @@ -88,38 +94,46 @@ func isNotExistError(err error) bool { return false } -func runCGI( +// RunCGI runs a specific program as a CGI script. +func RunCGI( ctx context.Context, - server *gemini.Server, req *gemini.Request, - filePath string, + executable string, pathInfo string, ) *gemini.Response { - pathSegments := strings.Split(filePath, "/") + pathSegments := strings.Split(executable, "/") dirPath := "." if len(pathSegments) > 1 { dirPath = strings.Join(pathSegments[:len(pathSegments)-1], "/") } - filePath = "./" + pathSegments[len(pathSegments)-1] + basename := pathSegments[len(pathSegments)-1] - cmd := exec.CommandContext(ctx, filePath) - cmd.Env = prepareCGIEnv(ctx, server, req, filePath, pathInfo) + scriptName := req.Path[:len(req.Path)-len(pathInfo)] + if strings.HasSuffix(scriptName, "/") { + scriptName = scriptName[:len(scriptName)-1] + } + + cmd := exec.CommandContext(ctx, "./" + basename) + cmd.Env = prepareCGIEnv(ctx, req, scriptName, pathInfo) cmd.Dir = dirPath responseBuffer := &bytes.Buffer{} cmd.Stdout = responseBuffer + fmt.Printf("running %s in %s\n", basename, dirPath) if err := cmd.Run(); err != nil { var exErr *exec.ExitError if errors.As(err, &exErr) { - return gemini.CGIError(fmt.Sprintf("CGI returned with exit code %d", exErr.ExitCode())) + errMsg := fmt.Sprintf("CGI returned exit code %d", exErr.ExitCode()) + return gemini.CGIError(errMsg) } return gemini.Failure(err) } response, err := gemini.ParseResponse(responseBuffer) if err != nil { + fmt.Printf("response: %q\n", responseBuffer) return gemini.Failure(err) } return response @@ -127,7 +141,6 @@ func runCGI( func prepareCGIEnv( ctx context.Context, - server *gemini.Server, req *gemini.Request, scriptName string, pathInfo string, @@ -136,7 +149,6 @@ func prepareCGIEnv( if len(req.TLSState.PeerCertificates) > 0 { authType = "Certificate" } - environ := []string{ "AUTH_TYPE=" + authType, "CONTENT_LENGTH=", @@ -155,8 +167,8 @@ func prepareCGIEnv( "REMOTE_HOST=", "REMOTE_IDENT=", "SCRIPT_NAME="+scriptName, - "SERVER_NAME="+server.Hostname(), - "SERVER_PORT="+server.Port(), + "SERVER_NAME="+req.Server.Hostname(), + "SERVER_PORT="+req.Server.Port(), "SERVER_PROTOCOL=GEMINI", "SERVER_SOFTWARE=GUS", ) @@ -166,6 +178,10 @@ func prepareCGIEnv( environ = append( environ, "TLS_CLIENT_HASH="+fingerprint(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, ) } diff --git a/examples/cgi/cgi-bin/cowsay b/examples/cgi/cgi-bin/cowsay index e63e909..fc1f4e5 100755 --- a/examples/cgi/cgi-bin/cowsay +++ b/examples/cgi/cgi-bin/cowsay @@ -2,8 +2,7 @@ set -euo pipefail -if [ -z "$QUERY_STRING" ]; -then +if [[ -z "$QUERY_STRING" ]]; then printf "10 Enter a phrase.\r\n" exit 0 fi diff --git a/examples/cgi/cgi-bin/environ b/examples/cgi/cgi-bin/environ new file mode 100755 index 0000000..62ca15a --- /dev/null +++ b/examples/cgi/cgi-bin/environ @@ -0,0 +1,8 @@ +#!/usr/bin/env sh + +set -euo pipefail + +printf "20 text/gemini\r\n" +echo "\`\`\`env(1) output" +env +echo "\`\`\`" diff --git a/examples/cgi/main.go b/examples/cgi/main.go index e784876..d1df220 100644 --- a/examples/cgi/main.go +++ b/examples/cgi/main.go @@ -21,7 +21,7 @@ func main() { } // make use of a CGI request handler - cgiHandler := cgi.CGIHandler("/cgi-bin", "cgi-bin") + cgiHandler := cgi.CGIDirectory("/cgi-bin", "./cgi-bin") // add stdout logging to the request handler handler := guslog.Requests(os.Stdout, nil)(cgiHandler) |