jerrygarciuh has asked for the wisdom of the Perl Monks concerning the following question:

I finished this last night. It is a news page script. If it receives form data it updates the page, if no data comes in it displays the current page. For security's sake the script mails the admin whenever it is used (not expected to be > once per day) and alerts the user that their IP was logged when the psswd fails. I know I am still using my screwed up mix of OO and standard formats. If business slows down I swear I will read the manpages for cgi.pm and do perltoot.
What I would like to know about this is how it should be improved for security, modularity, good perl practices etc. Please don't be shy about criticism, y'all are my only teachers who don't live in books or tute sites!
TIA
jg

#!/usr/bin/perl -w use strict; use Fcntl ':flock'; # import LOCK_* constants use CGI qw/:standard/; #then why the OO stuff, right? use CGI::Carp qw/fatalsToBrowser /; use POSIX qw(strftime); my $q = CGI->new(); #declare some vars my $mail ='weezelflesh@weezelflesh.com'; my $path_to_text="/home/weezelflesh/www/news/body.txt"; my $path_to_header="/home/weezelflesh/www/news/header.html"; my ($time_words,$topic,$words,$pass,$name,$user); my $url = "http://www.weezelflesh.com/cgi-bin/newspage.cgi"; my $goodpass="********"; parse_form(); show_news(); get_time(); if ($pass ne $goodpass){ bad_hacker_no_cookie(); }else { post_new_news(); } mail_routine(); exit; sub parse_form { my %FORM = $q->Vars('topic','words','pass','name'); $name= $q->param('name')|| "Sludge Factor"; chomp($name); $name =~ s/\s/_/g; $words = $q->param('words'); $words =~ s/\r\n/<BR>/g; $topic= $q->param('topic'); chomp($topic); $user = $ENV{'REMOTE_ADDR'}; chomp($pass = $q->param('pass')); return [$topic,$words,$pass,$name,$user]; } sub get_time { my ($hour,$suffix); $hour= strftime ("%H", localtime ()); if ($hour >= 12) { $suffix="PM"; } else { $suffix = "AM"; } if ($hour > 12) { $hour -= 12; } $time_words= strftime (" at $hour:%M $suffix on %m\/%d\/%Y \n", lo +caltime ()); $time_words; } sub post_new_news { open (FH, "+< $path_to_text") or die "where's the damn file? : $!" +; flock (FH,LOCK_EX) or die "Couldn't flock: $!"; my @old_file = <FH>; seek FH, 0, 0; truncate (FH,0) or die "Can't truncate: $!"; print FH "<tr> <td>&nbsp;</td> <td><BR> <H3><font color= #008080> +$topic <br>"; print FH "posted by <a href=mailto:$name\@weezelflesh.com>$name</ +a>"; print FH "$time_words."; print FH "</H3> </font></td></TR>\n"; print FH "<TR><td>&nbsp;</td> <TD> $words <BR><BR> </TD> </TR>\n\ +n\n"; print FH @old_file; flock(FH,LOCK_UN); #unlock the file close FH or die "close damn you : $!"; print "Location: $url\n\n"; } sub bad_hacker_no_cookie { print "Content-type: text/html\n\n"; print "<HTML> \n"; print "<body bgcolor=000000 text=99CCCC> \n"; print "<CENTER><H1><font face=Arial>!Unauthorized Access!\n"; print "<BR>"; print "<font color = red>Hey bub, stop messin' with my News Page! +\n"; print "<BR>You are at IP address $user !"; print "</H1></CENTER></font></body>"; exit 0; } sub mail_routine { if ($pass ne $goodpass) { open MAIL,"|mail $mail" or die "mail problem : $!"; print MAIL "$user tried to get in using $pass for a password. +\n"; close MAIL; } else { open MAIL,"|mail $mail" or die "mail problem : $!"; print MAIL "$user changed the menu $words.\n"; close MAIL; } } sub show_news { #This sub produces the current news page if ($topic eq "" && $words eq "") { open (FTXT, "$path_to_text") or die "where's the text file? : +$!"; my @text_file = <FTXT>; open (FHTML, "$path_to_header") or die "where's the html file? + : $!"; my @html_file = <FHTML>; print "Content-type: text/html\n\n"; print @html_file; print @text_file; print "</table></div></body></html> \n"; exit; } else { return; } }
_____________________________________________________
If it gets a little bit out of hand sometimes, don't let it fool you into thinkin' you don't care.TvZ

Replies are listed 'Best First'.
Re: Critque my second script for me?
by Hero Zzyzzx (Curate) on Nov 17, 2001 at 19:59 UTC

    Not too shabby. Nice use of subroutines, stick with organizing your code thusly and you'll be way ahead of the game.

    Couple of things

    Your return statement in parse_form() doesn't do anything, because you've declared those variables in a higher scope. You can take it out and your script will work the same.

    I didn't see a place in your script where you used the standard interface to CGI.pm. Am I missing something? You could probably ditch the :standard use.

    Globals can make things hard to debug, and will cut down on your ability to reuse your code. The way I like to think about subroutines is that they are black boxes- you pass to it directly what it needs, and you get from it exactly what you need. Examples?

    my($topic,$words,$pass,$name,$user)=parse_form($q); sub parse_form { my $q=shift; #get the value from @_ my $name= $q->param('name')|| "Sludge Factor"; $name =~ s/\s/_/g; my $words = $q->param('words'); $words =~ s/\r\n/<BR>/g; # don't think this will work like you exp +ect with *nix, #but that might not matter. my $topic= $q->param('topic'); my $user = $ENV{'REMOTE_ADDR'}; # or my $user=$q->remote_addr(); my $pass=$q->param('pass'); return [$topic,$words,$pass,$name,$user]; }
    or,
    sub parse_form { my $q=shift; #get the value from @_ my $name= $q->param('name')|| "Sludge Factor"; $name =~ s/\s/_/g; my $words = $q->param('words'); $words =~ s/\r\n/<BR>/g; # don't think this will work like you exp +ect with *nix, #but that might not matter. return [$q->param('topic'), $words, $q->param('pass'), $name, $q-> +remote_addr()]; }
    because you don't need chomps, CGI.pm will do it for you.

    Now this subroutine can standalone, because we know exactly what goes in by looking at the first few statements, and exactly what comes out by looking at the return. This will allow you to reuse it elsewhere by just cutting and copying it, and when you get into OOP you'll be ahead of the game.

    A note- using CGI to parse forms is a little easier than you're making it. . .

    Keep up using strict, start thinking about subroutines as black boxes, and you're doing well. Globals are fine for smaller scripts, but when you get into larger apps, you'll have maintenace problems ('where does $poo come from again?').

    If you're going to be doing a lot of CGI programming, you should take some time to learn a templatting system like HTML::Template. Once you start using it, you'll wonder how you made web apps any other way. . .

    -Any sufficiently advanced technology is
    indistinguishable from doubletalk.

Re: Critque my second script for me?
by chromatic (Archbishop) on Nov 17, 2001 at 23:52 UTC
    Solid comments from Hero. There are a couple of other (stylistic) things to consider.

    I don't care for calling exit in a subroutine. Suppose you want to log accesses where the password doesn't match. You would have to edit bad_hacker_no_cookie(), at least to remove the exit, or even to do the report. I'd rather write a new subroutine (log_password_mismatch()) and exit in the if clause.

    You don't have to unflock files. It'll happen automatically when Perl closes the file. That avoids a race condition, as well.

    It can be helpful to print the name of the file along with $! (if an open or close or flock fails) instead of a cute error message. You may remember that "WANK WANK: No such file or directory" means that the header can't be found now, but in a month that memory may be gone.

    print while <FH>; is gentler on memory than slurping a file into an array and then printing each line. If you think about it, you can avoid holding the new news file in memory with a clever use of unlink and rename.

    I don't like declaring lexical variables until they're needed. That's a style thing.

    Be careful stringifying things that don't need to be stringified. Check a couple of your open statements.

    If you must print raw HTML in big chunks, use a heredoc.

    That's a lot more than I thought I'd say when I started, but if there's anything that stands out as a warning, it's what Hero mentioned. Just because you use lexical variables doesn't mean they're not prone to the same mishaps as true globals. You're just less likely to lose the whole leg. Maybe a toe or two.

Re: Critque my second script for me?
by Jazz (Curate) on Nov 18, 2001 at 00:50 UTC

    I agree with everything that Hero Zzyzzx and chromatic noted above. I'll just throw in my 2¢ worth of suggestions, for what it's worth :)


    No spaces between subs and opening parens.

    It may make your sub behave unexpectedly. For more info, check out this node.


    Use CGI.pm for everything you can (especially if you're already using CGI.pm).

    You've imported all of the standard fuctions, but are handrolling your HTML. For example, your bad_hacker_no_cookie has:

    sub bad_hacker_no_cookie { print "Content-type: text/html\n\n"; print "<HTML> \n"; print "<body bgcolor=000000 text=99CCCC> \n"; print "<CENTER><H1><font face=Arial>!Unauthorized Access!\n"; print "<BR>"; print "<font color = red>Hey bub, stop messin' with my News Page! +\n"; print "<BR>You are at IP address $user !"; print "</H1></CENTER></font></body>"; exit 0; }

    But you can tidy it up, use the functions you've already imported, and make sure it's valid html/xhtml code at the same time by using:

    sub bad_hacker_no_cookie { print header, start_html( -bgcolor => '#000000', -text => '#99ccc'), h1( { -align => 'center' }, '!Unauthorized Access!', br(), font( { -color => 'red' }, "Hey bub, stop messin' with my News Page!", br(), "You are at IP address $user !", ), ), end_html; }

    Variable naming and grouping

    I like to set my config vars (using mnemonic names) in either a hash or href. It helps me to identify them at a glance, especially when there are a lot of config settings.

    $mail, in a large program with lots of settings may be confusing (was that the contents of a message, the path to the mail program, the type of mailer to use, an email address, etc.), especially when you pick up the script to modify it in 6 months. But if you had <NOBR>$cfg->{'email_notices_to'}</NOBR>, you would know exactly was it was for and the data it contains :)


    Print from filehandle instead

    If you're just opening a file to be output to the browser, you don't need to toss the whole file into memory. Here's the code from your show_news sub:

    if ($topic eq "" && $words eq "") { open (FTXT, "$path_to_text") or die "where's the text file? : $!"; my @text_file = <FTXT>; open (FHTML, "$path_to_header") or die "where's the html file? : $!"; my @html_file = <FHTML>; print "Content-type: text/html\n\n"; print @html_file; print @text_file; print "</table></div></body></html> \n"; exit;

    Instead of this, you can use:

    print header; open( FHTML, $path_to_header ) or die "where's the html file? $path_to +_header : $!\n"; print while <FHTML>; close FHTML or die "Blahblah $path_to_header $!\n"; #you forgot to cl +ose these filehandles open( FTXT, $path_to_text ) or die "where's the text file? $path_to_te +xt : $!\n"; print while <FTXT>; close FTXT or die "Blahblah $path_to_text $!\n"; print '</TABLE></DIV></BODY></HTML>';
    Hope at least some of this helps,
    Jasmine
My own idea of what I should do next:
by jerrygarciuh (Curate) on Nov 17, 2001 at 19:39 UTC
    As if I hadn't posted enough code, I decided to mention that these 2 editor subs which create a form with the previous posts in a textarea for editing and resubmission are the next scheduled improvement once I write a way for the script to know which form it is processing the new post or the edited post.
    Perl is fun!
    Peace
    jg
    if ($words eq "" && $edit eq "yes") { make_form(); } else { send_form(); } sub make_form { open (FTXT, "$path_to_text") or die "where's the text file? : $!"; my @text_file = <FTXT>; print "Content-type: text/html\n\n"; print "<head><title>!!Edit News!!</title></head>\n"; print "<body bgcolor=#FFFFFF><center><h1>!!Edit News!!</H1></cente +r>\n"; print "<form name=form1 method=post "; print "action=http://www.weezelflesh.com/cgi-bin/newseditor.cgi>\n +"; print "<center><p><textarea name=words cols=80 rows=10 wrap=hard>" +; print "@text_file</textarea></p><p><p>Password:<input type=passwor +d name=pass>\n"; print "</p><p><input type=submit name=Submit value=Edit the News!> +"; print "</p></center></form>"; print "</body></html> \n"; } sub send_form { if ($pass eq $goodpass){ open (FTXT, "+< $path_to_text") or die "where's the damn file? + : $!"; flock (FTXT,LOCK_EX) or die "Couldn't flock: $!"; seek (FTXT, 0, 0); truncate (FTXT,0) or die "Can't truncate: $!"; print FTXT $words; flock(FTXT,LOCK_UN); #unlock the file close FTXT or die "close damn you : $!"; print "Location: $url\n\n"; exit; } else { print "Content-type: text/html\n\n"; print "<HTML> \n"; print "<body bgcolor=000000 text=99CCCC> \n"; print "<CENTER><H1><font face=Arial>!Unauthorized Access!\ +n"; print "<BR>"; print "<font color = red>Hey bub, stop messin' with my New +s Page!\n"; print "<BR>You are at IP address $user !"; print "</H1></CENTER></font></body>"; open MAIL,"|mail $mail" or die "mail problem : $!"; print MAIL "$user tried to get in using $pass for a passwo +rd.\n"; close MAIL; exit 0; } }
    _____________________________________________________
    If it gets a little bit out of hand sometimes, don't let it fool you into thinkin' you don't care.TvZ