CGI improvements
continuous-integration/drone/push Build is passing Details

This commit is contained in:
tjpcc 2023-01-10 17:22:13 -07:00
parent 96f3a7607f
commit 474a28663f
4 changed files with 46 additions and 23 deletions

View File

@ -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,
)
}

View File

@ -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

8
examples/cgi/cgi-bin/environ Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env sh
set -euo pipefail
printf "20 text/gemini\r\n"
echo "\`\`\`env(1) output"
env
echo "\`\`\`"

View File

@ -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)