chrisShick
chrisShick

Reputation: 1096

CakePhp 3.x Sharing session with nodejs using redis

Alright, I have been following this example of sharing a session between php and nodejs using redis : https://gist.github.com/mscdex/9507b0d8df42e0aec825

I got that working beautifully, but now I am trying to see how I would go about it with CakePhp. I have a few questions about how I would go about it:

  1. Should I just create a new session handler and inside the constructor run the session configurations that I need to do before the session start??

  2. Should I just create a new Session.php class extended from the one provided by CakePhp? If I do this, how would I get the application to use it?

Code linked above:

var express = require('express'),
    app = express(),
    cookieParser = require('cookie-parser'),
    session = require('express-session'),
    RedisStore = require('connect-redis')(session);

app.use(express.static(__dirname + '/public'));
app.use(function(req, res, next) {
  if (~req.url.indexOf('favicon'))
    return res.send(404);
  next();
});
app.use(cookieParser());
app.use(session({
  store: new RedisStore({
    // this is the default prefix used by redis-session-php
    prefix: 'session:php:'
  }),
  // use the default PHP session cookie name
  name: 'PHPSESSID',
  secret: 'node.js rules',
  resave: false,
  saveUninitialized: false
}));
app.use(function(req, res, next) {
  req.session.nodejs = 'Hello from node.js!';
  res.send('<pre>' + JSON.stringify(req.session, null, '    ') + '</pre>');
});

app.listen(8080);
<?php
// this must match the express-session `secret` in your Express app
define('EXPRESS_SECRET', 'node.js rules');

// this id mutator function helps ensure we look up
// the session using the right id
define('REDIS_SESSION_ID_MUTATOR', 'express_mutator');
function express_mutator($id) {
  if (substr($id, 0, 2) === "s:")
    $id = substr($id, 2);
  $dot_pos = strpos($id, ".");
  if ($dot_pos !== false) {
    $hmac_in = substr($id, $dot_pos + 1);
    $id = substr($id, 0, $dot_pos);
  }
  return $id;
}

// check for existing express-session cookie ...
$sess_name = session_name();
if (isset($_COOKIE[$sess_name])) {
  // here we have to manipulate the cookie data in order for
  // the lookup in redis to work correctly

  // since express-session forces signed cookies now, we have
  // to deal with that here ...
  if (substr($_COOKIE[$sess_name], 0, 2) === "s:")
    $_COOKIE[$sess_name] = substr($_COOKIE[$sess_name], 2);
  $dot_pos = strpos($_COOKIE[$sess_name], ".");
  if ($dot_pos !== false) {
    $hmac_in = substr($_COOKIE[$sess_name], $dot_pos + 1);
    $_COOKIE[$sess_name] = substr($_COOKIE[$sess_name], 0, $dot_pos);

    // https://github.com/tj/node-cookie-signature/blob/0aa4ec2fffa29753efe7661ef9fe7f8e5f0f4843/index.js#L20-L23
    $hmac_calc = str_replace("=", "", base64_encode(hash_hmac('sha256', $_COOKIE[$sess_name], EXPRESS_SECRET, true)));
    if ($hmac_calc !== $hmac_in) {
      // the cookie data has been tampered with, you can decide
      // how you want to handle this. for this example we will
      // just ignore the cookie and generate a new session ...
      unset($_COOKIE[$sess_name]);
    }
  }
} else {
  // let PHP generate us a new id
  session_regenerate_id();
  $sess_id = session_id();
  $hmac = str_replace("=", "", base64_encode(hash_hmac('sha256', $sess_id, EXPRESS_SECRET, true)));
  // format it according to the express-session signed cookie format
  session_id("s:$sess_id.$hmac");
}
// https://github.com/TheDeveloper/redis-session-php
require('redis-session-php/redis-session.php');
RedisSession::start();

$_SESSION["php"] = "Hello from PHP";
if (!isset($_SESSION["cookie"]))
  $_SESSION["cookie"] = array();

echo "<pre>";
echo json_encode($_COOKIE, JSON_PRETTY_PRINT);
echo json_encode($_SESSION, JSON_PRETTY_PRINT);
echo "</pre>";

?>

Upvotes: 0

Views: 529

Answers (1)

ndm
ndm

Reputation: 60483

I'm not very familiar with Redis or Node, but from looking at the code of the RedisSession class (https://github.com/TheDeveloper/redis-session-php), I'd say you'll have to go with a custom session handler. Whether the session handler should fumble with cookies is highly debatable, I'd probably put that somewhere else in the bootstrapping process, maybe as a dispatcher filter.

However if you need the session ID to be in a specific format, then you'll also have to make use of a custom session class, at least unless you want to/can make use of the undocumented session id genereation handler stuff that was introduced with PHP 5.5.1.

Creating an extended session class that handles this is fairly easy, just overwrite the start() and renew() method and do whatever you need to do with the ID.

Injecting the new session class into the application is pretty easy, as throughout the framework the session is being retrieved from the request (\Cake\Network\Request::session()). However getting your custom class into the request is a little ugly, as there is no clean way to hook this into the process of creating a request from globals. In any case you'll have to modify your front controller (webroot/index.php) so that the proper(ly configured) request class is being passed to the dispatcher.

You can either

  • create a custom request class with for example an overwritten Request::createFromGlobals() method where you'll instantiate your custom session class and pass it to the config

  • instantiate a new request class manually where you could pass the session object to use using the session config key (this will require you to figure the base and webroot options yourself)

  • or overwrite the already assigned/constructed session class with your custom one using the Request::session() method.

See also

In this example I'll go with a custom request class, simply to avoid the additional session class instantiation.

src/Network/MyCustomSession.php

namespace App\Network;

use Cake\Network\Session;

class MyCustomSession extends Session
{
    public function start()
    {
        parent::start();
        $this->_processSessionId();
    }

    public function renew()
    {
        parent::renew();
        $this->_processSessionId();
    }

    protected function _processSessionId()
    {
        $id = $this->id();

        // To make this less handler specific, you could for example
        // use a configurable callback instead, or maybe even an event,
        // in the end this is just example code.

        if($id && substr($id, 0, 2) !== 's:') {
            $hmac = str_replace(
                "=", "", base64_encode(hash_hmac('sha256', $id, \EXPRESS_SECRET, true))
            );
            $this->id("s:$id.$hmac");
        }
    }
}

src/Network/MyCustomRequest.php

namespace App\Network;

use Cake\Network\Request;

class MyCustomRequest extends Request
{
    public static function createFromGlobals()
    {
        list($base, $webroot) = static::_base();
        $sessionConfig = (array)Configure::read('Session') + [
            'defaults' => 'php',
            'cookiePath' => $webroot
        ];
        $config = [
            'query' => $_GET,
            'post' => $_POST,
            'files' => $_FILES,
            'cookies' => $_COOKIE,
            'environment' => $_SERVER + $_ENV,
            'base' => $base,
            'webroot' => $webroot,

             // here comes the custom session
            'session' => MyCustomSession::create($sessionConfig)
        ];
        $config['url'] = static::_url($config);
        return new static($config);
    }
}

src/webroot/index.php

use App\Network\MyCustomRequest;

$dispatcher = DispatcherFactory::create();
$dispatcher->dispatch(
    MyCustomRequest::createFromGlobals(), // there goes the custom request
    new Response()
);

Upvotes: 1

Related Questions