diff --git a/cmd/tusd/cli/composer.go b/cmd/tusd/cli/composer.go index 70e8eec..09e6d33 100644 --- a/cmd/tusd/cli/composer.go +++ b/cmd/tusd/cli/composer.go @@ -135,8 +135,10 @@ func CreateComposer() { } accountKey := os.Getenv("AZURE_STORAGE_KEY") - if accountKey == "" { - stderr.Fatalf("No service account key for Azure BlockBlob Storage using the AZURE_STORAGE_KEY environment variable.\n") + sasUrl := os.Getenv("AZURE_STORAGE_SAS_URL") + + if accountKey == "" && sasUrl == "" { + stderr.Fatalf("No service account key or SAS URL for Azure BlockBlob Storage using the AZURE_STORAGE_KEY or AZURE_STORAGE_SAS_URL environment variable.\n") } azureEndpoint := Flags.AzEndpoint @@ -152,6 +154,7 @@ func CreateComposer() { azConfig := &azurestore.AzConfig{ AccountName: accountName, + SasUrl: sasUrl, AccountKey: accountKey, ContainerName: Flags.AzStorage, ContainerAccessType: Flags.AzContainerAccessType, diff --git a/pkg/azurestore/azureservice.go b/pkg/azurestore/azureservice.go index d8210dc..10e0cb3 100644 --- a/pkg/azurestore/azureservice.go +++ b/pkg/azurestore/azureservice.go @@ -20,9 +20,11 @@ import ( "encoding/binary" "fmt" "io" + "net/http" "net/url" "sort" "strings" + "time" "github.com/Azure/azure-storage-blob-go/azblob" "github.com/tus/tusd/pkg/handler" @@ -38,6 +40,8 @@ type azService struct { BlobAccessTier azblob.AccessTierType ContainerURL *azblob.ContainerURL ContainerName string + SasUrl string + Endpoint string } type AzService interface { @@ -46,6 +50,7 @@ type AzService interface { type AzConfig struct { AccountName string + SasUrl string AccountKey string BlobAccessTier string ContainerName string @@ -79,9 +84,24 @@ type InfoBlob struct { // New Azure service for communication to Azure BlockBlob Storage API func NewAzureService(config *AzConfig) (AzService, error) { // struct to store your credentials. - credential, err := azblob.NewSharedKeyCredential(config.AccountName, config.AccountKey) - if err != nil { - return nil, err + var credential azblob.Credential + var credError error + var cURL *url.URL + if config.AccountKey != "" { + // Use AccountKey for shared key credentials + credential, credError = azblob.NewSharedKeyCredential(config.AccountName, config.AccountKey) + if credError != nil { + return nil, credError + } + cURL, _ = url.Parse(fmt.Sprintf("%s/%s", config.Endpoint, config.ContainerName)) + } else { + // Use anonymous credentials, request initial SAS and append it to URL + credential = azblob.NewAnonymousCredential() + sas, err := getSasToken(config.SasUrl) + if err != nil { + return nil, err + } + cURL, _ = url.Parse(fmt.Sprintf("%s/%s?%s", config.Endpoint, config.ContainerName, sas)) } // Might be limited by the storage account @@ -113,7 +133,6 @@ func NewAzureService(config *AzConfig) (AzService, error) { // The pipeline specifies things like retry policies, logging, deserialization of HTTP response payloads, and more. p := azblob.NewPipeline(credential, azblob.PipelineOptions{}) - cURL, _ := url.Parse(fmt.Sprintf("%s/%s", config.Endpoint, config.ContainerName)) // Get the ContainerURL URL containerURL := azblob.NewContainerURL(*cURL, p) @@ -124,12 +143,31 @@ func NewAzureService(config *AzConfig) (AzService, error) { BlobAccessTier: blobAccessTierType, ContainerURL: &containerURL, ContainerName: config.ContainerName, + SasUrl: config.SasUrl, + Endpoint: config.Endpoint, }, nil } // Determine if we return a InfoBlob or BlockBlob, based on the name func (service *azService) NewBlob(ctx context.Context, name string) (AzBlob, error) { var fileBlob AzBlob + url := service.ContainerURL.URL() + skeParam := url.Query().Get("ske") + if skeParam != "" { + sasEndDate, _ := time.Parse(time.RFC3339, skeParam) + if sasEndDate.Before(time.Now()) { + // Renew SAS and append it to URL + sas, err := getSasToken(service.SasUrl) + if err != nil { + return nil, err + } + cURL, _ := url.Parse(fmt.Sprintf("%s/%s?%s", service.Endpoint, service.ContainerName, sas)) + p := azblob.NewPipeline(azblob.NewAnonymousCredential(), azblob.PipelineOptions{}) + newContainerURL := azblob.NewContainerURL(*cURL, p) + service.ContainerURL = &newContainerURL + } + } + bb := service.ContainerURL.NewBlockBlobURL(name) if strings.HasSuffix(name, InfoBlobSuffix) { fileBlob = &InfoBlob{ @@ -306,3 +344,16 @@ func isAzureError(err error, code string) bool { } return false } + +func getSasToken(sasUrl string) (sas string, err error) { + resp, err := http.Get(sasUrl) + if err != nil { + return "", err + } + defer resp.Body.Close() + sasResponse, readErr := io.ReadAll(resp.Body) + if readErr != nil { + return "", readErr + } + return string(sasResponse), nil +}