LCOV - code coverage report
Current view: top level - port - cpl_azure.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 489 585 83.6 %
Date: 2026-09-18 14:54:48 Functions: 25 28 89.3 %

          Line data    Source code
       1             : /**********************************************************************
       2             :  * Project:  CPL - Common Portability Library
       3             :  * Purpose:  Microsoft Azure Storage Blob routines
       4             :  * Author:   Even Rouault <even.rouault at spatialys.com>
       5             :  *
       6             :  **********************************************************************
       7             :  * Copyright (c) 2017, Even Rouault <even.rouault at spatialys.com>
       8             :  *
       9             :  * SPDX-License-Identifier: MIT
      10             :  ****************************************************************************/
      11             : 
      12             : #include "cpl_azure.h"
      13             : #include "cpl_json.h"
      14             : #include "cpl_minixml.h"
      15             : #include "cpl_vsi_error.h"
      16             : #include "cpl_sha256.h"
      17             : #include "cpl_time.h"
      18             : #include "cpl_http.h"
      19             : #include "cpl_multiproc.h"
      20             : #include "cpl_vsi_virtual.h"
      21             : #include "cpl_vsil_curl_class.h"
      22             : 
      23             : #include <mutex>
      24             : 
      25             : //! @cond Doxygen_Suppress
      26             : 
      27             : #ifdef HAVE_CURL
      28             : 
      29             : constexpr const char *X_MS_VERSION = "2019-12-12";
      30             : 
      31             : /************************************************************************/
      32             : /*                        RemoveTrailingSlash()                         */
      33             : /************************************************************************/
      34             : 
      35         663 : static std::string RemoveTrailingSlash(const std::string &osStr)
      36             : {
      37         663 :     std::string osRet(osStr);
      38         663 :     if (!osRet.empty() && osRet.back() == '/')
      39           1 :         osRet.pop_back();
      40         663 :     return osRet;
      41             : }
      42             : 
      43             : /************************************************************************/
      44             : /*                        CPLAzureGetSignature()                        */
      45             : /************************************************************************/
      46             : 
      47         218 : static std::string CPLAzureGetSignature(const std::string &osStringToSign,
      48             :                                         const std::string &osStorageKeyB64)
      49             : {
      50             : 
      51             :     /* -------------------------------------------------------------------- */
      52             :     /*      Compute signature.                                              */
      53             :     /* -------------------------------------------------------------------- */
      54             : 
      55         436 :     std::string osStorageKeyUnbase64(osStorageKeyB64);
      56         436 :     int nB64Length = CPLBase64DecodeInPlace(
      57         218 :         reinterpret_cast<GByte *>(&osStorageKeyUnbase64[0]));
      58         218 :     osStorageKeyUnbase64.resize(nB64Length);
      59             : #ifdef DEBUG_VERBOSE
      60             :     CPLDebug("AZURE", "signing key size: %d", nB64Length);
      61             : #endif
      62             : 
      63         218 :     GByte abySignature[CPL_SHA256_HASH_SIZE] = {};
      64         436 :     CPL_HMAC_SHA256(osStorageKeyUnbase64.c_str(), nB64Length,
      65         218 :                     osStringToSign.c_str(), osStringToSign.size(),
      66             :                     abySignature);
      67             : 
      68         218 :     char *pszB64Signature = CPLBase64Encode(CPL_SHA256_HASH_SIZE, abySignature);
      69         218 :     std::string osSignature(pszB64Signature);
      70         218 :     CPLFree(pszB64Signature);
      71         436 :     return osSignature;
      72             : }
      73             : 
      74             : /************************************************************************/
      75             : /*                        GetAzureBlobHeaders()                         */
      76             : /************************************************************************/
      77             : 
      78         224 : static struct curl_slist *GetAzureBlobHeaders(
      79             :     const std::string &osVerb, struct curl_slist *psHeaders,
      80             :     const std::string &osResource,
      81             :     const std::map<std::string, std::string> &oMapQueryParameters,
      82             :     const std::string &osStorageAccount, const std::string &osStorageKeyB64,
      83             :     bool bIncludeMSVersion)
      84             : {
      85             :     /* See
      86             :      * https://docs.microsoft.com/en-us/rest/api/storageservices/authentication-for-the-azure-storage-services
      87             :      */
      88             : 
      89         224 :     const auto AddHeaders = [bIncludeMSVersion](struct curl_slist *l_psHeaders,
      90         224 :                                                 const std::string &osDate)
      91             :     {
      92         224 :         l_psHeaders = curl_slist_append(
      93             :             l_psHeaders, CPLSPrintf("x-ms-date: %s", osDate.c_str()));
      94         224 :         if (bIncludeMSVersion)
      95             :         {
      96         217 :             l_psHeaders = curl_slist_append(
      97             :                 l_psHeaders, CPLSPrintf("x-ms-version: %s", X_MS_VERSION));
      98             :         }
      99         224 :         return l_psHeaders;
     100         224 :     };
     101             : 
     102         448 :     std::string osDate = CPLGetConfigOption("CPL_AZURE_TIMESTAMP", "");
     103         224 :     if (osDate.empty())
     104             :     {
     105           7 :         osDate = IVSIS3LikeHandleHelper::GetRFC822DateTime();
     106             :     }
     107             : 
     108         224 :     if (osStorageKeyB64.empty())
     109             :     {
     110          10 :         psHeaders = AddHeaders(psHeaders, osDate);
     111          10 :         return psHeaders;
     112             :     }
     113             : 
     114         428 :     std::map<std::string, std::string> oSortedMapMSHeaders;
     115         214 :     if (bIncludeMSVersion)
     116         207 :         oSortedMapMSHeaders["x-ms-version"] = X_MS_VERSION;
     117         214 :     oSortedMapMSHeaders["x-ms-date"] = osDate;
     118             :     std::string osCanonicalizedHeaders(
     119             :         IVSIS3LikeHandleHelper::BuildCanonicalizedHeaders(oSortedMapMSHeaders,
     120         428 :                                                           psHeaders, "x-ms-"));
     121             : 
     122         428 :     std::string osCanonicalizedResource;
     123         214 :     osCanonicalizedResource += "/" + osStorageAccount;
     124         214 :     osCanonicalizedResource += osResource;
     125             : 
     126             :     // We assume query parameters are in lower case and they are not repeated
     127             :     std::map<std::string, std::string>::const_iterator oIter =
     128         214 :         oMapQueryParameters.begin();
     129         551 :     for (; oIter != oMapQueryParameters.end(); ++oIter)
     130             :     {
     131         337 :         osCanonicalizedResource += "\n";
     132         337 :         osCanonicalizedResource += oIter->first;
     133         337 :         osCanonicalizedResource += ":";
     134         337 :         osCanonicalizedResource += oIter->second;
     135             :     }
     136             : 
     137         428 :     std::string osStringToSign;
     138         214 :     osStringToSign += osVerb + "\n";
     139         214 :     osStringToSign += CPLAWSGetHeaderVal(psHeaders, "Content-Encoding") + "\n";
     140         214 :     osStringToSign += CPLAWSGetHeaderVal(psHeaders, "Content-Language") + "\n";
     141             :     std::string osContentLength(
     142         428 :         CPLAWSGetHeaderVal(psHeaders, "Content-Length"));
     143         214 :     if (osContentLength == "0")
     144          34 :         osContentLength.clear();  // since x-ms-version 2015-02-21
     145         214 :     osStringToSign += osContentLength + "\n";
     146         214 :     osStringToSign += CPLAWSGetHeaderVal(psHeaders, "Content-MD5") + "\n";
     147         214 :     osStringToSign += CPLAWSGetHeaderVal(psHeaders, "Content-Type") + "\n";
     148         214 :     osStringToSign += CPLAWSGetHeaderVal(psHeaders, "Date") + "\n";
     149         214 :     osStringToSign += CPLAWSGetHeaderVal(psHeaders, "If-Modified-Since") + "\n";
     150         214 :     osStringToSign += CPLAWSGetHeaderVal(psHeaders, "If-Match") + "\n";
     151         214 :     osStringToSign += CPLAWSGetHeaderVal(psHeaders, "If-None-Match") + "\n";
     152             :     osStringToSign +=
     153         214 :         CPLAWSGetHeaderVal(psHeaders, "If-Unmodified-Since") + "\n";
     154         214 :     osStringToSign += CPLAWSGetHeaderVal(psHeaders, "Range") + "\n";
     155         214 :     osStringToSign += osCanonicalizedHeaders;
     156         214 :     osStringToSign += osCanonicalizedResource;
     157             : 
     158             : #ifdef DEBUG_VERBOSE
     159             :     CPLDebug("AZURE", "osStringToSign = '%s'", osStringToSign.c_str());
     160             : #endif
     161             : 
     162             :     /* -------------------------------------------------------------------- */
     163             :     /*      Compute signature.                                              */
     164             :     /* -------------------------------------------------------------------- */
     165             : 
     166             :     std::string osAuthorization(
     167         428 :         "SharedKey " + osStorageAccount + ":" +
     168         428 :         CPLAzureGetSignature(osStringToSign, osStorageKeyB64));
     169             : 
     170         214 :     psHeaders = AddHeaders(psHeaders, osDate);
     171         214 :     psHeaders = curl_slist_append(
     172             :         psHeaders, CPLSPrintf("Authorization: %s", osAuthorization.c_str()));
     173             : 
     174         214 :     return psHeaders;
     175             : }
     176             : 
     177             : /************************************************************************/
     178             : /*                      VSIAzureBlobHandleHelper()                      */
     179             : /************************************************************************/
     180         343 : VSIAzureBlobHandleHelper::VSIAzureBlobHandleHelper(
     181             :     const std::string &osPathForOption, const std::string &osEndpoint,
     182             :     const std::string &osBucket, const std::string &osObjectKey,
     183             :     const std::string &osStorageAccount, const std::string &osStorageKey,
     184             :     const std::string &osSAS, const std::string &osAccessToken,
     185         343 :     bool bFromManagedIdentities)
     186             :     : m_osPathForOption(osPathForOption),
     187             :       m_osURL(BuildURL(osEndpoint, osBucket, osObjectKey, osSAS)),
     188             :       m_osEndpoint(osEndpoint), m_osBucket(osBucket),
     189             :       m_osObjectKey(osObjectKey), m_osStorageAccount(osStorageAccount),
     190             :       m_osStorageKey(osStorageKey), m_osSAS(osSAS),
     191             :       m_osAccessToken(osAccessToken),
     192         343 :       m_bFromManagedIdentities(bFromManagedIdentities)
     193             : {
     194         343 : }
     195             : 
     196             : /************************************************************************/
     197             : /*                     ~VSIAzureBlobHandleHelper()                      */
     198             : /************************************************************************/
     199             : 
     200         686 : VSIAzureBlobHandleHelper::~VSIAzureBlobHandleHelper()
     201             : {
     202         686 : }
     203             : 
     204             : /************************************************************************/
     205             : /*                        AzureCSGetParameter()                         */
     206             : /************************************************************************/
     207             : 
     208        1145 : static std::string AzureCSGetParameter(const std::string &osStr,
     209             :                                        const char *pszKey, bool bErrorIfMissing)
     210             : {
     211        3435 :     std::string osKey(pszKey + std::string("="));
     212        1145 :     size_t nPos = osStr.find(osKey);
     213        1145 :     if (nPos == std::string::npos)
     214             :     {
     215             :         const char *pszMsg =
     216          21 :             CPLSPrintf("%s missing in AZURE_STORAGE_CONNECTION_STRING", pszKey);
     217          21 :         if (bErrorIfMissing)
     218             :         {
     219           0 :             CPLDebug("AZURE", "%s", pszMsg);
     220           0 :             VSIError(VSIE_InvalidCredentials, "%s", pszMsg);
     221             :         }
     222          21 :         return std::string();
     223             :     }
     224        1124 :     size_t nPos2 = osStr.find(";", nPos);
     225        1124 :     return osStr.substr(nPos + osKey.size(), nPos2 == std::string::npos
     226             :                                                  ? nPos2
     227        2248 :                                                  : nPos2 - nPos - osKey.size());
     228             : }
     229             : 
     230             : /************************************************************************/
     231             : /*                         CPLAzureCachedToken                          */
     232             : /************************************************************************/
     233             : 
     234             : std::mutex gMutex;
     235             : 
     236             : struct CPLAzureCachedToken
     237             : {
     238             :     std::string osAccessToken{};
     239             :     GIntBig nExpiresOn = 0;
     240             : };
     241             : 
     242             : static std::map<std::string, CPLAzureCachedToken> goMapIMDSURLToCachedToken;
     243             : 
     244             : /************************************************************************/
     245             : /*                GetConfigurationFromIMDSCredentials()                 */
     246             : /************************************************************************/
     247             : 
     248             : static bool
     249          27 : GetConfigurationFromIMDSCredentials(const std::string &osPathForOption,
     250             :                                     std::string &osAccessToken)
     251             : {
     252             :     // coverity[tainted_data]
     253             :     const std::string osRootURL(CPLGetConfigOption("CPL_AZURE_VM_API_ROOT_URL",
     254          54 :                                                    "http://169.254.169.254"));
     255          27 :     if (osRootURL == "disabled")
     256           3 :         return false;
     257             : 
     258             :     std::string osURLResource("/metadata/identity/oauth2/"
     259             :                               "token?api-version=2018-02-01&resource=https%"
     260          48 :                               "3A%2F%2Fstorage.azure.com%2F");
     261          24 :     const char *pszObjectId = VSIGetPathSpecificOption(
     262             :         osPathForOption.c_str(), "AZURE_IMDS_OBJECT_ID", nullptr);
     263          24 :     if (pszObjectId)
     264          12 :         osURLResource += "&object_id=" + CPLAWSURLEncode(pszObjectId, false);
     265          24 :     const char *pszClientId = VSIGetPathSpecificOption(
     266             :         osPathForOption.c_str(), "AZURE_IMDS_CLIENT_ID", nullptr);
     267          24 :     if (pszClientId)
     268          12 :         osURLResource += "&client_id=" + CPLAWSURLEncode(pszClientId, false);
     269          24 :     const char *pszMsiResId = VSIGetPathSpecificOption(
     270             :         osPathForOption.c_str(), "AZURE_IMDS_MSI_RES_ID", nullptr);
     271          24 :     if (pszMsiResId)
     272          12 :         osURLResource += "&msi_res_id=" + CPLAWSURLEncode(pszMsiResId, false);
     273             : 
     274          48 :     std::lock_guard<std::mutex> guard(gMutex);
     275             : 
     276             :     // Look for cached token corresponding to this IMDS request URL
     277          24 :     auto oIter = goMapIMDSURLToCachedToken.find(osURLResource);
     278          24 :     if (oIter != goMapIMDSURLToCachedToken.end())
     279             :     {
     280          20 :         const auto &oCachedToken = oIter->second;
     281             :         time_t nCurTime;
     282          20 :         time(&nCurTime);
     283             :         // Try to reuse credentials if they are still valid, but
     284             :         // keep one minute of margin...
     285          20 :         if (nCurTime < oCachedToken.nExpiresOn - 60)
     286             :         {
     287          17 :             osAccessToken = oCachedToken.osAccessToken;
     288          17 :             return true;
     289             :         }
     290             :     }
     291             : 
     292             :     // Fetch credentials
     293           7 :     CPLStringList oResponse;
     294           7 :     const char *const apszOptions[] = {"HEADERS=Metadata: true", nullptr};
     295             :     CPLHTTPResult *psResult =
     296           7 :         CPLHTTPFetch((osRootURL + osURLResource).c_str(), apszOptions);
     297           7 :     if (psResult)
     298             :     {
     299           7 :         if (psResult->nStatus == 0 && psResult->pabyData != nullptr)
     300             :         {
     301             :             const std::string osJSon =
     302          14 :                 reinterpret_cast<char *>(psResult->pabyData);
     303           7 :             oResponse = CPLParseKeyValueJson(osJSon.c_str());
     304           7 :             if (oResponse.FetchNameValue("error"))
     305             :             {
     306           0 :                 CPLDebug("AZURE",
     307             :                          "Cannot retrieve managed identities credentials: %s",
     308             :                          osJSon.c_str());
     309             :             }
     310             :         }
     311           7 :         CPLHTTPDestroyResult(psResult);
     312             :     }
     313           7 :     osAccessToken = oResponse.FetchNameValueDef("access_token", "");
     314             :     const GIntBig nExpiresOn =
     315           7 :         CPLAtoGIntBig(oResponse.FetchNameValueDef("expires_on", ""));
     316           7 :     if (!osAccessToken.empty() && nExpiresOn > 0)
     317             :     {
     318          14 :         CPLAzureCachedToken cachedToken;
     319           7 :         cachedToken.osAccessToken = osAccessToken;
     320           7 :         cachedToken.nExpiresOn = nExpiresOn;
     321           7 :         goMapIMDSURLToCachedToken[osURLResource] = std::move(cachedToken);
     322           7 :         CPLDebug("AZURE", "Storing credentials for %s until " CPL_FRMT_GIB,
     323             :                  osURLResource.c_str(), nExpiresOn);
     324             :     }
     325             : 
     326           7 :     return !osAccessToken.empty();
     327             : }
     328             : 
     329             : /************************************************************************/
     330             : /*                GetConfigurationFromWorkloadIdentity()                */
     331             : /************************************************************************/
     332             : 
     333             : // Last timestamp AZURE_FEDERATED_TOKEN_FILE was read
     334             : static GIntBig gnLastReadFederatedTokenFile = 0;
     335             : static std::string gosFederatedToken{};
     336             : 
     337             : // Azure Active Directory Workload Identity, typically for Azure Kubernetes
     338             : // Cf https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/identity/azure-identity/azure/identity/_credentials/workload_identity.py
     339          43 : static bool GetConfigurationFromWorkloadIdentity(std::string &osAccessToken)
     340             : {
     341             :     const std::string AZURE_CLIENT_ID(
     342          86 :         CPLGetConfigOption("AZURE_CLIENT_ID", ""));
     343             :     const std::string AZURE_TENANT_ID(
     344          86 :         CPLGetConfigOption("AZURE_TENANT_ID", ""));
     345             :     const std::string AZURE_AUTHORITY_HOST(
     346          86 :         CPLGetConfigOption("AZURE_AUTHORITY_HOST", ""));
     347             :     const std::string AZURE_FEDERATED_TOKEN_FILE(
     348          86 :         CPLGetConfigOption("AZURE_FEDERATED_TOKEN_FILE", ""));
     349          75 :     if (AZURE_CLIENT_ID.empty() || AZURE_TENANT_ID.empty() ||
     350          75 :         AZURE_AUTHORITY_HOST.empty() || AZURE_FEDERATED_TOKEN_FILE.empty())
     351             :     {
     352          27 :         return false;
     353             :     }
     354             : 
     355          32 :     std::lock_guard<std::mutex> guard(gMutex);
     356             : 
     357             :     time_t nCurTime;
     358          16 :     time(&nCurTime);
     359             : 
     360             :     // Look for cached token corresponding to this request URL
     361          16 :     const std::string osURL(AZURE_AUTHORITY_HOST + AZURE_TENANT_ID +
     362          32 :                             "/oauth2/v2.0/token");
     363          16 :     auto oIter = goMapIMDSURLToCachedToken.find(osURL);
     364          16 :     if (oIter != goMapIMDSURLToCachedToken.end())
     365             :     {
     366          14 :         const auto &oCachedToken = oIter->second;
     367             :         // Try to reuse credentials if they are still valid, but
     368             :         // keep one minute of margin...
     369          14 :         if (nCurTime < oCachedToken.nExpiresOn - 60)
     370             :         {
     371          10 :             osAccessToken = oCachedToken.osAccessToken;
     372          10 :             return true;
     373             :         }
     374             :     }
     375             : 
     376             :     // Ingest content of AZURE_FEDERATED_TOKEN_FILE if last time was more than
     377             :     // 600 seconds.
     378           6 :     if (nCurTime - gnLastReadFederatedTokenFile > 600)
     379             :     {
     380             :         auto fp = VSIVirtualHandleUniquePtr(
     381           2 :             VSIFOpenL(AZURE_FEDERATED_TOKEN_FILE.c_str(), "rb"));
     382           2 :         if (!fp)
     383             :         {
     384           0 :             CPLDebug("AZURE", "Cannot open AZURE_FEDERATED_TOKEN_FILE = %s",
     385             :                      AZURE_FEDERATED_TOKEN_FILE.c_str());
     386           0 :             return false;
     387             :         }
     388           2 :         fp->Seek(0, SEEK_END);
     389           2 :         const auto nSize = fp->Tell();
     390           2 :         if (nSize == 0 || nSize > 100 * 1024)
     391             :         {
     392           0 :             CPLDebug(
     393             :                 "AZURE",
     394             :                 "Invalid size for AZURE_FEDERATED_TOKEN_FILE = " CPL_FRMT_GUIB,
     395             :                 static_cast<GUIntBig>(nSize));
     396           0 :             return false;
     397             :         }
     398           2 :         fp->Seek(0, SEEK_SET);
     399           2 :         gosFederatedToken.resize(static_cast<size_t>(nSize));
     400           2 :         if (fp->Read(&gosFederatedToken[0], gosFederatedToken.size(), 1) != 1)
     401             :         {
     402           0 :             CPLDebug("AZURE", "Cannot read AZURE_FEDERATED_TOKEN_FILE");
     403           0 :             return false;
     404             :         }
     405           2 :         gnLastReadFederatedTokenFile = nCurTime;
     406             :     }
     407             : 
     408             :     /* -------------------------------------------------------------------- */
     409             :     /*      Prepare POST request.                                           */
     410             :     /* -------------------------------------------------------------------- */
     411          12 :     CPLStringList aosOptions;
     412             : 
     413             :     aosOptions.AddString(
     414           6 :         "HEADERS=Content-Type: application/x-www-form-urlencoded");
     415             : 
     416          12 :     std::string osItem("POSTFIELDS=client_assertion=");
     417           6 :     osItem += CPLAWSURLEncode(gosFederatedToken);
     418             :     osItem += "&client_assertion_type=urn:ietf:params:oauth:client-assertion-"
     419           6 :               "type:jwt-bearer";
     420           6 :     osItem += "&client_id=";
     421           6 :     osItem += CPLAWSURLEncode(AZURE_CLIENT_ID);
     422           6 :     osItem += "&grant_type=client_credentials";
     423           6 :     osItem += "&scope=https://storage.azure.com/.default";
     424           6 :     aosOptions.AddString(osItem.c_str());
     425             : 
     426             :     /* -------------------------------------------------------------------- */
     427             :     /*      Submit request by HTTP.                                         */
     428             :     /* -------------------------------------------------------------------- */
     429           6 :     CPLHTTPResult *psResult = CPLHTTPFetch(osURL.c_str(), aosOptions.List());
     430           6 :     if (!psResult)
     431           0 :         return false;
     432             : 
     433           6 :     if (!psResult->pabyData || psResult->pszErrBuf)
     434             :     {
     435           0 :         if (psResult->pszErrBuf)
     436           0 :             CPLDebug("AZURE", "%s", psResult->pszErrBuf);
     437           0 :         if (psResult->pabyData)
     438           0 :             CPLDebug("AZURE", "%s", psResult->pabyData);
     439             : 
     440           0 :         CPLDebug("AZURE",
     441             :                  "Fetching OAuth2 access code from workload identity failed.");
     442           0 :         CPLHTTPDestroyResult(psResult);
     443           0 :         return false;
     444             :     }
     445             : 
     446             :     CPLStringList oResponse =
     447           6 :         CPLParseKeyValueJson(reinterpret_cast<char *>(psResult->pabyData));
     448           6 :     CPLHTTPDestroyResult(psResult);
     449             : 
     450           6 :     osAccessToken = oResponse.FetchNameValueDef("access_token", "");
     451           6 :     const int nExpiresIn = atoi(oResponse.FetchNameValueDef("expires_in", ""));
     452           6 :     if (!osAccessToken.empty() && nExpiresIn > 0)
     453             :     {
     454          12 :         CPLAzureCachedToken cachedToken;
     455           6 :         cachedToken.osAccessToken = osAccessToken;
     456           6 :         cachedToken.nExpiresOn = nCurTime + nExpiresIn;
     457           6 :         goMapIMDSURLToCachedToken[osURL] = cachedToken;
     458           6 :         CPLDebug("AZURE", "Storing credentials for %s until " CPL_FRMT_GIB,
     459             :                  osURL.c_str(), cachedToken.nExpiresOn);
     460             :     }
     461             : 
     462           6 :     return !osAccessToken.empty();
     463             : }
     464             : 
     465             : /************************************************************************/
     466             : /*               GetConfigurationFromManagedIdentities()                */
     467             : /************************************************************************/
     468             : 
     469             : static bool
     470          43 : GetConfigurationFromManagedIdentities(const std::string &osPathForOption,
     471             :                                       std::string &osAccessToken)
     472             : {
     473          43 :     if (GetConfigurationFromWorkloadIdentity(osAccessToken))
     474          16 :         return true;
     475          27 :     return GetConfigurationFromIMDSCredentials(osPathForOption, osAccessToken);
     476             : }
     477             : 
     478             : /************************************************************************/
     479             : /*                             ClearCache()                             */
     480             : /************************************************************************/
     481             : 
     482        2830 : void VSIAzureBlobHandleHelper::ClearCache()
     483             : {
     484        5660 :     std::lock_guard<std::mutex> guard(gMutex);
     485        2830 :     goMapIMDSURLToCachedToken.clear();
     486        2830 :     gnLastReadFederatedTokenFile = 0;
     487        2830 :     gosFederatedToken.clear();
     488        2830 : }
     489             : 
     490             : /************************************************************************/
     491             : /*                    ParseStorageConnectionString()                    */
     492             : /************************************************************************/
     493             : 
     494             : static bool
     495         285 : ParseStorageConnectionString(const std::string &osStorageConnectionString,
     496             :                              const std::string &osServicePrefix,
     497             :                              bool &bUseHTTPS, std::string &osEndpoint,
     498             :                              std::string &osStorageAccount,
     499             :                              std::string &osStorageKey, std::string &osSAS)
     500             : {
     501             :     osStorageAccount =
     502         285 :         AzureCSGetParameter(osStorageConnectionString, "AccountName", false);
     503             :     osStorageKey =
     504         285 :         AzureCSGetParameter(osStorageConnectionString, "AccountKey", false);
     505             : 
     506             :     const std::string osProtocol(AzureCSGetParameter(
     507         570 :         osStorageConnectionString, "DefaultEndpointsProtocol", false));
     508         285 :     bUseHTTPS = (osProtocol != "http");
     509             : 
     510         285 :     if (osStorageAccount.empty() || osStorageKey.empty())
     511             :     {
     512           5 :         osStorageAccount.clear();
     513           5 :         osStorageKey.clear();
     514             : 
     515           5 :         std::string osBlobEndpoint = RemoveTrailingSlash(AzureCSGetParameter(
     516          10 :             osStorageConnectionString, "BlobEndpoint", false));
     517          10 :         osSAS = AzureCSGetParameter(osStorageConnectionString,
     518           5 :                                     "SharedAccessSignature", false);
     519           5 :         if (!osBlobEndpoint.empty() && !osSAS.empty())
     520             :         {
     521           2 :             osEndpoint = std::move(osBlobEndpoint);
     522           2 :             return true;
     523             :         }
     524             : 
     525           3 :         return false;
     526             :     }
     527             : 
     528             :     const std::string osBlobEndpoint =
     529         280 :         AzureCSGetParameter(osStorageConnectionString, "BlobEndpoint", false);
     530         280 :     if (!osBlobEndpoint.empty())
     531             :     {
     532         280 :         osEndpoint = RemoveTrailingSlash(osBlobEndpoint);
     533             :     }
     534             :     else
     535             :     {
     536             :         const std::string osEndpointSuffix(AzureCSGetParameter(
     537           0 :             osStorageConnectionString, "EndpointSuffix", false));
     538           0 :         if (!osEndpointSuffix.empty())
     539           0 :             osEndpoint = (bUseHTTPS ? "https://" : "http://") +
     540           0 :                          osStorageAccount + "." + osServicePrefix + "." +
     541           0 :                          RemoveTrailingSlash(osEndpointSuffix);
     542             :     }
     543             : 
     544         280 :     return true;
     545             : }
     546             : 
     547             : /************************************************************************/
     548             : /*                 GetConfigurationFromCLIConfigFile()                  */
     549             : /************************************************************************/
     550             : 
     551          38 : static bool GetConfigurationFromCLIConfigFile(
     552             :     const std::string &osPathForOption, const std::string &osServicePrefix,
     553             :     bool &bUseHTTPS, std::string &osEndpoint, std::string &osStorageAccount,
     554             :     std::string &osStorageKey, std::string &osSAS, std::string &osAccessToken,
     555             :     bool &bFromManagedIdentities)
     556             : {
     557             : #ifdef _WIN32
     558             :     const char *pszHome = CPLGetConfigOption("USERPROFILE", nullptr);
     559             :     constexpr char SEP_STRING[] = "\\";
     560             : #else
     561          38 :     const char *pszHome = CPLGetConfigOption("HOME", nullptr);
     562          38 :     constexpr char SEP_STRING[] = "/";
     563             : #endif
     564             : 
     565          76 :     std::string osDotAzure(pszHome ? pszHome : "");
     566          38 :     osDotAzure += SEP_STRING;
     567          38 :     osDotAzure += ".azure";
     568             : 
     569             :     const char *pszAzureConfigDir =
     570          38 :         CPLGetConfigOption("AZURE_CONFIG_DIR", osDotAzure.c_str());
     571          38 :     if (pszAzureConfigDir[0] == '\0')
     572           7 :         return false;
     573             : 
     574          62 :     std::string osConfigFilename = pszAzureConfigDir;
     575          31 :     osConfigFilename += SEP_STRING;
     576          31 :     osConfigFilename += "config";
     577             : 
     578          31 :     VSILFILE *fp = VSIFOpenL(osConfigFilename.c_str(), "rb");
     579          62 :     std::string osStorageConnectionString;
     580          31 :     if (fp == nullptr)
     581          19 :         return false;
     582             : 
     583          12 :     bool bInStorageSection = false;
     584          75 :     while (const char *pszLine = CPLReadLineL(fp))
     585             :     {
     586          63 :         if (pszLine[0] == '#' || pszLine[0] == ';')
     587             :         {
     588             :             // comment line
     589             :         }
     590          63 :         else if (strcmp(pszLine, "[storage]") == 0)
     591             :         {
     592          12 :             bInStorageSection = true;
     593             :         }
     594          51 :         else if (pszLine[0] == '[')
     595             :         {
     596          12 :             bInStorageSection = false;
     597             :         }
     598          39 :         else if (bInStorageSection)
     599             :         {
     600          15 :             char *pszKey = nullptr;
     601          15 :             const char *pszValue = CPLParseNameValue(pszLine, &pszKey);
     602          15 :             if (pszKey && pszValue)
     603             :             {
     604          15 :                 if (EQUAL(pszKey, "account"))
     605             :                 {
     606           6 :                     osStorageAccount = pszValue;
     607             :                 }
     608           9 :                 else if (EQUAL(pszKey, "connection_string"))
     609             :                 {
     610           3 :                     osStorageConnectionString = pszValue;
     611             :                 }
     612           6 :                 else if (EQUAL(pszKey, "key"))
     613             :                 {
     614           3 :                     osStorageKey = pszValue;
     615             :                 }
     616           3 :                 else if (EQUAL(pszKey, "sas_token"))
     617             :                 {
     618           3 :                     osSAS = pszValue;
     619             :                     // Az CLI apparently uses configparser with
     620             :                     // BasicInterpolation where the % character has a special
     621             :                     // meaning See
     622             :                     // https://docs.python.org/3/library/configparser.html#configparser.BasicInterpolation
     623             :                     // A token might end with %%3D which must be transformed to
     624             :                     // %3D
     625           3 :                     osSAS = CPLString(osSAS).replaceAll("%%", '%');
     626             :                 }
     627             :             }
     628          15 :             CPLFree(pszKey);
     629             :         }
     630          63 :     }
     631          12 :     VSIFCloseL(fp);
     632             : 
     633          12 :     if (!osStorageConnectionString.empty())
     634             :     {
     635           3 :         return ParseStorageConnectionString(
     636             :             osStorageConnectionString, osServicePrefix, bUseHTTPS, osEndpoint,
     637           3 :             osStorageAccount, osStorageKey, osSAS);
     638             :     }
     639             : 
     640           9 :     if (osStorageAccount.empty())
     641             :     {
     642           3 :         CPLDebug("AZURE", "Missing storage.account in %s",
     643             :                  osConfigFilename.c_str());
     644           3 :         return false;
     645             :     }
     646             : 
     647           6 :     if (osEndpoint.empty())
     648           0 :         osEndpoint = (bUseHTTPS ? "https://" : "http://") + osStorageAccount +
     649           0 :                      "." + osServicePrefix + ".core.windows.net";
     650             : 
     651           6 :     osAccessToken = CPLGetConfigOption("AZURE_STORAGE_ACCESS_TOKEN", "");
     652           6 :     if (!osAccessToken.empty())
     653           0 :         return true;
     654             : 
     655           6 :     if (osStorageKey.empty() && osSAS.empty())
     656             :     {
     657           0 :         if (CPLTestBool(CPLGetConfigOption("AZURE_NO_SIGN_REQUEST", "NO")))
     658             :         {
     659           0 :             return true;
     660             :         }
     661             : 
     662           0 :         std::string osTmpAccessToken;
     663           0 :         if (GetConfigurationFromManagedIdentities(osPathForOption,
     664             :                                                   osTmpAccessToken))
     665             :         {
     666           0 :             bFromManagedIdentities = true;
     667           0 :             return true;
     668             :         }
     669             : 
     670           0 :         CPLDebug("AZURE", "Missing storage.key or storage.sas_token in %s",
     671             :                  osConfigFilename.c_str());
     672           0 :         return false;
     673             :     }
     674             : 
     675           6 :     return true;
     676             : }
     677             : 
     678             : /************************************************************************/
     679             : /*                               GetSAS()                               */
     680             : /************************************************************************/
     681             : 
     682             : /* static */
     683          55 : std::string VSIAzureBlobHandleHelper::GetSAS(const char *pszFilename)
     684             : {
     685             :     return VSIGetPathSpecificOption(
     686             :         pszFilename, "AZURE_STORAGE_SAS_TOKEN",
     687             :         CPLGetConfigOption("AZURE_SAS",
     688          55 :                            ""));  // AZURE_SAS for GDAL < 3.5
     689             : }
     690             : 
     691             : /************************************************************************/
     692             : /*                          IsNoSignRequest()                           */
     693             : /************************************************************************/
     694             : 
     695             : /* static */
     696         388 : bool VSIAzureBlobHandleHelper::IsNoSignRequest(const char *pszFilename)
     697             : {
     698         388 :     return CPLTestBool(
     699         388 :         VSIGetPathSpecificOption(pszFilename, "AZURE_NO_SIGN_REQUEST", "NO"));
     700             : }
     701             : 
     702             : /************************************************************************/
     703             : /*                          GetConfiguration()                          */
     704             : /************************************************************************/
     705             : 
     706         378 : bool VSIAzureBlobHandleHelper::GetConfiguration(
     707             :     const std::string &osPathForOption, CSLConstList papszOptions,
     708             :     Service eService, bool &bUseHTTPS, std::string &osEndpoint,
     709             :     std::string &osStorageAccount, std::string &osStorageKey,
     710             :     std::string &osSAS, std::string &osAccessToken,
     711             :     bool &bFromManagedIdentities)
     712             : {
     713         378 :     bFromManagedIdentities = false;
     714             : 
     715             :     const std::string osServicePrefix(
     716         756 :         eService == Service::SERVICE_BLOB ? "blob" : "dfs");
     717         378 :     bUseHTTPS = CPLTestBool(VSIGetPathSpecificOption(
     718             :         osPathForOption.c_str(), "CPL_AZURE_USE_HTTPS", "YES"));
     719         756 :     osEndpoint = RemoveTrailingSlash(VSIGetPathSpecificOption(
     720         378 :         osPathForOption.c_str(), "CPL_AZURE_ENDPOINT", ""));
     721             : 
     722             :     const std::string osStorageConnectionString(CSLFetchNameValueDef(
     723             :         papszOptions, "AZURE_STORAGE_CONNECTION_STRING",
     724             :         VSIGetPathSpecificOption(osPathForOption.c_str(),
     725         756 :                                  "AZURE_STORAGE_CONNECTION_STRING", "")));
     726         378 :     if (!osStorageConnectionString.empty())
     727             :     {
     728         282 :         return ParseStorageConnectionString(
     729             :             osStorageConnectionString, osServicePrefix, bUseHTTPS, osEndpoint,
     730         282 :             osStorageAccount, osStorageKey, osSAS);
     731             :     }
     732             :     else
     733             :     {
     734             :         osStorageAccount = CSLFetchNameValueDef(
     735             :             papszOptions, "AZURE_STORAGE_ACCOUNT",
     736             :             VSIGetPathSpecificOption(osPathForOption.c_str(),
     737          96 :                                      "AZURE_STORAGE_ACCOUNT", ""));
     738          96 :         if (!osStorageAccount.empty())
     739             :         {
     740          58 :             if (osEndpoint.empty())
     741          46 :                 osEndpoint = (bUseHTTPS ? "https://" : "http://") +
     742          46 :                              osStorageAccount + "." + osServicePrefix +
     743          23 :                              ".core.windows.net";
     744             : 
     745             :             osAccessToken = CSLFetchNameValueDef(
     746             :                 papszOptions, "AZURE_STORAGE_ACCESS_TOKEN",
     747             :                 VSIGetPathSpecificOption(osPathForOption.c_str(),
     748          58 :                                          "AZURE_STORAGE_ACCESS_TOKEN", ""));
     749          58 :             if (!osAccessToken.empty())
     750           3 :                 return true;
     751             : 
     752             :             osStorageKey = CSLFetchNameValueDef(
     753             :                 papszOptions, "AZURE_STORAGE_ACCESS_KEY",
     754             :                 VSIGetPathSpecificOption(osPathForOption.c_str(),
     755          55 :                                          "AZURE_STORAGE_ACCESS_KEY", ""));
     756          55 :             if (osStorageKey.empty())
     757             :             {
     758          49 :                 osSAS = GetSAS(osPathForOption.c_str());
     759          49 :                 if (osSAS.empty())
     760             :                 {
     761          39 :                     if (IsNoSignRequest(osPathForOption.c_str()))
     762             :                     {
     763           7 :                         return true;
     764             :                     }
     765             : 
     766          64 :                     std::string osTmpAccessToken;
     767          32 :                     if (GetConfigurationFromManagedIdentities(osPathForOption,
     768             :                                                               osTmpAccessToken))
     769             :                     {
     770          29 :                         bFromManagedIdentities = true;
     771          29 :                         return true;
     772             :                     }
     773             : 
     774           3 :                     const char *pszMsg =
     775             :                         "AZURE_STORAGE_ACCESS_KEY or AZURE_STORAGE_SAS_TOKEN "
     776             :                         "or AZURE_NO_SIGN_REQUEST configuration option "
     777             :                         "not defined";
     778           3 :                     CPLDebug("AZURE", "%s", pszMsg);
     779           3 :                     VSIError(VSIE_InvalidCredentials, "%s", pszMsg);
     780           3 :                     return false;
     781             :                 }
     782             :             }
     783          16 :             return true;
     784             :         }
     785             :     }
     786             : 
     787          38 :     if (GetConfigurationFromCLIConfigFile(
     788             :             osPathForOption, osServicePrefix, bUseHTTPS, osEndpoint,
     789             :             osStorageAccount, osStorageKey, osSAS, osAccessToken,
     790             :             bFromManagedIdentities))
     791             :     {
     792           9 :         return true;
     793             :     }
     794             : 
     795          29 :     const char *pszMsg =
     796             :         "No valid Azure credentials found. "
     797             :         "For authenticated requests, you need to set "
     798             :         "AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_ACCESS_KEY, "
     799             :         "AZURE_STORAGE_SAS_TOKEN, "
     800             :         "AZURE_STORAGE_CONNECTION_STRING, or other configuration "
     801             :         "options. Consult "
     802             :         "https://gdal.org/en/stable/user/"
     803             :         "virtual_file_systems.html#vsiaz-microsoft-azure-blob-files "
     804             :         "for more details. "
     805             :         "For unauthenticated requests on public resources, set the "
     806             :         "AZURE_NO_SIGN_REQUEST configuration option to YES.";
     807          29 :     CPLDebug("AZURE", "%s", pszMsg);
     808          29 :     VSIError(VSIE_InvalidCredentials, "%s", pszMsg);
     809          29 :     return false;
     810             : }
     811             : 
     812             : /************************************************************************/
     813             : /*                            BuildFromURI()                            */
     814             : /************************************************************************/
     815             : 
     816         378 : VSIAzureBlobHandleHelper *VSIAzureBlobHandleHelper::BuildFromURI(
     817             :     const char *pszURI, const char *pszFSPrefix,
     818             :     const char *pszURIForPathSpecificOption, CSLConstList papszOptions)
     819             : {
     820         378 :     if (strcmp(pszFSPrefix, "/vsiaz/") != 0 &&
     821         107 :         strcmp(pszFSPrefix, "/vsiaz_streaming/") != 0 &&
     822          95 :         strcmp(pszFSPrefix, "/vsiadls/") != 0)
     823             :     {
     824           0 :         CPLError(CE_Failure, CPLE_AppDefined, "Unsupported FS prefix");
     825           0 :         return nullptr;
     826             :     }
     827             : 
     828         756 :     const auto eService = strcmp(pszFSPrefix, "/vsiaz/") == 0 ||
     829         107 :                                   strcmp(pszFSPrefix, "/vsiaz_streaming/") == 0
     830         378 :                               ? Service::SERVICE_BLOB
     831             :                               : Service::SERVICE_ADLS;
     832             : 
     833             :     std::string osPathForOption(
     834         756 :         eService == Service::SERVICE_BLOB ? "/vsiaz/" : "/vsiadls/");
     835             :     osPathForOption +=
     836         378 :         pszURIForPathSpecificOption ? pszURIForPathSpecificOption : pszURI;
     837             : 
     838         378 :     bool bUseHTTPS = true;
     839         756 :     std::string osStorageAccount;
     840         756 :     std::string osStorageKey;
     841         756 :     std::string osEndpoint;
     842         756 :     std::string osSAS;
     843         756 :     std::string osAccessToken;
     844         378 :     bool bFromManagedIdentities = false;
     845             : 
     846         378 :     if (!GetConfiguration(osPathForOption, papszOptions, eService, bUseHTTPS,
     847             :                           osEndpoint, osStorageAccount, osStorageKey, osSAS,
     848             :                           osAccessToken, bFromManagedIdentities))
     849             :     {
     850          35 :         return nullptr;
     851             :     }
     852             : 
     853         343 :     if (IsNoSignRequest(osPathForOption.c_str()))
     854             :     {
     855          15 :         osStorageKey.clear();
     856          15 :         osSAS.clear();
     857          15 :         osAccessToken.clear();
     858             :     }
     859             : 
     860             :     // pszURI == bucket/object
     861         686 :     const std::string osBucketObject(pszURI);
     862         686 :     std::string osBucket(osBucketObject);
     863         343 :     std::string osObjectKey;
     864         343 :     size_t nSlashPos = osBucketObject.find('/');
     865         343 :     if (nSlashPos != std::string::npos)
     866             :     {
     867         253 :         osBucket = osBucketObject.substr(0, nSlashPos);
     868         253 :         osObjectKey = osBucketObject.substr(nSlashPos + 1);
     869             :     }
     870             : 
     871             :     return new VSIAzureBlobHandleHelper(
     872             :         osPathForOption, osEndpoint, osBucket, osObjectKey, osStorageAccount,
     873         343 :         osStorageKey, osSAS, osAccessToken, bFromManagedIdentities);
     874             : }
     875             : 
     876             : /************************************************************************/
     877             : /*                              BuildURL()                              */
     878             : /************************************************************************/
     879             : 
     880         991 : std::string VSIAzureBlobHandleHelper::BuildURL(const std::string &osEndpoint,
     881             :                                                const std::string &osBucket,
     882             :                                                const std::string &osObjectKey,
     883             :                                                const std::string &osSAS)
     884             : {
     885         991 :     std::string osURL = osEndpoint;
     886         991 :     osURL += "/";
     887         991 :     osURL += CPLAWSURLEncode(osBucket, false);
     888         991 :     if (!osObjectKey.empty())
     889         493 :         osURL += "/" + CPLAWSURLEncode(osObjectKey, false);
     890         991 :     if (!osSAS.empty())
     891          15 :         osURL += '?' + osSAS;
     892         991 :     return osURL;
     893             : }
     894             : 
     895             : /************************************************************************/
     896             : /*                             RebuildURL()                             */
     897             : /************************************************************************/
     898             : 
     899         648 : void VSIAzureBlobHandleHelper::RebuildURL()
     900             : {
     901         648 :     m_osURL = BuildURL(m_osEndpoint, m_osBucket, m_osObjectKey, std::string());
     902         648 :     m_osURL += GetQueryString(false);
     903         648 :     if (!m_osSAS.empty())
     904          12 :         m_osURL += (m_oMapQueryParameters.empty() ? '?' : '&') + m_osSAS;
     905         648 : }
     906             : 
     907             : /************************************************************************/
     908             : /*                         GetSASQueryString()                          */
     909             : /************************************************************************/
     910             : 
     911          73 : std::string VSIAzureBlobHandleHelper::GetSASQueryString() const
     912             : {
     913          73 :     if (!m_osSAS.empty())
     914           4 :         return '?' + m_osSAS;
     915          69 :     return std::string();
     916             : }
     917             : 
     918             : /************************************************************************/
     919             : /*                           GetCurlHeaders()                           */
     920             : /************************************************************************/
     921             : 
     922             : struct curl_slist *
     923         236 : VSIAzureBlobHandleHelper::GetCurlHeaders(const std::string &osVerb,
     924             :                                          struct curl_slist *psHeaders,
     925             :                                          const void *, size_t) const
     926             : {
     927         236 :     if (m_bFromManagedIdentities || !m_osAccessToken.empty())
     928             :     {
     929          12 :         psHeaders = curl_slist_append(
     930             :             psHeaders,
     931          24 :             std::string("x-ms-version: ").append(X_MS_VERSION).c_str());
     932             : 
     933          24 :         std::string osAccessToken;
     934          12 :         if (m_bFromManagedIdentities)
     935             :         {
     936          11 :             if (!GetConfigurationFromManagedIdentities(m_osPathForOption,
     937             :                                                        osAccessToken))
     938           0 :                 return psHeaders;
     939             :         }
     940             :         else
     941             :         {
     942           1 :             osAccessToken = m_osAccessToken;
     943             :         }
     944             : 
     945             :         // Do not use CPLSPrintf() as we could get over the 8K character limit
     946             :         // with very large SAS tokens
     947          12 :         std::string osAuthorization = "Authorization: Bearer ";
     948          12 :         osAuthorization += osAccessToken;
     949          12 :         psHeaders = curl_slist_append(psHeaders, osAuthorization.c_str());
     950          12 :         return psHeaders;
     951             :     }
     952             : 
     953         448 :     std::string osResource;
     954         224 :     const auto nSlashSlashPos = m_osEndpoint.find("//");
     955         224 :     if (nSlashSlashPos != std::string::npos)
     956             :     {
     957         224 :         const auto nResourcePos = m_osEndpoint.find('/', nSlashSlashPos + 2);
     958         224 :         if (nResourcePos != std::string::npos)
     959         217 :             osResource = m_osEndpoint.substr(nResourcePos);
     960             :     }
     961         224 :     osResource += "/" + m_osBucket;
     962         224 :     if (!m_osObjectKey.empty())
     963         141 :         osResource += "/" + CPLAWSURLEncode(m_osObjectKey, false);
     964             : 
     965             :     // If accessing a Microsoft Azure account from an Azure VM, check that
     966             :     // Microsoft is still a sponsor, and if not, make some (kind) noise.
     967           0 :     if ((m_bFromManagedIdentities &&
     968           0 :          m_osEndpoint.find("core.windows.net") != std::string::npos)
     969             : #ifdef DEBUG
     970         224 :         || CPLTestBool(CPLGetConfigOption("GDAL_TEST_NAME_AND_SHAME", "NO"))
     971             : #endif
     972             :     )
     973             :     {
     974           0 :         static const bool bCheckSponsoring = []()
     975             :         {
     976           0 :             if (!CPLTestBool(CPLGetConfigOption("GDAL_NAME_AND_SHAME", "YES")))
     977           0 :                 return true;
     978             : 
     979           0 :             const std::string osCacheDir = []()
     980             :             {
     981             : #ifdef _WIN32
     982             :                 const char *pszHome =
     983             :                     CPLGetConfigOption("USERPROFILE", nullptr);
     984             : #else
     985           0 :                 const char *pszHome = CPLGetConfigOption("HOME", nullptr);
     986             : #endif
     987           0 :                 if (pszHome != nullptr)
     988             :                 {
     989           0 :                     return CPLFormFilenameSafe(pszHome, ".gdal", nullptr);
     990             :                 }
     991             :                 else
     992             :                 {
     993           0 :                     const char *pszDir = CPLGetConfigOption("TEMP", "/tmp");
     994             :                     VSIStatBufL sStat;
     995           0 :                     if (VSIStatL(pszDir, &sStat) == 0)
     996             :                     {
     997             :                         const char *pszUsername =
     998           0 :                             CPLGetConfigOption("USERNAME", nullptr);
     999           0 :                         if (pszUsername == nullptr)
    1000           0 :                             pszUsername = CPLGetConfigOption("USER", nullptr);
    1001             : 
    1002           0 :                         if (pszUsername != nullptr)
    1003             :                         {
    1004             :                             return CPLFormFilenameSafe(
    1005             :                                 pszDir, CPLSPrintf(".gdal_%s", pszUsername),
    1006           0 :                                 nullptr);
    1007             :                         }
    1008             :                     }
    1009             :                 }
    1010           0 :                 return std::string();
    1011           0 :             }();
    1012           0 :             if (!osCacheDir.empty())
    1013             :             {
    1014             :                 VSIStatBufL sStat;
    1015           0 :                 if (VSIStatL(osCacheDir.c_str(), &sStat) != 0)
    1016           0 :                     VSIMkdir(osCacheDir.c_str(), 0755);
    1017             :                 const std::string osCloudCheck = CPLFormFilenameSafe(
    1018           0 :                     osCacheDir.c_str(), "cloud_check_ms.txt", nullptr);
    1019             :                 // Sidereal day, why not? "Aim for the stars, expect dust"
    1020           0 :                 constexpr int ONE_DAY_IN_SECS = 86164;
    1021           0 :                 if (VSIStatL(osCloudCheck.c_str(), &sStat) == 0 &&
    1022           0 :                     sStat.st_mtime + ONE_DAY_IN_SECS >= time(nullptr))
    1023             :                 {
    1024           0 :                     CPLDebugOnly("GDAL", "%s checked", osCloudCheck.c_str());
    1025             :                 }
    1026             :                 else
    1027             :                 {
    1028           0 :                     FILE *f = fopen(osCloudCheck.c_str(), "wb");
    1029           0 :                     if (f)
    1030           0 :                         fclose(f);
    1031             : 
    1032           0 :                     const auto PingURL = [](const char *pszURL)
    1033             :                     {
    1034           0 :                         CPLErrorStateBackuper oBackuper(CPLQuietErrorHandler);
    1035           0 :                         const char *const apszOptions[] = {
    1036             :                             "CUSTOMREQUEST=HEAD", "TIMEOUT=1", nullptr};
    1037           0 :                         auto res = CPLHTTPFetch(pszURL, apszOptions);
    1038           0 :                         const bool bOK = res && !res->pszErrBuf;
    1039           0 :                         CPLHTTPDestroyResult(res);
    1040           0 :                         return bOK;
    1041             :                     };
    1042           0 :                     if (!PingURL("https://gdal.org/en/latest/sponsors/"
    1043           0 :                                  "did_microsoft_sponsor.html") &&
    1044             :                         // check that gdal.org is responding to avoid false positive
    1045           0 :                         PingURL("https://gdal.org/en/latest/index.html"))
    1046             :                     {
    1047           0 :                         const auto CPLE_NonCooperativeSponsor = CPLE_AppDefined;
    1048           0 :                         CPLError(
    1049             :                             CE_Warning, CPLE_NonCooperativeSponsor,
    1050             :                             "Due to lack of resources, Azure Cloud Storage "
    1051             :                             "access is undergoing minimal maintenance and may "
    1052             :                             "be removed in the future unless Microsoft Azure "
    1053             :                             "re-evaluates its decision to stop sponsoring "
    1054             :                             "GDAL. If you are interested in keeping this "
    1055             :                             "functionality please get in touch with your "
    1056             :                             "Microsoft Azure representative.");
    1057             :                     }
    1058             :                 }
    1059             :             }
    1060           0 :             return true;
    1061           0 :         }();
    1062           0 :         CPL_IGNORE_RET_VAL(bCheckSponsoring);
    1063             :     }
    1064             : 
    1065         448 :     return GetAzureBlobHeaders(osVerb, psHeaders, osResource,
    1066         224 :                                m_oMapQueryParameters, m_osStorageAccount,
    1067         224 :                                m_osStorageKey, m_bIncludeMSVersion);
    1068             : }
    1069             : 
    1070             : /************************************************************************/
    1071             : /*                         CanRestartOnError()                          */
    1072             : /************************************************************************/
    1073             : 
    1074          18 : bool VSIAzureBlobHandleHelper::CanRestartOnError(const char *pszErrorMsg,
    1075             :                                                  const char *pszHeaders,
    1076             :                                                  bool bSetError)
    1077             : {
    1078          18 :     if (pszErrorMsg[0] == '\xEF' && pszErrorMsg[1] == '\xBB' &&
    1079           2 :         pszErrorMsg[2] == '\xBF')
    1080           2 :         pszErrorMsg += 3;
    1081             : 
    1082             : #ifdef DEBUG_VERBOSE
    1083             :     CPLDebug("AZURE", "%s", pszErrorMsg);
    1084             :     CPLDebug("AZURE", "%s", pszHeaders ? pszHeaders : "");
    1085             : #endif
    1086             : 
    1087          18 :     if (STARTS_WITH(pszErrorMsg, "HTTP/") && pszHeaders &&
    1088          16 :         STARTS_WITH(pszHeaders, "HTTP/"))
    1089             :     {
    1090          16 :         if (bSetError)
    1091             :         {
    1092          32 :             std::string osMessage;
    1093          32 :             std::string osTmpMessage(pszHeaders);
    1094          16 :             auto nPos = osTmpMessage.find(' ');
    1095          16 :             if (nPos != std::string::npos)
    1096             :             {
    1097          16 :                 nPos = osTmpMessage.find(' ', nPos + 1);
    1098          16 :                 if (nPos != std::string::npos)
    1099             :                 {
    1100          16 :                     auto nPos2 = osTmpMessage.find('\r', nPos + 1);
    1101          16 :                     if (nPos2 != std::string::npos)
    1102             :                         osMessage =
    1103          16 :                             osTmpMessage.substr(nPos + 1, nPos2 - nPos - 1);
    1104             :                 }
    1105             :             }
    1106          16 :             if (strstr(pszHeaders, "x-ms-error-code: BlobNotFound") ||  // vsiaz
    1107          16 :                 strstr(pszHeaders, "x-ms-error-code: PathNotFound")  // vsiadls
    1108             :             )
    1109             :             {
    1110           0 :                 VSIError(VSIE_ObjectNotFound, "%s", osMessage.c_str());
    1111             :             }
    1112          16 :             else if (strstr(pszHeaders,
    1113          16 :                             "x-ms-error-code: InvalidAuthenticationInfo") ||
    1114          16 :                      strstr(pszHeaders,
    1115             :                             "x-ms-error-code: AuthenticationFailed"))
    1116             :             {
    1117           1 :                 VSIError(VSIE_InvalidCredentials, "%s", osMessage.c_str());
    1118             :             }
    1119             :             // /vsiadls
    1120          15 :             else if (strstr(pszHeaders, "x-ms-error-code: FilesystemNotFound"))
    1121             :             {
    1122           0 :                 VSIError(VSIE_BucketNotFound, "%s", osMessage.c_str());
    1123             :             }
    1124             :             else
    1125             :             {
    1126          15 :                 CPLDebug("AZURE", "%s", pszHeaders);
    1127             :             }
    1128             :         }
    1129          16 :         return false;
    1130             :     }
    1131             : 
    1132           2 :     if (!STARTS_WITH(pszErrorMsg, "<?xml") &&
    1133           0 :         !STARTS_WITH(pszErrorMsg, "<Error>"))
    1134             :     {
    1135           0 :         if (bSetError)
    1136             :         {
    1137           0 :             VSIError(VSIE_ObjectStorageGenericError,
    1138             :                      "Invalid Azure response: %s", pszErrorMsg);
    1139             :         }
    1140           0 :         return false;
    1141             :     }
    1142             : 
    1143           4 :     auto psTree = CPLXMLTreeCloser(CPLParseXMLString(pszErrorMsg));
    1144           2 :     if (psTree == nullptr)
    1145             :     {
    1146           0 :         if (bSetError)
    1147             :         {
    1148           0 :             VSIError(VSIE_ObjectStorageGenericError,
    1149             :                      "Malformed Azure XML response: %s", pszErrorMsg);
    1150             :         }
    1151           0 :         return false;
    1152             :     }
    1153             : 
    1154           2 :     const char *pszCode = CPLGetXMLValue(psTree.get(), "=Error.Code", nullptr);
    1155           2 :     if (pszCode == nullptr)
    1156             :     {
    1157           0 :         if (bSetError)
    1158             :         {
    1159           0 :             VSIError(VSIE_ObjectStorageGenericError,
    1160             :                      "Malformed Azure XML response: %s", pszErrorMsg);
    1161             :         }
    1162           0 :         return false;
    1163             :     }
    1164             : 
    1165           2 :     if (bSetError)
    1166             :     {
    1167             :         // Translate AWS errors into VSI errors.
    1168             :         const char *pszMessage =
    1169           2 :             CPLGetXMLValue(psTree.get(), "=Error.Message", nullptr);
    1170           4 :         std::string osMessage;
    1171           2 :         if (pszMessage)
    1172             :         {
    1173           2 :             osMessage = pszMessage;
    1174           2 :             const auto nPos = osMessage.find("\nRequestId:");
    1175           2 :             if (nPos != std::string::npos)
    1176           2 :                 osMessage.resize(nPos);
    1177             :         }
    1178             : 
    1179           2 :         if (pszMessage == nullptr)
    1180             :         {
    1181           0 :             VSIError(VSIE_ObjectStorageGenericError, "%s", pszErrorMsg);
    1182             :         }
    1183           2 :         else if (EQUAL(pszCode, "ContainerNotFound"))
    1184             :         {
    1185           0 :             VSIError(VSIE_BucketNotFound, "%s", osMessage.c_str());
    1186             :         }
    1187             :         else
    1188             :         {
    1189           2 :             VSIError(VSIE_ObjectStorageGenericError, "%s: %s", pszCode,
    1190             :                      pszMessage);
    1191             :         }
    1192             :     }
    1193             : 
    1194           2 :     return false;
    1195             : }
    1196             : 
    1197             : /************************************************************************/
    1198             : /*                            GetSignedURL()                            */
    1199             : /************************************************************************/
    1200             : 
    1201           7 : std::string VSIAzureBlobHandleHelper::GetSignedURL(CSLConstList papszOptions)
    1202             : {
    1203           7 :     if (m_osStorageKey.empty())
    1204           3 :         return m_osURL;
    1205             : 
    1206           8 :     std::string osStartDate(CPLGetAWS_SIGN4_Timestamp(time(nullptr)));
    1207           4 :     const char *pszStartDate = CSLFetchNameValue(papszOptions, "START_DATE");
    1208           4 :     if (pszStartDate)
    1209           2 :         osStartDate = pszStartDate;
    1210           4 :     int nYear, nMonth, nDay, nHour = 0, nMin = 0, nSec = 0;
    1211           4 :     if (sscanf(osStartDate.c_str(), "%04d%02d%02dT%02d%02d%02dZ", &nYear,
    1212           4 :                &nMonth, &nDay, &nHour, &nMin, &nSec) < 3)
    1213             :     {
    1214           0 :         return std::string();
    1215             :     }
    1216             :     osStartDate = CPLSPrintf("%04d-%02d-%02dT%02d:%02d:%02dZ", nYear, nMonth,
    1217           4 :                              nDay, nHour, nMin, nSec);
    1218             : 
    1219             :     struct tm brokendowntime;
    1220           4 :     brokendowntime.tm_year = nYear - 1900;
    1221           4 :     brokendowntime.tm_mon = nMonth - 1;
    1222           4 :     brokendowntime.tm_mday = nDay;
    1223           4 :     brokendowntime.tm_hour = nHour;
    1224           4 :     brokendowntime.tm_min = nMin;
    1225           4 :     brokendowntime.tm_sec = nSec;
    1226           4 :     GIntBig nStartDate = CPLYMDHMSToUnixTime(&brokendowntime);
    1227             :     GIntBig nEndDate =
    1228             :         nStartDate +
    1229           4 :         atoi(CSLFetchNameValueDef(papszOptions, "EXPIRATION_DELAY", "3600"));
    1230           4 :     CPLUnixTimeToYMDHMS(nEndDate, &brokendowntime);
    1231           4 :     nYear = brokendowntime.tm_year + 1900;
    1232           4 :     nMonth = brokendowntime.tm_mon + 1;
    1233           4 :     nDay = brokendowntime.tm_mday;
    1234           4 :     nHour = brokendowntime.tm_hour;
    1235           4 :     nMin = brokendowntime.tm_min;
    1236           4 :     nSec = brokendowntime.tm_sec;
    1237             :     std::string osEndDate = CPLSPrintf("%04d-%02d-%02dT%02d:%02d:%02dZ", nYear,
    1238           8 :                                        nMonth, nDay, nHour, nMin, nSec);
    1239             : 
    1240           8 :     std::string osVerb(CSLFetchNameValueDef(papszOptions, "VERB", "GET"));
    1241             :     std::string osSignedPermissions(CSLFetchNameValueDef(
    1242             :         papszOptions, "SIGNEDPERMISSIONS",
    1243           4 :         (EQUAL(osVerb.c_str(), "GET") || EQUAL(osVerb.c_str(), "HEAD")) ? "r"
    1244          12 :                                                                         : "w"));
    1245             : 
    1246             :     std::string osSignedIdentifier(
    1247           8 :         CSLFetchNameValueDef(papszOptions, "SIGNEDIDENTIFIER", ""));
    1248             : 
    1249           8 :     const std::string osSignedVersion("2020-12-06");
    1250           8 :     const std::string osSignedProtocol("https");
    1251           8 :     const std::string osSignedResource("b");  // blob
    1252             : 
    1253           8 :     std::string osCanonicalizedResource("/blob/");
    1254           4 :     osCanonicalizedResource += CPLAWSURLEncode(m_osStorageAccount, false);
    1255           4 :     osCanonicalizedResource += '/';
    1256           4 :     osCanonicalizedResource += CPLAWSURLEncode(m_osBucket, false);
    1257           4 :     osCanonicalizedResource += '/';
    1258           4 :     osCanonicalizedResource += CPLAWSURLEncode(m_osObjectKey, false);
    1259             : 
    1260             :     // Cf https://learn.microsoft.com/en-us/rest/api/storageservices/create-service-sas
    1261           8 :     std::string osStringToSign;
    1262           4 :     osStringToSign += osSignedPermissions + "\n";
    1263           4 :     osStringToSign += osStartDate + "\n";
    1264           4 :     osStringToSign += osEndDate + "\n";
    1265           4 :     osStringToSign += osCanonicalizedResource + "\n";
    1266           4 :     osStringToSign += osSignedIdentifier + "\n";
    1267           4 :     osStringToSign += "\n";  // signedIP
    1268           4 :     osStringToSign += osSignedProtocol + "\n";
    1269           4 :     osStringToSign += osSignedVersion + "\n";
    1270           4 :     osStringToSign += osSignedResource + "\n";
    1271           4 :     osStringToSign += "\n";  // signedSnapshotTime
    1272           4 :     osStringToSign += "\n";  // signedEncryptionScope
    1273           4 :     osStringToSign += "\n";  // rscc
    1274           4 :     osStringToSign += "\n";  // rscd
    1275           4 :     osStringToSign += "\n";  // rsce
    1276           4 :     osStringToSign += "\n";  // rscl
    1277             : 
    1278             : #ifdef DEBUG_VERBOSE
    1279             :     CPLDebug("AZURE", "osStringToSign = %s", osStringToSign.c_str());
    1280             : #endif
    1281             : 
    1282             :     /* -------------------------------------------------------------------- */
    1283             :     /*      Compute signature.                                              */
    1284             :     /* -------------------------------------------------------------------- */
    1285             :     std::string osSignature(
    1286           8 :         CPLAzureGetSignature(osStringToSign, m_osStorageKey));
    1287             : 
    1288           4 :     ResetQueryParameters();
    1289           4 :     AddQueryParameter("sv", osSignedVersion);
    1290           4 :     AddQueryParameter("st", osStartDate);
    1291           4 :     AddQueryParameter("se", osEndDate);
    1292           4 :     AddQueryParameter("sr", osSignedResource);
    1293           4 :     AddQueryParameter("sp", osSignedPermissions);
    1294           4 :     AddQueryParameter("spr", osSignedProtocol);
    1295           4 :     AddQueryParameter("sig", osSignature);
    1296           4 :     if (!osSignedIdentifier.empty())
    1297           0 :         AddQueryParameter("si", osSignedIdentifier);
    1298           4 :     return m_osURL;
    1299             : }
    1300             : 
    1301             : /************************************************************************/
    1302             : /*                             GetOptions()                             */
    1303             : /************************************************************************/
    1304             : 
    1305             : /* static */
    1306           3 : const char *VSIAzureBlobHandleHelper::GetOptions()
    1307             : {
    1308             :     static std::string osOptions(
    1309           2 :         std::string("<Options>") +
    1310             :         "  <Option name='AZURE_STORAGE_CONNECTION_STRING' type='string' "
    1311             :         "description='Connection string that contains account name and "
    1312             :         "secret key'/>"
    1313             :         "  <Option name='AZURE_STORAGE_ACCOUNT' type='string' "
    1314             :         "description='Storage account. To use with AZURE_STORAGE_ACCESS_KEY'/>"
    1315             :         "  <Option name='AZURE_STORAGE_ACCESS_KEY' type='string' "
    1316             :         "description='Secret key'/>"
    1317             :         "  <Option name='AZURE_STORAGE_ACCESS_TOKEN' type='string' "
    1318             :         "description='Access token typically obtained using Microsoft "
    1319             :         "Authentication Library (MSAL).'/>"
    1320             :         "  <Option name='AZURE_STORAGE_SAS_TOKEN' type='string' "
    1321             :         "description='Shared Access Signature'/>"
    1322             :         "  <Option name='AZURE_NO_SIGN_REQUEST' type='boolean' "
    1323             :         "description='Whether to disable signing of requests' default='NO'/>"
    1324             :         "  <Option name='VSIAZ_CHUNK_SIZE' type='int' "
    1325             :         "description='Size in MB for chunks of files that are uploaded' "
    1326           3 :         "default='4' min='1' max='4'/>" +
    1327           4 :         cpl::VSICurlFilesystemHandlerBase::GetOptionsStatic() + "</Options>");
    1328           3 :     return osOptions.c_str();
    1329             : }
    1330             : 
    1331             : #endif  // HAVE_CURL
    1332             : 
    1333             : //! @endcond

Generated by: LCOV version 1.14