vendor: google.golang.org/grpc v1.85.0

full diff: https://github.com/grpc/grpc-go/compare/v1.84.0...v1.85.0

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski committed 2026-10-27 14:04:33 +01:00
1 parent 9cad7b0b9f
commit 204ac6dcae
40 files changed
+593 -1875

No files matched your search

+1 -1
View File
@@ -118,7 +118,7 @@ require (
golang.org/x/text v0.43.0
golang.org/x/time v0.16.0
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc
google.golang.org/grpc v1.84.0
google.golang.org/grpc v1.85.0
google.golang.org/protobuf v1.36.12
gotest.tools/v3 v3.5.2
pgregory.net/rapid v1.3.0
+4 -4
View File
@@ -231,8 +231,8 @@ github.com/docker/libtrust v0.0.0-20150526203908-9cbd2a1374f4/go.mod h1:cyGadeNE
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
github.com/envoyproxy/go-control-plane/envoy v1.39.1-0.20260819172001-e6e3fd93e4be h1:SWe0x6yfglnxuvOiYgTTnNq7QD/yvqthOh1RBI8Bj8w=
github.com/envoyproxy/go-control-plane/envoy v1.39.1-0.20260819172001-e6e3fd93e4be/go.mod h1:PYEOlng9XcrulfyWpm49jECTPV0LT4q8cO7fLW/xwgk=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/erofs/go-erofs v0.3.1 h1:Sux82Jq9yvyYhIoLgSHDp741p/+370HsOj9dAh1+VVs=
@@ -867,8 +867,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc=
google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0=
google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
google.golang.org/grpc v1.85.0 h1:BPBCfuVRQ1qvBhKDOq9uvQllMvO59rKsYhXpp7Dd8vw=
google.golang.org/grpc v1.85.0/go.mod h1:0rQrOD58yjal/0EaVbGBANindixsCZNQPDV3VKtaKuQ=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-139
View File
@@ -1,139 +0,0 @@
// Copyright 2023 Google LLC
//
// 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 idtoken
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strconv"
"strings"
"sync"
"time"
"cloud.google.com/go/auth/internal"
"github.com/googleapis/gax-go/v2/internallog"
)
type cachingClient struct {
client *http.Client
// clock optionally specifies a func to return the current time.
// If nil, time.Now is used.
clock func() time.Time
mu sync.Mutex
certs map[string]*cachedResponse
logger *slog.Logger
}
func newCachingClient(client *http.Client, logger *slog.Logger) *cachingClient {
return &cachingClient{
client: client,
certs: make(map[string]*cachedResponse, 2),
logger: logger,
}
}
type cachedResponse struct {
resp *certResponse
exp time.Time
}
func (c *cachingClient) getCert(ctx context.Context, url string) (*certResponse, error) {
if response, ok := c.get(url); ok {
return response, nil
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
c.logger.DebugContext(ctx, "cert request", "request", internallog.HTTPRequest(req, nil))
resp, body, err := internal.DoRequest(c.client, req)
if err != nil {
return nil, err
}
c.logger.DebugContext(ctx, "cert response", "response", internallog.HTTPResponse(resp, body))
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("idtoken: unable to retrieve cert, got status code %d", resp.StatusCode)
}
certResp := &certResponse{}
if err := json.Unmarshal(body, &certResp); err != nil {
return nil, err
}
c.set(url, certResp, resp.Header)
return certResp, nil
}
func (c *cachingClient) now() time.Time {
if c.clock != nil {
return c.clock()
}
return time.Now()
}
func (c *cachingClient) get(url string) (*certResponse, bool) {
c.mu.Lock()
defer c.mu.Unlock()
cachedResp, ok := c.certs[url]
if !ok {
return nil, false
}
if c.now().After(cachedResp.exp) {
return nil, false
}
return cachedResp.resp, true
}
func (c *cachingClient) set(url string, resp *certResponse, headers http.Header) {
exp := c.calculateExpireTime(headers)
c.mu.Lock()
c.certs[url] = &cachedResponse{resp: resp, exp: exp}
c.mu.Unlock()
}
// calculateExpireTime will determine the expire time for the cache based on
// HTTP headers. If there is any difficulty reading the headers the fallback is
// to set the cache to expire now.
func (c *cachingClient) calculateExpireTime(headers http.Header) time.Time {
var maxAge int
cc := strings.Split(headers.Get("cache-control"), ",")
for _, v := range cc {
if strings.Contains(v, "max-age") {
ss := strings.Split(v, "=")
if len(ss) < 2 {
return c.now()
}
ma, err := strconv.Atoi(ss[1])
if err != nil {
return c.now()
}
maxAge = ma
}
}
a := headers.Get("age")
if a == "" {
return c.now().Add(time.Duration(maxAge) * time.Second)
}
age, err := strconv.Atoi(a)
if err != nil {
return c.now()
}
return c.now().Add(time.Duration(maxAge-age) * time.Second)
}
-89
View File
@@ -1,89 +0,0 @@
// Copyright 2023 Google LLC
//
// 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 idtoken
import (
"context"
"fmt"
"net/url"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/compute/metadata"
"github.com/googleapis/gax-go/v2/internallog"
)
const identitySuffix = "instance/service-accounts/default/identity"
// computeCredentials checks if this code is being run on GCE. If it is, it
// will use the metadata service to build a Credentials that fetches ID
// tokens.
func computeCredentials(opts *Options) (*auth.Credentials, error) {
if opts.CustomClaims != nil {
return nil, fmt.Errorf("idtoken: Options.CustomClaims can't be used with the metadata service, please provide a service account if you would like to use this feature")
}
metadataClient := metadata.NewWithOptions(&metadata.Options{
Logger: internallog.New(opts.Logger),
})
tp := &computeIDTokenProvider{
audience: opts.Audience,
format: opts.ComputeTokenFormat,
client: metadataClient,
}
return auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: auth.NewCachedTokenProvider(tp, &auth.CachedTokenProviderOptions{
ExpireEarly: 5 * time.Minute,
}),
ProjectIDProvider: auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) {
return metadataClient.ProjectIDWithContext(ctx)
}),
UniverseDomainProvider: &internal.ComputeUniverseDomainProvider{
MetadataClient: metadataClient,
},
}), nil
}
type computeIDTokenProvider struct {
audience string
format ComputeTokenFormat
client *metadata.Client
}
func (c *computeIDTokenProvider) Token(ctx context.Context) (*auth.Token, error) {
v := url.Values{}
v.Set("audience", c.audience)
if c.format != ComputeTokenFormatStandard {
v.Set("format", "full")
}
if c.format == ComputeTokenFormatFullWithLicense {
v.Set("licenses", "TRUE")
}
urlSuffix := identitySuffix + "?" + v.Encode()
res, err := c.client.GetWithContext(ctx, urlSuffix)
if err != nil {
return nil, err
}
if res == "" {
return nil, fmt.Errorf("idtoken: invalid empty response from metadata service")
}
return &auth.Token{
Value: res,
Type: internal.TokenTypeBearer,
// Compute tokens are valid for one hour:
// https://cloud.google.com/iam/docs/create-short-lived-credentials-direct#create-id
Expiry: time.Now().Add(1 * time.Hour),
}, nil
}
-143
View File
@@ -1,143 +0,0 @@
// Copyright 2023 Google LLC
//
// 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 idtoken
import (
"encoding/json"
"fmt"
"path/filepath"
"strings"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials"
"cloud.google.com/go/auth/credentials/impersonate"
intimpersonate "cloud.google.com/go/auth/credentials/internal/impersonate"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/credsfile"
"github.com/googleapis/gax-go/v2/internallog"
)
const (
jwtTokenURL = "https://oauth2.googleapis.com/token"
iamCredAud = "https://iamcredentials.googleapis.com/"
)
func credsFromDefault(creds *auth.Credentials, opts *Options) (*auth.Credentials, error) {
b := creds.JSON()
t, err := credsfile.ParseFileType(b)
if err != nil {
return nil, err
}
switch credentials.CredType(t) {
case credentials.ServiceAccount:
f, err := credsfile.ParseServiceAccount(b)
if err != nil {
return nil, err
}
var tp auth.TokenProvider
if resolveUniverseDomain(f) == internal.DefaultUniverseDomain {
tp, err = new2LOTokenProvider(f, opts)
if err != nil {
return nil, err
}
} else {
// In case of non-GDU universe domain, use IAM.
tp = intimpersonate.IDTokenIAMOptions{
Client: opts.client(),
Logger: internallog.New(opts.Logger),
// Pass the credentials universe domain to configure the endpoint.
UniverseDomain: auth.CredentialsPropertyFunc(creds.UniverseDomain),
ServiceAccountEmail: f.ClientEmail,
GenerateIDTokenRequest: intimpersonate.GenerateIDTokenRequest{
Audience: opts.Audience,
},
}
}
tp = auth.NewCachedTokenProvider(tp, nil)
return auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: tp,
JSON: b,
ProjectIDProvider: auth.CredentialsPropertyFunc(creds.ProjectID),
UniverseDomainProvider: auth.CredentialsPropertyFunc(creds.UniverseDomain),
}), nil
case credentials.ImpersonatedServiceAccount, credentials.ExternalAccount:
type url struct {
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
}
var accountURL url
if err := json.Unmarshal(b, &accountURL); err != nil {
return nil, err
}
account := filepath.Base(accountURL.ServiceAccountImpersonationURL)
account = strings.Split(account, ":")[0]
config := impersonate.IDTokenOptions{
Audience: opts.Audience,
TargetPrincipal: account,
IncludeEmail: true,
Client: opts.client(),
Credentials: creds,
Logger: internallog.New(opts.Logger),
}
idTokenCreds, err := impersonate.NewIDTokenCredentials(&config)
if err != nil {
return nil, err
}
return auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: idTokenCreds,
JSON: b,
ProjectIDProvider: auth.CredentialsPropertyFunc(creds.ProjectID),
UniverseDomainProvider: auth.CredentialsPropertyFunc(creds.UniverseDomain),
QuotaProjectIDProvider: auth.CredentialsPropertyFunc(creds.QuotaProjectID),
}), nil
default:
return nil, fmt.Errorf("idtoken: unsupported credentials type: %v", t)
}
}
func new2LOTokenProvider(f *credsfile.ServiceAccountFile, opts *Options) (auth.TokenProvider, error) {
opts2LO := &auth.Options2LO{
Email: f.ClientEmail,
PrivateKey: []byte(f.PrivateKey),
PrivateKeyID: f.PrivateKeyID,
TokenURL: f.TokenURL,
UseIDToken: true,
Logger: internallog.New(opts.Logger),
}
if opts2LO.TokenURL == "" {
opts2LO.TokenURL = jwtTokenURL
}
var customClaims map[string]interface{}
if opts != nil {
customClaims = opts.CustomClaims
}
if customClaims == nil {
customClaims = make(map[string]interface{})
}
customClaims["target_audience"] = opts.Audience
opts2LO.PrivateClaims = customClaims
return auth.New2LOTokenProvider(opts2LO)
}
// resolveUniverseDomain returns the default service domain for a given
// Cloud universe. This is the universe domain configured for the credentials,
// which will be used in endpoint.
func resolveUniverseDomain(f *credsfile.ServiceAccountFile) string {
if f.UniverseDomain != "" {
return f.UniverseDomain
}
return internal.DefaultUniverseDomain
}
-268
View File
@@ -1,268 +0,0 @@
// Copyright 2023 Google LLC
//
// 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 idtoken provides functionality for generating and validating ID
// tokens, with configurable options for audience, custom claims, and token
// formats.
//
// For more information on ID tokens, see
// https://cloud.google.com/docs/authentication/token-types#id.
package idtoken
import (
"errors"
"log/slog"
"net/http"
"os"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/credsfile"
"cloud.google.com/go/compute/metadata"
)
// ComputeTokenFormat dictates the the token format when requesting an ID token
// from the compute metadata service.
type ComputeTokenFormat int
const (
// ComputeTokenFormatDefault means the same as [ComputeTokenFormatFull].
ComputeTokenFormatDefault ComputeTokenFormat = iota
// ComputeTokenFormatStandard mean only standard JWT fields will be included
// in the token.
ComputeTokenFormatStandard
// ComputeTokenFormatFull means the token will include claims about the
// virtual machine instance and its project.
ComputeTokenFormatFull
// ComputeTokenFormatFullWithLicense means the same as
// [ComputeTokenFormatFull] with the addition of claims about licenses
// associated with the instance.
ComputeTokenFormatFullWithLicense
)
var (
defaultScopes = []string{
"https://iamcredentials.googleapis.com/",
"https://www.googleapis.com/auth/cloud-platform",
}
errMissingOpts = errors.New("idtoken: opts must be provided")
errMissingAudience = errors.New("idtoken: Audience must be provided")
errBothFileAndJSON = errors.New("idtoken: CredentialsFile and CredentialsJSON must not both be provided")
)
// Options for the configuration of creation of an ID token with
// [NewCredentials].
type Options struct {
// Audience is the `aud` field for the token, such as an API endpoint the
// token will grant access to. Required.
Audience string
// ComputeTokenFormat dictates the the token format when requesting an ID
// token from the compute metadata service. Optional.
ComputeTokenFormat ComputeTokenFormat
// CustomClaims specifies private non-standard claims for an ID token.
// Optional.
CustomClaims map[string]interface{}
// CredentialsFile sources a JSON credential file from the provided
// filepath. If provided, do not provide CredentialsJSON. Optional.
//
// Deprecated: This field is deprecated because of a potential security risk.
// It does not validate the credential configuration. The security risk occurs
// when a credential configuration is accepted from a source that is not
// under your control and used without validation on your side.
//
// If you know that you will be loading credential configurations of a
// specific type, it is recommended to use a credential-type-specific
// NewCredentialsFromFile method. This will ensure that an unexpected
// credential type with potential for malicious intent is not loaded
// unintentionally. You might still have to do validation for certain
// credential types. Please follow the recommendation for that method. For
// example, if you want to load only service accounts, you can use
//
// creds, err := idtoken.NewCredentialsFromFile(ctx, credentials.ServiceAccount, filename, opts)
//
// If you are loading your credential configuration from an untrusted source
// and have not mitigated the risks (e.g. by validating the configuration
// yourself), make these changes as soon as possible to prevent security
// risks to your environment.
//
// Regardless of the method used, it is always your responsibility to
// validate configurations received from external sources.
//
// For more details see:
// https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
CredentialsFile string
// CredentialsJSON sources a JSON credential file from the provided bytes.
// If provided, do not provide CredentialsJSON. Optional.
//
// Deprecated: This field is deprecated because of a potential security risk.
// It does not validate the credential configuration. The security risk occurs
// when a credential configuration is accepted from a source that is not
// under your control and used without validation on your side.
//
// If you know that you will be loading credential configurations of a
// specific type, it is recommended to use a credential-type-specific
// NewCredentialsFromJSON method. This will ensure that an unexpected
// credential type with potential for malicious intent is not loaded
// unintentionally. You might still have to do validation for certain
// credential types. Please follow the recommendation for that method. For
// example, if you want to load only service accounts, you can use
//
// creds, err := idtoken.NewCredentialsFromJSON(ctx, credentials.ServiceAccount, json, opts)
//
// If you are loading your credential configuration from an untrusted source
// and have not mitigated the risks (e.g. by validating the configuration
// yourself), make these changes as soon as possible to prevent security
// risks to your environment.
//
// Regardless of the method used, it is always your responsibility to
// validate configurations received from external sources.
//
// For more details see:
// https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
CredentialsJSON []byte
// Client configures the underlying client used to make network requests
// when fetching tokens. If provided this should be a fully-authenticated
// client. Optional.
Client *http.Client
// UniverseDomain is the default service domain for a given Cloud universe.
// The default value is "googleapis.com". This is the universe domain
// configured for the client, which will be compared to the universe domain
// that is separately configured for the credentials. Optional.
UniverseDomain string
// Logger is used for debug logging. If provided, logging will be enabled
// at the loggers configured level. By default logging is disabled unless
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
// logger will be used. Optional.
Logger *slog.Logger
}
func (o *Options) client() *http.Client {
if o == nil || o.Client == nil {
return internal.DefaultClient()
}
return o.Client
}
func (o *Options) validate() error {
if o == nil {
return errMissingOpts
}
if o.Audience == "" {
return errMissingAudience
}
if o.CredentialsFile != "" && len(o.CredentialsJSON) > 0 {
return errBothFileAndJSON
}
return nil
}
// NewCredentials creates a [cloud.google.com/go/auth.Credentials] that returns
// ID tokens configured by the opts provided. The parameter opts.Audience must
// not be empty. If both opts.CredentialsFile and opts.CredentialsJSON are
// empty, an attempt will be made to detect credentials from the environment
// (see [cloud.google.com/go/auth/credentials.DetectDefault]). Only service
// account, impersonated service account, external account and Compute
// credentials are supported.
func NewCredentials(opts *Options) (*auth.Credentials, error) {
if err := opts.validate(); err != nil {
return nil, err
}
b := opts.jsonBytes()
if b == nil && metadata.OnGCE() {
return computeCredentials(opts)
}
creds, err := credentials.DetectDefault(&credentials.DetectOptions{
Scopes: defaultScopes,
CredentialsJSON: b,
Client: opts.client(),
UseSelfSignedJWT: true,
})
if err != nil {
return nil, err
}
return credsFromDefault(creds, opts)
}
func (o *Options) jsonBytes() []byte {
if len(o.CredentialsJSON) > 0 {
return o.CredentialsJSON
}
var fnOverride string
if o != nil {
fnOverride = o.CredentialsFile
}
filename := credsfile.GetFileNameFromEnv(fnOverride)
if filename != "" {
b, _ := os.ReadFile(filename)
return b
}
return nil
}
// NewCredentialsFromJSON creates a [cloud.google.com/go/auth.Credentials] that
// returns ID tokens from the provided JSON bytes. The credType argument
// specifies the expected credential type. If the JSON does not match the
// expected type, an error is returned.
//
// This method is safe to use with untrusted credential configurations if the
// expected credType is NOT [credentials.ExternalAccount] or
// [credentials.ImpersonatedServiceAccount].
//
// **IMPORTANT**: If you use [credentials.ExternalAccount] or
// [credentials.ImpersonatedServiceAccount], you must validate the credential
// configuration before providing it to this method. Providing an unvalidated
// credential configuration to Google APIs can compromise the security of your
// systems and data. For more information, refer to [Validate credential
// configurations from external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
func NewCredentialsFromJSON(credType credentials.CredType, b []byte, opts *Options) (*auth.Credentials, error) {
if err := opts.validate(); err != nil {
return nil, err
}
// Use credentials.NewCredentialsFromJSON to validate type and create base credentials.
creds, err := credentials.NewCredentialsFromJSON(credType, b, &credentials.DetectOptions{
Client: opts.Client,
Logger: opts.Logger,
UniverseDomain: opts.UniverseDomain,
})
if err != nil {
return nil, err
}
return credsFromDefault(creds, opts)
}
// NewCredentialsFromFile creates a [cloud.google.com/go/auth.Credentials] that
// returns ID tokens from the provided file. The credType argument specifies the
// expected credential type. If the file content does not match the expected
// type, an error is returned.
//
// This method is safe to use with untrusted credential configurations if the
// expected credType is NOT [credentials.ExternalAccount] or
// [credentials.ImpersonatedServiceAccount].
//
// **IMPORTANT**: If you use [credentials.ExternalAccount] or
// [credentials.ImpersonatedServiceAccount], you must validate the credential
// configuration before providing it to this method. Providing an unvalidated
// credential configuration to Google APIs can compromise the security of your
// systems and data. For more information, refer to [Validate credential
// configurations from external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
func NewCredentialsFromFile(credType credentials.CredType, filename string, opts *Options) (*auth.Credentials, error) {
b, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
return NewCredentialsFromJSON(credType, b, opts)
}
-314
View File
@@ -1,314 +0,0 @@
// Copyright 2023 Google LLC
//
// 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 idtoken
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"log/slog"
"math/big"
"net/http"
"strings"
"time"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/jwt"
"github.com/googleapis/gax-go/v2/internallog"
)
const (
es256KeySize int = 32
// googleIAPCertsURL is used for ES256 Certs.
googleIAPCertsURL string = "https://www.gstatic.com/iap/verify/public_key-jwk"
// googleSACertsURL is used for RS256 Certs.
googleSACertsURL string = "https://www.googleapis.com/oauth2/v3/certs"
)
var (
defaultValidator = &Validator{client: newCachingClient(internal.DefaultClient(), internallog.New(nil))}
// now aliases time.Now for testing.
now = time.Now
)
// certResponse represents a list jwks. It is the format returned from known
// Google cert endpoints.
type certResponse struct {
Keys []jwk `json:"keys"`
}
// jwk is a simplified representation of a standard jwk. It only includes the
// fields used by Google's cert endpoints.
type jwk struct {
Alg string `json:"alg"`
Crv string `json:"crv"`
Kid string `json:"kid"`
Kty string `json:"kty"`
Use string `json:"use"`
E string `json:"e"`
N string `json:"n"`
X string `json:"x"`
Y string `json:"y"`
}
// Validator provides a way to validate Google ID Tokens
type Validator struct {
client *cachingClient
rs256URL string
es256URL string
}
// Payload represents a decoded payload of an ID token.
type Payload struct {
Issuer string `json:"iss"`
Audience string `json:"aud"`
Expires int64 `json:"exp"`
IssuedAt int64 `json:"iat"`
Subject string `json:"sub,omitempty"`
Claims map[string]interface{} `json:"-"`
}
// ValidatorOptions provides a way to configure a [Validator].
type ValidatorOptions struct {
// Client used to make requests to the certs URL. Optional.
Client *http.Client
// Custom certs URL for RS256 JWK to be used. If not provided, the default
// Google oauth2 endpoint will be used. Optional.
RS256CertsURL string
// Custom certs URL for ES256 JWK to be used. If not provided, the default
// Google IAP endpoint will be used. Optional.
ES256CertsURL string
// Logger is used for debug logging. If provided, logging will be enabled
// at the loggers configured level. By default logging is disabled unless
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
// Logger will be used. Optional.
Logger *slog.Logger
}
// NewValidator creates a Validator that uses the options provided to configure
// a the internal http.Client that will be used to make requests to fetch JWKs.
func NewValidator(opts *ValidatorOptions) (*Validator, error) {
if opts == nil {
opts = &ValidatorOptions{}
}
client := opts.Client
if client == nil {
client = internal.DefaultClient()
}
rs256URL := opts.RS256CertsURL
es256URL := opts.ES256CertsURL
logger := internallog.New(opts.Logger)
return &Validator{client: newCachingClient(client, logger), rs256URL: rs256URL, es256URL: es256URL}, nil
}
// Validate is used to validate the provided idToken with a known Google cert
// URL. If audience is not empty the audience claim of the Token is validated.
// Upon successful validation a parsed token Payload is returned allowing the
// caller to validate any additional claims.
func (v *Validator) Validate(ctx context.Context, idToken string, audience string) (*Payload, error) {
return v.validate(ctx, idToken, audience)
}
// Validate is used to validate the provided idToken with a known Google cert
// URL. If audience is not empty the audience claim of the Token is validated.
// Upon successful validation a parsed token Payload is returned allowing the
// caller to validate any additional claims.
func Validate(ctx context.Context, idToken string, audience string) (*Payload, error) {
return defaultValidator.validate(ctx, idToken, audience)
}
// ParsePayload parses the given token and returns its payload.
//
// Warning: This function does not validate the token prior to parsing it.
//
// ParsePayload is primarily meant to be used to inspect a token's payload. This is
// useful when validation fails and the payload needs to be inspected.
//
// Note: A successful Validate() invocation with the same token will return an
// identical payload.
func ParsePayload(idToken string) (*Payload, error) {
_, payload, _, err := parseToken(idToken)
if err != nil {
return nil, err
}
return payload, nil
}
func (v *Validator) validate(ctx context.Context, idToken string, audience string) (*Payload, error) {
header, payload, sig, err := parseToken(idToken)
if err != nil {
return nil, err
}
if audience != "" && payload.Audience != audience {
return nil, fmt.Errorf("idtoken: audience provided does not match aud claim in the JWT")
}
if now().Unix() > payload.Expires {
return nil, fmt.Errorf("idtoken: token expired: now=%v, expires=%v", now().Unix(), payload.Expires)
}
hashedContent := hashHeaderPayload(idToken)
switch header.Algorithm {
case jwt.HeaderAlgRSA256:
if err := v.validateRS256(ctx, header.KeyID, hashedContent, sig); err != nil {
return nil, err
}
case jwt.HeaderAlgES256:
if err := v.validateES256(ctx, header.KeyID, hashedContent, sig); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("idtoken: expected JWT signed with RS256 or ES256 but found %q", header.Algorithm)
}
return payload, nil
}
func (v *Validator) validateRS256(ctx context.Context, keyID string, hashedContent []byte, sig []byte) error {
certResp, err := v.client.getCert(ctx, v.rs256CertsURL())
if err != nil {
return err
}
j, err := findMatchingKey(certResp, keyID)
if err != nil {
return err
}
dn, err := decode(j.N)
if err != nil {
return err
}
de, err := decode(j.E)
if err != nil {
return err
}
pk := &rsa.PublicKey{
N: new(big.Int).SetBytes(dn),
E: int(new(big.Int).SetBytes(de).Int64()),
}
return rsa.VerifyPKCS1v15(pk, crypto.SHA256, hashedContent, sig)
}
func (v *Validator) rs256CertsURL() string {
if v.rs256URL == "" {
return googleSACertsURL
}
return v.rs256URL
}
func (v *Validator) validateES256(ctx context.Context, keyID string, hashedContent []byte, sig []byte) error {
certResp, err := v.client.getCert(ctx, v.es256CertsURL())
if err != nil {
return err
}
j, err := findMatchingKey(certResp, keyID)
if err != nil {
return err
}
dx, err := decode(j.X)
if err != nil {
return err
}
dy, err := decode(j.Y)
if err != nil {
return err
}
pk := &ecdsa.PublicKey{
Curve: elliptic.P256(),
X: new(big.Int).SetBytes(dx),
Y: new(big.Int).SetBytes(dy),
}
r := big.NewInt(0).SetBytes(sig[:es256KeySize])
s := big.NewInt(0).SetBytes(sig[es256KeySize:])
if valid := ecdsa.Verify(pk, hashedContent, r, s); !valid {
return fmt.Errorf("idtoken: ES256 signature not valid")
}
return nil
}
func (v *Validator) es256CertsURL() string {
if v.es256URL == "" {
return googleIAPCertsURL
}
return v.es256URL
}
func findMatchingKey(response *certResponse, keyID string) (*jwk, error) {
if response == nil {
return nil, fmt.Errorf("idtoken: cert response is nil")
}
for _, v := range response.Keys {
if v.Kid == keyID {
return &v, nil
}
}
return nil, fmt.Errorf("idtoken: could not find matching cert keyId for the token provided")
}
func parseToken(idToken string) (*jwt.Header, *Payload, []byte, error) {
segments := strings.Split(idToken, ".")
if len(segments) != 3 {
return nil, nil, nil, fmt.Errorf("idtoken: invalid token, token must have three segments; found %d", len(segments))
}
// Header
dh, err := decode(segments[0])
if err != nil {
return nil, nil, nil, fmt.Errorf("idtoken: unable to decode JWT header: %v", err)
}
var header *jwt.Header
err = json.Unmarshal(dh, &header)
if err != nil {
return nil, nil, nil, fmt.Errorf("idtoken: unable to unmarshal JWT header: %v", err)
}
// Payload
dp, err := decode(segments[1])
if err != nil {
return nil, nil, nil, fmt.Errorf("idtoken: unable to decode JWT claims: %v", err)
}
var payload *Payload
if err := json.Unmarshal(dp, &payload); err != nil {
return nil, nil, nil, fmt.Errorf("idtoken: unable to unmarshal JWT payload: %v", err)
}
if err := json.Unmarshal(dp, &payload.Claims); err != nil {
return nil, nil, nil, fmt.Errorf("idtoken: unable to unmarshal JWT payload claims: %v", err)
}
// Signature
signature, err := decode(segments[2])
if err != nil {
return nil, nil, nil, fmt.Errorf("idtoken: unable to decode JWT signature: %v", err)
}
return header, payload, signature, nil
}
// hashHeaderPayload gets the SHA256 checksum for verification of the JWT.
func hashHeaderPayload(idtoken string) []byte {
// remove the sig from the token
content := idtoken[:strings.LastIndex(idtoken, ".")]
hashed := sha256.Sum256([]byte(content))
return hashed[:]
}
func decode(s string) ([]byte, error) {
return base64.RawURLEncoding.DecodeString(s)
}
-45
View File
@@ -1,45 +0,0 @@
// Copyright 2023 Google LLC
//
// 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 impersonate is used to impersonate Google Credentials. If you need
// to impersonate some credentials to use with a client library see
// [NewCredentials]. If instead you would like to create an Open
// Connect ID token using impersonation see [NewIDTokenCredentials].
//
// # Required IAM roles
//
// In order to impersonate a service account the base service account must have
// the Service Account Token Creator role, roles/iam.serviceAccountTokenCreator,
// on the service account being impersonated. See
// https://cloud.google.com/iam/docs/understanding-service-accounts.
//
// Optionally, delegates can be used during impersonation if the base service
// account lacks the token creator role on the target. When using delegates,
// each service account must be granted roles/iam.serviceAccountTokenCreator
// on the next service account in the delgation chain.
//
// For example, if a base service account of SA1 is trying to impersonate target
// service account SA2 while using delegate service accounts DSA1 and DSA2,
// the following must be true:
//
// 1. Base service account SA1 has roles/iam.serviceAccountTokenCreator on
// DSA1.
// 2. DSA1 has roles/iam.serviceAccountTokenCreator on DSA2.
// 3. DSA2 has roles/iam.serviceAccountTokenCreator on target SA2.
//
// If the base credential is an authorized user and not a service account, or if
// the option WithQuotaProject is set, the target service account must have a
// role that grants the serviceusage.services.use permission such as
// roles/serviceusage.serviceUsageConsumer.
package impersonate
-141
View File
@@ -1,141 +0,0 @@
// Copyright 2023 Google LLC
//
// 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 impersonate
import (
"errors"
"log/slog"
"net/http"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials"
"cloud.google.com/go/auth/credentials/internal/impersonate"
"cloud.google.com/go/auth/httptransport"
"cloud.google.com/go/auth/internal"
"github.com/googleapis/gax-go/v2/internallog"
)
// IDTokenOptions for generating an impersonated ID token.
type IDTokenOptions struct {
// Audience is the `aud` field for the token, such as an API endpoint the
// token will grant access to. Required.
Audience string
// TargetPrincipal is the email address of the service account to
// impersonate. Required.
TargetPrincipal string
// IncludeEmail includes the target service account's email in the token.
// The resulting token will include both an `email` and `email_verified`
// claim. Optional.
IncludeEmail bool
// Delegates are the ordered service account email addresses in a delegation
// chain. Each service account must be granted
// roles/iam.serviceAccountTokenCreator on the next service account in the
// chain. Optional.
Delegates []string
// Credentials used in generating the impersonated ID token. If empty, an
// attempt will be made to detect credentials from the environment (see
// [cloud.google.com/go/auth/credentials.DetectDefault]). Optional.
Credentials *auth.Credentials
// Client configures the underlying client used to make network requests
// when fetching tokens. If provided this should be a fully-authenticated
// client. Optional.
Client *http.Client
// UniverseDomain is the default service domain for a given Cloud universe.
// The default value is "googleapis.com". This is the universe domain
// configured for the client, which will be compared to the universe domain
// that is separately configured for the credentials. Optional.
UniverseDomain string
// Logger is used for debug logging. If provided, logging will be enabled
// at the loggers configured level. By default logging is disabled unless
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
// logger will be used. Optional.
Logger *slog.Logger
}
func (o *IDTokenOptions) validate() error {
if o == nil {
return errors.New("impersonate: options must be provided")
}
if o.Audience == "" {
return errors.New("impersonate: audience must be provided")
}
if o.TargetPrincipal == "" {
return errors.New("impersonate: target service account must be provided")
}
return nil
}
var (
defaultScope = "https://www.googleapis.com/auth/cloud-platform"
)
// NewIDTokenCredentials creates an impersonated
// [cloud.google.com/go/auth/Credentials] that returns ID tokens configured
// with the provided config and using credentials loaded from Application
// Default Credentials as the base credentials if not provided with the opts.
// The tokens produced are valid for one hour and are automatically refreshed.
func NewIDTokenCredentials(opts *IDTokenOptions) (*auth.Credentials, error) {
if err := opts.validate(); err != nil {
return nil, err
}
client := opts.Client
creds := opts.Credentials
logger := internallog.New(opts.Logger)
if client == nil {
var err error
if creds == nil {
creds, err = credentials.DetectDefault(&credentials.DetectOptions{
Scopes: []string{defaultScope},
UseSelfSignedJWT: true,
Logger: logger,
})
if err != nil {
return nil, err
}
}
client, err = httptransport.NewClient(&httptransport.Options{
Credentials: creds,
UniverseDomain: opts.UniverseDomain,
Logger: logger,
})
if err != nil {
return nil, err
}
}
universeDomainProvider := resolveUniverseDomainProvider(creds)
var delegates []string
for _, v := range opts.Delegates {
delegates = append(delegates, internal.FormatIAMServiceAccountResource(v))
}
iamOpts := impersonate.IDTokenIAMOptions{
Client: client,
Logger: logger,
// Pass the credentials universe domain provider to configure the endpoint.
UniverseDomain: universeDomainProvider,
ServiceAccountEmail: opts.TargetPrincipal,
GenerateIDTokenRequest: impersonate.GenerateIDTokenRequest{
Audience: opts.Audience,
IncludeEmail: opts.IncludeEmail,
Delegates: delegates,
},
}
return auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: auth.NewCachedTokenProvider(iamOpts, nil),
UniverseDomainProvider: universeDomainProvider,
}), nil
}
-284
View File
@@ -1,284 +0,0 @@
// Copyright 2023 Google LLC
//
// 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 impersonate
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"strings"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials"
"cloud.google.com/go/auth/httptransport"
"cloud.google.com/go/auth/internal"
"github.com/googleapis/gax-go/v2/internallog"
)
var (
universeDomainPlaceholder = "UNIVERSE_DOMAIN"
iamCredentialsUniverseDomainEndpoint = "https://iamcredentials.UNIVERSE_DOMAIN"
oauth2Endpoint = "https://oauth2.googleapis.com"
errMissingTargetPrincipal = errors.New("impersonate: target service account must be provided")
errMissingScopes = errors.New("impersonate: scopes must be provided")
errLifetimeOverMax = errors.New("impersonate: max lifetime is 12 hours")
errUniverseNotSupportedDomainWideDelegation = errors.New("impersonate: service account user is configured for the credential. " +
"Domain-wide delegation is not supported in universes other than googleapis.com")
)
// TODO(codyoss): plumb through base for this and idtoken
// NewCredentials returns an impersonated
// [cloud.google.com/go/auth/NewCredentials] configured with the provided options
// and using credentials loaded from Application Default Credentials as the base
// credentials if not provided with the opts.
func NewCredentials(opts *CredentialsOptions) (*auth.Credentials, error) {
if err := opts.validate(); err != nil {
return nil, err
}
var isStaticToken bool
// Default to the longest acceptable value of one hour as the token will
// be refreshed automatically if not set.
lifetime := 1 * time.Hour
if opts.Lifetime != 0 {
lifetime = opts.Lifetime
// Don't auto-refresh token if a lifetime is configured.
isStaticToken = true
}
client := opts.Client
creds := opts.Credentials
logger := internallog.New(opts.Logger)
if client == nil {
var err error
if creds == nil {
creds, err = credentials.DetectDefault(&credentials.DetectOptions{
Scopes: []string{defaultScope},
UseSelfSignedJWT: true,
Logger: logger,
})
if err != nil {
return nil, err
}
}
client, err = httptransport.NewClient(transportOpts(opts, creds, logger))
if err != nil {
return nil, err
}
}
universeDomainProvider := resolveUniverseDomainProvider(creds)
// If a subject is specified a domain-wide delegation auth-flow is initiated
// to impersonate as the provided subject (user).
if opts.Subject != "" {
tp, err := user(opts, client, lifetime, isStaticToken, universeDomainProvider)
if err != nil {
return nil, err
}
return auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: tp,
UniverseDomainProvider: universeDomainProvider,
}), nil
}
its := impersonatedTokenProvider{
client: client,
targetPrincipal: opts.TargetPrincipal,
lifetime: fmt.Sprintf("%.fs", lifetime.Seconds()),
universeDomainProvider: universeDomainProvider,
logger: logger,
}
for _, v := range opts.Delegates {
its.delegates = append(its.delegates, internal.FormatIAMServiceAccountResource(v))
}
its.scopes = make([]string, len(opts.Scopes))
copy(its.scopes, opts.Scopes)
var tpo *auth.CachedTokenProviderOptions
if isStaticToken {
tpo = &auth.CachedTokenProviderOptions{
DisableAutoRefresh: true,
}
}
return auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: auth.NewCachedTokenProvider(its, tpo),
UniverseDomainProvider: universeDomainProvider,
}), nil
}
// transportOpts returns options for httptransport.NewClient. If opts.UniverseDomain
// is provided, it will be used in the transport for a validation ensuring that it
// matches the universe domain in the base credentials. If opts.UniverseDomain
// is not provided, this validation will be skipped.
func transportOpts(opts *CredentialsOptions, creds *auth.Credentials, logger *slog.Logger) *httptransport.Options {
tOpts := &httptransport.Options{
Credentials: creds,
Logger: logger,
}
if opts.UniverseDomain == "" {
tOpts.InternalOptions = &httptransport.InternalOptions{
SkipUniverseDomainValidation: true,
}
} else {
tOpts.UniverseDomain = opts.UniverseDomain
}
return tOpts
}
// resolveUniverseDomainProvider returns the default service domain for a given
// Cloud universe. This is the universe domain configured for the credentials,
// which will be used in endpoint(s), and compared to the universe domain that
// is separately configured for the client.
func resolveUniverseDomainProvider(creds *auth.Credentials) auth.CredentialsPropertyProvider {
if creds != nil {
return auth.CredentialsPropertyFunc(creds.UniverseDomain)
}
return internal.StaticCredentialsProperty(internal.DefaultUniverseDomain)
}
// CredentialsOptions for generating an impersonated credential token.
type CredentialsOptions struct {
// TargetPrincipal is the email address of the service account to
// impersonate. Required.
TargetPrincipal string
// Scopes that the impersonated credential should have. Required.
Scopes []string
// Delegates are the service account email addresses in a delegation chain.
// Each service account must be granted roles/iam.serviceAccountTokenCreator
// on the next service account in the chain. Optional.
Delegates []string
// Lifetime is the amount of time until the impersonated token expires. If
// unset the token's lifetime will be one hour and be automatically
// refreshed. If set the token may have a max lifetime of one hour and will
// not be refreshed. Service accounts that have been added to an org policy
// with constraints/iam.allowServiceAccountCredentialLifetimeExtension may
// request a token lifetime of up to 12 hours. Optional.
Lifetime time.Duration
// Subject is the sub field of a JWT. This field should only be set if you
// wish to impersonate as a user. This feature is useful when using domain
// wide delegation. Optional.
Subject string
// Credentials used in generating the impersonated token. If empty, an
// attempt will be made to detect credentials from the environment (see
// [cloud.google.com/go/auth/credentials.DetectDefault]). Optional.
Credentials *auth.Credentials
// Client configures the underlying client used to make network requests
// when fetching tokens. If provided this should be a fully-authenticated
// client. Optional.
Client *http.Client
// UniverseDomain is the default service domain for a given Cloud universe.
// This field has no default value, and only if provided will it be used to
// verify the universe domain from the credentials. Optional.
UniverseDomain string
// Logger is used for debug logging. If provided, logging will be enabled
// at the loggers configured level. By default logging is disabled unless
// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
// logger will be used. Optional.
Logger *slog.Logger
}
func (o *CredentialsOptions) validate() error {
if o == nil {
return errors.New("impersonate: options must be provided")
}
if o.TargetPrincipal == "" {
return errMissingTargetPrincipal
}
if len(o.Scopes) == 0 {
return errMissingScopes
}
if o.Lifetime.Hours() > 12 {
return errLifetimeOverMax
}
return nil
}
type generateAccessTokenRequest struct {
Delegates []string `json:"delegates,omitempty"`
Lifetime string `json:"lifetime,omitempty"`
Scope []string `json:"scope,omitempty"`
}
type generateAccessTokenResponse struct {
AccessToken string `json:"accessToken"`
ExpireTime string `json:"expireTime"`
}
type impersonatedTokenProvider struct {
client *http.Client
// universeDomain is used for endpoint construction.
universeDomainProvider auth.CredentialsPropertyProvider
logger *slog.Logger
targetPrincipal string
lifetime string
scopes []string
delegates []string
}
// Token returns an impersonated Token.
func (i impersonatedTokenProvider) Token(ctx context.Context) (*auth.Token, error) {
reqBody := generateAccessTokenRequest{
Delegates: i.delegates,
Lifetime: i.lifetime,
Scope: i.scopes,
}
b, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("impersonate: unable to marshal request: %w", err)
}
universeDomain, err := i.universeDomainProvider.GetProperty(ctx)
if err != nil {
return nil, err
}
endpoint := strings.Replace(iamCredentialsUniverseDomainEndpoint, universeDomainPlaceholder, universeDomain, 1)
url := fmt.Sprintf("%s/v1/%s:generateAccessToken", endpoint, internal.FormatIAMServiceAccountResource(i.targetPrincipal))
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(b))
if err != nil {
return nil, fmt.Errorf("impersonate: unable to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
i.logger.DebugContext(ctx, "impersonated token request", "request", internallog.HTTPRequest(req, b))
resp, body, err := internal.DoRequest(i.client, req)
if err != nil {
return nil, fmt.Errorf("impersonate: unable to generate access token: %w", err)
}
i.logger.DebugContext(ctx, "impersonated token response", "response", internallog.HTTPResponse(resp, body))
if c := resp.StatusCode; c < 200 || c > 299 {
return nil, fmt.Errorf("impersonate: status code %d: %s", c, body)
}
var accessTokenResp generateAccessTokenResponse
if err := json.Unmarshal(body, &accessTokenResp); err != nil {
return nil, fmt.Errorf("impersonate: unable to parse response: %w", err)
}
expiry, err := time.Parse(time.RFC3339, accessTokenResp.ExpireTime)
if err != nil {
return nil, fmt.Errorf("impersonate: unable to parse expiry: %w", err)
}
return &auth.Token{
Value: accessTokenResp.AccessToken,
Expiry: expiry,
}, nil
}
-203
View File
@@ -1,203 +0,0 @@
// Copyright 2023 Google LLC
//
// 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 impersonate
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/internal"
"github.com/googleapis/gax-go/v2/internallog"
)
var (
iamCredentialsEndpoint = "https://iamcredentials.googleapis.com"
)
// user provides an auth flow for domain-wide delegation, setting
// CredentialsConfig.Subject to be the impersonated user.
func user(opts *CredentialsOptions, client *http.Client, lifetime time.Duration, isStaticToken bool, universeDomainProvider auth.CredentialsPropertyProvider) (auth.TokenProvider, error) {
if opts.Subject == "" {
return nil, errors.New("CredentialsConfig.Subject must not be empty")
}
u := userTokenProvider{
client: client,
targetPrincipal: opts.TargetPrincipal,
subject: opts.Subject,
lifetime: lifetime,
universeDomainProvider: universeDomainProvider,
logger: internallog.New(opts.Logger),
}
u.delegates = make([]string, len(opts.Delegates))
for i, v := range opts.Delegates {
u.delegates[i] = internal.FormatIAMServiceAccountResource(v)
}
u.scopes = make([]string, len(opts.Scopes))
copy(u.scopes, opts.Scopes)
var tpo *auth.CachedTokenProviderOptions
if isStaticToken {
tpo = &auth.CachedTokenProviderOptions{
DisableAutoRefresh: true,
}
}
return auth.NewCachedTokenProvider(u, tpo), nil
}
type claimSet struct {
Iss string `json:"iss"`
Scope string `json:"scope,omitempty"`
Sub string `json:"sub,omitempty"`
Aud string `json:"aud"`
Iat int64 `json:"iat"`
Exp int64 `json:"exp"`
}
type signJWTRequest struct {
Payload string `json:"payload"`
Delegates []string `json:"delegates,omitempty"`
}
type signJWTResponse struct {
// KeyID is the key used to sign the JWT.
KeyID string `json:"keyId"`
// SignedJwt contains the automatically generated header; the
// client-supplied payload; and the signature, which is generated using
// the key referenced by the `kid` field in the header.
SignedJWT string `json:"signedJwt"`
}
type exchangeTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
}
type userTokenProvider struct {
client *http.Client
logger *slog.Logger
targetPrincipal string
subject string
scopes []string
lifetime time.Duration
delegates []string
universeDomainProvider auth.CredentialsPropertyProvider
}
func (u userTokenProvider) Token(ctx context.Context) (*auth.Token, error) {
// Because a subject is specified a domain-wide delegation auth-flow is initiated
// to impersonate as the provided subject (user).
// Return error if users try to use domain-wide delegation in a non-GDU universe.
ud, err := u.universeDomainProvider.GetProperty(ctx)
if err != nil {
return nil, err
}
if ud != internal.DefaultUniverseDomain {
return nil, errUniverseNotSupportedDomainWideDelegation
}
signedJWT, err := u.signJWT(ctx)
if err != nil {
return nil, err
}
return u.exchangeToken(ctx, signedJWT)
}
func (u userTokenProvider) signJWT(ctx context.Context) (string, error) {
now := time.Now()
exp := now.Add(u.lifetime)
claims := claimSet{
Iss: u.targetPrincipal,
Scope: strings.Join(u.scopes, " "),
Sub: u.subject,
Aud: fmt.Sprintf("%s/token", oauth2Endpoint),
Iat: now.Unix(),
Exp: exp.Unix(),
}
payloadBytes, err := json.Marshal(claims)
if err != nil {
return "", fmt.Errorf("impersonate: unable to marshal claims: %w", err)
}
signJWTReq := signJWTRequest{
Payload: string(payloadBytes),
Delegates: u.delegates,
}
bodyBytes, err := json.Marshal(signJWTReq)
if err != nil {
return "", fmt.Errorf("impersonate: unable to marshal request: %w", err)
}
reqURL := fmt.Sprintf("%s/v1/%s:signJwt", iamCredentialsEndpoint, internal.FormatIAMServiceAccountResource(u.targetPrincipal))
req, err := http.NewRequestWithContext(ctx, "POST", reqURL, bytes.NewReader(bodyBytes))
if err != nil {
return "", fmt.Errorf("impersonate: unable to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
u.logger.DebugContext(ctx, "impersonated user sign JWT request", "request", internallog.HTTPRequest(req, bodyBytes))
resp, body, err := internal.DoRequest(u.client, req)
if err != nil {
return "", fmt.Errorf("impersonate: unable to sign JWT: %w", err)
}
u.logger.DebugContext(ctx, "impersonated user sign JWT response", "response", internallog.HTTPResponse(resp, body))
if c := resp.StatusCode; c < 200 || c > 299 {
return "", fmt.Errorf("impersonate: status code %d: %s", c, body)
}
var signJWTResp signJWTResponse
if err := json.Unmarshal(body, &signJWTResp); err != nil {
return "", fmt.Errorf("impersonate: unable to parse response: %w", err)
}
return signJWTResp.SignedJWT, nil
}
func (u userTokenProvider) exchangeToken(ctx context.Context, signedJWT string) (*auth.Token, error) {
v := url.Values{}
v.Set("grant_type", "assertion")
v.Set("assertion_type", "http://oauth.net/grant_type/jwt/1.0/bearer")
v.Set("assertion", signedJWT)
req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/token", oauth2Endpoint), strings.NewReader(v.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
u.logger.DebugContext(ctx, "impersonated user token exchange request", "request", internallog.HTTPRequest(req, []byte(v.Encode())))
resp, body, err := internal.DoRequest(u.client, req)
if err != nil {
return nil, fmt.Errorf("impersonate: unable to exchange token: %w", err)
}
u.logger.DebugContext(ctx, "impersonated user token exchange response", "response", internallog.HTTPResponse(resp, body))
if c := resp.StatusCode; c < 200 || c > 299 {
return nil, fmt.Errorf("impersonate: status code %d: %s", c, body)
}
var tokenResp exchangeTokenResponse
if err := json.Unmarshal(body, &tokenResp); err != nil {
return nil, fmt.Errorf("impersonate: unable to parse response: %w", err)
}
return &auth.Token{
Value: tokenResp.AccessToken,
Type: tokenResp.TokenType,
Expiry: time.Now().Add(time.Second * time.Duration(tokenResp.ExpiresIn)),
}, nil
}
+23
View File
@@ -23,6 +23,29 @@ wanted"](https://github.com/grpc/grpc-go/issues?q=sort%3Aupdated-desc%20is%3Aiss
are especially nice for first-time contributors, as they should be well-defined
problems that already have agreed-upon solutions.
## Generative AI Policy
AI tools have the ability to produce more code than is possible for the gRPC
team to read, understand, review, and accept into the repository. For this
reason, we request that all contributions adhere to the following rules:
1. **No AI-Generated Interactions:** All communication in the repo must be
authored by a human. _Exception: AIs may be used for directed writing
assistance or translation._ Absolutely no automated agents are allowed to
directly publish to GitHub.
2. **Author Ownership and Accountability:** Code contributions are expected to
be fully owned and understood by the human contributor. If the code was
produced by generative AI, the author is expected to have reviewed and
understood it in its entirety before submitting it for review. This includes
all content: production code, tests, examples, tools, etc.
In addition to the above requirements, any AI-assisted contributions must also
comply with the [Linux Foundation Generative AI
Policy](https://www.linuxfoundation.org/legal/generative-ai). This includes
confirming that all contributions are legally allowed to be contributed to the
gRPC project under the applicable license terms.
## Code Style
We follow [Google's published Go style
@@ -19,7 +19,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc-gen-go v1.36.12
// protoc v5.27.1
// source: grpc/lb/v1/load_balancer.proto
+1 -1
View File
@@ -18,7 +18,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc-gen-go v1.36.12
// protoc v5.27.1
// source: grpc/binlog/v1/binarylog.proto
+2 -25
View File
@@ -20,10 +20,6 @@ package grpc
import (
"context"
"io"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// Invoke sends the RPC request on the wire and returns after response is
@@ -71,27 +67,8 @@ func invoke(ctx context.Context, method string, req, reply any, cc *ClientConn,
if err != nil {
return err
}
// In case of nil or io.EOF error, call RecvMsg.
if err := cs.SendMsg(req); err != nil && err != io.EOF {
if err := cs.SendMsg(req); err != nil {
return err
}
// CloseSend is needed because in some scenarios (e.g., xDS), the same
// interceptors are used to process both unary and streaming RPCs. Calling
// CloseSend signals to those interceptors that no more messages are on the
// way.
if err := cs.CloseSend(); err != nil && err != io.EOF {
return err
}
if err := cs.RecvMsg(reply); err != nil {
return err
}
// Call RecvMsg again to get the trailers.
err = cs.RecvMsg(reply)
if err == io.EOF {
return nil
}
if err == nil {
return status.Error(codes.Internal, "cardinality violation: expected <EOF> for non server-streaming RPCs, but received another message")
}
return err
return cs.RecvMsg(reply)
}
+6 -1
View File
@@ -553,7 +553,6 @@ func chainStreamClientInterceptors(cc *ClientConn) {
if cc.dopts.streamInt != nil {
interceptors = append([]StreamClientInterceptor{cc.dopts.streamInt}, interceptors...)
}
interceptors = append(interceptors, defaultStreamInterceptor)
var chainedInt StreamClientInterceptor
if len(interceptors) == 0 {
chainedInt = nil
@@ -1212,6 +1211,12 @@ func (cc *ClientConn) Close() error {
cc.mu.Unlock()
cc.resolverWrapper.close()
// Swap in a default ConfigSelector so the resolver-provided one (which may
// transitively retain resolver-owned state such as parsed service configs
// and cluster maps) is released once in-flight RPCs finish selecting on
// it. Done after the resolver has closed so no further UpdateState calls
// race with this swap.
cc.safeConfigSelector.UpdateConfigSelector(&defaultConfigSelector{nil})
// The order of closing matters here since the balancer wrapper assumes the
// picker is closed before it is closed.
cc.pickerWrapper.close()
+5 -4
View File
@@ -266,10 +266,11 @@ func (g *altsTC) Clone() credentials.TransportCredentials {
copy(accounts, g.accounts)
}
return &altsTC{
info: &info,
side: g.side,
hsAddress: g.hsAddress,
accounts: accounts,
info: &info,
side: g.side,
hsAddress: g.hsAddress,
accounts: accounts,
boundAccessToken: g.boundAccessToken,
}
}
@@ -17,7 +17,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc-gen-go v1.36.12
// protoc v5.27.1
// source: grpc/gcp/altscontext.proto
@@ -17,7 +17,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc-gen-go v1.36.12
// protoc v5.27.1
// source: grpc/gcp/handshaker.proto
@@ -17,7 +17,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc-gen-go v1.36.12
// protoc v5.27.1
// source: grpc/gcp/transport_security_common.proto
@@ -20,19 +20,14 @@ package google
import (
"context"
"errors"
"fmt"
"sync"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials/idtoken"
"cloud.google.com/go/compute/metadata"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/google/internal"
"google.golang.org/grpc/internal/backoff"
"google.golang.org/grpc/internal/transport"
"google.golang.org/grpc/status"
)
@@ -53,14 +48,14 @@ const (
type gcpServiceAccountIdentityCallCreds struct {
// The following fields are initialized at creation time and are read-only
// after that.
ctx context.Context
audience string
creds *auth.Credentials
backoff backoff.Strategy
ctx context.Context
audience string
backoff backoff.Strategy
fetchIDToken func(context.Context, string) (string, time.Time, error)
// The following fields are protected by mu.
mu sync.Mutex
token *auth.Token
token string // cached raw JWT token value
tokenExpiry time.Time // timestamp after which the cached token is considered invalid
preemptiveTokenRefresh time.Time // timestamp after which background preemptive refresh is triggered
fetching chan struct{} // used to ensure a single in-progress background token fetch
@@ -71,8 +66,8 @@ type gcpServiceAccountIdentityCallCreds struct {
func init() {
internal.BackoffStrategy = backoff.DefaultExponential
internal.NewIDTokenCredentials = func(opts *idtoken.Options) (*auth.Credentials, error) {
return idtoken.NewCredentials(opts)
internal.IDTokenFetcher = func() func(context.Context, string) (string, time.Time, error) {
return newIDTokenFetcher().fetchIDToken
}
}
@@ -103,16 +98,11 @@ func NewServiceAccountIdentityCredentials(ctx context.Context, audience string)
return nil, fmt.Errorf("credentials: audience cannot be empty")
}
creds, err := internal.NewIDTokenCredentials(&idtoken.Options{Audience: audience})
if err != nil {
return nil, fmt.Errorf("credentials: failed to create ID token credentials: %v", err)
}
return &gcpServiceAccountIdentityCallCreds{
ctx: ctx,
audience: audience,
creds: creds,
backoff: internal.BackoffStrategy,
ctx: ctx,
audience: audience,
backoff: internal.BackoffStrategy,
fetchIDToken: internal.IDTokenFetcher(),
}, nil
}
@@ -183,13 +173,13 @@ func (c *gcpServiceAccountIdentityCallCreds) cachedRequestMetadata(attemptPreemp
//
// It must be called with mu locked.
func (c *gcpServiceAccountIdentityCallCreds) cachedRequestMetadataLocked(attemptPreemptiveRefresh bool) (map[string]string, error) {
if c.token != nil && c.isTokenValidLocked() {
if c.token != "" && c.isTokenValidLocked() {
if attemptPreemptiveRefresh && c.isTokenStaleLocked() && c.fetching == nil {
c.fetching = make(chan struct{})
go c.startFetch()
}
return map[string]string{"authorization": "Bearer " + c.token.Value}, nil
return map[string]string{"authorization": "Bearer " + c.token}, nil
}
if c.lastErr != nil && time.Now().Before(c.nextRetryTime) {
@@ -222,46 +212,27 @@ func (c *gcpServiceAccountIdentityCallCreds) isTokenValidLocked() bool {
func (c *gcpServiceAccountIdentityCallCreds) startFetch() {
ctx, cancel := context.WithTimeout(c.ctx, metadataTimeout)
defer cancel()
token, err := c.creds.TokenProvider.Token(ctx)
token, exp, err := c.fetchIDToken(ctx, c.audience)
c.mu.Lock()
defer c.mu.Unlock()
close(c.fetching)
c.fetching = nil
c.updateStateLocked(token, err)
c.updateStateLocked(token, exp, err)
}
// updateStateLocked updates the credentials local token cache and
// backoff state based on the outcome of a background fetch attempt.
//
// If the fetch succeeded, the cached token is updated, and the backoff timers
// and error are reset.
//
// If the fetch failed, backoff attempts are calculated and the error is mapped
// to a gRPC status.
// - If the HTTP request fails with a status that maps to gRPC UNAVAILABLE
// according to HTTP to gRPC status code mappings, it returns UNAVAILABLE.
// - All other HTTP error status codes map to UNAUTHENTICATED.
// - Non-HTTP request failures are mapped to UNAVAILABLE.
// and error are reset. If the fetch failed, the error is cached and the next
// retry time is calculated using the backoff strategy.
//
// It must be called with mu locked.
func (c *gcpServiceAccountIdentityCallCreds) updateStateLocked(token *auth.Token, err error) {
func (c *gcpServiceAccountIdentityCallCreds) updateStateLocked(token string, exp time.Time, err error) {
if err != nil {
var mappedErr error
var metadataErr *metadata.Error
if errors.As(err, &metadataErr) {
switch transport.HTTPStatusConvTab[metadataErr.Code] {
case codes.Unavailable:
mappedErr = status.Errorf(codes.Unavailable, "credentials: failed to fetch token from metadata server: %v", err)
default:
mappedErr = status.Errorf(codes.Unauthenticated, "credentials: failed to fetch token from metadata server: %v", err)
}
} else {
mappedErr = status.Errorf(codes.Unavailable, "credentials: failed to fetch ID token: %v", err)
}
c.lastErr = mappedErr
c.lastErr = err
backoffDelay := c.backoff.Backoff(c.retryAttempt)
c.retryAttempt++
c.nextRetryTime = time.Now().Add(backoffDelay)
@@ -273,6 +244,6 @@ func (c *gcpServiceAccountIdentityCallCreds) updateStateLocked(token *auth.Token
c.token = token
// Per gRFC A83, the cached token is considered invalid 30 seconds before its
// actual expiration time to accommodate for clock skew.
c.tokenExpiry = token.Expiry.Add(-30 * time.Second)
c.tokenExpiry = exp.Add(-30 * time.Second)
c.preemptiveTokenRefresh = c.tokenExpiry.Add(-preemptiveRefresh)
}
+178
View File
@@ -0,0 +1,178 @@
/*
*
* Copyright 2026 gRPC 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 google
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"strings"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/internal/transport"
"google.golang.org/grpc/status"
)
// The HTTP transport timeout values match newDefaultHTTPClient in the
// GCE metadata client package (cloud.google.com/go/compute/metadata):
//
// Proxy is nil to bypass environment proxies per gRFC A83, a 2s dial
// timeout fails fast if the local metadata server is unreachable, TCP
// keep-alive is set to 30s, and idle pooled connections close after 60s.
var defaultMetadataHTTPClient = &http.Client{
Transport: &http.Transport{
Proxy: nil,
DialContext: (&net.Dialer{
Timeout: 2 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
IdleConnTimeout: 60 * time.Second,
},
}
// idTokenFetcher fetches ID tokens from the GCE Metadata Server.
type idTokenFetcher struct {
client *http.Client // unproxied HTTP client for metadata server communication
metadataHost string // hostname of the GCE metadata server
}
// newIDTokenFetcher creates a new idTokenFetcher configured to send direct,
// unproxied HTTP requests to the GCE metadata server.
func newIDTokenFetcher() *idTokenFetcher {
host := os.Getenv("GCE_METADATA_HOST")
if host == "" {
host = "metadata.google.internal"
}
return &idTokenFetcher{
metadataHost: host,
client: defaultMetadataHTTPClient,
}
}
// fetchIDToken fetches an ID token for the given audience from the GCE
// Metadata Server.
//
// It takes a context controlling HTTP request cancellation and an audience
// string for the requested token. It returns the raw JWT token string,
// expiration timestamp parsed from the token's "exp" claim, and an error
// mapped to a gRPC status if the HTTP request or JWT parsing fails:
// - If the HTTP request fails with a status that maps to gRPC UNAVAILABLE
// according to HTTP to gRPC status code mappings, it returns UNAVAILABLE.
// - All other HTTP error status codes and JWT parsing failures map to
// UNAUTHENTICATED.
// - Non-HTTP request failures are mapped to UNAVAILABLE.
func (f *idTokenFetcher) fetchIDToken(ctx context.Context, audience string) (string, time.Time, error) {
reqURL := fmt.Sprintf("http://%s/computeMetadata/v1/instance/service-accounts/default/identity?audience=%s&format=full", f.metadataHost, url.QueryEscape(audience))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
if err != nil {
return "", time.Time{}, status.Errorf(codes.Unavailable, "credentials: failed to fetch ID token: %v", err)
}
req.Header.Set("Metadata-Flavor", "Google")
resp, err := f.client.Do(req)
if err != nil {
return "", time.Time{}, status.Errorf(codes.Unavailable, "credentials: failed to fetch ID token: %v", err)
}
defer resp.Body.Close()
// Limit the response body read to 1 MB to prevent unbounded memory
// allocation if the server returns an unexpectedly large response, matching
// the limit used by the golang.org/x/oauth2 package when reading token
// responses.
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return "", time.Time{}, status.Errorf(codes.Unavailable, "credentials: failed to fetch ID token: %v", err)
}
// Per gRFC A83, if the returned HTTP status maps to UNAVAILABLE in the
// standard HTTP to gRPC status code mapping, return UNAVAILABLE; otherwise,
// return UNAUTHENTICATED.
if resp.StatusCode != http.StatusOK {
switch transport.HTTPStatusConvTab[resp.StatusCode] {
case codes.Unavailable:
return "", time.Time{}, status.Errorf(codes.Unavailable, "credentials: failed to fetch ID token: HTTP status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
default:
return "", time.Time{}, status.Errorf(codes.Unauthenticated, "credentials: failed to fetch ID token: HTTP status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
}
// Handle the HTTP 200 OK case.
rawJWT := strings.TrimSpace(string(body))
expiry, err := parseJWTExpiry(rawJWT)
if err != nil {
return "", time.Time{}, status.Errorf(codes.Unauthenticated, "credentials: failed to fetch ID token: %v", err)
}
return rawJWT, expiry, nil
}
type jwtPayload struct {
// Per RFC 7519 Section 2, JWT NumericDate values (such as "exp") are JSON
// numeric values that may contain non-integer fractional seconds. Using
// float64 ensures json.Unmarshal succeeds even if fractional seconds are
// present.
Exp float64 `json:"exp"`
}
// parseJWTExpiry parses a JWT string to extract its expiration timestamp
// ("exp" claim).
//
// A compact JWT consists of three dot-separated, Base64URL-encoded parts:
// 1. Header: Contains metadata like algorithm and token type. Ignored here.
// 2. Payload: Contains token claims in JSON format. Decoded to extract the
// expiration timestamp ("exp").
// 3. Signature: Cryptographic signature. Ignored here.
//
// Per gRFC A83, if the JWT format is invalid or the "exp" claim cannot be
// extracted, an error is returned.
func parseJWTExpiry(jwtStr string) (time.Time, error) {
// A valid JWT must have 3 parts: header, payload, and signature.
parts := strings.Split(jwtStr, ".")
if len(parts) != 3 {
return time.Time{}, fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts))
}
// Per the JWT specification (RFC 7519 / RFC 7515), compact JWT segments use
// Base64URL encoding with all trailing '=' padding characters omitted.
// base64.RawURLEncoding is unpadded Base64URL encoding and decodes these
// strings directly without requiring manual padding.
data, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return time.Time{}, fmt.Errorf("failed to decode JWT payload: %v", err)
}
var payload jwtPayload
if err := json.Unmarshal(data, &payload); err != nil {
return time.Time{}, fmt.Errorf("failed to unmarshal JWT payload: %v", err)
}
if payload.Exp <= 0 {
return time.Time{}, fmt.Errorf("missing or invalid 'exp' claim in JWT payload")
}
return time.Unix(int64(payload.Exp), 0), nil
}
+8 -4
View File
@@ -20,8 +20,9 @@
package internal
import (
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials/idtoken"
"context"
"time"
"google.golang.org/grpc/internal/backoff"
)
@@ -30,6 +31,9 @@ var (
// BackoffStrategy is the backoff strategy to use when token fetch fails.
BackoffStrategy backoff.Strategy
// NewIDTokenCredentials builds idtoken credentials using specified options.
NewIDTokenCredentials func(opts *idtoken.Options) (*auth.Credentials, error)
// IDTokenFetcher returns a function used to fetch an ID token for the
// given audience. It takes a ctx and audience string as parameter and
// returns the raw JWT token string, token's expiration timestamp and
// error if encountered.
IDTokenFetcher func() func(context.Context, string) (string, time.Time, error)
)
+1 -1
View File
@@ -17,7 +17,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc-gen-go v1.36.12
// protoc v5.27.1
// source: grpc/health/v1/health.proto
+3
View File
@@ -107,6 +107,9 @@ func (s *Server) Watch(in *healthpb.HealthCheckRequest, stream healthgrpc.Health
defer func() {
s.mu.Lock()
delete(s.updates[service], stream)
if len(s.updates[service]) == 0 {
delete(s.updates, service)
}
s.mu.Unlock()
}()
s.mu.Unlock()
+1 -1
View File
@@ -395,7 +395,7 @@ func (c *Cancel) toProto() *binlogpb.GrpcLogEntry {
// omitted.
func metadataKeyOmit(key string) bool {
switch key {
case "lb-token", ":path", ":authority", "content-encoding", "content-type", "user-agent", "te":
case "lb-token", ":path", ":authority", "content-encoding", "content-type", "user-agent", "te", "authorization":
return true
case "grpc-trace-bin": // grpc-trace-bin is special because it's visible to users.
return false
+3 -3
View File
@@ -144,14 +144,14 @@ var (
// ControlBufferThrottleLimit is the maximum number of control frames that can
// be queued in the control buffer before throttling is applied. The value
// must be between 1 and 10,000, and is set to 100 by default.
// must be between 1 and 100,000, and is set to 10,000 by default.
//
// This environment variable serves as an escape hatch to increase the
// throttling limit if unforeseen issues arise, and it will be removed in a
// future release.
//
// TODO: Remove this env var once v1.83.0 is released.
ControlBufferThrottleLimit = uint64FromEnv("GRPC_GO_EXPERIMENTAL_CONTROL_BUFFER_THROTTLE_LIMIT", 100, 1, 10000)
// TODO: Remove this env var once v1.86.0 is released.
ControlBufferThrottleLimit = uint64FromEnv("GRPC_GO_EXPERIMENTAL_CONTROL_BUFFER_THROTTLE_LIMIT", 10000, 1, 100000)
// ALTSMaxFrameSize is the maximum frame size for ALTS in bytes.
// This can be overridden by setting the environment variable
+3 -2
View File
@@ -47,10 +47,9 @@ const (
var bdpPing = &ping{data: [8]byte{2, 4, 16, 16, 9, 14, 7, 7}}
type bdpEstimator struct {
mu sync.Mutex
// sentAt is the time when the ping was sent.
sentAt time.Time
mu sync.Mutex
// bdp is the current bdp estimate.
bdp uint32
// sample is the number of bytes received in one measurement cycle.
@@ -75,7 +74,9 @@ func (b *bdpEstimator) timesnap(d [8]byte) {
if bdpPing.data != d {
return
}
b.mu.Lock()
b.sentAt = time.Now()
b.mu.Unlock()
}
// add adds bytes to the current sample for calculating bdp.
+113 -47
View File
@@ -26,6 +26,7 @@ import (
"runtime"
"sync"
"sync/atomic"
"time"
"golang.org/x/net/http2"
"golang.org/x/net/http2/hpack"
@@ -97,22 +98,35 @@ func (il *itemList) isEmpty() bool {
return il.head == nil
}
// maxQueuedControlBufferItems is the maximum number of frames (other than
// HEADERS and DATA) that we will buffer before preventing new reads from
// occurring on the transport. These are control frames sent in response to
// client requests, or frames that result in work being scheduled, such as
// RST_STREAM due to bad headers or settings acks.
var maxQueuedControlBufferItems = int(envconfig.ControlBufferThrottleLimit)
// maxQueuedTransportResponseFrames is the maximum number of "transport
// response" frames that we will buffer before preventing new reads from
// occurring on the transport. See cbItem.isTransportResponseFrame for the
// items that count towards this limit.
var maxQueuedTransportResponseFrames = int(envconfig.ControlBufferThrottleLimit)
type cbItem interface {
isThrottled() bool
// isTransportResponseFrame reports whether the item counts against the
// maxQueuedTransportResponseFrames limit, when queued in the
// controlBuffer of a transport on the given side.
//
// Items that the peer can make the transport queue in unbounded
// numbers, for example by sending PING or SETTINGS frames, must be
// counted, so that the transport stops reading from a peer that sends
// such frames without reading what the transport writes. Items whose
// number grows with the local application's load (for example, one
// per stream it creates) must not be counted. Otherwise, under high
// concurrency, both peers could stop reading at the same time and
// deadlock (see https://github.com/grpc/grpc-go/issues/9330).
isTransportResponseFrame(s side) bool
}
// throttledItem represents every item in the controlBuffer to which the overall
// throttling limit applies, other than outgoing HEADERS and DATA frames.
type throttledItem struct{}
// nonTransportResponseFrame represents items in the controlBuffer that are not
// queued in response to peer control/reset frames (for example,
// locally-initiated or flow-controlled items). These items are not counted
// against the maxQueuedTransportResponseFrames limit.
type nonTransportResponseFrame struct{}
func (throttledItem) isThrottled() bool { return true }
func (nonTransportResponseFrame) isTransportResponseFrame(side) bool { return false }
// The following defines various control items which could flow through
// the control buffer of transport. They represent different aspects of
@@ -120,12 +134,13 @@ func (throttledItem) isThrottled() bool { return true }
// registerStream is used to register an incoming stream with loopy writer.
type registerStream struct {
throttledItem
nonTransportResponseFrame
streamID uint32
wq *writeQuota
}
type clientHeaders struct {
nonTransportResponseFrame
streamID uint32
hf []hpack.HeaderField
initStream func(uint32) error
@@ -134,9 +149,8 @@ type clientHeaders struct {
onOrphaned func(error)
}
func (*clientHeaders) isThrottled() bool { return false }
type serverHeaders struct {
nonTransportResponseFrame
streamID uint32
hf []hpack.HeaderField
endStream bool
@@ -144,24 +158,37 @@ type serverHeaders struct {
cleanup *cleanupStream
}
func (h *serverHeaders) isThrottled() bool { return false }
type cleanupStream struct {
throttledItem
streamID uint32
rst bool
rstCode http2.ErrCode
onWrite func()
}
// isTransportResponseFrame returns true on the server since cleanupStream
// either results in a RST_STREAM frame being sent on the wire or in
// non-trivial work being performed when under a Rapid Reset attack.
//
// It returns false on the client, where a cleanupStream is queued at most once
// per stream, and the number of streams is controlled by the client and not by
// the server. Counting them on the client makes the client stop reading under
// high concurrency, which deadlocks with a server that has also stopped
// reading (see https://github.com/grpc/grpc-go/issues/9330).
func (*cleanupStream) isTransportResponseFrame(s side) bool { return s == serverSide }
type earlyAbortStream struct {
throttledItem
streamID uint32
rst bool
hf []hpack.HeaderField // Pre-built header fields
}
// isTransportResponseFrame returns true on the server since earlyAbortStream
// results in a HEADERS frame being sent on the wire, and possibly a RST_STREAM
// frame. It is never queued on the client.
func (*earlyAbortStream) isTransportResponseFrame(s side) bool { return s == serverSide }
type dataFrame struct {
nonTransportResponseFrame
streamID uint32
endStream bool
h []byte
@@ -172,36 +199,40 @@ type dataFrame struct {
onEachWrite func()
}
func (*dataFrame) isThrottled() bool { return false }
type incomingWindowUpdate struct {
throttledItem
streamID uint32
increment uint32
}
// isTransportResponseFrame returns true to protect against a WINDOW_UPDATE
// flood attack from the peer.
func (*incomingWindowUpdate) isTransportResponseFrame(side) bool { return true }
type outgoingWindowUpdate struct {
throttledItem
nonTransportResponseFrame
streamID uint32
increment uint32
}
type incomingSettings struct {
throttledItem
ss []http2.Setting
}
// isTransportResponseFrame returns true since incomingSettings results in a
// SETTINGS ACK frame being sent on the wire.
func (*incomingSettings) isTransportResponseFrame(side) bool { return true }
type outgoingSettings struct {
throttledItem
nonTransportResponseFrame
ss []http2.Setting
}
type incomingGoAway struct {
throttledItem
nonTransportResponseFrame
}
type goAway struct {
throttledItem
nonTransportResponseFrame
code http2.ErrCode
debugData []byte
headsUp bool
@@ -209,13 +240,23 @@ type goAway struct {
}
type ping struct {
throttledItem
ack bool
data [8]byte
}
// isTransportResponseFrame returns true for all pings, and not only for ping
// acks sent in response to pings from the peer. This is because a BDP ping is
// queued in response to a DATA frame (of any size) from the peer, whenever
// there is no BDP ping outstanding. And since the BDP ping's payload is fixed,
// the peer can make it look like the BDP ping was acked at any time, by
// sending a ping ack with that payload. A peer alternating between empty DATA
// frames and such ping acks would otherwise be able to queue an unbounded
// number of BDP pings. Pings initiated locally (keepalive and BDP) are rare
// enough to not cause throttling of well-behaved peers.
func (*ping) isTransportResponseFrame(side) bool { return true }
type outFlowControlSizeRequest struct {
throttledItem
nonTransportResponseFrame
resp chan uint32
}
@@ -225,7 +266,7 @@ type outFlowControlSizeRequest struct {
// can inspect settled accounting values (such as bytesOutStanding) after loopyWriter
// has run to completion without any data races.
type outStreamRequestForTesting struct {
throttledItem
nonTransportResponseFrame
streamID uint32
resp chan *outStream
}
@@ -235,7 +276,7 @@ type outStreamRequestForTesting struct {
// (by the client or server). The transport itself will close after the reader
// encounters the EOF caused by the connection closure.
type closeConnection struct {
throttledItem
nonTransportResponseFrame
}
type outStreamState int
@@ -316,8 +357,9 @@ func (l *outStreamList) dequeue() *outStream {
// shouldn't be confused with an HTTP2 frame, although some of the control
// frames like dataFrame and headerFrame do go out on wire as HTTP2 frames.
type controlBuffer struct {
wakeupCh chan struct{} // Unblocks readers waiting for something to read.
wakeupCh chan struct{} // Unblocks readers waiting for new items.
done <-chan struct{} // Closed when the transport is done.
side side // Side of the transport owning this buffer.
// Mutex guards all the fields below, except trfChan which can be read
// atomically without holding mu.
@@ -326,26 +368,30 @@ type controlBuffer struct {
closed bool // True when the controlbuf is finished.
list *itemList // List of queued control frames.
// transportResponseFrames counts the number of queued items that represent
// the response of an action initiated by the peer. trfChan is created
// when transportResponseFrames >= maxQueuedTransportResponseFrames and is
// closed and nilled when transportResponseFrames drops below the
// threshold. Both fields are protected by mu.
// transportResponseFrames counts the number of queued items that
// represent the response of an action initiated by the peer (as
// determined by cbItem.isTransportResponseFrame for this side).
// trfChan is created when transportResponseFrames >=
// maxQueuedTransportResponseFrames and is closed and nilled when
// transportResponseFrames drops below the threshold. Both fields are
// protected by mu.
transportResponseFrames int
trfChan atomic.Pointer[chan struct{}]
}
func newControlBuffer(done <-chan struct{}) *controlBuffer {
func newControlBuffer(done <-chan struct{}, s side) *controlBuffer {
return &controlBuffer{
wakeupCh: make(chan struct{}, 1),
list: &itemList{},
done: done,
side: s,
}
}
// throttle blocks if there are too many frames in the control buf that
// represent the response of an action initiated by the peer, like
// incomingSettings cleanupStreams etc.
// throttle blocks if there are too many items in the control buffer that count
// towards the maxQueuedTransportResponseFrames limit (see
// cbItem.isTransportResponseFrame), until their number drops below the limit,
// the control buffer is finished, or the transport is done.
func (c *controlBuffer) throttle() {
if ch := c.trfChan.Load(); ch != nil {
select {
@@ -355,6 +401,25 @@ func (c *controlBuffer) throttle() {
}
}
// throttleWithTimeout is like throttle, but it stops waiting and returns an
// error if the control buffer is still throttled after the given duration.
func (c *controlBuffer) throttleWithTimeout(timeout time.Duration) error {
ch := c.trfChan.Load()
if ch == nil {
return nil
}
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-(*ch):
return nil
case <-c.done:
return nil
case <-timer.C:
return fmt.Errorf("still throttled after %v, with at least %d queued frames", timeout, maxQueuedTransportResponseFrames)
}
}
// put adds an item to the controlbuf.
func (c *controlBuffer) put(it cbItem) error {
_, err := c.executeAndPut(nil, it)
@@ -390,9 +455,9 @@ func (c *controlBuffer) executeAndPut(f func() bool, it cbItem) (bool, error) {
c.consumerWaiting = false
}
c.list.enqueue(it)
if it.isThrottled() {
if it.isTransportResponseFrame(c.side) {
c.transportResponseFrames++
if c.transportResponseFrames == maxQueuedControlBufferItems {
if c.transportResponseFrames == maxQueuedTransportResponseFrames {
// We are adding the frame that puts us over the threshold; create
// a throttling channel.
ch := make(chan struct{})
@@ -447,8 +512,8 @@ func (c *controlBuffer) getOnceLocked() (any, error) {
return nil, nil
}
h := c.list.dequeue().(cbItem)
if h.isThrottled() {
if c.transportResponseFrames == maxQueuedControlBufferItems {
if h.isTransportResponseFrame(c.side) {
if c.transportResponseFrames == maxQueuedTransportResponseFrames {
// We are removing the frame that put us over the
// threshold; close and clear the throttling channel.
ch := c.trfChan.Swap(nil)
@@ -484,9 +549,10 @@ func (c *controlBuffer) finish() {
}
}
// In case throttle() is currently in flight, it needs to be unblocked.
// Otherwise, the transport may not close, since the transport is closed by
// the reader encountering the connection error.
// In case throttle() or throttleWithTimeout() is currently in flight,
// it needs to be unblocked. Otherwise, the transport may not close,
// since the transport is closed by the reader encountering the
// connection error.
ch := c.trfChan.Swap(nil)
if ch != nil {
close(*ch)
+3 -2
View File
@@ -268,7 +268,7 @@ func (ht *serverHandlerTransport) writeStatus(s *ServerStream, st *status.Status
for _, v := range vv {
// http2 ResponseWriter mechanism to send undeclared Trailers after
// the headers have possibly been written.
h.Add(http2.TrailerPrefix+k, encodeMetadataHeader(k, v))
h.Add(http.TrailerPrefix+k, encodeMetadataHeader(k, v))
}
}
}
@@ -494,7 +494,8 @@ func mapRecvMsgError(err error) error {
if err == io.EOF || err == io.ErrUnexpectedEOF {
return err
}
if se, ok := err.(http2.StreamError); ok {
var se http2.StreamError
if errors.As(err, &se) {
if code, ok := http2ErrConvTab[se.Code]; ok {
return status.Error(code, se.Error())
}
+28 -3
View File
@@ -65,6 +65,13 @@ var clientConnectionCounter uint64
var goAwayLoopyWriterTimeout = 5 * time.Second
// clientThrottleTimeout is the maximum amount of time for which the client
// stops reading from the connection because too many frames queued in
// response to the server (see maxQueuedTransportResponseFrames) are waiting
// to be written. After that, the client closes the connection instead of
// waiting forever, since the server is most likely not reading.
var clientThrottleTimeout = 10 * time.Second
var metadataFromOutgoingContextRaw = internal.FromOutgoingContextRaw.(func(context.Context) (metadata.MD, [][]string, bool))
// http2Client implements the ClientTransport interface with HTTP2.
@@ -107,6 +114,12 @@ type http2Client struct {
kp keepalive.ClientParameters
keepaliveEnabled bool
// throttleTimeout is the maximum amount of time for which reads can
// stay throttled before the connection is closed. It is copied from
// clientThrottleTimeout on creation, so that tests can override the
// latter without racing with the readers of other transports.
throttleTimeout time.Duration
statsHandler stats.Handler
initialWindowSize int32
@@ -359,6 +372,7 @@ func NewHTTP2Client(connectCtx, ctx context.Context, addr resolver.Address, opts
streamQuota: defaultMaxStreamsClient,
streamsQuotaAvailable: make(chan struct{}, 1),
keepaliveEnabled: keepaliveEnabled,
throttleTimeout: clientThrottleTimeout,
bufferPool: opts.BufferPool,
onClose: onClose,
}
@@ -386,7 +400,7 @@ func NewHTTP2Client(connectCtx, ctx context.Context, addr resolver.Address, opts
} else if md := imetadata.Get(addr); md != nil {
t.md = md
}
t.controlBuf = newControlBuffer(t.ctxDone)
t.controlBuf = newControlBuffer(t.ctxDone, clientSide)
if opts.InitialWindowSize >= defaultWindowSize {
t.initialWindowSize = opts.InitialWindowSize
}
@@ -952,6 +966,7 @@ func (t *http2Client) NewStream(ctx context.Context, callHdr *CallHdr, handler s
LocalAddr: t.localAddr,
Compression: callHdr.SendCompress,
Header: header,
Authority: callHdr.Host,
})
}
if transportDrainRequired {
@@ -1726,9 +1741,19 @@ func (t *http2Client) reader(errCh chan<- error) {
atomic.StoreInt64(&t.lastRead, time.Now().UnixNano())
}
// loop to keep reading incoming messages on this transport.
// Loop to keep reading incoming messages on this transport.
for {
t.controlBuf.throttle()
// Stop reading while too many items that the server can make
// the client queue (see cbItem.isTransportResponseFrame) are
// waiting to be written, to bound the memory that a server
// can make the client use by flooding it without reading. If
// reads stay throttled for t.throttleTimeout, close the
// connection instead of waiting forever.
if err := t.controlBuf.throttleWithTimeout(t.throttleTimeout); err != nil {
t.logger.Warningf("Closing connection since the server is not reading: %v", err)
errClose = connectionErrorf(true, err, "closing connection since the server is not reading: %v", err)
return
}
frame, err := t.framer.readFrame()
if t.keepaliveEnabled {
atomic.StoreInt64(&t.lastRead, time.Now().UnixNano())
+27 -16
View File
@@ -297,7 +297,7 @@ func NewServerTransport(conn net.Conn, config *ServerConfig) (_ ServerTransport,
)
t.logger = prefixLoggerForServerTransport(t)
t.controlBuf = newControlBuffer(t.done)
t.controlBuf = newControlBuffer(t.done, serverSide)
if !config.StaticWindowSize {
t.bdpEst = &bdpEstimator{
bdp: initialWindowSize,
@@ -380,6 +380,11 @@ func (t *http2Server) operateHeaders(ctx context.Context, frame *http2.MetaHeade
defer t.maxStreamMu.Unlock()
streamID := frame.Header().StreamID
if streamID%2 != 1 || streamID <= t.maxStreamID {
// illegal gRPC stream id.
return fmt.Errorf("received an illegal stream id: %v. headers frame: %+v", streamID, frame)
}
t.maxStreamID = streamID
// frame.Truncated is set to true when framer detects that the current header
// list size hits MaxHeaderListSize limit.
@@ -393,12 +398,6 @@ func (t *http2Server) operateHeaders(ctx context.Context, frame *http2.MetaHeade
return nil
}
if streamID%2 != 1 || streamID <= t.maxStreamID {
// illegal gRPC stream id.
return fmt.Errorf("received an illegal stream id: %v. headers frame: %+v", streamID, frame)
}
t.maxStreamID = streamID
s := &ServerStream{
Stream: Stream{
id: streamID,
@@ -1014,7 +1013,17 @@ func (t *http2Server) streamContextErr(s *ServerStream) error {
return ErrConnClosing
default:
}
return ContextErr(s.ctx.Err())
err := s.ctx.Err()
if err == nil {
// In closeStream and finishStream, the stream state is transitioned to
// streamDone before s.cancel() is invoked so that concurrent operations
// immediately observe the terminal state. A concurrent caller (such as
// write or writeHeader) can see streamDone and invoke streamContextErr
// before s.cancel() runs or propagates to s.ctx. Default to
// context.Canceled rather than passing nil to ContextErr.
err = context.Canceled
}
return ContextErr(err)
}
// WriteHeader sends the header metadata md back to the client.
@@ -1336,10 +1345,6 @@ func (t *http2Server) deleteStream(s *ServerStream, eosReceived bool) {
// finishStream closes the stream and puts the trailing headerFrame into controlbuf.
func (t *http2Server) finishStream(s *ServerStream, rst bool, rstCode http2.ErrCode, hdr *serverHeaders, eosReceived bool) {
// In case stream sending and receiving are invoked in separate
// goroutines (e.g., bi-directional streaming), cancel needs to be
// called to interrupt the potential blocking on other goroutines.
s.cancel()
oldState := s.swapState(streamDone)
if oldState == streamDone {
@@ -1347,6 +1352,11 @@ func (t *http2Server) finishStream(s *ServerStream, rst bool, rstCode http2.ErrC
return
}
// In case stream sending and receiving are invoked in separate
// goroutines (e.g., bi-directional streaming), cancel needs to be
// called to interrupt the potential blocking on other goroutines.
s.cancel()
hdr.cleanup = &cleanupStream{
streamID: s.id,
rst: rst,
@@ -1360,15 +1370,16 @@ func (t *http2Server) finishStream(s *ServerStream, rst bool, rstCode http2.ErrC
// closeStream clears the footprint of a stream when the stream is not needed any more.
func (t *http2Server) closeStream(s *ServerStream, rst bool, rstCode http2.ErrCode, eosReceived bool) {
// We can't return early even if the stream's state is "done" as the state
// might have been set by the `finishStream` method. Deleting the stream via
// `finishStream` can get blocked on flow control.
s.swapState(streamDone)
// In case stream sending and receiving are invoked in separate
// goroutines (e.g., bi-directional streaming), cancel needs to be
// called to interrupt the potential blocking on other goroutines.
s.cancel()
// We can't return early even if the stream's state is "done" as the state
// might have been set by the `finishStream` method. Deleting the stream via
// `finishStream` can get blocked on flow control.
s.swapState(streamDone)
t.deleteStream(s, eosReceived)
t.controlBuf.put(&cleanupStream{
+3 -1
View File
@@ -121,7 +121,9 @@ func (b *recvBuffer) put(r recvMsg) {
if b.err != nil {
// drop the buffer on the floor. Since b.err is not nil, any subsequent reads
// will always return an error, making this buffer inaccessible.
r.buffer.Free()
if r.buffer != nil {
r.buffer.Free()
}
// An error had occurred earlier, don't accept more
// data or errors.
return
+60
View File
@@ -360,6 +360,66 @@ func FromOutgoingContext(ctx context.Context) (MD, bool) {
return out, ok
}
// ValueFromOutgoingContext returns the metadata value corresponding to the
// metadata key from the outgoing metadata if it exists. Keys are matched in a
// case-insensitive manner.
//
// Unlike FromOutgoingContext, this does not copy the entire outgoing metadata.
func ValueFromOutgoingContext(ctx context.Context, key string) []string {
raw, ok := ctx.Value(mdOutgoingKey{}).(rawMD)
if !ok {
return nil
}
key = strings.ToLower(key)
// Unlike ValueFromIncomingContext, this can't return as soon as raw.md is
// checked: raw.added still needs to be walked and accumulated regardless.
// The else below prevents that from overwriting an exact match with an
// unrelated case-insensitive one.
var matchedMD []string
if v, ok := raw.md[key]; ok {
matchedMD = v
} else {
for k, v := range raw.md {
// Case insensitive comparison: MD is a map, and there's no
// guarantee that the MD attached to the context is created using
// our helper functions.
if strings.EqualFold(k, key) {
matchedMD = v
break
}
}
}
// Defer allocating vals until raw.added actually has a match, so a match
// found only in raw.md doesn't pay for both a copyOf and a growing
// append.
var vals []string
for _, added := range raw.added {
if len(added)%2 == 1 {
panic(fmt.Sprintf("metadata: ValueFromOutgoingContext got an odd number of input pairs for metadata: %d", len(added)))
}
// Case insensitive, like FromOutgoingContext: added isn't guaranteed
// lowercase, though AppendToOutgoingContext already lowercases it in
// practice, so try the cheap exact match first.
for i := 0; i < len(added); i += 2 {
if added[i] == key || strings.EqualFold(added[i], key) {
if vals == nil {
vals = make([]string, 0, len(matchedMD)+1)
vals = append(vals, matchedMD...)
}
vals = append(vals, added[i+1])
}
}
}
if vals == nil && matchedMD != nil {
return copyOf(matchedMD)
}
return vals
}
type rawMD struct {
md MD
added [][]string
+1 -3
View File
@@ -179,7 +179,6 @@ type callInfo struct {
contentSubtype string
codec baseCodec
maxRetryRPCBufferSize int
onFinish []func(err error)
authority string
acceptedResponseCompressors []string
}
@@ -394,8 +393,7 @@ type OnFinishCallOption struct {
OnFinish func(error)
}
func (o OnFinishCallOption) before(c *callInfo) error {
c.onFinish = append(c.onFinish, o.OnFinish)
func (o OnFinishCallOption) before(*callInfo) error {
return nil
}
+9 -3
View File
@@ -1400,7 +1400,7 @@ func (s *Server) processRPC(ctx context.Context, stream *transport.ServerStream,
ss.decompressorV1 = encoding.GetCompressor(rc)
if ss.decompressorV1 == nil {
st := status.Newf(codes.Unimplemented, "grpc: Decompressor is not installed for grpc-encoding %q", rc)
ss.writeStatus(st)
ss.s.WriteStatus(st)
return st.Err()
}
}
@@ -1491,7 +1491,7 @@ func (s *Server) processRPC(ctx context.Context, stream *transport.ServerStream,
binlog.Log(ctx, st)
}
}
if err := ss.writeStatus(appStatus); err != nil {
if err := ss.s.WriteStatus(appStatus); err != nil {
channelz.Warningf(logger, s.channelz, "grpc: Server.processRPC failed to write status: %v", err)
}
return appErr
@@ -1510,7 +1510,7 @@ func (s *Server) processRPC(ctx context.Context, stream *transport.ServerStream,
binlog.Log(ctx, st)
}
}
if err := ss.writeStatus(statusOK); err != nil {
if err := ss.s.WriteStatus(statusOK); err != nil {
channelz.Warningf(logger, s.channelz, "grpc: Server.processRPC failed to write status: %v", err)
}
return err
@@ -1719,7 +1719,13 @@ func (s *Server) stop(graceful bool) {
}
if graceful || s.opts.waitForHandlers {
// Release the lock while waiting for handlers to finish. Holding it
// here would deadlock a concurrent Stop() (the recommended way to abort
// a GracefulStop that is taking too long), because Stop() must acquire
// s.mu before it can force the server to shut down.
s.mu.Unlock()
s.handlersWG.Wait()
s.mu.Lock()
}
if s.events != nil {
+3
View File
@@ -190,6 +190,9 @@ type OutHeader struct {
Compression string
// Header contains the header metadata sent.
Header metadata.MD
// Authority is the :authority pseudo-header sent for the RPC.
// It is valid only if Client is true.
Authority string
// The following fields are valid only if Client is true.
// FullMethod is the full RPC method string, i.e., /package.service/method.
+80 -69
View File
@@ -201,9 +201,9 @@ func (w *clientStreamWrapper) SendMsg(m any) error {
}
// RecvMsg receives message m from the stream. For RPCs that call RecvMsg only
// once i.e. only client streaming RPCs, it calls the underlying RecvMsg a
// second time after receiving the first message to get the trailers.
// RecvMsg receives message m from the stream. For non-server-streaming RPCs
// (unary and client-streaming), it calls the underlying RecvMsg a second time
// after receiving the first message to get the trailers.
func (w *clientStreamWrapper) RecvMsg(m any) error {
err := w.ClientStream.RecvMsg(m)
if err != nil {
@@ -224,19 +224,6 @@ func (w *clientStreamWrapper) RecvMsg(m any) error {
return err
}
// defaultStreamInterceptor is a StreamClientInterceptor which wraps the
// ClientStream and is always invoked as the first interceptor. It consolidates
// behavior for different RPC types at the level closest to the application,
// which simplifies the underlying stream implementation and other interceptors
// by avoiding duplicate or scattered handling.
func defaultStreamInterceptor(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, streamer Streamer, opts ...CallOption) (ClientStream, error) {
cs, err := streamer(ctx, desc, cc, method, opts...)
if err != nil {
return nil, err
}
return &clientStreamWrapper{ClientStream: cs, desc: desc}, nil
}
// NewStream creates a new Stream for the client side. This is typically
// called by generated code. ctx is used for the lifetime of the stream.
//
@@ -392,7 +379,11 @@ func newClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, meth
}
}
return newStream(ctx, opts...)
cs, err := newStream(ctx, opts...)
if err != nil {
return nil, err
}
return &clientStreamWrapper{ClientStream: cs, desc: desc}, nil
}
func newClientStreamWithParams(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, mc *serviceconfig.MethodConfig, onCommit func(), nameResolutionDelayed bool, opts ...CallOption) (_ ClientStream, err error) {
@@ -632,7 +623,12 @@ func (a *csAttempt) getTransport() error {
func (a *csAttempt) newStream() error {
cs := a.cs
cs.callHdr.PreviousAttempts = cs.numRetries
// The header is copied because the fields set below, notably the authority
// override taken from the pick result, describe the endpoint picked for
// this attempt only. Mutating the clientStream's header would carry them
// into a later attempt.
callHdr := *cs.callHdr
callHdr.PreviousAttempts = cs.numRetries
// Merge metadata stored in PickResult, if any, with existing call metadata.
// It is safe to overwrite the csAttempt's context here, since all state
@@ -659,11 +655,11 @@ func (a *csAttempt) newStream() error {
// apply it, as specified in gRFC A81.
if cs.callInfo.authority == "" {
if authMD := a.pickResult.Metadata.Get(":authority"); len(authMD) > 0 {
cs.callHdr.Authority = authMD[0]
callHdr.Authority = authMD[0]
}
}
}
s, err := a.transport.NewStream(a.ctx, cs.callHdr, a.statsHandler)
s, err := a.transport.NewStream(a.ctx, &callHdr, a.statsHandler)
if err != nil {
nse, ok := err.(*transport.NewStreamError)
if !ok {
@@ -698,10 +694,6 @@ type clientStream struct {
cancel context.CancelFunc // cancels all attempts
sentLast bool // sent an end stream
receivedFirstMsg bool // set after the first message is received
methodConfig *MethodConfig
ctx context.Context // the application's context, wrapped by stats/tracing
@@ -709,19 +701,10 @@ type clientStream struct {
retryThrottler *retryThrottler // The throttler active when the RPC began.
binlogs []binarylog.MethodLogger
// serverHeaderBinlogged is a boolean for whether server header has been
// logged. Server header will be logged when the first time one of those
// happens: stream.Header(), stream.Recv().
//
// It's only read and used by Recv() and Header(), so it doesn't need to be
// synchronized.
serverHeaderBinlogged bool
mu sync.Mutex
firstAttempt bool // if true, transparent retry is valid
numRetries int // exclusive of transparent retry attempt(s)
numRetriesSincePushback int // retries since pushback; to reset backoff
finished bool // TODO: replace with atomic cmpxchg or sync.Once?
numRetries int // exclusive of transparent retry attempt(s)
numRetriesSincePushback int // retries since pushback; to reset backoff
// attempt is the active client stream attempt.
// The only place where it is written is the newAttemptLocked method and this method never writes nil.
// So, attempt can be nil only inside newClientStream function when clientStream is first created.
@@ -731,13 +714,33 @@ type clientStream struct {
// place where we need to check if the attempt is nil.
attempt *csAttempt
// TODO(hedging): hedging will have multiple attempts simultaneously.
committed bool // active attempt committed for retry?
onCommit func()
replayBuffer []replayOp // operations to replay on retry
replayBufferSize int // current size of replayBuffer
// Bool fields are grouped at the tail to eliminate the alignment padding
// that would otherwise follow each bool when the next field is pointer- or
// int-sized. See https://github.com/grpc/grpc-go/issues/9280 for benchmarks.
// Add new bool fields here, not inline above.
// Not guarded by mu.
sentLast bool // sent an end stream
receivedFirstMsg bool // set after the first message is received
// serverHeaderBinlogged is a boolean for whether server header has been
// logged. Server header will be logged when the first time one of those
// happens: stream.Header(), stream.Recv().
//
// It's only read and used by Recv() and Header(), so it doesn't need to be
// synchronized.
serverHeaderBinlogged bool
// nameResolutionDelay indicates if there was a delay in the name resolution.
// This field is only valid on client side, it's always false on server side.
nameResolutionDelay bool
// Guarded by mu.
firstAttempt bool // if true, transparent retry is valid
finished bool // TODO: replace with atomic cmpxchg or sync.Once?
committed bool // active attempt committed for retry?
}
type replayOp struct {
@@ -755,10 +758,8 @@ type csAttempt struct {
parser parser
pickResult balancer.PickResult
finished bool
decompressorV0 Decompressor
decompressorV1 encoding.Compressor
decompressorSet bool
decompressorV0 Decompressor
decompressorV1 encoding.Compressor
mu sync.Mutex // guards trInfo.tr
// trInfo may be nil (if EnableTracing is false).
@@ -769,10 +770,18 @@ type csAttempt struct {
statsHandler stats.Handler
beginTime time.Time
// set for newStream errors that may be transparently retried
allowTransparentRetry bool
// set for pick errors that are returned as a status
drop bool
// Bool fields are grouped at the tail to eliminate the alignment padding
// that would otherwise follow each bool when the next field is pointer- or
// int-sized. See https://github.com/grpc/grpc-go/issues/9347 for benchmarks.
// Add new bool fields here, not inline above.
// Not guarded by mu.
decompressorSet bool
allowTransparentRetry bool // set for newStream errors that may be transparently retried
drop bool // set for pick errors that are returned as a status
// Guarded by mu.
finished bool
}
func (cs *clientStream) commitAttemptLocked() {
@@ -1500,20 +1509,28 @@ type addrConnStream struct {
callInfo *callInfo
transport transport.ClientTransport
ctx context.Context
sentLast bool
receivedFirstMsg bool
desc *StreamDesc
codec baseCodec
sendCompressorV0 Compressor
sendCompressorV1 encoding.Compressor
decompressorSet bool
decompressorV0 Decompressor
decompressorV1 encoding.Compressor
parser parser
// mu guards finished and is held for the entire finish method.
mu sync.Mutex
finished bool
mu sync.Mutex
parser parser
// Bool fields are grouped at the tail to eliminate the alignment padding
// that would otherwise follow each bool when the next field is pointer- or
// int-sized. See https://github.com/grpc/grpc-go/issues/9348 for benchmarks.
// Add new bool fields here, not inline above.
// Not guarded by mu.
sentLast bool
receivedFirstMsg bool
decompressorSet bool
finished bool // guarded by mu
}
func (as *addrConnStream) Header() (metadata.MD, error) {
@@ -1728,15 +1745,23 @@ type serverStream struct {
sendCompressorName string
recvFirstMsg bool // set after the first message is received
maxReceiveMessageSize int
maxSendMessageSize int
trInfo *traceInfo
// mu guards trInfo.tr after the service handler runs.
mu sync.Mutex
trInfo *traceInfo
statsHandler stats.Handler
binlogs []binarylog.MethodLogger
// Bool fields are grouped at the tail to eliminate the alignment padding
// that would otherwise follow each bool when the next field is pointer- or
// int-sized. See https://github.com/grpc/grpc-go/issues/9349 for benchmarks.
// Add new bool fields here, not inline above.
recvFirstMsg bool // set after the first message is received
// serverHeaderBinlogged indicates whether server header has been logged. It
// will happen when one of the following two happens: stream.SendHeader(),
// stream.Send().
@@ -1744,10 +1769,6 @@ type serverStream struct {
// It's only checked in send and sendHeader, doesn't need to be
// synchronized.
serverHeaderBinlogged bool
statusWritten atomic.Bool // True if status has been written to the transport.
mu sync.Mutex // protects trInfo.tr after the service handler runs.
}
func (ss *serverStream) Context() context.Context {
@@ -1795,16 +1816,6 @@ func (ss *serverStream) SetTrailer(md metadata.MD) {
ss.s.SetTrailer(md)
}
// writeStatus sends the status of a stream to the client. It uses an atomic
// CAS to guarantee that the status is written to the transport exactly once,
// even if called concurrently.
func (ss *serverStream) writeStatus(st *status.Status) error {
if !ss.statusWritten.CompareAndSwap(false, true) {
return nil
}
return ss.s.WriteStatus(st)
}
func (ss *serverStream) SendMsg(m any) (err error) {
defer func() {
if ss.trInfo != nil {
@@ -1821,7 +1832,7 @@ func (ss *serverStream) SendMsg(m any) (err error) {
}
if err != nil && err != io.EOF {
st, _ := status.FromError(toRPCErr(err))
ss.writeStatus(st)
ss.s.WriteStatus(st)
// Non-user specified status was sent out. This should be an error
// case (as a server side Cancel maybe).
//
@@ -1904,7 +1915,7 @@ func (ss *serverStream) RecvMsg(m any) (err error) {
}
if err != nil && err != io.EOF {
st, _ := status.FromError(toRPCErr(err))
ss.writeStatus(st)
ss.s.WriteStatus(st)
// Non-user specified status was sent out. This should be an error
// case (as a server side Cancel maybe).
//
+1 -1
View File
@@ -19,4 +19,4 @@
package grpc
// Version is the current grpc version.
const Version = "1.84.0"
const Version = "1.85.0"
+2 -4
View File
@@ -5,8 +5,6 @@ cloud.google.com/go
## explicit; go 1.25.0
cloud.google.com/go/auth
cloud.google.com/go/auth/credentials
cloud.google.com/go/auth/credentials/idtoken
cloud.google.com/go/auth/credentials/impersonate
cloud.google.com/go/auth/credentials/internal/externalaccount
cloud.google.com/go/auth/credentials/internal/externalaccountuser
cloud.google.com/go/auth/credentials/internal/gdch
@@ -2104,8 +2102,8 @@ google.golang.org/genproto/googleapis/api/monitoredres
google.golang.org/genproto/googleapis/rpc/code
google.golang.org/genproto/googleapis/rpc/errdetails
google.golang.org/genproto/googleapis/rpc/status
# google.golang.org/grpc v1.84.0
## explicit; go 1.25.0
# google.golang.org/grpc v1.85.0
## explicit; go 1.26.0
google.golang.org/grpc
google.golang.org/grpc/attributes
google.golang.org/grpc/backoff