mirror of
https://code.rocketnine.space/tslocum/twins.git
synced 2024-11-27 13:18:13 +01:00
parent
e7cebf095e
commit
8465ad9871
4 changed files with 121 additions and 13 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
93
server.go
93
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):]
|
||||
|
|
Loading…
Reference in a new issue