<%@ WebHandler Language="C#" Class="Leaderboard" %> using System; using System.Web; using System.IO; using System.Linq; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using System.Web.Script.Serialization; public class Leaderboard : IHttpHandler, System.Web.SessionState.IRequiresSessionState { private static readonly HashSet allowedGameKeys = new HashSet { "snake", "muncher", "chain" }; private const int maxFileSize = 5 * 1024 * 1024; // 5 MB private const int limitTop = 3; // top N scores private const int rateLimit = 30; // max requests per window private const int rateWindow = 60; // seconds private const int cacheTTL = 5; // seconds private const int tokenLifetimeMinutes = 10; // token expires after 10 minutes public void ProcessRequest(HttpContext context) { SetHeaders(context); // --- 1. Validate token from session --- string token = (context.Request["token"] ?? "").Trim(); string sessionToken = context.Session["game_token"] as string; long? sessionTokenTime = context.Session["game_token_time"] as long?; if (string.IsNullOrEmpty(token) || sessionToken == null || token != sessionToken) { Respond(context, 401, new { success = false, error = "Invalid or missing token", scores = new object[] { } }); return; } // Optional: check token expiration if (sessionTokenTime.HasValue) { long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (now - sessionTokenTime.Value > tokenLifetimeMinutes * 60) { context.Session.Remove("game_token"); context.Session.Remove("game_token_time"); Respond(context, 401, new { success = false, error = "Token expired", scores = new object[] { } }); return; } } // --- 2. Rate limiting --- string ip = GetClientIP(context); if (!CheckRateLimit(context, ip)) { Respond(context, 429, new { success = false, error = "Rate limit exceeded", scores = new object[] { } }); return; } // --- 3. Validate game --- string game = (context.Request["game"] ?? "").Trim().ToLower(); if (!Regex.IsMatch(game, "^[a-z]+$") || !allowedGameKeys.Contains(game)) { Respond(context, 400, new { success = false, error = "Invalid game parameter", scores = new object[] { } }); return; } string file = context.Server.MapPath("~/data/" + game + "_scores.csv"); if (!File.Exists(file)) { Respond(context, 200, new { success = true, scores = new object[] { } }); return; } if (new FileInfo(file).Length > maxFileSize) { Respond(context, 500, new { success = false, error = "Leaderboard too large", scores = new object[] { } }); return; } // --- 4. Cache handling --- string cacheFile = context.Server.MapPath("~/data/cache_" + game + ".json"); if (File.Exists(cacheFile) && (DateTime.Now - File.GetLastWriteTime(cacheFile)).TotalSeconds < cacheTTL) { context.Response.Write(File.ReadAllText(cacheFile)); return; } // --- 5. Parse CSV --- var bestScores = new Dictionary(); try { using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read)) using (var sr = new StreamReader(fs)) { string line; bool isHeader = true; while ((line = sr.ReadLine()) != null) { if (isHeader) { isHeader = false; continue; } var parts = line.Split(','); if (parts.Length < 7) continue; string emailRaw = Clean(parts[1]); string title = Clean(parts[3]); string zone = Clean(parts[6]); int score; if (!int.TryParse(parts[2], out score)) continue; int time; if (!int.TryParse(parts[4], out time)) continue; string ts = parts[5]; string email = emailRaw.ToLower().Trim(); if (!IsValidEmail(email)) continue; email = Truncate(email, 100); title = Truncate(title, 60); zone = Truncate(zone, 20); if (score < 0 || score > 1000) continue; if (time < 0 || time > 3600) continue; if (!bestScores.ContainsKey(email) || score > bestScores[email].score) { bestScores[email] = new ScoreEntry { game = game, email = email, score = score, title = title, time = time, ts = ts, zone = zone }; } } } } catch { Respond(context, 500, new { success = false, error = "Unable to read leaderboard", scores = new object[] { } }); return; } // --- 6. Sort and return top scores --- var sorted = bestScores.Values .OrderByDescending(x => x.score) .ThenBy(x => x.time) .ThenByDescending(x => x.ts) .Take(limitTop) .ToList(); var response = new { success = true, scores = sorted }; string json = new JavaScriptSerializer().Serialize(response); File.WriteAllText(cacheFile, json); context.Response.Write(json); } #region Helpers private void SetHeaders(HttpContext context) { context.Response.ContentType = "application/json; charset=utf-8"; context.Response.Headers.Remove("X-Powered-By"); context.Response.Headers.Add("X-Content-Type-Options", "nosniff"); context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN"); context.Response.Headers.Add("Referrer-Policy", "no-referrer"); context.Response.Headers.Add("Cache-Control", "no-store, no-cache, must-revalidate"); } private void Respond(HttpContext context, int statusCode, object obj) { context.Response.StatusCode = statusCode; string json = new JavaScriptSerializer().Serialize(obj); context.Response.Write(json); } private string GetClientIP(HttpContext context) { string ip = context.Request.ServerVariables["REMOTE_ADDR"]; string forwarded = context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"]; if (!string.IsNullOrEmpty(forwarded)) ip = forwarded.Split(',')[0].Trim(); return ip ?? "UNKNOWN"; } private bool CheckRateLimit(HttpContext context, string ip) { string path = context.Server.MapPath("~/data/rl_lb_" + Hash(ip) + ".json"); List requests = new List(); if (File.Exists(path)) { try { requests = new JavaScriptSerializer().Deserialize>(File.ReadAllText(path)); } catch { } } long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); requests = requests.Where(t => t > now - rateWindow).ToList(); if (requests.Count >= rateLimit) return false; requests.Add(now); File.WriteAllText(path, new JavaScriptSerializer().Serialize(requests)); return true; } private string Clean(string input) { return Regex.Replace(input ?? "", @"[\x00-\x1F\x7F]", ""); } private bool IsValidEmail(string email) { try { return new System.Net.Mail.MailAddress(email).Address == email; } catch { return false; } } private string Truncate(string value, int maxLength) { if (string.IsNullOrEmpty(value)) return ""; return value.Length <= maxLength ? value : value.Substring(0, maxLength); } private string Hash(string input) { using (var sha = System.Security.Cryptography.SHA256.Create()) { var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(input)); return BitConverter.ToString(bytes).Replace("-", "").ToLower(); } } #endregion public bool IsReusable { get { return false; } } class ScoreEntry { public string game; public string email; public int score; public string title; public int time; public string ts; public string zone; } }