From 5e4402c230c0504112158c9489740cb22da131db Mon Sep 17 00:00:00 2001 From: "eapl.mx" Date: Mon, 23 Dec 2024 19:50:24 -0600 Subject: [PATCH 1/7] feat(session): WIP - Implement persistent cookies --- VERSION | 2 +- _wip_todo/add_url.php | 53 +++++++++-------- _wip_todo/session.php | 101 ------------------------------- index.php | 18 +++--- libs/persistent_session.php | 115 ++++++++++++++++++++++++++++++++++++ libs/session.php | 49 +++++++-------- partials/listSelect.php | 48 +++++++-------- partials/lists.php | 50 ++++++++-------- private/config_template.ini | 13 ++-- views/following.php | 60 ++++++++++--------- views/login.php | 44 +++++++------- views/logout.php | 3 +- views/test_login.php | 13 ++++ 13 files changed, 307 insertions(+), 262 deletions(-) delete mode 100644 _wip_todo/session.php create mode 100644 libs/persistent_session.php create mode 100644 views/test_login.php diff --git a/VERSION b/VERSION index a22da3b..a5ee8d9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2024.12.06 +2024.12.23 diff --git a/_wip_todo/add_url.php b/_wip_todo/add_url.php index 34494d1..886664c 100644 --- a/_wip_todo/add_url.php +++ b/_wip_todo/add_url.php @@ -10,9 +10,9 @@ if ($config['debug_mode']) { require_once('session.php'); -if (!isset($_SESSION['valid_session'])) { +if (!isset($_SESSION['valid_session'])) { $secretKey = $config['totp_secret']; - $cookieVal = decodeCookie($secretKey); + $cookieVal = isSavedCookieValid($secretKey); if ($cookieVal === false) { # Valid cookie ? header('Location: login.php'); @@ -56,27 +56,30 @@ if (isset($_POST['submit'])) { exit; } } else { ?> - - - - - twtxt - - - - -

twtxt

-
-
- -
URL is invalid, check it!

- - -
-
- -
-
- - + + + + + + twtxt + + + + + +

twtxt

+
+
+ +
URL is invalid, check it!

+ + +
+
+ +
+
+ + + \ No newline at end of file diff --git a/_wip_todo/session.php b/_wip_todo/session.php deleted file mode 100644 index 907fb00..0000000 --- a/_wip_todo/session.php +++ /dev/null @@ -1,101 +0,0 @@ - 'twtxt_session', - 'use_strict_mode' => true, - 'cookie_httponly' => true, - 'cookie_secure' => $config['secure_cookies'], - 'sid_length' => 64, - 'sid_bits_per_character' => 6, - 'cookie_samesite' => 'Strict', // Not compatible with PHP lower than 7.3 -]); - -function has_valid_session() { - $config = parse_ini_file('private/config.ini'); - $secretKey = $config['password']; - - if (isset($_SESSION['valid_session'])) { - return true; - } - - $cookieVal = decodeCookie($secretKey); - if ($cookieVal === false) { - #echo "Invalid cookie"; - return false; - } - - return true; -} - -function encrypt(string $data, string $key, string $method): string { - $ivSize = openssl_cipher_iv_length($method); - $iv = openssl_random_pseudo_bytes($ivSize); - $encrypted = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv); - # PHP 8.2 - Deprecated: implode(): - # Passing null to parameter #1 ($separator) of type array|string is deprecated - //$encrypted = strtoupper(implode(null, unpack('H*', $encrypted))); - $encrypted = strtoupper(implode(unpack('H*', $encrypted))); - - return $encrypted; -} - -function decrypt(string $data, string $key, string $method): string { - $data = pack('H*', $data); - $ivSize = openssl_cipher_iv_length($method); - $iv = openssl_random_pseudo_bytes($ivSize); - $decrypted = openssl_decrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv); - - return trim($decrypted); -} - -function saveLoginSuccess($secretKey) { - // Set a cookie to remember the user - $_SESSION['valid_session'] = true; - - // Set a cookie value to remember the user - $encoded_cookie_value = generateCookieValue('admin', $secretKey); - $cookie_expiry = time() + (30 * 24 * 60 * 60); // 30 days - - $config = parse_ini_file('private/config.ini'); - - setcookie(COOKIE_NAME, $encoded_cookie_value, [ - 'expires' => $cookie_expiry, - 'secure' => $config['secure_cookies'], - 'httponly' => true, - 'samesite' => 'Strict', - ]); -} - -function generateCookieValue($username, $secretKey) { - $key = bin2hex($secretKey); - - $encrypted = encrypt($username, $key, ENCRYPTION_METHOD); - return $encrypted; -} - -function decodeCookie($secretKey) { - // Retrieve the encoded cookie name - if (!isset($_COOKIE[COOKIE_NAME])) { - return false; - } - - $encoded_cookie_value = $_COOKIE[COOKIE_NAME]; - $key = bin2hex($secretKey); - - $config = parse_ini_file('private/config.ini'); - - // Extend expiry by 30 days - $cookie_expiry = time() + (30 * 24 * 60 * 60); - setcookie(COOKIE_NAME, $encoded_cookie_value, [ - 'expires' => $cookie_expiry, - 'secure' => $config['secure_cookies'], - 'httponly' => true, - 'samesite' => 'Strict', - ]); - - $decrypted = decrypt($encoded_cookie_value, $key, ENCRYPTION_METHOD); - return $decrypted; -} \ No newline at end of file diff --git a/index.php b/index.php index b4cb114..1e51db4 100644 --- a/index.php +++ b/index.php @@ -1,4 +1,4 @@ - 'new_twt.php', '/add' => 'add_feed.php', '/following' => 'following.php', - //'/refresh' => 'load_twt_files.php', '/refresh' => 'refresh.php', '/login' => 'login.php', '/logout' => 'logout.php', '/profile' => 'profile.php', '/replies' => 'replies.php', '/gallery' => 'gallery.php', - //'/profile/([a-zA-Z0-9_-]+)' => 'profile.php', - '/conv/([a-zA-Z0-9]{7})' => 'conv.php', // matches only twtHash of exactly 7 alphanumeric characters - '/post/([a-zA-Z0-9]{7})' => 'post.php', // matches only twtHash of exactly 7 alphanumeric characters - //'/thumb' => 'thumb.php', + '/conv/([a-zA-Z0-9]{7})' => 'conv.php', // matches only twtHash of exactly 7 alphanumeric characters + '/post/([a-zA-Z0-9]{7})' => 'post.php', // matches only twtHash of exactly 7 alphanumeric characters '/upload' => 'upload_img.php', '/webmention' => 'webmention_endpoint.php', + //'/thumb' => 'thumb.php', + //'/profile/([a-zA-Z0-9_-]+)' => 'profile.php', + + # Debug endpoints + '/test_login' => 'test_login.php', ]; // Loop through the defined routes and try to match the request URI foreach ($routes as $pattern => $action) { if (preg_match('#^' . $pattern . '$#', $path, $matches)) { - - // Extract any matched parameters (e.g., username) + + // Extract any matched parameters (e.g., username) if(!empty($matches[1])) { //array_shift($matches); $id = $matches[1]; diff --git a/libs/persistent_session.php b/libs/persistent_session.php new file mode 100644 index 0000000..45d45fd --- /dev/null +++ b/libs/persistent_session.php @@ -0,0 +1,115 @@ + !isset($config[$key])); + +if (!empty($missing_keys)) { + die('Missing required keys in config.ini: ' . implode(', ', $missing_keys)); +} + +# To make it more secure, something like JWT could be used instead + +const COOKIE_NAME = 'timeline_login'; +const ENCRYPTION_METHOD = 'aes-256-cbc'; +const EXPIRATION_DAYS = 30; + +session_start([ + 'name' => 'timeline_session', + 'use_strict_mode' => true, + 'cookie_httponly' => true, + 'cookie_secure' => $config['secure_cookies'], + 'sid_length' => 64, + 'sid_bits_per_character' => 6, + 'cookie_samesite' => 'Strict', # Not compatible with PHP < 7.3 +]); + +function hasValidSession(): bool|string { + # If short lived session is valid + if (isset($_SESSION['valid_session'])) { + return true; + } + + # Otherwise, check the persistent cookie + return isSavedCookieValid(); +} + +function encrypt(string $data, string $key, string $method): string { + $ivSize = openssl_cipher_iv_length($method); + $iv = openssl_random_pseudo_bytes($ivSize); + $encrypted = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv); + $encrypted = strtoupper(implode(unpack('H*', $encrypted))); + + return $encrypted; +} + +function decrypt(string $data, string $key, string $method): string | bool { + $data = pack('H*', $data); + $ivSize = openssl_cipher_iv_length($method); + $iv = openssl_random_pseudo_bytes($ivSize); + $decrypted = openssl_decrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv); + + var_dump($decrypted); + + if ($decrypted === false) { + return false; + } + + return trim($decrypted); +} + +function saveLoginSuccess() { + $_SESSION['valid_session'] = true; + + $config = parse_ini_file('private/config.ini'); + + # Set a cookie to remember the user + $cookieExpiry = EXPIRATION_DAYS * 24 * 60 * 60 + time(); + $encodedCookieValue = generateCookieValue(strval($cookieExpiry), $config['secret_key']); + + setcookie(COOKIE_NAME, $encodedCookieValue, [ + 'expires' => $cookieExpiry, + 'secure' => $config['secure_cookies'], + 'httponly' => true, + 'samesite' => 'Strict', + ]); +} + +function generateCookieValue($value, $secretKey) { + $key = bin2hex($secretKey); + + $encrypted = encrypt($value, $key, ENCRYPTION_METHOD); + return $encrypted; +} + +function isSavedCookieValid() { + if (!isset($_COOKIE[COOKIE_NAME])) { + return false; + } + + $config = parse_ini_file('private/config.ini'); + + $encoded_cookie_value = $_COOKIE[COOKIE_NAME]; + $key = bin2hex($config['secret_key']); + + $cookieVal = decrypt($encoded_cookie_value, $key, ENCRYPTION_METHOD); + + if ($cookieVal === false) { + deletePersistentCookie(); + return false; + } + + # TODO: Check that the cookie is not expired + + saveLoginSuccess(); # Extend expiracy for previous cookie + + return true; # If it was decoded correctly, it's a valid session +} + +function deletePersistentCookie() { + if (isset($_COOKIE[COOKIE_NAME])) { + unset($_COOKIE[COOKIE_NAME]); + setcookie(COOKIE_NAME, '', time() - 3600); + } +} diff --git a/libs/session.php b/libs/session.php index bb9f9fc..10a688b 100644 --- a/libs/session.php +++ b/libs/session.php @@ -1,30 +1,33 @@
- Selected for both public and private lists - - if( isset($_SESSION['password'])) { - if($_SESSION['password']=="$password") { // Hacky login + // TODO: fix it so if List -> Selected for both public and private lists + + if (isset($_SESSION['password'])) { + if ($_SESSION['password'] == "$passwordInConfig") { // Hacky login // Private lists echo ""; foreach (glob("private/twtxt-*.txt") as $filename) { - if($filename == $_GET['lists']) $attr="selected"; - else $attr = ""; - $listName = $filename; - $listName = str_replace("private/twtxt-", "", $listName); - $listName = str_replace("_", " ", $listName); - $listName = str_replace(".txt", "", $listName); - echo ""; + if ($filename == $_GET['lists']) $attr = "selected"; + else $attr = ""; + $listName = $filename; + $listName = str_replace("private/twtxt-", "", $listName); + $listName = str_replace("_", " ", $listName); + $listName = str_replace(".txt", "", $listName); + echo ""; } // Public Lists echo ""; - } } + } - foreach (glob("twtxt-*.txt") as $filename) { - if($filename == $_GET['lists']) $attr="selected"; - else $attr = ""; - $listName = $filename; - $listName = str_replace("twtxt-", "", $listName); - $listName = str_replace("_", " ", $listName); - $listName = str_replace(".txt", "", $listName); - echo ""; - } + foreach (glob("twtxt-*.txt") as $filename) { + if ($filename == $_GET['lists']) $attr = "selected"; + else $attr = ""; + $listName = $filename; + $listName = str_replace("twtxt-", "", $listName); + $listName = str_replace("_", " ", $listName); + $listName = str_replace(".txt", "", $listName); + echo ""; + } ?> - - + +
\ No newline at end of file diff --git a/partials/lists.php b/partials/lists.php index 32dca1b..ac0d3a1 100644 --- a/partials/lists.php +++ b/partials/lists.php @@ -1,43 +1,43 @@
- Selected for both public and private lists - - if( isset($_SESSION['password'])) { - if($_SESSION['password']=="$password") { // Hacky login + // TODO: fix it so if List -> Selected for both public and private lists + + if (isset($_SESSION['password'])) { + if ($_SESSION['password'] == "$passwordInConfig") { // Hacky login // Private lists echo ""; foreach (glob("private/twtxt-*.txt") as $filename) { - if($filename == $_GET['list']) $attr="selected"; - else $attr = ""; - $listName = $filename; - $listName = str_replace("private/twtxt-", "", $listName); - $listName = str_replace("_", " ", $listName); - $listName = str_replace(".txt", "", $listName); - echo ""; + if ($filename == $_GET['list']) $attr = "selected"; + else $attr = ""; + $listName = $filename; + $listName = str_replace("private/twtxt-", "", $listName); + $listName = str_replace("_", " ", $listName); + $listName = str_replace(".txt", "", $listName); + echo ""; } // Public Lists echo ""; - } } + } - foreach (glob("twtxt-*.txt") as $filename) { - if($filename == $_GET['list']) $attr="selected"; - else $attr = ""; - $listName = $filename; - $listName = str_replace("twtxt-", "", $listName); - $listName = str_replace("_", " ", $listName); - $listName = str_replace(".txt", "", $listName); - //$filename = "TODO".$baseURL."/".$filename; - echo ""; - } + foreach (glob("twtxt-*.txt") as $filename) { + if ($filename == $_GET['list']) $attr = "selected"; + else $attr = ""; + $listName = $filename; + $listName = str_replace("twtxt-", "", $listName); + $listName = str_replace("_", " ", $listName); + $listName = str_replace(".txt", "", $listName); + //$filename = "TODO".$baseURL."/".$filename; + echo ""; + } ?> - - + +
\ No newline at end of file diff --git a/private/config_template.ini b/private/config_template.ini index a49498c..cfef942 100644 --- a/private/config_template.ini +++ b/private/config_template.ini @@ -42,13 +42,18 @@ webmentions_txt_path = "./mentions.txt" public_webmentions = "https://example.com/timeline/mentions.txt" [security] -; Generate it with the TOTP module +; Secret key to encrypt cookies +; Create a new one here: https://randomkeygen.com +secret_key = "553GkZzIYZKx5z0lftt4yKDG4aKb4sAG" + +; Simple password +password = "change_me" + +; A dynamic password (TOTP) changing every 30 seconds +; Use a TOTP client with support for 10 digits like Aegis (Android) totp_digits = 10 totp_secret = "1234567890" ; It's recommended that your site is hosted on HTTPS ; In case it's in HTTP (not secure), set this to false secure_cookies = true - -; Simple password for unnamed user -password = "" \ No newline at end of file diff --git a/views/following.php b/views/following.php index a01c2ad..714e8cd 100644 --- a/views/following.php +++ b/views/following.php @@ -1,7 +1,7 @@ --> Nick URL - + Time ago - - - - - - - - - - - - twts)) { + + + + + + + + + + + + twts)) { - // Then test if latest twt is at start or end of file: - $resetVar = reset(getTwtsFromTwtxtString($currentFollower[1])->twts); - $endVar = end(getTwtsFromTwtxtString($currentFollower[1])->twts); - if ($resetVar->timestamp < $endVar->timestamp) { // TODO: this can be swapped to get time of first twt - echo $endVar->displayDate; - } else { - echo $resetVar->displayDate; + // Then test if latest twt is at start or end of file: + $resetVar = reset(getTwtsFromTwtxtString($currentFollower[1])->twts); + $endVar = end(getTwtsFromTwtxtString($currentFollower[1])->twts); + if ($resetVar->timestamp < $endVar->timestamp) { // TODO: this can be swapped to get time of first twt + echo $endVar->displayDate; + } else { + echo $resetVar->displayDate; + } } - } - ?> + ?> - - + + - + - + \ No newline at end of file diff --git a/views/login.php b/views/login.php index dfa0d93..116824d 100644 --- a/views/login.php +++ b/views/login.php @@ -1,28 +1,30 @@ -
-

Enter password or TOTP

-
-
- -

-
-
+ +
+

Enter password or TOTP

+
+
+ +

+ +

+
+
- + \ No newline at end of file diff --git a/views/logout.php b/views/logout.php index c44d92b..3f1c868 100644 --- a/views/logout.php +++ b/views/logout.php @@ -1,6 +1,7 @@ Date: Tue, 24 Dec 2024 14:31:38 -0600 Subject: [PATCH 2/7] feat(session): implement persistent cookies with hmac (replaced AES encryption) --- libs/persistent_session.php | 120 +++++++++++++++++++++--------------- libs/session.php | 4 +- views/test_login.php | 1 + 3 files changed, 72 insertions(+), 53 deletions(-) diff --git a/libs/persistent_session.php b/libs/persistent_session.php index 45d45fd..03e3b3d 100644 --- a/libs/persistent_session.php +++ b/libs/persistent_session.php @@ -9,12 +9,13 @@ if (!empty($missing_keys)) { die('Missing required keys in config.ini: ' . implode(', ', $missing_keys)); } -# To make it more secure, something like JWT could be used instead - const COOKIE_NAME = 'timeline_login'; const ENCRYPTION_METHOD = 'aes-256-cbc'; const EXPIRATION_DAYS = 30; +const HASH_LENGTH = 128; +const HASH_ALGORITHM = 'sha512'; + session_start([ 'name' => 'timeline_session', 'use_strict_mode' => true, @@ -25,50 +26,80 @@ session_start([ 'cookie_samesite' => 'Strict', # Not compatible with PHP < 7.3 ]); -function hasValidSession(): bool|string { +function hasValidSession(): bool { # If short lived session is valid - if (isset($_SESSION['valid_session'])) { + if (isset($_SESSION['session_expiration'])) { return true; } + # TODO: Check if the session has expired + # Add more protection to prevent session fixation + # https://owasp.org/www-community/attacks/Session_fixation + # Otherwise, check the persistent cookie return isSavedCookieValid(); } -function encrypt(string $data, string $key, string $method): string { - $ivSize = openssl_cipher_iv_length($method); - $iv = openssl_random_pseudo_bytes($ivSize); - $encrypted = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv); - $encrypted = strtoupper(implode(unpack('H*', $encrypted))); - - return $encrypted; -} - -function decrypt(string $data, string $key, string $method): string | bool { - $data = pack('H*', $data); - $ivSize = openssl_cipher_iv_length($method); - $iv = openssl_random_pseudo_bytes($ivSize); - $decrypted = openssl_decrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv); - - var_dump($decrypted); - - if ($decrypted === false) { +function getCookieData() { + if (!isset($_COOKIE[COOKIE_NAME])) { + #echo "Cookie " . COOKIE_NAME . " not found"; return false; } - return trim($decrypted); -} + $raw = base64_decode($_COOKIE[COOKIE_NAME]); + #var_dump($raw); -function saveLoginSuccess() { - $_SESSION['valid_session'] = true; + # Cookie should be at least the size of the hash length. + # If it's not, we can just bail out + if (strlen($raw) < HASH_LENGTH) { + #echo "Didn't get minimum length"; + return false; + } $config = parse_ini_file('private/config.ini'); - # Set a cookie to remember the user - $cookieExpiry = EXPIRATION_DAYS * 24 * 60 * 60 + time(); - $encodedCookieValue = generateCookieValue(strval($cookieExpiry), $config['secret_key']); + # The cookie data contains the actual data w/ the hash concatonated to the end, + # since the hash is a fixed length, we can extract the last hash_length chars + # to get the hash. + $hash = substr($raw, strlen($raw) - HASH_LENGTH, HASH_LENGTH); + $data = substr($raw, 0, - (HASH_LENGTH)); - setcookie(COOKIE_NAME, $encodedCookieValue, [ + # Calculate what the hash should be, based on the data. If the data has not been + # tampered with, $hash and $hash_calculated will be the same + $hash_calculated = hash_hmac(HASH_ALGORITHM, $data, $config['secret_key']); + + # If we calculate a different hash, we can't trust the data. + if ($hash_calculated !== $hash) { + #echo "Different HASH"; + return False; + } + + # Is it expired ? + if (intval($data) < time()) { + #echo "Cookie expired"; + return False; + } + + return $data; +} + +function makePersistentCookie() { + $config = parse_ini_file('private/config.ini'); + + $cookieExpiry = EXPIRATION_DAYS * 24 * 60 * 60 + time(); # X days + #$cookieExpiry = 10 + time(); # Debug value - 5 minutes + + # Calculate a hash for the data and append it to the end of the data string + $cookieValue = strval($cookieExpiry); + + $hash = hash_hmac(HASH_ALGORITHM, $cookieValue, $config['secret_key']); + $cookieValue .= $hash; + $cookieValue = base64_encode($cookieValue); + + # Also create the short-timed session + $_SESSION['session_expiration'] = $cookieExpiry; + + return setcookie(COOKIE_NAME, $cookieValue, [ 'expires' => $cookieExpiry, 'secure' => $config['secure_cookies'], 'httponly' => true, @@ -76,35 +107,22 @@ function saveLoginSuccess() { ]); } -function generateCookieValue($value, $secretKey) { - $key = bin2hex($secretKey); - - $encrypted = encrypt($value, $key, ENCRYPTION_METHOD); - return $encrypted; +function saveLogin() { + makePersistentCookie(); } function isSavedCookieValid() { - if (!isset($_COOKIE[COOKIE_NAME])) { - return false; - } - - $config = parse_ini_file('private/config.ini'); - - $encoded_cookie_value = $_COOKIE[COOKIE_NAME]; - $key = bin2hex($config['secret_key']); - - $cookieVal = decrypt($encoded_cookie_value, $key, ENCRYPTION_METHOD); - - if ($cookieVal === false) { + $cookieExpiry = getCookieData(); + + if ($cookieExpiry === false) { deletePersistentCookie(); return false; } - # TODO: Check that the cookie is not expired + # Refresh session + $_SESSION['session_expiration'] = intval($cookieExpiry); - saveLoginSuccess(); # Extend expiracy for previous cookie - - return true; # If it was decoded correctly, it's a valid session + return true; } function deletePersistentCookie() { diff --git a/libs/session.php b/libs/session.php index 10a688b..c21f512 100644 --- a/libs/session.php +++ b/libs/session.php @@ -14,14 +14,14 @@ if (isset($_POST['submit_pass']) && $_POST['pass']) { if ($passwordInForm == $passwordInConfig) { $_SESSION['password'] = $passwordInForm; - saveLoginSuccess(); + saveLogin(); } elseif ($isCodeValid = verifyTOTP( $config['totp_secret'], $passwordInForm, intval($config['totp_digits']) )) { $_SESSION['password'] = 'valid_totp'; - saveLoginSuccess(); + saveLogin(); } else { $error = 'Incorrect Password'; } diff --git a/views/test_login.php b/views/test_login.php index d750909..03d4be8 100644 --- a/views/test_login.php +++ b/views/test_login.php @@ -4,6 +4,7 @@ require_once "libs/persistent_session.php"; if (!hasValidSession()) { + echo "Not a valid session - Go to /login"; #header("Location: /login"); exit; } From 57737e8cd48011a7bddd7ca81fbc462ac8a31fb5 Mon Sep 17 00:00:00 2001 From: "eapl.mx" Date: Tue, 24 Dec 2024 14:35:52 -0600 Subject: [PATCH 3/7] feat(session): add validation for minimum length in secret_key --- libs/persistent_session.php | 6 +++++- private/config_template.ini | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/libs/persistent_session.php b/libs/persistent_session.php index 03e3b3d..7224923 100644 --- a/libs/persistent_session.php +++ b/libs/persistent_session.php @@ -9,6 +9,10 @@ if (!empty($missing_keys)) { die('Missing required keys in config.ini: ' . implode(', ', $missing_keys)); } +if (strlen($config['secret_key']) < 32) { + die('Secret key in config.ini must be at least 32 characters long'); +} + const COOKIE_NAME = 'timeline_login'; const ENCRYPTION_METHOD = 'aes-256-cbc'; const EXPIRATION_DAYS = 30; @@ -113,7 +117,7 @@ function saveLogin() { function isSavedCookieValid() { $cookieExpiry = getCookieData(); - + if ($cookieExpiry === false) { deletePersistentCookie(); return false; diff --git a/private/config_template.ini b/private/config_template.ini index cfef942..22ca1ea 100644 --- a/private/config_template.ini +++ b/private/config_template.ini @@ -42,9 +42,9 @@ webmentions_txt_path = "./mentions.txt" public_webmentions = "https://example.com/timeline/mentions.txt" [security] -; Secret key to encrypt cookies -; Create a new one here: https://randomkeygen.com -secret_key = "553GkZzIYZKx5z0lftt4yKDG4aKb4sAG" +; Secret key to encrypt cookies of at least 256-bit (32 characters) +; Create one here: https://randomkeygen.com (CodeIgniter Encryption Keys) +secret_key = "" ; Simple password password = "change_me" From 803518038002093e37608c98d3695d9c2a511e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?s=C3=B8renpeter?= Date: Wed, 25 Dec 2024 14:15:39 +0100 Subject: [PATCH 4/7] removed TODO about webmentions --- partials/webmentions_send.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partials/webmentions_send.php b/partials/webmentions_send.php index e09588f..3535bc9 100644 --- a/partials/webmentions_send.php +++ b/partials/webmentions_send.php @@ -1,6 +1,6 @@ Date: Wed, 25 Dec 2024 14:55:12 +0100 Subject: [PATCH 5/7] Use only nick as handle if nick and domain is the same --- libs/twtxt.php | 17 +++++++++++++---- partials/profile_card.php | 2 +- partials/timeline.php | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/libs/twtxt.php b/libs/twtxt.php index 9eb2003..a3533e8 100644 --- a/libs/twtxt.php +++ b/libs/twtxt.php @@ -16,6 +16,7 @@ class TwtxtFile { public $mainURL = ''; // First found URL public $URLs = []; public $nick = ''; + public $domain = ''; public $avatar = ''; public $emoji = ''; public $description = ''; @@ -456,8 +457,7 @@ function getTwtsFromTwtxtString($url) { } } - // Clean up nick if set to something like `@soren@darch.dk` instead of just `soren` - // mosty for (re)feeds from Mastodon etc. + // Clean up nick if set to something like `@soren@darch.dk` instead of just `soren` - mosty for (re)feeds from Mastodon etc. if (str_contains($twtxtData->nick, "@")) { $str = $twtxtData->nick; $str = ltrim($str, "@"); @@ -465,17 +465,25 @@ function getTwtsFromTwtxtString($url) { } // Fallback for nick and url if not set in twtxt.txt - // TODO: Use nick from local follow list as fallback? if ($twtxtData->nick === "") { $str = parse_url($url, PHP_URL_HOST); $str = str_replace("www.", "", $str); - $str = explode(".", $str)[0]; // take the first [0] from splitting the host at "." + //$str = explode(".", $str)[0]; // take the first [0] from splitting the host at "." $twtxtData->nick = $str; } if ($twtxtData->mainURL === "") { $twtxtData->mainURL = $url; } + // Use only nick as handle if nick and domain is the same + $twtxtData->domain = parse_url($twtxtData->mainURL, PHP_URL_HOST); + + if ($twtxtData->nick === $twtxtData->domain) { + $twtxtData->domain = ""; + } else { + $twtxtData->domain = "@".$twtxtData->domain; + } + if (!str_starts_with($currentLine, '#')) { $explodedLine = explode("\t", $currentLine); @@ -550,6 +558,7 @@ function getTwtsFromTwtxtString($url) { $twt->emoji = $twtxtData->emoji; $twt->nick = $twtxtData->nick; $twt->mainURL = $twtxtData->mainURL; + $twt->domain = $twtxtData->domain; $twtxtData->twts[$timestamp] = $twt; } diff --git a/partials/profile_card.php b/partials/profile_card.php index 1799e6d..dc0635b 100644 --- a/partials/profile_card.php +++ b/partials/profile_card.php @@ -63,7 +63,7 @@ if ($is_gallery) {
- nick?>@mainURL, PHP_URL_HOST);?> + @nick?>domain?>

description?>

diff --git a/partials/timeline.php b/partials/timeline.php index ca4a666..7fb60d4 100644 --- a/partials/timeline.php +++ b/partials/timeline.php @@ -15,7 +15,7 @@
- nick?>@mainURL, PHP_URL_HOST);?> + @nick?>domain?>
From b63b72d29e3bc117acfccbf6e3dc6fe1a7c92c94 Mon Sep 17 00:00:00 2001 From: Adnan ELARAJI Date: Thu, 26 Dec 2024 01:31:01 +0100 Subject: [PATCH 6/7] Fix #42 depricated creation of dynamic property Twt::$domain --- libs/twtxt.php | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/twtxt.php b/libs/twtxt.php index a3533e8..c127bfd 100644 --- a/libs/twtxt.php +++ b/libs/twtxt.php @@ -38,6 +38,7 @@ class Twt { public $avatar; public $emoji; public $nick; + public $domain; public $mainURL; public $images = []; public $tags = []; From 865b0d7e78570207c757b1fa4608e0f9c61b20d7 Mon Sep 17 00:00:00 2001 From: "eapl.mx" Date: Thu, 26 Dec 2024 11:56:44 -0600 Subject: [PATCH 7/7] feat(session): check for a valid session using function in session.php --- README.md | 12 +++--- VERSION | 2 +- libs/persistent_session.php | 15 ++++---- libs/session.php | 9 +++-- partials/timeline.php | 9 ++--- views/__load_twt_files.php | 19 ++++------ views/add_feed.php | 73 ++++++++++++++++--------------------- views/home.php | 20 +++++----- views/new_twt.php | 10 ++--- views/refresh.php | 6 +-- views/upload_img.php | 14 ++----- 11 files changed, 85 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index 3a2a48b..49eebbc 100644 --- a/README.md +++ b/README.md @@ -39,13 +39,13 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## 🛠 Installation and setup 0. You need to have a webhosting with **PHP 8** and perferable running Apache or similar for timeline to work. - + > There are free options, but I would suggest that you pay for your hosting and also get a nice domain, so you have more ownership over your data and online idetenty. 1. Download the code from https://github.com/sorenpeter/timeline as a zip 2. Upload the content of the zip to you webhosting using a FTP client - + - The default would be to put eveything from within the timeline-main folder in the root so you will have: ``` @@ -66,7 +66,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ### Webfinger endpoint setup -6. For allowing others to look you on using webfinger, you need to move the `.well-known` folder from within the `_webfinger_endpoint` to the root of your domain, so it is accesable from www.example.net/.well-know/webfinger +6. For allowing others to look you on using webfinger, you need to move the `.well-known` folder from within the `_webfinger_endpoint` to the root of your domain, so it is accesable from www.example.net/.well-know/webfinger 7. You also need to edit the `index.php` file wihtin the `.well-know/webfinger` folder and set the correct path for you timeline installation in `$timeline_dir` variable. @@ -84,6 +84,8 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## 🐞 Bugs to fix - [x] (2024-11-30) Fix issues with parsing markdown vs. twtxt syntax (replaceed slimdown with Parsedown, supporting lists, block quotes, code/blocks, links, images) +- [x] (2024-12-26) Extend session duration for 30 days +- [ ] (2024-12-26) Read the config.ini in a centralized place and add validations useful when installing or upgrading `timeline`. ## 🚀 Features to code @@ -97,7 +99,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # 🙏 Credits / shoutouts -## Ideas and inspiration +## Ideas and inspiration - [twtxt](https://twtxt.readthedocs.io) - The original decentralised, minimalist microblogging service for hackers @@ -109,7 +111,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - [twtxt-php](https://github.com/eapl-gemugami/twtxt-php) - A minimalistic and personal PHP site for your twtxt microblogging. -- [Slimdown](https://github.com/jbroadway/slimdown) - A simple regex-based Markdown parser in PHP. +- [Slimdown](https://github.com/jbroadway/slimdown) - A simple regex-based Markdown parser in PHP. - Tag cloud feature is based on php code by [Domingos Faria](https://social.dfaria.eu/search) diff --git a/VERSION b/VERSION index a5ee8d9..9d56291 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2024.12.23 +2024.12.26 diff --git a/libs/persistent_session.php b/libs/persistent_session.php index 7224923..cf444ec 100644 --- a/libs/persistent_session.php +++ b/libs/persistent_session.php @@ -32,7 +32,7 @@ session_start([ function hasValidSession(): bool { # If short lived session is valid - if (isset($_SESSION['session_expiration'])) { + if (isset($_SESSION['session_expiration']) && $_SESSION['session_expiration'] > time()) { return true; } @@ -62,23 +62,21 @@ function getCookieData() { $config = parse_ini_file('private/config.ini'); - # The cookie data contains the actual data w/ the hash concatonated to the end, + # The cookie data contains the actual data w/ the hash concatenated to the end, # since the hash is a fixed length, we can extract the last hash_length chars # to get the hash. $hash = substr($raw, strlen($raw) - HASH_LENGTH, HASH_LENGTH); $data = substr($raw, 0, - (HASH_LENGTH)); - # Calculate what the hash should be, based on the data. If the data has not been + # Calculate the expected hash from the data. If the data has not been # tampered with, $hash and $hash_calculated will be the same $hash_calculated = hash_hmac(HASH_ALGORITHM, $data, $config['secret_key']); - # If we calculate a different hash, we can't trust the data. if ($hash_calculated !== $hash) { - #echo "Different HASH"; + #echo "Different HASH. Tempered data?"; return False; } - # Is it expired ? if (intval($data) < time()) { #echo "Cookie expired"; return False; @@ -91,7 +89,7 @@ function makePersistentCookie() { $config = parse_ini_file('private/config.ini'); $cookieExpiry = EXPIRATION_DAYS * 24 * 60 * 60 + time(); # X days - #$cookieExpiry = 10 + time(); # Debug value - 5 minutes + #$cookieExpiry = 10 + time(); # Debug value - 10 seconds # Calculate a hash for the data and append it to the end of the data string $cookieValue = strval($cookieExpiry); @@ -123,6 +121,9 @@ function isSavedCookieValid() { return false; } + # @eapl As it's implemented, the user has to login again in 30 days + # since the first login, which I think is a good compromise. + # Refresh session $_SESSION['session_expiration'] = intval($cookieExpiry); diff --git a/libs/session.php b/libs/session.php index c21f512..a920687 100644 --- a/libs/session.php +++ b/libs/session.php @@ -5,9 +5,12 @@ require_once 'libs/persistent_session.php'; $config = parse_ini_file('private/config.ini'); $passwordInConfig = $config['password']; -# TODO: Replace using $_SESSION['password'] in other files -# to check for a valid session, as in 'new_twt.php' -# Use hasValidSession() instead +function checkValidSessionOrRedirectToLogin() { + if (!hasValidSession()) { + header('Location: ./login'); + exit(); + } +} if (isset($_POST['submit_pass']) && $_POST['pass']) { $passwordInForm = $_POST['pass']; diff --git a/partials/timeline.php b/partials/timeline.php index ca4a666..0f63089 100644 --- a/partials/timeline.php +++ b/partials/timeline.php @@ -39,7 +39,7 @@ if (isset($_SESSION['password'])) { echo ' | Reply'; - } + } ?> @@ -49,9 +49,8 @@ Comment via email'; -} - -?> \ No newline at end of file +} \ No newline at end of file diff --git a/views/__load_twt_files.php b/views/__load_twt_files.php index 96ce792..89d4286 100644 --- a/views/__load_twt_files.php +++ b/views/__load_twt_files.php @@ -8,16 +8,13 @@ require_once('libs/twtxt.php'); require_once('libs/hash.php'); */ -require_once("partials/base.php"); +require_once "partials/base.php"; +require_once "libs/session.php"; + +checkValidSessionOrRedirectToLogin(); $config = parse_ini_file('private/config.ini'); -if (!isset($_SESSION['password'])) { - header('Location: ./login'); - exit(); -} - - $max_execution_time = intval($config['max_execution_time']); if ($max_execution_time < 1) { $max_execution_time = 1; @@ -69,15 +66,15 @@ foreach ($fileLines as $currentLine) { $i = 1; $total = count($twtFollowingList); -foreach ($twtFollowingList as $following) { +foreach ($twtFollowingList as $following) { $float = $i/$total; $percent = intval($float * 100)."%"; - + // Javascript for updating the progress bar and information echo ''; updateCachedFile($following[1]); diff --git a/views/add_feed.php b/views/add_feed.php index 41f0e03..61f46c9 100644 --- a/views/add_feed.php +++ b/views/add_feed.php @@ -1,6 +1,9 @@ - + include 'partials/header.php'; + ?> -

Webfinger lookup

+

Webfinger lookup

-
- - -
-
-
- - - -

Add a new feed to follow

- -
-
- - - - + + +
- -
-
+
+ - + - +

Add a new feed to follow

+ +
+
+ + + + +
+ +
+
+ + + + \ No newline at end of file diff --git a/views/home.php b/views/home.php index a326e6d..a8d0ae0 100644 --- a/views/home.php +++ b/views/home.php @@ -18,25 +18,23 @@ if (!empty($_GET['profile'])) { // Show twts for some user (Profile view) // Load twts, taking $paginateTwts into consideration require_once 'partials/base.php'; +require_once 'libs/session.php'; -$title = "Timeline for ".$title; +$title = "Timeline for $title"; - -// Redirect guests to Profile view, if url not set til home twtxt.txt - -if (!isset($_SESSION['password']) && (isset($_GET['url']))) { - if ($_GET['url'] != $config['public_txt_url']) { - header('Location: ./profile'); - exit(); - } +// Redirect guests to Profile view, if URL isn't set to home twtxt.txt +if (!hasValidSession() && isset($_GET['url'])) { + if ($_GET['url'] != $config['public_txt_url']) { + header('Location: ./profile'); + exit(); + } } include_once 'partials/header.php'; -if (isset($_SESSION['password'])) { +if (hasValidSession()) { include 'views/new_twt.php'; // TODO: Split up new_twt into a view and a partial } else { - echo '

Timeline

'; echo '

Recent posts from feeds followed by diff --git a/views/new_twt.php b/views/new_twt.php index f90980b..5fcf6b8 100644 --- a/views/new_twt.php +++ b/views/new_twt.php @@ -1,4 +1,8 @@ ";