LCOV - code coverage report
Current view: top level - port - cpl_nasa_earthdata.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 149 179 83.2 %
Date: 2026-07-09 08:35:43 Functions: 5 5 100.0 %

          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

Generated by: LCOV version 1.14