Convert CGIPath handling from regexs to prefixes.
This commit is contained in:
parent
cc5410494e
commit
4ae154faed
39
README.md
39
README.md
|
@ -224,6 +224,18 @@ directory listing:
|
||||||
Molly Brown supports dynamically generated content using an adaptation
|
Molly Brown supports dynamically generated content using an adaptation
|
||||||
of the CGI standard, and also the SCGI standard.
|
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
|
It is very important to be aware that programs written in Go are
|
||||||
*unable* to reliably change their UID once started, due to how
|
*unable* to reliably change their UID once started, due to how
|
||||||
goroutines are implemented on unix systems. As an unavoidable
|
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
|
yourself. Allowing untrusted users to upload arbitrary executable
|
||||||
files into a CGI path is a serious security vulnerability.
|
files into a CGI path is a serious security vulnerability.
|
||||||
|
|
||||||
SCGI applications must be started separately, and as such can run e.g.
|
SCGI applications must be started separately (i.e. Molly Brown expects
|
||||||
as their own user and/or chrooted into their own filesystem, and as
|
them to already be running and will not attempt to start them itself),
|
||||||
such are less of a security issue.
|
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
|
* `CGIPaths`: A list of filesystem paths, within which
|
||||||
world-executable file contained in a directory which matches one of
|
world-executable files will be run as CGI processes. The paths act
|
||||||
the regexs in this list will be executed and its standard output
|
as prefixes, i.e. if `/var/gemini/cgi-bin` is listed then
|
||||||
will be sent as the response to the client. CGI applications are
|
`/var/gemini/cgi-bin/script.py` and
|
||||||
responsible for generating their own response headers, and must
|
`/var/gemini/cgi-bin/subdir/subsubdir/script.py` will both be run.
|
||||||
terminate within 10 seconds of being spawned to avoid being killed.
|
The paths may include basic wildcard characters, where `?` matches a
|
||||||
Details about the request are available to CGI applications through
|
single non-separator character and `*` matches a sequence of them -
|
||||||
environment variables.
|
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
|
* `SCGIPaths`: In this section of the config file, keys are path
|
||||||
regexs and values are paths to unix domain sockets. Any request
|
regexs and values are paths to unix domain sockets. Any request
|
||||||
whose path matches one of the regexs will cause an SCGI request to
|
whose path matches one of the regexs will cause an SCGI request to
|
||||||
|
|
34
dynamic.go
34
dynamic.go
|
@ -9,40 +9,30 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleCGI(config Config, path string, cgiPath string, URL *url.URL, log *LogEntry, errorLog chan string, conn net.Conn) {
|
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
|
// Find the shortest leading part of path which maps to an executable file.
|
||||||
// If we find such, call it script_path, and everything after it path_info
|
// Call this part scriptPath, and everything after it pathInfo.
|
||||||
components := strings.Split(path, "/")
|
components := strings.Split(path, "/")
|
||||||
script_path := ""
|
scriptPath := ""
|
||||||
path_info := ""
|
pathInfo := ""
|
||||||
matched := false
|
matched := false
|
||||||
for i := 0; i <= len(components); i++ {
|
for i := 0; i <= len(components); i++ {
|
||||||
script_path = strings.Join(components[0:i], "/")
|
scriptPath = strings.Join(components[0:i], "/")
|
||||||
path_info = strings.Join(components[i:], "/")
|
pathInfo = strings.Join(components[i:], "/")
|
||||||
if !strings.HasPrefix(script_path, config.DocBase) {
|
if !strings.HasPrefix(scriptPath, cgiPath) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
inCGIPath, err := regexp.Match(cgiPath, []byte(path))
|
info, err := os.Stat(scriptPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
} else if info.IsDir() {
|
||||||
if !inCGIPath {
|
|
||||||
continue
|
continue
|
||||||
}
|
} else if info.Mode().Perm()&0111 == 0111 {
|
||||||
info, err := os.Stat(script_path)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if info.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if info.Mode().Perm()&0111 == 0111 {
|
|
||||||
matched = true
|
matched = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -54,12 +44,12 @@ func handleCGI(config Config, path string, cgiPath string, URL *url.URL, log *Lo
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare environment variables
|
// Prepare environment variables
|
||||||
vars := prepareCGIVariables(config, URL, conn, script_path, path_info)
|
vars := prepareCGIVariables(config, URL, conn, scriptPath, pathInfo)
|
||||||
|
|
||||||
// Spawn process
|
// Spawn process
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
cmd := exec.CommandContext(ctx, script_path)
|
cmd := exec.CommandContext(ctx, scriptPath)
|
||||||
cmd.Env = []string{}
|
cmd.Env = []string{}
|
||||||
for key, value := range vars {
|
for key, value := range vars {
|
||||||
cmd.Env = append(cmd.Env, key+"="+value)
|
cmd.Env = append(cmd.Env, key+"="+value)
|
||||||
|
|
|
@ -21,8 +21,8 @@
|
||||||
## Dynamic content
|
## Dynamic content
|
||||||
#
|
#
|
||||||
#CGIPaths = [
|
#CGIPaths = [
|
||||||
# "^/var/gemini/cgi-bin/",
|
# "/var/gemini/cgi-bin",
|
||||||
# "^/var/gemini/users/trusted-user/cgi-bin/",
|
# "/var/gemini/users/*/cgi-bin/", # Unsafe!
|
||||||
#]
|
#]
|
||||||
#
|
#
|
||||||
#[SCGIPaths]
|
#[SCGIPaths]
|
||||||
|
|
12
handler.go
12
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
|
// Check whether this URL is in a configured CGI path
|
||||||
|
var cgiPaths []string
|
||||||
for _, cgiPath := range config.CGIPaths {
|
for _, cgiPath := range config.CGIPaths {
|
||||||
inCGIPath, err := regexp.Match(cgiPath, []byte(path))
|
expandedPaths, err := filepath.Glob(cgiPath)
|
||||||
if err == nil && inCGIPath {
|
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)
|
handleCGI(config, path, cgiPath, URL, &log, errorLogEntries, conn)
|
||||||
if log.Status != 0 {
|
if log.Status != 0 {
|
||||||
return
|
return
|
||||||
|
|
Loading…
Reference in New Issue