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)