· 6 years ago · Aug 21, 2019, 06:12 AM
1<?php
2error_reporting(0);
3
4/*
5Hostname - External hostname
6External - External path to play
7SecretKey - reCaptcha secret key, leave blank to disable
8Cloudflare - Is host behind cloudflare?
9Activate - Activate user by default (true) or send verification email (false)
10Approve - Approve usernames by default or leave them for manual approval
11Clean - Delete old inactive accounts?
12CleanDays - Number of days before inactive accounts expire
13ForceCase - Force CamelCase on usernames
14AllowedChars - Allowed characters in usernames
15EmailWhitelist - List of allowed email domains, can be array or path to list file, leave blank to disable
16MaxPerEmail - Max no of accounts per email
17Database
18 Host - MySQL host
19 User - MySQL user
20 Pass - MySQL password
21 Name - Database name
22*/
23
24$config = [
25 "Hostname" => "houdi.ni",
26 "External" => "http://play.houdi.ni/",
27 "SecretKey" => "6LfEnEcUAAAAACzGi3GqTtYI0aFfwN_qACSmi10B",
28 "Cloudflare" => false,
29 "Activate" => false,
30 "Approve" => false,
31 "Clean" => true,
32 "CleanDays" => 10,
33 "ForceCase" => true,
34 "AllowedChars" => "A-Za-z0-9)(*&^$!`\_+={};:@~#>.<",
35 // "EmailWhitelist" => ["gmail.com", "hotmail.com"]
36 // "EmailWhitelist" => "/path/to/whitelist"
37 "EmailWhitelist" => [],
38 "MaxPerEmail" => 5,
39 "Database" => [
40 "Host" => "127.0.0.1",
41 "User" => "root",
42 "Pass" => "",
43 "Name" => "houdoo"
44 ]
45];
46
47
48final class Database extends PDO {
49
50 private $connection = null;
51
52 public function __construct($host, $user, $password, $database) {
53 $connectionString = sprintf("mysql:dbname=%s;host=%s", $database, $host);
54
55 parent::__construct($connectionString, $user, $password);
56 }
57
58 public function encryptPassword($password, $md5 = true) {
59 if($md5 !== false) {
60 $password = md5($password);
61 }
62 $hash = substr($password, 16, 16) . substr($password, 0, 16);
63 return $hash;
64 }
65
66 public function getLoginHash($password, $staticKey) {
67 $hash = $this->encryptPassword($password, false);
68 $hash .= $staticKey;
69 $hash .= 'Y(02.>\'H}t":E1';
70 $hash = $this->encryptPassword($hash);
71 return $hash;
72 }
73
74 public function addUser($username, $password, $color, $email, $isActive = 0, $approval = 0) {
75 $hashedPassword = strtoupper(md5($password));
76 $staticKey = "houdini";
77 $flashClientHash = $this->getLoginHash($hashedPassword, $staticKey);
78 $bcryptPassword = password_hash($flashClientHash, PASSWORD_DEFAULT, [ "cost" => 12 ]);
79 $insertPenguin = "INSERT INTO `penguin` (`ID`, `Username`, `Nickname`, `Approval`, `Password`, `Email`, `Active`, `Color`) VALUES ";
80 $insertPenguin .= "(NULL, :Username, :Username, :Approval, :Password, :Email, :Active, :Color);";
81
82 $insertStatement = $this->prepare($insertPenguin);
83 $insertStatement->bindValue(":Username", $username);
84 $insertStatement->bindValue(":Password", $bcryptPassword);
85 $insertStatement->bindValue(":Approval", $approval);
86 $insertStatement->bindValue(":Email", $email);
87 $insertStatement->bindValue(":Active", $isActive);
88 $insertStatement->bindValue(":Color", $color);
89
90 $insertStatement->execute();
91 $insertStatement->closeCursor();
92
93 $penguinId = $this->lastInsertId();
94
95 $this->insertInventory($penguinId, $color);
96 $this->addActiveIgloo($penguinId);
97 $this->sendMail($penguinId, null, 125);
98
99 return $penguinId;
100 }
101
102 public function insertInventory($penguinId, $itemId) {
103 $insertInventory = $this->prepare("INSERT INTO `inventory` (`PenguinID`, `ItemID`) VALUES (:PenguinID, :ItemID);");
104 $insertInventory->bindValue(":PenguinID", $penguinId);
105 $insertInventory->bindValue(":ItemID", $itemId);
106 $insertInventory->execute();
107 $insertInventory->closeCursor();
108 }
109
110 public function sendMail($recipientId, $senderId, $postcardType) {
111 $sendMail = $this->prepare("INSERT INTO `postcard` (`ID`, `SenderID`, `RecipientID`, `Type`) VALUES (NULL, :SenderID, :RecipientID, :Type);");
112 $sendMail->bindValue(":RecipientID", $recipientId);
113 $sendMail->bindValue(":SenderID", $senderId);
114 $sendMail->bindValue(":Type", $postcardType);
115 $sendMail->execute();
116 $sendMail->closeCursor();
117
118 $postcardId = $this->lastInsertId();
119
120 return $postcardId;
121 }
122
123 private function addActiveIgloo($penguinId) {
124 $insertStatement = $this->prepare("INSERT INTO `igloo` (`ID`, `PenguinID`) VALUES (NULL, :PenguinID);");
125 $insertStatement->bindValue(":PenguinID", $penguinId);
126 $insertStatement->execute();
127 $insertStatement->closeCursor();
128
129 $iglooId = $this->lastInsertId();
130 return $iglooId;
131 }
132
133 public function usernameTaken($username) {
134 $usernameTaken = "SELECT Username FROM `penguin` WHERE Username = :Username;";
135
136 $takenQuery = $this->prepare($usernameTaken);
137 $takenQuery->bindValue(":Username", $username);
138 $takenQuery->execute();
139
140 $rowCount = $takenQuery->rowCount();
141 $takenQuery->closeCursor();
142
143 return $rowCount > 0;
144 }
145
146 public function getEmailCount($email) {
147 $emailCount = "SELECT ID FROM `penguin` WHERE Email = :Email;";
148
149 $emailQuery = $this->prepare($emailCount);
150 $emailQuery->bindValue(":Email", $email);
151 $emailQuery->execute();
152
153 $rowCount = $emailQuery->rowCount();
154 $emailQuery->closeCursor();
155
156 return $rowCount;
157 }
158
159 public function createActivationKey($penguinId, $key) {
160 $insertStatement = $this->prepare("INSERT INTO `activation_key` (`PenguinID`, `ActivationKey`) VALUES (:PenguinID, :Key);");
161 $insertStatement->bindValue(":PenguinID", $penguinId);
162 $insertStatement->bindValue(":Key", $key);
163 $insertStatement->execute();
164 $insertStatement->closeCursor();
165 }
166
167 public function activateUser($penguinId, $key) {
168 $setActive = $this->prepare("UPDATE `penguin` INNER JOIN activation_key on penguin.ID = activation_key.PenguinID " .
169 "SET penguin.Active = 1 WHERE activation_key.ActivationKey = :Key;");
170 $setActive->bindValue(":Key", $key);
171 $setActive->execute();
172 if($setActive->rowCount() > 0) {
173 $deleteActivation = $this->prepare("DELETE FROM `activation_key` WHERE `PenguinID` = :PenguinID");
174 $deleteActivation->bindValue(":PenguinID", $penguinId);
175 $deleteActivation->execute();
176 }
177 $setActive->closeCursor();
178 $deleteActivation->closeCursor();
179 }
180
181 public function takenUsernames($username) {
182 $usernamesTaken = "SELECT Username FROM `penguin` WHERE Username LIKE :Username;";
183
184 $usernamesQuery = $this->prepare($usernamesTaken);
185 $usernamesQuery->bindValue(":Username", $username . "%");
186 $usernamesQuery->execute();
187
188 $usernames = $usernamesQuery->fetchAll(self::FETCH_COLUMN);
189 return $usernames;
190 }
191
192 public function cleanInactive($expiry = 10) {
193 $deleteInactive = "DELETE FROM `penguin` WHERE Active = 0 AND RegistrationDate < :Expiry;";
194
195 $deleteQuery = $this->prepare($deleteInactive);
196 $deleteQuery->bindValue(":Expiry", date("Y-m-d", strtotime("-$expiry days", time())));
197 $deleteQuery->execute();
198 }
199
200}
201
202$localization = [
203 "en" => [
204 "terms" => "You must agree to the Rules and Terms of Use.",
205 "name_missing" => "You need to name your penguin.",
206 "name_short" => "Penguin name is too short.",
207 "name_number" => "Penguin names can only contain 5 numbers.",
208 "penguin_letter" => "Penguin names must contain at least 1 letter.",
209 "name_not_allowed" => "That penguin name is not allowed.",
210 "name_taken" => "That penguin name is already taken.",
211 "name_suggest" => "That penguin name is already taken. Try {suggestion}.",
212 "passwords_match" => "Passwords do not match.",
213 "password_short" => "Password is too short.",
214 "email_invalid" => "Invalid email address."
215 ],
216 "fr" => [
217 "terms" => "Tu dois accepter les conditions d'utilisation.",
218 "name_missing" => "Tu dois donner un nom à ton pingouin.",
219 "name_short" => "Le nom de pingouin est trop court.",
220 "name_number" => "Un nom de pingouin ne peut contenir plus de 5 nombres.",
221 "penguin_letter" => "Un nom de pingouin doit contenir au moins une lettre.",
222 "name_not_allowed" => "Ce nom de pingouing n'est pas autorisé.",
223 "name_taken" => "Ce nom de pingouin est pris.",
224 "name_suggest" => "Ce nom de pingouin est pris. Essaye {suggestion}.",
225 "passwords_match" => "Les mots de passes ne correspondent pas.",
226 "password_short" => "Le mot de passe est trop court.",
227 "email_invalid" => "Adresse email invalide."
228 ],
229 "es" => [
230 "terms" => "Debes seguir las reglas y los términos de uso.",
231 "name_missing" => "Debes escoger un nombre para tu pingüino.",
232 "name_short" => "El nombre de tu pingüino es muy corto.",
233 "name_number" => "Los nombres de usuario sólo pueden tener 5 números.",
234 "penguin_letter" => "Los nombres de usuario deben tener por lo menos 1 letra.",
235 "name_not_allowed" => "Ese nombre de usuario no está permitido.",
236 "name_taken" => "Ese nombre de usuario ya ha sido escogido.",
237 "name_suggest" => "Ese nombre de usuario ya ha sido escogido. Intenta éste {suggestion}.",
238 "passwords_match" => "Las contraseñas no coinciden.",
239 "password_short" => "La contraseña es muy corta.",
240 "email_invalid" => "El correo eléctronico es incorrecto."
241 ],
242 "pt" => [
243 "terms" => "Você precisa concordar com as Regras e com os Termos de Uso.",
244 "name_missing" => "Você precisa nomear seu pinguim.",
245 "name_short" => "O nome do pinguim é muito curto.",
246 "name_number" => "O nome do pinguim só pode conter 5 números",
247 "penguin_letter" => "O nome do seu pinguim tem de conter pelo menos uma letra.",
248 "name_not_allowed" => "Esse nome de pinguim não é permitido.",
249 "name_taken" => "Esse nome de pinguim já foi escolhido.",
250 "name_suggest" => "Esse nome de pinguim já foi escolhido. Tente {suggestion}.",
251 "passwords_match" => "As senhas não correspondem.",
252 "password_short" => "A senha é muito curta.",
253 "email_invalid" => "Esse endereço de E-Mail é invalido."
254 ]
255];
256
257if(!is_array($config["EmailWhitelist"]) && !empty($config["EmailWhitelist"])) {
258 $emailWhitelistFile = file_get_contents($config["EmailWhitelist"]);
259 $config["EmailWhitelist"] = explode("\n", $emailWhitelistFile);
260}
261
262if(isset($_GET["key"])) {
263 $db = new Database($config["Database"]["Host"], $config["Database"]["User"],
264 $config["Database"]["Pass"], $config["Database"]["Name"]);
265
266 $key = $_GET["key"];
267 $rawKey = base64_decode($key);
268 $rawKey = explode(":", $rawKey);
269 list($penguinId, $activationKey) = $rawKey;
270
271 $db->activateUser($penguinId, $activationKey);
272
273 header("Location: " . $config["External"]);
274 die($penguinId . $activationKey);
275}
276
277
278session_start();
279
280function response($data) {
281 die(http_build_query($data));
282}
283
284function attemptDataRetrieval($key, $session = false) {
285 if(!$session && array_key_exists($key, $_POST)) {
286 return $_POST[$key];
287 }
288
289 if($session && array_key_exists($key, $_SESSION)) {
290 return $_SESSION[$key];
291 }
292
293 response([
294 "error" => ""
295 ]);
296}
297
298function generateActivationKey($length, $keyspace = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") {
299 $str = "";
300 $max = mb_strlen($keyspace, "8bit") - 1;
301 for ($i = 0; $i < $length; ++$i) {
302 $str .= $keyspace[random_int(0, $max)];
303 }
304 return $str;
305}
306
307function createActivateUrl($baseUrl, $penguinId, $activationKey) {
308 $rawKey = implode(":", [$penguinId, $activationKey]);
309 $key = base64_encode($rawKey);
310 return $baseUrl . "/create_account/create_account.php?key=" . $key;
311}
312
313$action = attemptDataRetrieval("action");
314$lang = attemptDataRetrieval("lang");
315
316if(!in_array($lang, array_keys($localization))) {
317 response([
318 "error" => ""
319 ]);
320}
321
322if($action == "validate_agreement") {
323 $agreeTerms = attemptDataRetrieval("agree_to_terms");
324 $agreeRules = attemptDataRetrieval("agree_to_rules");
325 if(!$agreeTerms || !$agreeRules) {
326 response([
327 "error" => $localization[$lang]["terms"]
328 ]);
329 }
330
331 response([
332 "success" => 1
333 ]);
334} elseif($action == "validate_username") {
335 $username = attemptDataRetrieval("username");
336 $color = attemptDataRetrieval("colour");
337 $colors = range(1, 15);
338
339 if(strlen($username) == 0) {
340 response([
341 "error" => $localization[$lang]["name_missing"]
342 ]);
343 } elseif(strlen($username) < 4 || strlen($username) > 12) {
344 response([
345 "error" => $localization[$lang]["name_short"]
346 ]);
347 } elseif(preg_match_all("/[0-9]/", $username) > 5) {
348 response([
349 "error" => $localization[$lang]["name_number"]
350 ]);
351 } elseif(!preg_match("/[A-z]/i", $username)) {
352 response([
353 "error" => $localization[$lang]["penguin_letter"]
354 ]);
355 } elseif(preg_match("/[^" . $config["AllowedChars"] . "]/", $username)) {
356 response([
357 "error" => $localization[$lang]["name_not_allowed"]
358 ]);
359 } elseif(!is_numeric($color) || !in_array($color, $colors)) {
360 response([
361 "error" => ""
362 ]);
363 }
364
365 $db = new Database($config["Database"]["Host"], $config["Database"]["User"],
366 $config["Database"]["Pass"], $config["Database"]["Name"]);
367
368 if($db->usernameTaken($username)) {
369 $username = preg_replace("/\d+$/", "", $username);
370 $takenUsernames = $db->takenUsernames($username);
371 $i = 1;
372 while(true) {
373 $suggestion = $username . $i++;
374 if(preg_match_all("/[0-9]/", $username) > 1) {
375 response([
376 "error" => $localization[$lang]["name_taken"]
377 ]);
378 }
379 if(!in_array(strtolower($suggestion), $takenUsernames)) {
380 break;
381 }
382 }
383 response([
384 "error" => str_replace("{suggestion}", $suggestion, $localization[$lang]["name_suggest"])
385 ]);
386 }
387
388 $_SESSION["sid"] = session_id();
389 $_SESSION["username"] = ($config["ForceCase"] ? ucfirst(strtolower($username)) : $username);
390 $_SESSION["colour"] = $color;
391
392 response([
393 "success" => 1,
394 "sid" => session_id()
395 ]);
396} elseif($action == "validate_password_email") {
397 $sessionId = attemptDataRetrieval("sid", true);
398 $username = attemptDataRetrieval("username", true);
399 $color = attemptDataRetrieval("colour", true);
400 $password = attemptDataRetrieval("password");
401 $passwordConfirm = attemptDataRetrieval("password_confirm");
402 $email = attemptDataRetrieval("email");
403
404 if(!empty($config["SecretKey"])) {
405 $gtoken = attemptDataRetrieval("gtoken");
406 $data = [
407 "secret" => $config["SecretKey"],
408 "response" => $gtoken,
409 "remoteip" => ($config["Cloudflare"] ? $_SERVER["HTTP_CF_CONNECTING_IP"] : $_SERVER['REMOTE_ADDR'])
410 ];
411 $verify = curl_init();
412 curl_setopt($verify, CURLOPT_URL, "https://www.google.com/recaptcha/api/siteverify");
413 curl_setopt($verify, CURLOPT_POST, true);
414 curl_setopt($verify, CURLOPT_POSTFIELDS, http_build_query($data));
415 curl_setopt($verify, CURLOPT_SSL_VERIFYPEER, false);
416 curl_setopt($verify, CURLOPT_RETURNTRANSFER, true);
417 $response = curl_exec($verify);
418 $result = json_decode($response);
419 }
420
421 $emailDomain = substr(strrchr($email, "@"), 1);
422
423 if($sessionId !== session_id()) {
424 response([
425 "error" => ""
426 ]);
427 } elseif(empty($result->success) && !empty($config["SecretKey"])) {
428 response([
429 "error" => ""
430 ]);
431 } elseif($password !== $passwordConfirm) {
432 response([
433 "error" => $localization[$lang]["passwords_match"]
434 ]);
435 } elseif(strlen($password) < 4) {
436 response([
437 "error" => $localization[$lang]["password_short"]
438 ]);
439 } elseif(!filter_var($email, FILTER_VALIDATE_EMAIL)) {
440 response([
441 "error" => $localization[$lang]["email_invalid"]
442 ]);
443 } elseif(!in_array($emailDomain, $config["EmailWhitelist"]) && !empty($config["EmailWhitelist"])) {
444 response([
445 "error" => $localization[$lang]["email_invalid"]
446 ]);
447 }
448
449 $db = new Database($config["Database"]["Host"], $config["Database"]["User"],
450 $config["Database"]["Pass"], $config["Database"]["Name"]);
451
452 if($db->getEmailCount($email) >= $config["MaxPerEmail"]) {
453 response([
454 "error" => $localization[$lang]["email_invalid"]
455 ]);
456 }
457
458 $penguinId = $db->addUser($username, $password, $color, $email, ($config["Activate"] ? 1 : 0), ($config["Approve"] ? 1 : 0));
459
460 if(!$config["Activate"]) {
461 $activationKey = generateActivationKey(60);
462 $db->createActivationKey($penguinId, $activationKey);
463
464 $activationLink = createActivateUrl($config["External"], $penguinId, $activationKey);
465
466 $headers = "From: noreply@{$config['Hostname']}\r\n";
467 $headers .= "Reply-To: noreply@{$config['Hostname']}\r\n";
468 $headers .= "Return-Path: noreply@{$config['Hostname']}\r\n";
469 $headers .= "MIME-Version: 1.0\r\n";
470 $headers .= "Content-type: text/html; charset=iso-8859-1\r\n";
471 $headers .= "X-Mailer: PHP/" . phpversion();
472
473 ob_start();
474?>
475
476<!doctype html>
477<html>
478 <head>
479 <title>Activate your penguin!</title>
480 </head>
481 <body>
482 <p>Hello,</p>
483 <p>Thank you for creating a penguin on <?php print($config["Hostname"]); ?>. Please click below to activate your penguin account.</p>
484 <a href="<?php print($activationLink); ?>">Activate</a>
485 </body>
486</html>
487
488<?php
489 $emailContent = ob_get_clean();
490 mail($email, "Activate your penguin!", $emailContent, $headers);
491 }
492
493 if($config["Clean"] == true) {
494 $db->cleanInactive($config["CleanDays"]);
495 }
496
497 session_destroy();
498
499 response([
500 "success" => 1
501 ]);
502}
503
504?>