Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: CPL - Common Portability Library
4 : * Purpose: Implement credential provider for accessing NASA Earthdata resources
5 : * Author: Even Rouault, even.rouault at spatialys.com
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2026, Even Rouault <even.rouault at spatialys.com>
9 : *
10 : * SPDX-License-Identifier: MIT
11 : ****************************************************************************/
12 :
13 : #ifdef HAVE_CURL
14 :
15 : #include "cpl_error.h"
16 : #include "cpl_json.h"
17 : #include "cpl_http.h"
18 : #include "cpl_mem_cache.h"
19 : #include "cpl_nasa_earthdata.h"
20 : #include "cpl_time.h"
21 : #include "cpl_vsi.h"
22 : #include "cpl_vsi_virtual.h"
23 :
24 : #include <mutex>
25 :
26 : /************************************************************************/
27 : /* CPLNasaEarthdataCredentialProvider() */
28 : /************************************************************************/
29 :
30 : CPLNasaEarthdataCredentialProvider::CPLNasaEarthdataCredentialProvider() =
31 : default;
32 :
33 : /************************************************************************/
34 : /* CPLNasaEarthdataCredentialProvider::Build() */
35 : /************************************************************************/
36 :
37 : /* static */
38 : std::unique_ptr<CPLNasaEarthdataCredentialProvider>
39 19 : CPLNasaEarthdataCredentialProvider::Build(
40 : const std::string &osGetCredentialsURL, const std::string &osEarthdataHost,
41 : const std::string &osEarthdataToken, const std::string &osEarthdataUsername,
42 : const std::string &osEarthdataPassword, const std::string &osNetrcFilename)
43 : {
44 19 : if (osGetCredentialsURL.empty())
45 : {
46 0 : CPLError(CE_Failure, CPLE_IllegalArg,
47 : "Earthdata credentials provider: Credentials URL must not be "
48 : "empty");
49 0 : return nullptr;
50 : }
51 :
52 38 : std::string l_osEarthdataHost = osEarthdataHost;
53 19 : if (l_osEarthdataHost.empty())
54 5 : l_osEarthdataHost = "urs.earthdata.nasa.gov";
55 :
56 38 : std::string l_osEarthdataToken = osEarthdataToken;
57 38 : std::string l_osEarthdataUsername = osEarthdataUsername;
58 38 : std::string l_osEarthdataPassword = osEarthdataPassword;
59 :
60 : #define osEarthdataHost no_longer_use_me
61 : #define osEarthdataToken no_longer_use_me
62 : #define osEarthdataUsername no_longer_use_me
63 : #define osEarthdataPassword no_longer_use_me
64 :
65 33 : if (l_osEarthdataToken.empty() &&
66 14 : (l_osEarthdataUsername.empty() != l_osEarthdataPassword.empty()))
67 : {
68 0 : CPLError(
69 : CE_Failure, CPLE_IllegalArg,
70 : "Both Earthdata username and password must be provided, or none");
71 0 : return nullptr;
72 : }
73 :
74 19 : if (l_osEarthdataToken.empty() && l_osEarthdataUsername.empty())
75 : {
76 7 : std::string l_osNetrcFilename = osNetrcFilename;
77 7 : if (l_osNetrcFilename.empty())
78 : {
79 7 : l_osNetrcFilename = CPLGetConfigOption("NETRC", "");
80 : }
81 7 : if (l_osNetrcFilename.empty())
82 : {
83 0 : const char *pszHomeDir = CPLGetHomeDir();
84 0 : if (!pszHomeDir)
85 : {
86 0 : CPLError(
87 : CE_Failure, CPLE_AppDefined,
88 : "Earthdata credentials provider: HOME is not set, and no "
89 : "other Earthdata login mechanism defined (EARTHDATA_TOKEN, "
90 : "EARTHDATA_USERNAME+EARTHDATA_PASSWORD or NETRC)");
91 0 : return nullptr;
92 : }
93 : #ifdef _WIN32
94 : constexpr const char *pszNetrcFile = "_netrc";
95 : #else
96 0 : constexpr const char *pszNetrcFile = ".netrc";
97 : #endif
98 : l_osNetrcFilename =
99 0 : CPLFormFilenameSafe(pszHomeDir, pszNetrcFile, nullptr);
100 : }
101 : auto fp =
102 7 : VSIFilesystemHandler::OpenStatic(l_osNetrcFilename.c_str(), "rb");
103 7 : if (!fp)
104 : {
105 3 : CPLError(CE_Failure, CPLE_AppDefined,
106 : "Earthdata credentials provider: cannot open %s, and no "
107 : "other Earthdata login mechanism defined (EARTHDATA_TOKEN "
108 : "or EARTHDATA_USERNAME+EARTHDATA_PASSWORD)",
109 : l_osNetrcFilename.c_str());
110 3 : return nullptr;
111 : }
112 4 : constexpr int MAXLINE_LENGTH = 1024; // Arbitrary
113 4 : std::string osExpectedLineStart("machine ");
114 4 : osExpectedLineStart += l_osEarthdataHost;
115 4 : osExpectedLineStart += ' ';
116 4 : bool bMatchFound = false;
117 : while (const char *pszLine =
118 4 : CPLReadLine2L(fp.get(), MAXLINE_LENGTH, nullptr))
119 : {
120 1 : if (STARTS_WITH(pszLine, osExpectedLineStart.c_str()))
121 : {
122 1 : bMatchFound = true;
123 : const CPLStringList aosTokens(CSLTokenizeString2(
124 2 : pszLine + osExpectedLineStart.size(), " ", 0));
125 3 : for (int i = 0; i < aosTokens.size(); ++i)
126 : {
127 3 : if (EQUAL(aosTokens[i], "login") &&
128 1 : i + 1 < aosTokens.size())
129 : {
130 1 : l_osEarthdataUsername = aosTokens[i + 1];
131 1 : ++i;
132 : }
133 2 : else if (EQUAL(aosTokens[i], "password") &&
134 1 : i + 1 < aosTokens.size())
135 : {
136 1 : l_osEarthdataPassword = aosTokens[i + 1];
137 1 : ++i;
138 : }
139 : }
140 1 : break;
141 : }
142 0 : }
143 4 : if (!bMatchFound)
144 : {
145 3 : CPLError(
146 : CE_Failure, CPLE_AppDefined,
147 : "Earthdata credentials provider: no credentials for host %s "
148 : "found in %s, and no other Earthdata login mechanism defined "
149 : "(EARTHDATA_TOKEN or EARTHDATA_USERNAME+EARTHDATA_PASSWORD)",
150 : l_osEarthdataHost.c_str(), l_osNetrcFilename.c_str());
151 3 : return nullptr;
152 : }
153 1 : if (l_osEarthdataUsername.empty())
154 : {
155 0 : CPLError(CE_Failure, CPLE_AppDefined,
156 : "Earthdata credentials provider: line with credentials "
157 : "for host %s found in %s, but missing 'login'",
158 : l_osEarthdataHost.c_str(), l_osNetrcFilename.c_str());
159 0 : return nullptr;
160 : }
161 1 : if (l_osEarthdataPassword.empty())
162 : {
163 0 : CPLError(CE_Failure, CPLE_AppDefined,
164 : "Earthdata credentials provider: line with credentials "
165 : "for host %s found in %s, but missing 'password'",
166 : l_osEarthdataHost.c_str(), l_osNetrcFilename.c_str());
167 0 : return nullptr;
168 : }
169 : }
170 :
171 13 : if (l_osEarthdataToken.empty())
172 : {
173 8 : CPLStringList aosOptions;
174 8 : aosOptions.SetNameValue("CUSTOMREQUEST", "POST");
175 16 : aosOptions.SetNameValue("USERPWD", std::string(l_osEarthdataUsername)
176 8 : .append(":")
177 8 : .append(l_osEarthdataPassword)
178 8 : .c_str());
179 8 : aosOptions.SetNameValue("ACCEPT", "application/json");
180 8 : std::string osURL;
181 8 : if (!cpl::starts_with(l_osEarthdataHost, "http://") &&
182 0 : !cpl::starts_with(l_osEarthdataHost, "https://"))
183 : {
184 0 : osURL += "https://";
185 : }
186 8 : osURL += l_osEarthdataHost;
187 8 : osURL += "/api/users/find_or_create_token";
188 : CPLHTTPResult *psResult =
189 8 : CPLHTTPFetch(osURL.c_str(), aosOptions.List());
190 8 : if (!psResult || psResult->nStatus != 0 || !psResult->pabyData)
191 : {
192 0 : CPLError(CE_Failure, CPLE_AppDefined,
193 : "Earthdata credentials provider: request to %s to get "
194 : "access token failed: %s",
195 : osURL.c_str(),
196 0 : psResult && psResult->pszErrBuf ? psResult->pszErrBuf
197 : : "(null)");
198 0 : CPLHTTPDestroyResult(psResult);
199 0 : return nullptr;
200 : }
201 :
202 : const CPLStringList aosResponse =
203 8 : CPLParseKeyValueJson(reinterpret_cast<char *>(psResult->pabyData));
204 8 : CPLHTTPDestroyResult(psResult);
205 :
206 8 : if (const char *pszError = aosResponse.FetchNameValue("error"))
207 : {
208 1 : CPLError(CE_Failure, CPLE_AppDefined,
209 : "Earthdata credentials provider: %s in response of %s",
210 : pszError, osURL.c_str());
211 1 : return nullptr;
212 : }
213 :
214 7 : const char *pszAccessToken = aosResponse.FetchNameValue("access_token");
215 7 : if (!pszAccessToken)
216 : {
217 5 : CPLError(CE_Failure, CPLE_AppDefined,
218 : "Earthdata credentials provider: missing 'access_token' "
219 : "in response of %s",
220 : osURL.c_str());
221 5 : return nullptr;
222 : }
223 2 : const char *pszTokenType = aosResponse.FetchNameValue("token_type");
224 2 : if (!pszTokenType)
225 : {
226 0 : CPLError(CE_Failure, CPLE_AppDefined,
227 : "Earthdata credentials provider: missing 'token_type' in "
228 : "response of %s",
229 : osURL.c_str());
230 0 : return nullptr;
231 : }
232 2 : constexpr const char *pszExpectedTokenType = "Bearer";
233 2 : if (!EQUAL(pszTokenType, pszExpectedTokenType))
234 : {
235 0 : CPLError(CE_Failure, CPLE_AppDefined,
236 : "Earthdata credentials provider: in response of %s, got "
237 : "'token_type'='%s'. Expected '%s'",
238 : osURL.c_str(), pszTokenType, pszExpectedTokenType);
239 0 : return nullptr;
240 : }
241 2 : l_osEarthdataToken = pszAccessToken;
242 : }
243 :
244 : auto poProvider = std::unique_ptr<CPLNasaEarthdataCredentialProvider>(
245 14 : new CPLNasaEarthdataCredentialProvider());
246 7 : poProvider->m_osGetCredentialsURL = osGetCredentialsURL;
247 7 : poProvider->m_osEarthdataToken = l_osEarthdataToken;
248 7 : if (!poProvider->RefreshIfNeeded())
249 3 : return nullptr;
250 4 : return poProvider;
251 : }
252 :
253 : /************************************************************************/
254 : /* CPLNasaEarthdataCredentialProvider::RefreshIfNeeded() */
255 : /************************************************************************/
256 :
257 52 : bool CPLNasaEarthdataCredentialProvider::RefreshIfNeeded()
258 : {
259 104 : std::lock_guard oLock(m_oMutex);
260 :
261 52 : constexpr int knExpirationDelayMargin = 60;
262 97 : if (m_osAccessKeyId.empty() ||
263 45 : time(nullptr) + knExpirationDelayMargin > m_nTokenExpirationTimestamp)
264 : {
265 8 : m_osAccessKeyId.clear();
266 8 : m_osSecretAccessKey.clear();
267 8 : m_osSessionToken.clear();
268 8 : m_nTokenExpirationTimestamp = 0;
269 :
270 8 : CPLStringList aosOptions;
271 8 : aosOptions.SetNameValue("HTTPAUTH", "BEARER");
272 8 : aosOptions.SetNameValue("HTTP_BEARER", m_osEarthdataToken.c_str());
273 : CPLHTTPResult *psResult =
274 8 : CPLHTTPFetch(m_osGetCredentialsURL.c_str(), aosOptions.List());
275 8 : if (!psResult || psResult->nStatus != 0 || !psResult->pabyData)
276 : {
277 0 : CPLError(CE_Failure, CPLE_AppDefined,
278 : "Earthdata credentials provider: request to %s to get "
279 : "access token failed: %s",
280 : m_osGetCredentialsURL.c_str(),
281 0 : psResult && psResult->pszErrBuf ? psResult->pszErrBuf
282 : : "(null)");
283 0 : CPLHTTPDestroyResult(psResult);
284 0 : return false;
285 : }
286 :
287 : const CPLStringList aosResponse =
288 8 : CPLParseKeyValueJson(reinterpret_cast<char *>(psResult->pabyData));
289 8 : CPLHTTPDestroyResult(psResult);
290 :
291 8 : m_osAccessKeyId = aosResponse.FetchNameValueDef("accessKeyId", "");
292 : m_osSecretAccessKey =
293 8 : aosResponse.FetchNameValueDef("secretAccessKey", "");
294 8 : m_osSessionToken = aosResponse.FetchNameValueDef("sessionToken", "");
295 8 : const char *pszExpiration = aosResponse.FetchNameValue("expiration");
296 18 : if (m_osAccessKeyId.empty() || m_osSecretAccessKey.empty() ||
297 18 : m_osSessionToken.empty() || !pszExpiration)
298 : {
299 3 : CPLError(CE_Failure, CPLE_AppDefined,
300 : "Earthdata credentials provider: request to %s failed to "
301 : "return one of 'accessKeyId', 'secretAccessKey', "
302 : "'sessionToken' and/or 'expiration'",
303 : m_osGetCredentialsURL.c_str());
304 3 : return false;
305 : }
306 :
307 5 : int nYear = 0, nMonth = 0, nDay = 0, nHour = 0, nMin = 0, nSec = 0;
308 5 : if (sscanf(pszExpiration, "%04d-%02d-%02d %02d:%02d:%02d+00:00", &nYear,
309 5 : &nMonth, &nDay, &nHour, &nMin, &nSec) != 6)
310 : {
311 0 : CPLError(CE_Failure, CPLE_AppDefined,
312 : "Earthdata credentials provider: request to %s returned "
313 : "expiration='%s' which is an unexpected time format",
314 : m_osGetCredentialsURL.c_str(), pszExpiration);
315 0 : return false;
316 : }
317 : struct tm brokendowntime;
318 5 : brokendowntime.tm_year = nYear - 1900;
319 5 : brokendowntime.tm_mon = nMonth - 1;
320 5 : brokendowntime.tm_mday = nDay;
321 5 : brokendowntime.tm_hour = nHour;
322 5 : brokendowntime.tm_min = nMin;
323 5 : brokendowntime.tm_sec = nSec;
324 5 : m_nTokenExpirationTimestamp = CPLYMDHMSToUnixTime(&brokendowntime);
325 :
326 5 : CPLDebug("EARTHDATA", "Got S3 credentials until %s", pszExpiration);
327 : }
328 :
329 49 : return true;
330 : }
331 :
332 : /************************************************************************/
333 : /* GetCache() */
334 : /************************************************************************/
335 :
336 : using EarthdataCacheType =
337 : lru11::Cache<std::string,
338 : std::shared_ptr<CPLNasaEarthdataCredentialProvider>,
339 : std::mutex>;
340 :
341 2741 : static EarthdataCacheType &GetCache()
342 : {
343 2741 : static EarthdataCacheType oCache;
344 2741 : return oCache;
345 : }
346 :
347 : /************************************************************************/
348 : /* CPLNasaEarthdataCredentialProvider::Get() */
349 : /************************************************************************/
350 :
351 : /* static */
352 : std::shared_ptr<CPLNasaEarthdataCredentialProvider>
353 1072 : CPLNasaEarthdataCredentialProvider::Get(const std::string &osFilename,
354 : bool *pbErrorOccurred)
355 : {
356 1072 : if (pbErrorOccurred)
357 1072 : *pbErrorOccurred = false;
358 :
359 1072 : const char *pszCredentialsURL = VSIGetPathSpecificOption(
360 : osFilename.c_str(), "VSIS3_EARTHDATA_CREDENTIALS_URL", nullptr);
361 1072 : if (!pszCredentialsURL)
362 1042 : return nullptr;
363 :
364 30 : const char *pszEarthdataHost = VSIGetPathSpecificOption(
365 : osFilename.c_str(), "EARTHDATA_HOST",
366 : VSIGetPathSpecificOption(osFilename.c_str(), "DEFAULT_EARTHDATA_HOST",
367 : ""));
368 : const char *pszEarthdataToken =
369 30 : VSIGetPathSpecificOption(osFilename.c_str(), "EARTHDATA_TOKEN", "");
370 : const char *pszEarthdataUserame =
371 30 : VSIGetPathSpecificOption(osFilename.c_str(), "EARTHDATA_USERNAME", "");
372 : const char *pszEarthdataPassword =
373 30 : VSIGetPathSpecificOption(osFilename.c_str(), "EARTHDATA_PASSWORD", "");
374 :
375 30 : auto &oCache = GetCache();
376 30 : std::shared_ptr<CPLNasaEarthdataCredentialProvider> ret;
377 60 : std::string osCacheKey = pszCredentialsURL;
378 30 : osCacheKey += '|';
379 30 : osCacheKey += pszEarthdataHost;
380 30 : osCacheKey += '|';
381 30 : osCacheKey += pszEarthdataToken;
382 30 : osCacheKey += '|';
383 30 : osCacheKey += pszEarthdataUserame;
384 30 : osCacheKey += '|';
385 30 : osCacheKey += pszEarthdataPassword;
386 30 : if (!oCache.tryGet(osCacheKey, ret))
387 : {
388 38 : ret = Build(pszCredentialsURL, pszEarthdataHost, pszEarthdataToken,
389 19 : pszEarthdataUserame, pszEarthdataPassword);
390 19 : if (ret)
391 : {
392 4 : oCache.insert(osCacheKey, ret);
393 : }
394 15 : else if (pbErrorOccurred)
395 : {
396 15 : *pbErrorOccurred = true;
397 : }
398 : }
399 30 : return ret;
400 : }
401 :
402 : /************************************************************************/
403 : /* CPLNasaEarthdataCredentialProvider::ClearCache() */
404 : /************************************************************************/
405 :
406 : /* static */
407 2711 : void CPLNasaEarthdataCredentialProvider::ClearCache()
408 : {
409 2711 : GetCache().clear();
410 2711 : }
411 :
412 : #endif // HAVE_CURL
|