diff options
Diffstat (limited to 'contrib/cgi')
-rw-r--r-- | contrib/cgi/cgi.go | 105 | ||||
-rw-r--r-- | contrib/cgi/cgi_test.go | 4 | ||||
-rw-r--r-- | contrib/cgi/gemini.go | 47 | ||||
-rw-r--r-- | contrib/cgi/gopher.go | 45 |
4 files changed, 140 insertions, 61 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), diff --git a/contrib/cgi/cgi_test.go b/contrib/cgi/cgi_test.go index c265050..5c1ca33 100644 --- a/contrib/cgi/cgi_test.go +++ b/contrib/cgi/cgi_test.go @@ -21,8 +21,8 @@ func TestCGIDirectory(t *testing.T) { tlsconf, err := gemini.FileTLS("testdata/server.crt", "testdata/server.key") require.Nil(t, err) - handler := cgi.CGIDirectory("/cgi-bin", "./testdata") - server, err := gemini.NewServer(context.Background(), nil, tlsconf, "tcp", "127.0.0.1:0", handler) + handler := cgi.GeminiCGIDirectory("/cgi-bin", "./testdata") + server, err := gemini.NewServer(context.Background(), "localhost", "tcp", "127.0.0.1:0", handler, nil, tlsconf) require.Nil(t, err) go func() { assert.Nil(t, server.Serve()) }() diff --git a/contrib/cgi/gemini.go b/contrib/cgi/gemini.go new file mode 100644 index 0000000..8302e7e --- /dev/null +++ b/contrib/cgi/gemini.go @@ -0,0 +1,47 @@ +package cgi + +import ( + "context" + "fmt" + "strings" + + "tildegit.org/tjp/gus" + "tildegit.org/tjp/gus/gemini" +) + +// GeminiCGIDirectory 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 GeminiCGIDirectory(pathRoot, fsRoot string) gus.Handler { + fsRoot = strings.TrimRight(fsRoot, "/") + return func(ctx context.Context, request *gus.Request) *gus.Response { + if !strings.HasPrefix(request.Path, pathRoot) { + return nil + } + + filepath, pathinfo, err := ResolveCGI(request.Path[len(pathRoot):], fsRoot) + if err != nil { + return gemini.Failure(err) + } + if filepath == "" { + return nil + } + + stdout, exitCode, err := RunCGI(ctx, request, filepath, pathinfo) + if err != nil { + return gemini.Failure(err) + } + if exitCode != 0 { + return gemini.CGIError(fmt.Sprintf("CGI process exited with status %d", exitCode)) + } + + response, err := gemini.ParseResponse(stdout) + if err != nil { + return gemini.Failure(err) + } + return response + } +} diff --git a/contrib/cgi/gopher.go b/contrib/cgi/gopher.go new file mode 100644 index 0000000..29bfdba --- /dev/null +++ b/contrib/cgi/gopher.go @@ -0,0 +1,45 @@ +package cgi + +import ( + "context" + "fmt" + "strings" + + "tildegit.org/tjp/gus" + "tildegit.org/tjp/gus/gopher" +) + +// GopherCGIDirectory 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 GopherCGIDirectory(pathRoot, fsRoot string) gus.Handler { + fsRoot = strings.TrimRight(fsRoot, "/") + return func(ctx context.Context, request *gus.Request) *gus.Response { + if !strings.HasPrefix(request.Path, pathRoot) { + return nil + } + + filepath, pathinfo, err := ResolveCGI(request.Path[len(pathRoot):], fsRoot) + if err != nil { + return gopher.Error(err).Response() + } + if filepath == "" { + return nil + } + + stdout, exitCode, err := RunCGI(ctx, request, filepath, pathinfo) + if err != nil { + return gopher.Error(err).Response() + } + if exitCode != 0 { + return gopher.Error( + fmt.Errorf("CGI process exited with status %d", exitCode), + ).Response() + } + + return gopher.File(0, stdout) + } +} |