Support directory listing via HTTPS

This commit is contained in:
Trevor Slocum 2020-12-03 16:45:20 -08:00
parent 8d6cb6527e
commit faab032d45
4 changed files with 91 additions and 72 deletions

View file

@ -219,17 +219,17 @@ hosts:
key: /srv/gemini.rocks/data/cert.key key: /srv/gemini.rocks/data/cert.key
paths: paths:
- -
path: ^/sites/.*\.php$ path: ^/.*\.php$
root: /home/geminirocks/data root: /home/geminirocks/public_html
fastcgi: unix:///var/run/php.sock fastcgi: unix:///var/run/php.sock
- -
path: /sites path: /files
root: /home/geminirocks/data root: /home/geminirocks/files
cache: 604800 # Cache for 1 week cache: 604800 # Cache for 1 week
list: true # Enable directory listing list: true # Enable directory listing
- -
path: ^/(help|info)$ path: ^/(help|info)$
root: /home/geminirocks/data/help root: /home/geminirocks/docs/help
- -
path: ^/proxy-example$ path: ^/proxy-example$
proxy: gemini://localhost:1966 proxy: gemini://localhost:1966
@ -237,6 +237,13 @@ hosts:
path: ^/cmd-example$ path: ^/cmd-example$
command: uname -a command: uname -a
cache: 0 # Do not cache cache: 0 # Do not cache
-
path: /
root: /home/geminirocks/public_html
twins.rocketnine.space:
cert: /srv/twins.rocketnine.space/data/cert.crt
key: /srv/twins.rocketnine.space/data/cert.key
paths:
- -
path: /redir-path-example path: /redir-path-example
redirect: /other-resource redirect: /other-resource
@ -245,15 +252,5 @@ hosts:
redirect: gemini://gemini.circumlunar.space/ redirect: gemini://gemini.circumlunar.space/
- -
path: / path: /
root: /home/geminirocks/data/home root: /home/twins/public_html
twins.rocketnine.space:
cert: /srv/twins.rocketnine.space/data/cert.crt
key: /srv/twins.rocketnine.space/data/cert.key
paths:
-
path: /sites
root: /home/twins/data/sites
-
path: /
root: /home/twins/data/home
``` ```

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"net" "net"
@ -12,8 +13,9 @@ import (
"github.com/h2non/filetype" "github.com/h2non/filetype"
) )
func serveDirList(c net.Conn, serve *pathConfig, request *url.URL, dirPath string) int { func buildDirList(request *url.URL, dirPath string) ([]byte, error) {
var ( var (
b = &bytes.Buffer{}
files []os.FileInfo files []os.FileInfo
numDirs int numDirs int
numFiles int numFiles int
@ -36,7 +38,7 @@ func serveDirList(c net.Conn, serve *pathConfig, request *url.URL, dirPath strin
return nil return nil
}) })
if err != nil { if err != nil {
return writeStatus(c, statusTemporaryFailure) return nil, err
} }
// List directories first // List directories first
sort.Slice(files, func(i, j int) bool { sort.Slice(files, func(i, j int) bool {
@ -48,24 +50,22 @@ func serveDirList(c net.Conn, serve *pathConfig, request *url.URL, dirPath strin
return i < j return i < j
}) })
writeSuccess(c, serve, geminiType, -1) fmt.Fprintf(b, "# %s%s", request.Path, newLine)
fmt.Fprintf(c, "# %s%s", request.Path, newLine)
if numDirs == 1 { if numDirs == 1 {
c.Write([]byte("1 directory")) b.Write([]byte("1 directory"))
} else { } else {
fmt.Fprintf(c, "%d directories", numDirs) fmt.Fprintf(b, "%d directories", numDirs)
} }
c.Write([]byte(", ")) b.Write([]byte(", "))
if numDirs == 1 { if numDirs == 1 {
c.Write([]byte("1 file")) b.Write([]byte("1 file"))
} else { } else {
fmt.Fprintf(c, "%d files", numFiles) fmt.Fprintf(b, "%d files", numFiles)
} }
c.Write([]byte(newLine + newLine)) b.Write([]byte(newLine + newLine))
if request.Path != "/" { if request.Path != "/" {
c.Write([]byte("=> ../ ../" + newLine + newLine)) b.Write([]byte("=> ../ ../" + newLine + newLine))
} }
for _, info := range files { for _, info := range files {
@ -76,10 +76,10 @@ func serveDirList(c net.Conn, serve *pathConfig, request *url.URL, dirPath strin
filePath += "/" filePath += "/"
} }
c.Write([]byte("=> " + fileName + " " + filePath + newLine)) b.Write([]byte("=> " + fileName + " " + filePath + newLine))
if info.IsDir() || info.Mode()&os.ModeSymlink != 0 { if info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
c.Write([]byte(newLine)) b.Write([]byte(newLine))
continue continue
} }
@ -87,8 +87,20 @@ func serveDirList(c net.Conn, serve *pathConfig, request *url.URL, dirPath strin
if !info.ModTime().IsZero() { if !info.ModTime().IsZero() {
modified = info.ModTime().Format("2006-01-02 3:04 PM") modified = info.ModTime().Format("2006-01-02 3:04 PM")
} }
c.Write([]byte(modified + " - " + formatFileSize(info.Size()) + newLine + newLine)) b.Write([]byte(modified + " - " + formatFileSize(info.Size()) + newLine + newLine))
} }
return b.Bytes(), nil
}
func serveDirList(c net.Conn, serve *pathConfig, request *url.URL, dirPath string) int {
dirList, err := buildDirList(request, dirPath)
if err != nil {
return writeStatus(c, statusTemporaryFailure)
}
writeSuccess(c, serve, geminiType, -1)
c.Write(dirList)
return statusSuccess return statusSuccess
} }

View file

@ -8,6 +8,7 @@ import (
"net/url" "net/url"
"os" "os"
"path" "path"
"path/filepath"
"strings" "strings"
"gitlab.com/tslocum/gmitohtml/pkg/gmitohtml" "gitlab.com/tslocum/gmitohtml/pkg/gmitohtml"
@ -114,23 +115,35 @@ func serveHTTPS(w http.ResponseWriter, r *http.Request) (int, int64, string) {
return status, -1, serve.Log return status, -1, serve.Log
} }
_, err := os.Stat(path.Join(filePath, "index.gmi")) var found bool
if err != nil { for _, indexFile := range indexFiles {
_, err := os.Stat(path.Join(filePath, "index.gemini")) _, err := os.Stat(path.Join(filePath, indexFile))
if err != nil { if err == nil || os.IsExist(err) {
filePath = path.Join(filePath, indexFile)
found = true
break
}
}
if !found {
if serve.List { if serve.List {
dirList, err := buildDirList(r.URL, filePath)
if err != nil {
status := http.StatusInternalServerError status := http.StatusInternalServerError
http.Error(w, "HTTPS dir lost not yet implemented", status) http.Error(w, "Failed to build directory listing", status)
return status, -1, serve.Log return status, -1, serve.Log
} }
result := gmitohtml.Convert([]byte(dirList), r.URL.String())
status := http.StatusOK
w.Header().Set("Content-Type", htmlType)
w.WriteHeader(status)
w.Write(result)
return status, int64(len(result)), serve.Log
}
http.NotFound(w, r) http.NotFound(w, r)
return http.StatusNotFound, -1, serve.Log return http.StatusNotFound, -1, serve.Log
} }
filePath = path.Join(filePath, "index.gemini")
} else {
filePath = path.Join(filePath, "index.gmi")
}
} else if hasTrailingSlash && len(r.URL.Path) > 1 { } else if hasTrailingSlash && len(r.URL.Path) > 1 {
u, err := url.Parse(r.URL.String()) u, err := url.Parse(r.URL.String())
if err != nil { if err != nil {
@ -152,10 +165,20 @@ func serveHTTPS(w http.ResponseWriter, r *http.Request) (int, int64, string) {
return status, -1, serve.Log return status, -1, serve.Log
} }
result := gmitohtml.Convert([]byte(data), r.URL.String()) var result []byte
contentType := htmlType
fileExt := strings.ToLower(filepath.Ext(filePath))
if fileExt == ".gmi" || fileExt == ".gemini" {
result = gmitohtml.Convert([]byte(data), r.URL.String())
} else if fileExt == ".htm" || fileExt == ".html" {
result = data
} else {
result = data
contentType = plainType
}
status := http.StatusOK status := http.StatusOK
w.Header().Set("Content-Type", htmlType) w.Header().Set("Content-Type", contentType)
w.WriteHeader(status) w.WriteHeader(status)
w.Write(result) w.Write(result)

View file

@ -2,7 +2,6 @@ package main
import ( import (
"bufio" "bufio"
"bytes"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
@ -61,6 +60,8 @@ var newLine = "\r\n"
var logLock sync.Mutex var logLock sync.Mutex
var indexFiles = []string{"index.gmi", "index.gemini"}
func writeHeader(c net.Conn, code int, meta string) int { func writeHeader(c net.Conn, code int, meta string) int {
fmt.Fprintf(c, "%d %s%s", code, meta, newLine) fmt.Fprintf(c, "%d %s%s", code, meta, newLine)
return code return code
@ -110,22 +111,6 @@ func writeSuccess(c net.Conn, serve *pathConfig, contentType string, size int64)
return statusSuccess return statusSuccess
} }
func scanCRLF(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\r'); i >= 0 {
// We have a full newline-terminated line.
return i + 1, data[0:i], nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), data, nil
}
// Request more data.
return 0, nil, nil
}
func replaceWithUserInput(command []string, request *url.URL) []string { func replaceWithUserInput(command []string, request *url.URL) []string {
newCommand := make([]string, len(command)) newCommand := make([]string, len(command))
copy(newCommand, command) copy(newCommand, command)
@ -229,19 +214,21 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) (int, int64) {
return writeHeader(c, statusRedirectPermanent, request.String()+"/"), -1 return writeHeader(c, statusRedirectPermanent, request.String()+"/"), -1
} }
_, err := os.Stat(path.Join(filePath, "index.gmi")) var found bool
if err != nil { for _, indexFile := range indexFiles {
_, err := os.Stat(path.Join(filePath, "index.gemini")) _, err := os.Stat(path.Join(filePath, indexFile))
if err != nil { if err == nil || os.IsExist(err) {
filePath = path.Join(filePath, indexFile)
found = true
break
}
}
if !found {
if serve.List { if serve.List {
return serveDirList(c, serve, request, filePath), -1 return serveDirList(c, serve, request, filePath), -1
} }
return writeStatus(c, statusNotFound), -1 return writeStatus(c, statusNotFound), -1
} }
filePath = path.Join(filePath, "index.gemini")
} else {
filePath = path.Join(filePath, "index.gmi")
}
} else if hasTrailingSlash && len(request.Path) > 1 { } else if hasTrailingSlash && len(request.Path) > 1 {
r := request.String() r := request.String()
return writeHeader(c, statusRedirectPermanent, r[:len(r)-1]), -1 return writeHeader(c, statusRedirectPermanent, r[:len(r)-1]), -1