From 9a70ec6a48e96051ef4dbdfd071a5d4c1fb1f5da Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Thu, 25 Jul 2019 20:38:27 -0400 Subject: [PATCH] Give private replays unguessable URLs Future private replays will be saved under URLs that can't be guessed. Old private replays should be unaffected. Replays saved for locks will never have an unguessable URL, but they can still be considered private (and not show up in searches). --- js/client.js | 5 +-- replays/battle.log.php | 21 ++++++++++-- replays/battle.php | 44 +++++++++++++++++++----- replays/replays.lib.php | 76 +++++++++++++++++++++++++++++------------ replays/search.php | 4 ++- 5 files changed, 115 insertions(+), 35 deletions(-) diff --git a/js/client.js b/js/client.js index ac6c1db4e..b9715ee70 100644 --- a/js/client.js +++ b/js/client.js @@ -1262,8 +1262,9 @@ function toId() { log: data.log, id: id }, function (data) { - if (data === 'success') { - app.addPopup(ReplayUploadedPopup, {id: id}); + var sData = data.split(':'); + if (sData[0] === 'success') { + app.addPopup(ReplayUploadedPopup, {id: sData[1] || id}); } else if (data === 'hash mismatch') { app.addPopupMessage("Someone else is already uploading a replay of this battle. Try again in five seconds."); } else if (data === 'not found') { diff --git a/replays/battle.log.php b/replays/battle.log.php index a2c809774..2a2027a17 100644 --- a/replays/battle.log.php +++ b/replays/battle.log.php @@ -20,13 +20,30 @@ $manage = false; require_once 'replays.lib.php'; $replay = null; -if (@$_REQUEST['name']) { - $replay = $Replays->get($_REQUEST['name']); +$id = $_REQUEST['name'] ?? ''; +$password = ''; + +$fullid = $id; +if (substr($id, -2) === 'pw') { + $dashpos = strrpos($id, '-'); + $password = substr($id, $dashpos + 1, -2); + $id = substr($id, 0, $dashpos); + // die($id . ' ' . $password); +} + +if ($id) { + $replay = $Replays->get($id); } if (!$replay) { header('HTTP/1.1 404 Not Found'); die(); } +if ($replay['password'] ?? null) { + if ($password !== $replay['password']) { + header('HTTP/1.1 404 Not Found'); + die(); + } +} $replay['log'] = str_replace("\r","",$replay['log']); diff --git a/replays/battle.php b/replays/battle.php index 578ccba12..800c4dcab 100644 --- a/replays/battle.php +++ b/replays/battle.php @@ -6,7 +6,10 @@ ini_set('display_startup_errors', TRUE); include_once 'theme/panels.lib.php'; -if (!@$_REQUEST['name']) { +$id = $_REQUEST['name'] ?? ''; +$password = ''; + +if (!$id) { include '404.php'; die(); } @@ -20,7 +23,15 @@ if (isset($_REQUEST['manage'])) { $manage = true; } -if (preg_match('/[^A-Za-z0-9-]/', $_REQUEST['name'])) die("access denied"); +if (preg_match('/[^A-Za-z0-9-]/', $id)) die("access denied"); + +$fullid = $id; +if (substr($id, -2) === 'pw') { + $dashpos = strrpos($id, '-'); + $password = substr($id, $dashpos + 1, -2); + $id = substr($id, 0, $dashpos); + // die($id . ' ' . $password); +} $replay = null; $cached = false; @@ -28,8 +39,8 @@ $cached = false; // $forcecache = isset($_REQUEST['forcecache8723']); $forcecache = false; -if (file_exists('caches/' . $_REQUEST['name'] . '.inc.php')) { - include 'caches/' . $_REQUEST['name'] . '.inc.php'; +if (file_exists('caches/' . $id . '.inc.php')) { + include 'caches/' . $id . '.inc.php'; $replay['formatid'] = ''; $cached = true; } else { @@ -38,17 +49,34 @@ if (file_exists('caches/' . $_REQUEST['name'] . '.inc.php')) { include '503.php'; die(); } - $replay = $Replays->get($_REQUEST['name'], $forcecache); + $replay = $Replays->get($id, $forcecache); } if (!$replay) { include '404.php'; die(); } -if (@$replay['private']) header('X-Robots-Tag: noindex'); +if (@$replay['private']) { + header('X-Robots-Tag: noindex'); +} +if (@$replay['password']) { + if (!$password && !$manage) { + require_once '../lib/ntbb-session.lib.php'; + if ($curuser['userid'] !== $replay['p1id'] && $curuser['userid'] !== $replay['p2id']) { + die("Access denied (you must be logged into " . $replay['p1id'] . " or " . $replay['p2id'] . ")"); + } + $url = '/' . $id . '-' . $replay['password'] . 'pw'; + echo '

This private replay now has a new harder-to-guess URL:

'; + echo '

https://replay.pokemonshowdown.com' . $url . '

'; + die(); + } + if ($password !== $replay['password'] && !$manage) { + die("Access denied (please ask " . $replay['p1id'] . " or " . $replay['p2id'] . " for the password)"); + } +} if ($forcecache) { - file_put_contents('caches/' . $_REQUEST['name'] . '.inc.php', 'start(); THIS REPLAY IS PRIVATE - make sure you have the owner\'s permission to share
'; ?> -
+

: vs.

diff --git a/replays/replays.lib.php b/replays/replays.lib.php index aa18cdde2..e9582949c 100644 --- a/replays/replays.lib.php +++ b/replays/replays.lib.php @@ -31,6 +31,15 @@ class Replays { $this->db->setAttribute(PDO::ATTR_EMULATE_PREPARES, FALSE); } + function genPassword() { + $alphabet = '0123456789abcdefghijklmnopqrstuvwxyz'; + $password = ''; + for ($i = 0; $i < 31; $i++) { + $password .= $alphabet{mt_rand(0, 35)}; + } + return $password; + } + function get($id, $force = false) { if (!$this->db) { // if (!$force) return false; @@ -45,8 +54,14 @@ class Replays { if ($replay['p1'][0] === '!') $replay['p1'] = substr($replay['p1'], 1); if ($replay['p2'][0] === '!') $replay['p2'] = substr($replay['p2'], 1); - $res = $this->db->prepare("UPDATE ps_replays SET views = views + 1 WHERE id = ? LIMIT 1"); - $res->execute([$id]); + // if ($replay['private'] && !($replay['password'] ?? null)) { + // $replay['password'] = $this->genPassword(); + // $res = $this->db->prepare("UPDATE ps_replays SET views = views + 1, `password` = ? WHERE id = ? LIMIT 1"); + // $res->execute([$replay['password'], $id]); + // } else { + $res = $this->db->prepare("UPDATE ps_replays SET views = views + 1 WHERE id = ? LIMIT 1"); + $res->execute([$id]); + // } return $replay; } @@ -71,7 +86,7 @@ class Replays { $term = $this->toID($term); $isPrivate = $isPrivate ? 1 : 0; // $res = $this->db->prepare("SELECT uploadtime, id, format, p1, p2 FROM ps_replays WHERE private = 0 AND (p1id = ? OR p2id = ?) ORDER BY uploadtime DESC LIMIT ?, 51"); - $res = $this->db->prepare("(SELECT uploadtime, id, format, p1, p2 FROM ps_replays FORCE INDEX (p1) WHERE private = ? AND p1id = ? ORDER BY uploadtime DESC) UNION (SELECT uploadtime, id, format, p1, p2 FROM ps_replays FORCE INDEX (p2) WHERE private = ? AND p2id = ? ORDER BY uploadtime DESC) ORDER BY uploadtime DESC LIMIT ?, 51;"); + $res = $this->db->prepare("(SELECT uploadtime, id, format, p1, p2, password FROM ps_replays FORCE INDEX (p1) WHERE private = ? AND p1id = ? ORDER BY uploadtime DESC) UNION (SELECT uploadtime, id, format, p1, p2, password FROM ps_replays FORCE INDEX (p2) WHERE private = ? AND p2id = ? ORDER BY uploadtime DESC) ORDER BY uploadtime DESC LIMIT ?, 51;"); $res->execute([$isPrivate, $term, $isPrivate, $term, $limit1]); return $res->fetchAll(); @@ -93,11 +108,11 @@ class Replays { $res = null; switch (count($patterns)) { case 1: - $res = $this->db->prepare("SELECT /*+ MAX_EXECUTION_TIME(10000) */ uploadtime, id, format, p1, p2 FROM ps_replays FORCE INDEX (recent) WHERE private = 0 AND log LIKE ? ORDER BY uploadtime DESC LIMIT 10;"); + $res = $this->db->prepare("SELECT /*+ MAX_EXECUTION_TIME(10000) */ uploadtime, id, format, p1, p2, password FROM ps_replays FORCE INDEX (recent) WHERE private = 0 AND log LIKE ? ORDER BY uploadtime DESC LIMIT 10;"); $res->execute($patterns); break; case 2: - $res = $this->db->prepare("SELECT /*+ MAX_EXECUTION_TIME(10000) */ uploadtime, id, format, p1, p2 FROM ps_replays FORCE INDEX (recent) WHERE private = 0 AND log LIKE ? AND log LIKE ? ORDER BY uploadtime DESC LIMIT 10;"); + $res = $this->db->prepare("SELECT /*+ MAX_EXECUTION_TIME(10000) */ uploadtime, id, format, p1, p2, password FROM ps_replays FORCE INDEX (recent) WHERE private = 0 AND log LIKE ? AND log LIKE ? ORDER BY uploadtime DESC LIMIT 10;"); $res->execute($patterns); break; default; @@ -116,9 +131,9 @@ class Replays { $term = $this->toID($term); $res = null; if ($byRating) { - $res = $this->db->prepare("SELECT uploadtime, id, format, p1, p2, rating FROM ps_replays FORCE INDEX (top) WHERE private = 0 AND formatid = ? ORDER BY rating DESC LIMIT ?, 51"); + $res = $this->db->prepare("SELECT uploadtime, id, format, p1, p2, rating, password FROM ps_replays FORCE INDEX (top) WHERE private = 0 AND formatid = ? ORDER BY rating DESC LIMIT ?, 51"); } else { - $res = $this->db->prepare("SELECT uploadtime, id, format, p1, p2 FROM ps_replays FORCE INDEX (format) WHERE private = 0 AND formatid = ? ORDER BY uploadtime DESC LIMIT ?, 51"); + $res = $this->db->prepare("SELECT uploadtime, id, format, p1, p2, password FROM ps_replays FORCE INDEX (format) WHERE private = 0 AND formatid = ? ORDER BY uploadtime DESC LIMIT ?, 51"); } $res->execute([$term, $limit1]); @@ -138,6 +153,7 @@ class Replays { $id = $reqData['id']; $private = (@$reqData['hidden'] ? 1 : 0); + if ($reqData['hidden'] ?? null === '2') $private = 2; $p1 = $users->wordfilter($reqData['p1']); $p2 = $users->wordfilter($reqData['p2']); $format = $reqData['format']; @@ -177,42 +193,58 @@ class Replays { return 'database error for ' . $id . ': ' . $res->errorInfo(); } + $pReplay = $res->fetch(); + + $res = $this->db->prepare("SELECT id, `password` FROM ps_replays WHERE id = ?"); + $res->execute([$id]); $replay = $res->fetch(); - if (!$replay) { - $res = $this->db->prepare("SELECT id FROM ps_replays WHERE id = ?"); - $res->execute([$id]); - if ($res->fetch()) { + + if (!$pReplay) { + if ($replay) { // Someone else uploaded a replay while we were trying to upload it - return 'success'; + if ($replay['password']) $id .= '-' . $replay['password'] . 'pw'; + return 'success:' . $id; } if (!preg_match('/^[a-z0-9]+-[a-z0-9]+-[0-9]+$/', $id)) { return 'invalid id'; } return 'not found'; } - if (md5($this->stripNonAscii($reqData['log'])) !== $replay['loghash']) { + + $password = null; + if ($pReplay['private'] && $pReplay['private'] !== 2) { + if ($replay && $replay['password']) { + $password = $replay['password']; + } else if (!($replay && $replay['private'])) { + $password = $this->genPassword(); + } + } + $fullid = $id; + if ($password) $fullid .= '-' . $password . 'pw'; + + if (md5($this->stripNonAscii($reqData['log'])) !== $pReplay['loghash']) { $reqData['log'] = str_replace("\r",'', $reqData['log']); - if (md5($this->stripNonAscii($reqData['log'])) !== $replay['loghash']) { + if (md5($this->stripNonAscii($reqData['log'])) !== $pReplay['loghash']) { // Hashes don't match. // Someone else tried to upload a replay of the same battle, // while we were uploading this // ...pretend it was a success - return 'success'; + return 'success' . $fullid; } } - $p1id = $this->toID($replay['p1']); - $p2id = $this->toID($replay['p2']); - $formatid = $this->toID($replay['format']); + $p1id = $this->toID($pReplay['p1']); + $p2id = $this->toID($pReplay['p2']); + $formatid = $this->toID($pReplay['format']); - $res = $this->db->prepare("INSERT INTO ps_replays (id, p1, p2, format, p1id, p2id, formatid, uploadtime, private, rating, log, inputlog) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE log = ?, inputlog = ?, rating = ?"); - $res->execute([$id, $replay['p1'], $replay['p2'], $replay['format'], $p1id, $p2id, $formatid, $replay['uploadtime'], $replay['private'], $replay['rating'], $reqData['log'], $replay['inputlog'], $reqData['log'], $replay['inputlog'], $replay['rating']]); + $res = $this->db->prepare("INSERT INTO ps_replays (id, p1, p2, format, p1id, p2id, formatid, uploadtime, private, rating, log, inputlog, `password`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE log = ?, inputlog = ?, rating = ?, private = ?, `password` = ?"); + $res->execute([$id, $pReplay['p1'], $pReplay['p2'], $pReplay['format'], $p1id, $p2id, $formatid, $pReplay['uploadtime'], $pReplay['private'] ? 1 : 0, $pReplay['rating'], $reqData['log'], $pReplay['inputlog'], $password, $reqData['log'], $pReplay['inputlog'], $pReplay['rating'], $pReplay['private'] ? 1 : 0, $password]); $res = $this->db->prepare("DELETE FROM ps_prepreplays WHERE id = ? AND loghash = ?"); - $res->execute([$id, $replay['loghash']]); + $res->execute([$id, $pReplay['loghash']]); - return 'success'; + return 'success:' . $fullid; } function userid($username) { diff --git a/replays/search.php b/replays/search.php index b6a45b09a..fefccce18 100644 --- a/replays/search.php +++ b/replays/search.php @@ -237,8 +237,10 @@ if ($username || $format || $contains) { if ($prevtimeoffset === $timeoffset) break; } echo $timetext; + $replayid = $replay['id']; + if ($replay['password'] ?? null) $replayid .= '-' . $replay['password'] . 'pw'; ?> -

  • []
    vs.
  • +
  • []
    vs.