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 8 : std::string osUserPwd = std::move(l_osEarthdataUsername);
176 8 : osUserPwd += ':';
177 8 : osUserPwd += l_osEarthdataPassword;
178 8 : aosOptions.SetNameValue("USERPWD", osUserPwd.c_str());
179 8 : aosOptions.SetNameValue("ACCEPT", "application/json");
180 8 : std::string osURL;
181 8 : if (!cpl::starts_with(l_osEarthdataHost, "http://") &&
182 8 : !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 = std::move(l_osEarthdataToken);
248 14 : std::lock_guard oLock(poProvider->m_oMutex);
249 7 : if (!poProvider->RefreshIfNeededUnderLock())
250 3 : return nullptr;
251 4 : return poProvider;
252 : }
253 :
254 : /************************************************************************/
255 : /* CPLNasaEarthdataCredentialProvider::RefreshIfNeededUnderLock() */
256 : /************************************************************************/
257 :
258 52 : bool CPLNasaEarthdataCredentialProvider::RefreshIfNeededUnderLock()
259 : {
260 52 : constexpr int knExpirationDelayMargin = 60;
261 97 : if (m_osAccessKeyId.empty() ||
262 45 : time(nullptr) + knExpirationDelayMargin > m_nTokenExpirationTimestamp)
263 : {
264 8 : m_osAccessKeyId.clear();
265 8 : m_osSecretAccessKey.clear();
266 8 : m_osSessionToken.clear();
267 8 : m_nTokenExpirationTimestamp = 0;
268 :
269 8 : CPLStringList aosOptions;
270 8 : aosOptions.SetNameValue("HTTPAUTH", "BEARER");
271 8 : aosOptions.SetNameValue("HTTP_BEARER", m_osEarthdataToken.c_str());
272 : CPLHTTPResult *psResult =
273 8 : CPLHTTPFetch(m_osGetCredentialsURL.c_str(), aosOptions.List());
274 8 : if (!psResult || psResult->nStatus != 0 || !psResult->pabyData)
275 : {
276 0 : CPLError(CE_Failure, CPLE_AppDefined,
277 : "Earthdata credentials provider: request to %s to get "
278 : "access token failed: %s",
279 : m_osGetCredentialsURL.c_str(),
280 0 : psResult && psResult->pszErrBuf ? psResult->pszErrBuf
281 : : "(null)");
282 0 : CPLHTTPDestroyResult(psResult);
283 0 : return false;
284 : }
285 :
286 : const CPLStringList aosResponse =
287 8 : CPLParseKeyValueJson(reinterpret_cast<char *>(psResult->pabyData));
288 8 : CPLHTTPDestroyResult(psResult);
289 :
290 8 : m_osAccessKeyId = aosResponse.FetchNameValueDef("accessKeyId", "");
291 : m_osSecretAccessKey =
292 8 : aosResponse.FetchNameValueDef("secretAccessKey", "");
293 8 : m_osSessionToken = aosResponse.FetchNameValueDef("sessionToken", "");
294 8 : const char *pszExpiration = aosResponse.FetchNameValue("expiration");
295 18 : if (m_osAccessKeyId.empty() || m_osSecretAccessKey.empty() ||
296 18 : m_osSessionToken.empty() || !pszExpiration)
297 : {
298 3 : CPLError(CE_Failure, CPLE_AppDefined,
299 : "Earthdata credentials provider: request to %s failed to "
300 : "return one of 'accessKeyId', 'secretAccessKey', "
301 : "'sessionToken' and/or 'expiration'",
302 : m_osGetCredentialsURL.c_str());
303 3 : return false;
304 : }
305 :
306 5 : int nYear = 0, nMonth = 0, nDay = 0, nHour = 0, nMin = 0, nSec = 0;
307 5 : if (sscanf(pszExpiration, "%04d-%02d-%02d %02d:%02d:%02d+00:00", &nYear,
308 5 : &nMonth, &nDay, &nHour, &nMin, &nSec) != 6)
309 : {
310 0 : CPLError(CE_Failure, CPLE_AppDefined,
311 : "Earthdata credentials provider: request to %s returned "
312 : "expiration='%s' which is an unexpected time format",
313 : m_osGetCredentialsURL.c_str(), pszExpiration);
314 0 : return false;
315 : }
316 : struct tm brokendowntime;
317 5 : brokendowntime.tm_year = nYear - 1900;
318 5 : brokendowntime.tm_mon = nMonth - 1;
319 5 : brokendowntime.tm_mday = nDay;
320 5 : brokendowntime.tm_hour = nHour;
321 5 : brokendowntime.tm_min = nMin;
322 5 : brokendowntime.tm_sec = nSec;
323 5 : m_nTokenExpirationTimestamp = CPLYMDHMSToUnixTime(&brokendowntime);
324 :
325 5 : CPLDebug("EARTHDATA", "Got S3 credentials until %s", pszExpiration);
326 : }
327 :
328 49 : return true;
329 : }
330 :
331 : /************************************************************************/
332 : /* GetCache() */
333 : /************************************************************************/
334 :
335 : using EarthdataCacheType =
336 : lru11::Cache<std::string,
337 : std::shared_ptr<CPLNasaEarthdataCredentialProvider>,
338 : std::mutex>;
339 :
340 2743 : static EarthdataCacheType &GetCache()
341 : {
342 2743 : static EarthdataCacheType oCache;
343 2743 : return oCache;
344 : }
345 :
346 : /************************************************************************/
347 : /* CPLNasaEarthdataCredentialProvider::Get() */
348 : /************************************************************************/
349 :
350 : /* static */
351 : std::shared_ptr<CPLNasaEarthdataCredentialProvider>
352 1072 : CPLNasaEarthdataCredentialProvider::Get(const std::string &osFilename,
353 : bool *pbErrorOccurred)
354 : {
355 1072 : if (pbErrorOccurred)
356 1072 : *pbErrorOccurred = false;
357 :
358 1072 : const char *pszCredentialsURL = VSIGetPathSpecificOption(
359 : osFilename.c_str(), "VSIS3_EARTHDATA_CREDENTIALS_URL", nullptr);
360 1072 : if (!pszCredentialsURL)
361 1042 : return nullptr;
362 :
363 30 : const char *pszEarthdataHost = VSIGetPathSpecificOption(
364 : osFilename.c_str(), "EARTHDATA_HOST",
365 : VSIGetPathSpecificOption(osFilename.c_str(), "DEFAULT_EARTHDATA_HOST",
366 : ""));
367 : const char *pszEarthdataToken =
368 30 : VSIGetPathSpecificOption(osFilename.c_str(), "EARTHDATA_TOKEN", "");
369 : const char *pszEarthdataUserame =
370 30 : VSIGetPathSpecificOption(osFilename.c_str(), "EARTHDATA_USERNAME", "");
371 : const char *pszEarthdataPassword =
372 30 : VSIGetPathSpecificOption(osFilename.c_str(), "EARTHDATA_PASSWORD", "");
373 :
374 30 : auto &oCache = GetCache();
375 30 : std::shared_ptr<CPLNasaEarthdataCredentialProvider> ret;
376 60 : std::string osCacheKey = pszCredentialsURL;
377 30 : osCacheKey += '|';
378 30 : osCacheKey += pszEarthdataHost;
379 30 : osCacheKey += '|';
380 30 : osCacheKey += pszEarthdataToken;
381 30 : osCacheKey += '|';
382 30 : osCacheKey += pszEarthdataUserame;
383 30 : osCacheKey += '|';
384 30 : osCacheKey += pszEarthdataPassword;
385 30 : if (!oCache.tryGet(osCacheKey, ret))
386 : {
387 38 : ret = Build(pszCredentialsURL, pszEarthdataHost, pszEarthdataToken,
388 19 : pszEarthdataUserame, pszEarthdataPassword);
389 19 : if (ret)
390 : {
391 4 : oCache.insert(osCacheKey, ret);
392 : }
393 15 : else if (pbErrorOccurred)
394 : {
395 15 : *pbErrorOccurred = true;
396 : }
397 : }
398 30 : return ret;
399 : }
400 :
401 : /************************************************************************/
402 : /* CPLNasaEarthdataCredentialProvider::ClearCache() */
403 : /************************************************************************/
404 :
405 : /* static */
406 2713 : void CPLNasaEarthdataCredentialProvider::ClearCache()
407 : {
408 2713 : GetCache().clear();
409 2713 : }
410 :
411 : #endif // HAVE_CURL
|