Building Browsergames: The Login Page (Perl)

Yesterday, we talked about how to build a login page in php. Today, we’re going to walk through building the same thing – but in Perl. This writeup will assume that you didn’t follow any of the instructions in the PHP entry, and simple start from where our Perl left off.

To start, we are going to modify our users table from earlier, and add two more columns. These columns will allow us to track which users are administrators, along with what time a user last logged in at:

ALTER TABLE `users` ADD `is_admin` tinyint(1) NOT NULL DEFAULT '0';
ALTER TABLE `users` ADD `last_login` timestamp NULL;

After running that SQL code, if you refresh the view of your table structure you will see there are now two columns. We have created is_admin as a tinyint because we will be using it as a flag for whether users are administators or not – so it only needs to be settable to 1 and 0(true and false in boolean terms). last_login is a timestamp, which we will update whenever a user logs in.

Let’s move on to the actual code. First off, we set up our starting CGI page:

 

#!/usr/bin/perl -w
 
use strict;
use CGI qw(:cgi);
use CGI::Carp qw(fatalsToBrowser warningsToBrowser);
use DBI;
 
use config;
my $query = new CGI;
my $html;
 
$html .= qq ~
<form action='login.cgi' method='post'>
Username: <input type='text' name='username' /><br />
Password: <input type='password' name='password' /><br />
<input type='submit' value='Login' />
</form>
~;
 
print $query->header();
print $html;

While that code looks daunting, it really isn’t – it’s most of the basis for our login page. We begin by useing all of the modules we will need – strict, CGI, CGI::Carp for debugging, and DBI so that we can interact with our database. We then also use our configuration file from earlier. After that, we just set our HTML output, and print it out. Not much going on just yet.

All we’ve done so far is create a really basic login form. But now that we have that in place to build off of, we can start to add logic to the page. One of the first things we will add is a check to make sure arguments are being sent to the page before we try to do anything with those arguments:

my %arguments = $query->Vars;
if(%arguments) {
# do stuff here
}

$html .= qq ~[/php]
That small conditional will make sure that we don’t try to do anything without having parameters actually passed to our script.

While the whole login/register systems seems complicated, it’s actually very simple. This is the pseudocode for a login system:

if(username/password combination matches on in the database) {
log user in
} else {
login error
}
In our case, we will also be updating the user’s last_login attribute after they log in successfully. Let’s add the check to see if their data matches any in the database:

my $dbh = DBI->connect("DBI:mysql:$dbname:$dbhost",$dbuser,$dbpass,{RaiseError => 1});
	my $sth = $dbh->prepare("SELECT COUNT(id) FROM users WHERE UPPER(username) = UPPER(?) AND password = ?");
	my $count;
	$sth->execute($arguments{username},crypt($arguments{password},$arguments{username}));
	$sth->bind_columns(\$count);
	$sth->fetch;
	if($count == 1) {
		$html .= qq ~
<span style='color:green'>Login Successful.</span>		
		~;
	} else {
		$html .= qq ~
<span style='color:red'>Error: that username and password combination does not match any currently in our database.</span>		
		~;
	}
}

What did we do there? First, we connected to the database using the values from our configuration file. Then we ran a quick select query, to see if any users existed with the same username and password. The main ‘question’ a login page asks is “does the information supplied match any in the database?”, and if the answer is yes it logs the user in. We make sure to run the password through crypt(), so that it will actually match a value in our database that was inserted by our registration page from earlier. Then, we retrieve the number of users that matched into $count, and use a conditional statement to determine whether or not the login was successful. If it was, we print out a little green message. And if it wasn’t, we show them a (hopefully) helpful error message.

While this does technically go through all the motions of a regular login page, it doesn’t do the one we want it to most yet – that is, writing a cookie, updating the user’s last_login, and finally redirecting the user.

The first thing we will do is update the user’s last_login. Here’s how:

$sth = $dbh->prepare("UPDATE users SET last_login = NOW() WHERE UPPER(username) = UPPER(?) AND password = ?");
		$sth->execute($arguments{username},crypt($arguments{password},$arguments{username}));
		$html .= qq ~

All we do is create an UPDATE query, and then execute it with the same parameters as before. Pretty simple stuff.

The next thing we will do is retrieve whether or not the user is an administrator – so that we can redirect them to one page or the other based on that:

 

$sth = $dbh->prepare("SELECT is_admin FROM users WHERE UPPER(username) = UPPER(?) AND password = ?");
		my $is_admin;
		$sth->execute($arguments{username},crypt($arguments{password},$arguments{username}));
		$sth->bind_columns(\$is_admin);
		$sth->fetch;
		if($is_admin == 1) {
			# redirect to admin page
		}
		# redirect to normal page
		$html .= qq ~

The above code will just pull out whether the user is an administrator or not – so that we can send them to a different page based on that.

In Perl, we have to do a little more of the ‘dirty work’ of dealing with cookies than we do with PHP. We have to write out our cookie ourselves, along with sending the proper headers. In order to do that, we need to create a new cookie, and then print it out in a call to $query->header(). No matter whether the user is an administrator or not, the cookie will be exactly the same:

 

my $cookie = $query->cookie(
				-name	=>	'username+password',
				-value	=>	$arguments{username} . '+' . crypt($arguments{password},$arguments{username}),
				-expires	=>	'+3M',
			);
		my $uri = 'index.cgi';
		if($is_admin == 1) {
			# redirect to normal page
			$uri = 'admin.cgi';
		}
		print $query->header(-cookie=>$cookie,-location=>$uri);
	} else {
~;
 
print $html;

At line 28, we create a new cookie using $query. Then, we set up a name and a value for it – along with an expiry date of 3 months in the future. Right now, we’re storing the username and password of our user in the form of ‘username+password’ – but really, you could store it in any format you please(or even two cookies) as long as you can parse it back out into its separate parts.

We store the URL we’ll be redirecting the user to inside $uri – by default, it’s index.cgi. Administrators will be sent to admin.cgi.

After setting up these two pieces of data, we print a header using $query, that has both our cookie and the new location inside of it. This header will redirect the user to the new page.

And at this point, we’re finished! We now have a working login page that will redirect a user based on whether they’re an administrator or not, track the user’s most recent login, and write a cookie with their login data – so that we can check if they are logged in in other areas of our game. Here’s all of the code we wrote:

 

#!/usr/bin/perl -w
 
use strict;
use CGI qw(:cgi);
use CGI::Carp qw(fatalsToBrowser warningsToBrowser);
use DBI;
 
use config;
my $query = new CGI;
my $html;
 
my %arguments = $query->Vars;
if(%arguments) {
	my $dbh = DBI->connect("DBI:mysql:$dbname:$dbhost",$dbuser,$dbpass,{RaiseError => 1});
	my $sth = $dbh->prepare("SELECT COUNT(id) FROM users WHERE UPPER(username) = UPPER(?) AND password = ?");
	my $count;
	$sth->execute($arguments{username},crypt($arguments{password},$arguments{username}));
	$sth->bind_columns(\$count);
	$sth->fetch;
	if($count == 1) {
		$sth = $dbh->prepare("UPDATE users SET last_login = NOW() WHERE UPPER(username) = UPPER(?) AND password = ?");
		$sth->execute($arguments{username},crypt($arguments{password},$arguments{username}));
		$sth = $dbh->prepare("SELECT is_admin FROM users WHERE UPPER(username) = UPPER(?) AND password = ?");
		my $is_admin;
		$sth->execute($arguments{username},crypt($arguments{password},$arguments{username}));
		$sth->bind_columns(\$is_admin);
		$sth->fetch;
		my $cookie = $query->cookie(
				-name	=>	'username+password',
				-value	=>	$arguments{username} . '+' . crypt($arguments{password},$arguments{username}),
				-expires	=>	'+3M',
			);
		my $uri = 'index.cgi';
		if($is_admin == 1) {
			# redirect to admin page
			$uri = 'admin.cgi';
		}
		print $query->header(-cookie=>$cookie,-location=>$uri);
	} else {
		$html .= qq ~
<span style='color:red'>Error: that username and password combination does not match any currently in our database.</span>		
		~;
	}
}
 
$html .= qq ~
<form action='login.cgi' method='post'>
Username: <input type='text' name='username' /><br />
Password: <input type='password' name='password' /><br />
<input type='submit' value='Login' />
</form>
~;
print $query->header();
print $html;