Ajoute un ping serveur régulier comme troisième couche d'anti-triche sur Icare et la Corne
Build and deploy / deploy (push) Successful in 36s
Build and deploy / deploy (push) Successful in 36s
duration_ms/actions/merges restaient entièrement déclaratifs (calculés et envoyés par le client en une fois à la fin), donc falsifiables par un appel RPC direct. Le client envoie maintenant un ping toutes les 5s pendant la partie (ping_game_run), horodaté par le serveur, jamais par le client. submit_icarus_score/submit_melon_score corrèlent le nombre de pings et leur écart réel avec la durée annoncée (parties ≥ 15s uniquement) et signalent toute incohérence dans game_cheat_flags (missing_pings, ping_span_mismatch) — toujours en signalement silencieux, jamais de blocage, comme pour le reste de l'anti-triche. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -638,6 +638,15 @@ declare
|
||||
v_duration_ms numeric;
|
||||
v_actions numeric;
|
||||
v_details jsonb;
|
||||
-- Ping régulier envoyé par le client PENDANT la partie (ping_game_run(),
|
||||
-- section 18) : preuve horodatée par le SERVEUR, contrairement à
|
||||
-- duration_ms/actions ci-dessus qui restent entièrement déclaratifs
|
||||
-- (calculés et envoyés par le client en une seule fois à la fin, donc
|
||||
-- falsifiables par un appel RPC direct sans avoir vraiment joué).
|
||||
v_session_token uuid;
|
||||
v_ping_count integer;
|
||||
v_first_ping_at timestamptz;
|
||||
v_last_ping_at timestamptz;
|
||||
begin
|
||||
if auth.uid() is null then
|
||||
raise exception 'authentication required';
|
||||
@@ -680,6 +689,17 @@ begin
|
||||
v_actions := (p_run ->> 'actions')::numeric;
|
||||
end if;
|
||||
|
||||
-- Extraction protégée (begin/exception imbriqué) : une valeur
|
||||
-- malformée ne doit jamais faire échouer toute la soumission, juste
|
||||
-- être traitée comme une absence de token (donc 0 ping trouvé,
|
||||
-- signalé plus bas comme les autres incohérences).
|
||||
begin
|
||||
v_session_token := nullif(p_run ->> 'session_token', '')::uuid;
|
||||
exception
|
||||
when invalid_text_representation then
|
||||
v_session_token := null;
|
||||
end;
|
||||
|
||||
v_details := jsonb_build_object(
|
||||
'duration_ms', v_duration_ms,
|
||||
'actions', v_actions,
|
||||
@@ -747,6 +767,53 @@ begin
|
||||
v_details
|
||||
);
|
||||
end if;
|
||||
|
||||
-- En dessous de 15s, aucun ping n'est attendu (le premier n'arrive
|
||||
-- qu'après un cycle d'intervalle côté client, ~5s) : pas de
|
||||
-- signalement pour une partie courte, y compris un échec immédiat.
|
||||
if v_duration_ms >= 15000 then
|
||||
select count(*), min(created_at), max(created_at)
|
||||
into v_ping_count, v_first_ping_at, v_last_ping_at
|
||||
from public.game_run_pings
|
||||
where user_id = auth.uid() and game = 'icarus' and session_token = v_session_token;
|
||||
|
||||
if v_ping_count = 0 then
|
||||
insert into public.game_cheat_flags
|
||||
(user_id, game, score, severity, trigger_code, reason, details)
|
||||
values (
|
||||
auth.uid(),
|
||||
'icarus',
|
||||
p_score,
|
||||
'high',
|
||||
'missing_pings',
|
||||
'Aucun ping serveur reçu pendant une partie annoncée assez longue pour en attendre.',
|
||||
v_details || jsonb_build_object('ping_count', v_ping_count)
|
||||
);
|
||||
elsif extract(epoch from (v_last_ping_at - v_first_ping_at)) * 1000 < v_duration_ms * 0.4 then
|
||||
insert into public.game_cheat_flags
|
||||
(user_id, game, score, severity, trigger_code, reason, details)
|
||||
values (
|
||||
auth.uid(),
|
||||
'icarus',
|
||||
p_score,
|
||||
'high',
|
||||
'ping_span_mismatch',
|
||||
'L''écart réel entre les pings reçus est bien plus court que la durée de partie annoncée.',
|
||||
v_details || jsonb_build_object(
|
||||
'ping_count', v_ping_count,
|
||||
'ping_span_ms', extract(epoch from (v_last_ping_at - v_first_ping_at)) * 1000
|
||||
)
|
||||
);
|
||||
end if;
|
||||
end if;
|
||||
end if;
|
||||
|
||||
-- Nettoyage : les pings de cette partie ont été consommés (signalés ou
|
||||
-- non), inutile de les garder — évite aussi qu'un même session_token
|
||||
-- réutilisé par erreur ne corrèle avec une partie précédente.
|
||||
if v_session_token is not null then
|
||||
delete from public.game_run_pings
|
||||
where user_id = auth.uid() and session_token = v_session_token;
|
||||
end if;
|
||||
end if;
|
||||
|
||||
@@ -1750,6 +1817,14 @@ declare
|
||||
v_citizen_count integer;
|
||||
v_max_tier integer;
|
||||
v_details jsonb;
|
||||
-- Ping régulier envoyé par le client PENDANT la partie (ping_game_run(),
|
||||
-- section 18) : preuve horodatée par le SERVEUR, contrairement à
|
||||
-- duration_ms/actions/merges ci-dessus qui restent entièrement
|
||||
-- déclaratifs — voir la même remarque sur submit_icarus_score.
|
||||
v_session_token uuid;
|
||||
v_ping_count integer;
|
||||
v_first_ping_at timestamptz;
|
||||
v_last_ping_at timestamptz;
|
||||
begin
|
||||
if auth.uid() is null then
|
||||
raise exception 'authentication required';
|
||||
@@ -1802,6 +1877,17 @@ begin
|
||||
v_merges := (p_run ->> 'merges')::numeric;
|
||||
end if;
|
||||
|
||||
-- Extraction protégée (begin/exception imbriqué) : une valeur
|
||||
-- malformée ne doit jamais faire échouer toute la soumission, juste
|
||||
-- être traitée comme une absence de token (donc 0 ping trouvé,
|
||||
-- signalé plus bas comme les autres incohérences).
|
||||
begin
|
||||
v_session_token := nullif(p_run ->> 'session_token', '')::uuid;
|
||||
exception
|
||||
when invalid_text_representation then
|
||||
v_session_token := null;
|
||||
end;
|
||||
|
||||
v_details := jsonb_build_object(
|
||||
'duration_ms', v_duration_ms,
|
||||
'actions', v_actions,
|
||||
@@ -1888,6 +1974,53 @@ begin
|
||||
v_details
|
||||
);
|
||||
end if;
|
||||
|
||||
-- En dessous de 15s, aucun ping n'est attendu (le premier n'arrive
|
||||
-- qu'après un cycle d'intervalle côté client, ~5s) : pas de
|
||||
-- signalement pour une partie courte, y compris un échec immédiat.
|
||||
if v_duration_ms >= 15000 then
|
||||
select count(*), min(created_at), max(created_at)
|
||||
into v_ping_count, v_first_ping_at, v_last_ping_at
|
||||
from public.game_run_pings
|
||||
where user_id = auth.uid() and game = 'melon' and session_token = v_session_token;
|
||||
|
||||
if v_ping_count = 0 then
|
||||
insert into public.game_cheat_flags
|
||||
(user_id, game, score, severity, trigger_code, reason, details)
|
||||
values (
|
||||
auth.uid(),
|
||||
'melon',
|
||||
p_score,
|
||||
'high',
|
||||
'missing_pings',
|
||||
'Aucun ping serveur reçu pendant une partie annoncée assez longue pour en attendre.',
|
||||
v_details || jsonb_build_object('ping_count', v_ping_count)
|
||||
);
|
||||
elsif extract(epoch from (v_last_ping_at - v_first_ping_at)) * 1000 < v_duration_ms * 0.4 then
|
||||
insert into public.game_cheat_flags
|
||||
(user_id, game, score, severity, trigger_code, reason, details)
|
||||
values (
|
||||
auth.uid(),
|
||||
'melon',
|
||||
p_score,
|
||||
'high',
|
||||
'ping_span_mismatch',
|
||||
'L''écart réel entre les pings reçus est bien plus court que la durée de partie annoncée.',
|
||||
v_details || jsonb_build_object(
|
||||
'ping_count', v_ping_count,
|
||||
'ping_span_ms', extract(epoch from (v_last_ping_at - v_first_ping_at)) * 1000
|
||||
)
|
||||
);
|
||||
end if;
|
||||
end if;
|
||||
end if;
|
||||
|
||||
-- Nettoyage : les pings de cette partie ont été consommés (signalés ou
|
||||
-- non), inutile de les garder — évite aussi qu'un même session_token
|
||||
-- réutilisé par erreur ne corrèle avec une partie précédente.
|
||||
if v_session_token is not null then
|
||||
delete from public.game_run_pings
|
||||
where user_id = auth.uid() and session_token = v_session_token;
|
||||
end if;
|
||||
end if;
|
||||
|
||||
@@ -2034,3 +2167,50 @@ create policy "game_cheat_flags readable by judges"
|
||||
on public.game_cheat_flags for select
|
||||
to authenticated
|
||||
using (exists (select 1 from public.profiles p where p.id = auth.uid() and p.role = 'judge'));
|
||||
|
||||
-- 18. Anti-triche : ping régulier pendant la partie --------------------------
|
||||
-- Complète le signalement (section 17) : duration_ms/actions/merges dans
|
||||
-- p_run sont entièrement déclaratifs (calculés et envoyés par le client en
|
||||
-- une seule fois à la fin), donc falsifiables par quelqu'un qui appelle
|
||||
-- submit_icarus_score/submit_melon_score directement sans avoir vraiment
|
||||
-- joué. Le client envoie un ping régulier PENDANT la partie (toutes les
|
||||
-- ~5s tant qu'elle est en cours), horodaté ici par l'horloge du SERVEUR —
|
||||
-- submit_icarus_score/submit_melon_score (section 11/16) corrèlent ensuite
|
||||
-- le nombre de pings et l'écart réel entre le premier et le dernier avec
|
||||
-- la durée annoncée, et signalent (jamais ne bloquent, même philosophie
|
||||
-- que le reste de l'anti-triche) toute incohérence.
|
||||
create table if not exists public.game_run_pings (
|
||||
id bigint generated always as identity primary key,
|
||||
user_id uuid not null references public.profiles (id) on delete cascade,
|
||||
game text not null check (game in ('icarus', 'melon')),
|
||||
session_token uuid not null,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
alter table public.game_run_pings enable row level security;
|
||||
-- Aucun grant à authenticated/anon (même principe que l'ancien icarus_runs,
|
||||
-- section 11) : seules ping_game_run() (insert) et submit_icarus_score/
|
||||
-- submit_melon_score (lecture + nettoyage), toutes SECURITY DEFINER,
|
||||
-- contournent la RLS en tant que propriétaires des fonctions.
|
||||
|
||||
create or replace function public.ping_game_run(p_game text, p_session_token uuid)
|
||||
returns void
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
begin
|
||||
if auth.uid() is null then
|
||||
raise exception 'authentication required';
|
||||
end if;
|
||||
|
||||
if p_game not in ('icarus', 'melon') then
|
||||
raise exception 'invalid game';
|
||||
end if;
|
||||
|
||||
insert into public.game_run_pings (user_id, game, session_token)
|
||||
values (auth.uid(), p_game, p_session_token);
|
||||
end;
|
||||
$$;
|
||||
|
||||
grant execute on function public.ping_game_run(text, uuid) to authenticated;
|
||||
|
||||
Reference in New Issue
Block a user