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

Various bug fixes and minor improvements

- Fix static responder so it doesn't replace its own headers config, and instead replaces the actual response header values - caddyhttp.ResponseRecorder type optionally buffers response - Add interface guards to ensure regexp matchers get provisioned - Use default HTTP port if one is not explicitly set - Encode middleware writes status code 200 if not written upstream - Templates and markdown only try to execute on text responses - Static file server sets Content-Type based on file extension only (this whole thing -- MIME sniffing, etc -- needs more configurability)

Matthew Holt committed Jun 21, 2019 at 14:36 UTC d49f762f6d9cdc2e92e8de40f0b0e99a9d0c4fc9
12 files changed +109 -35
modules/caddyhttp/caddyhttp.go
+6 -1
@@ -265,7 +265,12 @@ func (app *App) automaticHTTPS() error {
265 if err != nil {
266 return fmt.Errorf("%s: invalid listener address: %v", srvName, addr)
267 }
268 - httpRedirLnAddr := joinListenAddr(netw, host, strconv.Itoa(app.HTTPPort))
268 +
269 + httpPort := app.HTTPPort
270 + if httpPort == 0 {
271 + httpPort = DefaultHTTPPort
272 + }
273 + httpRedirLnAddr := joinListenAddr(netw, host, strconv.Itoa(httpPort))
274 lnAddrMap[httpRedirLnAddr] = struct{}{}
275
276 if parts := strings.SplitN(port, "-", 2); len(parts) == 2 {
modules/caddyhttp/encode/encode.go
+5 -1
@@ -157,7 +157,11 @@ func (rw *responseWriter) init() {
157 rw.Header().Set("Content-Encoding", rw.encodingName)
158 }
159 rw.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-encoded content
160 - rw.ResponseWriter.WriteHeader(rw.statusCode)
160 + status := rw.statusCode
161 + if status == 0 {
162 + status = http.StatusOK
163 + }
164 + rw.ResponseWriter.WriteHeader(status)
165 }
166
167 // Close writes any remaining buffered response and
modules/caddyhttp/fileserver/browse.go
+1
@@ -66,6 +66,7 @@ func (fsrv *FileServer) serveBrowse(dirPath string, w http.ResponseWriter, r *ht
66 }
67 w.Header().Set("Content-Type", "text/html; charset=utf-8")
68 }
69 +
70 buf.WriteTo(w)
71
72 return nil
modules/caddyhttp/fileserver/staticfiles.go
+11 -3
@@ -4,6 +4,7 @@ import (
4 "fmt"
5 "html/template"
6 weakrand "math/rand"
7 + "mime"
8 "net/http"
9 "os"
10 "path"
@@ -185,14 +186,21 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) error
186
187 // TODO: Etag
188
188 - // do not allow Go to sniff the content-type
189 if w.Header().Get("Content-Type") == "" {
190 - w.Header()["Content-Type"] = nil
190 + mtyp := mime.TypeByExtension(filepath.Ext(filename))
191 + if mtyp == "" {
192 + // do not allow Go to sniff the content-type; see
193 + // https://www.youtube.com/watch?v=8t8JYpt0egE
194 + // TODO: Consider writing a default mime type of application/octet-stream - this is secure but violates spec
195 + w.Header()["Content-Type"] = nil
196 + } else {
197 + w.Header().Set("Content-Type", mtyp)
198 + }
199 }
200
201 // let the standard library do what it does best; note, however,
202 // that errors generated by ServeContent are written immediately
195 - // to the response, so we cannot handle them (but errors here
203 + // to the response, so we cannot handle them (but errors there
204 // are rare)
205 http.ServeContent(w, r, info.Name(), info.ModTime(), file)
206
modules/caddyhttp/headers/headers.go
+13 -8
@@ -40,20 +40,25 @@ type RespHeaderOps struct {
40 func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
41 repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
42 apply(h.Request, r.Header, repl)
43 - if h.Response.Deferred || h.Response.Require != nil {
44 - w = &responseWriterWrapper{
45 - ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w},
46 - replacer: repl,
47 - require: h.Response.Require,
48 - headerOps: h.Response.HeaderOps,
43 + if h.Response != nil {
44 + if h.Response.Deferred || h.Response.Require != nil {
45 + w = &responseWriterWrapper{
46 + ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w},
47 + replacer: repl,
48 + require: h.Response.Require,
49 + headerOps: h.Response.HeaderOps,
50 + }
51 + } else {
52 + apply(h.Response.HeaderOps, w.Header(), repl)
53 }
50 - } else {
51 - apply(h.Response.HeaderOps, w.Header(), repl)
54 }
55 return next.ServeHTTP(w, r)
56 }
57
58 func apply(ops *HeaderOps, hdr http.Header, repl caddy.Replacer) {
59 + if ops == nil {
60 + return
61 + }
62 for fieldName, vals := range ops.Add {
63 fieldName = repl.ReplaceAll(fieldName, "")
64 for _, v := range vals {
modules/caddyhttp/markdown/markdown.go
+11 -3
@@ -4,6 +4,7 @@ import (
4 "bytes"
5 "net/http"
6 "strconv"
7 + "strings"
8 "sync"
9
10 "gopkg.in/russross/blackfriday.v2"
@@ -28,12 +29,19 @@ func (m Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
29 buf.Reset()
30 defer bufPool.Put(buf)
31
31 - rr := caddyhttp.NewResponseRecorder(w, buf)
32 + shouldBuf := func(status int) bool {
33 + return strings.HasPrefix(w.Header().Get("Content-Type"), "text/")
34 + }
35 +
36 + rec := caddyhttp.NewResponseRecorder(w, buf, shouldBuf)
37
33 - err := next.ServeHTTP(rr, r)
38 + err := next.ServeHTTP(rec, r)
39 if err != nil {
40 return err
41 }
42 + if !rec.Buffered() {
43 + return nil
44 + }
45
46 output := blackfriday.Run(buf.Bytes())
47
@@ -43,7 +51,7 @@ func (m Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
51 w.Header().Del("Etag") // don't know a way to quickly generate etag for dynamic content
52 w.Header().Del("Last-Modified") // useless for dynamic content since it's always changing
53
46 - w.WriteHeader(rr.Status())
54 + w.WriteHeader(rec.Status())
55 w.Write(output)
56
57 return nil
modules/caddyhttp/matchers.go
+6 -3
@@ -226,9 +226,9 @@ func (m MatchHeaderRE) Match(r *http.Request) bool {
226 }
227
228 // Provision compiles m's regular expressions.
229 -func (m MatchHeaderRE) Provision() error {
229 +func (m MatchHeaderRE) Provision(ctx caddy.Context) error {
230 for _, rm := range m {
231 - err := rm.Provision()
231 + err := rm.Provision(ctx)
232 if err != nil {
233 return err
234 }
@@ -371,7 +371,7 @@ type MatchRegexp struct {
371 }
372
373 // Provision compiles the regular expression.
374 -func (mre *MatchRegexp) Provision() error {
374 +func (mre *MatchRegexp) Provision(caddy.Context) error {
375 re, err := regexp.Compile(mre.Pattern)
376 if err != nil {
377 return fmt.Errorf("compiling matcher regexp %s: %v", mre.Pattern, err)
@@ -479,14 +479,17 @@ var (
479 _ RequestMatcher = (*MatchHost)(nil)
480 _ RequestMatcher = (*MatchPath)(nil)
481 _ RequestMatcher = (*MatchPathRE)(nil)
482 + _ caddy.Provisioner = (*MatchPathRE)(nil)
483 _ RequestMatcher = (*MatchMethod)(nil)
484 _ RequestMatcher = (*MatchQuery)(nil)
485 _ RequestMatcher = (*MatchHeader)(nil)
486 _ RequestMatcher = (*MatchHeaderRE)(nil)
487 + _ caddy.Provisioner = (*MatchHeaderRE)(nil)
488 _ RequestMatcher = (*MatchProtocol)(nil)
489 _ RequestMatcher = (*MatchRemoteIP)(nil)
490 _ caddy.Provisioner = (*MatchRemoteIP)(nil)
491 _ RequestMatcher = (*MatchNegate)(nil)
492 _ caddy.Provisioner = (*MatchNegate)(nil)
493 _ RequestMatcher = (*MatchStarlarkExpr)(nil)
494 + _ caddy.Provisioner = (*MatchRegexp)(nil)
495 )
modules/caddyhttp/matchers_test.go
+2 -2
@@ -219,7 +219,7 @@ func TestPathREMatcher(t *testing.T) {
219 },
220 } {
221 // compile the regexp and validate its name
222 - err := tc.match.Provision()
222 + err := tc.match.Provision(caddy.Context{})
223 if err != nil {
224 t.Errorf("Test %d %v: Provisioning: %v", i, tc.match, err)
225 continue
@@ -337,7 +337,7 @@ func TestHeaderREMatcher(t *testing.T) {
337 },
338 } {
339 // compile the regexp and validate its name
340 - err := tc.match.Provision()
340 + err := tc.match.Provision(caddy.Context{})
341 if err != nil {
342 t.Errorf("Test %d %v: Provisioning: %v", i, tc.match, err)
343 continue
modules/caddyhttp/responsewriter.go
+37 -6
@@ -61,9 +61,11 @@ var ErrNotImplemented = fmt.Errorf("method not implemented")
61
62 type responseRecorder struct {
63 *ResponseWriterWrapper
64 - wroteHeader bool
65 - statusCode int
66 - buf *bytes.Buffer
64 + wroteHeader bool
65 + statusCode int
66 + buf *bytes.Buffer
67 + shouldBuffer func(status int) bool
68 + stream bool
69 }
70
71 // NewResponseRecorder returns a new ResponseRecorder that can be
@@ -76,6 +78,16 @@ type responseRecorder struct {
78 // responses by wrapping Write and WriteHeader methods instead of
79 // buffering whole response bodies.
80 //
81 +// Recorders optionally buffer the response. When the headers are
82 +// to be written, shouldBuffer will be called with the status
83 +// code that is being written. The rest of the headers can be read
84 +// from w.Header(). If shouldBuffer returns true, the response
85 +// will be buffered. You can know the response was buffered if
86 +// the Buffered() method returns true. If the response was not
87 +// buffered, Buffered() will return false and that means the
88 +// response bypassed the recorder and was written directly to the
89 +// underlying writer.
90 +//
91 // Before calling this function in a middleware handler, make a
92 // new buffer or obtain one from a pool (use the sync.Pool) type.
93 // Using a pool is generally recommended for performance gains;
@@ -85,12 +97,13 @@ type responseRecorder struct {
97 // The returned recorder can be used in place of w when calling
98 // the next handler in the chain. When that handler returns, you
99 // can read the status code from the recorder's Status() method.
88 -// The response body fills buf, and the headers are available in
89 -// w.Header().
90 -func NewResponseRecorder(w http.ResponseWriter, buf *bytes.Buffer) ResponseRecorder {
100 +// The response body fills buf if it was buffered, and the headers
101 +// are available via w.Header().
102 +func NewResponseRecorder(w http.ResponseWriter, buf *bytes.Buffer, shouldBuffer func(status int) bool) ResponseRecorder {
103 return &responseRecorder{
104 ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: w},
105 buf: buf,
106 + shouldBuffer: shouldBuffer,
107 }
108 }
109
@@ -100,10 +113,22 @@ func (rr *responseRecorder) WriteHeader(statusCode int) {
113 }
114 rr.statusCode = statusCode
115 rr.wroteHeader = true
116 +
117 + // decide whether we should buffer the response
118 + if rr.shouldBuffer == nil {
119 + return
120 + }
121 + rr.stream = !rr.shouldBuffer(rr.statusCode)
122 + if rr.stream {
123 + rr.ResponseWriterWrapper.WriteHeader(rr.statusCode)
124 + }
125 }
126
127 func (rr *responseRecorder) Write(data []byte) (int, error) {
128 rr.WriteHeader(http.StatusOK)
129 + if rr.stream {
130 + return rr.ResponseWriterWrapper.Write(data)
131 + }
132 return rr.buf.Write(data)
133 }
134
@@ -118,12 +143,18 @@ func (rr *responseRecorder) Buffer() *bytes.Buffer {
143 return rr.buf
144 }
145
146 +// Buffered returns whether rr has decided to buffer the response.
147 +func (rr *responseRecorder) Buffered() bool {
148 + return !rr.stream
149 +}
150 +
151 // ResponseRecorder is a http.ResponseWriter that records
152 // responses instead of writing them to the client.
153 type ResponseRecorder interface {
154 HTTPInterfaces
155 Status() int
156 Buffer() *bytes.Buffer
157 + Buffered() bool
158 }
159
160 // Interface guards
modules/caddyhttp/server.go
+2 -2
@@ -72,11 +72,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
72 err := s.executeCompositeRoute(w, r, errStack)
73 if err != nil {
74 // TODO: what should we do if the error handler has an error?
75 - log.Printf("[ERROR] handling error: %v", err)
75 + log.Printf("[ERROR] [%s %s] handling error: %v", r.Method, r.RequestURI, err)
76 }
77 } else {
78 // TODO: polish the default error handling
79 - log.Printf("[ERROR] Handler: %s", err)
79 + log.Printf("[ERROR] [%s %s] %v", r.Method, r.RequestURI, err)
80 if handlerErr, ok := err.(HandlerError); ok {
81 w.WriteHeader(handlerErr.StatusCode)
82 } else {
modules/caddyhttp/staticresp.go
+3 -2
@@ -33,10 +33,11 @@ func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
33 // set all headers
34 for field, vals := range s.Headers {
35 field = repl.ReplaceAll(field, "")
36 + newVals := make([]string, len(vals))
37 for i := range vals {
37 - vals[i] = repl.ReplaceAll(vals[i], "")
38 + newVals[i] = repl.ReplaceAll(vals[i], "")
39 }
39 - w.Header()[field] = vals
40 + w.Header()[field] = newVals
41 }
42
43 // do not allow Go to sniff the content-type
modules/caddyhttp/templates/templates.go
+12 -4
@@ -6,6 +6,7 @@ import (
6 "io"
7 "net/http"
8 "strconv"
9 + "strings"
10
11 "github.com/caddyserver/caddy"
12 "github.com/caddyserver/caddy/modules/caddyhttp"
@@ -37,14 +38,21 @@ func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
38 buf.Reset()
39 defer bufPool.Put(buf)
40
40 - rr := caddyhttp.NewResponseRecorder(w, buf)
41 + shouldBuf := func(status int) bool {
42 + return strings.HasPrefix(w.Header().Get("Content-Type"), "text/")
43 + }
44 +
45 + rec := caddyhttp.NewResponseRecorder(w, buf, shouldBuf)
46
42 - err := next.ServeHTTP(rr, r)
47 + err := next.ServeHTTP(rec, r)
48 if err != nil {
49 return err
50 }
51 + if !rec.Buffered() {
52 + return nil
53 + }
54
47 - err = t.executeTemplate(rr, r)
55 + err = t.executeTemplate(rec, r)
56 if err != nil {
57 return err
58 }
@@ -54,7 +62,7 @@ func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
62 w.Header().Del("Etag") // don't know a way to quickly generate etag for dynamic content
63 w.Header().Del("Last-Modified") // useless for dynamic content since it's always changing
64
57 - w.WriteHeader(rr.Status())
65 + w.WriteHeader(rec.Status())
66 io.Copy(w, buf)
67
68 return nil