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:
40 files changed
+593
-1875
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -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
|
||||
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -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
|
||||
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -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
|
||||
|
||||
|
||||
Generated
Vendored
+20
-49
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -19,4 +19,4 @@
|
||||
package grpc
|
||||
|
||||
// Version is the current grpc version.
|
||||
const Version = "1.84.0"
|
||||
const Version = "1.85.0"
|
||||
Vendored
+2
-4
@@ -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
|
||||
|
||||
Reference in new issue
Block a user