Fix resource path resolution

This commit is contained in:
Trevor Slocum 2020-11-04 20:18:59 -08:00
parent 8e2e404a81
commit dc58324dff
4 changed files with 318 additions and 303 deletions

375
server.go
View File

@ -6,21 +6,15 @@ import (
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"log"
"net"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"sort"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/h2non/filetype"
)
const (
@ -50,6 +44,8 @@ const (
statusBadRequest = 59
)
var slashesRegexp = regexp.MustCompile(`[^\\]\/`)
func writeHeader(c net.Conn, code int, meta string) {
fmt.Fprintf(c, "%d %s\r\n", code, meta)
@ -75,222 +71,6 @@ func writeStatus(c net.Conn, code int) {
writeHeader(c, code, meta)
}
func serveDirectory(c net.Conn, request *url.URL, dirPath string) {
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
} else if path == dirPath {
return nil
}
files = append(files, info)
if info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
numDirs++
} else {
numFiles++
}
if info.IsDir() {
return filepath.SkipDir
}
return nil
})
if err != nil {
writeStatus(c, statusTemporaryFailure)
return
}
// List directories first
sort.Slice(files, func(i, j int) bool {
iDir := files[i].IsDir() || files[i].Mode()&os.ModeSymlink != 0
jDir := files[j].IsDir() || files[j].Mode()&os.ModeSymlink != 0
if iDir != jDir {
return iDir
}
return i < j
})
writeHeader(c, statusSuccess, "text/gemini; charset=utf-8")
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"))
}
for _, info := range files {
fileName := info.Name()
filePath := url.PathEscape(info.Name())
if info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
fileName += "/"
filePath += "/"
}
c.Write([]byte("=> " + fileName + " " + filePath + "\r\n"))
if info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
c.Write([]byte("\r\n"))
continue
}
modified := "Never"
if !info.ModTime().IsZero() {
modified = info.ModTime().Format("2006-01-02 3:04 PM")
}
c.Write([]byte(modified + " - " + formatFileSize(info.Size()) + "\r\n\r\n"))
}
}
func serveFile(c net.Conn, request *url.URL, requestData, filePath string, listDir bool) {
fi, err := os.Stat(filePath)
if err != nil {
writeStatus(c, statusNotFound)
return
}
originalPath := filePath
var fetchIndex bool
if mode := fi.Mode(); mode.IsDir() {
if requestData[len(requestData)-1] != '/' {
// Add trailing slash
log.Println(requestData)
writeHeader(c, statusRedirectPermanent, requestData+"/")
return
}
fetchIndex = true
_, err := os.Stat(path.Join(filePath, "index.gemini"))
if err == nil {
filePath = path.Join(filePath, "index.gemini")
} else {
filePath = path.Join(filePath, "index.gmi")
}
}
fi, err = os.Stat(filePath)
if os.IsNotExist(err) {
if fetchIndex && listDir {
serveDirectory(c, request, originalPath)
return
}
writeStatus(c, statusNotFound)
return
} else if err != nil {
writeStatus(c, statusTemporaryFailure)
return
}
// Open file
file, _ := os.Open(filePath)
defer file.Close()
// Read file header
buf := make([]byte, 261)
n, _ := file.Read(buf)
// Write header
var mimeType string
if strings.HasSuffix(filePath, ".html") && strings.HasSuffix(filePath, ".htm") {
mimeType = "text/html; charset=utf-8"
} else if strings.HasSuffix(filePath, ".txt") && strings.HasSuffix(filePath, ".text") {
mimeType = "text/plain; charset=utf-8"
} else if !strings.HasSuffix(filePath, ".gmi") && !strings.HasSuffix(filePath, ".gemini") {
kind, _ := filetype.Match(buf[:n])
if kind != filetype.Unknown {
mimeType = kind.MIME.Value
}
}
if mimeType == "" {
mimeType = "text/gemini; charset=utf-8"
}
writeHeader(c, statusSuccess, mimeType)
// Write body
c.Write(buf[:n])
io.Copy(c, file)
}
func serveProxy(c net.Conn, requestData, proxyURL string) {
original := proxyURL
tlsConfig := &tls.Config{}
if strings.HasPrefix(proxyURL, "gemini://") {
proxyURL = proxyURL[9:]
} else if strings.HasPrefix(proxyURL, "gemini-insecure://") {
proxyURL = proxyURL[18:]
tlsConfig.InsecureSkipVerify = true
}
proxy, err := tls.Dial("tcp", proxyURL, tlsConfig)
if err != nil {
writeStatus(c, statusProxyError)
return
}
defer proxy.Close()
// Forward request
proxy.Write([]byte(requestData))
proxy.Write([]byte("\r\n"))
// Forward response
io.Copy(c, proxy)
if verbose {
log.Printf("< %s\n", original)
}
}
func serveCommand(c net.Conn, userInput string, command []string) {
var args []string
if len(command) > 0 {
args = command[1:]
}
cmd := exec.Command(command[0], args...)
var buf bytes.Buffer
if userInput != "" {
cmd.Stdin = strings.NewReader(userInput + "\n")
}
cmd.Stdout = &buf
cmd.Stderr = &buf
err := cmd.Run()
if err != nil {
writeStatus(c, statusProxyError)
return
}
writeHeader(c, statusSuccess, "text/gemini; charset=utf-8")
c.Write(buf.Bytes())
if verbose {
log.Printf("< %s\n", command)
}
}
func scanCRLF(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
@ -307,34 +87,64 @@ func scanCRLF(data []byte, atEOF bool) (advance int, token []byte, err error) {
return 0, nil, nil
}
func replaceWithUserInput(command []string, userInput string) []string {
func replaceWithUserInput(command []string, request *url.URL) []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)
requestQuery, err := url.QueryUnescape(request.RawQuery)
if err == nil {
newCommand[i] = strings.ReplaceAll(piece, "$USERINPUT", requestQuery)
}
}
}
return newCommand
}
func handleConn(c *tls.Conn) {
if verbose {
t := time.Now()
defer func() {
d := time.Since(t)
if d > time.Second {
d = d.Round(time.Second)
} else {
d = d.Round(time.Millisecond)
}
log.Printf("took %s", d)
}()
func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
resolvedPath := request.Path
requestSplit := strings.Split(request.Path, "/")
pathSlashes := len(slashesRegexp.FindAllStringIndex(serve.Path, -1))
if len(request.Path) > 0 && request.Path[0] == '/' {
pathSlashes++ // Regexp does not match starting slash
}
if len(requestSplit) >= pathSlashes+1 {
resolvedPath = "/" + strings.Join(requestSplit[pathSlashes+1:], "/")
}
defer c.Close()
c.SetReadDeadline(time.Now().Add(readTimeout))
if serve.Proxy != "" {
serveProxy(c, request, serve.Proxy)
return
} else if serve.FastCGI != "" {
contentType := "text/gemini; charset=utf-8"
if serve.Type != "" {
contentType = serve.Type
}
writeHeader(c, statusSuccess, contentType)
filePath := path.Join(serve.Root, request.Path[1:])
serveFastCGI(c, config.fcgiPools[serve.FastCGI], request, filePath)
return
} else if serve.cmd != nil {
requireInput := serve.Input != "" || serve.SensitiveInput != ""
if requireInput {
newCommand := replaceWithUserInput(serve.cmd, request)
if newCommand != nil {
serveCommand(c, request, newCommand)
return
}
}
serveCommand(c, request, serve.cmd)
return
}
filePath := resolvedPath
if len(filePath) > 0 && filePath[0] == '/' {
filePath = filePath[1:]
}
serveFile(c, request, path.Join(serve.Root, filePath), serve.ListDirectory)
}
func serveConn(c *tls.Conn) {
var requestData string
scanner := bufio.NewScanner(c)
scanner.Split(scanCRLF)
@ -342,7 +152,6 @@ func handleConn(c *tls.Conn) {
requestData = scanner.Text()
}
if err := scanner.Err(); err != nil {
log.Println(scanner.Text(), "FAILED")
writeStatus(c, statusBadRequest)
return
}
@ -405,11 +214,6 @@ func handleConn(c *tls.Conn) {
if strippedPath[0] == '/' {
strippedPath = strippedPath[1:]
}
requestQuery, err := url.QueryUnescape(request.RawQuery)
if err != nil {
writeStatus(c, statusBadRequest)
return
}
var matchedHost bool
for hostname := range config.Hosts {
@ -426,7 +230,7 @@ func handleConn(c *tls.Conn) {
}
requireInput := serve.Input != "" || serve.SensitiveInput != ""
if requestQuery == "" && requireInput {
if request.RawQuery == "" && requireInput {
if serve.Input != "" {
writeHeader(c, statusInput, serve.Input)
return
@ -436,63 +240,8 @@ func handleConn(c *tls.Conn) {
}
}
if matchedRegexp {
if serve.Proxy != "" {
serveProxy(c, requestData, serve.Proxy)
return
} else if serve.FastCGI != "" {
contentType := "text/gemini; charset=utf-8"
if serve.Type != "" {
contentType = serve.Type
}
writeHeader(c, statusSuccess, contentType)
filePath := path.Join(serve.Root, request.Path[1:])
serveFastCGI(c, config.fcgiPools[serve.FastCGI], request, filePath)
return
} else if serve.cmd != nil {
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 matchedPrefix {
if serve.Proxy != "" {
serveProxy(c, requestData, serve.Proxy)
return
} else if serve.FastCGI != "" {
contentType := "text/gemini; charset=utf-8"
if serve.Type != "" {
contentType = serve.Type
}
writeHeader(c, statusSuccess, contentType)
filePath := path.Join(serve.Root, request.Path[1:])
serveFastCGI(c, config.fcgiPools[serve.FastCGI], request, filePath)
return
} else if serve.cmd != nil {
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):]
if len(filePath) > 0 && filePath[0] == '/' {
filePath = filePath[1:]
}
serveFile(c, request, requestData, path.Join(serve.Root, filePath), serve.ListDirectory)
if matchedRegexp || matchedPrefix {
servePath(c, request, serve)
return
}
}
@ -506,6 +255,26 @@ func handleConn(c *tls.Conn) {
}
}
func handleConn(c *tls.Conn) {
if verbose {
t := time.Now()
defer func() {
d := time.Since(t)
if d > time.Second {
d = d.Round(time.Second)
} else {
d = d.Round(time.Millisecond)
}
log.Printf("took %s", d)
}()
}
defer c.Close()
c.SetReadDeadline(time.Now().Add(readTimeout))
serveConn(c)
}
func getCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
host := config.Hosts[info.ServerName]
if host != nil {

43
server_command.go Normal file
View File

@ -0,0 +1,43 @@
package main
import (
"bytes"
"log"
"net"
"net/url"
"os/exec"
"strings"
)
func serveCommand(c net.Conn, request *url.URL, command []string) {
var args []string
if len(command) > 0 {
args = command[1:]
}
cmd := exec.Command(command[0], args...)
var buf bytes.Buffer
if request.RawQuery != "" {
requestQuery, err := url.QueryUnescape(request.RawQuery)
if err != nil {
writeStatus(c, statusBadRequest)
return
}
cmd.Stdin = strings.NewReader(requestQuery + "\n")
}
cmd.Stdout = &buf
cmd.Stderr = &buf
err := cmd.Run()
if err != nil {
writeStatus(c, statusProxyError)
return
}
writeHeader(c, statusSuccess, "text/gemini; charset=utf-8")
c.Write(buf.Bytes())
if verbose {
log.Printf("< %s\n", command)
}
}

164
server_file.go Normal file
View File

@ -0,0 +1,164 @@
package main
import (
"fmt"
"io"
"net"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strings"
"github.com/h2non/filetype"
)
func serveDirList(c net.Conn, request *url.URL, dirPath string) {
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
} else if path == dirPath {
return nil
}
files = append(files, info)
if info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
numDirs++
} else {
numFiles++
}
if info.IsDir() {
return filepath.SkipDir
}
return nil
})
if err != nil {
writeStatus(c, statusTemporaryFailure)
return
}
// List directories first
sort.Slice(files, func(i, j int) bool {
iDir := files[i].IsDir() || files[i].Mode()&os.ModeSymlink != 0
jDir := files[j].IsDir() || files[j].Mode()&os.ModeSymlink != 0
if iDir != jDir {
return iDir
}
return i < j
})
writeHeader(c, statusSuccess, "text/gemini; charset=utf-8")
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"))
}
for _, info := range files {
fileName := info.Name()
filePath := url.PathEscape(info.Name())
if info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
fileName += "/"
filePath += "/"
}
c.Write([]byte("=> " + fileName + " " + filePath + "\r\n"))
if info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
c.Write([]byte("\r\n"))
continue
}
modified := "Never"
if !info.ModTime().IsZero() {
modified = info.ModTime().Format("2006-01-02 3:04 PM")
}
c.Write([]byte(modified + " - " + formatFileSize(info.Size()) + "\r\n\r\n"))
}
}
func serveFile(c net.Conn, request *url.URL, filePath string, listDir bool) {
fi, err := os.Stat(filePath)
if err != nil {
writeStatus(c, statusNotFound)
return
}
if mode := fi.Mode(); mode.IsDir() {
if len(request.Path) == 0 || request.Path[len(request.Path)-1] != '/' {
// Add trailing slash
writeHeader(c, statusRedirectPermanent, request.String()+"/")
return
}
_, err := os.Stat(path.Join(filePath, "index.gmi"))
if err != nil {
_, err := os.Stat(path.Join(filePath, "index.gemini"))
if err != nil {
if listDir {
serveDirList(c, request, filePath)
return
}
writeStatus(c, statusNotFound)
return
}
filePath = path.Join(filePath, "index.gemini")
} else {
filePath = path.Join(filePath, "index.gmi")
}
}
// Open file
file, _ := os.Open(filePath)
defer file.Close()
// Read file header
buf := make([]byte, 261)
n, _ := file.Read(buf)
// Write header
var mimeType string
if strings.HasSuffix(filePath, ".html") && strings.HasSuffix(filePath, ".htm") {
mimeType = "text/html; charset=utf-8"
} else if strings.HasSuffix(filePath, ".txt") && strings.HasSuffix(filePath, ".text") {
mimeType = "text/plain; charset=utf-8"
} else if !strings.HasSuffix(filePath, ".gmi") && !strings.HasSuffix(filePath, ".gemini") {
kind, _ := filetype.Match(buf[:n])
if kind != filetype.Unknown {
mimeType = kind.MIME.Value
}
}
if mimeType == "" {
mimeType = "text/gemini; charset=utf-8"
}
writeHeader(c, statusSuccess, mimeType)
// Write body
c.Write(buf[:n])
io.Copy(c, file)
}

39
server_proxy.go Normal file
View File

@ -0,0 +1,39 @@
package main
import (
"crypto/tls"
"io"
"log"
"net"
"net/url"
"strings"
)
func serveProxy(c net.Conn, request *url.URL, proxyURL string) {
original := proxyURL
tlsConfig := &tls.Config{}
if strings.HasPrefix(proxyURL, "gemini://") {
proxyURL = proxyURL[9:]
} else if strings.HasPrefix(proxyURL, "gemini-insecure://") {
proxyURL = proxyURL[18:]
tlsConfig.InsecureSkipVerify = true
}
proxy, err := tls.Dial("tcp", proxyURL, tlsConfig)
if err != nil {
writeStatus(c, statusProxyError)
return
}
defer proxy.Close()
// Forward request
proxy.Write([]byte(request.String()))
proxy.Write([]byte("\r\n"))
// Forward response
io.Copy(c, proxy)
if verbose {
log.Printf("< %s\n", original)
}
}