perlmeditation
jryan
<p>A very common web technique nowadays is the use of sessions. Most programming languages have support for sessions; however, what happens when you need more than one language able to access the same session? Well, it gets messy. At my most recent place of employment, we ran into the problem where we needed php and perl sessions to communicate with each other. My co-worker (a certain Steve Stetak, an awesome programmer who unfortunately isn't a perlmonk) and I eventually came up with a solution using Apache::Session::MySQL and some hacking of php's serialization.</p>
<readmore>
<h3>How do I use it?</h3>
<h4>1. What are sessions?</h4>
<p>Sessions are a method to hold state variables between different scripts or different instances of a script. For example, when I log into Hotmail. I give the first page my username and password. These values are then stored internally by the server, and accessed everytime I go to a new page. Only when I hit the logout link are these values destroyed.</p>
<h4>2. How do they work?</h4>
<p>When you first start a session, the script first creates a cookie on the client browser with the session ID. The session ID is a unique 32 character string that identifies each session. You scripts then register "session variables" which are stored in a MySQL database. The variables are then accessable as long as you have the session ID.
</p>
<h4>3. What are some limitations of our system?</h4>
<p>Due to the amount of PHP and Perl scripts on the server that would need to use sessions, we must come up with a universal way to access sessions in both languages. Unfortunately, this eliminates some of the functionality of sessions. For example, the only session variable that can be used is the PHP associative array "$session" and the Perl hash "%session"
</p>
<h4>4. How do I use sessions with PHP with our system?</h4>
To begin a session with PHP, you must first require the session handler file at the beginning of all the scripts that use sessions.
<code>
require "Sessions/mysql_session_handler.php"; // more on this later
</code>
<p>
Then, to begin the session, call the function
</p>
<code>
session_start();
</code>
<p>
This function first checks to see if there is a cookie defined with the session ID. If there is, it uses that ID to load the session variable. If there is no cookie, it creates a new, random session ID and places it in a cookie. It then creates an empty session.
</p>
<p>
If there is no session defined already, then you must register the session variable. To do this, you must first define the associative array, and then register it to the session.
</p>
<code>
$session = array();
session_register("session");
</code>
<p>
You can now use the $session variable throughout your script. All members of the $session array will be available for all the scripts using the same session.
</p>
<p>
When you are finished with the session (i.e. when the user logs out) you must call the session_destroy() function do destroy the session and remove the cookie.
</p>
<h4>5. How do I use sessions with Perl while using the system?</h4>
<p>
Unfortunately, using sessions with Perl is slightly more complicated. First, you must include the following lines at the beginning of your script:
</p>
<code>
use CGI;
use Apache::Session::MySQL;
</code>
<p>
The CGI modules is used to access the cookie, because, unlike PHP, Perl sessions do not handle cookies for you, while the Apache modules are used to access the MySQL database and store the session data there.
</p>
<p>
Next, you must initialize the CGI instance and get the cookie ID.
</p>
<code>
my $q = new CGI;
my $sess_id = $q->cookie(-name=>'sess_id');
</code><p>
Now, you can begin you session, by tieing the %session hash to the MySQL database. If $sess_id is undef then it will create a new session, with a new session ID.
</p>
<code>
tie my %session, "Apache::Session::MySQL", $sess_id, {
DataSource => 'dbi:mysql:sessions',
UserName => 'root',
Password => '',
LockDataSource => 'dbi:mysql:sessions',
LockUserName => 'root',
LockPassword => ''
};
</code>
<p>
You can now save your session variables in the hash. Note that if you pass a session ID to the tie function that does not exist, for example, from an old cookie whose data has been deleted, the script will die. See the sample Perl script for a workaround to this problem.
</p>
<p>
Note also that the session ID is stored in $session{'_session_id'}
</p>
<p>
If you have created a new session, you must set a cookie.
</p>
<code>
my $cookie = $q->cookie(-name=>'sess_id',-value=>$session{'_session_id'});
print $q->header(-cookie=>$cookie);
</code>
<p>
When you are done with a session (i.e. the user logs out) you should destory the session. Do this with:
</p>
<code>
tied(%session)->delete;
</code>
<h4>6. How do I use sessions with PHP and Perl?</h4>
<p>Using the above method, your sessions should work with both PHP and Perl. One condition is that, after creating a session in PHP, you must initialize the session ID variable, using the line:</p>
<code>
$session['_session_id'] = session_id();
</code>
<h4>7. What are some things to remember?</h4>
<ol>
<li>The cookie should always be named "sess_id" PHP handles this automatically, and the name is set in php.ini as "sess_id". However, in Perl the naming must be done manually.
</li>
<li>2. Only use a single level associative array or hash. Do not try to use multiple levels in PHP, such as <code>$session['username'][1]</code>, or use references in Perl, as in <code>$session{username}[2]</code>. Only store numerical and string data in session variables.
</li>
<li>3. Never register anything but "session" in PHP. Otherwise, the sessions will not function properly, and you will not be able to retrieve your session data.
</li>
<li>4. If you are writing a Perl script that accesses MySQL databases, you still must use DBI; Even though Apache::Session::MySQL uses DBI, it does not include it in your script. However, you do not need to use DBI for the Apache's module's sake alone.
</li>
</ol>
<h3>How does it work?</h3>
<p>
Perl and PHP each use a different method to serialize the data. Serialization allows the languages to "flatten" the data. Basically, it takes all the variables you want to store and puts the data necessary to recreate these variables into a string. Perl uses the functions Freeze and Thaw from the module Storable, while PHP uses its own, native serialization function. It is not possible to modify how the language serializes the session data. However, we can modify the way PHP stores the data and we can use Perl to emulate the PHP serialization functions.
</p>
<p>
So, no modifications have been made to the Perl side of the sessions. On the PHP side, however, we have to install custom storage handlers. The first step to doing this is to modify the etc/php.ini file, setting session.set_save_handler = user; This tells PHP that we will be using custom handlers. Unfortunately, it is not possible to use custom handlers in one script and PHP's default handlers in another. So, all PHP scripts that use sessions must use the same method.
</p><p>
Next, we have our custom handlers. The first is mysql_sessions_handler.php:
</p>
<code>
<?
function mysql_session_open ( $save_path, $session_name )
{
// Since all database editting is done in Perl scripts
// these functions do nothing...
return true;
}
function mysql_session_close ( )
{
return true;
}
function mysql_session_read ( $id )
{
return `perl /usr/share/php/Sessions/perl_get_serialized_session.pl $id`;
}
function mysql_session_write ( $id, $serialized )
{
return `perl /usr/share/php/Sessions/perl_save_serialized_session.pl $id '$serialized'`;
}
function mysql_session_destroy ( $id )
{
$db = mysql_connect("localhost", "root");
mysql_select_db('sessions', $db);
return mysql_query("DELETE FROM sessions WHERE id='$id'");
}
function mysql_session_gc ( $maxlife )
{
// Currently there is no good garbage collection
// Be sure to call "session_destroy()" in PHP scripts
return true;
}
session_set_save_handler (
'mysql_session_open',
'mysql_session_close',
'mysql_session_read',
'mysql_session_write',
'mysql_session_destroy',
'mysql_session_gc' );
?>
</code>
<p>
These functions define how the PHP scripts open, close, read, write, and destory the session. As you can see, the open and close functions are empty. Normally, we would open and close a connection to the MySQL database. However, all of that work is done in the Perl scripts.
</p>
<p>
Let's look at the read and write functions. Notice that they both call a perl script from the command line. The read function is passed a session ID and return serialized data. The Perl script below handles the data.
</p>
<code>
#!/usr/bin/perl -w
use strict;
use DBI;
use Serialize;
# NOTE!!!!
# The Serialize module is not found on CPAN
# it was found with a google search;
# you can download it here:
# http://furt.com/code/perl/serialize/
use Storable qw(freeze thaw);
my $id = $ARGV[0];
my $db = DBI->connect("DBI:mysql:sessions", "root", "" );
my $sel = $db->prepare("SELECT a_session FROM sessions WHERE id='$id'");
$sel->execute;
my @data = $sel->fetchrow_array;
if ( $data[0] )
{
my $ref = thaw($data[0]);
print "session|" . serialize($ref);
}
else
{
print "";
}
$sel->finish;
$db->disconnect;
</code>
<p>
The Perl script takes a single argument, the session ID. It then opens the MySQL database and returns the data. Remember, that the data in the MySQL database will always be "frozen", meaning serialized by the Perl functions. So, next the script will thaw the data. The script now has a Perl reference to the session data. It then uses the serialize function which emulates PHP's serialization function.. Notice also, that the script prepends "session|" This is due to differences in the freezing and serialization functions. So, this script outputs the serialized session data, and gives it to the custom handler, which feeds it back to PHP.
</p><p>
On to the write function. The write function gets passed a session ID and serialized data to store. Since the data is already serialized, there is another Perl script to unserialize, freeze, and store the data.
</p>
<code>
#!/usr/bin/perl -w
use strict;
use DBI;
use Serialize;
use Storable qw(freeze thaw);
my $id = $ARGV[0];
my $data = $ARGV[1];
if ( $data eq '' )
{
exit(0);
}
$data =~ s/^session\|//;
my $ref = unserialize($data);
$data = freeze($ref);
my $db = DBI->connect("DBI:mysql:sessions", "root", "" );
my $sel = $db->prepare("SELECT a_session FROM sessions WHERE id='$id'");
$sel->execute;
my @test = $sel->fetchrow_array;
if ( $test[0] )
{
$db->do( "UPDATE sessions SET a_session='$data' WHERE id='$id'" );
}
else
{
$db->do( "INSERT INTO sessions VALUES ( '$id', '$data' )" );
}
$sel->finish;
$db->disconnect;
print "1";
</code>
<p>
The first step is to remove "session|" from the beginning of the serialized data, so that it will freeze/thaw properly. The script then unserializes the data and freezes the reference, and then stores it in the MySQL database;
</p>
<h3>Sample PHP Script</h3>
<code>
<?
require "Sessions/mysql_session_handler.php"; // put in /usr/share/php
session_start();
if ( $action = "logout" )
{
// Someone hit the logout button. Destroy the session.
print "Session destroyed.<BR>";
session_destroy();
// The session associative array is still hanging around,
// however, it is no longer attached to the session,
// so we can just unset it.
unset ($session);
}
else if ( $username )
{
// The script was given a username, so let's create a session.
$session['username'] = $username;
// This line is to appease our Perl counterpart.
$session['_session_id'] = session_id();
session_register('session');
}
if ( $user = $session['username'] )
{
// There is a session defined already. Let's say hi.
print "Hello $user!<BR>";
print "<A HREF=\"test.php?action=logout\">Logout</A>";
}
else
{
// This is the first time we accessed the script.
// Print out a text box
print "<FORM METHOD=GET ACTION=\"test.php\">";
print "What is your username? <INPUT TYPE=TEXT NAME=\"username\">";
print "<INPUT TYPE=SUBMIT></FORM>";
}
?>
</code>
<h3>Sample Perl Script</h3>
<code>
!/usr/bin/perl -w
use strict;
use CGI;
use Apache::Session::MySQL;
use CGI::Carp qw(fatalsToBrowser);
my $q = new CGI;
my $sess_id = $q->cookie(-name=>'sess_id'); # substitute the name of your session cookie here
# These are the parameters for the session
my $params = { DataSource => 'dbi:mysql:sessions',
UserName => 'root',
Password => '',
LockDataSource => 'dbi:mysql:sessions',
LockUserName => 'root',
LockPassword => ''
};
my %session;
# The following lines tie %session to the session data
# The script will die if we give it a $sess_id that doesn't exist.
# So we put the tie call in an eval block. If there's an error
# in $@ then we create a new session.
eval {
tie (%session, 'Apache::Session::MySQL', $sess_id, $params);
};
tie (%session, 'Apache::Session::MySQL', undef, $params) if ( $@ );
if ( $q->param('action') )
{
# We should delete the session.
tied(%session)->delete;
# Even though the session is deleted, the hash is still hanging around.
# So we will just undef it, so that it doesn't confuse the script.
undef %session;
print $q->header;
}
elsif ( my $user = $q->param('username') )
{
# We just created a new session.
$session{'username'} = $user;
# Remember to create the cookie.
my $cookie = $q->cookie(-name=>'sess_id',
-value=>$session{_session_id});
print $q->header(-cookie=>$cookie);
}
else
{
print $q->header;
}
if ( $session{'username'} )
{
# The session already exists. Say hello.
print "Hello $session{'username'}!<BR>";
print "<A HREF=\"test.cgi?action=logout\">Logout</A>";
}
else
{
# Print login box
print "<FORM METHOD=GET ACTION=\"test.cgi\">";
print "What is your username? <INPUT TYPE=TEXT NAME=\"username\">";
print "<INPUT TYPE=SUBMIT></FORM>";
}
</code>