fastcgi++
Sessions

Tutorial

Our goal here will be a FastCGI application that has very simple session capabilities. We won't authenticate the creation of sessions, and the only data we will store in them is a user supplied text string. Be warned that the facilities that fastcgi++ supplies for handling sessions are quite low level to allow for maximum customizability and no overhead costs if they aren't used.

All code and data is located in the examples directory of the tarball. You'll have to compile with: pkg-config –libs –cflags fastcgi++

Error Logging

Our first step will be setting up an error logging system. Although requests can log errors directly to the HTTP server error log, I like to have an error logging system that's separate from the library when testing applications. Let's set up a function that takes a c style string and logs it to a file with a timestamp. Since everyone has access to the /tmp directory, I set it up to send error messages to /tmp/errlog. You can change it if you want to.

#include <fstream>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <string>
void error_log(const char* msg)
{
using namespace std;
using namespace boost;
static ofstream error;
if(!error.is_open())
{
error.open("/tmp/errlog", ios_base::out | ios_base::app);
error.imbue(locale(error.getloc(), new posix_time::time_facet()));
}
error << '[' << posix_time::second_clock::local_time() << "] " << msg << endl;
}

Request Handler

Now we need to write the code that actually handles the request. Quite simply, all we need to do is derive from Fastcgipp::Request and define the Fastcgipp::Request::response() function. For this example we'll just use ISO-8859-1 as the character set so we'll pass char as the template parameter to the Request class as opposed to wchar_t.

class SessionExample: public Fastcgipp::Request<char>
{

The first thing we are going to do here is set up a typedef to make accessing our sessions container easier. Sessions are contained in the Fastcgipp::Http::Sessions container which in almost all ways is just an std::map. The template parameter that the class takes is any data type you want to associate with the session. Typically someone might associate a struct that contains user ids and access level with each sessions. In this example we will just use a string. The passed template type ends up as the second type in the std::pair<> of the std::map. The first type will always be a type Fastcgipp::Http::SessionId which is a class for managing session id with their expiries effectively.

Now let's define our actual session data in the class. We need to declare two items: a static Sessions container and a non-static iterator for the container. The iterator will obviously point to the session data pair associate with this particular request.

static Sessions sessions;
Sessions::iterator session;

Now we can define our response function. It is this function that is called to generate a response for the client. It isn't a good idea to define the response() function inline as it is called from numerous spots, but for the examples readability we will make an exception.

bool response()
{
using namespace Fastcgipp;

First off let's clean up any expired sessions. Calling this function at the beginning of every request actually doesn't create all that much overhead as is explained later on.

sessions.cleanup();

Now we need to initialize the session data member. In this example we will do this by finding a session id passed to us from the client as a cookie. You could also use get data instead of cookies obviously. Let's call our session cookie SESSIONID. Remember that our cookies/GET data is stored in a Fastcgipp::Http::Environment object called environment().

session=sessions.find(environment().findCookie("SESSIONID").data());

Now the session data member will be a valid iterator to a session or it will point to end() if the client didn't provide a session id or it was invalid.

In order to get login/logout commands from the client we will use GET information. A simple ?command=login/logout system will suffice.

std::string command=environment().gets["command"];

Now we're going to need to set our locale to output boost::date_time stuff in a proper cookie expiry way. See Fastcgipp::Request::setloc().

setloc(std::locale(getloc(), new boost::posix_time::time_facet("%a, %d-%b-%Y %H:%M:%S GMT")));

Now we do what needs to be done if we received valid session data.

if(session!=sessions.end())
{
if(command=="logout")
{

If we received a logout command then we need to delete the cookie, remove the session data from the container, reset our session iterator, and then send the response.

out << "Set-Cookie: SESSIONID=deleted; expires=Thu, 01-Jan-1970 00:00:00 GMT;\n";
sessions.erase(session);
session=sessions.end();
handleNoSession();
}
else
{

Otherwise, we just call Fastcgipp::Http::SessionId::refresh() on our session ID to update the expiry time, update it on the client side and then do the response.

session->first.refresh();
out << "Set-Cookie: SESSIONID=" << encoding(URL) << session->first << encoding(NONE) << "; expires=" << sessions.getExpiry(session) << '\n';
handleSession();
}
}

With no valid session id from the client it is even easier.

else
{
if(command=="login")
{

Since the client wants to make a new session, we call Fastcgipp::Http::Sessions::generate(). This function will generate a new random session id and associated the passed session data with it. Calling a stream insertion operation on Fastcgipp::Http::SessionId will output a base64 encoding of the id value. Perfect for http communications. To coordinate, constructing the class from a char* / wchar_t* will assume the string is base64 encoded and decode it accordingly.

session=sessions.generate(environment().posts["data"].value);
out << "Set-Cookie: SESSIONID=" << encoding(URL) << session->first << encoding(NONE) << "; expires=" << sessions.getExpiry(session) << '\n';
handleSession();
}
else
handleNoSession();
}

Now we can output some basic stuff that will be sent regardless of whether we are in a session or not.

out << "<p>There are " << sessions.size() << " sessions open</p>\n\
</body>\n\
</html>";

And we're basically done defining our response! All we need to do is return a boolean value. Always return true if you are done. This will let apache and the manager know we are done so they can destroy the request and free it's resources. Return false if you are not finished but want to relinquish control and allow other requests to operate. You would do this if the request needed to wait for a message to be passed back to it through the task manager.

return true;
}

Now we can define the functions that generate a response whether in session or not.

void handleSession()
{
using namespace Fastcgipp;
out << "\
Content-Type: text/html; charset=ISO-8859-1\r\n\r\n\
<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Strict//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'>\n\
<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'>\n\
<head>\n\
<meta http-equiv='Content-Type' content='text/html; charset=ISO-8859-1' />\n\
<title>fastcgi++: Session Handling example</title>\n\
</head>\n\
<body>\n\
<p>We are currently in a session. The session id is " << session->first << " and the session data is \"" << encoding(HTML) << session->second << encoding(NONE) << "\". It will expire at " << sessions.getExpiry(session) << ".</p>\n\
<p>Click <a href='?command=logout'>here</a> to logout</p>\n";
}
void handleNoSession()
{
out << "\
Content-Type: text/html; charset=ISO-8859-1\r\n\r\n\
<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Strict//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'>\n\
<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'>\n\
<head>\n\
<meta http-equiv='Content-Type' content='text/html; charset=ISO-8859-1' />\n\
<title>fastcgi++: Session Handling example</title>\n\
</head>\n\
<body>\n\
<p>We are currently not in a session.</p>\n\
<form action='?command=login' method='post' enctype='multipart/form-data' accept-charset='ISO-8859-1'>\n\
<div>\n\
Text: <input type='text' name='data' value='Hola señor, usted me almacenó en una sesión' /> \n\
<input type='submit' name='submit' value='submit' />\n\
</div>\n\
</form>\n";
}
};

Now let's initialize the static sessions object. The constructor takes to parameters. The first defines how many seconds session live for without an refresh. The second defines the minimum number of seconds that have to pass between cleanups of old sessions. This means that if the value is 30 two calls to Fastcgipp::Sessions::cleanup() in a 30 second period will only actually preform the cleanup once. We'll go 30 seconds for each in this example.

Session::Sessions Session::sessions(30, 30);

Requests Manager

Now we need to make our main() function. Really all one needs to do is create a Fastcgipp::Manager object with the new class we made as a template parameter, then call it's handler. Let's go one step further though and set up a try/catch loop in case we get any exceptions and log them with our error_log function.

int main()
{
try
{
fcgi.handler();
}
catch(std::exception& e)
{
error_log(e.what());
}
}

And that's it! About as simple as it gets.

Full Source Code

#include <fstream>
#include <string>
void error_log(const char* msg)
{
using namespace std;
using namespace boost;
static ofstream error;
if(!error.is_open())
{
error.open("/tmp/errlog", ios_base::out | ios_base::app);
error.imbue(locale(error.getloc(), new posix_time::time_facet()));
}
error << '[' << posix_time::second_clock::local_time() << "] " << msg << endl;
}
class Session: public Fastcgipp::Request<char>
{
bool response()
{
using namespace Fastcgipp;
sessions.cleanup();
session=sessions.find(environment().findCookie("SESSIONID").data());
const std::string& command=environment().findGet("command");
setloc(std::locale(getloc(), new boost::posix_time::time_facet("%a, %d-%b-%Y %H:%M:%S GMT")));
if(session!=sessions.end())
{
if(command=="logout")
{
out << "Set-Cookie: SESSIONID=deleted; expires=Thu, 01-Jan-1970 00:00:00 GMT;\n";
sessions.erase(session);
session=sessions.end();
handleNoSession();
}
else
{
session->first.refresh();
out << "Set-Cookie: SESSIONID=" << encoding(URL) << session->first << encoding(NONE) << "; expires=" << sessions.getExpiry(session) << '\n';
handleSession();
}
}
else
{
if(command=="login")
{
session=sessions.generate(environment().findPost("data").value);
out << "Set-Cookie: SESSIONID=" << encoding(URL) << session->first << encoding(NONE) << "; expires=" << sessions.getExpiry(session) << '\n';
handleSession();
}
else
handleNoSession();
}
out << "<p>There are " << sessions.size() << " sessions open</p>\n\
</body>\n\
</html>";
return true;
}
void handleSession()
{
using namespace Fastcgipp;
out << "\
Content-Type: text/html; charset=ISO-8859-1\r\n\r\n\
<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Strict//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'>\n\
<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'>\n\
<head>\n\
<meta http-equiv='Content-Type' content='text/html; charset=ISO-8859-1' />\n\
<title>fastcgi++: Session Handling example</title>\n\
</head>\n\
<body>\n\
<p>We are currently in a session. The session id is " << session->first << " and the session data is \"" << encoding(HTML) << session->second << encoding(NONE) << "\". It will expire at " << sessions.getExpiry(session) << ".</p>\n\
<p>Click <a href='?command=logout'>here</a> to logout</p>\n";
}
void handleNoSession()
{
using namespace Fastcgipp;
out << "\
Content-Type: text/html; charset=ISO-8859-1\r\n\r\n\
<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Strict//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'>\n\
<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'>\n\
<head>\n\
<meta http-equiv='Content-Type' content='text/html; charset=ISO-8859-1' />\n\
<title>fastcgi++: Session Handling example</title>\n\
</head>\n\
<body>\n\
<p>We are currently not in a session.</p>\n\
<form action='?command=login' method='post' enctype='application/x-www-form-urlencoded' accept-charset='ISO-8859-1'>\n\
<div>\n\
Text: <input type='text' name='data' value='Hola señor, usted me almacenó en una sesión' /> \n\
<input type='submit' name='submit' value='submit' />\n\
</div>\n\
</form>\n";
}
static Sessions sessions;
Sessions::iterator session;
};
Session::Sessions Session::sessions(30, 30);
int main()
{
try
{
fcgi.handler();
}
catch(std::exception& e)
{
error_log(e.what());
}
}