Coverage Report

Created: 2025-06-09 07:07

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