Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Web Development

Simplified Web Hosting with Perl


Simon is a freelance programmer and author, whose titles include Beginning Perl (Wrox Press, 2000) and Extending and Embedding Perl (Manning Publications, 2002). He's the creator of over 30 CPAN modules and a former Parrot pumpking. Simon can be reached at [email protected].


Recently I've been working on a spare-time project setting up easy-to-use web hosting services, based partly on wikis, so that people who don't want to learn about HTML or even how to use an FTP client can get material up on the Web. It's been a fun project because it's forced me to learn some new technologies and work in new areas outside what I consider to be my core competencies of programming.

In this article, we are going to look at three of the things I've been playing with and how they relate to Perl.

Apache and DNS

Recently I had a look at the interesting work that's been coming out of a company called "43 Folders." They're using Ruby on Rails, a web framework suspiciously similar to my own Maypole for Perl, and they've used it to produce a couple of web applications, Basecamp and Backpack. Basecamp is for large-scale project management, and Backpack is sort of a personal version. What I particularly liked about these applications is that once you sign up, which is free, you instantaneously get a hostname reflecting your user name. That is, if I sign up as "simon" on Backpack, the address of my personal todo list is "http://simon.backpackit.com".

I particularly liked this because it's the way my hosting service does things as well. This works due to a combination of wildcard DNS and Apache mod_perl. The first part, wildcard DNS, is the ability to essentially resolve "*anything*.your-domain.com." This is done by putting a line in the domain's DNS configuration like so:

        * IN A 1.2.3.4

Now "simon.your-domain.com," and indeed everything else, will resolve to your web server. The next part, Apache mod_perl, helps us to configure Apache with knowledge of whether we should find a client site at that address, or a notice that this login name is unclaimed, and that they're quite welcome to register it. This is done by means of a special Perl section in the Apache configuration.

Perl sections allow you to use Perl variables in place of Apache configuration directives. For instance, what would normally be

        Alias /perl-bin/ /usr/local/apache/perl-bin/

might look like this:

        <Perl>
            push @Alias, [ "/perl-bin/" => "/usr/local/apache/perl-bin/" ];
        </Perl>

Similarly, we can configure VirtualHost sections like so:

        <Perl>
            push @{$VirtualHost{"*"}}, {
                ServerName => "simon.your-domain.com",
                SetHandler => "perl-script",
                PerlHandler => "Mitiki",
                ...
            };
        </Perl>

Incidentally, Mitiki is a customized Maypole-based wiki that is used for the hosting—this way, clients can update their web pages without the hassle of either learning HTML or messing about with FTP.

The beauty of configuring things with Perl sections is that instead of making the changes for individual sites by hand, we can do everything programmatically by looking at the hosted sites in the database. So my provider's apache.conf contains lines like so:

        <Perl>
        use Hosting;
        Hosting->configure_apache(\%VirtualHost);
        </Perl>

This allows me to populate the VirtualHost hash dynamically, which is then fed back to Apache. The configure_apache method looks like this:

        sub configure_apache {
            my ($class, $vh) = @_:
            push @{$vh->{"*"}}, {
                ServerName => "www.".$class->domain,
                DocumentRoot => "/web/our-roots/www/"
            }

            for my $hosting ($class->active_hostings) {
                my %hash = (
                   SetHandler => "perl-script",
                   PerlSetVar => "MitikiDB ". $hosting->database_dsn,
                   PerlHandler => "Mitiki",
                   DocumentRoot => $hosting->template_root,
                   ServerName => $hosting->domain
                );
                push @{$vh->{"*"}}, \%hash;
            }

            push @{$vh->{"*"}}, {
                ServerName => "*.".$class->domain,
                DocumentRoot => "/web/our-roots/unclaimed/"
            }
        }

This first configures a "www" virtual host, which will provide information about how you can sign up, and so on. Next, we query the database for all the sites that we host that are currently fully paid up. For each of those sites, we configure a virtual host with the appropriate domain name, which will run "Mitiki" with a particular database DSN-specific to the client. Finally, we configure a wildcard host to catch everything that isn't "www" or a genuine client site.

Now when a client registers, the system adds a new entry in the "hosting" table, sets up the SQLite database for Mitiki, copies the default templates to a new location, and restarts Apache. When Apache is restarted, the configuration is loaded up, which in turn causes the configure_apache method to be called. This looks at the database, sees the new client, and sets up the site for them, ready for them to immediately begin working on.

This is all very neat, but there's just one slight niggle. I actually needed to set up several PerlSetVars in the same virtual host, to tell Mitiki more about the client and where to find its templates. Obviously, I can't do this:

        my %hash = (
           PerlSetVar => "MitikiDB " . $hosting->database_dsn,
           PerlSetVar => "MitikiClient " . $hosting->client->id,
           PerlSetVar => "MitikiTemplates " . $hosting->template_root,
           ...

because it's an ordinary hash and each of the keys would override the previous one. Instead, the solution is to use the Tie::DxHash module, which allows you to use multiple keys of the same name:

        tie my %hash, "Tie::DxHash";
        my %hash = (
           PerlSetVar => "MitikiDB ". $hosting->database_dsn,
           PerlSetVar => "MitikiClient ". $hosting->client->id,
           PerlSetVar => "MitikiTemplates ". $hosting->template_root,
           ...

Apache knows how to deal with this kind of hash and configures itself appropriately.

There's one final trick: How does Mitiki cope with the fact that it needs to take its database handle from the Apache configuration? Normally, you'd configure a Maypole application (or any Class::DBI application) with a static database handle, like so:

        package Mitiki::DBI;
        use base 'Class::DBI';
        Mitiki::DBI->connection("dbi:SQLite:mitiki.db");

However, we need to have a different database for every site, but configured dynamically. Thankfully this is something that Class::DBI provides for—dynamically fetching the DBI handle. We do it by overriding the db_Main method that selects the handle. Normally, it selects the handle by looking at the connection class data, but we can make it do something a little more clever:

        sub db_Main {
            my $class = shift;
            my $dsn = Apache->request->dir_config("MitikiDB");
            return DBI->connect_cached($dsn, undef, undef,
                {$class->_default_attributes}
            );
        }

Here we're getting back the DSN, which we stored in the Apache configuration, and connecting to it. Notice that we pass in the value of the _default_attributes method—this allows Class::DBI to configure the connection its way.

So here we have a completely data-driven application: When the new user is registered, the site switches over from being unclaimed to being a Mitiki site automatically, and Mitiki configures itself to work with the appropriate database for the new user.

But how do we make sure that the new user is allowed to be a new user?

Ajax

"Ajax" first became a big buzzword in February, when Jesse James Garrett of Adaptive Path wrote an essay on creating web applications. It stands for "Asynchronous JavaScript and XML," and it's produced a disproportionate amount of buzz for such a simple technology. It used to be called "Javascript RPC" and technologically, it just means that a web page's JavaScript can go and fetch another web page. What you do with the result of that web page is up to you, and that's where Ajax gets interesting.

If you're Google Maps, then your JavaScript request will fetch tiles of an image, and then sew them together. If you're Google Suggest, on the other hand, you'll fetch a list of search terms, and automatically fill in a drop-down box with them.

If you're me, and you're writing a page to register a user for a web hosting service, you can use Ajax to check whether or not the user name has already been taken and provide immediate feedback to the user. The moment I move off the username field in my form, a Javascript event runs; this event makes a request to ask the server if the field data, the username, is already in use. When that request returns, it updates the field with a message saying whether or not the username was valid.

Now, the advent of Ajax has lead to various "Ajax libraries" to work with your favourite web framework, but since we're using Maypole for the web side of the hosting service, it's so easy to do that we don't need an external library. We'll post a request to "/client/check/?login=whatever" and expect to get back either the word "ok" or the word "no." To implement the server-side interface, we just add a Maypole action in our client class:

        sub check :Exported {
            my ($self, $r) = @_;
            my $name = $r->params->{login};
            $r->content_type("text/plain");
            if ($name =~ /^(www|admin|root|simon|ns\d*|(web|post)master)$/) {
                $r->{output} = "no"; return;
            }
            my ($user) = $self->search(login => $name);
            $r->{output} = $obj ? "no" : "ok";
        }

For those of you who aren't familiar with Maypole, this method declaration enables the /client/check URL; whenever we hit that URL, this method is called. The third line extracts the login CGI parameter out of the Maypole request, the next line states that we're going to be returning a plain text page. Then we check if the user is trying to be naughty by passing in a reserved name, and if so, we set the output to the word "no." Finally, we search for a user with the name that's been passed in, and return "no" or "ok" depending on whether a user already exists.

That's the server side—we can ask the server about the validity of a name and it will return one word telling us. The difficult side of Ajax is connecting that up to the web interface, using Javascript. First, we want the login form field to trigger a Javascript action.

        <input name="login" id="login" onblur="check_login()">
        <div id="signupstatus" class="status">
        </div>

Now we need to start writing the check_login() function, which will validate the login name by contacting the server. First we'll get the value of the form field:

        var http;
        function check_login () {
            var login = document.getElementById("login").value;
            login = login.toLowerCase();

At this point, we can do the Ajax thing. Unfortunately, this being Javascript, there are two conflicting implementations: one for Mozilla, and one for Internet Explorer. We test to see whether we're provided with the XMLHttpRequest class (Mozilla) or the ActiveXObject class (IE).

            if (window.XMLHttpRequest) {
                http = new XMLHttpRequest();
            } else if (window.ActiveXObject) {
                http = new ActiveXObject();
            }
            if (!http) { return; } // Bail out

Now we have an HTTP object, with which we can make a request.

            http.onreadystatechange = handle_http_response;
            http.open("GET", "http://.../client/check?user=" + escape(login), true);
            http.send();
            return;
        }

We said that Ajax was "asynchronous JavaScript and XML," and this is where the "asynchronous" bit comes in. We send off the HTTP request, and sometime later, we'll get a response object back. When we do, it will be passed to the handler function we registered with the http object, handle_http_response. So now we have to declare that handler:

        function handleHttpResponse() {
            if (http.readyState != 4) { return } // Didn't get "200 OK"

            var status = document.getElementById("signupstatus");
            if (http.responseText.match(/ok/) {
                status.style.background = "#dfd";
                status.innerHTML = "That login name appears to be fine. Thanks.";
            } else {
                status.style.background = "#fdd";
                status.innerHTML = "That login name appears to be in use.";
            }
        }

And that's the basics of Ajax: When something happens (the user leaves the field, signalling that they have finished filling it in), a piece of Javascript makes a web request to the server. The server returns some data, and another piece of Javascript handles it, providing real-time feedback to the user. It's how all the fancy Web 2.0 applications do their clever integration between the web client and the back-end server, and it's really not that tricky to do.

CSS

So now we have a new user able to register, to get their own host name, and to get a wiki started up on it. There's just one problem: Their wiki looks just like everyone else's. Now, this is the problem I'm working on at the moment, and I know what the solution is going to look like but I haven't finished it yet—I'm still learning myself.

Sites like CSS Zen Garden (http://www.csszengarden.com/) are very good at showcasing what can be done to style a site in completely different ways using the same content but just changing the CSS. This is the principle we're going to use, but there's a bit of a twist: We're writing this hosting service for people who don't know HTML or how to use FTP, so they're certainly not going to be expected to know how to write CSS. Instead, we're going to write the CSS for them.

First, we start with a few default page designs, which we adapt from http://www.thenoodleincident.com/tutorials/box_lesson/boxes.html. This gives the user a choice of how they want their site to look in overall terms: one column, three columns, columns with a header and a footer, and so on. This is stored in the database.

Next, we have a Javascript construction that actually allows them to change various elements of the page, by clicking on them and selecting the font size, background, spacing, colors, and so on that they want those elements to be. This is submitted to a Maypole action that stores the CSS parameters in the database.

The final piece of the puzzle comes with the fact that the reference to the CSS stylesheet in the header of each Mitiki page changes from being a static file to itself being a Maypole request:

        <link rel="stylesheet" href="/client/css/" type="text/css" />

The basic stylesheet that they have chosen becomes the template that Maypole uses to process with Template Toolkit:

        sub css :Exported {
            my ($self, $r) = @_;
            $r->template($r->user->prefs->css_template);

And the CSS preferences that we've just saved go on to be template parameters to this templated CSS file:

            $r->template_args($r->user->prefs->css);
        }

The template toolkit will then work on this outline CSS file, filling it in with the user's preferences for individual display elements, and in this way, they can customize the look of their web site just by clicking on paragraphs or headers, and altering the parameters. Once again, we've made it as easy as possible to create, update, and customize a web site.

Conclusion

Part of the fun of programming is that there are always new ideas coming out—people doing interesting things with CSS or Apache or JavaScript and we can integrate these ideas into our Perl applications. It's usually pretty easy to do, and the ideas generally make our applications easier to use and, well, more fun.

It takes a little bit of investment into looking at what people are doing and how they're doing it (http://del.icio.us/popular/ is always a good source for articles on the latest web techniques) and a little bit of time to learn something outside your normal area of competence—even a year ago, I'd have hated to think of myself as a JavaScript programmer—but any new thing you learn will broaden your horizons and improve your all-around programming and problem-solving abilities.

Make yourself a better programmer and learn something completely new today!

TPJ


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.