From 66a1b1f39a1e1d5499b548b36d18c8daa872d7da Mon Sep 17 00:00:00 2001 From: tjpcc Date: Sat, 28 Jan 2023 14:52:35 -0700 Subject: 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. --- contrib/cgi/cgi.go | 105 +++++++++++++++++++++++------------------------------ 1 file changed, 46 insertions(+), 59 deletions(-) (limited to 'contrib/cgi/cgi.go') 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), -- cgit v1.2.3