|
|
|
@ -14,6 +14,7 @@ import (
|
|
|
|
|
"regexp"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
"unicode/utf8"
|
|
|
|
|
)
|
|
|
|
@ -51,15 +52,14 @@ var slashesRegexp = regexp.MustCompile(`[^\\]\/`)
|
|
|
|
|
|
|
|
|
|
var newLine = "\r\n"
|
|
|
|
|
|
|
|
|
|
func writeHeader(c net.Conn, code int, meta string) {
|
|
|
|
|
fmt.Fprintf(c, "%d %s%s", code, meta, newLine)
|
|
|
|
|
var logLock sync.Mutex
|
|
|
|
|
|
|
|
|
|
if verbose {
|
|
|
|
|
log.Printf("< %d %s\n", code, meta)
|
|
|
|
|
}
|
|
|
|
|
func writeHeader(c net.Conn, code int, meta string) int {
|
|
|
|
|
fmt.Fprintf(c, "%d %s%s", code, meta, newLine)
|
|
|
|
|
return code
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeStatus(c net.Conn, code int) {
|
|
|
|
|
func writeStatus(c net.Conn, code int) int {
|
|
|
|
|
var meta string
|
|
|
|
|
switch code {
|
|
|
|
|
case statusTemporaryFailure:
|
|
|
|
@ -74,9 +74,10 @@ func writeStatus(c net.Conn, code int) {
|
|
|
|
|
meta = "Proxy request refused"
|
|
|
|
|
}
|
|
|
|
|
writeHeader(c, code, meta)
|
|
|
|
|
return code
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeSuccess(c net.Conn, serve *pathConfig, contentType string, size int64) {
|
|
|
|
|
func writeSuccess(c net.Conn, serve *pathConfig, contentType string, size int64) int {
|
|
|
|
|
meta := contentType
|
|
|
|
|
if serve.Type != "" {
|
|
|
|
|
meta = serve.Type
|
|
|
|
@ -91,6 +92,7 @@ func writeSuccess(c net.Conn, serve *pathConfig, contentType string, size int64)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writeHeader(c, statusSuccess, meta)
|
|
|
|
|
return statusSuccess
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func scanCRLF(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
|
|
|
@ -123,7 +125,7 @@ func replaceWithUserInput(command []string, request *url.URL) []string {
|
|
|
|
|
return newCommand
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
|
|
|
|
|
func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) (int, int64) {
|
|
|
|
|
resolvedPath := request.Path
|
|
|
|
|
requestSplit := strings.Split(request.Path, "/")
|
|
|
|
|
pathSlashes := len(slashesRegexp.FindAllStringIndex(serve.Path, -1))
|
|
|
|
@ -142,8 +144,7 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
|
|
|
|
|
if !serve.HiddenFiles {
|
|
|
|
|
for _, piece := range requestSplit {
|
|
|
|
|
if len(piece) > 0 && piece[0] == '.' {
|
|
|
|
|
writeStatus(c, statusTemporaryFailure)
|
|
|
|
|
return
|
|
|
|
|
return writeStatus(c, statusTemporaryFailure), -1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -159,8 +160,7 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
|
|
|
|
|
for i := range requestSplit[pathSlashes:] {
|
|
|
|
|
info, err := os.Lstat(path.Join(root, strings.Join(requestSplit[pathSlashes:pathSlashes+i+1], "/")))
|
|
|
|
|
if err != nil || info.Mode()&os.ModeSymlink == os.ModeSymlink {
|
|
|
|
|
writeStatus(c, statusTemporaryFailure)
|
|
|
|
|
return
|
|
|
|
|
return writeStatus(c, statusTemporaryFailure), -1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -169,12 +169,10 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if serve.Proxy != "" {
|
|
|
|
|
serveProxy(c, request, serve.Proxy)
|
|
|
|
|
return
|
|
|
|
|
return serveProxy(c, request, serve.Proxy), -1
|
|
|
|
|
} else if serve.FastCGI != "" {
|
|
|
|
|
if filePath == "" {
|
|
|
|
|
writeStatus(c, statusNotFound)
|
|
|
|
|
return
|
|
|
|
|
return writeStatus(c, statusNotFound), -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
contentType := geminiType
|
|
|
|
@ -184,37 +182,32 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
|
|
|
|
|
writeSuccess(c, serve, contentType, -1)
|
|
|
|
|
|
|
|
|
|
serveFastCGI(c, config.fcgiPools[serve.FastCGI], request, filePath)
|
|
|
|
|
return
|
|
|
|
|
return statusSuccess, -1
|
|
|
|
|
} else if serve.cmd != nil {
|
|
|
|
|
requireInput := serve.Input != "" || serve.SensitiveInput != ""
|
|
|
|
|
if requireInput {
|
|
|
|
|
newCommand := replaceWithUserInput(serve.cmd, request)
|
|
|
|
|
if newCommand != nil {
|
|
|
|
|
serveCommand(c, serve, request, newCommand)
|
|
|
|
|
return
|
|
|
|
|
return serveCommand(c, serve, request, newCommand)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
serveCommand(c, serve, request, serve.cmd)
|
|
|
|
|
return
|
|
|
|
|
return serveCommand(c, serve, request, serve.cmd)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if filePath == "" {
|
|
|
|
|
writeStatus(c, statusNotFound)
|
|
|
|
|
return
|
|
|
|
|
return writeStatus(c, statusNotFound), -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fi, err := os.Stat(filePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeStatus(c, statusNotFound)
|
|
|
|
|
return
|
|
|
|
|
return writeStatus(c, statusNotFound), -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mode := fi.Mode()
|
|
|
|
|
hasTrailingSlash := len(request.Path) > 0 && request.Path[len(request.Path)-1] == '/'
|
|
|
|
|
if mode.IsDir() {
|
|
|
|
|
if !hasTrailingSlash {
|
|
|
|
|
writeHeader(c, statusRedirectPermanent, request.String()+"/")
|
|
|
|
|
return
|
|
|
|
|
return writeHeader(c, statusRedirectPermanent, request.String()+"/"), -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err := os.Stat(path.Join(filePath, "index.gmi"))
|
|
|
|
@ -222,11 +215,9 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
|
|
|
|
|
_, err := os.Stat(path.Join(filePath, "index.gemini"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
if serve.ListDirectory {
|
|
|
|
|
serveDirList(c, serve, request, filePath)
|
|
|
|
|
return
|
|
|
|
|
return serveDirList(c, serve, request, filePath), -1
|
|
|
|
|
}
|
|
|
|
|
writeStatus(c, statusNotFound)
|
|
|
|
|
return
|
|
|
|
|
return writeStatus(c, statusNotFound), -1
|
|
|
|
|
}
|
|
|
|
|
filePath = path.Join(filePath, "index.gemini")
|
|
|
|
|
} else {
|
|
|
|
@ -234,14 +225,105 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) {
|
|
|
|
|
}
|
|
|
|
|
} else if hasTrailingSlash && len(request.Path) > 1 {
|
|
|
|
|
r := request.String()
|
|
|
|
|
writeHeader(c, statusRedirectPermanent, r[:len(r)-1])
|
|
|
|
|
return
|
|
|
|
|
return writeHeader(c, statusRedirectPermanent, r[:len(r)-1]), -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serveFile(c, serve, filePath)
|
|
|
|
|
return statusSuccess, fi.Size()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func handleRequest(c *tls.Conn, request *url.URL, requestData string) (int, int64, string) {
|
|
|
|
|
if request.Path == "" {
|
|
|
|
|
// Redirect to /
|
|
|
|
|
return writeHeader(c, statusRedirectPermanent, requestData+"/"), -1, ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pathBytes := []byte(request.Path)
|
|
|
|
|
strippedPath := request.Path
|
|
|
|
|
if strippedPath[0] == '/' {
|
|
|
|
|
strippedPath = strippedPath[1:]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var matchedHost bool
|
|
|
|
|
requestHostname := request.Hostname()
|
|
|
|
|
for hostname := range config.Hosts {
|
|
|
|
|
if requestHostname != hostname {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
matchedHost = true
|
|
|
|
|
|
|
|
|
|
for _, serve := range config.Hosts[hostname].Paths {
|
|
|
|
|
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 request.RawQuery == "" && requireInput {
|
|
|
|
|
if serve.Input != "" {
|
|
|
|
|
return writeHeader(c, statusInput, serve.Input), -1, ""
|
|
|
|
|
} else if serve.SensitiveInput != "" {
|
|
|
|
|
return writeHeader(c, statusSensitiveInput, serve.SensitiveInput), -1, ""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if matchedRegexp || matchedPrefix {
|
|
|
|
|
status, size := servePath(c, request, serve)
|
|
|
|
|
return status, size, serve.Log
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if matchedHost {
|
|
|
|
|
return writeStatus(c, statusNotFound), -1, ""
|
|
|
|
|
}
|
|
|
|
|
return writeStatus(c, statusProxyRequestRefused), -1, ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func serveConn(c *tls.Conn) {
|
|
|
|
|
func handleConn(c *tls.Conn) {
|
|
|
|
|
t := time.Now()
|
|
|
|
|
var request *url.URL
|
|
|
|
|
var logPath string
|
|
|
|
|
status := 0
|
|
|
|
|
size := int64(-1)
|
|
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
|
if !verbose && logPath == "" {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entry := logEntry(request, status, size, time.Since(t))
|
|
|
|
|
|
|
|
|
|
if verbose {
|
|
|
|
|
log.Println(string(entry))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if logPath == "" {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logLock.Lock()
|
|
|
|
|
defer logLock.Unlock()
|
|
|
|
|
|
|
|
|
|
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("ERROR: Failed to open log file at %s: %s", logPath, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
|
|
if _, err = f.Write(entry); err != nil {
|
|
|
|
|
log.Printf("ERROR: Failed to write to log file at %s: %s", logPath, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
f.Write([]byte("\n"))
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
defer c.Close()
|
|
|
|
|
c.SetReadDeadline(time.Now().Add(readTimeout))
|
|
|
|
|
|
|
|
|
|
var requestData string
|
|
|
|
|
scanner := bufio.NewScanner(c)
|
|
|
|
|
if !config.SaneEOL {
|
|
|
|
@ -251,7 +333,7 @@ func serveConn(c *tls.Conn) {
|
|
|
|
|
requestData = scanner.Text()
|
|
|
|
|
}
|
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
|
|
writeStatus(c, statusBadRequest)
|
|
|
|
|
status = writeStatus(c, statusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -266,24 +348,21 @@ func serveConn(c *tls.Conn) {
|
|
|
|
|
clientCertKeys = append(clientCertKeys, pubKey)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if verbose {
|
|
|
|
|
log.Printf("> %s\n", requestData)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(requestData) > urlMaxLength || !utf8.ValidString(requestData) {
|
|
|
|
|
writeStatus(c, statusBadRequest)
|
|
|
|
|
status = writeStatus(c, statusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
request, err := url.Parse(requestData)
|
|
|
|
|
var err error
|
|
|
|
|
request, err = url.Parse(requestData)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeStatus(c, statusBadRequest)
|
|
|
|
|
status = writeStatus(c, statusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
requestHostname := request.Hostname()
|
|
|
|
|
if requestHostname == "" || strings.ContainsRune(requestHostname, ' ') {
|
|
|
|
|
writeStatus(c, statusBadRequest)
|
|
|
|
|
status = writeStatus(c, statusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -299,79 +378,41 @@ func serveConn(c *tls.Conn) {
|
|
|
|
|
request.Scheme = "gemini"
|
|
|
|
|
}
|
|
|
|
|
if request.Scheme != "gemini" || (requestPort > 0 && requestPort != config.port) {
|
|
|
|
|
writeStatus(c, statusProxyRequestRefused)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if request.Path == "" {
|
|
|
|
|
// Redirect to /
|
|
|
|
|
writeHeader(c, statusRedirectPermanent, requestData+"/")
|
|
|
|
|
status = writeStatus(c, statusProxyRequestRefused)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pathBytes := []byte(request.Path)
|
|
|
|
|
strippedPath := request.Path
|
|
|
|
|
if strippedPath[0] == '/' {
|
|
|
|
|
strippedPath = strippedPath[1:]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var matchedHost bool
|
|
|
|
|
for hostname := range config.Hosts {
|
|
|
|
|
if requestHostname != hostname {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
matchedHost = true
|
|
|
|
|
|
|
|
|
|
for _, serve := range config.Hosts[hostname].Paths {
|
|
|
|
|
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 request.RawQuery == "" && requireInput {
|
|
|
|
|
if serve.Input != "" {
|
|
|
|
|
writeHeader(c, statusInput, serve.Input)
|
|
|
|
|
return
|
|
|
|
|
} else if serve.SensitiveInput != "" {
|
|
|
|
|
writeHeader(c, statusSensitiveInput, serve.SensitiveInput)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
status, size, logPath = handleRequest(c, request, requestData)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if matchedRegexp || matchedPrefix {
|
|
|
|
|
servePath(c, request, serve)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
func logEntry(request *url.URL, status int, size int64, elapsed time.Duration) []byte {
|
|
|
|
|
hostFormatted := "-"
|
|
|
|
|
if request.Hostname() != "" {
|
|
|
|
|
hostFormatted = request.Hostname()
|
|
|
|
|
if request.Port() != "" {
|
|
|
|
|
hostFormatted += ":" + request.Port()
|
|
|
|
|
} else {
|
|
|
|
|
hostFormatted += ":1965"
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if matchedHost {
|
|
|
|
|
writeStatus(c, statusNotFound)
|
|
|
|
|
} else {
|
|
|
|
|
writeStatus(c, statusProxyRequestRefused)
|
|
|
|
|
timeFormatted := time.Now().Format("02/Jan/2006 03:04:05")
|
|
|
|
|
sizeFormatted := "-"
|
|
|
|
|
if size >= 0 {
|
|
|
|
|
sizeFormatted = strconv.FormatInt(size, 10)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
return []byte(fmt.Sprintf(`%s - - - [%s] "GET %s Gemini" %d %s %.4f`, hostFormatted, timeFormatted, request.Path, status, sizeFormatted, elapsed.Seconds()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defer c.Close()
|
|
|
|
|
c.SetReadDeadline(time.Now().Add(readTimeout))
|
|
|
|
|
func handleListener(l net.Listener) {
|
|
|
|
|
for {
|
|
|
|
|
conn, err := l.Accept()
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serveConn(c)
|
|
|
|
|
go handleConn(conn.(*tls.Conn))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
|
|
@ -385,17 +426,6 @@ func getCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func handleListener(l net.Listener) {
|
|
|
|
|
for {
|
|
|
|
|
conn, err := l.Accept()
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
go handleConn(conn.(*tls.Conn))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func listen(address string) {
|
|
|
|
|
tlsConfig := &tls.Config{
|
|
|
|
|
ClientAuth: tls.RequestClientCert,
|
|
|
|
|