summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorMatthew Holt <mholt@users.noreply.github.com>2019-05-07 09:56:13 -0600
committerMatthew Holt <mholt@users.noreply.github.com>2019-05-07 09:56:18 -0600
commite40bbecb16d196d2d700a9484e53c11b64dfe8d9 (patch)
treec4ab6cce5c98fdec6d9d5a3eaf666d5bea55a25f /modules
parent8eba582efe2ec8646447d8721a42c486363b3bc2 (diff)
Rough implementation of auto HTTP->HTTPS redirects
Also added GracePeriod for server shutdowns
Diffstat (limited to 'modules')
-rw-r--r--modules/caddyhttp/caddyhttp.go169
-rw-r--r--modules/caddyhttp/matchers.go12
-rw-r--r--modules/caddyhttp/replacer.go30
-rw-r--r--modules/caddyhttp/staticresp.go (renamed from modules/caddyhttp/staticresp/staticresp.go)16
4 files changed, 195 insertions, 32 deletions
diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go
index 0731fea..e309053 100644
--- a/modules/caddyhttp/caddyhttp.go
+++ b/modules/caddyhttp/caddyhttp.go
@@ -30,13 +30,15 @@ func init() {
}
type httpModuleConfig struct {
- Servers map[string]*httpServerConfig `json:"servers"`
+ HTTPPort int `json:"http_port"`
+ HTTPSPort int `json:"https_port"`
+ GracePeriod caddy2.Duration `json:"grace_period"`
+ Servers map[string]*httpServerConfig `json:"servers"`
servers []*http.Server
}
func (hc *httpModuleConfig) Provision() error {
- // TODO: Either prevent overlapping listeners on different servers, or combine them into one
for _, srv := range hc.Servers {
err := srv.Routes.setup()
if err != nil {
@@ -51,6 +53,27 @@ func (hc *httpModuleConfig) Provision() error {
return nil
}
+func (hc *httpModuleConfig) Validate() error {
+ // each server must use distinct listener addresses
+ lnAddrs := make(map[string]string)
+ for srvName, srv := range hc.Servers {
+ for _, addr := range srv.Listen {
+ netw, expanded, err := parseListenAddr(addr)
+ if err != nil {
+ return fmt.Errorf("invalid listener address '%s': %v", addr, err)
+ }
+ for _, a := range expanded {
+ if sn, ok := lnAddrs[netw+a]; ok {
+ return fmt.Errorf("listener address repeated: %s (already claimed by server '%s')", a, sn)
+ }
+ lnAddrs[netw+a] = srvName
+ }
+ }
+ }
+
+ return nil
+}
+
func (hc *httpModuleConfig) Start(handle caddy2.Handle) error {
err := hc.automaticHTTPS(handle)
if err != nil {
@@ -83,7 +106,12 @@ func (hc *httpModuleConfig) Start(handle caddy2.Handle) error {
}
// enable TLS
- if len(srv.TLSConnPolicies) > 0 {
+ httpPort := hc.HTTPPort
+ if httpPort == 0 {
+ httpPort = DefaultHTTPPort
+ }
+ _, port, _ := net.SplitHostPort(addr)
+ if len(srv.TLSConnPolicies) > 0 && port != strconv.Itoa(httpPort) {
tlsCfg, err := srv.TLSConnPolicies.TLSConfig(handle)
if err != nil {
return fmt.Errorf("%s/%s: making TLS configuration: %v", network, addr, err)
@@ -100,9 +128,16 @@ func (hc *httpModuleConfig) Start(handle caddy2.Handle) error {
return nil
}
+// Stop gracefully shuts down the HTTP server.
func (hc *httpModuleConfig) Stop() error {
+ ctx := context.Background()
+ if hc.GracePeriod > 0 {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, time.Duration(hc.GracePeriod))
+ defer cancel()
+ }
for _, s := range hc.servers {
- err := s.Shutdown(context.Background()) // TODO
+ err := s.Shutdown(ctx)
if err != nil {
return err
}
@@ -117,6 +152,9 @@ func (hc *httpModuleConfig) automaticHTTPS(handle caddy2.Handle) error {
}
tlsApp := tlsAppIface.(*caddytls.TLS)
+ lnAddrMap := make(map[string]struct{})
+ var redirRoutes routeList
+
for srvName, srv := range hc.Servers {
srv.tlsApp = tlsApp
@@ -157,13 +195,93 @@ func (hc *httpModuleConfig) automaticHTTPS(handle caddy2.Handle) error {
{ALPN: defaultALPN},
}
- // TODO: create HTTP->HTTPS redirects
+ if srv.DisableAutoHTTPSRedir {
+ continue
+ }
+
+ // create HTTP->HTTPS redirects
+ for _, addr := range srv.Listen {
+ netw, host, port, err := splitListenAddr(addr)
+ if err != nil {
+ return fmt.Errorf("%s: invalid listener address: %v", srvName, addr)
+ }
+ httpRedirLnAddr := joinListenAddr(netw, host, strconv.Itoa(hc.HTTPPort))
+ lnAddrMap[httpRedirLnAddr] = struct{}{}
+
+ if parts := strings.SplitN(port, "-", 2); len(parts) == 2 {
+ port = parts[0]
+ }
+ redirTo := "https://{request.host}"
+
+ httpsPort := hc.HTTPSPort
+ if httpsPort == 0 {
+ httpsPort = DefaultHTTPSPort
+ }
+ if port != strconv.Itoa(httpsPort) {
+ redirTo += ":" + port
+ }
+ redirTo += "{request.uri}"
+
+ redirRoutes = append(redirRoutes, serverRoute{
+ matchers: []RouteMatcher{
+ matchProtocol("http"),
+ matchHost(domains),
+ },
+ responder: Static{
+ StatusCode: http.StatusTemporaryRedirect, // TODO: use permanent redirect instead
+ Headers: http.Header{
+ "Location": []string{redirTo},
+ "Connection": []string{"close"},
+ },
+ Close: true,
+ },
+ })
+ }
+ }
+ }
+
+ if len(lnAddrMap) > 0 {
+ var lnAddrs []string
+ mapLoop:
+ for addr := range lnAddrMap {
+ netw, addrs, err := parseListenAddr(addr)
+ if err != nil {
+ continue
+ }
+ for _, a := range addrs {
+ if hc.listenerTaken(netw, a) {
+ continue mapLoop
+ }
+ }
+ lnAddrs = append(lnAddrs, addr)
+ }
+ hc.Servers["auto_https_redirects"] = &httpServerConfig{
+ Listen: lnAddrs,
+ Routes: redirRoutes,
+ DisableAutoHTTPS: true,
}
}
return nil
}
+func (hc *httpModuleConfig) listenerTaken(network, address string) bool {
+ for _, srv := range hc.Servers {
+ for _, addr := range srv.Listen {
+ netw, addrs, err := parseListenAddr(addr)
+ if err != nil || netw != network {
+ continue
+ }
+ for _, a := range addrs {
+ if a == address {
+ return true
+ }
+ }
+ }
+ }
+ return false
+}
+
var defaultALPN = []string{"h2", "http/1.1"}
type httpServerConfig struct {
@@ -204,6 +322,7 @@ func (s httpServerConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// it can be accessed by error handlers
c := context.WithValue(r.Context(), ErrorCtxKey, err)
r = r.WithContext(c)
+ // TODO: add error values to Replacer
if len(s.Errors.Routes) == 0 {
// TODO: implement a default error handler?
@@ -284,13 +403,11 @@ func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
var emptyHandler HandlerFunc = func(w http.ResponseWriter, r *http.Request) error { return nil }
func parseListenAddr(a string) (network string, addrs []string, err error) {
- network = "tcp"
- if idx := strings.Index(a, "/"); idx >= 0 {
- network = strings.ToLower(strings.TrimSpace(a[:idx]))
- a = a[idx+1:]
- }
var host, port string
- host, port, err = net.SplitHostPort(a)
+ network, host, port, err = splitListenAddr(a)
+ if network == "" {
+ network = "tcp"
+ }
if err != nil {
return
}
@@ -317,6 +434,27 @@ func parseListenAddr(a string) (network string, addrs []string, err error) {
return
}
+func splitListenAddr(a string) (network, host, port string, err error) {
+ if idx := strings.Index(a, "/"); idx >= 0 {
+ network = strings.ToLower(strings.TrimSpace(a[:idx]))
+ a = a[idx+1:]
+ }
+ host, port, err = net.SplitHostPort(a)
+ return
+}
+
+func joinListenAddr(network, host, port string) string {
+ var a string
+ if network != "" {
+ a = network + "/"
+ }
+ a += host
+ if port != "" {
+ a += ":" + port
+ }
+ return a
+}
+
type middlewareResponseWriter struct {
*ResponseWriterWrapper
allowWrites bool
@@ -336,7 +474,16 @@ func (mrw middlewareResponseWriter) Write(b []byte) (int, error) {
return mrw.ResponseWriterWrapper.Write(b)
}
+// ReplacerCtxKey is the context key for the request's replacer.
const ReplacerCtxKey caddy2.CtxKey = "replacer"
+const (
+ // DefaultHTTPPort is the default port for HTTP.
+ DefaultHTTPPort = 80
+
+ // DefaultHTTPSPort is the default port for HTTPS.
+ DefaultHTTPSPort = 443
+)
+
// Interface guards
var _ HTTPInterfaces = middlewareResponseWriter{}
diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go
index 731832b..7336a1b 100644
--- a/modules/caddyhttp/matchers.go
+++ b/modules/caddyhttp/matchers.go
@@ -68,17 +68,11 @@ func (m matchScript) Match(r *http.Request) bool {
func (m matchProtocol) Match(r *http.Request) bool {
switch string(m) {
case "grpc":
- if r.Header.Get("content-type") == "application/grpc" {
- return true
- }
+ return r.Header.Get("content-type") == "application/grpc"
case "https":
- if r.TLS != nil {
- return true
- }
+ return r.TLS != nil
case "http":
- if r.TLS == nil {
- return true
- }
+ return r.TLS == nil
}
return false
diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go
index 6a2ecd1..947de09 100644
--- a/modules/caddyhttp/replacer.go
+++ b/modules/caddyhttp/replacer.go
@@ -1,10 +1,14 @@
package caddyhttp
import (
+ "net"
"net/http"
+ "os"
"strings"
)
+// Replacer can replace values in strings based
+// on a request and/or response writer.
type Replacer struct {
req *http.Request
resp http.ResponseWriter
@@ -42,15 +46,33 @@ func (r *Replacer) replaceAll(input, empty string, mapping map[string]string) st
func (r *Replacer) defaults() map[string]string {
m := map[string]string{
- "host": r.req.Host,
- "method": r.req.Method,
- "scheme": func() string {
+ "request.host": func() string {
+ host, _, err := net.SplitHostPort(r.req.Host)
+ if err != nil {
+ return r.req.Host // OK; there probably was no port
+ }
+ return host
+ }(),
+ "request.hostport": r.req.Host, // may include both host and port
+ "request.method": r.req.Method,
+ "request.port": func() string {
+ // if there is no port, there will be an error; in
+ // that case, port is the empty string anyway
+ _, port, _ := net.SplitHostPort(r.req.Host)
+ return port
+ }(),
+ "request.scheme": func() string {
if r.req.TLS != nil {
return "https"
}
return "http"
}(),
- "uri": r.req.URL.RequestURI(),
+ "request.uri": r.req.URL.RequestURI(),
+ "system.hostname": func() string {
+ // OK if there is an error; just return empty string
+ name, _ := os.Hostname()
+ return name
+ }(),
}
for field, vals := range r.req.Header {
diff --git a/modules/caddyhttp/staticresp/staticresp.go b/modules/caddyhttp/staticresp.go
index e169133..dca60cb 100644
--- a/modules/caddyhttp/staticresp/staticresp.go
+++ b/modules/caddyhttp/staticresp.go
@@ -1,11 +1,10 @@
-package staticresp
+package caddyhttp
import (
"fmt"
"net/http"
"bitbucket.org/lightcodelabs/caddy2"
- "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp"
)
func init() {
@@ -16,15 +15,16 @@ func init() {
}
// Static implements a simple responder for static responses.
+// It is Caddy's default responder. TODO: Or is it?
type Static struct {
- StatusCode int `json:"status_code"`
- Headers map[string][]string `json:"headers"`
- Body string `json:"body"`
- Close bool `json:"close"`
+ StatusCode int `json:"status_code"`
+ Headers http.Header `json:"headers"`
+ Body string `json:"body"`
+ Close bool `json:"close"`
}
func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
- repl := r.Context().Value(caddyhttp.ReplacerCtxKey).(*caddyhttp.Replacer)
+ repl := r.Context().Value(ReplacerCtxKey).(*Replacer)
// close the connection
r.Close = s.Close
@@ -54,4 +54,4 @@ func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
}
// Interface guard
-var _ caddyhttp.Handler = (*Static)(nil)
+var _ Handler = (*Static)(nil)