mirror of
https://code.rocketnine.space/tslocum/twins.git
synced 2024-11-27 13:38:14 +01:00
Fix resource path resolution
This commit is contained in:
parent
8e2e404a81
commit
dc58324dff
4 changed files with 318 additions and 303 deletions
373
server.go
373
server.go
|
@ -6,21 +6,15 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"regexp"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/h2non/filetype"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -50,6 +44,8 @@ const (
|
||||||
statusBadRequest = 59
|
statusBadRequest = 59
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var slashesRegexp = regexp.MustCompile(`[^\\]\/`)
|
||||||
|
|
||||||
func writeHeader(c net.Conn, code int, meta string) {
|
func writeHeader(c net.Conn, code int, meta string) {
|
||||||
fmt.Fprintf(c, "%d %s\r\n", code, meta)
|
fmt.Fprintf(c, "%d %s\r\n", code, meta)
|
||||||
|
|
||||||
|
@ -75,222 +71,6 @@ func writeStatus(c net.Conn, code int) {
|
||||||
writeHeader(c, code, meta)
|
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) {
|
func scanCRLF(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
if atEOF && len(data) == 0 {
|
if atEOF && len(data) == 0 {
|
||||||
return 0, nil, nil
|
return 0, nil, nil
|
||||||
|
@ -307,34 +87,64 @@ 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 {
|
func replaceWithUserInput(command []string, request *url.URL) []string {
|
||||||
newCommand := make([]string, len(command))
|
newCommand := make([]string, len(command))
|
||||||
copy(newCommand, command)
|
copy(newCommand, command)
|
||||||
for i, piece := range newCommand {
|
for i, piece := range newCommand {
|
||||||
if strings.Contains(piece, "$USERINPUT") {
|
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
|
return newCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleConn(c *tls.Conn) {
|
func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
|
||||||
if verbose {
|
resolvedPath := request.Path
|
||||||
t := time.Now()
|
requestSplit := strings.Split(request.Path, "/")
|
||||||
defer func() {
|
pathSlashes := len(slashesRegexp.FindAllStringIndex(serve.Path, -1))
|
||||||
d := time.Since(t)
|
if len(request.Path) > 0 && request.Path[0] == '/' {
|
||||||
if d > time.Second {
|
pathSlashes++ // Regexp does not match starting slash
|
||||||
d = d.Round(time.Second)
|
|
||||||
} else {
|
|
||||||
d = d.Round(time.Millisecond)
|
|
||||||
}
|
}
|
||||||
log.Printf("took %s", d)
|
if len(requestSplit) >= pathSlashes+1 {
|
||||||
}()
|
resolvedPath = "/" + strings.Join(requestSplit[pathSlashes+1:], "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
defer c.Close()
|
if serve.Proxy != "" {
|
||||||
c.SetReadDeadline(time.Now().Add(readTimeout))
|
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
|
var requestData string
|
||||||
scanner := bufio.NewScanner(c)
|
scanner := bufio.NewScanner(c)
|
||||||
scanner.Split(scanCRLF)
|
scanner.Split(scanCRLF)
|
||||||
|
@ -342,7 +152,6 @@ func handleConn(c *tls.Conn) {
|
||||||
requestData = scanner.Text()
|
requestData = scanner.Text()
|
||||||
}
|
}
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
log.Println(scanner.Text(), "FAILED")
|
|
||||||
writeStatus(c, statusBadRequest)
|
writeStatus(c, statusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -405,11 +214,6 @@ func handleConn(c *tls.Conn) {
|
||||||
if strippedPath[0] == '/' {
|
if strippedPath[0] == '/' {
|
||||||
strippedPath = strippedPath[1:]
|
strippedPath = strippedPath[1:]
|
||||||
}
|
}
|
||||||
requestQuery, err := url.QueryUnescape(request.RawQuery)
|
|
||||||
if err != nil {
|
|
||||||
writeStatus(c, statusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var matchedHost bool
|
var matchedHost bool
|
||||||
for hostname := range config.Hosts {
|
for hostname := range config.Hosts {
|
||||||
|
@ -426,7 +230,7 @@ func handleConn(c *tls.Conn) {
|
||||||
}
|
}
|
||||||
|
|
||||||
requireInput := serve.Input != "" || serve.SensitiveInput != ""
|
requireInput := serve.Input != "" || serve.SensitiveInput != ""
|
||||||
if requestQuery == "" && requireInput {
|
if request.RawQuery == "" && requireInput {
|
||||||
if serve.Input != "" {
|
if serve.Input != "" {
|
||||||
writeHeader(c, statusInput, serve.Input)
|
writeHeader(c, statusInput, serve.Input)
|
||||||
return
|
return
|
||||||
|
@ -436,63 +240,8 @@ func handleConn(c *tls.Conn) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if matchedRegexp {
|
if matchedRegexp || matchedPrefix {
|
||||||
if serve.Proxy != "" {
|
servePath(c, request, serve)
|
||||||
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)
|
|
||||||
return
|
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) {
|
func getCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
host := config.Hosts[info.ServerName]
|
host := config.Hosts[info.ServerName]
|
||||||
if host != nil {
|
if host != nil {
|
||||||
|
|
43
server_command.go
Normal file
43
server_command.go
Normal 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
164
server_file.go
Normal 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
39
server_proxy.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue