Keepers dbb3bd486d
handle large item client redirects (#3350)
adds the khttp redirectMiddleware to the http wrapper used for large item downloads, to ensure that proxy servers and other 3xx class responses appropriately follow their redirection.

---

#### Does this PR need a docs update or release note?

- [x]  Yes, it's included

#### Type of change

- [x] 🐛 Bugfix

#### Issue(s)

* #3344

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
2023-05-09 20:12:08 +00:00

276 lines
8.0 KiB
Go

package graph
import (
"net/http"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/alcionai/clues"
"github.com/microsoft/kiota-abstractions-go/serialization"
kauth "github.com/microsoft/kiota-authentication-azure-go"
khttp "github.com/microsoft/kiota-http-go"
msgraphsdkgo "github.com/microsoftgraph/msgraph-sdk-go"
msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core"
"github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/path"
)
const (
logGraphRequestsEnvKey = "LOG_GRAPH_REQUESTS"
log2xxGraphRequestsEnvKey = "LOG_2XX_GRAPH_REQUESTS"
log2xxGraphResponseEnvKey = "LOG_2XX_GRAPH_RESPONSES"
retryAttemptHeader = "Retry-Attempt"
retryAfterHeader = "Retry-After"
defaultMaxRetries = 3
defaultDelay = 3 * time.Second
rateLimitHeader = "RateLimit-Limit"
rateRemainingHeader = "RateLimit-Remaining"
rateResetHeader = "RateLimit-Reset"
defaultHTTPClientTimeout = 1 * time.Hour
)
// AllMetadataFileNames produces the standard set of filenames used to store graph
// metadata such as delta tokens and folderID->path references.
func AllMetadataFileNames() []string {
return []string{DeltaURLsFileName, PreviousPathFileName}
}
type QueryParams struct {
Category path.CategoryType
ResourceOwner idname.Provider
Credentials account.M365Config
}
// ---------------------------------------------------------------------------
// Interfaces
// ---------------------------------------------------------------------------
type Servicer interface {
// Client() returns msgraph Service client that can be used to process and execute
// the majority of the queries to the M365 Backstore
Client() *msgraphsdkgo.GraphServiceClient
// Adapter() returns GraphRequest adapter used to process large requests, create batches
// and page iterators
Adapter() *msgraphsdkgo.GraphRequestAdapter
}
// ---------------------------------------------------------------------------
// Service Handler
// ---------------------------------------------------------------------------
var _ Servicer = &Service{}
type Service struct {
adapter *msgraphsdkgo.GraphRequestAdapter
client *msgraphsdkgo.GraphServiceClient
}
func NewService(adapter *msgraphsdkgo.GraphRequestAdapter) *Service {
return &Service{
adapter: adapter,
client: msgraphsdkgo.NewGraphServiceClient(adapter),
}
}
func (s Service) Adapter() *msgraphsdkgo.GraphRequestAdapter {
return s.adapter
}
func (s Service) Client() *msgraphsdkgo.GraphServiceClient {
return s.client
}
// Seraialize writes an M365 parsable object into a byte array using the built-in
// application/json writer within the adapter.
func (s Service) Serialize(object serialization.Parsable) ([]byte, error) {
writer, err := s.adapter.GetSerializationWriterFactory().GetSerializationWriter("application/json")
if err != nil || writer == nil {
return nil, clues.Wrap(err, "creating json serialization writer")
}
err = writer.WriteObjectValue("", object)
if err != nil {
return nil, clues.Wrap(err, "serializing object")
}
return writer.GetSerializedContent()
}
// ---------------------------------------------------------------------------
// Adapter
// ---------------------------------------------------------------------------
// CreateAdapter uses provided credentials to log into M365 using Kiota Azure Library
// with Azure identity package. An adapter object is a necessary to component
// to create *msgraphsdk.GraphServiceClient
func CreateAdapter(
tenant, client, secret string,
opts ...Option,
) (*msgraphsdkgo.GraphRequestAdapter, error) {
auth, err := GetAuth(tenant, client, secret)
if err != nil {
return nil, err
}
httpClient := KiotaHTTPClient(opts...)
return msgraphsdkgo.NewGraphRequestAdapterWithParseNodeFactoryAndSerializationWriterFactoryAndHttpClient(
auth,
nil, nil,
httpClient)
}
func GetAuth(tenant string, client string, secret string) (*kauth.AzureIdentityAuthenticationProvider, error) {
// Client Provider: Uses Secret for access to tenant-level data
cred, err := azidentity.NewClientSecretCredential(tenant, client, secret, nil)
if err != nil {
return nil, clues.Wrap(err, "creating m365 client identity")
}
auth, err := kauth.NewAzureIdentityAuthenticationProviderWithScopes(
cred,
[]string{"https://graph.microsoft.com/.default"},
)
if err != nil {
return nil, clues.Wrap(err, "creating azure authentication")
}
return auth, nil
}
// KiotaHTTPClient creates a httpClient with middlewares and timeout configured
// for use in the graph adapter.
//
// Re-use of http clients is critical, or else we leak OS resources
// and consume relatively unbound socket connections. It is important
// to centralize this client to be passed downstream where api calls
// can utilize it on a per-download basis.
func KiotaHTTPClient(opts ...Option) *http.Client {
var (
clientOptions = msgraphsdkgo.GetDefaultClientOptions()
cc = populateConfig(opts...)
middlewares = kiotaMiddlewares(&clientOptions, cc)
httpClient = msgraphgocore.GetDefaultClient(&clientOptions, middlewares...)
)
httpClient.Timeout = defaultHTTPClientTimeout
cc.apply(httpClient)
return httpClient
}
// ---------------------------------------------------------------------------
// HTTP Client Config
// ---------------------------------------------------------------------------
type clientConfig struct {
noTimeout bool
// MaxRetries before failure
maxRetries int
// The minimum delay in seconds between retries
minDelay time.Duration
overrideRetryCount bool
appendMiddleware []khttp.Middleware
}
type Option func(*clientConfig)
// populate constructs a clientConfig according to the provided options.
func populateConfig(opts ...Option) *clientConfig {
cc := clientConfig{
maxRetries: defaultMaxRetries,
minDelay: defaultDelay,
}
for _, opt := range opts {
opt(&cc)
}
return &cc
}
// apply updates the http.Client with the expected options.
func (c *clientConfig) apply(hc *http.Client) {
if c.noTimeout {
// FIXME: This should ideally be 0, but if we set to 0, graph
// client with automatically set the context timeout to 0 as
// well which will make the client unusable.
// https://github.com/microsoft/kiota-http-go/pull/71
hc.Timeout = 48 * time.Hour
}
}
// NoTimeout sets the httpClient.Timeout to 0 (unlimited).
// The resulting client isn't suitable for most queries, due to the
// capacity for a call to persist forever. This configuration should
// only be used when downloading very large files.
func NoTimeout() Option {
return func(c *clientConfig) {
c.noTimeout = true
}
}
func MaxRetries(max int) Option {
return func(c *clientConfig) {
c.overrideRetryCount = true
c.maxRetries = max
}
}
func MinimumBackoff(dur time.Duration) Option {
return func(c *clientConfig) {
c.minDelay = dur
}
}
func appendMiddleware(mw ...khttp.Middleware) Option {
return func(c *clientConfig) {
if len(mw) > 0 {
c.appendMiddleware = mw
}
}
}
// ---------------------------------------------------------------------------
// Middleware Control
// ---------------------------------------------------------------------------
// kiotaMiddlewares creates a default slice of middleware for the Graph Client.
func kiotaMiddlewares(
options *msgraphgocore.GraphClientOptions,
cc *clientConfig,
) []khttp.Middleware {
mw := []khttp.Middleware{}
// Optionally add concurrency limiter middleware if it has been initialized
if concurrencyLim != nil {
mw = append(mw, concurrencyLim)
}
mw = append(mw, []khttp.Middleware{
msgraphgocore.NewGraphTelemetryHandler(options),
&RetryMiddleware{
MaxRetries: cc.maxRetries,
Delay: cc.minDelay,
},
khttp.NewRetryHandler(),
khttp.NewRedirectHandler(),
khttp.NewCompressionHandler(),
khttp.NewParametersNameDecodingHandler(),
khttp.NewUserAgentHandler(),
&LoggingMiddleware{},
&ThrottleControlMiddleware{},
&MetricsMiddleware{},
}...)
if len(cc.appendMiddleware) > 0 {
mw = append(mw, cc.appendMiddleware...)
}
return mw
}