Home > Articles > Web Development > Perl

Like this article? We recommend

Like this article? We recommend

Sample tie() Module

Now, on with our module! We'll create a module that ties to a password file for Apache, which means that there will be a file with entries like this:

'<name>:<encrypted password>'

There will also be a simple command-line script using the Example class. The script will let you enter names and passwords which will be stored in the password file through the magic of tie().

The start of the module is fairly uninteresting, aside from the fact that we're importing the group of file-locking constants from the Fcntl module. We do this because we want to use the system values for LOCK_EX, LOCK_SH, LOCK_UN, and LOCK_NB instead of assuming the operating system value for these.

package Example;

use strict;
use vars qw($VERSION);

# pull $VERSION from CVS version identifier
($VERSION = substr(q$Revision: 0.7 $, 10)) =~ s/\s+$//;

sub Version {return $VERSION;}

use Fcntl qw(:flock);

use Carp;

As explained earlier, the TIEHASH method is called implicitly when you invoke tie(). The first three lines tell our method the following:

  • What the class object is, as well as the LIST elements passed during our invocation

  • The file we're tying to

  • The modes we're opening the file with

If the permission ($mode) argument isn't given, it defaults to 'r', read only. Or it can be given 'rw', which will allow for read and write operations. It's a good practice to add some sort of permission argument to a tie() class, to make sure that no one accidentally stores or removes information from the resource. It's also a good practice to have the default permission be the most restrictive.

Next, check to make sure that no extra arguments are passed to the module. If there are extra arguments, it croaks with a usage/syntax message.

Notice the scalar variable $clobber. Clobber is a term that tells the class whether the caller is able to make changes to the tied resource. It's directly affected by the permissions with which it was tied. If the mode is 'r', clobber is 0; for 'rw', clobber is 1. The $clobber variable will be used later to make sure that the file is opened with correct permissions, and deny changes to the file if it wasn't tie()'d with read-write permissions.

Next, an anonymous hash, %node, is built. This anonymous hash houses some instance data that will be referred to throughout the module. This hash contains the current settings of the path to the file, whether or not it can clobber, and the current hash values. The value of the current hash is held in memory because otherwise some later methods would have to open and read the file to build the hash again.

NOTE

You may not want this for your application. If an application would be used concurrently by multiple users, each method should get the current values from your tied resource so that no data is lost. It's done this way here to save space and resources for this example.

This allows for the current hash values to be in memory at all times. Speaking of which, the next few lines build that hash, and store it in the CURRENT field ($self->{CURRENT}).

You may choose to store the information from your resource in memory if the resource can be changed frequently only by your current process. If the application is used frequently by many users, each method should get the current data from the resource in some fashion. An alternative is to have a private method, which reads in the resources data. This method can then be called from within your class' methods to get the latest values. I suggest adding that functionality later, when playing around with the Example class.

Finally, the bless() function is called, which tells %node that it's now an object in the class. We're tied and ready to go!

# Create tied hash

sub TIEHASH {
    my $self = shift;
    my $path = shift;
    my $mode = shift || 'r';

    if (@_) {
        croak ("usage: tie(\%hash, \$file, [mode])");
    }

    my $clobber = ($mode eq 'rw' ? 1 : 0);

    my $node = {
        PATH  => $path,
        CLOBBER => $clobber,
        CURRENT => {}
    };

    open(FH, "$path");
        my @lines = <FH>;
    close FH;

    my ($line, $id, $pass);
    foreach $line (@lines) {
        ($id, $pass) = split(/\:/,$line);
        $node->{CURRENT}{$id} = $pass;
    }

    return bless $node => $self;
}

The next method to create is STORE. The STORE method handles the actual writing of data to the resource when a value in the tied hash changes. This method also performs any complex behavior that must be done before the data is written, such as encrypting a password.

NOTE

"Complex behavior" includes any preprocessing tasks needed to ready data, or possibly logging access to the tied variable.

This method is called when doing something such as the following:

$hash{FOO} = "bar";

The call above tells the class to store the value "bar" under key name FOO in %hash. This sets the name/value pairs in CURRENT and eventually in the password file. Since we're writing to an htpasswd-like password file, this STORE method also does the encryption (which can be considered hidden complex behavior).

The method begins by defining variables. If the call noted above were used, $id would be FOO, and $passwd would be "bar". It grabs the path, as well as checks for clobbering, from the class data that was saved in the constructor. If clobbering isn't allowed, it returns with an error message. For good programming measure, we're taking into account the fact that STORE is called after the upcoming method CLEAR finishes.

NOTE

This is part of the internal workings of the tie() function, and not something done programmatically.

When STORE is called via CLEAR there will be no arguments (besides $self, of course). So STORE returns before writing an entry with no username or password.

# Store an entry

sub STORE {
    my $self = shift;
    my $id = shift;
    my $passwd = shift;
    my $passwdFile = $self->{PATH};
    my $return = 0;
    my @cache;
    my $cryptedPass;

    unless ($self->{CLOBBER}) {
        carp ("No write access for $self->{PATH}");
        return;
    }

    if (!$id && !$passwd) {
        return 1;
    }

The next step is to create the new encrypted password. The "salt" for encryption is obtained by getting the first two letters of the system's hostname by the hostname() method of the Sys::Hostname module (distributed with Perl). Before crypt() is called, we want to make sure that there's a password to crypt. There would be no password if the function is called intending to delete the password like this:

$hash{name} = "";
# or
$hash{name} = undef;

NOTE

Win32 ports of Perl may not implement crypt(). To make sure that your Perl is compiled to support crypt(), you can do a test from the command line:

perl -e "print crypt("ab","test")";

Look into Crypt.pm if crypt() is unavailable to you.

The method takes the situation of a blank password into account so that a username isn't written with no password. If there's a password, we encrypt it.

use Sys::Hostname;


my $salt = substr(hostname, 0, 2);

if ($passwd eq "") {
    $cryptedPass = "";
}else{
    $cryptedPass = crypt($passwd, $salt);
}

The new name/password pair is ready to be written to the password file. The file is opened and locked with flock() to make sure that the file isn't modified before the method finishes writing the new data. A check is made against the existing name/password pairs stored in memory to see whether there's already a password for this username. If the entry exists, the method runs through the file and replaces the old entry with the new one. If no such entry exists, the method appends the new entry to the file. Finally, it closes our file and adds the new entry into the hash in memory so the saved hash is in synch with the password file.

if (!open(FH,"{CURRENT}{Id}) {
    while (<FH>) {
        if ( /^$Id\:/ ) {
            push (@cache, "$Id\:$cryptedPass\n") unless $cryptedPass eq "";
            $return = 1; 
        } else {
            push (@cache, $_);
        }
    }
}
close FH;


if ($return) { 
    if (!open(FH, ">$passwdFile")) {
        carp("Cannot open $passwdFile: $!");
        return;
    }
    flock(FH, LOCK_EX);
    while (@cache) { 
        print FH shift (@cache); 
    }
}else{
    if (!open(FH, ">>$passwdFile")) {
        carp("Cannot open $passwdFile: $!");
        return;
    }
    flock(FH, LOCK_EX);
    print FH "$Id\:$cryptedPass\n" unless $cryptedPass eq "";
    $foo = $hash{FOO};
}

The FETCH method has a very specific function: to get a value. To do this it first checks whether the username ($Id) exists in the current hash. If so, it returns that value; if not, it returns a message saying that the username doesn't exist. The FETCH method is very simple and straightforward because it isn't performing any magic in the background.

sub FETCH {
    my $self = shift;
    my $Id = shift;     
    if (exists $self->{CURRENT}{$Id}) {
        return $self->{CURRENT}{$Id};
    }else{
        return "$Id doesn't exist";
    }
}

Here's a fast quiz. Judging by the names of the methods so far, what would you guess the method name is for deleting an entry in the file? (Faint sounds of the Jeopardy theme...) If you said DELETE, you're correct!

The DELETE method deletes an entry in the hash, and in turn the tied resource. It doesn't delete just the value, but the key/value pair. The DELETE method is only called when the delete() function is called. Assigning undef or "" to an entry in the hash doesn't delete that entry, so DELETE is not called.

delete $hash{FOO};

The above DELETE call deletes all instances of entry FOO. What you see in the example DELETE method should look familiar now. First a check for clobbering is done. If it's okay to clobber, a check is made in the local hash to make sure that the entry to be deleted actually exists. If it does, the file is opened and its entries are read, removing the entry marked for deletion. Finally, the new file is written. I added in a return of 1 when the entry doesn't exist, since the user may have thought that it did (I didn't think it warranted the script exiting). It also returns 1 when an entry is successfully deleted. The only error, aside from not being able to open a password file, that can make the subroutine die() is if the file was opened with read-only permissions.

sub DELETE {
    my $self = shift;
    my ($Id) = shift;
    my ($passwdFile) = $self->{PATH};
    my (@cache);

    unless ($self->{CLOBBER}) {
        carp ("No write access for $self->{PATH}");
        return;
    } 


    if (!exists $self->{CURRENT}{$Id}) {return 1;}

    delete $self->{CURRENT}{$Id};

    if (!open(FH,") {
            if ( /^$Id\:/ ) { 
                next;
            } else {
                push (@cache, $_);
            }
        }
    close FH;

    if (!open(FH,">$passwdFile")) {
        carp("Cannot open $passwdFile: $!");
        return;
    }
    flock(FH, LOCK_EX);
        while (@cache) {
            print FH shift (@cache); 
        }
    close FH;
    return 1;
}

The module is almost complete. Our next method, CLEAR, clears the entire hash, as well as clearing all the data out of the tied resource. CLEAR is generally called when you assign an empty list as the value of your tied hash. This occurs when assigning a null string to another hash, or undef. This can be very dangerous in a situation where a programmer isn't paying attention and makes a call to invoke CLEAR by accident. The following illustrates ways in which CLEAR will be invoked:

%hash = "";
%hash = %newHash;
%hash = {};
undef %hash;

To help prevent this sort of mishap, the module can be made more foolproof by adding another level of 'mode' to tie()—one that will set the $clobber variable to something higher than 1 (such as 2). Then the TIE* method can be written to understand not only 'r' and 'rw', but something like 'rwe' as well. This way, an extra level of security is added, and the user knows that he can erase the resource because he used the proper permissions when he invoked the tie(). I didn't add that into this example, but left it as an exercise for the reader. The example's CLEAR method will CLEAR the local hash, as well as the password file.

sub CLEAR {
    my $self = shift;
    my ($passwdFile) = $self->{PATH};

    unless ($self->{CLOBBER}) {
        carp ("No write access for $self->{PATH}");
        return;
    }

    if (!open(FH,">$passwdFile")) {
        carp("Cannot open $passwdFile: $!");
        return;
    }
    close FH;
    $self->{CURRENT} = {};
}

Now we get into the last few methods. These are very simple methods, and very short! The FIRSTKEY method is invoked when a call is made to iterate through the hash, generally with the keys() or each() functions.

sub FIRSTKEY {
    my $self = shift; 
    my $a = keys %{$self->{CURRENT}};
    each %{$self->{CURRENT}};
}

One of the last methods is NEXTKEY. This method is also invoked during an each() or keys() iteration. Behind the scenes it is given two arguments, 'this' and 'lastkey', which are the object and the last key iterated through, respectively. It's very similar to FIRSTKEY but it returns all the keys, as opposed to the first key in the hash. Arguments are ignored because the method is using the each function behind the scenes to iterate over the $self->{CURRENT} hash.

sub NEXTKEY {
my $self = shift;
   return each %{$self->{CURRENT}};

}

The last method is the class destructor, DESTROY. This method is invoked when the tied variable is to be destroyed. Unless the return value of tie() has been saved, this can be done with the untie() function. If the tied variable hasn't been untie()'d, DESTROY is called when the script exits. In general, you don't need to have anything in a DESTROY method, unless you're doing some special debugging or you want to do some cleanup. In fact, you don't need to have a DESTROY method at all, and our Example.pm won't have this method.

Suppose you created a temp file for whatever reason, and want to delete it only when you know the tie() is finished. The line below would do that:

sub DESTROY { unlink "/tmp/tie.txt";}

And there you have it! A module that will bind a variable from a script to a password (or whatever) file. Now that it's written, let's use it. This is a quick command-line program to test this module. Try adding, deleting, and getting passwords.

#!/usr/bin/perl
use Example;

tie(%hash, "Example", "example", "rw") || die "Can't tie : $!";

&ask;

sub ask {
    print "(A)dd, (D)elete, or (G)et user:";
    $ans = <STDIN>;
    if ($ans =~ /a/i) { &add; }
    elsif ($ans =~ /d/i) { &delete;} 
    elsif ($ans =~ /g/i) {&get;}
    else { print "Try again\n"; &ask;}
}

sub add {
    print "User Name:";
    $name = <STDIN>;
    print "\nPassword:";
    $pass = <STDIN>;
    chop $name;
    chop $pass;
    $hash{$name} = $pass;
    print "\nAdded\nAgain (Y/N)?";
    $again = <STDIN>;
    if ($again !~ /y/i) { untie %hash; exit;}else{&ask;}

}

sub delete {
    print "User Name:";
    $name = <STDIN>;
    chop $name;
    delete $hash{$name};
    print "\nDeleted\nAgain (Y/N)?";
    $again = <STDIN>;
    if ($again !~ /y/i) { untie %hash; exit;}else{&ask;}

}

sub get {
    print "User Name:";
    $name = <STDIN>;
    chop $name;
    if (!exists $hash{$name}) {
        print "$name isn't valid";
    }else{
        print "$name\'s encrypted password is " . $hash{$name};
    }
    print "\nAgain (Y/N)?";
    $again = <STDIN>;
    if ($again !~ /y/i) { untie %hash; exit;}else{&ask;}

}

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.

Overview


Pearson Education, Inc., 221 River Street, Hoboken, New Jersey 07030, (Pearson) presents this site to provide information about products and services that can be purchased through this site.

This privacy notice provides an overview of our commitment to privacy and describes how we collect, protect, use and share personal information collected through this site. Please note that other Pearson websites and online products and services have their own separate privacy policies.

Collection and Use of Information


To conduct business and deliver products and services, Pearson collects and uses personal information in several ways in connection with this site, including:

Questions and Inquiries

For inquiries and questions, we collect the inquiry or question, together with name, contact details (email address, phone number and mailing address) and any other additional information voluntarily submitted to us through a Contact Us form or an email. We use this information to address the inquiry and respond to the question.

Online Store

For orders and purchases placed through our online store on this site, we collect order details, name, institution name and address (if applicable), email address, phone number, shipping and billing addresses, credit/debit card information, shipping options and any instructions. We use this information to complete transactions, fulfill orders, communicate with individuals placing orders or visiting the online store, and for related purposes.

Surveys

Pearson may offer opportunities to provide feedback or participate in surveys, including surveys evaluating Pearson products, services or sites. Participation is voluntary. Pearson collects information requested in the survey questions and uses the information to evaluate, support, maintain and improve products, services or sites, develop new products and services, conduct educational research and for other purposes specified in the survey.

Contests and Drawings

Occasionally, we may sponsor a contest or drawing. Participation is optional. Pearson collects name, contact information and other information specified on the entry form for the contest or drawing to conduct the contest or drawing. Pearson may collect additional personal information from the winners of a contest or drawing in order to award the prize and for tax reporting purposes, as required by law.

Newsletters

If you have elected to receive email newsletters or promotional mailings and special offers but want to unsubscribe, simply email information@informit.com.

Service Announcements

On rare occasions it is necessary to send out a strictly service related announcement. For instance, if our service is temporarily suspended for maintenance we might send users an email. Generally, users may not opt-out of these communications, though they can deactivate their account information. However, these communications are not promotional in nature.

Customer Service

We communicate with users on a regular basis to provide requested services and in regard to issues relating to their account we reply via email or phone in accordance with the users' wishes when a user submits their information through our Contact Us form.

Other Collection and Use of Information


Application and System Logs

Pearson automatically collects log data to help ensure the delivery, availability and security of this site. Log data may include technical information about how a user or visitor connected to this site, such as browser type, type of computer/device, operating system, internet service provider and IP address. We use this information for support purposes and to monitor the health of the site, identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents and appropriately scale computing resources.

Web Analytics

Pearson may use third party web trend analytical services, including Google Analytics, to collect visitor information, such as IP addresses, browser types, referring pages, pages visited and time spent on a particular site. While these analytical services collect and report information on an anonymous basis, they may use cookies to gather web trend information. The information gathered may enable Pearson (but not the third party web trend services) to link information with application and system log data. Pearson uses this information for system administration and to identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents, appropriately scale computing resources and otherwise support and deliver this site and its services.

Cookies and Related Technologies

This site uses cookies and similar technologies to personalize content, measure traffic patterns, control security, track use and access of information on this site, and provide interest-based messages and advertising. Users can manage and block the use of cookies through their browser. Disabling or blocking certain cookies may limit the functionality of this site.

Do Not Track

This site currently does not respond to Do Not Track signals.

Security


Pearson uses appropriate physical, administrative and technical security measures to protect personal information from unauthorized access, use and disclosure.

Children


This site is not directed to children under the age of 13.

Marketing


Pearson may send or direct marketing communications to users, provided that

  • Pearson will not use personal information collected or processed as a K-12 school service provider for the purpose of directed or targeted advertising.
  • Such marketing is consistent with applicable law and Pearson's legal obligations.
  • Pearson will not knowingly direct or send marketing communications to an individual who has expressed a preference not to receive marketing.
  • Where required by applicable law, express or implied consent to marketing exists and has not been withdrawn.

Pearson may provide personal information to a third party service provider on a restricted basis to provide marketing solely on behalf of Pearson or an affiliate or customer for whom Pearson is a service provider. Marketing preferences may be changed at any time.

Correcting/Updating Personal Information


If a user's personally identifiable information changes (such as your postal address or email address), we provide a way to correct or update that user's personal data provided to us. This can be done on the Account page. If a user no longer desires our service and desires to delete his or her account, please contact us at customer-service@informit.com and we will process the deletion of a user's account.

Choice/Opt-out


Users can always make an informed choice as to whether they should proceed with certain services offered by InformIT. If you choose to remove yourself from our mailing list(s) simply visit the following page and uncheck any communication you no longer want to receive: www.informit.com/u.aspx.

Sale of Personal Information


Pearson does not rent or sell personal information in exchange for any payment of money.

While Pearson does not sell personal information, as defined in Nevada law, Nevada residents may email a request for no sale of their personal information to NevadaDesignatedRequest@pearson.com.

Supplemental Privacy Statement for California Residents


California residents should read our Supplemental privacy statement for California residents in conjunction with this Privacy Notice. The Supplemental privacy statement for California residents explains Pearson's commitment to comply with California law and applies to personal information of California residents collected in connection with this site and the Services.

Sharing and Disclosure


Pearson may disclose personal information, as follows:

  • As required by law.
  • With the consent of the individual (or their parent, if the individual is a minor)
  • In response to a subpoena, court order or legal process, to the extent permitted or required by law
  • To protect the security and safety of individuals, data, assets and systems, consistent with applicable law
  • In connection the sale, joint venture or other transfer of some or all of its company or assets, subject to the provisions of this Privacy Notice
  • To investigate or address actual or suspected fraud or other illegal activities
  • To exercise its legal rights, including enforcement of the Terms of Use for this site or another contract
  • To affiliated Pearson companies and other companies and organizations who perform work for Pearson and are obligated to protect the privacy of personal information consistent with this Privacy Notice
  • To a school, organization, company or government agency, where Pearson collects or processes the personal information in a school setting or on behalf of such organization, company or government agency.

Links


This web site contains links to other sites. Please be aware that we are not responsible for the privacy practices of such other sites. We encourage our users to be aware when they leave our site and to read the privacy statements of each and every web site that collects Personal Information. This privacy statement applies solely to information collected by this web site.

Requests and Contact


Please contact us about this Privacy Notice or if you have any requests or questions relating to the privacy of your personal information.

Changes to this Privacy Notice


We may revise this Privacy Notice through an updated posting. We will identify the effective date of the revision in the posting. Often, updates are made to provide greater clarity or to comply with changes in regulatory requirements. If the updates involve material changes to the collection, protection, use or disclosure of Personal Information, Pearson will provide notice of the change through a conspicuous notice on this site or other appropriate way. Continued use of the site after the effective date of a posted revision evidences acceptance. Please contact us if you have questions or concerns about the Privacy Notice or any objection to any revisions.

Last Update: November 17, 2020