mirror of
https://code.rocketnine.space/tslocum/twins.git
synced 2024-11-27 16:38:14 +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.
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
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) {
|
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):]
|
||||||
|
|
Loading…
Reference in a new issue