prometheus/util/httputil/compression.go
jub0bs 329ec6831a
util/httputil: reduce heap allocations in newCompressedResponseWriter (#16001)
* util/httputil: Benchmark newCompressedResponseWriter

This benchmark illustrates that newCompressedResponseWriter incurs a
prohibitive amount of heap allocations when handling a request containing a
malicious Accept-Encoding header.¬

Signed-off-by: jub0bs <jcretel-infosec+github@protonmail.com>

* util/httputil: Improve newCompressedResponseWriter

This change dramatically reduces the heap allocations (in bytes)
incurred when handling a request containing a malicious Accept-Encoding header.

Below are some benchmark results; for conciseness, I've omitted the name of the
benchmark function (BenchmarkNewCompressionHandler_MaliciousAcceptEncoding):

```
goos: darwin
goarch: amd64
pkg: github.com/prometheus/prometheus/util/httputil
cpu: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
│     old     │                 new                 │
│   sec/op    │   sec/op     vs base                │
  18.60m ± 2%   13.54m ± 3%  -27.17% (p=0.000 n=10)

│       old        │                 new                 │
│       B/op       │    B/op     vs base                 │
  16785442.50 ± 0%   32.00 ± 0%  -100.00% (p=0.000 n=10)

│    old     │                new                 │
│ allocs/op  │ allocs/op   vs base                │
  2.000 ± 0%   1.000 ± 0%  -50.00% (p=0.000 n=10)
```

Signed-off-by: jub0bs <jcretel-infosec+github@protonmail.com>

---------

Signed-off-by: jub0bs <jcretel-infosec+github@protonmail.com>
2025-02-11 14:14:55 +01:00

102 lines
2.9 KiB
Go

// Copyright 2013 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httputil
import (
"io"
"net/http"
"strings"
"github.com/klauspost/compress/gzip"
"github.com/klauspost/compress/zlib"
)
const (
acceptEncodingHeader = "Accept-Encoding"
contentEncodingHeader = "Content-Encoding"
gzipEncoding = "gzip"
deflateEncoding = "deflate"
)
// Wrapper around http.Handler which adds suitable response compression based
// on the client's Accept-Encoding headers.
type compressedResponseWriter struct {
http.ResponseWriter
writer io.Writer
}
// Writes HTTP response content data.
func (c *compressedResponseWriter) Write(p []byte) (int, error) {
return c.writer.Write(p)
}
// Closes the compressedResponseWriter and ensures to flush all data before.
func (c *compressedResponseWriter) Close() {
if zlibWriter, ok := c.writer.(*zlib.Writer); ok {
zlibWriter.Flush()
}
if gzipWriter, ok := c.writer.(*gzip.Writer); ok {
gzipWriter.Flush()
}
if closer, ok := c.writer.(io.Closer); ok {
defer closer.Close()
}
}
// Constructs a new compressedResponseWriter based on client request headers.
func newCompressedResponseWriter(writer http.ResponseWriter, req *http.Request) *compressedResponseWriter {
raw := req.Header.Get(acceptEncodingHeader)
var (
encoding string
commaFound bool
)
for {
encoding, raw, commaFound = strings.Cut(raw, ",")
switch strings.TrimSpace(encoding) {
case gzipEncoding:
writer.Header().Set(contentEncodingHeader, gzipEncoding)
return &compressedResponseWriter{
ResponseWriter: writer,
writer: gzip.NewWriter(writer),
}
case deflateEncoding:
writer.Header().Set(contentEncodingHeader, deflateEncoding)
return &compressedResponseWriter{
ResponseWriter: writer,
writer: zlib.NewWriter(writer),
}
}
if !commaFound {
break
}
}
return &compressedResponseWriter{
ResponseWriter: writer,
writer: writer,
}
}
// CompressionHandler is a wrapper around http.Handler which adds suitable
// response compression based on the client's Accept-Encoding headers.
type CompressionHandler struct {
Handler http.Handler
}
// ServeHTTP adds compression to the original http.Handler's ServeHTTP() method.
func (c CompressionHandler) ServeHTTP(writer http.ResponseWriter, req *http.Request) {
compWriter := newCompressedResponseWriter(writer, req)
c.Handler.ServeHTTP(compWriter, req)
compWriter.Close()
}