handlerLogger's responseRecorder and handlerWriteTimeout's writeTimeoutWriter both wrap http.ResponseWriter but implement neither http.Flusher nor Unwrap(). Any handler that flushes mid-response has that flush silently dropped, and its output only reaches the client when the handler returns. That defeats the stated purpose of writeTimeoutWriter, whose own comment says it exists so one can "write long responses, splitted in chunks, without causing timeouts": the deadline is reset per Write(), but nothing ever leaves the buffer until the end. Found while adding a server-sent-events endpoint: events written and flushed at t=0.4s only reached the client at t=2.2s, when the handler returned. Adding Flush() plus Unwrap() (so http.ResponseController can reach through) fixes both. The new test drives a real listener through both wrappers and reads while the handler is still blocked, so it fails on the current code by hitting the read deadline.
174 lines
3.8 KiB
Go
174 lines
3.8 KiB
Go
package httpp
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/bluenviron/mediamtx/internal/logger"
|
|
)
|
|
|
|
const (
|
|
maxRequestBodySizeToLog = 10 * 1024
|
|
)
|
|
|
|
var requestHeadersToRedact = map[string]struct{}{
|
|
"Authorization": {},
|
|
"Cookie": {},
|
|
"Proxy-Authorization": {},
|
|
"Set-Cookie": {},
|
|
"X-Api-Key": {},
|
|
"X-Auth-Token": {},
|
|
}
|
|
|
|
var requestBodyContentTypeToLog = map[string]struct{}{
|
|
"application/sdp": {},
|
|
"application/trickle-ice-sdpfrag": {},
|
|
}
|
|
|
|
func valueOrDefault(value, def string) string {
|
|
if value != "" {
|
|
return value
|
|
}
|
|
return def
|
|
}
|
|
|
|
// this is an improvement of httputil.DumpRequest with the following changes:
|
|
// - sensitive headers are redacted
|
|
// - body is truncated to prevent memory exhaustion
|
|
func dumpRequest(req *http.Request) []byte {
|
|
peek, err := io.ReadAll(io.LimitReader(req.Body, maxRequestBodySizeToLog+1))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
capped := peek
|
|
if int64(len(capped)) > maxRequestBodySizeToLog {
|
|
capped = append([]byte(nil), capped[:maxRequestBodySizeToLog]...)
|
|
capped = append(capped, []byte("\n\n(truncated body)\n")...)
|
|
}
|
|
|
|
req.Body = io.NopCloser(io.MultiReader(bytes.NewReader(peek), req.Body))
|
|
|
|
var b bytes.Buffer
|
|
|
|
reqURI := req.RequestURI
|
|
if reqURI == "" {
|
|
reqURI = req.URL.RequestURI()
|
|
}
|
|
|
|
fmt.Fprintf(&b, "%s %s HTTP/%d.%d\r\n", valueOrDefault(req.Method, "GET"),
|
|
reqURI, req.ProtoMajor, req.ProtoMinor)
|
|
|
|
absRequestURI := strings.HasPrefix(req.RequestURI, "http://") || strings.HasPrefix(req.RequestURI, "https://")
|
|
if !absRequestURI {
|
|
host := req.Host
|
|
if host == "" && req.URL != nil {
|
|
host = req.URL.Host
|
|
}
|
|
if host != "" {
|
|
fmt.Fprintf(&b, "Host: %s\r\n", host)
|
|
}
|
|
}
|
|
|
|
keys := make([]string, 0, len(req.Header))
|
|
for k := range req.Header {
|
|
keys = append(keys, k)
|
|
}
|
|
|
|
slices.Sort(keys)
|
|
|
|
for _, k := range keys {
|
|
for _, v := range req.Header[k] {
|
|
if _, ok := requestHeadersToRedact[k]; ok {
|
|
v = "<redacted>"
|
|
}
|
|
fmt.Fprintf(&b, "%s: %s\r\n", k, v)
|
|
}
|
|
}
|
|
|
|
io.WriteString(&b, "\r\n") //nolint:errcheck
|
|
|
|
b.Write(capped)
|
|
|
|
return b.Bytes()
|
|
}
|
|
|
|
type responseRecorder struct {
|
|
w http.ResponseWriter
|
|
status int
|
|
body []byte
|
|
size int
|
|
}
|
|
|
|
func (w *responseRecorder) Header() http.Header {
|
|
return w.w.Header()
|
|
}
|
|
|
|
func (w *responseRecorder) Write(b []byte) (int, error) {
|
|
if w.status == 0 {
|
|
w.status = http.StatusOK
|
|
}
|
|
|
|
contentType := w.Header().Get("Content-Type")
|
|
if _, ok := requestBodyContentTypeToLog[contentType]; ok {
|
|
w.body = append(w.body, b...)
|
|
} else {
|
|
w.size += len(b)
|
|
}
|
|
|
|
return w.w.Write(b)
|
|
}
|
|
|
|
func (w *responseRecorder) WriteHeader(statusCode int) {
|
|
w.status = statusCode
|
|
w.w.WriteHeader(statusCode)
|
|
}
|
|
|
|
func (w *responseRecorder) dump() string {
|
|
var buf bytes.Buffer
|
|
fmt.Fprintf(&buf, "%s %d %s\n", "HTTP/1.1", w.status, http.StatusText(w.status))
|
|
w.w.Header().Write(&buf) //nolint:errcheck
|
|
buf.Write([]byte("\n"))
|
|
|
|
if w.body != nil {
|
|
buf.Write(w.body)
|
|
} else if w.size > 0 {
|
|
fmt.Fprintf(&buf, "(body of %d bytes)", w.size)
|
|
}
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
// log requests and responses.
|
|
type handlerLogger struct {
|
|
h http.Handler
|
|
log logger.Writer
|
|
}
|
|
|
|
func (h *handlerLogger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
h.log.Log(logger.Debug, "[conn %v] [c->s] %s", r.RemoteAddr, dumpRequest(r))
|
|
|
|
resRecorder := &responseRecorder{w: w}
|
|
|
|
h.h.ServeHTTP(resRecorder, r)
|
|
|
|
h.log.Log(logger.Debug, "[conn %v] [s->c] %s", r.RemoteAddr, resRecorder.dump())
|
|
}
|
|
|
|
// Flush propagates the flush to the wrapped writer, so that handlers streaming
|
|
// a long response (server-sent events) are not held until they return.
|
|
func (w *responseRecorder) Flush() {
|
|
if f, ok := w.w.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
}
|
|
|
|
// Unwrap lets http.ResponseController reach the underlying writer.
|
|
func (w *responseRecorder) Unwrap() http.ResponseWriter {
|
|
return w.w
|
|
}
|