Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ services:
volumes:
- ./src:/var/www/html/
- ./vendor:/var/www/vendor/
redis:
master:
image: redis

# (EXTENDED-HA)
# master:

# slave:

# sentinel:

slave:
image: redis
command: redis-server --slaveof master 6379
sentinel:
image: redis
command:
- redis-sentinel
- /dev/null
- --sentinel monitor chatapp master 6379 2
- --sentinel down-after-milliseconds chatapp 5000
- --sentinel config-epoch chatapp 0
40 changes: 16 additions & 24 deletions src/init.php
Original file line number Diff line number Diff line change
@@ -1,36 +1,28 @@
<?php

ini_set('display_errors', 1);
require "../vendor/autoload.php";

$redis = new Predis\Client(
[
'tcp://workshop-redis_sentinel_1:26379',
'tcp://workshop-redis_sentinel_2:26379',
'tcp://workshop-redis_sentinel_3:26379',
],
['replication' => 'sentinel', 'service' => 'chatapp']
);

$redis = new Predis\Client([
'scheme' => 'tcp',
'host' => 'redis',
'port' => 6379,
]);

function loadCurentUserId($authSecret) {
return 1; // EXTENDED TASK: delete this line to complete the extended task

function verify_auth_secret($authSecret) {
global $redis;

// empty auth secret means the user is logged out
if ($authSecret == '') {
return null;
}

// use the auth secret to get the user ID
// $userId = _____________ (EXTENDED TASK)
if ($userId) {
// cross check that this auth secret is also stored in the user hash
// $userAuthSecret = _____________ (EXTENDED TASK)
if ($userAuthSecret != $authSecret) {
return null;
if ($authSecret) {
if ($username = $redis->get($authSecret)) {
return $username;
}

return $userId;
// invalid secret - wait few seconds to defense against brutal force attack
sleep(3);
}

// no user ID was found by the auth secret
// no user was found by the auth secret
return null;
}
46 changes: 17 additions & 29 deletions src/login.php
Original file line number Diff line number Diff line change
@@ -1,53 +1,41 @@
<?php

exit; // EXTENDED TASK: delete this line to complete the extended task
define('SESSION_TTL', 3600); // 1 hour

require "init.php";

$username = $_POST['username'];
$password = $_POST['password'];

// lookup the user IDs by username
// $userId = ___________________ (EXTENDED TASK)
// lookup the user by username
$isRegistered = $redis->hexists('users', $username);

if ($userId) {
// user ID exists => continue with the login flow
// $realPassword = __________________ (EXTENDED TASK)
if ($password === $realPassword) {
doLogin($userId);
if ($isRegistered) {
// user exists => continue with the login flow
$hash = $redis->hget('users', $username);
if (password_verify($password, $hash)) {
doLogin($username);
} else {
http_response_code(401);
echo 'This account already exists and entered password is incorrect!';
exit;
}
} else {
// user ID does not exist => continue with the register flow
// obtain new user ID
// $userId = _________________ (EXTENDED TASK)
// store this user account into a hash
// ________________________ (EXTENDED TASK)
// store the user ID into a hash - this is needed to lookup user IDs by usernames
// ________________________ (EXTENDED TASK)
// user does not exist => continue with the register flow
$hash = password_hash($password, PASSWORD_DEFAULT); // always store only a hash of the password - USE secure methods
$redis->hset('users', $username, $hash);

// login the user
doLogin($userId);
doLogin($username);
}

function doLogin($userId) {
function doLogin($username) {
global $redis;

// calculate random user secret
$rand = rand(0, PHP_INT_MAX) . $userId;
$authSecret = hash('sha256', $rand);

// delete the old auth secret (in case it exists)
// ________________________ (EXTENDED TASK)

// update the auth secret stored in the user hash
// ________________________ (EXTENDED TASK)
$authSecret = bin2hex(random_bytes(16)); // use cryptographically safe functions (rand() is too predictable)

// store the user ID into a hash - this is needed to lookup user IDs by user secrets
// ________________________ (EXTENDED TASK)
// save session with expiration
$redis->setex($authSecret, SESSION_TTL, $username);

setcookie("auth", $authSecret, time() + 3600 * 24 * 365);
setcookie("auth", $authSecret, time() + SESSION_TTL, '', '', false, true); // make sure that session ID is not readable by JS
}
26 changes: 15 additions & 11 deletions src/sendMessage.php
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
<?php
define('MESSAGE_CAP', 10);

require "init.php";

$userId = loadCurentUserId($_COOKIE['auth']);
$username = verify_auth_secret($_COOKIE['auth']);

if (!$userId) {
if (!$username) {
http_response_code(401);
exit;
}

$time = time();
$text = $_POST['text'];
// redis can store any string value, for complex data is better use serialization
// like a JSON than native Redis structures (especially if you need simple list)
$message = [
'time' => time(),
'text' => $_POST['text'],
'username' => $username,
];

// get the ID of the message
// $messageId = _______________ (BASIC TASK)
// this is much more convenient for list of messages, store whole message in the
// simple list - don't pollute Redis DB
$redis->lpush('messages', json_encode($message));

// insert the message into its own hash
// _______________ (BASIC TASK)

// push the message into the list of message IDs
// _______________ (BASIC TASK)
// this will make sure that list is capped on 10 messages (no DB overflow)
$redis->ltrim('messages', 0, MESSAGE_CAP - 1);
18 changes: 8 additions & 10 deletions src/showMessages.php
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
<?php

use Predis\Collection\Iterator;

require "init.php";

$userId = loadCurentUserId($_COOKIE['auth']);
$username = verify_auth_secret($_COOKIE['auth']);

if (!$userId) {
if (!$username) {
http_response_code(401);
exit;
}

// get 10 latest messages
// $messages = _______________ (BASIC TASK)
// get messages
$messages = new Iterator\ListKey($redis, 'messages');

foreach ($messages as $id) {
foreach ($messages as $message) {
// get all properties of the message
// $message = _______________ (BASIC TASK)

// add the author's username to the message array
$message['username'] = 'Anonymous';
// $message['username'] = _____________ (EXTENDED TASK)
$message = json_decode($message, true);

printMessage($message);
}
Expand Down