twins-upstream/server.go

510 lines
12 KiB
Go
Raw Normal View History

package main
import (
"bufio"
"crypto/tls"
2020-11-04 21:48:55 +01:00
"crypto/x509"
"fmt"
2020-12-03 20:12:13 +01:00
"io"
"log"
"net"
2020-12-03 20:12:13 +01:00
"net/http"
"net/url"
"os"
"path"
2020-11-05 05:18:59 +01:00
"regexp"
"strconv"
"strings"
2020-11-12 18:56:59 +01:00
"sync"
2020-10-30 01:17:23 +01:00
"time"
"unicode/utf8"
)
2020-11-04 21:48:55 +01:00
const (
readTimeout = 30 * time.Second
urlMaxLength = 1024
2020-11-10 05:10:53 +01:00
2020-11-17 20:31:22 +01:00
plainType = "text/plain; charset=utf-8"
2020-11-10 05:10:53 +01:00
geminiType = "text/gemini; charset=utf-8"
2020-11-17 20:31:22 +01:00
htmlType = "text/html; charset=utf-8"
2020-12-03 20:12:13 +01:00
cssType = "text/css; charset=utf-8"
2020-11-16 18:17:42 +01:00
logTimeFormat = "2006-01-02 15:04:05"
2020-11-04 21:48:55 +01:00
)
const (
statusInput = 10
statusSensitiveInput = 11
statusSuccess = 20
statusRedirectTemporary = 30
statusRedirectPermanent = 31
statusTemporaryFailure = 40
statusUnavailable = 41
statusCGIError = 42
statusProxyError = 43
statusPermanentFailure = 50
statusNotFound = 51
statusGone = 52
statusProxyRequestRefused = 53
statusBadRequest = 59
)
2020-10-30 01:17:23 +01:00
2020-11-05 05:18:59 +01:00
var slashesRegexp = regexp.MustCompile(`[^\\]\/`)
2020-11-05 21:57:28 +01:00
var newLine = "\r\n"
2020-11-12 18:56:59 +01:00
var logLock sync.Mutex
2020-10-30 01:17:23 +01:00
2020-12-04 01:45:20 +01:00
var indexFiles = []string{"index.gmi", "index.gemini"}
2020-11-12 18:56:59 +01:00
func writeHeader(c net.Conn, code int, meta string) int {
fmt.Fprintf(c, "%d %s%s", code, meta, newLine)
return code
}
2020-11-12 18:56:59 +01:00
func writeStatus(c net.Conn, code int) int {
var meta string
switch code {
2020-11-04 21:48:55 +01:00
case statusTemporaryFailure:
meta = "Temporary failure"
2020-11-04 21:48:55 +01:00
case statusProxyError:
2020-10-30 01:17:23 +01:00
meta = "Proxy error"
2020-11-04 21:48:55 +01:00
case statusBadRequest:
meta = "Bad request"
2020-11-04 21:48:55 +01:00
case statusNotFound:
meta = "Not found"
2020-11-04 21:48:55 +01:00
case statusProxyRequestRefused:
meta = "Proxy request refused"
}
writeHeader(c, code, meta)
2020-11-12 18:56:59 +01:00
return code
}
2020-11-12 18:56:59 +01:00
func writeSuccess(c net.Conn, serve *pathConfig, contentType string, size int64) int {
2020-12-02 19:29:23 +01:00
// Content type
2020-11-10 05:10:53 +01:00
meta := contentType
if serve.Type != "" {
meta = serve.Type
}
2020-12-02 19:29:23 +01:00
// Cache
2020-11-10 06:12:22 +01:00
if serve.cache != cacheUnset {
meta += fmt.Sprintf("; cache=%d", serve.cache)
2020-11-10 05:10:53 +01:00
}
2020-12-02 19:29:23 +01:00
// Language
if serve.Lang != "" {
meta += fmt.Sprintf("; lang=%s", serve.Lang)
}
// Size
if !config.DisableSize && size >= 0 {
meta += fmt.Sprintf("; size=%d", size)
}
2020-11-10 05:10:53 +01:00
writeHeader(c, statusSuccess, meta)
2020-11-12 18:56:59 +01:00
return statusSuccess
2020-11-10 05:10:53 +01:00
}
2020-11-05 05:18:59 +01:00
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") {
2020-11-05 05:18:59 +01:00
requestQuery, err := url.QueryUnescape(request.RawQuery)
if err == nil {
newCommand[i] = strings.ReplaceAll(piece, "$USERINPUT", requestQuery)
}
}
}
return newCommand
}
2020-11-12 18:56:59 +01:00
func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) (int, int64) {
2020-11-05 05:18:59 +01:00
resolvedPath := request.Path
requestSplit := strings.Split(request.Path, "/")
pathSlashes := len(slashesRegexp.FindAllStringIndex(serve.Path, -1))
if len(serve.Path) > 0 {
if serve.Path[0] == '/' {
pathSlashes++ // Regexp does not match starting slash
}
}
if len(requestSplit) >= pathSlashes {
resolvedPath = strings.Join(requestSplit[pathSlashes:], "/")
2020-11-05 05:18:59 +01:00
}
2020-11-19 17:57:28 +01:00
if !serve.Hidden {
for _, piece := range requestSplit {
if len(piece) > 0 && piece[0] == '.' {
return writeStatus(c, statusNotFound), -1
}
}
}
var filePath string
if serve.Root != "" {
root := serve.Root
if root[len(root)-1] != '/' {
root += "/"
}
if !serve.SymLinks {
for i := range requestSplit[pathSlashes:] {
info, err := os.Lstat(path.Join(root, strings.Join(requestSplit[pathSlashes:pathSlashes+i+1], "/")))
if err != nil {
return writeStatus(c, statusNotFound), -1
} else if info.Mode()&os.ModeSymlink == os.ModeSymlink {
2020-11-12 18:56:59 +01:00
return writeStatus(c, statusTemporaryFailure), -1
}
}
}
filePath = path.Join(root, resolvedPath)
}
2020-11-19 18:24:12 +01:00
if serve.cmd != nil {
requireInput := serve.Input != "" || serve.SensitiveInput != ""
if requireInput {
newCommand := replaceWithUserInput(serve.cmd, request)
if newCommand != nil {
return serveCommand(c, serve, request, newCommand)
}
}
return serveCommand(c, serve, request, serve.cmd)
} else if serve.Proxy != "" {
2020-11-12 18:56:59 +01:00
return serveProxy(c, request, serve.Proxy), -1
2020-11-05 05:18:59 +01:00
} else if serve.FastCGI != "" {
if filePath == "" {
2020-11-12 18:56:59 +01:00
return writeStatus(c, statusNotFound), -1
}
2020-11-10 05:10:53 +01:00
contentType := geminiType
2020-11-05 05:18:59 +01:00
if serve.Type != "" {
contentType = serve.Type
}
2020-11-10 05:10:53 +01:00
writeSuccess(c, serve, contentType, -1)
2020-10-30 01:17:23 +01:00
2020-11-05 05:18:59 +01:00
serveFastCGI(c, config.fcgiPools[serve.FastCGI], request, filePath)
2020-11-12 18:56:59 +01:00
return statusSuccess, -1
2020-11-19 18:24:12 +01:00
} else if serve.Redirect != "" {
return writeHeader(c, statusRedirectTemporary, serve.Redirect), -1
2020-11-05 05:18:59 +01:00
}
if filePath == "" {
2020-11-12 18:56:59 +01:00
return writeStatus(c, statusNotFound), -1
2020-11-05 05:18:59 +01:00
}
fi, err := os.Stat(filePath)
if err != nil {
2020-11-12 18:56:59 +01:00
return writeStatus(c, statusNotFound), -1
}
mode := fi.Mode()
hasTrailingSlash := len(request.Path) > 0 && request.Path[len(request.Path)-1] == '/'
if mode.IsDir() {
if !hasTrailingSlash {
2020-11-12 18:56:59 +01:00
return writeHeader(c, statusRedirectPermanent, request.String()+"/"), -1
}
2020-12-04 01:45:20 +01:00
var found bool
for _, indexFile := range indexFiles {
_, err := os.Stat(path.Join(filePath, indexFile))
if err == nil || os.IsExist(err) {
filePath = path.Join(filePath, indexFile)
found = true
break
}
2020-12-04 01:45:20 +01:00
}
if !found {
if serve.List {
return serveDirList(c, serve, request, filePath), -1
}
return writeStatus(c, statusNotFound), -1
}
} else if hasTrailingSlash && len(request.Path) > 1 {
r := request.String()
2020-11-12 18:56:59 +01:00
return writeHeader(c, statusRedirectPermanent, r[:len(r)-1]), -1
}
2020-11-10 05:10:53 +01:00
serveFile(c, serve, filePath)
2020-11-12 18:56:59 +01:00
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 {
matchedRegexp = serve.r != nil && serve.r.Match(append(pathBytes[:], byte('/')))
matchedPrefix = serve.r == nil && strings.HasPrefix(request.Path+"/", serve.Path)
if matchedRegexp || matchedPrefix {
newRequest, err := url.Parse(request.String())
if err != nil {
return writeStatus(c, statusBadRequest), -1, ""
}
newRequest.Path += "/"
return writeHeader(c, statusRedirectTemporary, newRequest.String()), -1, serve.Log
}
2020-11-12 18:56:59 +01:00
continue
}
requireInput := serve.Input != "" || serve.SensitiveInput != ""
if request.RawQuery == "" && requireInput {
if serve.SensitiveInput != "" {
return writeHeader(c, statusSensitiveInput, serve.SensitiveInput), -1, serve.Log
2020-11-12 18:56:59 +01:00
}
return writeHeader(c, statusInput, serve.Input), -1, serve.Log
2020-11-12 18:56:59 +01:00
}
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, ""
2020-11-05 05:18:59 +01:00
}
2020-11-12 18:56:59 +01:00
func handleConn(c *tls.Conn) {
t := time.Now()
var request *url.URL
var logPath string
2020-12-09 03:53:02 +01:00
method := "GET"
2020-11-12 18:56:59 +01:00
status := 0
size := int64(-1)
2020-12-03 20:12:13 +01:00
protocol := "Gemini"
2020-11-12 18:56:59 +01:00
defer func() {
2020-11-17 05:21:00 +01:00
if quiet && logPath == "" {
2020-11-12 18:56:59 +01:00
return
}
2020-12-09 03:53:02 +01:00
entry := logEntry(method, protocol, request, status, size, time.Since(t))
2020-11-12 18:56:59 +01:00
2020-11-17 05:21:00 +01:00
if !quiet {
2020-11-12 18:56:59 +01:00
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"))
}()
c.SetReadDeadline(time.Now().Add(readTimeout))
2020-12-03 20:12:13 +01:00
defer c.Close()
2020-11-12 18:56:59 +01:00
2020-12-03 20:12:13 +01:00
var dataBuf []byte
var buf = make([]byte, 1)
var readCR bool
if config.SaneEOL {
readCR = true
2020-11-05 21:57:28 +01:00
}
2020-12-03 20:12:13 +01:00
for {
n, err := c.Read(buf)
if err == io.EOF {
break
} else if err != nil || n != 1 {
2021-07-10 18:13:39 +02:00
if debug {
log.Printf("error: failed to read client: %s", err)
}
2020-12-03 20:12:13 +01:00
return
}
if buf[0] == '\r' {
readCR = true
continue
} else if readCR && buf[0] == '\n' {
break
}
dataBuf = append(dataBuf, buf[0])
}
2020-12-03 20:12:13 +01:00
requestData := string(dataBuf)
2020-11-04 21:48:55 +01:00
state := c.ConnectionState()
certs := state.PeerCertificates
var clientCertKeys [][]byte
for _, cert := range certs {
pubKey, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
if err != nil {
continue
}
clientCertKeys = append(clientCertKeys, pubKey)
}
2020-12-09 03:53:02 +01:00
if strings.HasPrefix(requestData, "GET ") || strings.HasPrefix(requestData, "HEAD ") {
2020-12-03 20:12:13 +01:00
w := newResponseWriter(c)
defer w.WriteHeader(http.StatusOK)
if config.DisableHTTPS {
status = statusProxyRequestRefused
http.Error(w, "Error: Proxy request refused", http.StatusBadRequest)
return
}
r, err := http.ReadRequest(bufio.NewReader(io.MultiReader(strings.NewReader(requestData+"\r\n"), c)))
if err != nil {
status = http.StatusInternalServerError
http.Error(w, err.Error(), status)
return
}
if r.Proto == "" {
protocol = "HTTP"
} else {
protocol = r.Proto
}
r.URL.Scheme = "https"
r.URL.Host = strings.ToLower(r.Host)
2020-12-09 03:53:02 +01:00
method = r.Method
2020-12-03 20:12:13 +01:00
request = r.URL
status, size, logPath = serveHTTPS(w, r)
return
}
2020-11-04 21:48:55 +01:00
if len(requestData) > urlMaxLength || !utf8.ValidString(requestData) {
2020-11-12 18:56:59 +01:00
status = writeStatus(c, statusBadRequest)
return
}
2020-11-12 18:56:59 +01:00
var err error
request, err = url.Parse(requestData)
if err != nil {
2020-11-12 18:56:59 +01:00
status = writeStatus(c, statusBadRequest)
return
}
2020-12-03 20:12:13 +01:00
request.Host = strings.ToLower(request.Host)
requestHostname := request.Hostname()
if requestHostname == "" || strings.ContainsRune(requestHostname, ' ') {
2020-11-12 18:56:59 +01:00
status = writeStatus(c, statusBadRequest)
return
}
var requestPort int
if request.Port() != "" {
requestPort, err = strconv.Atoi(request.Port())
if err != nil {
requestPort = 0
}
}
2020-12-03 20:12:13 +01:00
validScheme := request.Scheme == "gemini" || (!config.DisableHTTPS && request.Scheme == "https")
if !validScheme || (requestPort > 0 && requestPort != config.port) {
2020-11-12 18:56:59 +01:00
status = writeStatus(c, statusProxyRequestRefused)
return
}
2020-11-12 18:56:59 +01:00
status, size, logPath = handleRequest(c, request, requestData)
}
2020-12-09 03:53:02 +01:00
func logEntry(method string, protocol string, request *url.URL, status int, size int64, elapsed time.Duration) []byte {
2020-11-12 18:56:59 +01:00
hostFormatted := "-"
2020-11-16 18:17:42 +01:00
pathFormatted := "-"
sizeFormatted := "-"
if request != nil {
if request.Path != "" {
pathFormatted = request.Path
}
if request.Hostname() != "" {
hostFormatted = request.Hostname()
if request.Port() != "" {
hostFormatted += ":" + request.Port()
} else {
hostFormatted += ":1965"
}
2020-10-30 01:17:23 +01:00
}
}
2020-11-12 18:56:59 +01:00
if size >= 0 {
sizeFormatted = strconv.FormatInt(size, 10)
}
2020-12-09 03:53:02 +01:00
return []byte(fmt.Sprintf(`%s - - - [%s] "%s %s %s" %d %s %.4f`, hostFormatted, time.Now().Format(logTimeFormat), method, pathFormatted, protocol, status, sizeFormatted, elapsed.Seconds()))
2020-11-12 18:56:59 +01:00
}
2020-11-05 05:18:59 +01:00
2020-11-12 18:56:59 +01:00
func handleListener(l net.Listener) {
for {
conn, err := l.Accept()
if err != nil {
log.Fatal(err)
}
2020-11-05 05:18:59 +01:00
2020-11-12 18:56:59 +01:00
go handleConn(conn.(*tls.Conn))
}
2020-11-05 05:18:59 +01:00
}
func getCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
host := config.Hosts[info.ServerName]
if host != nil {
return host.cert, nil
}
for _, host := range config.Hosts {
return host.cert, nil
}
return nil, nil
}
func listen(address string) {
tlsConfig := &tls.Config{
ClientAuth: tls.RequestClientCert,
GetCertificate: getCertificate,
2021-01-03 11:17:50 +01:00
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: true,
}
listener, err := tls.Listen("tcp", address, tlsConfig)
if err != nil {
log.Fatalf("failed to listen on %s: %s", address, err)
}
handleListener(listener)
}