LCOV - code coverage report
Current view: top level - port - cpl_nasa_earthdata.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 148 179 82.7 %
Date: 2026-06-20 20:44:25 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          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

Generated by: LCOV version 1.14