@marcel91200 / caddy Mirror · read-only / commits / fec7fa8b
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

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 = "{", "}"