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