summaryrefslogtreecommitdiff
path: root/contrib/cgi
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/cgi')
-rw-r--r--contrib/cgi/cgi.go105
-rw-r--r--contrib/cgi/cgi_test.go4
-rw-r--r--contrib/cgi/gemini.go47
-rw-r--r--contrib/cgi/gopher.go45
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)
+ }
+}