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