Cleanup Twitch timestamp verification
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled

This commit is contained in:
WarmUpTill
2026-07-03 19:53:41 +02:00
committed by WarmUpTill
parent 2658ced18e
commit 338a478ead
9 changed files with 291 additions and 85 deletions

View File

@@ -32,32 +32,6 @@ if(NOT ZLIB_FOUND)
return()
endif()
set(DATE_LIB_DIR "${ADVSS_SOURCE_DIR}/deps/date")
if(EXISTS "${DATE_LIB_DIR}/CMakeLists.txt"
AND NOT DISABLE_TWITCH_TIMESTAMP_VERIFICATION)
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)
@@ -92,6 +66,8 @@ target_sources(
content-classification.hpp
event-sub.cpp
event-sub.hpp
twitch-timestamp.cpp
twitch-timestamp.hpp
language-selection.cpp
language-selection.hpp
macro-action-twitch.cpp
@@ -114,13 +90,6 @@ 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)

View File

@@ -4,10 +4,7 @@
#include <log-helper.hpp>
#include <plugin-state-helpers.hpp>
#ifdef VERIFY_TIMESTAMPS
#include "date/tz.h"
#endif
#include "twitch-timestamp.hpp"
using namespace std::chrono_literals;
@@ -357,46 +354,6 @@ void EventSub::OnOpen(connection_hdl)
_connected = true;
}
static bool isValidTimestamp(const std::string &timestamp)
{
#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<std::chrono::seconds> 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 <= 10min && duration >= -1min;
} 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)
{
auto it = std::find(_messageIDs.begin(), _messageIDs.end(), id);
@@ -436,7 +393,7 @@ EventSub::ParseWebSocketMessage(const EventSubWSClient::message_ptr &message)
OBSDataAutoRelease metadata = obs_data_get_obj(json, "metadata");
std::string timestamp =
obs_data_get_string(metadata, "message_timestamp");
if (_validateTimestamps && !isValidTimestamp(timestamp)) {
if (_validateTimestamps && !IsValidEventSubTimestamp(timestamp)) {
blog(LOG_WARNING,
"discarding Twitch EventSub with invalid timestamp %s",
timestamp.c_str());

View File

@@ -652,9 +652,6 @@ TwitchTokenSettingsDialog::TwitchTokenSettingsDialog(
}
_validateTimestamps->setChecked(settings._validateEventSubTimestamps);
#ifndef VERIFY_TIMESTAMPS
_validateTimestamps->hide();
#endif
_warnIfInvalid->setChecked(settings._warnIfInvalid);
_currentToken = settings;

View File

@@ -0,0 +1,86 @@
#include "twitch-timestamp.hpp"
#include <log-helper.hpp>
#include <chrono>
#include <ctime>
#include <regex>
#include <string>
using namespace std::chrono_literals;
namespace advss {
bool IsValidEventSubTimestamp(const std::string &timestamp)
{
// Example input: 2023-07-19T14:56:51.634234626Z
// or: 2023-07-19T14:56:51+05:30
static const std::regex pattern(
R"(^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})$)");
std::smatch m;
if (!std::regex_match(timestamp, m, pattern)) {
blog(LOG_WARNING, "failed to parse timestamp %s",
timestamp.c_str());
return false;
}
std::tm tm = {};
tm.tm_year = std::stoi(m[1]) - 1900;
tm.tm_mon = std::stoi(m[2]) - 1;
tm.tm_mday = std::stoi(m[3]);
tm.tm_hour = std::stoi(m[4]);
tm.tm_min = std::stoi(m[5]);
tm.tm_sec = std::stoi(m[6]);
// Range checks; timegm/_mkgmtime silently normalize out-of-range
// fields instead of failing, so we need to catch that ourselves.
if (tm.tm_mon < 0 || tm.tm_mon > 11 || tm.tm_mday < 1 ||
tm.tm_mday > 31 || tm.tm_hour > 23 || tm.tm_min > 59 ||
tm.tm_sec > 60 /* allow leap second */) {
blog(LOG_WARNING, "timestamp field out of range %s",
timestamp.c_str());
return false;
}
#ifdef _WIN32
time_t t = _mkgmtime(&tm);
#else
time_t t = timegm(&tm);
#endif
if (t == -1) {
blog(LOG_WARNING, "failed to convert timestamp %s",
timestamp.c_str());
return false;
}
// Reject dates that normalized to something else (e.g. Feb 30 -> Mar 2)
if (tm.tm_mday != std::stoi(m[3]) || tm.tm_mon != std::stoi(m[2]) - 1) {
blog(LOG_WARNING,
"timestamp date invalid after normalization %s",
timestamp.c_str());
return false;
}
const std::string &tz = m[7];
if (tz != "Z") {
int offSign = (tz[0] == '+') ? 1 : -1;
int offH = std::stoi(tz.substr(1, 2));
int offM = std::stoi(tz.substr(4, 2));
if (offH > 23 || offM > 59) {
blog(LOG_WARNING, "invalid timestamp offset %s",
timestamp.c_str());
return false;
}
time_t offsetSecs = (offH * 60 + offM) * 60;
t += (offSign > 0) ? -offsetSecs : offsetSecs;
}
auto parsedTime = std::chrono::system_clock::from_time_t(t);
auto now = std::chrono::system_clock::now();
auto duration = now - parsedTime;
// Clocks might be off by a bit, so allow negative values also
return duration <= 10min && duration >= -1min;
}
} // namespace advss

View File

@@ -0,0 +1,8 @@
#pragma once
#include <string>
namespace advss {
bool IsValidEventSubTimestamp(const std::string &timestamp);
} // namespace advss