diff --git a/README.md b/README.md index ecaccd6..8831ad3 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,18 @@ directory listing: Molly Brown supports dynamically generated content using an adaptation of the CGI standard, and also the SCGI standard. +The `stdout` of CGI processes will be sent verbatim as the response to +the client, and CGI applications are responsible for generating their +own response headers. CGI processes must terminate naturally within +10 seconds of being spawned to avoid being killed. Details about the +request are available to CGI applications through environment +variables, generally following RFC 3875. In particular, note that if +a request URL includes components after the path to an executable +(e.g. `cgi-bin/script.py/foo/bar/baz`) then the environment variable +`SCRIPT_PATH` will contain the part of the URL path mapping to the +executable (e.g. `/var/gemini/cgi-bin/scripty.py`) while the variable +`PATH_INFO` will contain the remainder (e.g. `foo/bar/baz`). + It is very important to be aware that programs written in Go are *unable* to reliably change their UID once started, due to how goroutines are implemented on unix systems. As an unavoidable @@ -236,18 +248,23 @@ applications, ideally only applications you have carefully written yourself. Allowing untrusted users to upload arbitrary executable files into a CGI path is a serious security vulnerability. -SCGI applications must be started separately, and as such can run e.g. -as their own user and/or chrooted into their own filesystem, and as -such are less of a security issue. +SCGI applications must be started separately (i.e. Molly Brown expects +them to already be running and will not attempt to start them itself), +and as such they can run e.g. as their own user and/or chrooted into +their own filesystem, meaning that they are less of a security threat +in addition to avoiding the overhead of process startup, database +connection etc. on each request. -* `CGIPaths`: A list of path regexs. Any request which maps to a - world-executable file contained in a directory which matches one of - the regexs in this list will be executed and its standard output - will be sent as the response to the client. CGI applications are - responsible for generating their own response headers, and must - terminate within 10 seconds of being spawned to avoid being killed. - Details about the request are available to CGI applications through - environment variables. +* `CGIPaths`: A list of filesystem paths, within which + world-executable files will be run as CGI processes. The paths act + as prefixes, i.e. if `/var/gemini/cgi-bin` is listed then + `/var/gemini/cgi-bin/script.py` and + `/var/gemini/cgi-bin/subdir/subsubdir/script.py` will both be run. + The paths may include basic wildcard characters, where `?` matches a + single non-separator character and `*` matches a sequence of them - + if wildcards are used, the path should *not* end in a trailing slash + - this appears to be a peculiarity of the Go standard library's + `filepath.Glob` function. * `SCGIPaths`: In this section of the config file, keys are path regexs and values are paths to unix domain sockets. Any request whose path matches one of the regexs will cause an SCGI request to diff --git a/dynamic.go b/dynamic.go index 8946689..644b52e 100644 --- a/dynamic.go +++ b/dynamic.go @@ -9,40 +9,30 @@ import ( "net/url" "os" "os/exec" - "regexp" "strconv" "strings" "time" ) func handleCGI(config Config, path string, cgiPath string, URL *url.URL, log *LogEntry, errorLog chan string, conn net.Conn) { - // Attempt to find the shortest leading part of path which maps to an executable file while still matching cgiPath - // If we find such, call it script_path, and everything after it path_info + // Find the shortest leading part of path which maps to an executable file. + // Call this part scriptPath, and everything after it pathInfo. components := strings.Split(path, "/") - script_path := "" - path_info := "" + scriptPath := "" + pathInfo := "" matched := false for i := 0; i <= len(components); i++ { - script_path = strings.Join(components[0:i], "/") - path_info = strings.Join(components[i:], "/") - if !strings.HasPrefix(script_path, config.DocBase) { + scriptPath = strings.Join(components[0:i], "/") + pathInfo = strings.Join(components[i:], "/") + if !strings.HasPrefix(scriptPath, cgiPath) { continue } - inCGIPath, err := regexp.Match(cgiPath, []byte(path)) + info, err := os.Stat(scriptPath) if err != nil { break - } - if !inCGIPath { + } else if info.IsDir() { continue - } - info, err := os.Stat(script_path) - if err != nil { - break - } - if info.IsDir() { - continue - } - if info.Mode().Perm()&0111 == 0111 { + } else if info.Mode().Perm()&0111 == 0111 { matched = true break } @@ -54,12 +44,12 @@ func handleCGI(config Config, path string, cgiPath string, URL *url.URL, log *Lo } // Prepare environment variables - vars := prepareCGIVariables(config, URL, conn, script_path, path_info) + vars := prepareCGIVariables(config, URL, conn, scriptPath, pathInfo) // Spawn process ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - cmd := exec.CommandContext(ctx, script_path) + cmd := exec.CommandContext(ctx, scriptPath) cmd.Env = []string{} for key, value := range vars { cmd.Env = append(cmd.Env, key+"="+value) diff --git a/example.conf b/example.conf index 47af47a..6ccc3c5 100644 --- a/example.conf +++ b/example.conf @@ -21,8 +21,8 @@ ## Dynamic content # #CGIPaths = [ -# "^/var/gemini/cgi-bin/", -# "^/var/gemini/users/trusted-user/cgi-bin/", +# "/var/gemini/cgi-bin", +# "/var/gemini/users/*/cgi-bin/", # Unsafe! #] # #[SCGIPaths] diff --git a/handler.go b/handler.go index cf16c18..f6c290e 100644 --- a/handler.go +++ b/handler.go @@ -91,9 +91,17 @@ func handleGeminiRequest(conn net.Conn, config Config, accessLogEntries chan Log } // Check whether this URL is in a configured CGI path + var cgiPaths []string for _, cgiPath := range config.CGIPaths { - inCGIPath, err := regexp.Match(cgiPath, []byte(path)) - if err == nil && inCGIPath { + expandedPaths, err := filepath.Glob(cgiPath) + if err != nil { + errorLogEntries <- "Error expanding CGI path glob " + cgiPath + ": " + err.Error() + continue + } + cgiPaths = append(cgiPaths, expandedPaths...) + } + for _, cgiPath := range cgiPaths { + if strings.HasPrefix(path, cgiPath) { handleCGI(config, path, cgiPath, URL, &log, errorLogEntries, conn) if log.Status != 0 { return