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.
### 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

View file

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

View file

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

View file

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