azure-service-bus-go/namespace.go

334 строки
10 KiB
Go

package servicebus
// MIT License
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE
import (
"context"
"crypto/tls"
"fmt"
"runtime"
"strings"
"sync"
"time"
"github.com/Azure/azure-amqp-common-go/v3/aad"
"github.com/Azure/azure-amqp-common-go/v3/auth"
"github.com/Azure/azure-amqp-common-go/v3/cbs"
"github.com/Azure/azure-amqp-common-go/v3/conn"
"github.com/Azure/azure-amqp-common-go/v3/sas"
"github.com/Azure/go-amqp"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/devigned/tab"
"nhooyr.io/websocket"
)
const (
// banner = `
// _____ _ ____
// / ___/___ ______ __(_)________ / __ )__ _______
// \__ \/ _ \/ ___/ | / / // ___/ _ \ / __ / / / / ___/
// ___/ / __/ / | |/ / // /__/ __/ / /_/ / /_/ (__ )
///____/\___/_/ |___/_/ \___/\___/ /_____/\__,_/____/
//`
// Version is the semantic version number
Version = "0.10.9"
rootUserAgent = "/golang-service-bus"
// default ServiceBus resource uri to authenticate with
serviceBusResourceURI = "https://servicebus.azure.net/"
)
type (
// Namespace provides a simplified facade over the AMQP implementation of Azure Service Bus and is the entry point
// for using Queues, Topics and Subscriptions
Namespace struct {
Name string
Suffix string
TokenProvider auth.TokenProvider
Environment azure.Environment
tlsConfig *tls.Config
userAgent string
useWebSocket bool
// used to ensure only one goroutine is running for auth auto-refresh
initRefresh sync.Once
// populated with the result from auto-refresh, to be called elsewhere
cancelRefresh func() <-chan struct{}
}
// NamespaceOption provides structure for configuring a new Service Bus namespace
NamespaceOption func(h *Namespace) error
)
// NamespaceWithConnectionString configures a namespace with the information provided in a Service Bus connection string
func NamespaceWithConnectionString(connStr string) NamespaceOption {
return func(ns *Namespace) error {
parsed, err := conn.ParsedConnectionFromStr(connStr)
if err != nil {
return err
}
if parsed.Namespace != "" {
ns.Name = parsed.Namespace
}
if parsed.Suffix != "" {
ns.Suffix = parsed.Suffix
}
provider, err := sas.NewTokenProvider(sas.TokenProviderWithKey(parsed.KeyName, parsed.Key))
if err != nil {
return err
}
ns.TokenProvider = provider
return nil
}
}
// NamespaceWithTLSConfig appends to the TLS config.
func NamespaceWithTLSConfig(tlsConfig *tls.Config) NamespaceOption {
return func(ns *Namespace) error {
ns.tlsConfig = tlsConfig
return nil
}
}
// NamespaceWithUserAgent appends to the root user-agent value.
func NamespaceWithUserAgent(userAgent string) NamespaceOption {
return func(ns *Namespace) error {
ns.userAgent = userAgent
return nil
}
}
// NamespaceWithWebSocket configures the namespace and all entities to use wss:// rather than amqps://
func NamespaceWithWebSocket() NamespaceOption {
return func(ns *Namespace) error {
ns.useWebSocket = true
return nil
}
}
// NamespaceWithEnvironmentBinding configures a namespace using the environment details. It uses one of the following methods:
//
// 1. Client Credentials: attempt to authenticate with a Service Principal via "AZURE_TENANT_ID", "AZURE_CLIENT_ID" and
// "AZURE_CLIENT_SECRET"
//
// 2. Client Certificate: attempt to authenticate with a Service Principal via "AZURE_TENANT_ID", "AZURE_CLIENT_ID",
// "AZURE_CERTIFICATE_PATH" and "AZURE_CERTIFICATE_PASSWORD"
//
// 3. Managed Identity (MI): attempt to authenticate via the MI assigned to the Azure resource
//
//
// The Azure Environment used can be specified using the name of the Azure Environment set in "AZURE_ENVIRONMENT" var.
func NamespaceWithEnvironmentBinding(name string) NamespaceOption {
return func(ns *Namespace) error {
provider, err := aad.NewJWTProvider(
aad.JWTProviderWithEnvironmentVars(),
// TODO: fix bug upstream to use environment resourceURI
aad.JWTProviderWithResourceURI(ns.getResourceURI()),
)
if err != nil {
return err
}
ns.TokenProvider = provider
ns.Name = name
return nil
}
}
// NamespaceWithAzureEnvironment sets the namespace's Environment, Suffix and ResourceURI parameters according
// to the Azure Environment defined in "github.com/Azure/go-autorest/autorest/azure" package.
// This allows to configure the library to be used in the different Azure clouds.
// environmentName is the name of the cloud as defined in autorest : https://github.com/Azure/go-autorest/blob/b076c1437d051bf4c328db428b70f4fe22ad38b0/autorest/azure/environments.go#L34-L39
func NamespaceWithAzureEnvironment(namespaceName, environmentName string) NamespaceOption {
return func(ns *Namespace) error {
azureEnv, err := azure.EnvironmentFromName(environmentName)
if err != nil {
return err
}
ns.Environment = azureEnv
ns.Suffix = azureEnv.ServiceBusEndpointSuffix
ns.Name = namespaceName
return nil
}
}
// NamespaceWithTokenProvider sets the token provider on the namespace
func NamespaceWithTokenProvider(provider auth.TokenProvider) NamespaceOption {
return func(ns *Namespace) error {
ns.TokenProvider = provider
return nil
}
}
// NewNamespace creates a new namespace configured through NamespaceOption(s)
func NewNamespace(opts ...NamespaceOption) (*Namespace, error) {
ns := &Namespace{
Environment: azure.PublicCloud,
}
for _, opt := range opts {
err := opt(ns)
if err != nil {
return nil, err
}
}
return ns, nil
}
func (ns *Namespace) newClient(ctx context.Context) (*amqp.Client, error) {
ctx, span := ns.startSpanFromContext(ctx, "sb.namespace.newClient")
defer span.End()
defaultConnOptions := []amqp.ConnOption{
amqp.ConnSASLAnonymous(),
amqp.ConnMaxSessions(65535),
amqp.ConnProperty("product", "MSGolangClient"),
amqp.ConnProperty("version", Version),
amqp.ConnProperty("platform", runtime.GOOS),
amqp.ConnProperty("framework", runtime.Version()),
amqp.ConnProperty("user-agent", ns.getUserAgent()),
}
if ns.tlsConfig != nil {
defaultConnOptions = append(
defaultConnOptions,
amqp.ConnTLS(true),
amqp.ConnTLSConfig(ns.tlsConfig),
)
}
if ns.useWebSocket {
wssHost := ns.getWSSHostURI() + "$servicebus/websocket"
opts := &websocket.DialOptions{Subprotocols: []string{"amqp"}}
wssConn, _, err := websocket.Dial(ctx, wssHost, opts)
if err != nil {
return nil, err
}
nConn := websocket.NetConn(context.Background(), wssConn, websocket.MessageBinary)
return amqp.New(nConn, append(defaultConnOptions, amqp.ConnServerHostname(ns.getHostname()))...)
}
return amqp.Dial(ns.getAMQPHostURI(), defaultConnOptions...)
}
// negotiateClaim performs initial authentication and starts periodic refresh of credentials.
// the returned func is to cancel() the refresh goroutine.
func (ns *Namespace) negotiateClaim(ctx context.Context, client *amqp.Client, entityPath string) (func() <-chan struct{}, error) {
ctx, span := ns.startSpanFromContext(ctx, "sb.namespace.negotiateClaim")
defer span.End()
audience := ns.getEntityAudience(entityPath)
if err := cbs.NegotiateClaim(ctx, audience, client, ns.TokenProvider); err != nil {
return nil, err
}
ns.initRefresh.Do(func() {
// start the periodic refresh of credentials
refreshCtx, cancel := context.WithCancel(context.Background())
exitChan := make(chan struct{})
go func() {
defer func() {
// reset the guard when the refresh goroutine exits
ns.initRefresh = sync.Once{}
// signal that the refresh goroutine has exited
close(exitChan)
}()
for {
select {
case <-refreshCtx.Done():
return
case <-time.After(15 * time.Minute):
refreshCtx, span := ns.startSpanFromContext(refreshCtx, "sb.namespace.negotiateClaim.refresh")
defer span.End()
if err := cbs.NegotiateClaim(refreshCtx, audience, client, ns.TokenProvider); err != nil {
tab.For(refreshCtx).Error(err)
// if auth failed cancel auto-refresh
cancel()
}
}
}
}()
ns.cancelRefresh = func() <-chan struct{} {
cancel()
return exitChan
}
})
return ns.cancelRefresh, nil
}
func (ns *Namespace) getWSSHostURI() string {
suffix := ns.resolveSuffix()
if strings.HasSuffix(suffix, "onebox.windows-int.net") {
return fmt.Sprintf("wss://%s:4446/", ns.getHostname())
}
return fmt.Sprintf("wss://%s/", ns.getHostname())
}
func (ns *Namespace) getAMQPHostURI() string {
return fmt.Sprintf("amqps://%s/", ns.getHostname())
}
func (ns *Namespace) getHTTPSHostURI() string {
suffix := ns.resolveSuffix()
if strings.HasSuffix(suffix, "onebox.windows-int.net") {
return fmt.Sprintf("https://%s:4446/", ns.getHostname())
}
return fmt.Sprintf("https://%s/", ns.getHostname())
}
func (ns *Namespace) getHostname() string {
return strings.Join([]string{ns.Name, ns.resolveSuffix()}, ".")
}
func (ns *Namespace) getEntityAudience(entityPath string) string {
return ns.getAMQPHostURI() + entityPath
}
func (ns *Namespace) getUserAgent() string {
userAgent := rootUserAgent
if ns.userAgent != "" {
userAgent = fmt.Sprintf("%s/%s", userAgent, ns.userAgent)
}
return userAgent
}
func (ns *Namespace) getResourceURI() string {
if ns.Environment.ResourceIdentifiers.ServiceBus == "" {
return serviceBusResourceURI
}
return ns.Environment.ResourceIdentifiers.ServiceBus
}
func (ns *Namespace) resolveSuffix() string {
if ns.Suffix != "" {
return ns.Suffix
}
return azure.PublicCloud.ServiceBusEndpointSuffix
}