Support proxying requests

This commit is contained in:
Trevor Slocum 2020-10-29 17:17:23 -07:00
parent d66e5d6384
commit 29c059c43f
5 changed files with 83 additions and 12 deletions

View file

@ -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. 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 # config.yaml
```yaml ```yaml
@ -9,7 +15,7 @@ Fixed string paths will match with and without a trailing slash.
cert: /home/twins/data/certfile.crt cert: /home/twins/data/certfile.crt
key: /home/twins/data/keyfile.key key: /home/twins/data/keyfile.key
# Paths to serve # Server paths
serve: serve:
- -
path: /sites path: /sites
@ -17,6 +23,9 @@ serve:
- -
path: ^/(help|info)$ path: ^/(help|info)$
root: /home/twins/data/help root: /home/twins/data/help
-
path: ^/proxy-example$
proxy: gemini://gemini.rocks
- -
path: / path: /
root: /home/twins/data/home root: /home/twins/data/home

View file

@ -7,6 +7,7 @@
## Features ## Features
- Serve static files - Serve static files
- Reverse proxy support
## Download ## Download

View file

@ -3,6 +3,7 @@ package main
import ( import (
"errors" "errors"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"regexp" "regexp"
@ -11,7 +12,9 @@ import (
type serveConfig struct { type serveConfig struct {
Path string Path string
Root string
Root string
Proxy string
r *regexp.Regexp r *regexp.Regexp
} }
@ -46,7 +49,9 @@ func readconfig(configPath string) error {
for _, serve := range config.Serve { for _, serve := range config.Serve {
if serve.Path == "" { 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] == '^' { if serve.Path[0] == '^' {

View file

@ -7,8 +7,15 @@ import (
"path" "path"
) )
func init() {
log.SetOutput(os.Stdout)
}
var verbose bool
func main() { func main() {
configFile := flag.String("config", "", "path to configuration file") configFile := flag.String("config", "", "path to configuration file")
flag.BoolVar(&verbose, "verbose", false, "print request and response information")
flag.Parse() flag.Parse()
if *configFile == "" { if *configFile == "" {

View file

@ -12,13 +12,20 @@ import (
"os" "os"
"path" "path"
"strings" "strings"
"time"
"github.com/h2non/filetype" "github.com/h2non/filetype"
"github.com/makeworld-the-better-one/go-gemini" "github.com/makeworld-the-better-one/go-gemini"
) )
const readTimeout = 30 * time.Second
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)
if verbose {
log.Printf("< %d %s\n", code, meta)
}
} }
func writeStatus(c net.Conn, code int) { func writeStatus(c net.Conn, code int) {
@ -26,6 +33,8 @@ func writeStatus(c net.Conn, code int) {
switch code { switch code {
case gemini.StatusTemporaryFailure: case gemini.StatusTemporaryFailure:
meta = "Temporary failure" meta = "Temporary failure"
case gemini.StatusProxyError:
meta = "Proxy error"
case gemini.StatusBadRequest: case gemini.StatusBadRequest:
meta = "Bad request" meta = "Bad request"
case gemini.StatusNotFound: case gemini.StatusNotFound:
@ -34,6 +43,35 @@ func writeStatus(c net.Conn, code int) {
writeHeader(c, code, meta) 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) { func serveFile(c net.Conn, filePath string) {
fi, err := os.Stat(filePath) fi, err := os.Stat(filePath)
if os.IsNotExist(err) { 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) { func handleConn(c net.Conn) {
defer c.Close() defer c.Close()
c.SetReadDeadline(time.Now().Add(readTimeout))
var requestData string var requestData string
scanner := bufio.NewScanner(c) scanner := bufio.NewScanner(c)
scanner.Split(scanCRLF) scanner.Split(scanCRLF)
@ -135,19 +175,28 @@ func handleConn(c net.Conn) {
if strippedPath[0] == '/' { if strippedPath[0] == '/' {
strippedPath = strippedPath[1:] strippedPath = strippedPath[1:]
} }
if verbose {
log.Printf("> %s\n", request)
}
var realPath string
for _, serve := range config.Serve { for _, serve := range config.Serve {
if serve.r != nil && serve.r.Match(pathBytes) { if serve.Proxy != "" {
realPath = path.Join(serve.Root, strippedPath) if serve.r != nil && serve.r.Match(pathBytes) {
} else if serve.r == nil && strings.HasPrefix(request.Path, serve.Path) { serveProxy(c, serve.Proxy, requestData)
realPath = path.Join(serve.Root, request.Path[len(serve.Path):]) return
} else { } else if serve.r == nil && strings.HasPrefix(request.Path, serve.Path) {
continue serveProxy(c, serve.Proxy, requestData)
return
}
} }
serveFile(c, realPath) if serve.r != nil && serve.r.Match(pathBytes) {
return 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) writeStatus(c, gemini.StatusNotFound)