Skip to content

Commit d931fdc

Browse files
authored
client: allow overriding grpc-accept-encoding header (#8718)
- Add `grpc.WithAcceptedCompressionNames` so a client can explicitly cap the `grpc-accept-encoding` header to a vetted subset of registered compressors - Ensure the new value is propagated to the proper code while still appending per-call legacy compressors when needed Updates #2786. RELEASE NOTES: * client: Add `experimental.AcceptCompressors` so callers can restrict the `grpc-accept-encoding` header advertised for a call. --------- Signed-off-by: Israel Blancas <iblancasa@gmail.com>
1 parent 0800ec7 commit d931fdc

8 files changed

Lines changed: 281 additions & 12 deletions

File tree

experimental/experimental.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,12 @@ func WithBufferPool(bufferPool mem.BufferPool) grpc.DialOption {
6262
func BufferPool(bufferPool mem.BufferPool) grpc.ServerOption {
6363
return internal.BufferPool.(func(mem.BufferPool) grpc.ServerOption)(bufferPool)
6464
}
65+
66+
// AcceptCompressors returns a CallOption that limits the values
67+
// advertised in the grpc-accept-encoding header for the provided RPC. The
68+
// supplied names must correspond to compressors registered via
69+
// encoding.RegisterCompressor. Passing no names advertises "identity" (no
70+
// compression) only.
71+
func AcceptCompressors(names ...string) grpc.CallOption {
72+
return internal.AcceptCompressors.(func(...string) grpc.CallOption)(names...)
73+
}

internal/experimental.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,8 @@ var (
2525
// BufferPool is implemented by the grpc package and returns a server
2626
// option to configure a shared buffer pool for a grpc.Server.
2727
BufferPool any // func (grpc.SharedBufferPool) grpc.ServerOption
28+
29+
// AcceptCompressors is implemented by the grpc package and returns
30+
// a call option that restricts the grpc-accept-encoding header for a call.
31+
AcceptCompressors any // func(...string) grpc.CallOption
2832
)

internal/transport/http2_client.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,9 @@ func (t *http2Client) createHeaderFields(ctx context.Context, callHdr *CallHdr)
551551
hfLen := 7 // :method, :scheme, :path, :authority, content-type, user-agent, te
552552
hfLen += len(authData) + len(callAuthData)
553553
registeredCompressors := t.registeredCompressors
554+
if callHdr.AcceptedCompressors != nil {
555+
registeredCompressors = *callHdr.AcceptedCompressors
556+
}
554557
if callHdr.PreviousAttempts > 0 {
555558
hfLen++
556559
}

internal/transport/transport.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,12 @@ type CallHdr struct {
553553
// outbound message.
554554
SendCompress string
555555

556+
// AcceptedCompressors overrides the grpc-accept-encoding header for this
557+
// call. When nil, the transport advertises the default set of registered
558+
// compressors. A non-nil pointer overrides that value (including the empty
559+
// string to advertise none).
560+
AcceptedCompressors *string
561+
556562
// Creds specifies credentials.PerRPCCredentials for a call.
557563
Creds credentials.PerRPCCredentials
558564

rpc_util.go

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import (
3333
"google.golang.org/grpc/credentials"
3434
"google.golang.org/grpc/encoding"
3535
"google.golang.org/grpc/encoding/proto"
36+
"google.golang.org/grpc/internal"
37+
"google.golang.org/grpc/internal/grpcutil"
3638
"google.golang.org/grpc/internal/transport"
3739
"google.golang.org/grpc/mem"
3840
"google.golang.org/grpc/metadata"
@@ -41,6 +43,10 @@ import (
4143
"google.golang.org/grpc/status"
4244
)
4345

46+
func init() {
47+
internal.AcceptCompressors = acceptCompressors
48+
}
49+
4450
// Compressor defines the interface gRPC uses to compress a message.
4551
//
4652
// Deprecated: use package encoding.
@@ -151,16 +157,32 @@ func (d *gzipDecompressor) Type() string {
151157

152158
// callInfo contains all related configuration and information about an RPC.
153159
type callInfo struct {
154-
compressorName string
155-
failFast bool
156-
maxReceiveMessageSize *int
157-
maxSendMessageSize *int
158-
creds credentials.PerRPCCredentials
159-
contentSubtype string
160-
codec baseCodec
161-
maxRetryRPCBufferSize int
162-
onFinish []func(err error)
163-
authority string
160+
compressorName string
161+
failFast bool
162+
maxReceiveMessageSize *int
163+
maxSendMessageSize *int
164+
creds credentials.PerRPCCredentials
165+
contentSubtype string
166+
codec baseCodec
167+
maxRetryRPCBufferSize int
168+
onFinish []func(err error)
169+
authority string
170+
acceptedResponseCompressors []string
171+
}
172+
173+
func acceptedCompressorAllows(allowed []string, name string) bool {
174+
if allowed == nil {
175+
return true
176+
}
177+
if name == "" || name == encoding.Identity {
178+
return true
179+
}
180+
for _, a := range allowed {
181+
if a == name {
182+
return true
183+
}
184+
}
185+
return false
164186
}
165187

166188
func defaultCallInfo() *callInfo {
@@ -170,6 +192,29 @@ func defaultCallInfo() *callInfo {
170192
}
171193
}
172194

195+
func newAcceptedCompressionConfig(names []string) ([]string, error) {
196+
if len(names) == 0 {
197+
return nil, nil
198+
}
199+
var allowed []string
200+
seen := make(map[string]struct{}, len(names))
201+
for _, name := range names {
202+
name = strings.TrimSpace(name)
203+
if name == "" || name == encoding.Identity {
204+
continue
205+
}
206+
if !grpcutil.IsCompressorNameRegistered(name) {
207+
return nil, status.Errorf(codes.InvalidArgument, "grpc: compressor %q is not registered", name)
208+
}
209+
if _, dup := seen[name]; dup {
210+
continue
211+
}
212+
seen[name] = struct{}{}
213+
allowed = append(allowed, name)
214+
}
215+
return allowed, nil
216+
}
217+
173218
// CallOption configures a Call before it starts or extracts information from
174219
// a Call after it completes.
175220
type CallOption interface {
@@ -471,6 +516,31 @@ func (o CompressorCallOption) before(c *callInfo) error {
471516
}
472517
func (o CompressorCallOption) after(*callInfo, *csAttempt) {}
473518

519+
// acceptCompressors returns a CallOption that limits the compression algorithms
520+
// advertised in the grpc-accept-encoding header for response messages.
521+
// Compression algorithms not in the provided list will not be advertised, and
522+
// responses compressed with non-listed algorithms will be rejected.
523+
func acceptCompressors(names ...string) CallOption {
524+
cp := append([]string(nil), names...)
525+
return acceptCompressorsCallOption{names: cp}
526+
}
527+
528+
// acceptCompressorsCallOption is a CallOption that limits response compression.
529+
type acceptCompressorsCallOption struct {
530+
names []string
531+
}
532+
533+
func (o acceptCompressorsCallOption) before(c *callInfo) error {
534+
allowed, err := newAcceptedCompressionConfig(o.names)
535+
if err != nil {
536+
return err
537+
}
538+
c.acceptedResponseCompressors = allowed
539+
return nil
540+
}
541+
542+
func (acceptCompressorsCallOption) after(*callInfo, *csAttempt) {}
543+
474544
// CallContentSubtype returns a CallOption that will set the content-subtype
475545
// for a call. For example, if content-subtype is "json", the Content-Type over
476546
// the wire will be "application/grpc+json". The content-subtype is converted
@@ -857,8 +927,7 @@ func (p *payloadInfo) free() {
857927
// the buffer is no longer needed.
858928
// TODO: Refactor this function to reduce the number of arguments.
859929
// See: https://google.github.io/styleguide/go/best-practices.html#function-argument-lists
860-
func recvAndDecompress(p *parser, s recvCompressor, dc Decompressor, maxReceiveMessageSize int, payInfo *payloadInfo, compressor encoding.Compressor, isServer bool,
861-
) (out mem.BufferSlice, err error) {
930+
func recvAndDecompress(p *parser, s recvCompressor, dc Decompressor, maxReceiveMessageSize int, payInfo *payloadInfo, compressor encoding.Compressor, isServer bool) (out mem.BufferSlice, err error) {
862931
pf, compressed, err := p.recvMsg(maxReceiveMessageSize)
863932
if err != nil {
864933
return nil, err

rpc_util_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,118 @@ const (
4848
decompressionErrorMsg = "invalid compression format"
4949
)
5050

51+
type testCompressorForRegistry struct {
52+
name string
53+
}
54+
55+
func (c *testCompressorForRegistry) Compress(w io.Writer) (io.WriteCloser, error) {
56+
return &testWriteCloser{w}, nil
57+
}
58+
59+
func (c *testCompressorForRegistry) Decompress(r io.Reader) (io.Reader, error) {
60+
return r, nil
61+
}
62+
63+
func (c *testCompressorForRegistry) Name() string {
64+
return c.name
65+
}
66+
67+
type testWriteCloser struct {
68+
io.Writer
69+
}
70+
71+
func (w *testWriteCloser) Close() error {
72+
return nil
73+
}
74+
75+
func (s) TestNewAcceptedCompressionConfig(t *testing.T) {
76+
// Register a test compressor for multi-compressor tests
77+
testCompressor := &testCompressorForRegistry{name: "test-compressor"}
78+
encoding.RegisterCompressor(testCompressor)
79+
defer func() {
80+
// Unregister the test compressor
81+
encoding.RegisterCompressor(&testCompressorForRegistry{name: "test-compressor"})
82+
}()
83+
84+
tests := []struct {
85+
name string
86+
input []string
87+
wantAllowed []string
88+
wantErr bool
89+
}{
90+
{
91+
name: "identity-only",
92+
input: nil,
93+
wantAllowed: nil,
94+
},
95+
{
96+
name: "single valid",
97+
input: []string{"gzip"},
98+
wantAllowed: []string{"gzip"},
99+
},
100+
{
101+
name: "dedupe and trim",
102+
input: []string{" gzip ", "gzip"},
103+
wantAllowed: []string{"gzip"},
104+
},
105+
{
106+
name: "ignores identity",
107+
input: []string{"identity", "gzip"},
108+
wantAllowed: []string{"gzip"},
109+
},
110+
{
111+
name: "explicit identity only",
112+
input: []string{"identity"},
113+
wantAllowed: nil,
114+
},
115+
{
116+
name: "invalid compressor",
117+
input: []string{"does-not-exist"},
118+
wantErr: true,
119+
},
120+
{
121+
name: "only whitespace",
122+
input: []string{" ", "\t"},
123+
wantAllowed: nil,
124+
},
125+
{
126+
name: "multiple valid compressors",
127+
input: []string{"gzip", "test-compressor"},
128+
wantAllowed: []string{"gzip", "test-compressor"},
129+
},
130+
{
131+
name: "multiple with identity and whitespace",
132+
input: []string{"gzip", "identity", " test-compressor ", " "},
133+
wantAllowed: []string{"gzip", "test-compressor"},
134+
},
135+
{
136+
name: "empty string in list",
137+
input: []string{"gzip", "", "test-compressor"},
138+
wantAllowed: []string{"gzip", "test-compressor"},
139+
},
140+
{
141+
name: "mixed valid and invalid",
142+
input: []string{"gzip", "invalid-comp"},
143+
wantErr: true,
144+
},
145+
}
146+
147+
for _, tt := range tests {
148+
t.Run(tt.name, func(t *testing.T) {
149+
allowed, err := newAcceptedCompressionConfig(tt.input)
150+
if (err != nil) != tt.wantErr {
151+
t.Fatalf("newAcceptedCompressionConfig(%v) error = %v, wantErr %v", tt.input, err, tt.wantErr)
152+
}
153+
if tt.wantErr {
154+
return
155+
}
156+
if diff := cmp.Diff(tt.wantAllowed, allowed); diff != "" {
157+
t.Fatalf("allowed diff (-want +got): %v", diff)
158+
}
159+
})
160+
}
161+
}
162+
51163
type fullReader struct {
52164
data []byte
53165
}

stream.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"math"
2626
rand "math/rand/v2"
2727
"strconv"
28+
"strings"
2829
"sync"
2930
"time"
3031

@@ -321,6 +322,10 @@ func newClientStreamWithParams(ctx context.Context, desc *StreamDesc, cc *Client
321322
DoneFunc: doneFunc,
322323
Authority: callInfo.authority,
323324
}
325+
if allowed := callInfo.acceptedResponseCompressors; len(allowed) > 0 {
326+
headerValue := strings.Join(allowed, ",")
327+
callHdr.AcceptedCompressors = &headerValue
328+
}
324329

325330
// Set our outgoing compression according to the UseCompressor CallOption, if
326331
// set. In that case, also find the compressor from the encoding package.
@@ -1145,6 +1150,10 @@ func (a *csAttempt) recvMsg(m any, payInfo *payloadInfo) (err error) {
11451150
a.decompressorV0 = nil
11461151
a.decompressorV1 = encoding.GetCompressor(ct)
11471152
}
1153+
// Validate that the compression method is acceptable for this call.
1154+
if !acceptedCompressorAllows(cs.callInfo.acceptedResponseCompressors, ct) {
1155+
return status.Errorf(codes.Internal, "grpc: peer compressed the response with %q which is not allowed by AcceptCompressors", ct)
1156+
}
11481157
} else {
11491158
// No compression is used; disable our decompressor.
11501159
a.decompressorV0 = nil
@@ -1490,6 +1499,10 @@ func (as *addrConnStream) RecvMsg(m any) (err error) {
14901499
as.decompressorV0 = nil
14911500
as.decompressorV1 = encoding.GetCompressor(ct)
14921501
}
1502+
// Validate that the compression method is acceptable for this call.
1503+
if !acceptedCompressorAllows(as.callInfo.acceptedResponseCompressors, ct) {
1504+
return status.Errorf(codes.Internal, "grpc: peer compressed the response with %q which is not allowed by AcceptCompressors", ct)
1505+
}
14931506
} else {
14941507
// No compression is used; disable our decompressor.
14951508
as.decompressorV0 = nil

0 commit comments

Comments
 (0)