From dc58324dffb139447ac35ef9f30e5dc9e3a8cfcf Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Wed, 4 Nov 2020 20:18:59 -0800 Subject: [PATCH] Fix resource path resolution --- server.go | 375 +++++++++------------------------------------- server_command.go | 43 ++++++ server_file.go | 164 ++++++++++++++++++++ server_proxy.go | 39 +++++ 4 files changed, 318 insertions(+), 303 deletions(-) create mode 100644 server_command.go create mode 100644 server_file.go create mode 100644 server_proxy.go diff --git a/server.go b/server.go index fd8386b..5ca0581 100644 --- a/server.go +++ b/server.go @@ -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 { diff --git a/server_command.go b/server_command.go new file mode 100644 index 0000000..c7b1b29 --- /dev/null +++ b/server_command.go @@ -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) + } +} diff --git a/server_file.go b/server_file.go new file mode 100644 index 0000000..5a1fc01 --- /dev/null +++ b/server_file.go @@ -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) +} diff --git a/server_proxy.go b/server_proxy.go new file mode 100644 index 0000000..5d8ab32 --- /dev/null +++ b/server_proxy.go @@ -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) + } +}