From 5603d284bf4af763169d3c37f4ff17f68203fdf8 Mon Sep 17 00:00:00 2001 From: heinrich5991 Date: Thu, 14 Mar 2024 12:18:05 +0100 Subject: [PATCH] Parse `Date` and `Last-Modified` HTTP headers --- ddnet-libs | 2 +- src/engine/shared/http.cpp | 70 ++++++++++++++++++++++++++++++++++++++ src/engine/shared/http.h | 9 +++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/ddnet-libs b/ddnet-libs index a4fbe0e52..87fbb5783 160000 --- a/ddnet-libs +++ b/ddnet-libs @@ -1 +1 @@ -Subproject commit a4fbe0e52338a129bc3ac2e3a1de54de1d175279 +Subproject commit 87fbb57839080f40d16cd77d8ad11ae91bb9c849 diff --git a/src/engine/shared/http.cpp b/src/engine/shared/http.cpp index 76bbec657..6b72e980d 100644 --- a/src/engine/shared/http.cpp +++ b/src/engine/shared/http.cpp @@ -148,6 +148,8 @@ bool CHttpRequest::ConfigureHandle(void *pHandle) curl_easy_setopt(pH, CURLOPT_USERAGENT, GAME_NAME " " GAME_RELEASE_VERSION " (" CONF_PLATFORM_STRING "; " CONF_ARCH_STRING ")"); curl_easy_setopt(pH, CURLOPT_ACCEPT_ENCODING, ""); // Use any compression algorithm supported by libcurl. + curl_easy_setopt(pH, CURLOPT_HEADERDATA, this); + curl_easy_setopt(pH, CURLOPT_HEADERFUNCTION, HeaderCallback); curl_easy_setopt(pH, CURLOPT_WRITEDATA, this); curl_easy_setopt(pH, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(pH, CURLOPT_NOPROGRESS, 0L); @@ -206,6 +208,52 @@ bool CHttpRequest::ConfigureHandle(void *pHandle) return true; } +size_t CHttpRequest::OnHeader(char *pHeader, size_t HeaderSize) +{ + // `pHeader` is NOT null-terminated. + // `pHeader` has a trailing newline. + + if(HeaderSize <= 1) + { + m_HeadersEnded = true; + return HeaderSize; + } + if(m_HeadersEnded) + { + // redirect, clear old headers + m_HeadersEnded = false; + m_ResultDate = {}; + m_ResultLastModified = {}; + } + + static const char DATE[] = "Date: "; + static const char LAST_MODIFIED[] = "Last-Modified: "; + + // Trailing newline and null termination evens out. + if(HeaderSize - 1 >= sizeof(DATE) - 1 && str_startswith_nocase(pHeader, DATE)) + { + char aValue[128]; + str_truncate(aValue, sizeof(aValue), pHeader + (sizeof(DATE) - 1), HeaderSize - (sizeof(DATE) - 1) - 1); + int64_t Value = curl_getdate(aValue, nullptr); + if(Value != -1) + { + m_ResultDate = Value; + } + } + if(HeaderSize - 1 >= sizeof(LAST_MODIFIED) - 1 && str_startswith_nocase(pHeader, LAST_MODIFIED)) + { + char aValue[128]; + str_truncate(aValue, sizeof(aValue), pHeader + (sizeof(LAST_MODIFIED) - 1), HeaderSize - (sizeof(LAST_MODIFIED) - 1) - 1); + int64_t Value = curl_getdate(aValue, nullptr); + if(Value != -1) + { + m_ResultLastModified = Value; + } + } + + return HeaderSize; +} + size_t CHttpRequest::OnData(char *pData, size_t DataSize) { // Need to check for the maximum response size here as curl can only @@ -244,6 +292,12 @@ size_t CHttpRequest::OnData(char *pData, size_t DataSize) } } +size_t CHttpRequest::HeaderCallback(char *pData, size_t Size, size_t Number, void *pUser) +{ + dbg_assert(Size == 1, "invalid size parameter passed to header callback"); + return ((CHttpRequest *)pUser)->OnHeader(pData, Number); +} + size_t CHttpRequest::WriteCallback(char *pData, size_t Size, size_t Number, void *pUser) { return ((CHttpRequest *)pUser)->OnData(pData, Size * Number); @@ -390,6 +444,22 @@ int CHttpRequest::StatusCode() const return m_StatusCode; } +std::optional CHttpRequest::ResultAgeSeconds() const +{ + dbg_assert(State() == EHttpState::DONE, "Request not done"); + if(!m_ResultDate || !m_ResultLastModified) + { + return {}; + } + return *m_ResultDate - *m_ResultLastModified; +} + +std::optional CHttpRequest::ResultLastModified() const +{ + dbg_assert(State() == EHttpState::DONE, "Request not done"); + return m_ResultLastModified; +} + bool CHttp::Init(std::chrono::milliseconds ShutdownDelay) { m_ShutdownDelay = ShutdownDelay; diff --git a/src/engine/shared/http.h b/src/engine/shared/http.h index 87b808be9..86e53fac9 100644 --- a/src/engine/shared/http.h +++ b/src/engine/shared/http.h @@ -118,6 +118,9 @@ class CHttpRequest : public IHttpRequest std::atomic m_Abort{false}; int m_StatusCode = 0; + bool m_HeadersEnded = false; + std::optional m_ResultDate = {}; + std::optional m_ResultLastModified = {}; // Abort the request with an error if `BeforeInit()` returns false. bool BeforeInit(); @@ -125,11 +128,15 @@ class CHttpRequest : public IHttpRequest // `pHandle` can be nullptr if no handle was ever created for this request. void OnCompletionInternal(void *pHandle, unsigned int Result); // void * == CURL *, unsigned int == CURLcode + // Abort the request if `OnHeader()` returns something other than + // `DataSize`. `pHeader` is NOT null-terminated. + size_t OnHeader(char *pHeader, size_t HeaderSize); // Abort the request if `OnData()` returns something other than // `DataSize`. size_t OnData(char *pData, size_t DataSize); static int ProgressCallback(void *pUser, double DlTotal, double DlCurr, double UlTotal, double UlCurr); + static size_t HeaderCallback(char *pData, size_t Size, size_t Number, void *pUser); static size_t WriteCallback(char *pData, size_t Size, size_t Number, void *pUser); protected: @@ -207,6 +214,8 @@ public: const SHA256_DIGEST &ResultSha256() const; int StatusCode() const; + std::optional ResultAgeSeconds() const; + std::optional ResultLastModified() const; }; inline std::unique_ptr HttpHead(const char *pUrl)