Skip to content

Commit 338a478

Browse files
committed
Cleanup Twitch timestamp verification
1 parent 2658ced commit 338a478

9 files changed

Lines changed: 291 additions & 85 deletions

File tree

.gitmodules

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@
2828
[submodule "deps/libusb"]
2929
path = deps/libusb
3030
url = https://github.com/libusb/libusb.git
31-
[submodule "deps/date"]
32-
path = deps/date
33-
url = https://github.com/HowardHinnant/date.git
3431
[submodule "deps/jsoncons"]
3532
path = deps/jsoncons
3633
url = https://github.com/danielaparker/jsoncons.git

deps/date

Lines changed: 0 additions & 1 deletion
This file was deleted.

plugins/twitch/CMakeLists.txt

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -32,32 +32,6 @@ if(NOT ZLIB_FOUND)
3232
return()
3333
endif()
3434

35-
set(DATE_LIB_DIR "${ADVSS_SOURCE_DIR}/deps/date")
36-
if(EXISTS "${DATE_LIB_DIR}/CMakeLists.txt"
37-
AND NOT DISABLE_TWITCH_TIMESTAMP_VERIFICATION)
38-
set(BUILD_TZ_LIB ON)
39-
if(OS_WINDOWS)
40-
if(CURL_FOUND AND TARGET CURL::libcurl)
41-
get_target_property(CURL_INCLUDE_DIR CURL::libcurl
42-
INTERFACE_INCLUDE_DIRECTORIES)
43-
add_subdirectory("${DATE_LIB_DIR}" "${DATE_LIB_DIR}/build"
44-
EXCLUDE_FROM_ALL)
45-
target_include_directories(date-tz PRIVATE "${CURL_INCLUDE_DIR}")
46-
set(VERIFY_TWITCH_TIMESTAMPS ON)
47-
else()
48-
message(WARNING "CURL not found - not verifying Twitch timestamps")
49-
endif()
50-
else()
51-
add_subdirectory("${DATE_LIB_DIR}" "${DATE_LIB_DIR}/build" EXCLUDE_FROM_ALL)
52-
target_compile_options(date-tz PUBLIC -Wno-error=conversion
53-
-Wno-error=shadow)
54-
set(VERIFY_TWITCH_TIMESTAMPS ON)
55-
endif()
56-
else()
57-
message(WARNING "date lib not found in \"${DATE_LIB_DIR}\"!\n"
58-
"Twitch timestamps will not be checked!")
59-
endif()
60-
6135
# --- End of section ---
6236

6337
add_library(${PROJECT_NAME} MODULE)
@@ -92,6 +66,8 @@ target_sources(
9266
content-classification.hpp
9367
event-sub.cpp
9468
event-sub.hpp
69+
twitch-timestamp.cpp
70+
twitch-timestamp.hpp
9571
language-selection.cpp
9672
language-selection.hpp
9773
macro-action-twitch.cpp
@@ -114,13 +90,6 @@ set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
11490
target_include_directories(${PROJECT_NAME} PRIVATE "${CPP_HTTPLIB_DIR}/"
11591
"${OPENSSL_INCLUDE_DIR}")
11692
target_link_libraries(${PROJECT_NAME} PRIVATE ${OPENSSL_LIBRARIES} ZLIB::ZLIB)
117-
if(DEFINED VERIFY_TWITCH_TIMESTAMPS)
118-
target_compile_definitions(${PROJECT_NAME} PRIVATE VERIFY_TIMESTAMPS=1)
119-
target_link_libraries(${PROJECT_NAME} PRIVATE date::date-tz)
120-
if(OS_WINDOWS)
121-
target_link_libraries(${PROJECT_NAME} PRIVATE CURL::libcurl)
122-
endif()
123-
endif()
12493

12594
install_advss_plugin(${PROJECT_NAME})
12695
if(OS_WINDOWS)

plugins/twitch/event-sub.cpp

Lines changed: 2 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@
44

55
#include <log-helper.hpp>
66
#include <plugin-state-helpers.hpp>
7-
8-
#ifdef VERIFY_TIMESTAMPS
9-
#include "date/tz.h"
10-
#endif
7+
#include "twitch-timestamp.hpp"
118

129
using namespace std::chrono_literals;
1310

@@ -357,46 +354,6 @@ void EventSub::OnOpen(connection_hdl)
357354
_connected = true;
358355
}
359356

360-
static bool isValidTimestamp(const std::string &timestamp)
361-
{
362-
#ifdef VERIFY_TIMESTAMPS
363-
// Example input: 2023-07-19T14:56:51.634234626Z
364-
try {
365-
// Discard the nanosecond part
366-
static constexpr size_t dotPos = 19;
367-
std::string trimmed = timestamp.substr(0, dotPos);
368-
auto tzStart = timestamp.find_first_of("Z+-", dotPos);
369-
trimmed = timestamp.substr(0, dotPos);
370-
if (tzStart != std::string::npos) {
371-
trimmed += timestamp.substr(tzStart);
372-
}
373-
374-
std::istringstream in(trimmed);
375-
date::sys_time<std::chrono::seconds> parsedTime;
376-
in >> date::parse("%FT%TZ", parsedTime);
377-
if (in.fail()) {
378-
blog(LOG_WARNING, "failed to parse timestamp %s",
379-
timestamp.c_str());
380-
return false;
381-
}
382-
383-
auto now = date::zoned_time{date::current_zone(),
384-
std::chrono::system_clock::now()}
385-
.get_sys_time();
386-
387-
auto duration = now - parsedTime;
388-
// Clocks might be off by a bit, so allow negative values also
389-
return duration <= 10min && duration >= -1min;
390-
} catch (const std::exception &e) {
391-
blog(LOG_WARNING, "%s: %s", __func__, e.what());
392-
return false;
393-
}
394-
#else
395-
// Just assume timestamps are always valid
396-
return true;
397-
#endif
398-
}
399-
400357
bool EventSub::IsValidMessageID(const std::string &id)
401358
{
402359
auto it = std::find(_messageIDs.begin(), _messageIDs.end(), id);
@@ -436,7 +393,7 @@ EventSub::ParseWebSocketMessage(const EventSubWSClient::message_ptr &message)
436393
OBSDataAutoRelease metadata = obs_data_get_obj(json, "metadata");
437394
std::string timestamp =
438395
obs_data_get_string(metadata, "message_timestamp");
439-
if (_validateTimestamps && !isValidTimestamp(timestamp)) {
396+
if (_validateTimestamps && !IsValidEventSubTimestamp(timestamp)) {
440397
blog(LOG_WARNING,
441398
"discarding Twitch EventSub with invalid timestamp %s",
442399
timestamp.c_str());

plugins/twitch/token.cpp

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -652,9 +652,6 @@ TwitchTokenSettingsDialog::TwitchTokenSettingsDialog(
652652
}
653653

654654
_validateTimestamps->setChecked(settings._validateEventSubTimestamps);
655-
#ifndef VERIFY_TIMESTAMPS
656-
_validateTimestamps->hide();
657-
#endif
658655
_warnIfInvalid->setChecked(settings._warnIfInvalid);
659656

660657
_currentToken = settings;
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#include "twitch-timestamp.hpp"
2+
3+
#include <log-helper.hpp>
4+
5+
#include <chrono>
6+
#include <ctime>
7+
#include <regex>
8+
#include <string>
9+
10+
using namespace std::chrono_literals;
11+
12+
namespace advss {
13+
14+
bool IsValidEventSubTimestamp(const std::string &timestamp)
15+
{
16+
// Example input: 2023-07-19T14:56:51.634234626Z
17+
// or: 2023-07-19T14:56:51+05:30
18+
static const std::regex pattern(
19+
R"(^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})$)");
20+
21+
std::smatch m;
22+
if (!std::regex_match(timestamp, m, pattern)) {
23+
blog(LOG_WARNING, "failed to parse timestamp %s",
24+
timestamp.c_str());
25+
return false;
26+
}
27+
28+
std::tm tm = {};
29+
tm.tm_year = std::stoi(m[1]) - 1900;
30+
tm.tm_mon = std::stoi(m[2]) - 1;
31+
tm.tm_mday = std::stoi(m[3]);
32+
tm.tm_hour = std::stoi(m[4]);
33+
tm.tm_min = std::stoi(m[5]);
34+
tm.tm_sec = std::stoi(m[6]);
35+
36+
// Range checks; timegm/_mkgmtime silently normalize out-of-range
37+
// fields instead of failing, so we need to catch that ourselves.
38+
if (tm.tm_mon < 0 || tm.tm_mon > 11 || tm.tm_mday < 1 ||
39+
tm.tm_mday > 31 || tm.tm_hour > 23 || tm.tm_min > 59 ||
40+
tm.tm_sec > 60 /* allow leap second */) {
41+
blog(LOG_WARNING, "timestamp field out of range %s",
42+
timestamp.c_str());
43+
return false;
44+
}
45+
46+
#ifdef _WIN32
47+
time_t t = _mkgmtime(&tm);
48+
#else
49+
time_t t = timegm(&tm);
50+
#endif
51+
if (t == -1) {
52+
blog(LOG_WARNING, "failed to convert timestamp %s",
53+
timestamp.c_str());
54+
return false;
55+
}
56+
57+
// Reject dates that normalized to something else (e.g. Feb 30 -> Mar 2)
58+
if (tm.tm_mday != std::stoi(m[3]) || tm.tm_mon != std::stoi(m[2]) - 1) {
59+
blog(LOG_WARNING,
60+
"timestamp date invalid after normalization %s",
61+
timestamp.c_str());
62+
return false;
63+
}
64+
65+
const std::string &tz = m[7];
66+
if (tz != "Z") {
67+
int offSign = (tz[0] == '+') ? 1 : -1;
68+
int offH = std::stoi(tz.substr(1, 2));
69+
int offM = std::stoi(tz.substr(4, 2));
70+
if (offH > 23 || offM > 59) {
71+
blog(LOG_WARNING, "invalid timestamp offset %s",
72+
timestamp.c_str());
73+
return false;
74+
}
75+
time_t offsetSecs = (offH * 60 + offM) * 60;
76+
t += (offSign > 0) ? -offsetSecs : offsetSecs;
77+
}
78+
79+
auto parsedTime = std::chrono::system_clock::from_time_t(t);
80+
auto now = std::chrono::system_clock::now();
81+
auto duration = now - parsedTime;
82+
// Clocks might be off by a bit, so allow negative values also
83+
return duration <= 10min && duration >= -1min;
84+
}
85+
86+
} // namespace advss
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#pragma once
2+
#include <string>
3+
4+
namespace advss {
5+
6+
bool IsValidEventSubTimestamp(const std::string &timestamp);
7+
8+
} // namespace advss

tests/CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,16 @@ target_sources(
202202
${ADVSS_SOURCE_DIR}/plugins/base/macro-condition-window.cpp
203203
${ADVSS_SOURCE_DIR}/plugins/base/utils/window-selection.cpp)
204204

205+
# --- twitch timestamp --- #
206+
207+
set(TWITCH_PLUGIN_DIR
208+
"${ADVSS_SOURCE_DIR}/plugins/twitch"
209+
CACHE INTERNAL "")
210+
target_include_directories(${PROJECT_NAME} PRIVATE "${TWITCH_PLUGIN_DIR}")
211+
target_sources(
212+
${PROJECT_NAME} PRIVATE test-twitch-timestamp.cpp
213+
"${TWITCH_PLUGIN_DIR}/twitch-timestamp.cpp")
214+
205215
# --- Testing --- #
206216

207217
enable_testing()

0 commit comments

Comments
 (0)