diff --git a/sdk/storage/azqueue/client.go b/sdk/storage/azqueue/client.go new file mode 100644 index 000000000000..50568048adba --- /dev/null +++ b/sdk/storage/azqueue/client.go @@ -0,0 +1,154 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azqueue + +import ( + "context" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue/internal/base" + "github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue/internal/exported" + "github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue/internal/generated" + "github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue/internal/shared" + "net/http" +) + +// ClientOptions contains the optional parameters when creating a Client. +type ClientOptions struct { + azcore.ClientOptions +} + +// Client represents a URL to the Azure Queue Storage service allowing you to manipulate queues. +type Client base.Client[generated.ServiceClient] + +// NewClientWithNoCredential creates an instance of Client with the specified values. +// This is used to anonymously access a storage account or with a shared access signature (SAS) token. +// - serviceURL - the URL of the storage account e.g. https://.queue.core.windows.net/? +// - options - client options; pass nil to accept the default values +func NewClientWithNoCredential(serviceURL string, options *ClientOptions) (*Client, error) { + conOptions := shared.GetClientOptions(options) + pl := runtime.NewPipeline(exported.ModuleName, exported.ModuleVersion, runtime.PipelineOptions{}, &conOptions.ClientOptions) + + return (*Client)(base.NewServiceClient(serviceURL, pl, nil)), nil +} + +// NewClientWithSharedKeyCredential creates an instance of Client with the specified values. +// - serviceURL - the URL of the storage account e.g. https://.queue.core.windows.net/ +// - cred - a SharedKeyCredential created with the matching storage account and access key +// - options - client options; pass nil to accept the default values +func NewClientWithSharedKeyCredential(serviceURL string, cred *SharedKeyCredential, options *ClientOptions) (*Client, error) { + authPolicy := exported.NewSharedKeyCredPolicy(cred) + conOptions := shared.GetClientOptions(options) + conOptions.PerRetryPolicies = append(conOptions.PerRetryPolicies, authPolicy) + pl := runtime.NewPipeline(exported.ModuleName, exported.ModuleVersion, runtime.PipelineOptions{}, &conOptions.ClientOptions) + + return (*Client)(base.NewServiceClient(serviceURL, pl, cred)), nil +} + +// NewClientFromConnectionString creates an instance of Client with the specified values. +// - connectionString - a connection string for the desired storage account +// - options - client options; pass nil to accept the default values +func NewClientFromConnectionString(connectionString string, options *ClientOptions) (*Client, error) { + parsed, err := shared.ParseConnectionString(connectionString) + if err != nil { + return nil, err + } + + if parsed.AccountKey != "" && parsed.AccountName != "" { + credential, err := exported.NewSharedKeyCredential(parsed.AccountName, parsed.AccountKey) + if err != nil { + return nil, err + } + return NewClientWithSharedKeyCredential(parsed.ServiceURL, credential, options) + } + + return NewClientWithNoCredential(parsed.ServiceURL, options) +} + +func (s *Client) generated() *generated.ServiceClient { + return base.InnerClient((*base.Client[generated.ServiceClient])(s)) +} + +func (s *Client) sharedKey() *SharedKeyCredential { + return base.SharedKey((*base.Client[generated.ServiceClient])(s)) +} + +// URL returns the URL endpoint used by the Client object. +func (s *Client) URL() string { + return s.generated().Endpoint() +} + +// TODO: CreateQueue +// TODO: DeleteQueue + +// GetProperties - gets the properties of a storage account's Queue service, including properties for Storage Analytics +// and CORS (Cross-Origin Resource Sharing) rules. +func (s *Client) GetProperties(ctx context.Context, o *GetPropertiesOptions) (GetPropertiesResponse, error) { + getPropertiesOptions := o.format() + resp, err := s.generated().GetProperties(ctx, getPropertiesOptions) + return resp, err +} + +// SetProperties Sets the properties of a storage account's Queue service, including Azure Storage Analytics. +// If an element (e.g. analytics_logging) is left as None, the existing settings on the service for that functionality are preserved. +func (s *Client) SetProperties(ctx context.Context, o *SetPropertiesOptions) (SetPropertiesResponse, error) { + properties, setPropertiesOptions := o.format() + resp, err := s.generated().SetProperties(ctx, properties, setPropertiesOptions) + return resp, err +} + +// GetStatistics Retrieves statistics related to replication for the Queue service. +func (s *Client) GetStatistics(ctx context.Context, o *GetStatisticsOptions) (GetStatisticsResponse, error) { + getStatisticsOptions := o.format() + resp, err := s.generated().GetStatistics(ctx, getStatisticsOptions) + + return resp, err +} + +// NewListQueuesPager operation returns a pager of the queues under the specified account. +// Use an empty Marker to start enumeration from the beginning. Queue names are returned in lexicographic order. +// For more information, see https://learn.microsoft.com/en-us/rest/api/storageservices/list-queues1. +func (s *Client) NewListQueuesPager(o *ListQueuesOptions) *runtime.Pager[ListQueuesResponse] { + listOptions := generated.ServiceClientListQueuesSegmentOptions{} + if o != nil { + if o.Include.Metadata { + listOptions.Include = append(listOptions.Include, "metadata") + } + listOptions.Marker = o.Marker + listOptions.Maxresults = o.MaxResults + listOptions.Prefix = o.Prefix + } + return runtime.NewPager(runtime.PagingHandler[ListQueuesResponse]{ + More: func(page ListQueuesResponse) bool { + return page.NextMarker != nil && len(*page.NextMarker) > 0 + }, + Fetcher: func(ctx context.Context, page *ListQueuesResponse) (ListQueuesResponse, error) { + var req *policy.Request + var err error + if page == nil { + req, err = s.generated().ListQueuesSegmentCreateRequest(ctx, &listOptions) + } else { + listOptions.Marker = page.NextMarker + req, err = s.generated().ListQueuesSegmentCreateRequest(ctx, &listOptions) + } + if err != nil { + return ListQueuesResponse{}, err + } + resp, err := s.generated().Pipeline().Do(req) + if err != nil { + return ListQueuesResponse{}, err + } + if !runtime.HasStatusCode(resp, http.StatusOK) { + return ListQueuesResponse{}, runtime.NewResponseError(resp) + } + return s.generated().ListQueuesSegmentHandleResponse(resp) + }, + }) +} + +// TODO: GetSASURL() diff --git a/sdk/storage/azqueue/go.mod b/sdk/storage/azqueue/go.mod index 8a24f5064305..5dcf20fc113e 100644 --- a/sdk/storage/azqueue/go.mod +++ b/sdk/storage/azqueue/go.mod @@ -1,11 +1,13 @@ module github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue -go 1.19 +go 1.18 -require github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0 +require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0 + github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 +) require ( - github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 // indirect golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect golang.org/x/text v0.3.7 // indirect ) diff --git a/sdk/storage/azqueue/internal/base/clients.go b/sdk/storage/azqueue/internal/base/clients.go new file mode 100644 index 000000000000..b270c96f6448 --- /dev/null +++ b/sdk/storage/azqueue/internal/base/clients.go @@ -0,0 +1,58 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package base + +import ( + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue/internal/exported" + "github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue/internal/generated" +) + +type Client[T any] struct { + inner *T + sharedKey *exported.SharedKeyCredential +} + +func InnerClient[T any](client *Client[T]) *T { + return client.inner +} + +func SharedKey[T any](client *Client[T]) *exported.SharedKeyCredential { + return client.sharedKey +} + +func NewClient[T any](inner *T) *Client[T] { + return &Client[T]{inner: inner} +} + +func NewServiceClient(queueURL string, pipeline runtime.Pipeline, sharedKey *exported.SharedKeyCredential) *Client[generated.ServiceClient] { + return &Client[generated.ServiceClient]{ + inner: generated.NewServiceClient(queueURL, pipeline), + sharedKey: sharedKey, + } +} + +func NewQueueClient(queueURL string, pipeline runtime.Pipeline, sharedKey *exported.SharedKeyCredential) *Client[generated.QueueClient] { + return &Client[generated.QueueClient]{ + inner: generated.NewQueueClient(queueURL, pipeline), + sharedKey: sharedKey, + } +} + +type CompositeClient[T, U any] struct { + innerT *T + innerU *U + sharedKey *exported.SharedKeyCredential +} + +func InnerClients[T, U any](client *CompositeClient[T, U]) (*Client[T], *U) { + return &Client[T]{inner: client.innerT}, client.innerU +} + +func SharedKeyComposite[T, U any](client *CompositeClient[T, U]) *exported.SharedKeyCredential { + return client.sharedKey +} diff --git a/sdk/storage/azqueue/internal/exported/shared_key_credential.go b/sdk/storage/azqueue/internal/exported/shared_key_credential.go new file mode 100644 index 000000000000..a8d648104cad --- /dev/null +++ b/sdk/storage/azqueue/internal/exported/shared_key_credential.go @@ -0,0 +1,218 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package exported + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "fmt" + "net/http" + "net/url" + "sort" + "strings" + "sync/atomic" + "time" + + azlog "github.com/Azure/azure-sdk-for-go/sdk/azcore/log" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/internal/log" + "github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue/internal/shared" +) + +// NewSharedKeyCredential creates an immutable SharedKeyCredential containing the +// storage account's name and either its primary or secondary key. +func NewSharedKeyCredential(accountName string, accountKey string) (*SharedKeyCredential, error) { + c := SharedKeyCredential{accountName: accountName} + if err := c.SetAccountKey(accountKey); err != nil { + return nil, err + } + return &c, nil +} + +// SharedKeyCredential contains an account's name and its primary or secondary key. +type SharedKeyCredential struct { + // Only the NewSharedKeyCredential method should set these; all other methods should treat them as read-only + accountName string + accountKey atomic.Value // []byte +} + +// AccountName returns the Storage account's name. +func (c *SharedKeyCredential) AccountName() string { + return c.accountName +} + +// SetAccountKey replaces the existing account key with the specified account key. +func (c *SharedKeyCredential) SetAccountKey(accountKey string) error { + _bytes, err := base64.StdEncoding.DecodeString(accountKey) + if err != nil { + return fmt.Errorf("decode account key: %w", err) + } + c.accountKey.Store(_bytes) + return nil +} + +// ComputeHMACSHA256 generates a hash signature for an HTTP request or for a SAS. +func (c *SharedKeyCredential) computeHMACSHA256(message string) (string, error) { + h := hmac.New(sha256.New, c.accountKey.Load().([]byte)) + _, err := h.Write([]byte(message)) + return base64.StdEncoding.EncodeToString(h.Sum(nil)), err +} + +func (c *SharedKeyCredential) buildStringToSign(req *http.Request) (string, error) { + // https://docs.microsoft.com/en-us/rest/api/storageservices/authentication-for-the-azure-storage-services + headers := req.Header + contentLength := getHeader(shared.HeaderContentLength, headers) + if contentLength == "0" { + contentLength = "" + } + + canonicalizedResource, err := c.buildCanonicalizedResource(req.URL) + if err != nil { + return "", err + } + + stringToSign := strings.Join([]string{ + req.Method, + getHeader(shared.HeaderContentEncoding, headers), + getHeader(shared.HeaderContentLanguage, headers), + contentLength, + getHeader(shared.HeaderContentMD5, headers), + getHeader(shared.HeaderContentType, headers), + "", // Empty date because x-ms-date is expected (as per web page above) + getHeader(shared.HeaderIfModifiedSince, headers), + getHeader(shared.HeaderIfMatch, headers), + getHeader(shared.HeaderIfNoneMatch, headers), + getHeader(shared.HeaderIfUnmodifiedSince, headers), + getHeader(shared.HeaderRange, headers), + c.buildCanonicalizedHeader(headers), + canonicalizedResource, + }, "\n") + return stringToSign, nil +} + +func getHeader(key string, headers map[string][]string) string { + if headers == nil { + return "" + } + if v, ok := headers[key]; ok { + if len(v) > 0 { + return v[0] + } + } + + return "" +} + +func (c *SharedKeyCredential) buildCanonicalizedHeader(headers http.Header) string { + cm := map[string][]string{} + for k, v := range headers { + headerName := strings.TrimSpace(strings.ToLower(k)) + if strings.HasPrefix(headerName, "x-ms-") { + cm[headerName] = v // NOTE: the value must not have any whitespace around it. + } + } + if len(cm) == 0 { + return "" + } + + keys := make([]string, 0, len(cm)) + for key := range cm { + keys = append(keys, key) + } + sort.Strings(keys) + ch := bytes.NewBufferString("") + for i, key := range keys { + if i > 0 { + ch.WriteRune('\n') + } + ch.WriteString(key) + ch.WriteRune(':') + ch.WriteString(strings.Join(cm[key], ",")) + } + return ch.String() +} + +func (c *SharedKeyCredential) buildCanonicalizedResource(u *url.URL) (string, error) { + // https://docs.microsoft.com/en-us/rest/api/storageservices/authentication-for-the-azure-storage-services + cr := bytes.NewBufferString("/") + cr.WriteString(c.accountName) + + if len(u.Path) > 0 { + // Any portion of the CanonicalizedResource string that is derived from + // the resource's URI should be encoded exactly as it is in the URI. + // -- https://msdn.microsoft.com/en-gb/library/azure/dd179428.aspx + cr.WriteString(u.EscapedPath()) + } else { + // a slash is required to indicate the root path + cr.WriteString("/") + } + + // params is a map[string][]string; param name is key; params values is []string + params, err := url.ParseQuery(u.RawQuery) // Returns URL decoded values + if err != nil { + return "", fmt.Errorf("failed to parse query params: %w", err) + } + + if len(params) > 0 { // There is at least 1 query parameter + var paramNames []string // We use this to sort the parameter key names + for paramName := range params { + paramNames = append(paramNames, paramName) // paramNames must be lowercase + } + sort.Strings(paramNames) + + for _, paramName := range paramNames { + paramValues := params[paramName] + sort.Strings(paramValues) + + // Join the sorted key values separated by ',' + // Then prepend "keyName:"; then add this string to the buffer + cr.WriteString("\n" + paramName + ":" + strings.Join(paramValues, ",")) + } + } + return cr.String(), nil +} + +// ComputeHMACSHA256 is a helper for computing the signed string outside of this package. +func ComputeHMACSHA256(cred *SharedKeyCredential, message string) (string, error) { + return cred.computeHMACSHA256(message) +} + +// the following content isn't actually exported but must live +// next to SharedKeyCredential as it uses its unexported methods + +type SharedKeyCredPolicy struct { + cred *SharedKeyCredential +} + +func NewSharedKeyCredPolicy(cred *SharedKeyCredential) *SharedKeyCredPolicy { + return &SharedKeyCredPolicy{cred: cred} +} + +func (s *SharedKeyCredPolicy) Do(req *policy.Request) (*http.Response, error) { + if d := getHeader(shared.HeaderXmsDate, req.Raw().Header); d == "" { + req.Raw().Header.Set(shared.HeaderXmsDate, time.Now().UTC().Format(http.TimeFormat)) + } + stringToSign, err := s.cred.buildStringToSign(req.Raw()) + if err != nil { + return nil, err + } + signature, err := s.cred.computeHMACSHA256(stringToSign) + if err != nil { + return nil, err + } + authHeader := strings.Join([]string{"SharedKey ", s.cred.AccountName(), ":", signature}, "") + req.Raw().Header.Set(shared.HeaderAuthorization, authHeader) + + response, err := req.Next() + if err != nil && response != nil && response.StatusCode == http.StatusForbidden { + // Service failed to authenticate request, log it + log.Write(azlog.EventResponse, "===== HTTP Forbidden status, String-to-Sign:\n"+stringToSign+"\n===============================\n") + } + return response, err +} diff --git a/sdk/storage/azqueue/internal/exported/version.go b/sdk/storage/azqueue/internal/exported/version.go new file mode 100644 index 000000000000..cfbc93f4ea9d --- /dev/null +++ b/sdk/storage/azqueue/internal/exported/version.go @@ -0,0 +1,12 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package exported + +const ( + ModuleName = "azqueue" + ModuleVersion = "v0.1.0" +) diff --git a/sdk/storage/azqueue/internal/generated/autorest.md b/sdk/storage/azqueue/internal/generated/autorest.md index e75176d443e5..830046f50f3c 100644 --- a/sdk/storage/azqueue/internal/generated/autorest.md +++ b/sdk/storage/azqueue/internal/generated/autorest.md @@ -67,4 +67,18 @@ directive: replace(/func \(client \*ServiceClient\) NewListQueuesSegmentPager\(.+\/\/ listQueuesSegmentCreateRequest creates the ListQueuesSegment request/s, `// ListQueuesSegmentCreateRequest creates the ListQueuesFlatSegment ListQueuesSegment`). replace(/\(client \*ServiceClient\) listQueuesSegmentCreateRequest\(/, `(client *ServiceClient) ListQueuesSegmentCreateRequest(`). replace(/\(client \*ServiceClient\) listQueuesSegmentHandleResponse\(/, `(client *ServiceClient) ListQueuesSegmentHandleResponse(`); +``` + +### Fix encoder and decoder parameter names to be non-conflicting + +``` yaml +directive: + - from: zz_models_serde.go + where: $ + transform: >- + return $. + replace(/d\s*\*xml\.Decoder/g, "dec *xml.Decoder"). + replace(/d\.DecodeElement\(/g, "dec.DecodeElement("). + replace(/e\s*\*xml\.Encoder/g, "enc *xml.Encoder"). + replace(/e\.EncodeElement\(/g, "enc.EncodeElement("); ``` \ No newline at end of file diff --git a/sdk/storage/azqueue/internal/generated/service_client.go b/sdk/storage/azqueue/internal/generated/service_client.go new file mode 100644 index 000000000000..1f449b955e82 --- /dev/null +++ b/sdk/storage/azqueue/internal/generated/service_client.go @@ -0,0 +1,17 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package generated + +import "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + +func (client *ServiceClient) Endpoint() string { + return client.endpoint +} + +func (client *ServiceClient) Pipeline() runtime.Pipeline { + return client.pl +} diff --git a/sdk/storage/azqueue/internal/generated/zz_models_serde.go b/sdk/storage/azqueue/internal/generated/zz_models_serde.go index e34236f6e5b3..665a367f6156 100644 --- a/sdk/storage/azqueue/internal/generated/zz_models_serde.go +++ b/sdk/storage/azqueue/internal/generated/zz_models_serde.go @@ -19,7 +19,7 @@ import ( ) // MarshalXML implements the xml.Marshaller interface for type AccessPolicy. -func (a AccessPolicy) MarshalXML(e *xml.Encoder, start xml.StartElement) error { +func (a AccessPolicy) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { type alias AccessPolicy aux := &struct { *alias @@ -30,11 +30,11 @@ func (a AccessPolicy) MarshalXML(e *xml.Encoder, start xml.StartElement) error { Expiry: (*timeRFC3339)(a.Expiry), Start: (*timeRFC3339)(a.Start), } - return e.EncodeElement(aux, start) + return enc.EncodeElement(aux, start) } // UnmarshalXML implements the xml.Unmarshaller interface for type AccessPolicy. -func (a *AccessPolicy) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { +func (a *AccessPolicy) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error { type alias AccessPolicy aux := &struct { *alias @@ -43,7 +43,7 @@ func (a *AccessPolicy) UnmarshalXML(d *xml.Decoder, start xml.StartElement) erro }{ alias: (*alias)(a), } - if err := d.DecodeElement(aux, &start); err != nil { + if err := dec.DecodeElement(aux, &start); err != nil { return err } a.Expiry = (*time.Time)(aux.Expiry) @@ -52,7 +52,7 @@ func (a *AccessPolicy) UnmarshalXML(d *xml.Decoder, start xml.StartElement) erro } // MarshalXML implements the xml.Marshaller interface for type DequeuedMessageItem. -func (d DequeuedMessageItem) MarshalXML(e *xml.Encoder, start xml.StartElement) error { +func (d DequeuedMessageItem) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { type alias DequeuedMessageItem aux := &struct { *alias @@ -65,11 +65,11 @@ func (d DequeuedMessageItem) MarshalXML(e *xml.Encoder, start xml.StartElement) InsertionTime: (*timeRFC1123)(d.InsertionTime), TimeNextVisible: (*timeRFC1123)(d.TimeNextVisible), } - return e.EncodeElement(aux, start) + return enc.EncodeElement(aux, start) } // UnmarshalXML implements the xml.Unmarshaller interface for type DequeuedMessageItem. -func (d *DequeuedMessageItem) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { +func (d *DequeuedMessageItem) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error { type alias DequeuedMessageItem aux := &struct { *alias @@ -79,7 +79,7 @@ func (d *DequeuedMessageItem) UnmarshalXML(d *xml.Decoder, start xml.StartElemen }{ alias: (*alias)(d), } - if err := d.DecodeElement(aux, &start); err != nil { + if err := dec.DecodeElement(aux, &start); err != nil { return err } d.ExpirationTime = (*time.Time)(aux.ExpirationTime) @@ -89,7 +89,7 @@ func (d *DequeuedMessageItem) UnmarshalXML(d *xml.Decoder, start xml.StartElemen } // MarshalXML implements the xml.Marshaller interface for type EnqueuedMessage. -func (e EnqueuedMessage) MarshalXML(e *xml.Encoder, start xml.StartElement) error { +func (e EnqueuedMessage) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { type alias EnqueuedMessage aux := &struct { *alias @@ -102,11 +102,11 @@ func (e EnqueuedMessage) MarshalXML(e *xml.Encoder, start xml.StartElement) erro InsertionTime: (*timeRFC1123)(e.InsertionTime), TimeNextVisible: (*timeRFC1123)(e.TimeNextVisible), } - return e.EncodeElement(aux, start) + return enc.EncodeElement(aux, start) } // UnmarshalXML implements the xml.Unmarshaller interface for type EnqueuedMessage. -func (e *EnqueuedMessage) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { +func (e *EnqueuedMessage) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error { type alias EnqueuedMessage aux := &struct { *alias @@ -116,7 +116,7 @@ func (e *EnqueuedMessage) UnmarshalXML(d *xml.Decoder, start xml.StartElement) e }{ alias: (*alias)(e), } - if err := d.DecodeElement(aux, &start); err != nil { + if err := dec.DecodeElement(aux, &start); err != nil { return err } e.ExpirationTime = (*time.Time)(aux.ExpirationTime) @@ -126,7 +126,7 @@ func (e *EnqueuedMessage) UnmarshalXML(d *xml.Decoder, start xml.StartElement) e } // MarshalXML implements the xml.Marshaller interface for type GeoReplication. -func (g GeoReplication) MarshalXML(e *xml.Encoder, start xml.StartElement) error { +func (g GeoReplication) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { type alias GeoReplication aux := &struct { *alias @@ -135,11 +135,11 @@ func (g GeoReplication) MarshalXML(e *xml.Encoder, start xml.StartElement) error alias: (*alias)(&g), LastSyncTime: (*timeRFC1123)(g.LastSyncTime), } - return e.EncodeElement(aux, start) + return enc.EncodeElement(aux, start) } // UnmarshalXML implements the xml.Unmarshaller interface for type GeoReplication. -func (g *GeoReplication) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { +func (g *GeoReplication) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error { type alias GeoReplication aux := &struct { *alias @@ -147,7 +147,7 @@ func (g *GeoReplication) UnmarshalXML(d *xml.Decoder, start xml.StartElement) er }{ alias: (*alias)(g), } - if err := d.DecodeElement(aux, &start); err != nil { + if err := dec.DecodeElement(aux, &start); err != nil { return err } g.LastSyncTime = (*time.Time)(aux.LastSyncTime) @@ -155,7 +155,7 @@ func (g *GeoReplication) UnmarshalXML(d *xml.Decoder, start xml.StartElement) er } // MarshalXML implements the xml.Marshaller interface for type ListQueuesSegmentResponse. -func (l ListQueuesSegmentResponse) MarshalXML(e *xml.Encoder, start xml.StartElement) error { +func (l ListQueuesSegmentResponse) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { type alias ListQueuesSegmentResponse aux := &struct { *alias @@ -166,11 +166,11 @@ func (l ListQueuesSegmentResponse) MarshalXML(e *xml.Encoder, start xml.StartEle if l.QueueItems != nil { aux.QueueItems = &l.QueueItems } - return e.EncodeElement(aux, start) + return enc.EncodeElement(aux, start) } // MarshalXML implements the xml.Marshaller interface for type PeekedMessageItem. -func (p PeekedMessageItem) MarshalXML(e *xml.Encoder, start xml.StartElement) error { +func (p PeekedMessageItem) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { type alias PeekedMessageItem aux := &struct { *alias @@ -181,11 +181,11 @@ func (p PeekedMessageItem) MarshalXML(e *xml.Encoder, start xml.StartElement) er ExpirationTime: (*timeRFC1123)(p.ExpirationTime), InsertionTime: (*timeRFC1123)(p.InsertionTime), } - return e.EncodeElement(aux, start) + return enc.EncodeElement(aux, start) } // UnmarshalXML implements the xml.Unmarshaller interface for type PeekedMessageItem. -func (p *PeekedMessageItem) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { +func (p *PeekedMessageItem) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error { type alias PeekedMessageItem aux := &struct { *alias @@ -194,7 +194,7 @@ func (p *PeekedMessageItem) UnmarshalXML(d *xml.Decoder, start xml.StartElement) }{ alias: (*alias)(p), } - if err := d.DecodeElement(aux, &start); err != nil { + if err := dec.DecodeElement(aux, &start); err != nil { return err } p.ExpirationTime = (*time.Time)(aux.ExpirationTime) @@ -203,7 +203,7 @@ func (p *PeekedMessageItem) UnmarshalXML(d *xml.Decoder, start xml.StartElement) } // UnmarshalXML implements the xml.Unmarshaller interface for type QueueItem. -func (q *QueueItem) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { +func (q *QueueItem) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error { type alias QueueItem aux := &struct { *alias @@ -211,7 +211,7 @@ func (q *QueueItem) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { }{ alias: (*alias)(q), } - if err := d.DecodeElement(aux, &start); err != nil { + if err := dec.DecodeElement(aux, &start); err != nil { return err } q.Metadata = (map[string]*string)(aux.Metadata) @@ -246,7 +246,7 @@ func (s *StorageError) UnmarshalJSON(data []byte) error { } // MarshalXML implements the xml.Marshaller interface for type StorageServiceProperties. -func (s StorageServiceProperties) MarshalXML(e *xml.Encoder, start xml.StartElement) error { +func (s StorageServiceProperties) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { type alias StorageServiceProperties aux := &struct { *alias @@ -257,7 +257,7 @@ func (s StorageServiceProperties) MarshalXML(e *xml.Encoder, start xml.StartElem if s.Cors != nil { aux.Cors = &s.Cors } - return e.EncodeElement(aux, start) + return enc.EncodeElement(aux, start) } func populate(m map[string]interface{}, k string, v interface{}) { diff --git a/sdk/storage/azqueue/internal/shared/shared.go b/sdk/storage/azqueue/internal/shared/shared.go new file mode 100644 index 000000000000..e634fec6fbc6 --- /dev/null +++ b/sdk/storage/azqueue/internal/shared/shared.go @@ -0,0 +1,146 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package shared + +import ( + "errors" + "fmt" + "hash/crc64" + "net" + "strings" +) + +const ( + TokenScope = "https://storage.azure.com/.default" +) + +const ( + HeaderAuthorization = "Authorization" + HeaderXmsDate = "x-ms-date" + HeaderContentLength = "Content-Length" + HeaderContentEncoding = "Content-Encoding" + HeaderContentLanguage = "Content-Language" + HeaderContentType = "Content-Type" + HeaderContentMD5 = "Content-MD5" + HeaderIfModifiedSince = "If-Modified-Since" + HeaderIfMatch = "If-Match" + HeaderIfNoneMatch = "If-None-Match" + HeaderIfUnmodifiedSince = "If-Unmodified-Since" + HeaderRange = "Range" +) + +const crc64Polynomial uint64 = 0x9A6C9329AC4BC9B5 + +var CRC64Table = crc64.MakeTable(crc64Polynomial) + +// CopyOptions returns a zero-value T if opts is nil. +// If opts is not nil, a copy is made and its address returned. +func CopyOptions[T any](opts *T) *T { + if opts == nil { + return new(T) + } + cp := *opts + return &cp +} + +var errConnectionString = errors.New("connection string is either blank or malformed. The expected connection string " + + "should contain key value pairs separated by semicolons. For example 'DefaultEndpointsProtocol=https;AccountName=;" + + "AccountKey=;EndpointSuffix=core.windows.net'") + +type ParsedConnectionString struct { + ServiceURL string + AccountName string + AccountKey string +} + +func ParseConnectionString(connectionString string) (ParsedConnectionString, error) { + const ( + defaultScheme = "https" + defaultSuffix = "core.windows.net" + ) + + connStrMap := make(map[string]string) + connectionString = strings.TrimRight(connectionString, ";") + + splitString := strings.Split(connectionString, ";") + if len(splitString) == 0 { + return ParsedConnectionString{}, errConnectionString + } + for _, stringPart := range splitString { + parts := strings.SplitN(stringPart, "=", 2) + if len(parts) != 2 { + return ParsedConnectionString{}, errConnectionString + } + connStrMap[parts[0]] = parts[1] + } + + accountName, ok := connStrMap["AccountName"] + if !ok { + return ParsedConnectionString{}, errors.New("connection string missing AccountName") + } + + accountKey, ok := connStrMap["AccountKey"] + if !ok { + sharedAccessSignature, ok := connStrMap["SharedAccessSignature"] + if !ok { + return ParsedConnectionString{}, errors.New("connection string missing AccountKey and SharedAccessSignature") + } + return ParsedConnectionString{ + ServiceURL: fmt.Sprintf("%v://%v.queue.%v/?%v", defaultScheme, accountName, defaultSuffix, sharedAccessSignature), + }, nil + } + + protocol, ok := connStrMap["DefaultEndpointsProtocol"] + if !ok { + protocol = defaultScheme + } + + suffix, ok := connStrMap["EndpointSuffix"] + if !ok { + suffix = defaultSuffix + } + + if queueEndpoint, ok := connStrMap["QueueEndpoint"]; ok { + return ParsedConnectionString{ + ServiceURL: queueEndpoint, + AccountName: accountName, + AccountKey: accountKey, + }, nil + } + + return ParsedConnectionString{ + ServiceURL: fmt.Sprintf("%v://%v.queue.%v", protocol, accountName, suffix), + AccountName: accountName, + AccountKey: accountKey, + }, nil +} + +func GetClientOptions[T any](o *T) *T { + if o == nil { + return new(T) + } + return o +} + +// IsIPEndpointStyle checkes if URL's host is IP, in this case the storage account endpoint will be composed as: +// http(s)://IP(:port)/storageaccount/queue/... +// As url's Host property, host could be both host or host:port +func IsIPEndpointStyle(host string) bool { + if host == "" { + return false + } + if h, _, err := net.SplitHostPort(host); err == nil { + host = h + } + // For IPv6, there could be case where SplitHostPort fails for cannot finding port. + // In this case, eliminate the '[' and ']' in the URL. + // For details about IPv6 URL, please refer to https://tools.ietf.org/html/rfc2732 + if host[0] == '[' && host[len(host)-1] == ']' { + host = host[1 : len(host)-1] + } + return net.ParseIP(host) != nil +} diff --git a/sdk/storage/azqueue/models.go b/sdk/storage/azqueue/models.go new file mode 100644 index 000000000000..e570dd648adc --- /dev/null +++ b/sdk/storage/azqueue/models.go @@ -0,0 +1,128 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azqueue + +import ( + "github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue/internal/exported" + "github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue/internal/generated" +) + +// SharedKeyCredential contains an account's name and its primary or secondary key. +type SharedKeyCredential = exported.SharedKeyCredential + +// NewSharedKeyCredential creates an immutable SharedKeyCredential containing the +// storage account's name and either its primary or secondary key. +func NewSharedKeyCredential(accountName, accountKey string) (*SharedKeyCredential, error) { + return exported.NewSharedKeyCredential(accountName, accountKey) +} + +// CorsRule - CORS is an HTTP feature that enables a web application running under one domain to access resources in another +// domain. Web browsers implement a security restriction known as same-origin policy that +// prevents a web page from calling APIs in a different domain; CORS provides a secure way to allow one domain (the origin +// domain) to call APIs in another domain +type CorsRule = generated.CorsRule + +// GeoReplication - Geo-Replication information for the Secondary Storage Service +type GeoReplication = generated.GeoReplication + +// RetentionPolicy - the retention policy which determines how long the associated data should persist +type RetentionPolicy = generated.RetentionPolicy + +// Metrics - a summary of request statistics grouped by API in hour or minute aggregates for queues +type Metrics = generated.Metrics + +// Logging - Azure Analytics Logging settings. +type Logging = generated.Logging + +// TODO: CreateQueueOptions = queue.CreateOptions +// TODO: DeleteQueueOptions = queue.DeleteOptions + +// StorageServiceProperties - Storage Service Properties. +type StorageServiceProperties = generated.StorageServiceProperties + +// StorageServiceStats - Stats for the storage service. +type StorageServiceStats = generated.StorageServiceStats + +// --------------------------------------------------------------------------------------------------------------------- + +// ListQueuesOptions provides set of configurations for ListQueues operation +type ListQueuesOptions struct { + Include ListQueuesInclude + + // A string value that identifies the portion of the list of queues to be returned with the next listing operation. The + // operation returns the NextMarker value within the response body if the listing operation did not return all queues + // remaining to be listed with the current page. The NextMarker value can be used as the value for the marker parameter in + // a subsequent call to request the next page of list items. The marker value is opaque to the client. + Marker *string + + // Specifies the maximum number of queues to return. If the request does not specify max results, or specifies a value + // greater than 5000, the server will return up to 5000 items. Note that if the listing operation crosses a partition boundary, + // then the service will return a continuation token for retrieving the remainder of the results. For this reason, it is possible + // that the service will return fewer results than specified by max results, or than the default of 5000. + MaxResults *int32 + + // Filters the results to return only queues whose name begins with the specified prefix. + Prefix *string +} + +// ListQueuesInclude indicates what additional information the service should return with each queue. +type ListQueuesInclude struct { + // Tells the service whether to return metadata for each queue. + Metadata bool +} + +// --------------------------------------------------------------------------------------------------------------------- + +// SetPropertiesOptions provides set of options for Client.SetProperties +type SetPropertiesOptions struct { + // The set of CORS rules. + Cors []*CorsRule + + // a summary of request statistics grouped by API in hour or minute aggregates for queues + HourMetrics *Metrics + + // Azure Analytics Logging settings. + Logging *Logging + + // a summary of request statistics grouped by API in hour or minute aggregates for queues + MinuteMetrics *Metrics +} + +func (o *SetPropertiesOptions) format() (generated.StorageServiceProperties, *generated.ServiceClientSetPropertiesOptions) { + if o == nil { + return generated.StorageServiceProperties{}, nil + } + + return generated.StorageServiceProperties{ + Cors: o.Cors, + HourMetrics: o.HourMetrics, + Logging: o.Logging, + MinuteMetrics: o.MinuteMetrics, + }, nil +} + +// --------------------------------------------------------------------------------------------------------------------- + +// GetPropertiesOptions contains the optional parameters for the Client.GetProperties method. +type GetPropertiesOptions struct { + // placeholder for future options +} + +func (o *GetPropertiesOptions) format() *generated.ServiceClientGetPropertiesOptions { + return nil +} + +// --------------------------------------------------------------------------------------------------------------------- + +// GetStatisticsOptions provides set of options for Client.GetStatistics +type GetStatisticsOptions struct { + // placeholder for future options +} + +func (o *GetStatisticsOptions) format() *generated.ServiceClientGetStatisticsOptions { + return nil +} diff --git a/sdk/storage/azqueue/responses.go b/sdk/storage/azqueue/responses.go new file mode 100644 index 000000000000..9a35cb46afd1 --- /dev/null +++ b/sdk/storage/azqueue/responses.go @@ -0,0 +1,29 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package azqueue + +import ( + "github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue/internal/generated" +) + +// CreateQueueResponse contains the response from method queue.Client.Create. +type CreateQueueResponse = generated.QueueClientCreateResponse + +// DeleteQueueResponse contains the response from method queue.Client.Delete +type DeleteQueueResponse = generated.QueueClientDeleteResponse + +// ListQueuesResponse contains the response from method Client.ListQueuesSegment. +type ListQueuesResponse = generated.ServiceClientListQueuesSegmentResponse + +// GetPropertiesResponse contains the response from method Client.GetProperties. +type GetPropertiesResponse = generated.ServiceClientGetPropertiesResponse + +// SetPropertiesResponse contains the response from method Client.SetProperties. +type SetPropertiesResponse = generated.ServiceClientSetPropertiesResponse + +// GetStatisticsResponse contains the response from method Client.GetStatistics. +type GetStatisticsResponse = generated.ServiceClientGetStatisticsResponse