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

Co-authored-by: flga <flga@users.noreply.github.com>
This commit is contained in:
fleandro 2022-09-02 05:02:48 +01:00 committed by GitHub
parent ec2a5762b0
commit f2a7e7c966
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 116 additions and 41 deletions

View file

@ -35,16 +35,16 @@ func init() {
// UnmarshalCaddyfile deserializes Caddyfile tokens into h. // UnmarshalCaddyfile deserializes Caddyfile tokens into h.
// //
// transport fastcgi { // transport fastcgi {
// root <path> // root <path>
// split <at> // split <at>
// env <key> <value> // env <key> <value>
// resolve_root_symlink // resolve_root_symlink
// dial_timeout <duration> // dial_timeout <duration>
// read_timeout <duration> // read_timeout <duration>
// write_timeout <duration> // write_timeout <duration>
// } // capture_stderr
// // }
func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() { for d.Next() {
for d.NextBlock(0) { for d.NextBlock(0) {
@ -107,6 +107,12 @@ func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
} }
t.WriteTimeout = caddy.Duration(dur) t.WriteTimeout = caddy.Duration(dur)
case "capture_stderr":
if d.NextArg() {
return d.ArgErr()
}
t.CaptureStderr = true
default: default:
return d.Errf("unrecognized subdirective %s", d.Val()) return d.Errf("unrecognized subdirective %s", d.Val())
} }
@ -120,31 +126,31 @@ func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Unmarshaler is invoked by this function) but the resulting proxy is specially // Unmarshaler is invoked by this function) but the resulting proxy is specially
// configured for most™ PHP apps over FastCGI. A line such as this: // configured for most™ PHP apps over FastCGI. A line such as this:
// //
// php_fastcgi localhost:7777 // php_fastcgi localhost:7777
// //
// is equivalent to a route consisting of: // is equivalent to a route consisting of:
// //
// # Add trailing slash for directory requests // # Add trailing slash for directory requests
// @canonicalPath { // @canonicalPath {
// file {path}/index.php // file {path}/index.php
// not path */ // not path */
// } // }
// redir @canonicalPath {path}/ 308 // redir @canonicalPath {path}/ 308
// //
// # If the requested file does not exist, try index files // # If the requested file does not exist, try index files
// @indexFiles file { // @indexFiles file {
// try_files {path} {path}/index.php index.php // try_files {path} {path}/index.php index.php
// split_path .php // split_path .php
// } // }
// rewrite @indexFiles {http.matchers.file.relative} // rewrite @indexFiles {http.matchers.file.relative}
// //
// # Proxy PHP files to the FastCGI responder // # Proxy PHP files to the FastCGI responder
// @phpFiles path *.php // @phpFiles path *.php
// reverse_proxy @phpFiles localhost:7777 { // reverse_proxy @phpFiles localhost:7777 {
// transport fastcgi { // transport fastcgi {
// split .php // split .php
// } // }
// } // }
// //
// Thus, this directive produces multiple handlers, each with a different // Thus, this directive produces multiple handlers, each with a different
// matcher because multiple consecutive handlers are necessary to support // matcher because multiple consecutive handlers are necessary to support
@ -154,7 +160,7 @@ func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// //
// If a matcher is specified by the user, for example: // If a matcher is specified by the user, for example:
// //
// php_fastcgi /subpath localhost:7777 // php_fastcgi /subpath localhost:7777
// //
// then the resulting handlers are wrapped in a subroute that uses the // then the resulting handlers are wrapped in a subroute that uses the
// user's matcher as a prerequisite to enter the subroute. In other // user's matcher as a prerequisite to enter the subroute. In other
@ -303,6 +309,14 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
fcgiTransport.WriteTimeout = caddy.Duration(dur) fcgiTransport.WriteTimeout = caddy.Duration(dur)
dispenser.Delete() dispenser.Delete()
dispenser.Delete() dispenser.Delete()
case "capture_stderr":
args := dispenser.RemainingArgs()
dispenser.Delete()
for range args {
dispenser.Delete()
}
fcgiTransport.CaptureStderr = true
} }
} }
} }

View file

@ -42,6 +42,8 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"go.uber.org/zap"
) )
// FCGIListenSockFileno describes listen socket file number. // FCGIListenSockFileno describes listen socket file number.
@ -180,6 +182,7 @@ type FCGIClient struct {
stderr bytes.Buffer stderr bytes.Buffer
keepAlive bool keepAlive bool
reqID uint16 reqID uint16
logger *zap.Logger
} }
// DialWithDialerContext connects to the fcgi responder at the specified network address, using custom net.Dialer // DialWithDialerContext connects to the fcgi responder at the specified network address, using custom net.Dialer
@ -339,7 +342,6 @@ type streamReader struct {
} }
func (w *streamReader) Read(p []byte) (n int, err error) { func (w *streamReader) Read(p []byte) (n int, err error) {
if len(p) > 0 { if len(p) > 0 {
if len(w.buf) == 0 { if len(w.buf) == 0 {
@ -400,9 +402,24 @@ func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err er
type clientCloser struct { type clientCloser struct {
*FCGIClient *FCGIClient
io.Reader io.Reader
status int
logger *zap.Logger
} }
func (f clientCloser) Close() error { return f.rwc.Close() } func (f clientCloser) Close() error {
stderr := f.FCGIClient.stderr.Bytes()
if len(stderr) == 0 {
return f.FCGIClient.rwc.Close()
}
if f.status >= 400 {
f.logger.Error("stderr", zap.ByteString("body", stderr))
} else {
f.logger.Warn("stderr", zap.ByteString("body", stderr))
}
return f.FCGIClient.rwc.Close()
}
// Request returns a HTTP Response with Header and Body // Request returns a HTTP Response with Header and Body
// from fcgi responder // from fcgi responder
@ -442,9 +459,19 @@ func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Res
resp.ContentLength, _ = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64) resp.ContentLength, _ = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
if chunked(resp.TransferEncoding) { if chunked(resp.TransferEncoding) {
resp.Body = clientCloser{c, httputil.NewChunkedReader(rb)} resp.Body = clientCloser{
FCGIClient: c,
Reader: httputil.NewChunkedReader(rb),
status: resp.StatusCode,
logger: c.logger,
}
} else { } else {
resp.Body = clientCloser{c, io.NopCloser(rb)} resp.Body = clientCloser{
FCGIClient: c,
Reader: rb,
status: resp.StatusCode,
logger: c.logger,
}
} }
return return
} }

View file

@ -34,6 +34,8 @@ import (
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
) )
var noopLogger = zap.NewNop()
func init() { func init() {
caddy.RegisterModule(Transport{}) caddy.RegisterModule(Transport{})
} }
@ -74,6 +76,11 @@ type Transport struct {
// The duration used to set a deadline when sending to the FastCGI server. // The duration used to set a deadline when sending to the FastCGI server.
WriteTimeout caddy.Duration `json:"write_timeout,omitempty"` WriteTimeout caddy.Duration `json:"write_timeout,omitempty"`
// Capture and log any messages sent by the upstream on stderr. Logs at WARN
// level by default. If the response has a 4xx or 5xx status ERROR level will
// be used instead.
CaptureStderr bool `json:"capture_stderr,omitempty"`
serverSoftware string serverSoftware string
logger *zap.Logger logger *zap.Logger
} }
@ -108,6 +115,8 @@ func (t *Transport) Provision(ctx caddy.Context) error {
// RoundTrip implements http.RoundTripper. // RoundTrip implements http.RoundTripper.
func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) { func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) {
server := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server)
// Disallow null bytes in the request path, because // Disallow null bytes in the request path, because
// PHP upstreams may do bad things, like execute a // PHP upstreams may do bad things, like execute a
// non-PHP file as PHP code. See #4574 // non-PHP file as PHP code. See #4574
@ -135,10 +144,16 @@ func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) {
address = dialInfo.Address address = dialInfo.Address
} }
logCreds := server.Logs != nil && server.Logs.ShouldLogCredentials
loggableReq := caddyhttp.LoggableHTTPRequest{
Request: r,
ShouldLogCredentials: logCreds,
}
loggableEnv := loggableEnv{vars: env, logCredentials: logCreds}
t.logger.Debug("roundtrip", t.logger.Debug("roundtrip",
zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: r}), zap.Object("request", loggableReq),
zap.String("dial", address), zap.String("dial", address),
zap.Object("env", env), zap.Object("env", loggableEnv),
) )
fcgiBackend, err := DialContext(ctx, network, address) fcgiBackend, err := DialContext(ctx, network, address)
@ -146,6 +161,14 @@ func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) {
// TODO: wrap in a special error type if the dial failed, so retries can happen if enabled // TODO: wrap in a special error type if the dial failed, so retries can happen if enabled
return nil, fmt.Errorf("dialing backend: %v", err) return nil, fmt.Errorf("dialing backend: %v", err)
} }
if t.CaptureStderr {
fcgiBackend.logger = t.logger.With(
zap.Object("request", loggableReq),
zap.Object("env", loggableEnv),
)
} else {
fcgiBackend.logger = noopLogger
}
// fcgiBackend gets closed when response body is closed (see clientCloser) // fcgiBackend gets closed when response body is closed (see clientCloser)
// read/write timeouts // read/write timeouts
@ -364,11 +387,22 @@ func (t Transport) splitPos(path string) int {
return -1 return -1
} }
// envVars is a simple type to allow for speeding up zap log encoding.
type envVars map[string]string type envVars map[string]string
func (env envVars) MarshalLogObject(enc zapcore.ObjectEncoder) error { // loggableEnv is a simple type to allow for speeding up zap log encoding.
for k, v := range env { type loggableEnv struct {
vars envVars
logCredentials bool
}
func (env loggableEnv) MarshalLogObject(enc zapcore.ObjectEncoder) error {
for k, v := range env.vars {
if !env.logCredentials {
switch strings.ToLower(k) {
case "http_cookie", "http_set_cookie", "http_authorization", "http_proxy_authorization":
v = ""
}
}
enc.AddString(k, v) enc.AddString(k, v)
} }
return nil return nil
@ -387,7 +421,7 @@ var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
// Interface guards // Interface guards
var ( var (
_ zapcore.ObjectMarshaler = (*envVars)(nil) _ zapcore.ObjectMarshaler = (*loggableEnv)(nil)
_ caddy.Provisioner = (*Transport)(nil) _ caddy.Provisioner = (*Transport)(nil)
_ http.RoundTripper = (*Transport)(nil) _ http.RoundTripper = (*Transport)(nil)