mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-08-06 04:44:32 -05:00
Cleanup Twitch timestamp verification
This commit is contained in:
parent
2658ced18e
commit
338a478ead
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -28,9 +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
|
||||
[submodule "deps/jsoncons"]
|
||||
path = deps/jsoncons
|
||||
url = https://github.com/danielaparker/jsoncons.git
|
||||
|
|
|
|||
1
deps/date
vendored
1
deps/date
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 5bdb7e6f31fac909c090a46dbd9fea27b6e609a4
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ×tamp)
|
||||
{
|
||||
#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());
|
||||
|
|
|
|||
|
|
@ -652,9 +652,6 @@ TwitchTokenSettingsDialog::TwitchTokenSettingsDialog(
|
|||
}
|
||||
|
||||
_validateTimestamps->setChecked(settings._validateEventSubTimestamps);
|
||||
#ifndef VERIFY_TIMESTAMPS
|
||||
_validateTimestamps->hide();
|
||||
#endif
|
||||
_warnIfInvalid->setChecked(settings._warnIfInvalid);
|
||||
|
||||
_currentToken = settings;
|
||||
|
|
|
|||
86
plugins/twitch/twitch-timestamp.cpp
Normal file
86
plugins/twitch/twitch-timestamp.cpp
Normal 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 ×tamp)
|
||||
{
|
||||
// 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
|
||||
8
plugins/twitch/twitch-timestamp.hpp
Normal file
8
plugins/twitch/twitch-timestamp.hpp
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
|
||||
namespace advss {
|
||||
|
||||
bool IsValidEventSubTimestamp(const std::string ×tamp);
|
||||
|
||||
} // namespace advss
|
||||
|
|
@ -202,6 +202,16 @@ target_sources(
|
|||
${ADVSS_SOURCE_DIR}/plugins/base/macro-condition-window.cpp
|
||||
${ADVSS_SOURCE_DIR}/plugins/base/utils/window-selection.cpp)
|
||||
|
||||
# --- twitch timestamp --- #
|
||||
|
||||
set(TWITCH_PLUGIN_DIR
|
||||
"${ADVSS_SOURCE_DIR}/plugins/twitch"
|
||||
CACHE INTERNAL "")
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE "${TWITCH_PLUGIN_DIR}")
|
||||
target_sources(
|
||||
${PROJECT_NAME} PRIVATE test-twitch-timestamp.cpp
|
||||
"${TWITCH_PLUGIN_DIR}/twitch-timestamp.cpp")
|
||||
|
||||
# --- Testing --- #
|
||||
|
||||
enable_testing()
|
||||
|
|
|
|||
183
tests/test-twitch-timestamp.cpp
Normal file
183
tests/test-twitch-timestamp.cpp
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
#include "catch.hpp"
|
||||
|
||||
#include <twitch-timestamp.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
// Build an RFC3339 UTC timestamp offset by 'offsetSeconds' from now.
|
||||
// A positive value means that many seconds in the past.
|
||||
static std::string makeTimestamp(int offsetSeconds = 0, bool withNanos = false,
|
||||
const char *tzSuffix = "Z")
|
||||
{
|
||||
auto now = std::chrono::system_clock::now() -
|
||||
std::chrono::seconds(offsetSeconds);
|
||||
time_t t = std::chrono::system_clock::to_time_t(now);
|
||||
std::tm tm = {};
|
||||
#ifdef _WIN32
|
||||
gmtime_s(&tm, &t);
|
||||
#else
|
||||
gmtime_r(&t, &tm);
|
||||
#endif
|
||||
char buf[64];
|
||||
if (withNanos) {
|
||||
snprintf(buf, sizeof(buf),
|
||||
"%04d-%02d-%02dT%02d:%02d:%02d.123456789%s",
|
||||
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
|
||||
tm.tm_hour, tm.tm_min, tm.tm_sec, tzSuffix);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d%s",
|
||||
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
|
||||
tm.tm_hour, tm.tm_min, tm.tm_sec, tzSuffix);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
TEST_CASE("Valid timestamps are accepted", "[twitch][timestamp]")
|
||||
{
|
||||
SECTION("Current time with Z suffix")
|
||||
{
|
||||
REQUIRE(advss::IsValidEventSubTimestamp(makeTimestamp(0)));
|
||||
}
|
||||
|
||||
SECTION("5 seconds in the past")
|
||||
{
|
||||
REQUIRE(advss::IsValidEventSubTimestamp(makeTimestamp(5)));
|
||||
}
|
||||
|
||||
SECTION("9 minutes in the past (within 10-minute window)")
|
||||
{
|
||||
REQUIRE(advss::IsValidEventSubTimestamp(makeTimestamp(9 * 60)));
|
||||
}
|
||||
|
||||
SECTION("Timestamp with nanoseconds and Z suffix")
|
||||
{
|
||||
REQUIRE(advss::IsValidEventSubTimestamp(
|
||||
makeTimestamp(0, true, "Z")));
|
||||
}
|
||||
|
||||
SECTION("Lowercase z suffix is rejected (RFC 3339 requires uppercase Z)")
|
||||
{
|
||||
REQUIRE_FALSE(advss::IsValidEventSubTimestamp(
|
||||
makeTimestamp(0, false, "z")));
|
||||
}
|
||||
|
||||
SECTION("+00:00 offset (UTC expressed as positive offset)")
|
||||
{
|
||||
REQUIRE(advss::IsValidEventSubTimestamp(
|
||||
makeTimestamp(0, false, "+00:00")));
|
||||
}
|
||||
|
||||
SECTION("-00:00 offset")
|
||||
{
|
||||
REQUIRE(advss::IsValidEventSubTimestamp(
|
||||
makeTimestamp(0, false, "-00:00")));
|
||||
}
|
||||
|
||||
SECTION("+05:30 offset (IST) with nanoseconds")
|
||||
{
|
||||
// Build a timestamp whose wall-clock value is now, expressed in
|
||||
// IST (+05:30). The UTC equivalent must still be within the window.
|
||||
auto now = std::chrono::system_clock::now();
|
||||
time_t t = std::chrono::system_clock::to_time_t(now);
|
||||
// Advance the displayed time by 5h30m to represent +05:30
|
||||
t += (5 * 60 + 30) * 60;
|
||||
std::tm tm = {};
|
||||
#ifdef _WIN32
|
||||
gmtime_s(&tm, &t);
|
||||
#else
|
||||
gmtime_r(&t, &tm);
|
||||
#endif
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"%04d-%02d-%02dT%02d:%02d:%02d.000000000+05:30",
|
||||
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
|
||||
tm.tm_hour, tm.tm_min, tm.tm_sec);
|
||||
REQUIRE(advss::IsValidEventSubTimestamp(buf));
|
||||
}
|
||||
|
||||
SECTION("-08:00 offset (PST)")
|
||||
{
|
||||
auto now = std::chrono::system_clock::now();
|
||||
time_t t = std::chrono::system_clock::to_time_t(now);
|
||||
t -= 8 * 3600;
|
||||
std::tm tm = {};
|
||||
#ifdef _WIN32
|
||||
gmtime_s(&tm, &t);
|
||||
#else
|
||||
gmtime_r(&t, &tm);
|
||||
#endif
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"%04d-%02d-%02dT%02d:%02d:%02d-08:00",
|
||||
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
|
||||
tm.tm_hour, tm.tm_min, tm.tm_sec);
|
||||
REQUIRE(advss::IsValidEventSubTimestamp(buf));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Expired timestamps are rejected", "[twitch][timestamp]")
|
||||
{
|
||||
SECTION("11 minutes in the past (outside 10-minute window)")
|
||||
{
|
||||
REQUIRE_FALSE(advss::IsValidEventSubTimestamp(
|
||||
makeTimestamp(11 * 60)));
|
||||
}
|
||||
|
||||
SECTION("1 hour in the past")
|
||||
{
|
||||
REQUIRE_FALSE(
|
||||
advss::IsValidEventSubTimestamp(makeTimestamp(3600)));
|
||||
}
|
||||
|
||||
SECTION("2 minutes in the future (outside -1min allowance)")
|
||||
{
|
||||
REQUIRE_FALSE(advss::IsValidEventSubTimestamp(
|
||||
makeTimestamp(-2 * 60)));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Invalid timestamp strings are rejected", "[twitch][timestamp]")
|
||||
{
|
||||
SECTION("Empty string")
|
||||
{
|
||||
REQUIRE_FALSE(advss::IsValidEventSubTimestamp(""));
|
||||
}
|
||||
|
||||
SECTION("Completely wrong format")
|
||||
{
|
||||
REQUIRE_FALSE(
|
||||
advss::IsValidEventSubTimestamp("not-a-timestamp"));
|
||||
}
|
||||
|
||||
SECTION("Missing time component")
|
||||
{
|
||||
REQUIRE_FALSE(advss::IsValidEventSubTimestamp("2023-07-19Z"));
|
||||
}
|
||||
|
||||
SECTION("Missing UTC offset")
|
||||
{
|
||||
REQUIRE_FALSE(advss::IsValidEventSubTimestamp(
|
||||
"2023-07-19T14:56:51.634234626"));
|
||||
}
|
||||
|
||||
SECTION("Malformed offset - missing minutes")
|
||||
{
|
||||
REQUIRE_FALSE(advss::IsValidEventSubTimestamp(
|
||||
"2023-07-19T14:56:51+05"));
|
||||
}
|
||||
|
||||
SECTION("Malformed offset - non-numeric")
|
||||
{
|
||||
REQUIRE_FALSE(advss::IsValidEventSubTimestamp(
|
||||
"2023-07-19T14:56:51+XX:XX"));
|
||||
}
|
||||
|
||||
SECTION("Truncated after seconds")
|
||||
{
|
||||
REQUIRE_FALSE(
|
||||
advss::IsValidEventSubTimestamp("2023-07-19T14:56:"));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user