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
|