diff --git a/.gitmodules b/.gitmodules index d2e6356a..265f2845 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,3 +28,6 @@ [submodule "deps/libusb"] path = deps/libusb url = https://github.com/libusb/libusb.git +[submodule "deps/date"] + path = deps/date + url = https://github.com/HowardHinnant/date.git diff --git a/deps/date b/deps/date new file mode 160000 index 00000000..5bdb7e6f --- /dev/null +++ b/deps/date @@ -0,0 +1 @@ +Subproject commit 5bdb7e6f31fac909c090a46dbd9fea27b6e609a4 diff --git a/plugins/twitch/CMakeLists.txt b/plugins/twitch/CMakeLists.txt index 61225d88..0667809f 100644 --- a/plugins/twitch/CMakeLists.txt +++ b/plugins/twitch/CMakeLists.txt @@ -30,6 +30,31 @@ if(NOT ZLIB_FOUND) return() endif() +set(DATE_LIB_DIR "${ADVSS_SOURCE_DIR}/deps/date") +if(EXISTS "${DATE_LIB_DIR}/CMakeLists.txt") + set(BUILD_TZ_LIB ON) + if(OS_WINDOWS) + if(CURL_FOUND AND TARGET CURL::libcurl) + get_target_property(CURL_INCLUDE_DIR CURL::libcurl + INTERFACE_INCLUDE_DIRECTORIES) + add_subdirectory("${DATE_LIB_DIR}" "${DATE_LIB_DIR}/build" + EXCLUDE_FROM_ALL) + target_include_directories(date-tz PRIVATE "${CURL_INCLUDE_DIR}") + set(VERIFY_TWITCH_TIMESTAMPS ON) + else() + message(WARNING "CURL not found - not verifying Twitch timestamps") + endif() + else() + add_subdirectory("${DATE_LIB_DIR}" "${DATE_LIB_DIR}/build" EXCLUDE_FROM_ALL) + target_compile_options(date-tz PUBLIC -Wno-error=conversion + -Wno-error=shadow) + set(VERIFY_TWITCH_TIMESTAMPS ON) + endif() +else() + message(WARNING "date lib not found in \"${DATE_LIB_DIR}\"!\n" + "Twitch timestamps will not be checked!") +endif() + # --- End of section --- add_library(${PROJECT_NAME} MODULE) @@ -76,6 +101,14 @@ set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "") target_include_directories(${PROJECT_NAME} PRIVATE "${CPP_HTTPLIB_DIR}/" "${OPENSSL_INCLUDE_DIR}") target_link_libraries(${PROJECT_NAME} PRIVATE ${OPENSSL_LIBRARIES} ZLIB::ZLIB) +if(DEFINED VERIFY_TWITCH_TIMESTAMPS) + target_compile_definitions(${PROJECT_NAME} PRIVATE VERIFY_TIMESTAMPS=1) + target_link_libraries(${PROJECT_NAME} PRIVATE date::date-tz) + if(OS_WINDOWS) + target_link_libraries(${PROJECT_NAME} PRIVATE CURL::libcurl) + endif() +endif() + install_advss_plugin(${PROJECT_NAME}) if(OS_WINDOWS) # Couldn't really find a better way to install runtime dependencies for diff --git a/plugins/twitch/event-sub.cpp b/plugins/twitch/event-sub.cpp index 8c22afd7..671a4009 100644 --- a/plugins/twitch/event-sub.cpp +++ b/plugins/twitch/event-sub.cpp @@ -4,6 +4,10 @@ #include +#ifdef VERIFY_TIMESTAMPS +#include "date/tz.h" +#endif + namespace advss { using websocketpp::lib::placeholders::_1; @@ -237,15 +241,43 @@ void EventSub::OnOpen(connection_hdl) static bool isValidTimestamp(const std::string ×tamp) { - std::tm tm = {}; - std::istringstream ss(timestamp); - ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S.%fZ"); - auto tp = std::chrono::system_clock::from_time_t(std::mktime(&tm)); - tp += std::chrono::hours(1); // UTC - std::chrono::system_clock::time_point currentTime = - std::chrono::system_clock::now(); - auto diff = currentTime - tp; - return diff <= std::chrono::minutes(10); +#ifdef VERIFY_TIMESTAMPS + // Example input: 2023-07-19T14:56:51.634234626Z + try { + // Discard the nanosecond part + static constexpr size_t dotPos = 19; + std::string trimmed = timestamp.substr(0, dotPos); + auto tzStart = timestamp.find_first_of("Z+-", dotPos); + trimmed = timestamp.substr(0, dotPos); + if (tzStart != std::string::npos) { + trimmed += timestamp.substr(tzStart); + } + + std::istringstream in(trimmed); + date::sys_time parsedTime; + in >> date::parse("%FT%TZ", parsedTime); + if (in.fail()) { + blog(LOG_WARNING, "failed to parse timestamp %s", + timestamp.c_str()); + return false; + } + + auto now = date::zoned_time{date::current_zone(), + std::chrono::system_clock::now()} + .get_sys_time(); + + auto duration = now - parsedTime; + // Clocks might be off by a bit, so allow negative values also + return duration <= std::chrono::minutes(10) && + duration >= std::chrono::minutes(-1); + } catch (const std::exception &e) { + blog(LOG_WARNING, "%s: %s", __func__, e.what()); + return false; + } +#else + // Just assume timestamps are always valid + return true; +#endif } bool EventSub::IsValidMessageID(const std::string &id) @@ -288,7 +320,8 @@ void EventSub::OnMessage(connection_hdl, EventSubWSClient::message_ptr message) obs_data_get_string(metadata, "message_timestamp"); if (!isValidTimestamp(timestamp)) { blog(LOG_WARNING, - "Discarding Twitch EventSub with invalid timestamp"); + "Discarding Twitch EventSub with invalid timestamp %s", + timestamp.c_str()); return; } std::string id = obs_data_get_string(metadata, "message_id");