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