Beefy Boxes and Bandwidth Generously Provided by pair Networks
The stupid question is the question not asked
 
PerlMonks  

RFC: Proposed tutorial - simple login script using CGI::Application

by scorpio17 (Monsignor)
on Jun 19, 2007 at 18:16 UTC ( #622071=perlmeditation: print w/ replies, xml ) Need Help??

A Beginners Guide to CGI::Application

CGI::Application is a framework for building web applications in Perl. I discovered it a few months ago while browsing perl.com (see Rapid Website Development with CGI::Application) and decided to give it a try on my next project.

There are a large number of plugins available, which makes it easy to do almost anything you wish. I was able to quickly find plugins for session management, database access, authentication, etc. However, the sheer volume of options quickly became confusing. I had trouble figuring out how to best put all the seperate parts together into one big (working) piece.

Of course, there are examples out there ( see the CGI::Application wiki) - but I couldn't find anything exactly like what I wanted to do. Furthermore, I wanted to use SSL for secure authentication, and couldn't find any info on that documented anywhere. So, after much time and effort, I'm presenting this small, working example in order to help anyone else that might come along and find themselves in the same situation.

I don't consider myself an expert, and I don't claim that this is the only way to do it, nor the best way to do it. But it does work, and I think one could use this as a starting point upon which to build. After reading all the necessary module documentation on CPAN, you should be able to modify this as necessary to suit your own needs.

Here's a list of the modules that I use in this tutorial:

The finished example demonstrates a simple CGI login page. Authentication is performed against a MySQL database. The session ID is stored as a cookie on the client side (browser). There are public pages anyone can see, and private pages one must login to see. The login form uses SSL to provide greater security.

I'm going to assume you already have apache installed and configured properly. If you need help with this part, try looking here: apache docs. I'm also going to assume you have MySql installed and configured properly. If you need help with that, try looking here: mysql docs. I'm also going to assume that you've installed all the necessary perl modules on your system. If you need help with that, try looking here: Modules: How to Create, Install, and Use .

If you've made it this far - congratulations! Now let's get started.

Database Setup

The database used here contains two tables. The first table will be used to store user names and passwords. The second table will be used to store session data. Having sessions allows data to persist over time. For example, if you develop a shopping cart application, and someone places an item in the cart, the session will 'remember' the cart's contents. For this tutorial, we'll only be remembering if someone is logged in or not.

First, login to mysql as the root user:

mysql -u root -p

Then type the following commands at the mysql prompt:

create database webapp;
grant all on webapp.* to webadmin@'localhost';
flush privileges;
quit

Now login to mysql as user 'webadmin':

mysql -u webadmin webapp

Enter this to create our 'user_info' table:

create table user_info (
  username varchar(64) not null,
  password varchar(64) not null
);

Enter this to insert a user into the table:

insert into user_info (username, password) values (
  'username',
  '5f4dcc3b5aa765d61d8327deb882cf99'
);

Note that '5f4dcc3b5aa765d61d8327deb882cf99' is the MD5 hexhash of the word 'password'.

Next, create the table for holding session data:

create table sessions (
  id char(32) not null primary key,
  a_session text not null
);

That's it! You can quit mysql. It's time to write some perl scripts.

CGI-BIN Scripts

I'm going to assume that your cgi-bin directory is located at this path:

/var/www/cgi-bin

If for some reason yours is in a different location, that's okay, you'll just have to change the path appropriately in the instructions below.

First, I'm going to create a directory for my application, called WebApp. Inside this directory, I'll create a script called simple.pl. Just to make sure everything is working, put the following code inside simple.pl:

#!/usr/bin/perl print "content-type: text/html\n\n"; print "hello world\n";

Now, if you go to this URL:

http://localhost/cgi-bin/WebApp/simple.pl

You should see the message "hello world". If you do not, then don't read any further until you get that working. Check your apache config file and be sure to 'chmod 755' the WebApp directory and the simple.pl script.

Inside the WebApp directory, create a 'templates' subdirectory, and a 'libs' subdirectory. Next create a 'MyLib' directory inside of 'libs'.

Line-by-Line Discussion of Code Listings

The code listings to follow have each line numbered for easy reference. If you'd like to download these files and try running them yourself, use the following perl one-liner to remove the numbers from the start of each line:

perl -p -i.orig -e 's/^=\s+\d+\s+= ?//' simple.pl

In this particular case, 'simple.pl' will be edited in place (line numbers removed), and the original file backed up as 'simple.pl.orig'.


The file 'simple.pl' (Listing 1) is the application script. Line 3 tells perl where to look for my custom module files. Line 4 loads the module that actually defines the run modes (pages) for my application, and line 12 starts it running. Lines 7-10 set some instance-specific run parameters. In this case, we're defining 'simple.ini' to be our configuration file (Listing 2). It's basically just name = value pairs. Using these will help us avoid hard-coding anything in our scripts that may need to be changed. For now, all that's in here is info needed for establishing a connection to the database.

Normally, you would create a web app by inheriting from class CGI::Application. In this case, class MyLib::Simple actually inherits from MyLib::Login. MyLib::Login inherits from CGI::Application. This lets us put all of the login/logout related functions into a seperate module. Plus, if we later develop a second web app (MyLib::Simple2, for example) it can easily reuse all of the same login code. Imagine having to maintain 10 different web apps. Let's say you want to switch databases from MySql to Postgres. This approach allows you to modify one single Login.pm module, rather than 10 seperate app-specific modules. See? Code reuse is good!

Since the first thing Simple.pm does is load Login.pm (line 22), let's start with Login.pm (Listing 4). We'll come back to Listing 3 later.

Line 97 loads CGI::Application. Lines 99-104 load all of the plugin modules we want to use. Here's a quick overview of what each one does:

'AutoRunmode' will allow us to use shorter URLs to refer to our web pages. For example, instead of this:

http://localhost/cgi-bin/WebApp/simple.pl?rm=index

we can instead use this:

http://localhost/cgi-bin/WebApp/simple.pl/index

In other words, it will allow us to extract the desired run mode from the PATH_INFO environment variable. It also lets us define run mode methods using attributes, like this:

sub mypage : Runmode {

Otherwise, we'd have to use the runmodes() method to register each page we want to define. This would normally be done in the setup method, but since we're using AutoRunmode, our setup method (lines 107-114) is pretty short.

The DBH plugin gives us easy access to perl's DBI module (database interface).

The Session plugin is a wrapper around CGI::Session. This will help us maintain state from one page view to the next (in other words, it helps us provide persistent data).

The Authentication module provides methods for logging in and out. Let's say we have 50 people browsing our site at the same time, and each one has placed items in their shopping cart. Authentication allows us to identify individual users. Otherwise, there would be no way to determine which cart belongs to which user.

The ConfigAuto module helps us read parameters from our config file.

Line 105 loads the MD5 module, which we'll use later to encrypt passwords.

Lines 107-114 define our setup method. The most important thing here is line 111, which tells CGI::Application to parse the PATH_INFO environment variable. The AutoRunmode plugin needs this to work properly.

Line 116 begins our cgiapp_init method. As the name implies, this is where we do most of our 'initialization'. Line 119 uses the cfg method from the ConfigAuto plugin to read our config file and store all our name-value pairs into a hash called %CFG.

Line 121 tells CGI::Application where to look for template files (HTML::Template will need to know this later on).

Lines 124-128 initialize our database connection. The dbh_config method is defined by the DBH plugin. Rather than hard-code the necessary parameters, we're giving it the data we read from our config file. Note that this lets us avoid hard coding our mysql password inside our script. If we need to run our web app on several different servers, we might have a different database on each. In this case, we'd have a different config file for each server, but the perl code would be the same for all of them.

Lines 130-142 initialize the session configuration. There are lots of different ways to do this. You could, for example, choose to save session data to a file instead of to a database. Or you could choose to use Data::Dumper to serialize your data instead of Storable. Or, you could use Postgres instead of MySql. But in this case, I've decided to use MySql, so I specify the 'mysql' driver. Storable is a good serializer because it's fast and uses compression, but the result is not humanly readable like Data::Dumper. If you run mysql and type 'select * from sessions' you'll see lots of strange characters! So Data::Dumper might be better if you're trying to debug problems with your database, but the two formats are incompatible, so it's not easy to switch back and forth between the two (if you want to swap methods, I'd recommend dropping the 'sessions' table and recreating it each time).

In Line 133, self->query will return the current CGI object, and $self->dbh will return the current database handle (the one we just configured in lines 124-128).

Line 136 defines the default expiration time for our session. This means that once someone logs in, they will automatically be logged out after 1 hour of inactivity.

Each session created will be assigned a unique 32 character id code. This id will be written into a browser cookie. Lines 137-141 show how to configure cookie-related parameters. I've commented these out, because I prefer to use the defaults.

Lines 145-160 configure parameters for the Authentication plugin. In line 146, we specify the DBI driver because we're using a database. Line 147 gives the Authentication plugin a copy of our database handle. Line 148 gives it the name of the table to use for finding usernames and passwords. Lines 150 and 151 define which columns to use within that table. Line 151 also specifies that passwords will be encrypted using MD5. Line 155 says that we'll save our login state inside a session. If a user is not logged in, but tries to access a protected page, the Authentication plugin will automatically redirect the user to the login page. Once the user enters a valid username and passsword, they get redirected back to the protected page they originally requested. If a session expires, they get automatically logged out. For this to work properly, the Authentication plugin has to know which methods to call to perform the login/logout functions. These are defined in lines 156-157.

For added security, I wanted to use SSL to prevent a password entered on the login page from being transmitted over the internet as plain text. So I make sure the login page is accessed using https, rather than http (more on this later). But https is generally slower than http, so once logged in, I wanted to switch back to regular http. There's no easy way to do this! My work-around is to introduce a post-login run mode (line 158). This is a run mode that gets called after a user successfully logs in. What does this special run mode do? It basically figures out which run mode (page) the user really wanted to see, then redirects the browser to that page using http (not https).

Line 159 specifies a subroutine to use to generate a login form. Note that the Authentication plugin comes with a default form that you can use. I'm including this one just to demonstrate how to go about creating one of your own, in case you really want to. The default one actually looks much better than mine, so you might wish to comment out lines 157-159 in order to see it! The best solution is to write a custom login form of your own, that looks the way you want. The one I present here is intended only to demonstrate the basic functionality.

Lines 163-165 define which runmodes require a successful login. The Login.pm module doesn't define any content - all of the actual web pages are in Simple.pm. So I define the 'mustlogin' page here as a kind of place-holder. It's a dummy page that forces you to login, but immediately redirects you back to the default start page (usually the index page). The mustlogin runmode is defined in lines 174-178.

The teardown method (lines 169-172) is used to close the database connection.

The 'okay' method (lines 180-192) exists only to switch from https back to http. It assumes that the target run mode is stored in a cgi parameter named 'destination', but if for some reason this is not the case, it will default back to the index page.

The login method (lines 194-213) basically just displays the login form (line 211). But first, it checks to make sure you're not already logged in (lines 198-204), and second, it makes sure you're connecting with https. If you try to access the login page with http, it will automatically redirect you using https.

The login form is generated by my_login_form (lines 215-238). Actually, most of the form is pregenerated in the form of a template (see Listing 7). Line 217 loads this template, lines 234-236 insert values into the template parameters, and line 237 generates the final HTML. The 'destination' parameter is important, because it contains the URL of the page to go to once the user has successfully logged in. This gets tricky, because there are two ways to log in. On the index page, there's a link which says "click here to log in". If you click that link, and log in successfully, you'll be taken back to the index page. If you try to access a protected page before logging in, you'll automatically get redirected to the login page, but it will use the 'destination' parameter to remember where you were trying to go, and take you there once you do login.

So, my_login_form first tries to get a value for 'destination' from the CGI query object (in case it was passed as a hidden variable). If that fails, it tries looking at the PATH_INFO environment variable (in case it's being passed as part of the URL). If all else fails, it defaults to the index page. In the event the login attempt fails, you get redirected back to the login form, where it asks you to try again.

Adding SSL into this process gets tricky. The login method will ensure that the login page has a URL like this:

https://localhost/cgi-bin/WebApp/simple.pl/login

But for the username/password data to be encrypted, it's important that the page that this form gets submitted to also have an https in the URL. So if you look at line 341 (inside the login_form.html template) you'll see the "action" part of the form tag is set to point to the 'mustlogin' method. But once you DO successfully login, the Authentication plugin is going to run the post-login run mode 'okay', which redirects us back to the indended destination, minus the https URL.

Note that depending on which browser you're using, and what security settings you have enabled, you may see popups asking you to accept a security certificate, or that you're entering/leaving a secure area, etc. These are all normal and can be safely ignored.

Lines 240-247 define the logout method. Note that logging out actually deletes the current session (see line 244).

Lines 249-257 define the default error run mode: if any run mode fails to eval, CGI::Application will redirect you to this run mode. It gives you a nice way to trap errors in a sane way.

Lines 259-268 define the AUTOLOAD method. This will get called if you try to access a non-existant run mode. Having this in place gives a user a nice error message if they accidentally type in a URL wrong.

Now we're ready to return to Listing 3. Since MyLib::Simple inherits from MyLib::Login, it has access to all the methods we've just discussed, in addition to all the methods defined by CGI::Application.

Lines 24-35 define cgiapp_init. Line 27 calls the cgiapp_init method we saw before, in Login.pm. The "SUPER::" tag means "call this method from my parent class". We need to do this because we need the database connection, session config, etc. like before - but we also want to do some additional, application specific initialization. For example, Simple.pm defines two private run modes (pages that require a user to login in order to view). We specify those in lines 30-33. But without line 27, the cgiapp_init in Simple.pm would over-ride the one in Login.pm, and be run INSTEAD of that one, not IN ADDITION to that one, like we want.

So just remember, every web app we create can inherit from Login.pm to get the same login functionality, and each will have its own cgiapp_init to do "local initialization", but each will need to call SUPER::cgiapp_init to do Login.pm's "global initialization" stuff (database connect, session config, etc.). The alternative is to copy & paste the code each time, but then if you ever want to make a change, you'll have to change it in each copy. This way you can simply change it in one place and globally effect every application that depends on it (for example, if you decide you'd rather use SHA1 instead of MD5 - just change Login.pm).

Lines 37-46 define our index page. Notice that it has the attribute "StartRunmode" instead of "Runmode". That means it will be the page loaded if no other page is specified. So these three URLs are all equivalent:

http://localhost/cgi-bin/WebApp/simple.pl?rm=index
http://localhost/cgi-bin/WebApp/simple.pl/index
http://localhost/cgi-bin/WebApp/simple.pl

The first URL explicitly sets the runmode to 'index', the second URL sets the run mode to 'index' via the PATH_INFO environment variable, and the third URL defaults to 'index' as the start run mode because no other mode was specified.

So what does the index run mode do? It loads the index.html template (Listing 5, line 39), populates a few template parameters (lines 41-43) - note that you can define several values inside a single param statement - then returns the final HTML for rendering (line 45).

Note that we call $self->authen->username. If someone is logged in, this will return their username. Otherwise, it will return null. We can test the USER value inside the template to display different messages accordingly (lines 292-297). So a user that is NOT logged in will see a "click here to login" message, and a user that IS logged in will see a "click here to log out" message.

Finally, the index page displays links to other pages: two public, and two private. All of these pages use the same default.html template (Listing 6), but the parameters like NAME and MESSAGE vary for each one.

If you'd like to test the "error run mode", uncomment line 66 then try clicking the link for "public2". The "die" statement will generate an error message, and the error run mode will trap it.

To test the "autorunmode", try asking for a nonexistant page, like this:

http://localhost/cgi-bin/WebApp/simple.pl/bogus

You should be able to visit pages public and punlic2 without logging in. If you click on the link for private or private2, you'll get redirected to the login page (URL will switch to https), once you submit your username and password you'll get redirected to the private page (the URL will go back to http). If you click on "login" first, you'll be taken back to the index page. The, if you click on a link to a private page, it will take you there directly. If you do nothing for 1 hour, then try to again access a private page, it will ask you to login again (because the session will have expired).

Note that the templates contain a < META > tag with an expiration date several years old. This is a hack to trick the browser into NOT caching the page. Otherwise, you might still be able to see a private page after logging out, because the browser will pull it from the local page cache. If you force the page to reload, you should be prompted to login again.

Note that if this were a real application, the Login.pm module would include a "register" run mode to add new users, a "reset" method to let users change their password, a "forgot" method in case someone can't recall their password, etc.

I'll leave that as an exercise for the reader. The tricky part is logging in and out and getting the SSL to work, and hopefully I've been able to help with that.


update: corrected typo in perl one-liner. Also corrected typo regarding default login form.
update: corrected typo in 'templates' directory name.

Listing 1: WebApp/simple.pl

(go back)
= 1 = #!/usr/bin/perl = 2 = use strict; = 3 = use lib '/var/www/cgi-bin/WebApp/libs'; = 4 = use MyLib::Simple; = 5 = = 6 = my $webapp = MyLib::Simple->new( = 7 = PARAMS => { = 8 = cfg_file => ['simple.ini'], = 9 = format => 'equal', = 10 = }, = 11 = ); = 12 = $webapp->run(); = 13 =

Listing 2: WebApp/simple.ini

(go back)
= 14 = DB_DSN = dbi:mysql:database=webapp = 15 = DB_USER = webadmin = 16 = DB_PASS = = 17 =

Listing 3: WebApp/libs/MyLib/Simple.pm

(go back)
= 18 = package MyLib::Simple; = 19 = use strict; = 20 = = 21 = use lib '/var/www/cgi-bin/WebApp/libs'; = 22 = use base 'MyLib::Login'; = 23 = = 24 = sub cgiapp_init { = 25 = my $self = shift; = 26 = = 27 = $self->SUPER::cgiapp_init; = 28 = = 29 = # define runmodes (pages) that require successful login: = 30 = $self->authen->protected_runmodes( = 31 = 'private', = 32 = 'private2', = 33 = ); = 34 = = 35 = } = 36 = = 37 = sub index : StartRunmode { = 38 = my $self = shift; = 39 = my $template = $self->load_tmpl("index.html"); = 40 = $template->param({ = 41 = NAME => 'INDEX', = 42 = MYURL => $self->query->url(), = 43 = USER => $self->authen->username, = 44 = }); = 45 = return $template->output; = 46 = } = 47 = = 48 = sub public : Runmode { = 49 = my $self = shift; = 50 = my $template = $self->load_tmpl("default.html"); = 51 = my $msg = "You can see this without logging in.<br>"; = 52 = $template->param(MESSAGE => $msg); = 53 = $template->param(NAME => 'Public'); = 54 = $template->param(MYURL => $self->query->url); = 55 = return $template->output; = 56 = } = 57 = = 58 = sub public2 : Runmode { = 59 = my $self = shift; = 60 = my $template = $self->load_tmpl("default.html"); = 61 = my $msg = "You can see this without logging in.<br>"; = 62 = $template->param(MESSAGE => $msg); = 63 = $template->param(NAME => 'Public2'); = 64 = $template->param(MYURL => $self->query->url); = 65 = = 66 = ### die "made it here\n"; = 67 = = 68 = return $template->output; = 69 = } = 70 = = 71 = sub private : Runmode { = 72 = my $self = shift; = 73 = my $template = $self->load_tmpl("default.html"); = 74 = my $msg = "You must log in to see this."; = 75 = $template->param(MESSAGE => $msg); = 76 = $template->param(NAME => 'Private'); = 77 = $template->param(MYURL => $self->query->url); = 78 = return $template->output; = 79 = } = 80 = = 81 = sub private2 : Runmode { = 82 = my $self = shift; = 83 = my $template = $self->load_tmpl("default.html"); = 84 = my $msg = "You must log in to see this."; = 85 = $template->param(MESSAGE => $msg); = 86 = $template->param(NAME => 'Private2'); = 87 = $template->param(MYURL => $self->query->url); = 88 = return $template->output; = 89 = } = 90 = = 91 = = 92 = 1; = 93 =

Listing 4: WebApp/libs/MyLib/Login.pm

(go back)
= 94 = package MyLib::Login; = 95 = = 96 = use strict; = 97 = use base 'CGI::Application'; = 98 = = 99 = use CGI::Application::Plugin::AutoRunmode; = 100 = use CGI::Application::Plugin::DBH(qw/dbh_config dbh/); = 101 = use CGI::Application::Plugin::Session; = 102 = use CGI::Application::Plugin::Authentication; = 103 = use CGI::Application::Plugin::Redirect; = 104 = use CGI::Application::Plugin::ConfigAuto (qw/cfg/); = 105 = use Digest::MD5 qw(md5_hex); = 106 = = 107 = sub setup { = 108 = my $self = shift; = 109 = = 110 = $self->mode_param( = 111 = path_info => 1, = 112 = param => 'rm', = 113 = ); = 114 = } = 115 = = 116 = sub cgiapp_init { = 117 = my $self = shift; = 118 = = 119 = my %CFG = $self->cfg; = 120 = = 121 = $self->tmpl_path(['./templates']); = 122 = = 123 = # open database connection = 124 = $self->dbh_config( = 125 = $CFG{'DB_DSN'}, # "dbi:mysql:database=webapp", = 126 = $CFG{'DB_USER'}, # "webadmin", = 127 = $CFG{'DB_PASS'}, # "" = 128 = ); = 129 = = 130 = $self->session_config( = 131 = CGI_SESSION_OPTIONS => [ = 132 = "driver:mysql;serializer:Storable;id:md5", = 133 = $self->query, {Handle => $self->dbh}, = 134 = ], = 135 = = 136 = DEFAULT_EXPIRY => '+1h', = 137 = # COOKIE_PARAMS => { = 138 = # -name => 'MYCGIAPPSID', = 139 = # -expires => '+24h', = 140 = # -path => '/', = 141 = # }, = 142 = ); = 143 = = 144 = # configure authentication parameters = 145 = $self->authen->config( = 146 = DRIVER => [ 'DBI', = 147 = DBH => $self->dbh, = 148 = TABLE => 'user_info', = 149 = CONSTRAINTS => { = 150 = 'user_info.username' => '__CREDENTIAL_1__', = 151 = 'MD5:user_info.password' => '__CREDENTIAL_2__' = 152 = }, = 153 = ], = 154 = = 155 = STORE => 'Session', = 156 = LOGOUT_RUNMODE => 'logout', = 157 = LOGIN_RUNMODE => 'login', = 158 = POST_LOGIN_RUNMODE => 'okay', = 159 = RENDER_LOGIN => \&my_login_form, = 160 = ); = 161 = = 162 = # define runmodes (pages) that require successful login: = 163 = $self->authen->protected_runmodes( = 164 = 'mustlogin', = 165 = ); = 166 = = 167 = } = 168 = = 169 = sub teardown { = 170 = my $self = shift; = 171 = $self->dbh->disconnect(); # close database connection = 172 = } = 173 = = 174 = sub mustlogin : Runmode { = 175 = my $self = shift; = 176 = my $url = $self->query->url; = 177 = return $self->redirect($url); = 178 = } = 179 = = 180 = sub okay : Runmode { = 181 = my $self = shift; = 182 = = 183 = my $url = $self->query->url; = 184 = # my $user = $self->authen->username; = 185 = my $dest = $self->query->param('destination') || 'index'; = 186 = = 187 = if ($url =~ /^https/) { = 188 = $url =~ s/^https/http/; = 189 = } = 190 = = 191 = return $self->redirect("$url/$dest"); = 192 = } = 193 = = 194 = sub login : Runmode { = 195 = my $self = shift; = 196 = my $url = $self->query->url; = 197 = = 198 = my $user = $self->authen->username; = 199 = if ($user) { = 200 = my $message = "User $user is already logged in!"; = 201 = my $template = $self->load_tmpl('default.html'); = 202 = $template->param(MESSAGE => $message); = 203 = $template->param(MYURL => $url); = 204 = return $template->output; = 205 = } else { = 206 = my $url = $self->query->self_url; = 207 = unless ($url =~ /^https/) { = 208 = $url =~ s/^http/https/; = 209 = return $self->redirect($url); = 210 = } = 211 = return $self->my_login_form; = 212 = } = 213 = } = 214 = = 215 = sub my_login_form { = 216 = my $self = shift; = 217 = my $template = $self->load_tmpl('login_form.html'); = 218 = = 219 = (undef, my $info) = split(/\//, $ENV{'PATH_INFO'}); = 220 = my $url = $self->query->url; = 221 = = 222 = my $destination = $self->query->param('destination'); = 223 = = 224 = unless ($destination) { = 225 = if ($info) { = 226 = $destination = $info; = 227 = } else { = 228 = $destination = "index"; = 229 = } = 230 = } = 231 = = 232 = my $error = $self->authen->login_attempts; = 233 = = 234 = $template->param(MYURL => $url); = 235 = $template->param(ERROR => $error); = 236 = $template->param(DESTINATION => $destination); = 237 = return $template->output; = 238 = } = 239 = = 240 = sub logout : Runmode { = 241 = my $self = shift; = 242 = if ($self->authen->username) { = 243 = $self->authen->logout; = 244 = $self->session->delete; = 245 = } = 246 = return $self->redirect($self->query->url); = 247 = } = 248 = = 249 = sub myerror : ErrorRunmode { = 250 = my $self = shift; = 251 = my $error = shift; = 252 = my $template = $self->load_tmpl("default.html"); = 253 = $template->param(NAME => 'ERROR'); = 254 = $template->param(MESSAGE => $error); = 255 = $template->param(MYURL => $self->query->url); = 256 = return $template->output; = 257 = } = 258 = = 259 = sub AUTOLOAD : Runmode { = 260 = my $self = shift; = 261 = my $rm = shift; = 262 = my $template = $self->load_tmpl("default.html"); = 263 = $template->param(NAME => 'AUTOLOAD'); = 264 = $template->param(MESSAGE => = 265 = "<p>Error: could not find run mode \'$rm\'<br>\n"); = 266 = $template->param(MYURL => $self->query->url); = 267 = return $template->output; = 268 = } = 269 = = 270 = = 271 = 1; = 272 =

Listing 5: WebApp/templates/index.html

(go back)
= 273 = <html> = 274 = <head> = 275 = <META HTTP-EQUIV="Expires" CONTENT="Tue, 04 Dec 1993 21:29:02 +GMT"> = 276 = <title> = 277 = <TMPL_IF NAME> <TMPL_VAR NAME> </TMPL_IF> = 278 = </title> = 279 = </head> = 280 = <body> = 281 = = 282 = <TMPL_IF NAME> = 283 = <p>This is the <TMPL_VAR NAME> page. </p> = 284 = </TMPL_IF> = 285 = = 286 = <p> <hr/> </p> = 287 = = 288 = <TMPL_IF MESSAGE> = 289 = <p> <TMPL_VAR MESSAGE> </p> = 290 = </TMPL_IF> = 291 = = 292 = <TMPL_IF USER> = 293 = <p> User: <TMPL_VAR USER> is logged in. </p> = 294 = <p> Click <a href="<TMPL_VAR MYURL>/logout">here</a> to logo +ut. </p> = 295 = <TMPL_ELSE> = 296 = <p> Click <a href="<TMPL_VAR MYURL>/mustlogin">here</a> to l +ogin. </p> = 297 = </TMPL_IF> = 298 = = 299 = <p> <hr/> </p> = 300 = <p><a href="<TMPL_VAR MYURL>/public">public</a></p> = 301 = <p><a href="<TMPL_VAR MYURL>/public2">public2</a></p> = 302 = <p><a href="<TMPL_VAR MYURL>/private">private</a></p> = 303 = <p><a href="<TMPL_VAR MYURL>/private2">private2</a></p> = 304 = <p> <hr/> </p> = 305 = </body> = 306 = </html> = 307 =

Listing 6: WebApp/templates/default.html

(go back)
= 308 = <html> = 309 = <head> = 310 = <META HTTP-EQUIV="Expires" CONTENT="Tue, 04 Dec 1993 21:29:02 +GMT"> = 311 = <title> = 312 = <TMPL_IF NAME> <TMPL_VAR NAME> </TMPL_IF> = 313 = </title> = 314 = </head> = 315 = <body> = 316 = <TMPL_IF NAME> = 317 = <p>This is the <TMPL_VAR NAME> page. </p> = 318 = </TMPL_IF> = 319 = = 320 = <TMPL_IF MESSAGE> = 321 = <p> <TMPL_VAR MESSAGE> </p> = 322 = </TMPL_IF> = 323 = = 324 = <TMPL_IF MYURL> = 325 = <p> <a href="<TMPL_VAR MYURL>">Back</a> </p> = 326 = </TMPL_IF> = 327 = <hr/> = 328 = </body> = 329 = </html> = 330 =

Listing 7: WebApp/templates/login_form.html

(go back)
= 331 = <html> = 332 = <head> = 333 = <title>Log In</title> = 334 = </head> = 335 = <body onLoad="document.loginform.authen_username.focus();"> = 336 = <h2>Please login:</h2> = 337 = <TMPL_IF ERROR> = 338 = <p style="color:red">ERROR - Please try again.</p> = 339 = </TMPL_IF> = 340 = <p> </p> = 341 = <form name="loginform" method=POST action="<TMPL_VAR MYURL>/mu +stlogin" > = 342 = <table> = 343 = <tr> = 344 = <td align="right">User name:</td> = 345 = <td><input type="text" name="authen_username" size=20 /></ +td> = 346 = </tr> = 347 = <tr> = 348 = <td align="right">Password:</td> = 349 = <td><input type="password" name="authen_password" size=20 +/></td> = 350 = </tr> = 351 = <tr> = 352 = <td> &nbsp; </td> = 353 = <td><input type="submit" name="submit" value="Submit" /></ +td> = 354 = </tr> = 355 = </table> = 356 = <input type="hidden" name="destination" value="<TMPL_VAR DESTI +NATION>" /> = 357 = </form> = 358 = <hr/> = 359 = </body> = 360 = </html> = 361 =

Comment on RFC: Proposed tutorial - simple login script using CGI::Application
Select or Download Code
Re: RFC: Proposed tutorial - simple login script using CGI::Application
by planetscape (Canon) on Jun 19, 2007 at 19:13 UTC

    Please post your tutorial as a Meditation first - or edit your node to contain it. This allows for comments and editing time. Once you feel it is ready, then post it as a Tutorial. Writeups placed on scratchpads are usually transient; replies to this node, for example, will make no sense without context a month or a year from now.

    Thanks!

    HTH,

    planetscape
Re: RFC: Proposed tutorial - simple login script using CGI::Application
by eric256 (Parson) on Jun 19, 2007 at 20:30 UTC

    Hey,

    This looks very detailed and i'm actualy anxious to try it out myself. A few points though. I don't like your line numbers. I have an editor that does line numbers and perlmonks itself I have configured for line numbers, your number don't match those and add a layer of confusion. I can't download everything and work from your line numbers and run the scripts, since the line numbers break the script so that part bugs me. Also while mentioning the line numbers it would be nice if they were bold so i could see a line i was curious about and then jump up to your explanation. I don't know if I'm alone, but I like to read the code and only read the text when I am confused.

    Second I think any beginner is going to see that huge list of modules and the length of this and be quite afraid. Maybe eliminate some of the unneeded modules? AutoRun and Config are definitely nice, but if you went without your code doesn't get much worse while the prerequisite list will drop down.

    Finaly, for the mysql username addition, you probably want to mention they can use the MD5 function in mysql to get that hash. You should be able to just change the example to

    insert into user_info (username, password) values ( 'username', md5('password') );


    ___________
    Eric Hodges
      Second I think any beginner is going to see that huge list of modules and the length of this and be quite afraid.
      I liked the contents and wouldn't like to see any of it dropped. What about splitting it into several linked tutorials. Each tutorial could add another feature to the application (a bit like the Catalyst tutorial).
      Eric256,

      My intent, for those who actually want to run the script, was for you to download the files, then run the provided perl one-liner to strip the numbers out. For example, you could download Simple.pm, then run this:

      perl -p -i.orig -e 's/^=\s+\d+\s+= //' Simple.pm
      

      And you'll have a working copy of Simple.pm (no line numbers) and a backup called Simple.pm.orig (line numbers included) that you can use to follow along with the tutorial.

      I'm guessing your editor numbers each file starting at line 1. This makes perfect sense, but if I numbered the listings that way it would get confusing because of the overlap (i.e., 7 listings => 7 line number 1's, etc.). I concatenated all the files together, then ran a script to prepend the numbers, so that each line would have a unique identifier.

      But I'm open to suggestions - if there's a better way to do it, I'm willing to give it a try. I'll wait a few days and see what other people suggest/recommend.

      Thanks for the feedback.

        For the line numbers you could do Simple.pm:10-15 or something like that. I for one would find that much less confusing. Plus once i've downloaded and stripped the line numbers to actualy use it, your directions are now useless because they no longer match the line numbers.


        ___________
        Eric Hodges
        ...download the files, then ... strip the numbers out

        Please don't. That is really inconvenient for your readers/users. PerlMonks supports adding line numbers automatically to code blocks. You can turn it on for yourself while you're writing your post; turn it off again later if you don't want it. As it stands, I'm not at all inclined to download your code.

        I reckon we are the only monastery ever to have a dungeon stuffed with 16,000 zombies.
Re: RFC: Proposed tutorial - simple login script using CGI::Application
by GrandFather (Cardinal) on Jun 19, 2007 at 21:28 UTC

    Probably all command line material and code should be in code tags so that it stands out from the body text.

    A "line number stripper" might be a useful utility script to add. Perhaps something like:

    perl.exe -e "while (<>) {s/^=\s*\d*\s= ?//; print;}"

    Excellent work btw!


    DWIM is Perl's answer to Gödel
Re: RFC: Proposed tutorial - simple login script using CGI::Application
by naikonta (Curate) on Jun 20, 2007 at 03:29 UTC
    I thank you for taking such effort to present this tutorial++. But I must I admit that I only skimmed over the code listings so I can't comment on the logic or syntax. This is not a code snippet which I can just simply get it and run it. Since this is a complete application, I'd like to rather see the actual application online to see how it works. So we know what to expect before trying. Especially you claim that:
    it does work
    I don't think using a large number of modules would be a problem because tutorials has levels of audience. We can say that this tutorial is aimed for intermediate users, not merely beginners.

    I'd like also to point about the documentation you refer to. It's nice to tell that those docs are also available locally, because one isn't always connected to the Internet. I guess many intermediate users already know about this, but I'd considered it value added if you mention that users can access Apache docs via http://localhost/manual and MySQL docs, provided that it's installed properly by whatever Linux distro users use or installed manually, via1:

    $ info mysql $ pinfo mysql

    And about the line numbers.... You should know that codes in PerlMonks are meant for downloading so it can be run right away without any modification (additional step, even if you provide a nice snippet to remove them) on the text unrelated to the code logic or internal data. But, I also support the usage of line numbers in the tutorial code as guidance or reference of the explanation. In this case, it's good to provide other means to obtain the actual codes.

    Each listing should start with their own number 1. And it's better for me as audience to see the explanations separated for each listing. So when you say, for example, "Simple.pm:10-15" as suggeted by eric256, I know for sure what's the context that particular saying in. In this case of this clear separation of the explanations, it's useful to make cross references by explicitly mentioning the listing name, or merely the listing number. Putting backlinks would also improve user experience so we can go back and forth between the listing and the text. For example:

    Some text that refers to listing. The explanation contniues....

    later in the listing....
    Listing1: foo.pl - Back to text

    1: #!/usr/bin/perl 2: use strict; 3: use warnings; ...

    The only benefit I can see by using continuation number is that I know right way that it takes around 270 LOCs to build such application in Perl (but the number is not such a big deal).

    Hope you can find this useful. Once again, great effort!

    Update
    1: info and pinfo are pretty standard in Linux box. I don't know the equivalent tools for another OSes


    Open source softwares? Share and enjoy. Make profit from them if you can. Yet, share and enjoy!

      naikonta,

      Thank you for the feedback.

      About the number of modules used... I probably didn't give enough background. When I first began working with CGI::Appliction, creating a login form was one of the first things I wanted to do. CGI::Application assumes you're already familiar with CGI and HTML::Template, so if you're not, there's a fairly steep learning curve (but less so than, say, Catalyst! IMO). Browsing the list of plugins on CPAN (to avoid "reinventing the wheel") I found CGI::Application::Plugin::Authentication. At first, I thought this would be all I needed. But then I realized that it required CGI::Application::Plugin::Storable (unless you want to maintain state with cookies). And it also requires CGI::Application::Plugin::DBH, if you want your passwords stored in a database, instead of a text file. Well, the DBH plugin is really just a wrapper around DBI, so you have to read the documentation for both, but that's still not enough, because at that level it's independent of which database you actually plan to use. Once you decide on, say, mysql, then you have to look up the mysql specific driver modules to learn how to configure things properly, etc. So, what happened to me (and I suspect everyone else that goes down this path) is that you start looking at "just one module" and before you know it you've downloaded about 20 things from CPAN and have two dozen browser windows open to all the various documentation files and while you know all these things are supposed to work together (and each part may, in fact, have good docs and good examples), there is no example of everything put together in final form. So that's what I wanted to do. Too many tutorials are made up of code snippets, and the reader is left having to "connect the dots". The other problem is too much documentation. I am barely scratching the surface of what modules like DBI and Storable are capable of. But for what I want to do, I don't need to know 99% of what's documented - so it's a shame to spend all day reading it trying to find the one detail that you DO need to know.

      As for the line numbers, I'm going to point the finger at Randal Schwartz. I love Randal! And his articles way-back-when in Web Techniques magazine really helped me learn how to program in perl. And he ALWAYS showed complete code listings with each line numbered, for easy reference. So I did my stuff the same way. I agree, it would be nice to upload a working demo online somewhere, maybe with a downloadable tar ball of all the code, to save people from having to download each piece and then strip off the line numbers (oh the horror!). But I don't currently have an ISP that allows CGI scripts, so that's not possible.

      Including backlinks is an excellent idea. I actually had thought of that myself, but was too lazy to do it. I'll try to add that in, later.

      One more thing - regarding my claim that it works. I'm running perl5.8, apache2, and red hat linux (RHEL4). So if anyone out there is trying to develop on win32, er, your milage may vary.

        Hi, scorpio17,
        I'm going to point the finger at Randal Schwartz
        We're referring to the same thing. But I see you only follow him halfway :-) For example, this article is accompanied by a separate listing.
        But I don't currently have an ISP that allows CGI scripts, so that's not possible.
        Check http://www.perlmonk.org out and try to contact the administrator and see if you can get an account on the server and put your application sample online.

        Adding information about your environment is very good so we might have something to blame on in case something goes unexpected :-)


        Open source softwares? Share and enjoy. Make profit from them if you can. Yet, share and enjoy!

Re: RFC: Proposed tutorial - simple login script using CGI::Application
by snoopy (Deacon) on Jun 21, 2007 at 05:38 UTC
    ++ scorpio17.

    It just so happens I'm just starting a new web application as a first time CG::Application user! I have downloaded and run this. An excellent and much needed starting point CGI::Application and friends.

    I didn't have my desktop Apache (1.3) configured for https, so I commented out the https redirects in Login.pm lines 187-189 and 207-209. I'm also new to Apache 1.3 SSL setup and will get back to it later.

    It my be useful to add a bit more on on the required https set-up. Also get the user to test it early, say try https access to the first 'hello world' script.

    Well done!

      snoopy:

      If you comment out lines 156-159:

      # LOGOUT_RUNMODE => 'logout', # LOGIN_RUNMODE => 'login', # POST_LOGIN_RUNMODE => 'okay', # RENDER_LOGIN => \&my_login_form,

      Then the Authentication plugin will use its default login form, which does NOT make use of SSL. The main reason I needed to create my own was to enable SSL functionality.

      As for setting up apache+ssl, you can try looking here:

      http://ubuntuforums.org/showthread.php?t=51753

      It's geared towards ubuntu linux, but should apply to any Debian based linux distibution with minor changes.

      Good luck!

Reaped:
by NodeReaper (Curate) on Nov 26, 2008 at 16:53 UTC
      And if your going to use SQLite, make sure you get rid of the database disconnection feature, just comment out the line:
      $self->dbh->disconnect(); # close database connection
      According to this...

      http://blog.gmane.org/gmane.comp.lang.perl.modules.cgi-session.user/month=20080201

      Also be sure to:
      # $CFG{'DB_USER'}, # "webadmin",
      and
      "driver:sqlite;serializer:Storable;id:md5", # "driver:mysql;serializer:Storable;id:md5",
      All in Login.pm

      And simple.ini would need to be changed as well for the DB stuff.

Re: RFC: Proposed tutorial - simple login script using CGI::Application
by Anonymous Monk on Dec 14, 2011 at 18:46 UTC

    very nice tutorial!

    Please fix this line:

     Inside the WebApp directory, create a 'template' subdirectory, and a 'libs' subdirectory. Next create a 'MyLib' directory inside of 'libs'.

    It should be "create a 'templates' subdir". Easy typo but just took me half an hour to find the mistake.

    Thanks!
      Sorry about that - I've fixed it. Thanks for the feedback.

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlmeditation [id://622071]
Approved by planetscape
Front-paged by GrandFather
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others pondering the Monastery: (9)
As of 2014-12-27 17:10 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    Is guessing a good strategy for surviving in the IT business?





    Results (177 votes), past polls