Support requesting user input

Resolves #3.
This commit is contained in:
Trevor Slocum 2020-10-30 22:11:54 -07:00
parent e7cebf095e
commit 8465ad9871
4 changed files with 121 additions and 13 deletions

View file

@ -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. 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. 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. Forward request to Gemini server at specified URL.
Use the pseudo-scheme `gemini-insecure://` to disable certificate verification. Use the pseudo-scheme `gemini-insecure://` to disable certificate verification.
#### Command ##### Command
Serve output of system 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 # Example config.yaml
```yaml ```yaml

View file

@ -10,6 +10,7 @@ Breaking changes may be made.
## Features ## Features
- Serve static files - Serve static files
- Directory listing may be enabled
- Serve the output of system commands - Serve the output of system commands
- Reverse proxy requests - Reverse proxy requests

View file

@ -22,6 +22,12 @@ type pathConfig struct {
Proxy string Proxy string
Command string Command string
// Request input
Input string
// Request sensitive input
SensitiveInput string
// List directory entries // List directory entries
ListDirectory bool ListDirectory bool

View file

@ -51,7 +51,11 @@ func writeStatus(c net.Conn, code int) {
} }
func serveDirectory(c net.Conn, request *url.URL, dirPath string) { 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 { err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
@ -59,6 +63,11 @@ func serveDirectory(c net.Conn, request *url.URL, dirPath string) {
return nil return nil
} }
files = append(files, info) files = append(files, info)
if info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
numDirs++
} else {
numFiles++
}
if info.IsDir() { if info.IsDir() {
return filepath.SkipDir 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") 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 != "/" { if request.Path != "/" {
c.Write([]byte("=> ../ ../\r\n\r\n")) 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 var args []string
if len(command) > 0 { if len(command) > 0 {
args = command[1:] args = command[1:]
@ -220,6 +249,9 @@ func serveCommand(c net.Conn, command []string) {
cmd := exec.Command(command[0], args...) cmd := exec.Command(command[0], args...)
var buf bytes.Buffer var buf bytes.Buffer
if userInput != "" {
cmd.Stdin = strings.NewReader(userInput + "\n")
}
cmd.Stdout = &buf cmd.Stdout = &buf
cmd.Stderr = &buf cmd.Stderr = &buf
@ -253,6 +285,17 @@ func scanCRLF(data []byte, atEOF bool) (advance int, token []byte, err error) {
return 0, nil, nil 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) { func handleConn(c net.Conn) {
if verbose { if verbose {
t := time.Now() t := time.Now()
@ -329,6 +372,11 @@ func handleConn(c net.Conn) {
if strippedPath[0] == '/' { if strippedPath[0] == '/' {
strippedPath = strippedPath[1:] strippedPath = strippedPath[1:]
} }
requestQuery, err := url.QueryUnescape(request.RawQuery)
if err != nil {
writeStatus(c, gemini.StatusBadRequest)
return
}
var matchedHost bool var matchedHost bool
for hostname := range config.Hosts { for hostname := range config.Hosts {
@ -338,22 +386,53 @@ func handleConn(c net.Conn) {
matchedHost = true matchedHost = true
for _, serve := range config.Hosts[hostname] { 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 != "" { if serve.Proxy != "" {
serveProxy(c, requestData, serve.Proxy) serveProxy(c, requestData, serve.Proxy)
return return
} else if serve.cmd != nil { } 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 return
} }
serveFile(c, request, requestData, path.Join(serve.Root, strippedPath), serve.ListDirectory) serveFile(c, request, requestData, path.Join(serve.Root, strippedPath), serve.ListDirectory)
return return
} else if serve.r == nil && strings.HasPrefix(request.Path, serve.Path) { } else if matchedPrefix {
if serve.Proxy != "" { if serve.Proxy != "" {
serveProxy(c, requestData, serve.Proxy) serveProxy(c, requestData, serve.Proxy)
return return
} else if serve.cmd != nil { } 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 return
} }
filePath := request.Path[len(serve.Path):] filePath := request.Path[len(serve.Path):]