4

I am using session_set_save_handler() to save sessions to a DB.

After moving from php v.5.3 to v.5.4 the write() function is not called at all; neither when calling the session_write_close() function, nor when the script is terminated (it was working correctly before and there were no changes done to code). The read(), open() and close() functions are still called as usual.

I know there are several changes in php 5.4 related to session_set_save_handler() mechanism. Does anybody have similar problem or know what was changed?

class session {

    private $table_name;

    function __construct() {
        $this->table_name = SESS_TABLE;

        session_set_save_handler(array($this, 'open'), array($this, 'close'), array($this, 'read'), array($this, 'write'), array($this, 'destroy'), array($this, 'gc'));
        register_shutdown_function('session_write_close');

    }

    function start_session($session_name, $secure) {
        global $session;

        // Make sure the session cookie is not accessable via javascript.
        $httponly = true;

        // Hash algorithm to use for the sessionid. (use hash_algos() to get a list of available hashes.)
        $session_hash = 'sha512';

        // Check if hash is available
        if (in_array($session_hash, hash_algos())) {
          // Set the has function.
          ini_set('session.hash_function', $session_hash);
        }
        // How many bits per character of the hash.
        // The possible values are '4' (0-9, a-f), '5' (0-9, a-v), and '6' (0-9, a-z, A-Z, "-", ",").
        ini_set('session.hash_bits_per_character', 5);

        // Force the session to only use cookies, not URL variables.
        ini_set('session.use_only_cookies', 1);

        // Get session cookie parameters 
        $cookieParams = session_get_cookie_params(); 
        // Set the parameters
        session_set_cookie_params($cookieParams["lifetime"], $cookieParams["path"], $cookieParams["domain"], $secure, $httponly); 
        // Change the session name 
        session_name($session_name);
        // Now we cat start the session
        session_start();
        // This line regenerates the session and delete the old one. 
        // It also generates a new encryption key in the database. 
        if(USE_REGENERATE){
            $this->regenerate_id();
        }
        //session_regenerate_id(true);    
    }

    function regenerate_id() {

        $old_sess_id = session_id();
        session_regenerate_id(true);
        $new_sess_id = session_id();
        Logger::write($old_sess_id .'-'.$new_sess_id   ,  'session.log');

        $time = time();
        if(!isset($this->u_stmt)) {
          $this->u_stmt = $this->db->prepare(" UPDATE ".$this->table_name." set id = ? where id=?");
        }

        $this->u_stmt->bind_param('ss', $new_sess_id,$old_sess_id);
        $this->u_stmt->execute();
        return true;
    }

    function open() {
        $host = 'localhost';
        $user = SESS_USER;
        $pass = SESS_PASSWORD;
        $name = SESS_DBNAME;
        $mysqli = new mysqli($host, $user, $pass, $name);
        $this->db = $mysqli;
        return true;
    }

    function close() {
        $this->db->close();
        return true;
    }

    function read($id) {
        global $s_read_start, $s_read_end;
        $s_read_start = microtime(true);
        if(!isset($this->read_stmt)) {
          $this->read_stmt = $this->db->prepare("SELECT data FROM ".$this->table_name." WHERE id = ? LIMIT 1");
        }
        $this->read_stmt->bind_param('s', $id);
        $this->read_stmt->execute();
        $this->read_stmt->store_result();
        $this->read_stmt->bind_result($data);
        $this->read_stmt->fetch();
        $key = $this->getkey($id);
        $data = $this->decrypt($data, $key);
        $s_read_end = microtime(true);
        if($s_read_end-$s_read_start > MORE_THEN)
            error_log (date("Y-m-d H:i:s").' '.'READ: '. ($s_read_end-$s_read_start).PHP_EOL,3,BASE_DIR.'/logs/cookies.log');
        return $data;
    }

    function write($id, $data) {
        error_log (date("Y-m-d H:i:s").' '.'WRITE: '.PHP_EOL,3,BASE_DIR.'/logs/cookies.log');
        global $s_write_start, $s_write_end;
        $s_write_start = microtime(true);
        // Get unique key
        $key = $this->getkey($id);
        // Encrypt the data
        $data = $this->encrypt($data, $key);

        $time = time();
        if(!isset($this->w_stmt)) {
          $this->w_stmt = $this->db->prepare("REPLACE INTO ".$this->table_name." (id, set_time, data, session_key) VALUES (?, ?, ?, ?)");
        }

        $this->w_stmt->bind_param('siss', $id, $time, $data, $key);
        $this->w_stmt->execute();
        $s_write_end = microtime(true);
        if($s_write_end-$s_write_start > MORE_THEN)
            error_log (date("Y-m-d H:i:s").' '.'WRITE: '. ($s_write_end-$s_write_start).PHP_EOL,3,BASE_DIR.'/logs/cookies.log');
        return true;
    }

    function destroy($id) {
        global $s_destroy_start, $s_destroy_end;
        $s_destroy_start = microtime(true);

        if(!isset($this->delete_stmt)) {
          $this->delete_stmt = $this->db->prepare("DELETE FROM ".$this->table_name." WHERE id = ?");
        }
        $this->delete_stmt->bind_param('s', $id);
        $this->delete_stmt->execute();
        $s_destroy_end = microtime(true);
        if($s_destroy_end-$s_destroy_start > MORE_THEN)
            error_log (date("Y-m-d H:i:s").' '.'DESTROY: '. ($s_destroy_end-$s_destroy_start).PHP_EOL,3,BASE_DIR.'/logs/cookies.log');
        return true;
    }

    function gc($max) {
        global $s_gc_start, $s_gc_end;
        $s_gc_start = microtime(true);

        if(!isset($this->gc_stmt)) {
            $this->gc_stmt = $this->db->prepare("DELETE FROM ".$this->table_name." WHERE set_time < ?");
        }
        $old = time() - $max;
        $this->gc_stmt->bind_param('s', $old);
        $this->gc_stmt->execute();
        $s_gc_end = microtime(true);
        if($s_gc_end-$s_gc_start > MORE_THEN)
            error_log (date("Y-m-d H:i:s").' '.'GC: '. ($s_gc_end-$s_gc_start).PHP_EOL,3,BASE_DIR.'/logs/cookies.log');
        return true;
    }

    private function getkey($id) {
        if(!isset($this->key_stmt)) {
          $this->key_stmt = $this->db->prepare("SELECT session_key FROM ".$this->table_name." WHERE id = ? LIMIT 1");
        }
        $this->key_stmt->bind_param('s', $id);
        $this->key_stmt->execute();
        $this->key_stmt->store_result();
        if($this->key_stmt->num_rows == 1) { 
          $this->key_stmt->bind_result($key);
          $this->key_stmt->fetch();
          return $key;
        } else {
          $random_key = hash('sha512', uniqid(mt_rand(1, mt_getrandmax()), true));
          return $random_key;
        }
    }

    private function encrypt($data, $key) {
        $salt = 'cH!swe!retReGu7W6bEDRup7usuDUh9THeD2CHeGE*ewr4n39=E@rAsp7c-Ph@pH';
        $key = substr(hash('sha256', $salt.$key.$salt), 0, 32);
        $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB);
        $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
        $encrypted = base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $data, MCRYPT_MODE_ECB, $iv));
        return $encrypted;
    }

    private function decrypt($data, $key) {
        $salt = 'cH!swe!retReGu7W6bEDRup7usuDUh9THeD2CHeGE*ewr4n39=E@rAsp7c-Ph@pH';
        $key = substr(hash('sha256', $salt.$key.$salt), 0, 32);
        $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB);
        $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
        $decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, base64_decode($data), MCRYPT_MODE_ECB, $iv);
        return $decrypted;
    }
}

Usage:

$session = new session();
$session->start_session('name', false);

Sorry for some debug in the code.

Decent Dabbler
  • 22,532
  • 8
  • 74
  • 106
  • 1
    Can you show the code? (if it is too much then post it to pastebin and share the link may also be ok) – hek2mgl Mar 26 '13 at 19:32
  • seems like session_register_shutdown ( void ) is used in a way not intended for >= 5.4? Use instead `register_shutdown_function('shutdown');`? Also, isn't there an interface you can use now too? – ficuscr Mar 26 '13 at 19:45
  • @ficuscr I can see no problem with `register_shutdown_function()` – hek2mgl Mar 26 '13 at 19:50
  • I tried this change : register_shutdown_function('shutdown'); - it didn't help. Due to debug I can see that close() function is called but it should be called after write()! – Valery Patskevich Mar 26 '13 at 19:52
  • Bah, think I confused myself there. Anyway, My suggestion is if you are taking this to PHP >= 5.4 you might as well refactor a bit and make use of the [`SessionHandlerInterface`](http://www.php.net/manual/en/class.sessionhandlerinterface.php). I fell like there is a mismatch of old and new approaches in your code. – ficuscr Mar 26 '13 at 19:58
  • Can you try this : http://pastebin.com/MzayDP6A – hek2mgl Mar 26 '13 at 19:59
  • I have tried it. The object still exists. – Valery Patskevich Mar 26 '13 at 20:03
  • Are you sure, that *session read* works? – hek2mgl Mar 26 '13 at 20:08
  • the read() is called but it can read nothing because nothing was written. open() close() and read() are called. – Valery Patskevich Mar 26 '13 at 20:09
  • That's why it is strange for me. – Valery Patskevich Mar 26 '13 at 20:10
  • How do you make sure that *it is called* ? – hek2mgl Mar 26 '13 at 20:10
  • I just added the dbug write to file there. – Valery Patskevich Mar 26 '13 at 20:12
  • 1
    let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/26970/discussion-between-hek2mgl-and-valery-patskevich) – hek2mgl Mar 26 '13 at 20:15
  • 1
    I have tested with php 5.4.4-14 It works, meaning that write will being called – hek2mgl Mar 26 '13 at 23:26
  • I have made tests using SessionHandlerInterface (new in v.5.4). The open() read() and close() are called. write() is not called. And it do work on other servers. It seems to be some php build issue. I have sent the question to shared hosting support about the php build. Thank you . – Valery Patskevich Mar 27 '13 at 12:24
  • There's too much stuff in this code. You should boil this down to the shortest script that tests the functionality. Open session, write var. In your custom close, echo something. – Steve Clay Aug 01 '13 at 16:34
  • Thanks for `$salt` `n pepper! – djot Sep 24 '13 at 23:23
  • Maybe this will help you - Once when I was struggling with my own custom session handlers I noticed that if OOP was used - open/close/read/write were implemented as class methods - I was not able to make this work. Without OOP worked perfectly. Try using functions without classes - maybe you are struggling with the same stuff right now. – Artur Sep 26 '13 at 16:45

1 Answers1

0

It seems your session object destroyed by PHP internal garbage collector before the shutdown function call. In this case you need to move session_write_close() function into destructor:

function __destruct() {
  session_write_close();
}

I recommend you to rewrite your code according to new session handling mechanism. Implement SessionHandlerInterface to aviod such behaviour in future.

clover
  • 4,910
  • 1
  • 18
  • 26