relayd/main.go

127 lines
2.8 KiB
Go

// Command relayd is a simple TLS terminator using let's encrypt.
package main
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"flag"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
"time"
"github.com/facebookgo/flagenv"
"golang.org/x/crypto/acme/autocert"
)
func fwdhttps(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "POST", "PUT", "PATCH":
http.Error(w, "HTTPS access required", 400)
return
default:
http.RedirectHandler(fmt.Sprintf("https://%s%s", r.Host, r.RequestURI), http.StatusPermanentRedirect).ServeHTTP(w, r)
}
}
var (
insecurePort = flag.String("insecure-bind", ":80", "host/port to bind on for insecure (HTTP) traffic")
securePort = flag.String("secure-bind", ":443", "host/port to bind on for secure (HTTPS) traffic")
config = flag.String("cfg", "./relayd.json", "config file to read backend data from")
dataDir = flag.String("data-dir", "./.relayd", "data dir for certificates")
)
type Site struct {
Domain string `json:"domain"`
URL *url.URL `json:"-"`
Target string `json:"target"`
PathPrefix string `json:"path_prefix"`
}
type Config []Site
func main() {
flagenv.Parse()
flag.Parse()
go http.ListenAndServe(*insecurePort, http.HandlerFunc(fwdhttps))
var cfg Config
fin, err := os.Open(*config)
if err != nil {
log.Fatalf("can't open config file: %v", err)
}
defer fin.Close()
err = json.NewDecoder(fin).Decode(&cfg)
if err != nil {
log.Fatalf("can't parse config: %v", err)
}
m := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: cfg.checkCert,
Cache: autocert.DirCache(*dataDir),
}
for _, cfg := range cfg {
u, err := url.Parse(cfg.Target + cfg.PathPrefix)
if err != nil {
log.Fatalf("%v somehow isn't a url: %v", cfg.Target, err)
}
cfg.URL = u
}
go func() {
err := http.ListenAndServe(*insecurePort, m.HTTPHandler(http.HandlerFunc(http.NotFound)))
if err != nil {
log.Fatal(err)
}
}()
s := &http.Server{
IdleTimeout: 5 * time.Minute,
Addr: *securePort,
TLSConfig: &tls.Config{GetCertificate: m.GetCertificate},
Handler: cfg,
}
s.ListenAndServeTLS("", "")
}
func (c Config) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var s Site
for _, cfg := range c {
if r.Host == cfg.Domain && strings.HasPrefix(r.URL.Path, cfg.PathPrefix) {
s = cfg
}
}
if s == *new(Site) {
http.Error(w, "unknown domain "+r.Host, http.StatusNotFound)
return
}
if s.PathPrefix != "" {
r.URL.Path = strings.Split(r.URL.Path, s.PathPrefix)[1]
}
rp := httputil.NewSingleHostReverseProxy(s.URL)
rp.ServeHTTP(w, r)
}
func (c Config) checkCert(ctx context.Context, host string) error {
for _, cfg := range c {
if host == cfg.Domain {
return nil
}
}
return errors.New("not allowed")
}