diff --git a/CONFIGURATION.md b/CONFIGURATION.md index ff7d26b..6d36d4d 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -62,24 +62,46 @@ Fixed string paths will match with and without a trailing slash. When accessing a directory the file `index.gemini` or `index.gmi` is served. -### Path attributes +### Path -#### Root +#### Resources + +One resource must be defined for each path. + +##### Root Serve static files from specified root directory. -Directory listing may be enabled by adding `listdirectory: true`. - -#### Proxy +##### Proxy Forward request to Gemini server at specified URL. Use the pseudo-scheme `gemini-insecure://` to disable certificate verification. -#### Command +##### Command Serve output of system command. +When input is requested from the user, it is available as a pseudo-variable +`$USERINPUT` which does not require surrounding quotes. It may be used as an +argument to the command, otherwise user input is passed via standard input. + +#### Attributes + +Any number of attributes may be defined for a path. + +##### ListDirectory + +Directory listing may be enabled by adding `listdirectory: true`. + +##### Input + +Request text input from user. + +##### SensitiveInput + +Request sensitive text input from the user. Text will not be shown as it is entered. + # Example config.yaml ```yaml diff --git a/README.md b/README.md index 5000e6c..70d5438 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Breaking changes may be made. ## Features - Serve static files + - Directory listing may be enabled - Serve the output of system commands - Reverse proxy requests diff --git a/config.go b/config.go index ee05aae..a09b676 100644 --- a/config.go +++ b/config.go @@ -22,6 +22,12 @@ type pathConfig struct { Proxy string Command string + // Request input + Input string + + // Request sensitive input + SensitiveInput string + // List directory entries ListDirectory bool diff --git a/server.go b/server.go index 375253f..3e804b0 100644 --- a/server.go +++ b/server.go @@ -51,7 +51,11 @@ func writeStatus(c net.Conn, code int) { } func serveDirectory(c net.Conn, request *url.URL, dirPath string) { - var files []os.FileInfo + var ( + files []os.FileInfo + numDirs int + numFiles int + ) err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err @@ -59,6 +63,11 @@ func serveDirectory(c net.Conn, request *url.URL, dirPath string) { return nil } files = append(files, info) + if info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + numDirs++ + } else { + numFiles++ + } if info.IsDir() { return filepath.SkipDir } @@ -80,7 +89,27 @@ func serveDirectory(c net.Conn, request *url.URL, dirPath string) { writeHeader(c, gemini.StatusSuccess, "text/gemini; charset=utf-8") - c.Write([]byte("# " + request.Path + "\r\n\r\n")) + fmt.Fprintf(c, "# %s\r\n", request.Path) + if numDirs > 0 || numFiles > 0 { + if numDirs > 0 { + if numDirs == 1 { + c.Write([]byte("1 directory")) + } else { + fmt.Fprintf(c, "%d directories", numDirs) + } + } + if numFiles > 0 { + if numDirs > 0 { + c.Write([]byte(" and ")) + } + if numDirs == 1 { + c.Write([]byte("1 file")) + } else { + fmt.Fprintf(c, "%d files", numFiles) + } + } + c.Write([]byte("\r\n\n")) + } if request.Path != "/" { c.Write([]byte("=> ../ ../\r\n\r\n")) @@ -212,7 +241,7 @@ func serveProxy(c net.Conn, requestData, proxyURL string) { } } -func serveCommand(c net.Conn, command []string) { +func serveCommand(c net.Conn, userInput string, command []string) { var args []string if len(command) > 0 { args = command[1:] @@ -220,6 +249,9 @@ func serveCommand(c net.Conn, command []string) { cmd := exec.Command(command[0], args...) var buf bytes.Buffer + if userInput != "" { + cmd.Stdin = strings.NewReader(userInput + "\n") + } cmd.Stdout = &buf cmd.Stderr = &buf @@ -253,6 +285,17 @@ func scanCRLF(data []byte, atEOF bool) (advance int, token []byte, err error) { return 0, nil, nil } +func replaceWithUserInput(command []string, userInput string) []string { + newCommand := make([]string, len(command)) + copy(newCommand, command) + for i, piece := range newCommand { + if strings.Contains(piece, "$USERINPUT") { + newCommand[i] = strings.ReplaceAll(piece, "$USERINPUT", userInput) + } + } + return newCommand +} + func handleConn(c net.Conn) { if verbose { t := time.Now() @@ -329,6 +372,11 @@ func handleConn(c net.Conn) { if strippedPath[0] == '/' { strippedPath = strippedPath[1:] } + requestQuery, err := url.QueryUnescape(request.RawQuery) + if err != nil { + writeStatus(c, gemini.StatusBadRequest) + return + } var matchedHost bool for hostname := range config.Hosts { @@ -338,22 +386,53 @@ func handleConn(c net.Conn) { matchedHost = true for _, serve := range config.Hosts[hostname] { - if serve.r != nil && serve.r.Match(pathBytes) { + matchedRegexp := serve.r != nil && serve.r.Match(pathBytes) + matchedPrefix := serve.r == nil && strings.HasPrefix(request.Path, serve.Path) + if !matchedRegexp && !matchedPrefix { + continue + } + + requireInput := serve.Input != "" || serve.SensitiveInput != "" + if requestQuery == "" && requireInput { + if serve.Input != "" { + writeHeader(c, gemini.StatusInput, serve.Input) + return + } else if serve.SensitiveInput != "" { + writeHeader(c, gemini.StatusSensitiveInput, serve.SensitiveInput) + return + } + } + + if matchedRegexp { if serve.Proxy != "" { serveProxy(c, requestData, serve.Proxy) return } else if serve.cmd != nil { - serveCommand(c, serve.cmd) + if requireInput { + newCommand := replaceWithUserInput(serve.cmd, requestQuery) + if newCommand != nil { + serveCommand(c, "", newCommand) + return + } + } + serveCommand(c, requestQuery, serve.cmd) return } serveFile(c, request, requestData, path.Join(serve.Root, strippedPath), serve.ListDirectory) return - } else if serve.r == nil && strings.HasPrefix(request.Path, serve.Path) { + } else if matchedPrefix { if serve.Proxy != "" { serveProxy(c, requestData, serve.Proxy) return } else if serve.cmd != nil { - serveCommand(c, serve.cmd) + if requireInput { + newCommand := replaceWithUserInput(serve.cmd, requestQuery) + if newCommand != nil { + serveCommand(c, "", newCommand) + return + } + } + serveCommand(c, requestQuery, serve.cmd) return } filePath := request.Path[len(serve.Path):]