// 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") }