diff --git a/CONFIGURATION.md b/CONFIGURATION.md index ea54d75..d00358d 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -2,6 +2,12 @@ Paths may be defined as fixed strings or regular expressions (starting with `^`) Fixed string paths will match with and without a trailing slash. +Serve entries have either a `root` path or `proxy` URL. When a `root` path is +provided static files and directories are served from that location. When a +`proxy` URL is provided requests are forwarded to the Gemini server at that URL. + +When accessing a directory `index.gemini` or `index.gmi` is served. + # config.yaml ```yaml @@ -9,7 +15,7 @@ Fixed string paths will match with and without a trailing slash. cert: /home/twins/data/certfile.crt key: /home/twins/data/keyfile.key -# Paths to serve +# Server paths serve: - path: /sites @@ -17,6 +23,9 @@ serve: - path: ^/(help|info)$ root: /home/twins/data/help + - + path: ^/proxy-example$ + proxy: gemini://gemini.rocks - path: / root: /home/twins/data/home diff --git a/README.md b/README.md index a320445..1298de3 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ ## Features - Serve static files +- Reverse proxy support ## Download diff --git a/config.go b/config.go index 94f8b2b..832e516 100644 --- a/config.go +++ b/config.go @@ -3,6 +3,7 @@ package main import ( "errors" "io/ioutil" + "log" "os" "regexp" @@ -11,7 +12,9 @@ import ( type serveConfig struct { Path string - Root string + + Root string + Proxy string r *regexp.Regexp } @@ -46,7 +49,9 @@ func readconfig(configPath string) error { for _, serve := range config.Serve { if serve.Path == "" { - continue + log.Fatal("path must be specified in serve entry") + } else if serve.Root != "" && serve.Proxy != "" { + log.Fatal("only one root or reverse proxy may defined for a serve entry") } if serve.Path[0] == '^' { diff --git a/main.go b/main.go index 89ffdb9..03ef6df 100644 --- a/main.go +++ b/main.go @@ -7,8 +7,15 @@ import ( "path" ) +func init() { + log.SetOutput(os.Stdout) +} + +var verbose bool + func main() { configFile := flag.String("config", "", "path to configuration file") + flag.BoolVar(&verbose, "verbose", false, "print request and response information") flag.Parse() if *configFile == "" { diff --git a/server.go b/server.go index 6df72f8..bd85bda 100644 --- a/server.go +++ b/server.go @@ -12,13 +12,20 @@ import ( "os" "path" "strings" + "time" "github.com/h2non/filetype" "github.com/makeworld-the-better-one/go-gemini" ) +const readTimeout = 30 * time.Second + func writeHeader(c net.Conn, code int, meta string) { fmt.Fprintf(c, "%d %s\r\n", code, meta) + + if verbose { + log.Printf("< %d %s\n", code, meta) + } } func writeStatus(c net.Conn, code int) { @@ -26,6 +33,8 @@ func writeStatus(c net.Conn, code int) { switch code { case gemini.StatusTemporaryFailure: meta = "Temporary failure" + case gemini.StatusProxyError: + meta = "Proxy error" case gemini.StatusBadRequest: meta = "Bad request" case gemini.StatusNotFound: @@ -34,6 +43,35 @@ func writeStatus(c net.Conn, code int) { writeHeader(c, code, meta) } +func serveProxy(c net.Conn, proxyURL, requestData 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, gemini.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 serveFile(c net.Conn, filePath string) { fi, err := os.Stat(filePath) if os.IsNotExist(err) { @@ -111,6 +149,8 @@ func scanCRLF(data []byte, atEOF bool) (advance int, token []byte, err error) { func handleConn(c net.Conn) { defer c.Close() + c.SetReadDeadline(time.Now().Add(readTimeout)) + var requestData string scanner := bufio.NewScanner(c) scanner.Split(scanCRLF) @@ -135,19 +175,28 @@ func handleConn(c net.Conn) { if strippedPath[0] == '/' { strippedPath = strippedPath[1:] } + if verbose { + log.Printf("> %s\n", request) + } - var realPath string for _, serve := range config.Serve { - if serve.r != nil && serve.r.Match(pathBytes) { - realPath = path.Join(serve.Root, strippedPath) - } else if serve.r == nil && strings.HasPrefix(request.Path, serve.Path) { - realPath = path.Join(serve.Root, request.Path[len(serve.Path):]) - } else { - continue + if serve.Proxy != "" { + if serve.r != nil && serve.r.Match(pathBytes) { + serveProxy(c, serve.Proxy, requestData) + return + } else if serve.r == nil && strings.HasPrefix(request.Path, serve.Path) { + serveProxy(c, serve.Proxy, requestData) + return + } } - serveFile(c, realPath) - return + if serve.r != nil && serve.r.Match(pathBytes) { + serveFile(c, path.Join(serve.Root, strippedPath)) + return + } else if serve.r == nil && strings.HasPrefix(request.Path, serve.Path) { + serveFile(c, path.Join(serve.Root, strippedPath[len(serve.Path)-1:])) + return + } } writeStatus(c, gemini.StatusNotFound)