Procházet zdrojové kódy

Implement new login, Improve installer script

Andy Baugh před 7 roky
rodič
revize
f2e9aa70ca
7 změnil soubory, kde provedl 353 přidání a 58 odebrání
  1. 10 8
      README.md
  2. 28 40
      bin/install
  3. 110 0
      lib/auth.inc
  4. 0 0
      lib/configure.php
  5. 25 10
      sys/admin/index.php
  6. 172 0
      sys/admin/login.inc
  7. 8 0
      sys/admin/logout.inc

+ 10 - 8
README.md

@@ -10,17 +10,19 @@ Oh, yeah, did I mention it's responsive by default and degrades well on IE (last
 
 See http://tcms.troglodyne.net for more information.
 
-Installing this is pretty easy,
-either grab an archive from above site and extract it or git clone it into a public html directory.
+INSTALLING
+----------
+* Clone this repo to wherever.
+* Run `bin/install` from the repository's top directory. It will guide you through the process.
+* Load the website you installed this to in a web browser. Continue to follow instructions.
 
-WARNING: If you don't setup HTTP Server based Authentication, you deserve what you get.
-See the manual for more information: http://tcms.troglodyne.net/index.php?nav=1&dir=fileshare/manual
-
-As of the latest version, there should be no upgrade issues,
-despite switching to using JSON to store new postings.
-The code anticipates and uses the legacy style of accessing old posts in that instance.
+UPGRADING
+---------
+* Re-run the installer. Since you have already installed, it will skip most prompting and
+simply update the files.
 
 TODO/Ideas:
+-----------
  * Convert blog posts to use JSON, similar to microblog, mostly to enable storing better metadata.
  * Theming importation ability, or a decent upgrading script for more effective cruise control.
  * Test code. I'll probably do this in perl,

+ 28 - 40
bin/install

@@ -1,12 +1,13 @@
-#!/usr/bin/php
+#!/usr/bin/env php
 <?php
-$update_only = 0;
-if( $argv[1] && stripos( $argv[1], "update") !== false ) $update_only = 1;
-
 $user_info = posix_getpwuid(posix_geteuid());
 $wd = realpath( dirname( __FILE__ ) . "/../" );
 
-if( !$update_only ) {
+// Look for the $private_base and $public_base. Assume /var/www/ if no homedir.
+$homedir = ( $user_info['dir'] ? $user_info['dir'] : '/var/www/' );
+$private_base = ( file_exists( "$homedir/.tCMS_basedir" ) ) ? file_get_contents( "$homedir/.tCMS_basedir" ) : "";
+
+if( empty( $private_base ) ) {
     echo "[NOTE] Make sure the directories you install to are R+W accessible by your web server.\n";
     echo "       On most shared hosts (cPanel, etc.) this is fine to leave in your home directory.\n\n";
     echo "Please enter the directory you wish for tCMS' private data files to be stored under.\n";
@@ -19,6 +20,7 @@ if( !$update_only ) {
         $private_base = realpath( dirname( $input ) ) . "/" . basename( $input );
     }
 }
+
 echo "Please enter the directory you for tCMS' public data files.\n";
 echo "Default [" . $user_info['dir'] . "/public_html]: ";
 $input = get_string_from_stdin();
@@ -27,55 +29,41 @@ if( empty( $input ) ) {
 } else {
     $public_base = realpath( dirname( $input ) ) . "/" . basename( $input );
 }
+
+echo "[INFO] Installing private data to $private_base now...\n";
+$files = [ 'lib', 'templates' ];
+install( $private_base, $wd, $files );
+ensure_directories_exist( [ "$private_base/blog", "$private_base/microblog", "$private_base/conf" ] );
+
 # Make sure our webserver (and my horrible code) knows how to find your custom directory
-if( !$update_only ) {
-    if( $private_base != $user_info['dir'] . "/.tCMS" ) {
-        echo "[WARN]: For tCMS to know where this directory we install to is, we will write the following\n";
-        echo "        into your document root for the default/virtual host you control on your Web Server:\n";
-        echo "            '$public_base/basedir'\n";
-        echo "        Please add an HTACCESS or similar blocker to prevent public acess to this file if you\n";
-        echo "        wish to prevent this information disclosure. Please press [enter] to continue.";
-        get_string_from_stdin(); # Mostly just to make the user confirm they read this.
-    }
+if( !file_exists($user_info['dir'] . "/.tCMS_basedir") ) {
+    echo "[INFO]: For tCMS to know where the directory we installed to was, we will write the following\n";
+    echo "        into your home directory:\n";
+    echo "            '" . $user_info["dir"] . "/.tCMS_basedir'\n";
+    echo "        Please press [enter] to continue.";
+    get_string_from_stdin(); # Mostly just to make the user confirm they read this.
+    file_put_contents( $user_info['dir'] . "/.tCMS_basedir", $private_base );
+}
+
+# Create admin user
+if( !file_exists("$private_base/conf/users.json") ) {
     while( empty($username) ) {
-        echo "Please enter in the name you'd like to login to tCMS as (you can add more users later): ";
+        echo "Please enter in the name you'd like to login to tCMS as administrator (you can add more users later): ";
         $username = get_string_from_stdin();
     }
     while( empty($password) ) {
         echo "Please enter a password for the user: ";
         $password = get_string_from_stdin(1);
+        $auth_hash = password_hash( $password, PASSWORD_DEFAULT );
     }
-
-    echo "[INFO] Installing private data to $private_base now...\n";
-    $files = [ 'lib', 'templates' ];
-    install( $private_base, $wd, $files );
-    ensure_directories_exist( [ "$private_base/blog", "$private_base/microblog", "$private_base/conf" ] );
-
-    echo "[INFO] Writing user configuration and generating 4096 bit RSA keypair now...\n";
-    $openssl_config = [
-        'private_key_bits' => 4096,
-        'digest_alg'       => "sha512",
-        'private_key_type' => OPENSSL_KEYTYPE_RSA,
-    ];
-    $cryptkeeper = openssl_pkey_new( $openssl_config );
-    openssl_pkey_export( $cryptkeeper, $privkey, $password );
-    $pubkey = openssl_pkey_get_details($cryptkeeper);
-    $pubkey = $pubkey["key"];
-    file_put_contents( "$private_base/conf/users.json", json_encode( [ $username => [ 'pubkey' => $pubkey ] ] ) );
-    echo "*** PLEASE COPY THE BELOW PRIVATE KEY AND SAVE IT SOMEWHERE SAFE!!!! ***\n";
-    echo "*** THIS IS IMPORTANT AS THIS KEY WILL BE USED FOR YOUR LOGIN!       ***\n\n";
-    echo "$privkey\n\n";
-    echo "Please press [ENTER] to continue.";
-    get_string_from_stdin();
+    echo "\n[INFO] Writing user configuration now...\n";
+    file_put_contents( "$private_base/conf/users.json", json_encode( [ $username => [ 'auth_hash' => $auth_hash ] ] ) );
 }
 
 echo "[INFO] Installing public data to $public_base now...\n";
 $files = [ 'css', 'fileshare', 'img', 'index.php', 'sys' ];
 install( $public_base, $wd, $files );
 ensure_directories_exist( [ "$public_base/css/custom", "$public_base/img/icon/custom" ] );
-if( !$update_only && $private_base != $user_info['dir'] . "/.tCMS" ) {
-    file_put_contents( "$public_base/basedir", $private_base );
-}
 
 echo "[INFO] Done.\n";
 exit(0);

+ 110 - 0
lib/auth.inc

@@ -0,0 +1,110 @@
+<?php
+    class auth {
+
+        public function __construct() {
+            ini_set( "session.cookie_httponly", true );
+            ini_set( "session.cookie_secure",   true );
+            return;
+        }
+
+        public function ensure_auth( $redirect=true ) {
+            // Force HTTPS
+            if( empty( $_SERVER["HTTPS"] ) ) {
+                http_response_code(301);
+                header("Location: https://" . $_SERVER["SERVER_NAME"] . "/" . $_SERVER["REQUEST_URI"]);
+                die();
+            }
+            // Check on the session
+            $session_status = session_status();
+            // Way to be consistent, PHP
+            if( $session_status !== PHP_SESSION_ACTIVE || $session_status !== 2 ) session_start(); # Will re-use the existing session and jam the deets into the $_SESSION global
+            $session_status = session_status();
+            if( empty( $_SESSION )
+                || ( $session_status !== PHP_SESSION_ACTIVE || $session_status !== 2 )
+                || ( isset( $_SESSION['LAST_ACTIVITY'] ) && ( time() - $_SESSION['LAST_ACTIVITY'] > 3600 ) )
+                || $_SESSION['REMOTE_ADDR'] !== $_SERVER['REMOTE_ADDR'] ) {
+                $to = ( $_GET['app'] ) ? "&to=" . $_GET['app'] : "";
+                auth::invalidate_auth( $redirect, $to );
+            }
+            $_SESSION['LAST_ACTIVITY'] = time();
+            return session_id();
+        }
+
+        public function invalidate_auth( $redirect=true, $to="" ) { 
+            // need to invalidate the session here, though we may not have loaded it yet, so do that first.
+            $session_status = session_status();
+            if( $session_status !== PHP_SESSION_ACTIVE || $session_status !== 2 ) session_start();
+            $session_status = session_status();
+            $session_id = session_id();
+            if( $session_status === PHP_SESSION_ACTIVE || $session_status === 2 ) { 
+                session_unset();
+                session_destroy();
+            }   
+            setcookie('PHPSESSID'); //Otherwise it'll stick around. I don't wanna reuse these.
+            if( $redirect ) {
+                http_response_code(302);
+                header( "Location: https://" . $_SERVER["SERVER_NAME"] . "/sys/admin/index.php?app=login$to" );
+            } else {
+                http_response_code(401);
+                header('Content-Type: application/json');
+                echo json_encode( [ 'code' => '401', 'message' => 'Unauthorized' ] );
+            }   
+            die();
+        }   
+
+        public function do_auth($user=null, $pass=null) {
+            if( empty($user) || empty($pass) ) {
+                return array( 'err' => 1, 'msg' => "No Credentials provided yet" );
+            }
+                        if( empty( ini_get('session.entropy_file') ) && version_compare(PHP_VERSION, "7.1.0") === -1 ) {
+                ini_set('session.entropy_file', '/dev/urandom');
+                ini_set('session.entropy_length', '32');
+            }
+            // Check it
+            $user_info = posix_getpwuid(posix_geteuid());
+            $homedir = ( $user_info['dir'] ? $user_info['dir'] : '/var/www/' );
+            $confdir = ( file_exists( "$homedir/.tCMS_basedir") ? file_get_contents("$homedir/.tCMS_basedir") . "/conf" : "$homedir/.tCMS/conf" );
+            $conf = file_get_contents("$confdir/users.json");
+            if( $conf === false ) return [ 'err' => 1, "msg" => "Login Failed: Configuration missing" ];
+            $conf = json_decode( $conf, 1 );
+            if( $conf === null ) return [ 'err' => 1, "msg" => "Login Failed: Configuration malformed" ];
+            if( empty($conf[$user]) || empty($conf[$user]['auth_hash']) || !password_verify( $pass, $conf[$user]['auth_hash'] ) ) return [ 'err' => 1, 'msg' => "Login Failed" ];
+
+            // Gotta have a touch of eval
+            $session_started = @session_start();
+            if( empty($_SESSION) ) {
+                @session_destroy();
+                session_id(uniqid("tCMS-"));
+                session_start();
+            }
+            $session_state   = session_status();
+            $session_id      = session_id();
+            if( empty($session_started) || empty( $session_state ) || $session_state !== PHP_SESSION_ACTIVE || empty( $session_id ) ) {
+                return [ 'err' => 1, 'msg' => 'Failed to generate valid PHP Session' ];
+            }
+            $_SESSION['LAST_ACTIVITY'] = time(); //Timeout helper
+            $_SESSION['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR']; // Hijacking prevention helper
+            return [ 'success' => 1 ];
+        }
+
+        public static function process_token( $api_token=null ) {
+            if( session_status() !== PHP_SESSION_ACTIVE ) session_start();
+            if( empty( $api_token ) ) return 0;
+            $key = file_get_contents( "/var/cpanel/qa/.userdata_cache/global/session_keys/" . session_id() );
+            if( empty( $key ) ) return 0;
+            $api_token = base64_decode($api_token);
+            $key = openssl_get_privatekey($key);
+            openssl_private_decrypt( $api_token, $credentials, $key );
+            if( empty( $credentials ) ) return 0;
+            $credentials = explode( ":", $credentials );
+            if( count( $credentials ) !== 2 ) return 0;
+            return array( 'user' => $credentials[0], 'pass' => $credentials[1] );
+        }
+
+        private static function get_tCMS_basedir() {
+            $file = realpath( __FILE__ . "../../../basedir" );
+            $exists = ( file_exists( $file ) ? explode( "\n", file_get_contents($file) )[0] : "/" );
+        }
+
+    }
+?>

+ 0 - 0
sys/admin/lib/configure.php → lib/configure.php


+ 25 - 10
sys/admin/index.php

@@ -1,13 +1,27 @@
 <?php
-    include_once("lib/auth.inc");
-	auth::ensure_auth();
-    if( empty($_GET['app']) || $_GET['app'] == 'config' ) {
+    $args = ( $_SERVER['REQUEST_METHOD'] == 'POST' ? $_POST : $_GET );
+    $user_info = posix_getpwuid(posix_geteuid());
+    // Probably the 'sanest' default I could think of when you have no homedir
+    $dir = ( $user_info['dir'] ? $user_info['dir'] : '/var/www/' );
+    $basedir = ( file_exists( $dir . "/.tCMS_basedir") ? file_get_contents("$dir/.tCMS_basedir") : "$dir/.tCMS" );
+    if( !empty($args['app']) && $args['app'] == 'login' ) {
+        include("login.inc");
+        die();
+    } elseif( !empty($args['app']) && $args['app'] == 'logout' ) {
+        include("logout.inc");
+        die();
+    } else {
+        include_once("$basedir/lib/auth.inc");
+        $auth = new auth;
+        $auth->ensure_auth();
+    }
+    if( empty($args['app']) || $args['app'] == 'config' ) {
         $kontent = "settings.inc";
-    } elseif ($_GET['app'] == 'blog') {
+    } elseif ($args['app'] == 'blog') {
         $kontent = "bengine.inc";
-    } elseif ($_GET['app'] == 'microblog') {
+    } elseif ($args['app'] == 'microblog') {
         $kontent = "mbengine.inc";
-    } elseif ($_GET['app'] == 'users' ) {
+    } elseif ($args['app'] == 'users' ) {
         $kontent = "users.inc";
     } else {
         $kontent = "settings.inc";
@@ -32,16 +46,17 @@
   <link rel="icon" type="image/vnd.microsoft.icon" href="../../img/icon/favicon.ico" />
   <title>tCMS Admin</title>
   <?php
-   $config = json_decode(file_get_contents('config/main.json'),true);
+   $config = @json_decode(@file_get_contents("$basedir/config/main.json"),true);
   ?>
  </head>
  <body>
   <div id="topkek" style="text-align: center; vertical-align: middle;">
    <button title="Menu" id="clickme">&#9776;</button>
    <span id="configbar">
-    <a class="topbar" title="Edit Various Settings" href="index.php">Settings</a>
-    <a class="topbar" title="Blog Writer" href="index.php?nav=1">Blog Writer</a>
-    <a class="topbar" title="Pop off about Stuff" href="index.php?nav=2">MicroBlogger</a>
+    <a class="topbar" title="Edit Various Settings" href="index.php?app=config">Settings</a>
+    <a class="topbar" title="Blog Writer" href="index.php?app=blog">Blog Writer</a>
+    <a class="topbar" title="Pop off about Stuff" href="index.php?app=microblog">MicroBlogger</a>
+    <a class="topbar" title="Logout" href="index.php?app=logout">Logout</a>
    </span>
   </div>
   <div id="kontent" style="display: block;">

+ 172 - 0
sys/admin/login.inc

@@ -0,0 +1,172 @@
+<?php
+    if( empty( $_SERVER["HTTPS"] ) ) {
+        http_response_code(301);
+        header("Location: https://" . $_SERVER["SERVER_NAME"] . "/" . $_SERVER["REQUEST_URI"]);
+        die();
+    }
+    $loginFailure = -1;
+    $supported_methods = [ 'GET', 'POST' ];
+    if( in_array( $_SERVER['REQUEST_METHOD'], $supported_methods ) ) {
+        switch($_SERVER['REQUEST_METHOD']) {
+            case 'GET' : $request = &$_GET; break;
+            case 'POST': $request = &$_POST; break;
+        }
+        $args = [];
+        foreach( $request  as $key => $val ) {
+            //Clean the junk
+            $args[$key] = urldecode(filter_var( $val, FILTER_SANITIZE_ENCODED, FILTER_SANITIZE_SPECIAL_CHARS ));
+        }
+        $loginMsg = "Unknown Error";
+        if( $_SERVER['REQUEST_METHOD'] === 'POST' ) {
+            require_once("$basedir/lib/auth.inc"); //Authenticate
+            $auth = new auth;
+            $login = $auth->do_auth( $args['username'], $args['password'] );
+            if(!is_array($login) || !empty($login['err']) || empty( $login['success'] ) ) {
+                $loginFailure = 1;
+                if( !empty( $login['msg'] ) ) $loginMsg = $login['msg'];
+                http_response_code(401);
+            } else { # We succeeded
+                $loginMsg = "Login Succeded, redirecting...";
+                $loginFailure = 0;
+            }
+        }
+    }
+    # Construct the redirection URL
+    if( empty( $args['to'] ) ) {
+        $redir = 'index.php';
+    } else {
+        $redir = 'index.php?app=' . $args["to"];
+    }
+    $redirection_URL = "https://" . $_SERVER["SERVER_NAME"] . "/sys/admin/$redir";
+?>
+
+<!doctype html>
+<html dir="ltr" lang="en-US">
+  <head>
+    <title>tCMS 2 ~ Login</title>
+    <style>
+      body {
+        background-color: gray;
+        font-family: sans-serif;
+      }
+      #login {
+        margin: 0 auto;
+        max-width: 25rem;
+        color: white;
+      }
+      #logo {
+        display: block;
+        max-width: 90%;
+        margin: 0 0 0 5%;
+        height: 2rem;
+      }
+      #login form {
+        max-width: 85%;
+        display: block;
+        margin: 0 auto;
+      }
+      #maximumGo {
+        width: calc(100% - 1rem);
+      }
+      #copyright {
+        font-size: .75rem;
+        text-align: center;
+      }
+      #jsalert {
+        margin: 1rem;
+        border-radius: .75rem;
+        border-color: rgba(255,0,0,.75);
+        padding: 1rem;
+      }
+      .alert-danger {
+        background-color: rgba(255,0,0,.25);
+      }
+      .alert-success {
+        background-color: rgba(0,255,0,.25);
+      }
+      input {
+        box-sizing: border-box;
+        border-radius: .5em;
+        border: .25em solid black;
+        color: white;
+        padding: .25em;
+        margin: .25em;
+      }
+      .input-group {
+        display: table;
+        width: 100%;
+      }
+      .input-group > input, .input-group > label {
+        display: table-cell;
+      }
+      .input-group > input {
+        width: calc(100% - 1rem);
+        background-color: #333;
+      }
+      input[type="submit"] {
+        box-shadow: 0 0 .5em black;
+        background-color: #333;
+        color: white;
+      }
+    </style>
+    <script>
+      document.addEventListener("DOMContentLoaded", function(event) {
+        var loginFailure = <?php echo $loginFailure; ?>;
+        if( loginFailure === -1 ) {
+          document.querySelector('#jsalert').style.cssText = 'visibility: hidden;';
+        } else if ( loginFailure === 1 ) {
+          document.querySelector('#jsalert').classList.remove("alert-success");
+          document.querySelector('#jsalert').classList.add("alert-danger");
+          document.querySelector('#msgIcon').innerHTML = "❌";
+          document.querySelector('#message').innerHTML = "<?php echo $loginMsg ?>";
+        } else {
+          document.querySelector('#jsalert').classList.remove("alert-danger");
+          document.querySelector('#jsalert').classList.add("alert-success");
+          document.querySelector('#msgIcon').innerHTML = "✓";
+          document.querySelector('#message').innerHTML = "<?php echo $loginMsg ?>";
+          window.setTimeout(function() {
+            window.location="<?php echo $redirection_URL; ?>";
+          }, 500);
+        }
+      });
+    </script>
+    <link rel="icon" type="image/vnd.microsoft.icon" href="../../img/icon/favicon.ico" />
+  </head>
+  <body>
+      <div id="login">
+        <div id="jsalert" class="alert-danger">
+          <table>
+            <tr>
+              <td id="msgIcon">
+                ⚠
+              </td>
+              <td id="message" style="padding-left: 1rem;">
+                Please enable JavaScript on this domain.
+              </td>
+            </tr>
+          </table>
+        </div>
+        <div>
+          <img id="logo" src="../../img/icon/tCMS.svg" style="float:left" /><span style="font-family:courier;font-size:2rem;">CMS Login</span>
+        </div>
+        <div id="spacer" style="clear: both;"><br /></div>
+        <form method="POST" action="index.php">
+          <input type="hidden" name="app" value="login" />
+          <input type="hidden" name="to" value="<?php if(!empty($args['to'])) echo $args['to']; ?>" />
+          Username<br />
+          <div class="input-group">
+            <label for="username">😎</span></label>
+            <input name="username" id="username" placeholder="AzureDiamond" value="" type="text"></input>
+          </div>
+          <br />
+          Password<br />
+          <div class="input-group">
+            <label for="password">🔑</label>
+            <input name="password" id="password" placeholder="hunter2" value="" type="password"></input>
+          </div>
+          <br />
+          <input type="submit" id="maximumGo" value="Log in"></input>
+        </form>
+      </div>
+  </body>
+</html>

+ 8 - 0
sys/admin/logout.inc

@@ -0,0 +1,8 @@
+<?php
+    $user_info = posix_getpwuid(posix_geteuid());
+    $homedir = ( $user_info['dir'] ? $user_info['dir'] : '/var/www' );
+    $libdir = ( file_exists( "$homedir/.tCMS_basedir") ? file_get_contents("$homedir/.tCMS_basedir") . "/lib" : "$homedir/.tCMS/lib" );
+    include_once("$libdir/auth.inc");
+    auth::invalidate_auth();
+    die();
+?>