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
Implement most of static file server; refactor and improve Replacer
Matthew Holt committed
May 20, 2019 at 10:59 UTC
fec7fa8bfda713e8042b9bbf9a480c7792b78c41
14 files changed
+994
-258
caddy.go
+1
-2
@@ -34,7 +34,7 @@ func Run(newCfg *Config) error {
34
// modules - essentially our new config's
35
// execution environment; be sure that
36
// cleanup occurs when we return if there
37
- // was an error; otherwise, it will get
37
+ // was an error; if no error, it will get
38
// cleaned up on next config cycle
39
ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
40
defer func() {
@@ -139,7 +139,6 @@ type Config struct {
139
StorageRaw json.RawMessage `json:"storage"`
140
storage certmagic.Storage
141
142
- TestVal string `json:"testval"`
142
AppsRaw map[string]json.RawMessage `json:"apps"`
143
144
// apps stores the decoded Apps values,
modules/caddyhttp/caddyhttp.go
+17
-100
@@ -45,7 +45,13 @@ type App struct {
45
func (app *App) Provision(ctx caddy2.Context) error {
46
app.ctx = ctx
47
48
+ repl := caddy2.NewReplacer()
49
+
50
for _, srv := range app.Servers {
51
+ // TODO: Test this function to ensure these replacements are performed
52
+ for i := range srv.Listen {
53
+ srv.Listen[i] = repl.ReplaceAll(srv.Listen[i], "")
54
+ }
55
err := srv.Routes.Provision(ctx)
56
if err != nil {
57
return fmt.Errorf("setting up server routes: %v", err)
@@ -78,6 +84,13 @@ func (app *App) Validate() error {
84
}
85
}
86
87
+ // each server's max rehandle value must be valid
88
+ for srvName, srv := range app.Servers {
89
+ if srv.MaxRehandles < 0 {
90
+ return fmt.Errorf("%s: invalid max_rehandles value: %d", srvName, srv.MaxRehandles)
91
+ }
92
+ }
93
+
94
return nil
95
}
96
@@ -231,7 +244,7 @@ func (app *App) automaticHTTPS() error {
244
redirTo += "{request.uri}"
245
246
redirRoutes = append(redirRoutes, ServerRoute{
234
- matchers: []RouteMatcher{
247
+ matchers: []RequestMatcher{
248
matchProtocol("http"),
249
matchHost(domains),
250
},
@@ -292,84 +305,9 @@ func (app *App) listenerTaken(network, address string) bool {
305
306
var defaultALPN = []string{"h2", "http/1.1"}
307
295
-// Server is an HTTP server.
296
-type Server struct {
297
- Listen []string `json:"listen"`
298
- ReadTimeout caddy2.Duration `json:"read_timeout"`
299
- ReadHeaderTimeout caddy2.Duration `json:"read_header_timeout"`
300
- HiddenFiles []string `json:"hidden_files"` // TODO:... experimenting with shared/common state
301
- Routes RouteList `json:"routes"`
302
- Errors httpErrorConfig `json:"errors"`
303
- TLSConnPolicies caddytls.ConnectionPolicies `json:"tls_connection_policies"`
304
- DisableAutoHTTPS bool `json:"disable_auto_https"`
305
- DisableAutoHTTPSRedir bool `json:"disable_auto_https_redir"`
306
-
307
- tlsApp *caddytls.TLS
308
-}
309
-
310
-type httpErrorConfig struct {
311
- Routes RouteList `json:"routes"`
312
- // TODO: some way to configure the logging of errors, probably? standardize
313
- // the logging configuration first.
314
-}
315
-
316
-// ServeHTTP is the entry point for all HTTP requests.
317
-func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
318
- if s.tlsApp.HandleHTTPChallenge(w, r) {
319
- return
320
- }
321
-
322
- // set up the replacer
323
- repl := NewReplacer(r, w)
324
- ctx := context.WithValue(r.Context(), ReplacerCtxKey, repl)
325
- r = r.WithContext(ctx)
326
-
327
- // build and execute the main middleware chain
328
- stack := s.Routes.BuildHandlerChain(w, r)
329
- err := executeMiddlewareChain(w, r, stack)
330
- if err != nil {
331
- // add the error value to the request context so
332
- // it can be accessed by error handlers
333
- c := context.WithValue(r.Context(), ErrorCtxKey, err)
334
- r = r.WithContext(c)
335
- // TODO: add error values to Replacer
336
-
337
- if len(s.Errors.Routes) == 0 {
338
- // TODO: implement a default error handler?
339
- log.Printf("[ERROR] %s", err)
340
- } else {
341
- errStack := s.Errors.Routes.BuildHandlerChain(w, r)
342
- err := executeMiddlewareChain(w, r, errStack)
343
- if err != nil {
344
- // TODO: what should we do if the error handler has an error?
345
- log.Printf("[ERROR] handling error: %v", err)
346
- }
347
- }
348
- }
349
-}
350
-
351
-// executeMiddlewareChain executes stack with w and r. This function handles
352
-// the special ErrRehandle error value, which reprocesses requests through
353
-// the stack again. Any error value returned from this function would be an
354
-// actual error that needs to be handled.
355
-func executeMiddlewareChain(w http.ResponseWriter, r *http.Request, stack Handler) error {
356
- const maxRehandles = 3
357
- var err error
358
- for i := 0; i < maxRehandles; i++ {
359
- err = stack.ServeHTTP(w, r)
360
- if err != ErrRehandle {
361
- break
362
- }
363
- if i == maxRehandles-1 {
364
- return fmt.Errorf("too many rehandles")
365
- }
366
- }
367
- return err
368
-}
369
-
370
-// RouteMatcher is a type that can match to a request.
308
+// RequestMatcher is a type that can match to a request.
309
// A route matcher MUST NOT modify the request.
372
-type RouteMatcher interface {
310
+type RequestMatcher interface {
311
Match(*http.Request) bool
312
}
313
@@ -421,7 +359,6 @@ func parseListenAddr(a string) (network string, addrs []string, err error) {
359
if err != nil {
360
return
361
}
424
- host = NewReplacer(nil, nil).Replace(host, "")
362
ports := strings.SplitN(port, "-", 2)
363
if len(ports) == 1 {
364
ports = append(ports, ports[0])
@@ -466,25 +403,6 @@ func joinListenAddr(network, host, port string) string {
403
return a
404
}
405
469
-type middlewareResponseWriter struct {
470
- *ResponseWriterWrapper
471
- allowWrites bool
472
-}
473
-
474
-func (mrw middlewareResponseWriter) WriteHeader(statusCode int) {
475
- if !mrw.allowWrites {
476
- panic("WriteHeader: middleware cannot write to the response")
477
- }
478
- mrw.ResponseWriterWrapper.WriteHeader(statusCode)
479
-}
480
-
481
-func (mrw middlewareResponseWriter) Write(b []byte) (int, error) {
482
- if !mrw.allowWrites {
483
- panic("Write: middleware cannot write to the response")
484
- }
485
- return mrw.ResponseWriterWrapper.Write(b)
486
-}
487
-
406
const (
407
// DefaultHTTPPort is the default port for HTTP.
408
DefaultHTTPPort = 80
@@ -493,6 +411,5 @@ const (
411
DefaultHTTPSPort = 443
412
)
413
496
-// Interface guards
497
-var _ HTTPInterfaces = middlewareResponseWriter{}
414
+// Interface guard
415
var _ caddy2.App = (*App)(nil)
modules/caddyhttp/caddyhttp_test.go
-11
@@ -1,7 +1,6 @@
1
package caddyhttp
2
3
import (
4
- "os"
4
"reflect"
5
"testing"
6
)
@@ -115,11 +114,6 @@ func TestJoinListenerAddr(t *testing.T) {
114
}
115
116
func TestParseListenerAddr(t *testing.T) {
118
- hostname, err := os.Hostname()
119
- if err != nil {
120
- t.Fatalf("Cannot ascertain system hostname: %v", err)
121
- }
122
-
117
for i, tc := range []struct {
118
input string
119
expectNetwork string
@@ -176,11 +170,6 @@ func TestParseListenerAddr(t *testing.T) {
170
expectNetwork: "tcp",
171
expectAddrs: []string{"localhost:0"},
172
},
179
- {
180
- input: "{system.hostname}:0",
181
- expectNetwork: "tcp",
182
- expectAddrs: []string{hostname + ":0"},
183
- },
173
} {
174
actualNetwork, actualAddrs, err := parseListenAddr(tc.input)
175
if tc.expectErr && err == nil {
modules/caddyhttp/matchers.go
+40
-28
@@ -6,6 +6,8 @@ import (
6
"net/http"
7
"net/textproto"
8
"net/url"
9
+ "path"
10
+ "path/filepath"
11
"regexp"
12
"strings"
13
@@ -15,15 +17,16 @@ import (
17
)
18
19
type (
18
- matchHost []string
19
- matchPath []string
20
- matchPathRE struct{ matchRegexp }
21
- matchMethod []string
22
- matchQuery url.Values
23
- matchHeader http.Header
24
- matchHeaderRE map[string]*matchRegexp
25
- matchProtocol string
26
- matchStarlark string
20
+ matchHost []string
21
+ matchPath []string
22
+ matchPathRE struct{ matchRegexp }
23
+ matchMethod []string
24
+ matchQuery url.Values
25
+ matchHeader http.Header
26
+ matchHeaderRE map[string]*matchRegexp
27
+ matchProtocol string
28
+ matchStarlarkExpr string
29
+ matchTable string
30
)
31
32
func init() {
@@ -60,8 +63,8 @@ func init() {
63
New: func() (interface{}, error) { return new(matchProtocol), nil },
64
})
65
caddy2.RegisterModule(caddy2.Module{
63
- Name: "http.matchers.caddyscript",
64
- New: func() (interface{}, error) { return new(matchStarlark), nil },
66
+ Name: "http.matchers.starlark_expr",
67
+ New: func() (interface{}, error) { return new(matchStarlarkExpr), nil },
68
})
69
}
70
@@ -91,8 +94,17 @@ outer:
94
}
95
96
func (m matchPath) Match(r *http.Request) bool {
94
- for _, path := range m {
95
- if strings.HasPrefix(r.URL.Path, path) {
97
+ for _, matchPath := range m {
98
+ compare := r.URL.Path
99
+ if strings.HasPrefix(matchPath, "*") {
100
+ compare = path.Base(compare)
101
+ }
102
+ // can ignore error here because we can't handle it anyway
103
+ matches, _ := filepath.Match(matchPath, compare)
104
+ if matches {
105
+ return true
106
+ }
107
+ if strings.HasPrefix(r.URL.Path, matchPath) {
108
return true
109
}
110
}
@@ -100,7 +112,7 @@ func (m matchPath) Match(r *http.Request) bool {
112
}
113
114
func (m matchPathRE) Match(r *http.Request) bool {
103
- repl := r.Context().Value(ReplacerCtxKey).(*Replacer)
115
+ repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer)
116
return m.match(r.URL.Path, repl, "path_regexp")
117
}
118
@@ -147,7 +159,7 @@ func (m matchHeader) Match(r *http.Request) bool {
159
160
func (m matchHeaderRE) Match(r *http.Request) bool {
161
for field, rm := range m {
150
- repl := r.Context().Value(ReplacerCtxKey).(*Replacer)
162
+ repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer)
163
match := rm.match(r.Header.Get(field), repl, "header_regexp")
164
if !match {
165
return false
@@ -188,7 +200,7 @@ func (m matchProtocol) Match(r *http.Request) bool {
200
return false
201
}
202
191
-func (m matchStarlark) Match(r *http.Request) bool {
203
+func (m matchStarlarkExpr) Match(r *http.Request) bool {
204
input := string(m)
205
thread := new(starlark.Thread)
206
env := caddyscript.MatcherEnv(r)
@@ -225,7 +237,7 @@ func (mre *matchRegexp) Validate() error {
237
return nil
238
}
239
228
-func (mre *matchRegexp) match(input string, repl *Replacer, scope string) bool {
240
+func (mre *matchRegexp) match(input string, repl caddy2.Replacer, scope string) bool {
241
matches := mre.compiled.FindStringSubmatch(input)
242
if matches == nil {
243
return false
@@ -234,14 +246,14 @@ func (mre *matchRegexp) match(input string, repl *Replacer, scope string) bool {
246
// save all capture groups, first by index
247
for i, match := range matches {
248
key := fmt.Sprintf("matchers.%s.%s.%d", scope, mre.Name, i)
237
- repl.Map(key, match)
249
+ repl.Set(key, match)
250
}
251
252
// then by name
253
for i, name := range mre.compiled.SubexpNames() {
254
if i != 0 && name != "" {
255
key := fmt.Sprintf("matchers.%s.%s.%s", scope, mre.Name, name)
244
- repl.Map(key, matches[i])
256
+ repl.Set(key, matches[i])
257
}
258
}
259
@@ -252,13 +264,13 @@ var wordRE = regexp.MustCompile(`\w+`)
264
265
// Interface guards
266
var (
255
- _ RouteMatcher = (*matchHost)(nil)
256
- _ RouteMatcher = (*matchPath)(nil)
257
- _ RouteMatcher = (*matchPathRE)(nil)
258
- _ RouteMatcher = (*matchMethod)(nil)
259
- _ RouteMatcher = (*matchQuery)(nil)
260
- _ RouteMatcher = (*matchHeader)(nil)
261
- _ RouteMatcher = (*matchHeaderRE)(nil)
262
- _ RouteMatcher = (*matchProtocol)(nil)
263
- _ RouteMatcher = (*matchStarlark)(nil)
267
+ _ RequestMatcher = (*matchHost)(nil)
268
+ _ RequestMatcher = (*matchPath)(nil)
269
+ _ RequestMatcher = (*matchPathRE)(nil)
270
+ _ RequestMatcher = (*matchMethod)(nil)
271
+ _ RequestMatcher = (*matchQuery)(nil)
272
+ _ RequestMatcher = (*matchHeader)(nil)
273
+ _ RequestMatcher = (*matchHeaderRE)(nil)
274
+ _ RequestMatcher = (*matchProtocol)(nil)
275
+ _ RequestMatcher = (*matchStarlarkExpr)(nil)
276
)
modules/caddyhttp/matchers_test.go
+28
-6
@@ -7,6 +7,8 @@ import (
7
"net/http/httptest"
8
"net/url"
9
"testing"
10
+
11
+ "bitbucket.org/lightcodelabs/caddy2"
12
)
13
14
func TestHostMatcher(t *testing.T) {
@@ -131,6 +133,26 @@ func TestPathMatcher(t *testing.T) {
133
input: "/other/",
134
expect: true,
135
},
136
+ {
137
+ match: matchPath{"*.ext"},
138
+ input: "foo.ext",
139
+ expect: true,
140
+ },
141
+ {
142
+ match: matchPath{"*.ext"},
143
+ input: "/foo/bar.ext",
144
+ expect: true,
145
+ },
146
+ {
147
+ match: matchPath{"/foo/*/baz"},
148
+ input: "/foo/bar/baz",
149
+ expect: true,
150
+ },
151
+ {
152
+ match: matchPath{"/foo/*/baz/bam"},
153
+ input: "/foo/bar/bam",
154
+ expect: false,
155
+ },
156
} {
157
req := &http.Request{URL: &url.URL{Path: tc.input}}
158
actual := tc.match.Match(req)
@@ -205,8 +227,8 @@ func TestPathREMatcher(t *testing.T) {
227
228
// set up the fake request and its Replacer
229
req := &http.Request{URL: &url.URL{Path: tc.input}}
208
- repl := NewReplacer(req, httptest.NewRecorder())
209
- ctx := context.WithValue(req.Context(), ReplacerCtxKey, repl)
230
+ repl := newReplacer(req, httptest.NewRecorder())
231
+ ctx := context.WithValue(req.Context(), caddy2.ReplacerCtxKey, repl)
232
req = req.WithContext(ctx)
233
234
actual := tc.match.Match(req)
@@ -218,7 +240,7 @@ func TestPathREMatcher(t *testing.T) {
240
241
for key, expectVal := range tc.expectRepl {
242
placeholder := fmt.Sprintf("{matchers.path_regexp.%s}", key)
221
- actualVal := repl.Replace(placeholder, "<empty>")
243
+ actualVal := repl.ReplaceAll(placeholder, "<empty>")
244
if actualVal != expectVal {
245
t.Errorf("Test %d [%v]: Expected placeholder {matchers.path_regexp.%s} to be '%s' but got '%s'",
246
i, tc.match.Pattern, key, expectVal, actualVal)
@@ -322,8 +344,8 @@ func TestHeaderREMatcher(t *testing.T) {
344
345
// set up the fake request and its Replacer
346
req := &http.Request{Header: tc.input, URL: new(url.URL)}
325
- repl := NewReplacer(req, httptest.NewRecorder())
326
- ctx := context.WithValue(req.Context(), ReplacerCtxKey, repl)
347
+ repl := newReplacer(req, httptest.NewRecorder())
348
+ ctx := context.WithValue(req.Context(), caddy2.ReplacerCtxKey, repl)
349
req = req.WithContext(ctx)
350
351
actual := tc.match.Match(req)
@@ -335,7 +357,7 @@ func TestHeaderREMatcher(t *testing.T) {
357
358
for key, expectVal := range tc.expectRepl {
359
placeholder := fmt.Sprintf("{matchers.header_regexp.%s}", key)
338
- actualVal := repl.Replace(placeholder, "<empty>")
360
+ actualVal := repl.ReplaceAll(placeholder, "<empty>")
361
if actualVal != expectVal {
362
t.Errorf("Test %d [%v]: Expected placeholder {matchers.header_regexp.%s} to be '%s' but got '%s'",
363
i, tc.match, key, expectVal, actualVal)
modules/caddyhttp/replacer.go
+64
-100
@@ -1,119 +1,83 @@
1
package caddyhttp
2
3
import (
4
+ "fmt"
5
"net"
6
"net/http"
6
- "os"
7
+ "path"
8
"strings"
9
10
"bitbucket.org/lightcodelabs/caddy2"
11
)
12
12
-// Replacer can replace values in strings based
13
-// on a request and/or response writer. The zero
14
-// Replacer is not valid; use NewReplacer() to
15
-// initialize one.
16
-type Replacer struct {
17
- req *http.Request
18
- resp http.ResponseWriter
19
- custom map[string]string
20
-}
21
-
22
-// NewReplacer makes a new Replacer, initializing all necessary
23
-// fields. The request and response writer are optional, but
24
-// necessary for most replacements to work.
25
-func NewReplacer(req *http.Request, rw http.ResponseWriter) *Replacer {
26
- return &Replacer{
27
- req: req,
28
- resp: rw,
29
- custom: make(map[string]string),
30
- }
31
-}
32
-
33
-// Map sets a custom variable mapping to a value.
34
-func (r *Replacer) Map(variable, value string) {
35
- r.custom[variable] = value
36
-}
37
-
38
-// Replace replaces placeholders in input with the value. If
39
-// the value is empty string, the placeholder is substituted
40
-// with the value empty.
41
-func (r *Replacer) Replace(input, empty string) string {
42
- if !strings.Contains(input, phOpen) {
43
- return input
44
- }
45
-
46
- input = r.replaceAll(input, empty, r.defaults())
47
- input = r.replaceAll(input, empty, r.custom)
48
-
49
- return input
50
-}
51
-
52
-func (r *Replacer) replaceAll(input, empty string, mapping map[string]string) string {
53
- for key, val := range mapping {
54
- if val == "" {
55
- val = empty
56
- }
57
- input = strings.ReplaceAll(input, phOpen+key+phClose, val)
58
- }
59
- return input
60
-}
61
-
62
-func (r *Replacer) defaults() map[string]string {
63
- m := map[string]string{
64
- "system.hostname": func() string {
65
- // OK if there is an error; just return empty string
66
- name, _ := os.Hostname()
67
- return name
68
- }(),
69
- }
70
-
71
- if r.req != nil {
72
- m["request.host"] = func() string {
73
- host, _, err := net.SplitHostPort(r.req.Host)
74
- if err != nil {
75
- return r.req.Host // OK; there probably was no port
13
+// TODO: A simple way to format or escape or encode each value would be nice
14
+// ... TODO: Should we just use templates? :-/ yeesh...
15
+
16
+func newReplacer(req *http.Request, w http.ResponseWriter) caddy2.Replacer {
17
+ repl := caddy2.NewReplacer()
18
+
19
+ httpVars := func() map[string]string {
20
+ m := make(map[string]string)
21
+ if req != nil {
22
+ m["http.request.host"] = func() string {
23
+ host, _, err := net.SplitHostPort(req.Host)
24
+ if err != nil {
25
+ return req.Host // OK; there probably was no port
26
+ }
27
+ return host
28
+ }()
29
+ m["http.request.hostport"] = req.Host // may include both host and port
30
+ m["http.request.method"] = req.Method
31
+ m["http.request.port"] = func() string {
32
+ // if there is no port, there will be an error; in
33
+ // that case, port is the empty string anyway
34
+ _, port, _ := net.SplitHostPort(req.Host)
35
+ return port
36
+ }()
37
+ m["http.request.scheme"] = func() string {
38
+ if req.TLS != nil {
39
+ return "https"
40
+ }
41
+ return "http"
42
+ }()
43
+ m["http.request.uri"] = req.URL.RequestURI()
44
+ m["http.request.uri.path"] = req.URL.Path
45
+ m["http.request.uri.path.file"] = func() string {
46
+ _, file := path.Split(req.URL.Path)
47
+ return file
48
+ }()
49
+ m["http.request.uri.path.dir"] = func() string {
50
+ dir, _ := path.Split(req.URL.Path)
51
+ return dir
52
+ }()
53
+
54
+ for field, vals := range req.Header {
55
+ m["http.request.header."+strings.ToLower(field)] = strings.Join(vals, ",")
56
}
77
- return host
78
- }()
79
- m["request.hostport"] = r.req.Host // may include both host and port
80
- m["request.method"] = r.req.Method
81
- m["request.port"] = func() string {
82
- // if there is no port, there will be an error; in
83
- // that case, port is the empty string anyway
84
- _, port, _ := net.SplitHostPort(r.req.Host)
85
- return port
86
- }()
87
- m["request.scheme"] = func() string {
88
- if r.req.TLS != nil {
89
- return "https"
57
+ for _, cookie := range req.Cookies() {
58
+ m["http.request.cookie."+cookie.Name] = cookie.Value
59
+ }
60
+ for param, vals := range req.URL.Query() {
61
+ m["http.request.uri.query."+param] = strings.Join(vals, ",")
62
}
91
- return "http"
92
- }()
93
- m["request.uri"] = r.req.URL.RequestURI()
94
- m["request.uri.path"] = r.req.URL.Path
63
96
- for field, vals := range r.req.Header {
97
- m["request.header."+strings.ToLower(field)] = strings.Join(vals, ",")
98
- }
99
- for _, cookie := range r.req.Cookies() {
100
- m["request.cookie."+cookie.Name] = cookie.Value
101
- }
102
- for param, vals := range r.req.URL.Query() {
103
- m["request.uri.query."+param] = strings.Join(vals, ",")
64
+ hostLabels := strings.Split(req.Host, ".")
65
+ for i, label := range hostLabels {
66
+ key := fmt.Sprintf("http.request.host.labels.%d", len(hostLabels)-i-1)
67
+ m[key] = label
68
+ }
69
}
105
- }
70
107
- if r.resp != nil {
108
- for field, vals := range r.resp.Header() {
109
- m["response.header."+strings.ToLower(field)] = strings.Join(vals, ",")
71
+ if w != nil {
72
+ for field, vals := range w.Header() {
73
+ m["http.response.header."+strings.ToLower(field)] = strings.Join(vals, ",")
74
+ }
75
}
111
- }
76
113
- return m
114
-}
77
+ return m
78
+ }
79
116
-const phOpen, phClose = "{", "}"
80
+ repl.Map(httpVars)
81
118
-// ReplacerCtxKey is the context key for the request's replacer.
119
-const ReplacerCtxKey caddy2.CtxKey = "replacer"
82
+ return repl
83
+}
modules/caddyhttp/routes.go
+49
-4
@@ -12,6 +12,7 @@ import (
12
// middlewares, and a responder for handling HTTP
13
// requests.
14
type ServerRoute struct {
15
+ Group string `json:"group"`
16
Matchers map[string]json.RawMessage `json:"match"`
17
Apply []json.RawMessage `json:"apply"`
18
Respond json.RawMessage `json:"respond"`
@@ -19,7 +20,7 @@ type ServerRoute struct {
20
Terminal bool `json:"terminal"`
21
22
// decoded values
22
- matchers []RouteMatcher
23
+ matchers []RequestMatcher
24
middleware []MiddlewareHandler
25
responder Handler
26
}
@@ -37,7 +38,7 @@ func (routes RouteList) Provision(ctx caddy2.Context) error {
38
if err != nil {
39
return fmt.Errorf("loading matcher module '%s': %v", modName, err)
40
}
40
- routes[i].matchers = append(routes[i].matchers, val.(RouteMatcher))
41
+ routes[i].matchers = append(routes[i].matchers, val.(RequestMatcher))
42
}
43
routes[i].Matchers = nil // allow GC to deallocate - TODO: Does this help?
44
@@ -64,9 +65,9 @@ func (routes RouteList) Provision(ctx caddy2.Context) error {
65
return nil
66
}
67
67
-// BuildHandlerChain creates a chain of handlers by
68
+// BuildCompositeRoute creates a chain of handlers by
69
// applying all the matching routes.
69
-func (routes RouteList) BuildHandlerChain(w http.ResponseWriter, r *http.Request) Handler {
70
+func (routes RouteList) BuildCompositeRoute(w http.ResponseWriter, r *http.Request) Handler {
71
if len(routes) == 0 {
72
return emptyHandler
73
}
@@ -74,17 +75,39 @@ func (routes RouteList) BuildHandlerChain(w http.ResponseWriter, r *http.Request
75
var mid []Middleware
76
var responder Handler
77
mrw := &middlewareResponseWriter{ResponseWriterWrapper: &ResponseWriterWrapper{w}}
78
+ groups := make(map[string]struct{})
79
80
routeLoop:
81
for _, route := range routes {
82
+ // see if route matches
83
for _, m := range route.matchers {
84
if !m.Match(r) {
85
continue routeLoop
86
}
87
}
88
+
89
+ // if route is part of a group, ensure only
90
+ // the first matching route in the group is
91
+ // applied
92
+ if route.Group != "" {
93
+ _, ok := groups[route.Group]
94
+ if ok {
95
+ // this group has already been satisfied
96
+ // by a matching route
97
+ continue
98
+ }
99
+ // this matching route satisfies the group
100
+ groups[route.Group] = struct{}{}
101
+ }
102
+
103
+ // apply the rest of the route
104
for _, m := range route.middleware {
105
mid = append(mid, func(next HandlerFunc) HandlerFunc {
106
return func(w http.ResponseWriter, r *http.Request) error {
107
+ // TODO: This is where request tracing could be implemented; also
108
+ // see below to trace the responder as well
109
+ // TODO: Trace a diff of the request, would be cool too! see what changed since the last middleware (host, headers, URI...)
110
+ // TODO: see what the std lib gives us in terms of stack trracing too
111
return m.ServeHTTP(mrw, r, next)
112
}
113
})
@@ -111,3 +134,25 @@ routeLoop:
134
135
return stack
136
}
137
+
138
+type middlewareResponseWriter struct {
139
+ *ResponseWriterWrapper
140
+ allowWrites bool
141
+}
142
+
143
+func (mrw middlewareResponseWriter) WriteHeader(statusCode int) {
144
+ if !mrw.allowWrites {
145
+ panic("WriteHeader: middleware cannot write to the response")
146
+ }
147
+ mrw.ResponseWriterWrapper.WriteHeader(statusCode)
148
+}
149
+
150
+func (mrw middlewareResponseWriter) Write(b []byte) (int, error) {
151
+ if !mrw.allowWrites {
152
+ panic("Write: middleware cannot write to the response")
153
+ }
154
+ return mrw.ResponseWriterWrapper.Write(b)
155
+}
156
+
157
+// Interface guard
158
+var _ HTTPInterfaces = middlewareResponseWriter{}
modules/caddyhttp/server.go
new
+91
@@ -0,0 +1,91 @@
1
+package caddyhttp
2
+
3
+import (
4
+ "context"
5
+ "fmt"
6
+ "log"
7
+ "net/http"
8
+
9
+ "bitbucket.org/lightcodelabs/caddy2"
10
+ "bitbucket.org/lightcodelabs/caddy2/modules/caddytls"
11
+)
12
+
13
+// Server is an HTTP server.
14
+type Server struct {
15
+ Listen []string `json:"listen"`
16
+ ReadTimeout caddy2.Duration `json:"read_timeout"`
17
+ ReadHeaderTimeout caddy2.Duration `json:"read_header_timeout"`
18
+ Routes RouteList `json:"routes"`
19
+ Errors httpErrorConfig `json:"errors"`
20
+ TLSConnPolicies caddytls.ConnectionPolicies `json:"tls_connection_policies"`
21
+ DisableAutoHTTPS bool `json:"disable_auto_https"`
22
+ DisableAutoHTTPSRedir bool `json:"disable_auto_https_redir"`
23
+ MaxRehandles int `json:"max_rehandles"`
24
+
25
+ tlsApp *caddytls.TLS
26
+}
27
+
28
+// ServeHTTP is the entry point for all HTTP requests.
29
+func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
30
+ if s.tlsApp.HandleHTTPChallenge(w, r) {
31
+ return
32
+ }
33
+
34
+ // set up the replacer
35
+ repl := newReplacer(r, w)
36
+ ctx := context.WithValue(r.Context(), caddy2.ReplacerCtxKey, repl)
37
+ ctx = context.WithValue(ctx, TableCtxKey, make(map[string]interface{})) // TODO: Implement this
38
+ r = r.WithContext(ctx)
39
+
40
+ // build and execute the main handler chain
41
+ stack := s.Routes.BuildCompositeRoute(w, r)
42
+ err := s.executeCompositeRoute(w, r, stack)
43
+ if err != nil {
44
+ // add the error value to the request context so
45
+ // it can be accessed by error handlers
46
+ c := context.WithValue(r.Context(), ErrorCtxKey, err)
47
+ r = r.WithContext(c)
48
+ // TODO: add error values to Replacer
49
+
50
+ if len(s.Errors.Routes) == 0 {
51
+ // TODO: implement a default error handler?
52
+ log.Printf("[ERROR] %s", err)
53
+ } else {
54
+ errStack := s.Errors.Routes.BuildCompositeRoute(w, r)
55
+ err := s.executeCompositeRoute(w, r, errStack)
56
+ if err != nil {
57
+ // TODO: what should we do if the error handler has an error?
58
+ log.Printf("[ERROR] handling error: %v", err)
59
+ }
60
+ }
61
+ }
62
+}
63
+
64
+// executeCompositeRoute executes stack with w and r. This function handles
65
+// the special ErrRehandle error value, which reprocesses requests through
66
+// the stack again. Any error value returned from this function would be an
67
+// actual error that needs to be handled.
68
+func (s *Server) executeCompositeRoute(w http.ResponseWriter, r *http.Request, stack Handler) error {
69
+ var err error
70
+ for i := -1; i <= s.MaxRehandles; i++ {
71
+ // we started the counter at -1 because we
72
+ // always want to run this at least once
73
+ err = stack.ServeHTTP(w, r)
74
+ if err != ErrRehandle {
75
+ break
76
+ }
77
+ if i >= s.MaxRehandles-1 {
78
+ return fmt.Errorf("too many rehandles")
79
+ }
80
+ }
81
+ return err
82
+}
83
+
84
+type httpErrorConfig struct {
85
+ Routes RouteList `json:"routes"`
86
+ // TODO: some way to configure the logging of errors, probably? standardize
87
+ // the logging configuration first.
88
+}
89
+
90
+// TableCtxKey is the context key for the request's variable table.
91
+const TableCtxKey caddy2.CtxKey = "table"
modules/caddyhttp/staticfiles/browse.go
new
+205
@@ -0,0 +1,205 @@
1
+package staticfiles
2
+
3
+import (
4
+ "net/http"
5
+)
6
+
7
+// Browse configures directory browsing.
8
+type Browse struct {
9
+}
10
+
11
+// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
12
+// If so, control is handed over to ServeListing.
13
+func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
14
+ // TODO: convert this handler
15
+ return nil
16
+
17
+ // // Browse works on existing directories; delegate everything else
18
+ // requestedFilepath, err := bc.Fs.Root.Open(r.URL.Path)
19
+ // if err != nil {
20
+ // switch {
21
+ // case os.IsPermission(err):
22
+ // return http.StatusForbidden, err
23
+ // case os.IsExist(err):
24
+ // return http.StatusNotFound, err
25
+ // default:
26
+ // return b.Next.ServeHTTP(w, r)
27
+ // }
28
+ // }
29
+ // defer requestedFilepath.Close()
30
+
31
+ // info, err := requestedFilepath.Stat()
32
+ // if err != nil {
33
+ // switch {
34
+ // case os.IsPermission(err):
35
+ // return http.StatusForbidden, err
36
+ // case os.IsExist(err):
37
+ // return http.StatusGone, err
38
+ // default:
39
+ // return b.Next.ServeHTTP(w, r)
40
+ // }
41
+ // }
42
+ // if !info.IsDir() {
43
+ // return b.Next.ServeHTTP(w, r)
44
+ // }
45
+
46
+ // // Do not reply to anything else because it might be nonsensical
47
+ // switch r.Method {
48
+ // case http.MethodGet, http.MethodHead:
49
+ // // proceed, noop
50
+ // case "PROPFIND", http.MethodOptions:
51
+ // return http.StatusNotImplemented, nil
52
+ // default:
53
+ // return b.Next.ServeHTTP(w, r)
54
+ // }
55
+
56
+ // // Browsing navigation gets messed up if browsing a directory
57
+ // // that doesn't end in "/" (which it should, anyway)
58
+ // u := *r.URL
59
+ // if u.Path == "" {
60
+ // u.Path = "/"
61
+ // }
62
+ // if u.Path[len(u.Path)-1] != '/' {
63
+ // u.Path += "/"
64
+ // http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
65
+ // return http.StatusMovedPermanently, nil
66
+ // }
67
+
68
+ // return b.ServeListing(w, r, requestedFilepath, bc)
69
+}
70
+
71
+// func (b Browse) loadDirectoryContents(requestedFilepath http.File, urlPath string, config *Config) (*Listing, bool, error) {
72
+// files, err := requestedFilepath.Readdir(-1)
73
+// if err != nil {
74
+// return nil, false, err
75
+// }
76
+
77
+// // Determine if user can browse up another folder
78
+// var canGoUp bool
79
+// curPathDir := path.Dir(strings.TrimSuffix(urlPath, "/"))
80
+// for _, other := range b.Configs {
81
+// if strings.HasPrefix(curPathDir, other.PathScope) {
82
+// canGoUp = true
83
+// break
84
+// }
85
+// }
86
+
87
+// // Assemble listing of directory contents
88
+// listing, hasIndex := directoryListing(files, canGoUp, urlPath, config)
89
+
90
+// return &listing, hasIndex, nil
91
+// }
92
+
93
+// // handleSortOrder gets and stores for a Listing the 'sort' and 'order',
94
+// // and reads 'limit' if given. The latter is 0 if not given.
95
+// //
96
+// // This sets Cookies.
97
+// func (b Browse) handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, limit int, err error) {
98
+// sort, order, limitQuery := r.URL.Query().Get("sort"), r.URL.Query().Get("order"), r.URL.Query().Get("limit")
99
+
100
+// // If the query 'sort' or 'order' is empty, use defaults or any values previously saved in Cookies
101
+// switch sort {
102
+// case "":
103
+// sort = sortByNameDirFirst
104
+// if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil {
105
+// sort = sortCookie.Value
106
+// }
107
+// case sortByName, sortByNameDirFirst, sortBySize, sortByTime:
108
+// http.SetCookie(w, &http.Cookie{Name: "sort", Value: sort, Path: scope, Secure: r.TLS != nil})
109
+// }
110
+
111
+// switch order {
112
+// case "":
113
+// order = "asc"
114
+// if orderCookie, orderErr := r.Cookie("order"); orderErr == nil {
115
+// order = orderCookie.Value
116
+// }
117
+// case "asc", "desc":
118
+// http.SetCookie(w, &http.Cookie{Name: "order", Value: order, Path: scope, Secure: r.TLS != nil})
119
+// }
120
+
121
+// if limitQuery != "" {
122
+// limit, err = strconv.Atoi(limitQuery)
123
+// if err != nil { // if the 'limit' query can't be interpreted as a number, return err
124
+// return
125
+// }
126
+// }
127
+
128
+// return
129
+// }
130
+
131
+// // ServeListing returns a formatted view of 'requestedFilepath' contents'.
132
+// func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFilepath http.File, bc *Config) (int, error) {
133
+// listing, containsIndex, err := b.loadDirectoryContents(requestedFilepath, r.URL.Path, bc)
134
+// if err != nil {
135
+// switch {
136
+// case os.IsPermission(err):
137
+// return http.StatusForbidden, err
138
+// case os.IsExist(err):
139
+// return http.StatusGone, err
140
+// default:
141
+// return http.StatusInternalServerError, err
142
+// }
143
+// }
144
+// if containsIndex && !b.IgnoreIndexes { // directory isn't browsable
145
+// return b.Next.ServeHTTP(w, r)
146
+// }
147
+// listing.Context = httpserver.Context{
148
+// Root: bc.Fs.Root,
149
+// Req: r,
150
+// URL: r.URL,
151
+// }
152
+// listing.User = bc.Variables
153
+
154
+// // Copy the query values into the Listing struct
155
+// var limit int
156
+// listing.Sort, listing.Order, limit, err = b.handleSortOrder(w, r, bc.PathScope)
157
+// if err != nil {
158
+// return http.StatusBadRequest, err
159
+// }
160
+
161
+// listing.applySort()
162
+
163
+// if limit > 0 && limit <= len(listing.Items) {
164
+// listing.Items = listing.Items[:limit]
165
+// listing.ItemsLimitedTo = limit
166
+// }
167
+
168
+// var buf *bytes.Buffer
169
+// acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ","))
170
+// switch {
171
+// case strings.Contains(acceptHeader, "application/json"):
172
+// if buf, err = b.formatAsJSON(listing, bc); err != nil {
173
+// return http.StatusInternalServerError, err
174
+// }
175
+// w.Header().Set("Content-Type", "application/json; charset=utf-8")
176
+
177
+// default: // There's no 'application/json' in the 'Accept' header; browse normally
178
+// if buf, err = b.formatAsHTML(listing, bc); err != nil {
179
+// return http.StatusInternalServerError, err
180
+// }
181
+// w.Header().Set("Content-Type", "text/html; charset=utf-8")
182
+
183
+// }
184
+
185
+// _, _ = buf.WriteTo(w)
186
+
187
+// return http.StatusOK, nil
188
+// }
189
+
190
+// func (b Browse) formatAsJSON(listing *Listing, bc *Config) (*bytes.Buffer, error) {
191
+// marsh, err := json.Marshal(listing.Items)
192
+// if err != nil {
193
+// return nil, err
194
+// }
195
+
196
+// buf := new(bytes.Buffer)
197
+// _, err = buf.Write(marsh)
198
+// return buf, err
199
+// }
200
+
201
+// func (b Browse) formatAsHTML(listing *Listing, bc *Config) (*bytes.Buffer, error) {
202
+// buf := new(bytes.Buffer)
203
+// err := bc.Template.Execute(buf, listing)
204
+// return buf, err
205
+// }
modules/caddyhttp/staticfiles/matcher.go
new
+54
@@ -0,0 +1,54 @@
1
+package staticfiles
2
+
3
+import (
4
+ "net/http"
5
+ "os"
6
+ "path/filepath"
7
+
8
+ "bitbucket.org/lightcodelabs/caddy2"
9
+ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp"
10
+)
11
+
12
+func init() {
13
+ caddy2.RegisterModule(caddy2.Module{
14
+ Name: "http.matchers.file",
15
+ New: func() (interface{}, error) { return new(FileMatcher), nil },
16
+ })
17
+}
18
+
19
+// TODO: Not sure how to do this well; we'd need the ability to
20
+// hide files, etc...
21
+// TODO: Also consider a feature to match directory that
22
+// contains a certain filename (use filepath.Glob), useful
23
+// if wanting to map directory-URI requests where the dir
24
+// has index.php to PHP backends, for example (although this
25
+// can effectively be done with rehandling already)
26
+type FileMatcher struct {
27
+ Root string `json:"root"`
28
+ Path string `json:"path"`
29
+ Flags []string `json:"flags"`
30
+}
31
+
32
+func (m FileMatcher) Match(r *http.Request) bool {
33
+ // TODO: sanitize path
34
+ fullPath := filepath.Join(m.Root, m.Path)
35
+ var match bool
36
+ if len(m.Flags) > 0 {
37
+ match = true
38
+ fi, err := os.Stat(fullPath)
39
+ for _, f := range m.Flags {
40
+ switch f {
41
+ case "EXIST":
42
+ match = match && os.IsNotExist(err)
43
+ case "DIR":
44
+ match = match && err == nil && fi.IsDir()
45
+ default:
46
+ match = false
47
+ }
48
+ }
49
+ }
50
+ return match
51
+}
52
+
53
+// Interface guard
54
+var _ caddyhttp.RequestMatcher = (*FileMatcher)(nil)
modules/caddyhttp/staticfiles/staticfiles.go
+296
-3
@@ -1,7 +1,15 @@
1
package staticfiles
2
3
import (
4
+ "fmt"
5
+ weakrand "math/rand"
6
"net/http"
7
+ "os"
8
+ "path"
9
+ "path/filepath"
10
+ "strconv"
11
+ "strings"
12
+ "time"
13
14
"bitbucket.org/lightcodelabs/caddy2"
15
"bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp"
@@ -16,13 +24,298 @@ func init() {
24
25
// StaticFiles implements a static file server responder for Caddy.
26
type StaticFiles struct {
19
- Root string
27
+ Root string `json:"root"` // default is current directory
28
+ IndexNames []string `json:"index_names"`
29
+ Files []string `json:"files"` // all relative to the root; default is request URI path
30
+ SelectionPolicy string `json:"selection_policy"`
31
+ Fallback caddyhttp.RouteList `json:"fallback"`
32
+ Browse *Browse `json:"browse"`
33
+ Hide []string `json:"hide"`
34
+ Rehandle bool `json:"rehandle"` // issue a rehandle (internal redirect) if request is rewritten
35
+ // TODO: Etag
36
+ // TODO: Content negotiation
37
}
38
22
-func (sf StaticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
23
- http.FileServer(http.Dir(sf.Root)).ServeHTTP(w, r)
39
+// Provision sets up the static files responder.
40
+func (sf *StaticFiles) Provision(ctx caddy2.Context) error {
41
+ if sf.Fallback != nil {
42
+ err := sf.Fallback.Provision(ctx)
43
+ if err != nil {
44
+ return fmt.Errorf("setting up fallback routes: %v", err)
45
+ }
46
+ }
47
+ if sf.IndexNames == nil {
48
+ sf.IndexNames = defaultIndexNames
49
+ }
50
return nil
51
}
52
53
+// Validate ensures that sf has a valid configuration.
54
+func (sf *StaticFiles) Validate() error {
55
+ switch sf.SelectionPolicy {
56
+ case "",
57
+ "first_existing",
58
+ "largest_size",
59
+ "smallest_size",
60
+ "most_recently_modified":
61
+ default:
62
+ return fmt.Errorf("unknown selection policy %s", sf.SelectionPolicy)
63
+ }
64
+ return nil
65
+}
66
+
67
+func (sf *StaticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
68
+ // TODO: Prevent directory traversal, see https://play.golang.org/p/oh77BiVQFti
69
+
70
+ // http.FileServer(http.Dir(sf.Directory)).ServeHTTP(w, r)
71
+
72
+ //////////////
73
+
74
+ // TODO: Still needed?
75
+ // // Prevent absolute path access on Windows, e.g.: http://localhost:5000/C:\Windows\notepad.exe
76
+ // // TODO: does stdlib http.Dir handle this? see first check of http.Dir.Open()...
77
+ // if runtime.GOOS == "windows" && len(reqPath) > 0 && filepath.IsAbs(reqPath[1:]) {
78
+ // return caddyhttp.Error(http.StatusNotFound, fmt.Errorf("request path was absolute"))
79
+ // }
80
+
81
+ repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer)
82
+
83
+ // map the request to a filename
84
+ pathBefore := r.URL.Path
85
+ filename := sf.selectFile(r, repl)
86
+ if filename == "" {
87
+ // no files worked, so resort to fallback
88
+ if sf.Fallback != nil {
89
+ fallback := sf.Fallback.BuildCompositeRoute(w, r)
90
+ return fallback.ServeHTTP(w, r)
91
+ }
92
+ return caddyhttp.Error(http.StatusNotFound, nil)
93
+ }
94
+
95
+ // if the ultimate destination has changed, submit
96
+ // this request for a rehandling (internal redirect)
97
+ // if configured to do so
98
+ // TODO: double check this against https://docs.nginx.com/nginx/admin-guide/web-server/serving-static-content/
99
+ if r.URL.Path != pathBefore && sf.Rehandle {
100
+ return caddyhttp.ErrRehandle
101
+ }
102
+
103
+ // get information about the file
104
+ info, err := os.Stat(filename)
105
+ if err != nil {
106
+ if os.IsNotExist(err) {
107
+ return caddyhttp.Error(http.StatusNotFound, err)
108
+ } else if os.IsPermission(err) {
109
+ return caddyhttp.Error(http.StatusForbidden, err)
110
+ }
111
+ // TODO: treat this as resource exhaustion like with os.Open? Or unnecessary here?
112
+ return caddyhttp.Error(http.StatusInternalServerError, err)
113
+ }
114
+
115
+ // if the request mapped to a directory, see if
116
+ // there is an index file we can serve
117
+ if info.IsDir() && len(sf.IndexNames) > 0 {
118
+ filesToHide := sf.transformHidePaths(repl)
119
+
120
+ for _, indexPage := range sf.IndexNames {
121
+ indexPath := path.Join(filename, indexPage)
122
+ if fileIsHidden(indexPath, filesToHide) {
123
+ // pretend this file doesn't exist
124
+ continue
125
+ }
126
+
127
+ indexInfo, err := os.Stat(indexPath)
128
+ if err != nil {
129
+ continue
130
+ }
131
+
132
+ // we found an index file that might work,
133
+ // so rewrite the request path and, if
134
+ // configured, do an internal redirect
135
+ // TODO: I don't know if the logic for rewriting
136
+ // the URL here is the right logic
137
+ r.URL.Path = path.Join(r.URL.Path, indexPage)
138
+ if sf.Rehandle {
139
+ return caddyhttp.ErrRehandle
140
+ }
141
+
142
+ info = indexInfo
143
+ break
144
+ }
145
+ }
146
+
147
+ // if still referencing a directory, delegate
148
+ // to browse or return an error
149
+ if info.IsDir() {
150
+ if sf.Browse != nil {
151
+ return sf.Browse.ServeHTTP(w, r)
152
+ }
153
+ return caddyhttp.Error(http.StatusNotFound, nil)
154
+ }
155
+
156
+ // open the file
157
+ file, err := os.Open(info.Name())
158
+ if err != nil {
159
+ if os.IsNotExist(err) {
160
+ return caddyhttp.Error(http.StatusNotFound, err)
161
+ } else if os.IsPermission(err) {
162
+ return caddyhttp.Error(http.StatusForbidden, err)
163
+ }
164
+ // maybe the server is under load and ran out of file descriptors?
165
+ // have client wait arbitrary seconds to help prevent a stampede
166
+ backoff := weakrand.Intn(maxBackoff-minBackoff) + minBackoff
167
+ w.Header().Set("Retry-After", strconv.Itoa(backoff))
168
+ return caddyhttp.Error(http.StatusServiceUnavailable, err)
169
+ }
170
+ defer file.Close()
171
+
172
+ // TODO: Right now we return an invalid response if the
173
+ // request is for a directory and there is no index file
174
+ // or dir browsing; we should return a 404 I think...
175
+
176
+ // TODO: Etag?
177
+
178
+ // TODO: content negotiation? (brotli sidecar files, etc...)
179
+
180
+ // let the standard library do what it does best; note, however,
181
+ // that errors generated by ServeContent are written immediately
182
+ // to the response, so we cannot handle them (but errors here are rare)
183
+ http.ServeContent(w, r, info.Name(), info.ModTime(), file)
184
+
185
+ return nil
186
+}
187
+
188
+func (sf *StaticFiles) transformHidePaths(repl caddy2.Replacer) []string {
189
+ hide := make([]string, len(sf.Hide))
190
+ for i := range sf.Hide {
191
+ hide[i] = repl.ReplaceAll(sf.Hide[i], "")
192
+ }
193
+ return hide
194
+}
195
+
196
+func (sf *StaticFiles) selectFile(r *http.Request, repl caddy2.Replacer) string {
197
+ root := repl.ReplaceAll(sf.Root, "")
198
+ if root == "" {
199
+ root = "."
200
+ }
201
+
202
+ if sf.Files == nil {
203
+ return filepath.Join(root, r.URL.Path)
204
+ }
205
+
206
+ switch sf.SelectionPolicy {
207
+ // TODO: Make these policy names constants
208
+ case "", "first_existing":
209
+ filesToHide := sf.transformHidePaths(repl)
210
+ for _, f := range sf.Files {
211
+ suffix := repl.ReplaceAll(f, "")
212
+ // TODO: sanitize path
213
+ fullpath := filepath.Join(root, suffix)
214
+ if !fileIsHidden(fullpath, filesToHide) && fileExists(fullpath) {
215
+ r.URL.Path = suffix
216
+ return fullpath
217
+ }
218
+ }
219
+
220
+ case "largest_size":
221
+ var largestSize int64
222
+ var largestFilename string
223
+ var largestSuffix string
224
+ for _, f := range sf.Files {
225
+ suffix := repl.ReplaceAll(f, "")
226
+ // TODO: sanitize path
227
+ fullpath := filepath.Join(root, suffix)
228
+ info, err := os.Stat(fullpath)
229
+ if err == nil && info.Size() > largestSize {
230
+ largestSize = info.Size()
231
+ largestFilename = fullpath
232
+ largestSuffix = suffix
233
+ }
234
+ }
235
+ r.URL.Path = largestSuffix
236
+ return largestFilename
237
+
238
+ case "smallest_size":
239
+ var smallestSize int64
240
+ var smallestFilename string
241
+ var smallestSuffix string
242
+ for _, f := range sf.Files {
243
+ suffix := repl.ReplaceAll(f, "")
244
+ // TODO: sanitize path
245
+ fullpath := filepath.Join(root, suffix)
246
+ info, err := os.Stat(fullpath)
247
+ if err == nil && (smallestSize == 0 || info.Size() < smallestSize) {
248
+ smallestSize = info.Size()
249
+ smallestFilename = fullpath
250
+ smallestSuffix = suffix
251
+ }
252
+ }
253
+ r.URL.Path = smallestSuffix
254
+ return smallestFilename
255
+
256
+ case "most_recently_modified":
257
+ var recentDate time.Time
258
+ var recentFilename string
259
+ var recentSuffix string
260
+ for _, f := range sf.Files {
261
+ suffix := repl.ReplaceAll(f, "")
262
+ // TODO: sanitize path
263
+ fullpath := filepath.Join(root, suffix)
264
+ info, err := os.Stat(fullpath)
265
+ if err == nil &&
266
+ (recentDate.IsZero() || info.ModTime().After(recentDate)) {
267
+ recentDate = info.ModTime()
268
+ recentFilename = fullpath
269
+ recentSuffix = suffix
270
+ }
271
+ }
272
+ r.URL.Path = recentSuffix
273
+ return recentFilename
274
+ }
275
+
276
+ return ""
277
+}
278
+
279
+// fileExists returns true if file exists.
280
+func fileExists(file string) bool {
281
+ _, err := os.Stat(file)
282
+ return !os.IsNotExist(err)
283
+}
284
+
285
+func fileIsHidden(filename string, hide []string) bool {
286
+ nameOnly := filepath.Base(filename)
287
+ sep := string(filepath.Separator)
288
+
289
+ // see if file is hidden
290
+ for _, h := range hide {
291
+ // assuming h is a glob/shell-like pattern,
292
+ // use it to compare the whole file path;
293
+ // but if there is no separator in h, then
294
+ // just compare against the file's name
295
+ compare := filename
296
+ if !strings.Contains(h, sep) {
297
+ compare = nameOnly
298
+ }
299
+
300
+ hidden, err := filepath.Match(h, compare)
301
+ if err != nil {
302
+ // malformed pattern; fallback by checking prefix
303
+ if strings.HasPrefix(filename, h) {
304
+ return true
305
+ }
306
+ }
307
+ if hidden {
308
+ // file name or path matches hide pattern
309
+ return true
310
+ }
311
+ }
312
+
313
+ return false
314
+}
315
+
316
+var defaultIndexNames = []string{"index.html"}
317
+
318
+const minBackoff, maxBackoff = 2, 5
319
+
320
// Interface guard
321
var _ caddyhttp.Handler = (*StaticFiles)(nil)
modules/caddyhttp/staticresp.go
+4
-4
@@ -23,16 +23,16 @@ type Static struct {
23
}
24
25
func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
26
- repl := r.Context().Value(ReplacerCtxKey).(*Replacer)
26
+ repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer)
27
28
// close the connection after responding
29
r.Close = s.Close
30
31
// set all headers, with replacements
32
for field, vals := range s.Headers {
33
- field = repl.Replace(field, "")
33
+ field = repl.ReplaceAll(field, "")
34
for i := range vals {
35
- vals[i] = repl.Replace(vals[i], "")
35
+ vals[i] = repl.ReplaceAll(vals[i], "")
36
}
37
w.Header()[field] = vals
38
}
@@ -46,7 +46,7 @@ func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
46
47
// write the response body, with replacements
48
if s.Body != "" {
49
- fmt.Fprint(w, repl.Replace(s.Body, ""))
49
+ fmt.Fprint(w, repl.ReplaceAll(s.Body, ""))
50
}
51
52
return nil
modules/caddyhttp/table.go
new
+41
@@ -0,0 +1,41 @@
1
+package caddyhttp
2
+
3
+import (
4
+ "net/http"
5
+
6
+ "bitbucket.org/lightcodelabs/caddy2"
7
+)
8
+
9
+func init() {
10
+ caddy2.RegisterModule(caddy2.Module{
11
+ Name: "http.middleware.table",
12
+ New: func() (interface{}, error) { return new(tableMiddleware), nil },
13
+ })
14
+
15
+ caddy2.RegisterModule(caddy2.Module{
16
+ Name: "http.matchers.table",
17
+ New: func() (interface{}, error) { return new(tableMatcher), nil },
18
+ })
19
+}
20
+
21
+type tableMiddleware struct {
22
+}
23
+
24
+func (t tableMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error {
25
+ // tbl := r.Context().Value(TableCtxKey).(map[string]interface{})
26
+
27
+ // TODO: implement this...
28
+
29
+ return nil
30
+}
31
+
32
+type tableMatcher struct {
33
+}
34
+
35
+func (m tableMatcher) Match(r *http.Request) bool {
36
+ return false // TODO: implement
37
+}
38
+
39
+// Interface guards
40
+var _ MiddlewareHandler = (*tableMiddleware)(nil)
41
+var _ RequestMatcher = (*tableMatcher)(nil)
replacer.go
new
+104
@@ -0,0 +1,104 @@
1
+package caddy2
2
+
3
+import (
4
+ "os"
5
+ "path/filepath"
6
+ "runtime"
7
+ "strings"
8
+)
9
+
10
+// Replacer can replace values in strings.
11
+type Replacer interface {
12
+ Set(variable, value string)
13
+ Delete(variable string)
14
+ Map(func() map[string]string)
15
+ ReplaceAll(input, empty string) string
16
+}
17
+
18
+// NewReplacer returns a new Replacer.
19
+func NewReplacer() Replacer {
20
+ rep := &replacer{
21
+ static: make(map[string]string),
22
+ }
23
+ rep.providers = []ReplacementsFunc{
24
+ defaultReplacements,
25
+ func() map[string]string { return rep.static },
26
+ }
27
+ return rep
28
+}
29
+
30
+type replacer struct {
31
+ providers []ReplacementsFunc
32
+ static map[string]string
33
+}
34
+
35
+// Map augments the map of replacements with those returned
36
+// by the given replacements function. The function is only
37
+// executed at replace-time.
38
+func (r *replacer) Map(replacements func() map[string]string) {
39
+ r.providers = append(r.providers, replacements)
40
+}
41
+
42
+// Set sets a custom variable to a static value.
43
+func (r *replacer) Set(variable, value string) {
44
+ r.static[variable] = value
45
+}
46
+
47
+// Delete removes a variable with a static value
48
+// that was created using Set.
49
+func (r *replacer) Delete(variable string) {
50
+ delete(r.static, variable)
51
+}
52
+
53
+// ReplaceAll replaces placeholders in input with their values.
54
+// Values that are empty string will be substituted with the
55
+// empty parameter.
56
+func (r *replacer) ReplaceAll(input, empty string) string {
57
+ if !strings.Contains(input, phOpen) {
58
+ return input
59
+ }
60
+ for _, replacements := range r.providers {
61
+ for key, val := range replacements() {
62
+ if val == "" {
63
+ val = empty
64
+ }
65
+ input = strings.ReplaceAll(input, phOpen+key+phClose, val)
66
+ }
67
+ }
68
+ return input
69
+}
70
+
71
+// ReplacementsFunc is a function that returns replacements,
72
+// which is variable names mapped to their values. The
73
+// function will be evaluated only at replace-time to ensure
74
+// the most current values are mapped.
75
+type ReplacementsFunc func() map[string]string
76
+
77
+var defaultReplacements = func() map[string]string {
78
+ m := map[string]string{
79
+ "system.hostname": func() string {
80
+ // OK if there is an error; just return empty string
81
+ name, _ := os.Hostname()
82
+ return name
83
+ }(),
84
+ "system.slash": string(filepath.Separator),
85
+ "system.os": runtime.GOOS,
86
+ "system.arch": runtime.GOARCH,
87
+ }
88
+
89
+ // add environment variables
90
+ for _, keyval := range os.Environ() {
91
+ parts := strings.SplitN(keyval, "=", 2)
92
+ if len(parts) != 2 {
93
+ continue
94
+ }
95
+ m["env."+strings.ToUpper(parts[0])] = parts[1]
96
+ }
97
+
98
+ return m
99
+}
100
+
101
+// ReplacerCtxKey is the context key for a replacer.
102
+const ReplacerCtxKey CtxKey = "replacer"
103
+
104
+const phOpen, phClose = "{", "}"