@marcel91200 / caddy Mirror · read-only / commits / f2a7e7c9
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 13, 14:15 UTC

fastcgi: allow users to log stderr output (#4967) (#5004)

Co-authored-by: flga <flga@users.noreply.github.com>

fleandro committed Sep 2, 2022 at 05:02 UTC f2a7e7c966c42b3a09bc8414136af0439fd7b630
3 files changed +116 -41
modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
+45 -31
@@ -35,16 +35,16 @@ func init() {
35
36 // UnmarshalCaddyfile deserializes Caddyfile tokens into h.
37 //
38 -// transport fastcgi {
39 -// root <path>
40 -// split <at>
41 -// env <key> <value>
42 -// resolve_root_symlink
43 -// dial_timeout <duration>
44 -// read_timeout <duration>
45 -// write_timeout <duration>
46 -// }
47 -//
38 +// transport fastcgi {
39 +// root <path>
40 +// split <at>
41 +// env <key> <value>
42 +// resolve_root_symlink
43 +// dial_timeout <duration>
44 +// read_timeout <duration>
45 +// write_timeout <duration>
46 +// capture_stderr
47 +// }
48 func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
49 for d.Next() {
50 for d.NextBlock(0) {
@@ -107,6 +107,12 @@ func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
107 }
108 t.WriteTimeout = caddy.Duration(dur)
109
110 + case "capture_stderr":
111 + if d.NextArg() {
112 + return d.ArgErr()
113 + }
114 + t.CaptureStderr = true
115 +
116 default:
117 return d.Errf("unrecognized subdirective %s", d.Val())
118 }
@@ -120,31 +126,31 @@ func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
126 // Unmarshaler is invoked by this function) but the resulting proxy is specially
127 // configured for most™️ PHP apps over FastCGI. A line such as this:
128 //
123 -// php_fastcgi localhost:7777
129 +// php_fastcgi localhost:7777
130 //
131 // is equivalent to a route consisting of:
132 //
127 -// # Add trailing slash for directory requests
128 -// @canonicalPath {
129 -// file {path}/index.php
130 -// not path */
131 -// }
132 -// redir @canonicalPath {path}/ 308
133 +// # Add trailing slash for directory requests
134 +// @canonicalPath {
135 +// file {path}/index.php
136 +// not path */
137 +// }
138 +// redir @canonicalPath {path}/ 308
139 //
134 -// # If the requested file does not exist, try index files
135 -// @indexFiles file {
136 -// try_files {path} {path}/index.php index.php
137 -// split_path .php
138 -// }
139 -// rewrite @indexFiles {http.matchers.file.relative}
140 +// # If the requested file does not exist, try index files
141 +// @indexFiles file {
142 +// try_files {path} {path}/index.php index.php
143 +// split_path .php
144 +// }
145 +// rewrite @indexFiles {http.matchers.file.relative}
146 //
141 -// # Proxy PHP files to the FastCGI responder
142 -// @phpFiles path *.php
143 -// reverse_proxy @phpFiles localhost:7777 {
144 -// transport fastcgi {
145 -// split .php
146 -// }
147 -// }
147 +// # Proxy PHP files to the FastCGI responder
148 +// @phpFiles path *.php
149 +// reverse_proxy @phpFiles localhost:7777 {
150 +// transport fastcgi {
151 +// split .php
152 +// }
153 +// }
154 //
155 // Thus, this directive produces multiple handlers, each with a different
156 // matcher because multiple consecutive handlers are necessary to support
@@ -154,7 +160,7 @@ func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
160 //
161 // If a matcher is specified by the user, for example:
162 //
157 -// php_fastcgi /subpath localhost:7777
163 +// php_fastcgi /subpath localhost:7777
164 //
165 // then the resulting handlers are wrapped in a subroute that uses the
166 // user's matcher as a prerequisite to enter the subroute. In other
@@ -303,6 +309,14 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
309 fcgiTransport.WriteTimeout = caddy.Duration(dur)
310 dispenser.Delete()
311 dispenser.Delete()
312 +
313 + case "capture_stderr":
314 + args := dispenser.RemainingArgs()
315 + dispenser.Delete()
316 + for range args {
317 + dispenser.Delete()
318 + }
319 + fcgiTransport.CaptureStderr = true
320 }
321 }
322 }
modules/caddyhttp/reverseproxy/fastcgi/client.go
+31 -4
@@ -42,6 +42,8 @@ import (
42 "strings"
43 "sync"
44 "time"
45 +
46 + "go.uber.org/zap"
47 )
48
49 // FCGIListenSockFileno describes listen socket file number.
@@ -180,6 +182,7 @@ type FCGIClient struct {
182 stderr bytes.Buffer
183 keepAlive bool
184 reqID uint16
185 + logger *zap.Logger
186 }
187
188 // DialWithDialerContext connects to the fcgi responder at the specified network address, using custom net.Dialer
@@ -339,7 +342,6 @@ type streamReader struct {
342 }
343
344 func (w *streamReader) Read(p []byte) (n int, err error) {
342 -
345 if len(p) > 0 {
346 if len(w.buf) == 0 {
347
@@ -400,9 +402,24 @@ func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err er
402 type clientCloser struct {
403 *FCGIClient
404 io.Reader
405 +
406 + status int
407 + logger *zap.Logger
408 }
409
405 -func (f clientCloser) Close() error { return f.rwc.Close() }
410 +func (f clientCloser) Close() error {
411 + stderr := f.FCGIClient.stderr.Bytes()
412 + if len(stderr) == 0 {
413 + return f.FCGIClient.rwc.Close()
414 + }
415 +
416 + if f.status >= 400 {
417 + f.logger.Error("stderr", zap.ByteString("body", stderr))
418 + } else {
419 + f.logger.Warn("stderr", zap.ByteString("body", stderr))
420 + }
421 + return f.FCGIClient.rwc.Close()
422 +}
423
424 // Request returns a HTTP Response with Header and Body
425 // from fcgi responder
@@ -442,9 +459,19 @@ func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Res
459 resp.ContentLength, _ = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
460
461 if chunked(resp.TransferEncoding) {
445 - resp.Body = clientCloser{c, httputil.NewChunkedReader(rb)}
462 + resp.Body = clientCloser{
463 + FCGIClient: c,
464 + Reader: httputil.NewChunkedReader(rb),
465 + status: resp.StatusCode,
466 + logger: c.logger,
467 + }
468 } else {
447 - resp.Body = clientCloser{c, io.NopCloser(rb)}
469 + resp.Body = clientCloser{
470 + FCGIClient: c,
471 + Reader: rb,
472 + status: resp.StatusCode,
473 + logger: c.logger,
474 + }
475 }
476 return
477 }
modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
+40 -6
@@ -34,6 +34,8 @@ import (
34 "github.com/caddyserver/caddy/v2"
35 )
36
37 +var noopLogger = zap.NewNop()
38 +
39 func init() {
40 caddy.RegisterModule(Transport{})
41 }
@@ -74,6 +76,11 @@ type Transport struct {
76 // The duration used to set a deadline when sending to the FastCGI server.
77 WriteTimeout caddy.Duration `json:"write_timeout,omitempty"`
78
79 + // Capture and log any messages sent by the upstream on stderr. Logs at WARN
80 + // level by default. If the response has a 4xx or 5xx status ERROR level will
81 + // be used instead.
82 + CaptureStderr bool `json:"capture_stderr,omitempty"`
83 +
84 serverSoftware string
85 logger *zap.Logger
86 }
@@ -108,6 +115,8 @@ func (t *Transport) Provision(ctx caddy.Context) error {
115
116 // RoundTrip implements http.RoundTripper.
117 func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) {
118 + server := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server)
119 +
120 // Disallow null bytes in the request path, because
121 // PHP upstreams may do bad things, like execute a
122 // non-PHP file as PHP code. See #4574
@@ -135,10 +144,16 @@ func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) {
144 address = dialInfo.Address
145 }
146
147 + logCreds := server.Logs != nil && server.Logs.ShouldLogCredentials
148 + loggableReq := caddyhttp.LoggableHTTPRequest{
149 + Request: r,
150 + ShouldLogCredentials: logCreds,
151 + }
152 + loggableEnv := loggableEnv{vars: env, logCredentials: logCreds}
153 t.logger.Debug("roundtrip",
139 - zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: r}),
154 + zap.Object("request", loggableReq),
155 zap.String("dial", address),
141 - zap.Object("env", env),
156 + zap.Object("env", loggableEnv),
157 )
158
159 fcgiBackend, err := DialContext(ctx, network, address)
@@ -146,6 +161,14 @@ func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) {
161 // TODO: wrap in a special error type if the dial failed, so retries can happen if enabled
162 return nil, fmt.Errorf("dialing backend: %v", err)
163 }
164 + if t.CaptureStderr {
165 + fcgiBackend.logger = t.logger.With(
166 + zap.Object("request", loggableReq),
167 + zap.Object("env", loggableEnv),
168 + )
169 + } else {
170 + fcgiBackend.logger = noopLogger
171 + }
172 // fcgiBackend gets closed when response body is closed (see clientCloser)
173
174 // read/write timeouts
@@ -364,11 +387,22 @@ func (t Transport) splitPos(path string) int {
387 return -1
388 }
389
367 -// envVars is a simple type to allow for speeding up zap log encoding.
390 type envVars map[string]string
391
370 -func (env envVars) MarshalLogObject(enc zapcore.ObjectEncoder) error {
371 - for k, v := range env {
392 +// loggableEnv is a simple type to allow for speeding up zap log encoding.
393 +type loggableEnv struct {
394 + vars envVars
395 + logCredentials bool
396 +}
397 +
398 +func (env loggableEnv) MarshalLogObject(enc zapcore.ObjectEncoder) error {
399 + for k, v := range env.vars {
400 + if !env.logCredentials {
401 + switch strings.ToLower(k) {
402 + case "http_cookie", "http_set_cookie", "http_authorization", "http_proxy_authorization":
403 + v = ""
404 + }
405 + }
406 enc.AddString(k, v)
407 }
408 return nil
@@ -387,7 +421,7 @@ var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
421
422 // Interface guards
423 var (
390 - _ zapcore.ObjectMarshaler = (*envVars)(nil)
424 + _ zapcore.ObjectMarshaler = (*loggableEnv)(nil)
425
426 _ caddy.Provisioner = (*Transport)(nil)
427 _ http.RoundTripper = (*Transport)(nil)