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)