diff --git a/orbisrpc/daemon.c b/orbisrpc/daemon.c index 1d463ac..6593f05 100644 --- a/orbisrpc/daemon.c +++ b/orbisrpc/daemon.c @@ -16,6 +16,7 @@ #include "cfg.h" #include "clock.h" #include "lock.h" +#include "timesync.h" #include "log.h" #include "ws.h" #include "discord.h" @@ -96,6 +97,9 @@ int daemon_run(const char *fixed_game_name){ if(!g_cfg.enabled){ log_msg("disabled in config; exiting"); log_close(); return 0; } log_set_debug(g_cfg.debug); if(g_cfg.debug) log_dbg("debug logging on (config)"); + /* Wall-clock correction for Discord timestamps (PSN time sync is + * typically blocked on jailbroken consoles). Non-fatal. */ + time_sync(); if(!have_token(&g_cfg)){ log_msg("FATAL: put your Discord user token in %s as \"token\":\"...\"", CFG_PATH); log_close(); @@ -158,6 +162,7 @@ int daemon_run(const char *fixed_game_name){ static char last[128] = ""; static char sess_tid[16] = ""; static int64_t started = 0; + static int64_t last_tsync = 0; /* Resume window: if the same title vanishes briefly (detection * flicker, quick menu hop) and returns within 10 minutes, the * timer resumes instead of resetting to 0:00. */ @@ -173,6 +178,7 @@ int daemon_run(const char *fixed_game_name){ int need_post = active && last[0]; while(!s_stop){ /* inner: live session, serviced every second */ int64_t now = orbis_mono_s(); + if(now - last_tsync >= 3600){ last_tsync = now; time_sync(); } if(now != last_poll){ last_poll = now; if(now - last_alive >= 60){ @@ -214,7 +220,7 @@ int daemon_run(const char *fixed_game_name){ log_msg("SESSION_RESUMED title=%s (gap %llds, timer kept)", cur_tid[0]?cur_tid:"?", (long long)(now - prev_end)); } else { - started = time(NULL); + started = time_fixed(); } need_post = 1; log_msg("GAME_DETECTED title=%s name=%s", cur_tid[0]?cur_tid:"?", name); diff --git a/orbisrpc/timesync.c b/orbisrpc/timesync.c new file mode 100644 index 0000000..fa37742 --- /dev/null +++ b/orbisrpc/timesync.c @@ -0,0 +1,92 @@ +/* timesync.c - minimal SNTP client (RFC 5905, client mode only). + * One UDP exchange per call; 3s I/O budget; any failure keeps the old + * offset. Host-testable (POSIX only). */ +#include "timesync.h" +#include "log.h" +#include +#include +#include +#include +#include +#ifdef ORBISRPC_SDK_PAYLOAD +#include +#include +#include +#include +#else +#include +#include +#include +#include +#endif + +#define NTP_EPOCH_OFFSET 2208988800u +#define NTP_PORT 123 + +static int64_t s_offset = 0; +static int s_synced = 0; + +static int sntp_once(const char *host){ + unsigned char pkt[48]; + memset(pkt, 0, sizeof pkt); + pkt[0] = 0x1b; /* LI=0, VN=3, Mode=3 (client) */ +#ifdef ORBISRPC_SDK_PAYLOAD + struct addrinfo hints, *res = NULL; + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_DGRAM; + { + char portbuf[8]; + snprintf(portbuf, sizeof portbuf, "%d", NTP_PORT); + if(getaddrinfo(host, portbuf, &hints, &res) != 0 || !res) return -1; + } + int fd = socket(AF_INET, SOCK_DGRAM, 0); + if(fd < 0){ freeaddrinfo(res); return -1; } + struct timeval tv = { 3, 0 }; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); + int ok = -1; + if(sendto(fd, pkt, sizeof pkt, 0, res->ai_addr, res->ai_addrlen) == (int)sizeof pkt){ + unsigned char rep[48]; + ssize_t n = recv(fd, (char *)rep, sizeof rep, 0); + if(n >= 48){ + uint32_t tx; + memcpy(&tx, rep + 40, 4); + tx = ((tx & 0xff) << 24) | ((tx & 0xff00) << 8) | + ((tx & 0xff0000) >> 8) | ((tx & 0xff000000) >> 24); + if(tx > NTP_EPOCH_OFFSET + 1700000000u){ + int64_t ntp_unix = (int64_t)(tx - NTP_EPOCH_OFFSET); + s_offset = ntp_unix - (int64_t)time(NULL); + s_synced = 1; + ok = 0; + } + } + } + freeaddrinfo(res); + close(fd); + return ok; +#else + (void)host; + return -1; +#endif +} + +int time_sync(void){ + static const char *hosts[] = { + "time.google.com", "pool.ntp.org", "time.cloudflare.com", NULL + }; + for(int i = 0; hosts[i]; i++){ + if(sntp_once(hosts[i]) == 0){ + log_msg("time sync ok via %s (offset %+llds)", + hosts[i], (long long)s_offset); + return 0; + } + } + log_msg("time sync failed; using local clock (offset %+llds)", + (long long)s_offset); + return -1; +} + +int64_t time_fixed(void){ + return (int64_t)time(NULL) + s_offset; +} diff --git a/orbisrpc/timesync.h b/orbisrpc/timesync.h new file mode 100644 index 0000000..8078077 --- /dev/null +++ b/orbisrpc/timesync.h @@ -0,0 +1,17 @@ +/* timesync.h - SNTP wall-clock correction for Discord timestamps. + * + * Jailbroken consoles commonly block Sony servers (ban avoidance), which + * also kills PSN time sync — the PS4 clock then drifts (observed: +38min + * and growing, plus wrong timezones). Discord timestamps must be true + * UTC or the timer wedges at 0:00, so the daemon syncs itself against + * public NTP (no Sony involved) and stamps sessions with corrected time. + * Failure degrades to the local clock (old behavior), never fatal. */ +#ifndef ORBISRPC_TIMESYNC_H +#define ORBISRPC_TIMESYNC_H +#include +/* Try an SNTP exchange; on success stores offset and returns 0. -1 = keep + * old offset (first boot without sync behaves like before). */ +int time_sync(void); +/* Corrected wall clock for Discord epoch stamps. */ +int64_t time_fixed(void); +#endif diff --git a/scripts/build_sdk.sh b/scripts/build_sdk.sh index d7789df..a837fd9 100755 --- a/scripts/build_sdk.sh +++ b/scripts/build_sdk.sh @@ -15,7 +15,7 @@ OUT="build-sdk" mkdir -p "$OUT" CFLAGS="-O2 -Wall -DORBISRPC_SDK_PAYLOAD -Iorbisrpc -Ithird_party/mbedtls/include" echo "=== daemon sources (SDK) ===" -for f in log cfg jsonlite b64 sfo tmdb_crypto tmdb updater updater_util tls ws detect discord daemon compat clock lock main; do +for f in log cfg jsonlite b64 sfo tmdb_crypto tmdb updater updater_util tls ws detect discord daemon compat clock lock timesync main; do # clock has no .c (header-only helper lives in compat.c); skip if missing [ -f "orbisrpc/$f.c" ] || continue "$CC" $CFLAGS -c -o "$OUT/$f.o" "orbisrpc/$f.c" || { echo "FAIL: $f"; exit 1; }