@marcel91200 / caddy Mirror · read-only / commits / e40bbecb
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 14, 05:15 UTC

Rough implementation of auto HTTP->HTTPS redirects

Also added GracePeriod for server shutdowns

Matthew Holt committed May 7, 2019 at 09:56 UTC e40bbecb16d196d2d700a9484e53c11b64dfe8d9
5 files changed +195 -33
cmd/caddy2/main.go
-1
@@ -9,7 +9,6 @@ import (
9 _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/caddylog"
10 _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/reverseproxy"
11 _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/staticfiles"
12 - _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/staticresp"
12 _ "bitbucket.org/lightcodelabs/caddy2/modules/caddytls"
13 )
14
modules/caddyhttp/caddyhttp.go
+158 -11
@@ -30,13 +30,15 @@ func init() {
30 }
31
32 type httpModuleConfig struct {
33 - Servers map[string]*httpServerConfig `json:"servers"`
33 + HTTPPort int `json:"http_port"`
34 + HTTPSPort int `json:"https_port"`
35 + GracePeriod caddy2.Duration `json:"grace_period"`
36 + Servers map[string]*httpServerConfig `json:"servers"`
37
38 servers []*http.Server
39 }
40
41 func (hc *httpModuleConfig) Provision() error {
39 - // TODO: Either prevent overlapping listeners on different servers, or combine them into one
42 for _, srv := range hc.Servers {
43 err := srv.Routes.setup()
44 if err != nil {
@@ -51,6 +53,27 @@ func (hc *httpModuleConfig) Provision() error {
53 return nil
54 }
55
56 +func (hc *httpModuleConfig) Validate() error {
57 + // each server must use distinct listener addresses
58 + lnAddrs := make(map[string]string)
59 + for srvName, srv := range hc.Servers {
60 + for _, addr := range srv.Listen {
61 + netw, expanded, err := parseListenAddr(addr)
62 + if err != nil {
63 + return fmt.Errorf("invalid listener address '%s': %v", addr, err)
64 + }
65 + for _, a := range expanded {
66 + if sn, ok := lnAddrs[netw+a]; ok {
67 + return fmt.Errorf("listener address repeated: %s (already claimed by server '%s')", a, sn)
68 + }
69 + lnAddrs[netw+a] = srvName
70 + }
71 + }
72 + }
73 +
74 + return nil
75 +}
76 +
77 func (hc *httpModuleConfig) Start(handle caddy2.Handle) error {
78 err := hc.automaticHTTPS(handle)
79 if err != nil {
@@ -83,7 +106,12 @@ func (hc *httpModuleConfig) Start(handle caddy2.Handle) error {
106 }
107
108 // enable TLS
86 - if len(srv.TLSConnPolicies) > 0 {
109 + httpPort := hc.HTTPPort
110 + if httpPort == 0 {
111 + httpPort = DefaultHTTPPort
112 + }
113 + _, port, _ := net.SplitHostPort(addr)
114 + if len(srv.TLSConnPolicies) > 0 && port != strconv.Itoa(httpPort) {
115 tlsCfg, err := srv.TLSConnPolicies.TLSConfig(handle)
116 if err != nil {
117 return fmt.Errorf("%s/%s: making TLS configuration: %v", network, addr, err)
@@ -100,9 +128,16 @@ func (hc *httpModuleConfig) Start(handle caddy2.Handle) error {
128 return nil
129 }
130
131 +// Stop gracefully shuts down the HTTP server.
132 func (hc *httpModuleConfig) Stop() error {
133 + ctx := context.Background()
134 + if hc.GracePeriod > 0 {
135 + var cancel context.CancelFunc
136 + ctx, cancel = context.WithTimeout(ctx, time.Duration(hc.GracePeriod))
137 + defer cancel()
138 + }
139 for _, s := range hc.servers {
105 - err := s.Shutdown(context.Background()) // TODO
140 + err := s.Shutdown(ctx)
141 if err != nil {
142 return err
143 }
@@ -117,6 +152,9 @@ func (hc *httpModuleConfig) automaticHTTPS(handle caddy2.Handle) error {
152 }
153 tlsApp := tlsAppIface.(*caddytls.TLS)
154
155 + lnAddrMap := make(map[string]struct{})
156 + var redirRoutes routeList
157 +
158 for srvName, srv := range hc.Servers {
159 srv.tlsApp = tlsApp
160
@@ -157,13 +195,93 @@ func (hc *httpModuleConfig) automaticHTTPS(handle caddy2.Handle) error {
195 {ALPN: defaultALPN},
196 }
197
160 - // TODO: create HTTP->HTTPS redirects
198 + if srv.DisableAutoHTTPSRedir {
199 + continue
200 + }
201 +
202 + // create HTTP->HTTPS redirects
203 + for _, addr := range srv.Listen {
204 + netw, host, port, err := splitListenAddr(addr)
205 + if err != nil {
206 + return fmt.Errorf("%s: invalid listener address: %v", srvName, addr)
207 + }
208 + httpRedirLnAddr := joinListenAddr(netw, host, strconv.Itoa(hc.HTTPPort))
209 + lnAddrMap[httpRedirLnAddr] = struct{}{}
210 +
211 + if parts := strings.SplitN(port, "-", 2); len(parts) == 2 {
212 + port = parts[0]
213 + }
214 + redirTo := "https://{request.host}"
215 +
216 + httpsPort := hc.HTTPSPort
217 + if httpsPort == 0 {
218 + httpsPort = DefaultHTTPSPort
219 + }
220 + if port != strconv.Itoa(httpsPort) {
221 + redirTo += ":" + port
222 + }
223 + redirTo += "{request.uri}"
224 +
225 + redirRoutes = append(redirRoutes, serverRoute{
226 + matchers: []RouteMatcher{
227 + matchProtocol("http"),
228 + matchHost(domains),
229 + },
230 + responder: Static{
231 + StatusCode: http.StatusTemporaryRedirect, // TODO: use permanent redirect instead
232 + Headers: http.Header{
233 + "Location": []string{redirTo},
234 + "Connection": []string{"close"},
235 + },
236 + Close: true,
237 + },
238 + })
239 + }
240 + }
241 + }
242 +
243 + if len(lnAddrMap) > 0 {
244 + var lnAddrs []string
245 + mapLoop:
246 + for addr := range lnAddrMap {
247 + netw, addrs, err := parseListenAddr(addr)
248 + if err != nil {
249 + continue
250 + }
251 + for _, a := range addrs {
252 + if hc.listenerTaken(netw, a) {
253 + continue mapLoop
254 + }
255 + }
256 + lnAddrs = append(lnAddrs, addr)
257 + }
258 + hc.Servers["auto_https_redirects"] = &httpServerConfig{
259 + Listen: lnAddrs,
260 + Routes: redirRoutes,
261 + DisableAutoHTTPS: true,
262 }
263 }
264
265 return nil
266 }
267
268 +func (hc *httpModuleConfig) listenerTaken(network, address string) bool {
269 + for _, srv := range hc.Servers {
270 + for _, addr := range srv.Listen {
271 + netw, addrs, err := parseListenAddr(addr)
272 + if err != nil || netw != network {
273 + continue
274 + }
275 + for _, a := range addrs {
276 + if a == address {
277 + return true
278 + }
279 + }
280 + }
281 + }
282 + return false
283 +}
284 +
285 var defaultALPN = []string{"h2", "http/1.1"}
286
287 type httpServerConfig struct {
@@ -204,6 +322,7 @@ func (s httpServerConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) {
322 // it can be accessed by error handlers
323 c := context.WithValue(r.Context(), ErrorCtxKey, err)
324 r = r.WithContext(c)
325 + // TODO: add error values to Replacer
326
327 if len(s.Errors.Routes) == 0 {
328 // TODO: implement a default error handler?
@@ -284,13 +403,11 @@ func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
403 var emptyHandler HandlerFunc = func(w http.ResponseWriter, r *http.Request) error { return nil }
404
405 func parseListenAddr(a string) (network string, addrs []string, err error) {
287 - network = "tcp"
288 - if idx := strings.Index(a, "/"); idx >= 0 {
289 - network = strings.ToLower(strings.TrimSpace(a[:idx]))
290 - a = a[idx+1:]
291 - }
406 var host, port string
293 - host, port, err = net.SplitHostPort(a)
407 + network, host, port, err = splitListenAddr(a)
408 + if network == "" {
409 + network = "tcp"
410 + }
411 if err != nil {
412 return
413 }
@@ -317,6 +434,27 @@ func parseListenAddr(a string) (network string, addrs []string, err error) {
434 return
435 }
436
437 +func splitListenAddr(a string) (network, host, port string, err error) {
438 + if idx := strings.Index(a, "/"); idx >= 0 {
439 + network = strings.ToLower(strings.TrimSpace(a[:idx]))
440 + a = a[idx+1:]
441 + }
442 + host, port, err = net.SplitHostPort(a)
443 + return
444 +}
445 +
446 +func joinListenAddr(network, host, port string) string {
447 + var a string
448 + if network != "" {
449 + a = network + "/"
450 + }
451 + a += host
452 + if port != "" {
453 + a += ":" + port
454 + }
455 + return a
456 +}
457 +
458 type middlewareResponseWriter struct {
459 *ResponseWriterWrapper
460 allowWrites bool
@@ -336,7 +474,16 @@ func (mrw middlewareResponseWriter) Write(b []byte) (int, error) {
474 return mrw.ResponseWriterWrapper.Write(b)
475 }
476
477 +// ReplacerCtxKey is the context key for the request's replacer.
478 const ReplacerCtxKey caddy2.CtxKey = "replacer"
479
480 +const (
481 + // DefaultHTTPPort is the default port for HTTP.
482 + DefaultHTTPPort = 80
483 +
484 + // DefaultHTTPSPort is the default port for HTTPS.
485 + DefaultHTTPSPort = 443
486 +)
487 +
488 // Interface guards
489 var _ HTTPInterfaces = middlewareResponseWriter{}
modules/caddyhttp/matchers.go
+3 -9
@@ -68,17 +68,11 @@ func (m matchScript) Match(r *http.Request) bool {
68 func (m matchProtocol) Match(r *http.Request) bool {
69 switch string(m) {
70 case "grpc":
71 - if r.Header.Get("content-type") == "application/grpc" {
72 - return true
73 - }
71 + return r.Header.Get("content-type") == "application/grpc"
72 case "https":
75 - if r.TLS != nil {
76 - return true
77 - }
73 + return r.TLS != nil
74 case "http":
79 - if r.TLS == nil {
80 - return true
81 - }
75 + return r.TLS == nil
76 }
77
78 return false
modules/caddyhttp/replacer.go
+26 -4
@@ -1,10 +1,14 @@
1 package caddyhttp
2
3 import (
4 + "net"
5 "net/http"
6 + "os"
7 "strings"
8 )
9
10 +// Replacer can replace values in strings based
11 +// on a request and/or response writer.
12 type Replacer struct {
13 req *http.Request
14 resp http.ResponseWriter
@@ -42,15 +46,33 @@ func (r *Replacer) replaceAll(input, empty string, mapping map[string]string) st
46
47 func (r *Replacer) defaults() map[string]string {
48 m := map[string]string{
45 - "host": r.req.Host,
46 - "method": r.req.Method,
47 - "scheme": func() string {
49 + "request.host": func() string {
50 + host, _, err := net.SplitHostPort(r.req.Host)
51 + if err != nil {
52 + return r.req.Host // OK; there probably was no port
53 + }
54 + return host
55 + }(),
56 + "request.hostport": r.req.Host, // may include both host and port
57 + "request.method": r.req.Method,
58 + "request.port": func() string {
59 + // if there is no port, there will be an error; in
60 + // that case, port is the empty string anyway
61 + _, port, _ := net.SplitHostPort(r.req.Host)
62 + return port
63 + }(),
64 + "request.scheme": func() string {
65 if r.req.TLS != nil {
66 return "https"
67 }
68 return "http"
69 }(),
53 - "uri": r.req.URL.RequestURI(),
70 + "request.uri": r.req.URL.RequestURI(),
71 + "system.hostname": func() string {
72 + // OK if there is an error; just return empty string
73 + name, _ := os.Hostname()
74 + return name
75 + }(),
76 }
77
78 for field, vals := range r.req.Header {
modules/caddyhttp/staticresp.go renamed
+8 -8
@@ -1,11 +1,10 @@
1 -package staticresp
1 +package caddyhttp
2
3 import (
4 "fmt"
5 "net/http"
6
7 "bitbucket.org/lightcodelabs/caddy2"
8 - "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp"
8 )
9
10 func init() {
@@ -16,15 +15,16 @@ func init() {
15 }
16
17 // Static implements a simple responder for static responses.
18 +// It is Caddy's default responder. TODO: Or is it?
19 type Static struct {
20 - StatusCode int `json:"status_code"`
21 - Headers map[string][]string `json:"headers"`
22 - Body string `json:"body"`
23 - Close bool `json:"close"`
20 + StatusCode int `json:"status_code"`
21 + Headers http.Header `json:"headers"`
22 + Body string `json:"body"`
23 + Close bool `json:"close"`
24 }
25
26 func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
27 - repl := r.Context().Value(caddyhttp.ReplacerCtxKey).(*caddyhttp.Replacer)
27 + repl := r.Context().Value(ReplacerCtxKey).(*Replacer)
28
29 // close the connection
30 r.Close = s.Close
@@ -54,4 +54,4 @@ func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
54 }
55
56 // Interface guard
57 -var _ caddyhttp.Handler = (*Static)(nil)
57 +var _ Handler = (*Static)(nil)