diff options
author | Miquel Lionel <lionel@les-miquelots.net> | 2021-04-11 18:20:41 +0100 |
---|---|---|
committer | Miquel Lionel <lionelmiquel@sfr.fr> | 2021-04-11 18:20:41 +0100 |
commit | 8cbc83817dbafd2ac26d9834009e6cfa3d58b3d7 (patch) | |
tree | a35776798fd2a5edc33c989a0f4b0b38c927eb93 | |
parent | 734fe365eac84cc94d5b814df617f24f7d597f91 (diff) | |
download | gpigeon-8cbc83817dbafd2ac26d9834009e6cfa3d58b3d7.tar.gz gpigeon-8cbc83817dbafd2ac26d9834009e6cfa3d58b3d7.zip |
Cookie based login added
- the cookie last 1 year and is set upon login
- deleted when pressing "Disconnect" button
- updated the config.mk and Makefile for COOKIES_DIR variable
- camel-cased the subroutines. I like it that way, it stands out more
compared to variables
- reinserted some vars back into link-tmpl.cgi, it wasn't making sense
to have them in gpigeon-template.cgi
- no more 'Unknown' link asker in the links table: if the string is a valid
email address, we link to the file in the table.
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | README.md | 53 | ||||
-rw-r--r-- | config.mk | 1 | ||||
-rwxr-xr-x | gpigeon-template.cgi | 264 | ||||
-rw-r--r-- | link-tmpl.cgi | 39 |
5 files changed, 230 insertions, 129 deletions
@@ -14,7 +14,7 @@ install: ARGON2ID_HASH="`tail -1 genpass.txt`"; \ fi sed -e 's|argon2id_hash_goes_here|$(ARGON2ID_HASH)|g' -i $(DESTDIR)$(WWWPREFIX)/cgi-bin/gpigeon.cgi - sed -e "s|cookies_dir_goes_here|${PREFIX}/cookies|g" -i $(DESTDIR)$(WWWPREFIX)/cgi-bin/gpigeon.cgi + sed -e "s|cookies_dir_goes_here|$(COOKIES_DIR)|g" -i $(DESTDIR)$(WWWPREFIX)/cgi-bin/gpigeon.cgi sed -e "s|link_template_path_goes_here|$(LINK_TEMPLATE_PATH)|g" -i $(DESTDIR)$(WWWPREFIX)/cgi-bin/gpigeon.cgi sed -e "s|msg_char_limit_goes_here|$(MSG_FORM_CHAR_LIMIT)|g" -i $(DESTDIR)$(LINK_TEMPLATE_PATH) cp -f link-tmpl.cgi $(DESTDIR)$(LINK_TEMPLATE_PATH) @@ -1,49 +1,58 @@ GPIGEON ======== -Gpigeon generate links for a GPG user to be sent to a non technical person (or -not a GPG user) so they can send you encrypted mail messages via a one-time -web link. +Gpigeon generate links for a non technical person or someone not familiar +with GPG, so they can send you encrypted mails via a one-time +web form. Feels of déjàvu ? I was inspired by https://hawkpost.co but wasn't really interested in the multi-user perspective and managing a database. -Features +Overview ======== - * Single user: no database required. + * Single user. * One-time GPG form: after sending the encrypted message, the generated form self-destructs. - * A table of the links generated is visible when you connect so you can - keep track. You can also delete link individually or all at once. + * Cookie based login. If you block cookies, it will switch back to + hidden fields so you can still login, manage and create links. + * A table of the links generated is visible after connecting so you can + keep track of what has been created. You can also delete links + individually, or all at once. + * No javascript used for the moment. Dependencies ============ -You need perl and the following modules and my perl version is v5.32.0, YMMV: +You will need perl and the following modules and my perl version is v5.32.0, YMMV: - * Net:SSLeay - * Digest::SHA - * Email::Valid - * String::Random * HTML::Entities - * CGI (I'm planning on removing it, I use it just for the - convenient param function.) - * CGI::Carp (primarly for debugging, comment the line in - gpigeon-template.cgi if you won't need it) + * CGI + * CGI::Carp + * CGI::Cookies + * Crypt::Argon2 + * GPG + * Net:SSLeay * Net::SMTP * Net::SMTPS - * GPG + * Email::Valid + * String::Random Having a webserver with CGI support or a separate CGI engine is needed. I'm using nginx and fcgiwrap. A note on Net::SMTP and Net:SMTPS dependencies: if you have a mailserver well -configured with OpenDKIM and the likes (so your chances to get your mail -treated as spam is greatly reduced) you could replace these two deps with -Mail::Sendmail then comment and uncomment some lines in <gpigeon-template.cgi>. +configured with SPF and OpenDKIM (so your chances to get your mail +treated as spam is greatly reduced) you should set the `HAS_MAILSERVER` +variable to 1 in the config.mk file. Installation ============ -Look in the [gpigeon-template.cgi](https://git.les-miquelots.net/gpigeon/plain/gpigeon-template.cgi) source code you should figure things out quickly. -Hint: look for variables values ending in 'goes_here'. +Edit the config.mk file to customize the installation to your needs, and then +execute: +`sudo make` + +You should also look in the +[gpigeon-template.cgi](https://git.les-miquelots.net/gpigeon/plain/gpigeon-template.cgi) +and [link-tmpl.cgi](https://git.les-miquelots.net/gpigeon/plain/link-tmpl.cgi) source code, you should figure things out quickly. +**Hint**: look for variables values ending in _goes_here_. @@ -2,6 +2,7 @@ # paths PREFIX = /usr/share/webapps/gpigeon +COOKIES_DIR = $(PREFIX)/cookies _GPG_HOMEDIR = $(PREFIX)/gnupg LINK_TEMPLATE_PATH = $(PREFIX)/link-tmpl.cgi WWWPREFIX = /var/www/gpigeon diff --git a/gpigeon-template.cgi b/gpigeon-template.cgi index 277ae0a..5470dfd 100755 --- a/gpigeon-template.cgi +++ b/gpigeon-template.cgi @@ -6,23 +6,79 @@ use Crypt::Argon2 qw(argon2id_verify); use Email::Valid; use String::Random; use CGI qw(param); -#use CGI::Session; +use CGI::Cookie; use CGI::Carp qw(fatalsToBrowser); -sub notif_if_defined{ - my $notif = shift; - if (defined $notif){ - return $notif; +sub ValidCookie { + my $client_login_cookie = shift; + my $dir = shift; + if (not defined $client_login_cookie){ + return; + } + my $cookie_line = undef; + my $filename = $client_login_cookie->value; + if ($filename =~ /^([\w]+)$/){ + $filename = $1; + } + else{ + return; + } + my $login_cookiefile = "$dir/$filename.txt"; + if (-e $login_cookiefile){ + open my $in, '<', $login_cookiefile or die "can't read file: $!"; + for(1){ + my $cookie_line = readline $in; + } + close $in; + if (not defined $cookie_line){ + return; + } } else{ - return '<!-- undef -->'; + return; + } + + if ($client_login_cookie == $cookie_line){ + return 1; + } + return; + +} + +sub LoginCookieGen { + my $id_cookie = shift; + my $dir = shift; + my $str_rand_obj = String::Random->new; + my $val = $str_rand_obj->randregex('\w{64}'); + my $cookiefile = "$dir/$val.txt"; + my $new_login_cookie = CGI::Cookie->new( + -name => 'id', + -value => $val, + -expires => '+1y', + '-max-age' => '+1y', + -domain => ".$ENV{'SERVER_NAME'}", + -path => '/', + -secure => 1, + -httponly => 1, + -samesite => 'Strict', + ) or die "Can't create cookie: $!"; + if (not defined $id_cookie){ + open my $out, '>', $cookiefile or die "Can't write to $cookiefile: $!"; + print $out "$new_login_cookie"; + close $out; + print "Set-Cookie: $new_login_cookie\n"; } } -sub untaint_cgi_filename { +sub EscapeArobase { + my $escapedmailaddress = shift; + $escapedmailaddress =~ s/@/\\@/; + return $escapedmailaddress; +} + +sub UntaintCGIFilename { my $filename = shift; if ($filename =~ /^([-\@\w.\/]+)$/) { - #data untainted $filename = $1; } else { @@ -32,16 +88,27 @@ sub untaint_cgi_filename { return $filename; } +sub NotifIfDefined{ + my $notif = shift; + if (defined $notif){ + return $notif; + } + else{ + return '<!--undef notif-->'; + } +} + +my ($linkgen_notif, $link_asker, $mailisok_notif, $deletion_notif, + $checkedornot, $hidden_pwfield, $id_cookie, + $delete_id_cookie, $idval, $refresh_form) = undef; +my @created_links = (); delete @ENV{qw(IFS PATH CDPATH BASH_ENV)}; $ENV{'PATH'} = '/usr/bin'; -my $cgi_query_get = CGI->new; -my @created_links = (); -my ($linkgen_notif, $mailisok_notif, $deletion_notif) = undef; -my $LINK_TEMPLATE_PATH='/usr/share/webapps/gpigeon/link-template.pl'; # this is the file where the SMTP and mail address values goes -my $HOSTNAME = $ENV{'SERVER_NAME'}; -my $msg_form_char_limit = 3000; -my $PASSWD_HASH = q{password_hash_goes_here}; #argon2id hash format -my $PASSWD = $cgi_query_get->param('password'); +my $hostname = $ENV{'SERVER_NAME'}; + +my $argon2id_hash = q{argon2id_hash_goes_here} +my $cookies_dir = q{cookies_dir_goes_here}; +my $link_template_path = q{link_template_path_goes_here}; my %text_strings = ( addr => 'Address', @@ -51,20 +118,20 @@ my %text_strings = ( create_link_btn => 'Generate link', delete_link_btn_text => 'Delete', delete_links_btn_text => 'Delete all links', - disconnect_btn_text => 'Disconnect', + logout_btn_text => 'Logout', here => 'here', - link_web_title => 'One time GPG messaging form', login => 'Login', + link_asker_field_label => q{Asker's mail :}, + link_web_title => 'One time GPG messaging form', link_del_ok => 'Successful removal !', link_legend_textarea =>'Type your message below :', link_send_btn => 'Send', - link_generated_ok => 'Generated a link for', + link_ok_for => 'Generated a link for', link_del_failed => 'Deletion failed and here is why : ', - notif_login_failure => 'Cannot login. Check if your username and password match.' mailto_body => 'Your link is ', mailto_subject => 'Link to your one time GPG messaging form', - msg_too_long => 'Cannot send message : message length must be under ' .$msg_form_char_limit . ' characters.', - msg_empty => 'Cannot send message : message is empty. You can type up to ' . $msg_form_char_limit . ' characters.', + notif_login_failure => 'Cannot login. Check if your username and password match.' + passwd_label => 'Password :', refresh_btn_text => 'Refresh', type_msg_below => 'Type your message below', theader_link => 'Link', @@ -73,17 +140,62 @@ my %text_strings = ( web_title => 'GPIGEON.CGI: generate one time GPG messaging links !', web_greet_msg => 'Hi and welcome.', ); +my $cgi_query_get = CGI->new; +my $pw = $cgi_query_get->param('password'); +my $logout = $cgi_query_get->param('logout'); +my %cur_cookies = CGI::Cookie->fetch; +$id_cookie = $cur_cookies{'id'}; + +if (not defined $id_cookie){ + $hidden_pwfield = qq{<input type="hidden" name="password" value="$pw">}; + $refresh_form = qq{<form method="POST"> + $hidden_pwfield + <input type="submit" value="$text_strings{refresh_btn_text}"> + </form>}; +} +else{ + $refresh_form = qq{<form method="GET"> + <input type="submit" value="$text_strings{refresh_btn_text}"> + </form>}; + $idval = $id_cookie->value; -if (argon2id_verify($PASSWD_HASH,$PASSWD)){ - my $hidden_pwfield = qq{<input type="hidden" name="password" value="$PASSWD">}; + if ($idval =~ /^([\w]+)$/){ + $idval = $1; + } + + if ($logout){ + $delete_id_cookie = CGI::Cookie->new( + -name => 'id', + -value => $idval, + -expires => '-1d', + '-max-age' => '-1d', + -domain => ".$hostname", + -path => '/', + -secure => 1, + -httponly => 1, + -samesite => 'Strict', + ); + my $f = "$cookies_dir/$idval.txt"; + if (-e "$f"){ + unlink "$f" or die "Can't delete file :$!"; + } + print "Set-Cookie: $delete_id_cookie\n"; + } +} + +print "Cache-Control: no-store, must-revalidate\n"; +if (ValidCookie($id_cookie, $cookies_dir) or argon2id_verify($argon2id_hash,$pw)){ + + LoginCookieGen($id_cookie, $cookies_dir); + if (defined $cgi_query_get->param('supprlien')){ my $pending_deletion = $cgi_query_get->param('supprlien'); my $linkfile_fn = "./l/$pending_deletion"; - if (unlink untaint_cgi_filename($linkfile_fn)){ - $deletion_notif=qq{<span style="color:green">$text_strings{link_del_ok}</span>}; + if (unlink UntaintCGIFilename($linkfile_fn)){ + $deletion_notif = qq{<span style="color:green">$text_strings{link_del_ok}</span>}; } else { - $deletion_notif=qq{<span style="color:red">$text_strings{link_del_failed} $linkfile_fn : $!</span>}; + $deletion_notif = qq{<span style="color:red">$text_strings{link_del_failed} $linkfile_fn : $!</span>}; } } @@ -91,56 +203,47 @@ if (argon2id_verify($PASSWD_HASH,$PASSWD)){ opendir my $link_dir_handle, './l' or die "Can't open ./l: $!"; while (readdir $link_dir_handle) { if ($_ ne '.' and $_ ne '..'){ - my $linkfile_fn = "./l/$_"; - unlink untaint_cgi_filename($linkfile_fn) or die "$!"; - $deletion_notif=qq{<span style="color:green">$text_strings{link_del_ok}</span>}; + unlink UntaintCGIFilename("./l/$_") or die "$!"; + $deletion_notif = qq{<span style="color:green">$text_strings{link_del_ok}</span>}; } } closedir $link_dir_handle; } if (defined $cgi_query_get->param('mail')){ - my $link_asker = scalar $cgi_query_get->param('mail'); + $link_asker = scalar $cgi_query_get->param('mail'); if ( Email::Valid->address($link_asker) ){ $mailisok_notif = qq{<span style="color:green">$text_strings{addr} $link_asker $text_strings{addr_ok}</span>}; - my $escaped_link_asker = escape_arobase($link_asker); + my $escaped_link_asker = EscapeArobase($link_asker); my $str_rand_obj = String::Random->new; - my $random_fn = $str_rand_obj->randregex('\w{64}'); - my $GENERATED_FORM_FILENAME = "$random_fn.cgi"; - my $HREF_LINK = "https://$HOSTNAME/cgi-bin/l/$GENERATED_FORM_FILENAME"; - my $LINK_PATH = "./l/$GENERATED_FORM_FILENAME"; + my $generated_form_filename = $str_rand_obj->randregex('\w{64}') . '.cgi'; + my $href = "https://$hostname/cgi-bin/l/$generated_form_filename"; + my $link_path = "./l/$generated_form_filename"; - open my $in, '<', $LINK_TEMPLATE_PATH or die "Can't read link template file: $!"; - open my $out, '>', $LINK_PATH or die "Can't write to link file: $!"; + open my $in, '<', $link_template_path or die "Can't read link template file: $!"; + open my $out, '>', $link_path or die "Can't write to link file: $!"; while( <$in> ) { s/{link_user}/{$link_asker}/g; - s/{link_filename}/{$GENERATED_FORM_FILENAME}/g; - s/{msg_too_long}/$text_strings{msg_too_long}/g; - s/{msg_empty}/$text_strings{msg_empty}/g; - s/{msg_form_char_limit}/$msg_form_char_limit/g; s/{link_web_title}/$text_strings{link_web_title}/g; s/{link_send_btn}/$text_strings{link_send_btn}/g; s/{type_msg_below}/$text_strings{type_msg_below}/g; print $out $_; } close $in or die; - chmod(0755,$LINK_PATH) or die; + chmod(0755,$link_path) or die; close $out or die; - - $linkgen_notif = qq{<span style="color:green">$text_strings{link_generated_ok} $link_asker: </span><br><a href="$HREF_LINK">$HREF_LINK</a>}; + $linkgen_notif = qq{<span style="color:green">$text_strings{link_ok_for} $link_asker: </span><br><a href="$href">$href</a>}; } else{ $mailisok_notif = qq{<span style="color:red">$text_strings{addr} $link_asker $text_strings{addr_nok}.</span>}; } } - opendir my $link_dir_handle, './l' or die "Can't open ./l: $!"; while (readdir $link_dir_handle) { if ($_ ne '.' and $_ ne '..'){ my $linkfile_fn = $_; - my $link_asker = undef; if (open my $linkfile_handle , '<', "./l/$linkfile_fn"){ for (1..2){ $link_asker = readline $linkfile_handle; @@ -149,23 +252,20 @@ if (argon2id_verify($PASSWD_HASH,$PASSWD)){ } close $linkfile_handle; - if (not defined $link_asker){ - $link_asker = $text_strings{unknown}; + if (Email::Valid->address($link_asker){ + push @created_links, + qq{<tr> + <td><a href="/cgi-bin/l/$linkfile_fn">$text_strings{here}</a></td> + <td><a href="mailto:$link_asker?subject=$text_strings{mailto_subject}&body=$text_strings{mailto_body} http://$hostname/cgi-bin/l/$linkfile_fn">$link_asker</a></td> + <td> + <form method="POST"> + $hidden_pwfield + <input type="hidden" name="supprlien" value="$linkfile_fn"> + <input type="submit" value="$text_strings{delete_link_btn_text}"> + </form> + </td> + </tr>}; } - #create links table html - push @created_links, - qq{<tr> - <td><a href="/cgi-bin/l/$linkfile_fn">ici</a></td> - <td><a href="mailto:$link_asker?subject=$text_strings{mailto_subject}&body=$text_strings{mailto_body} http://$HOSTNAME/cgi-bin/l/$linkfile_fn">$link_asker</a></td> - <td> - <form method="POST"> - <input type="hidden" name="supprlien" value="$linkfile_fn"> - <input type="hidden" name="password" value="$PASSWD"> - <input type="submit" value="$text_strings{delete_link_btn_text}"> - </form> - </td> - </tr>}; - } else { close $linkfile_handle; @@ -187,32 +287,29 @@ if (argon2id_verify($PASSWD_HASH,$PASSWD)){ </head> <body> <p>$text_strings{web_greet_msg}</p> - <form method="POST"> - <input type="hidden" name="password" value="0"> - <input type="submit" value="$text_strings{disconnect_btn_text}"> - </form> - <form method="POST"> - $hidden_pwfield - <input type="submit" value="$text_strings{refresh_btn_text}"> + <form method="GET"> + <input type="hidden" name="logout" value="1"> + <input type="submit" value="$text_strings{logout_btn_text}"> </form> + $refresh_form <hr> <br> <form method="POST"> $hidden_pwfield - Mail de la personne:<br> + $text_strings{link_asker_field_label}<br> <input tabindex="1" type="text" name="mail"> <input tabindex="2" type="submit" value="$text_strings{create_link_btn}"> </form>}, - notif_if_defined($mailisok_notif), + NotifIfDefined($mailisok_notif), '<br>', - notif_if_defined($linkgen_notif), + NotifIfDefined($linkgen_notif), qq{<hr> <form method="POST"> $hidden_pwfield <input type="hidden" name="supprtout"> <input type="submit" value="$text_strings{delete_links_btn_text}"> </form>}, - notif_if_defined($deletion_notif), + NotifIfDefined($deletion_notif), qq{<table> <tr> <th>$text_strings{theader_link}</th> @@ -224,8 +321,8 @@ if (argon2id_verify($PASSWD_HASH,$PASSWD)){ </body> </html>}; } -else { - print 'Content-type: text/html',"\n\n", +else{ + print "Content-Type: text/html\n\n", qq{<!DOCTYPE html> <html> <head> @@ -236,21 +333,14 @@ else { </head> <body> <form action="/cgi-bin/gpigeon.cgi" method="POST"> - <h1 style="text-align:center">GPIGEON</h1> - Mot de passe : <input type="password" name="password"><br> + <h1>GPIGEON</h1> + <label for="pwfield"> $text_strings{passwd_label}:</label> + <input id="pwfield" type="password" name="password"><br> <input type="submit" value="$text_strings{login}"> </form> - - <p><a + <p> <a href="http://git.les-miquelots.net/gpigeon" title="gpigeon download link" alt="gpigeon download link">Source code here.</a> It is similar to <a href="https://hawkpost.co/">hawkpost.co</a>.</p> - - <a href="https://xkcd.com/538"><img id="crypto_secu" - src="security.png" title="XKCD fait redescendre les nerds du - chiffrement sur terre (xkcd.com/538)" alt="BD de XKCD faisant redescendre les - nerds du chiffrement sur terre"></a> - </body> - </html>}; } diff --git a/link-tmpl.cgi b/link-tmpl.cgi index 2c51268..b2fe3b3 100644 --- a/link-tmpl.cgi +++ b/link-tmpl.cgi @@ -1,7 +1,5 @@ #! /usr/bin/perl -wT my $linkuser = q{link_user}; -my $linkfilename = q{link_filename}; - use warnings; use strict; use GPG; @@ -10,30 +8,31 @@ use CGI qw(param); $ENV{'PATH'}="/usr/bin"; delete @ENV{qw(IFS PATH CDPATH BASH_ENV)}; -sub escape_arobase { +sub EscapeArobase { my $escapedmailaddress = shift; $escapedmailaddress =~ s/@/\\@/; return $escapedmailaddress; } -my $HAS_MAILSERVER = 0; -my $mymailaddr = q{your mail address goes here}; -my $mymail_gpgid = q{your gpg id in the 0xlong form goes here}; #0xlong keyid form -my $mailsender = q{mail address sending encrypted text goes here. recommended to be different from $mymailaddr}; -my $mailsender_smtp = q{your SMTP mail domain name goes here}; -my $mailsender_port = q{your SMTP port goes here}; -my $mailsender_pw = q{password for $mailsender address goes here}; -my $GPG_HOMEDIR = '/usr/share/webapps/gpigeon/gnupg/'; +my $HAS_MAILSERVER = q{has_mailserver_goes_here}; +my $msg_form_char_limit = q{msg_char_limit_goes_here}; +my $mymailaddr = q{your_addr_goes_here}; +my $mymail_gpgid = q{gpgid_goes_here}; #0xlong keyid form +my $mailsender = q{sender_addr_goes_here}; +my $mailsender_smtp = q{smtp_domain_goes_here}; +my $mailsender_port = q{smtp_port_goes_here}; +my $mailsender_pw = q{sender_pw_goes_here}; +my $GPG_HOMEDIR = q{gpg_homedir_goes_here}; my $cgi_query_get = CGI->new; my $msg_form = $cgi_query_get->param('msg'); my $length_msg_form = length $msg_form; my ($enc_msg, $error_processing_msg) = undef; -if (defined $length_msg_form and $length_msg_form > {msg_form_char_limit}){ - $error_processing_msg = q{<span style="color:red"><b>{msg_too_long}</b></span>}; +if (defined $length_msg_form and $length_msg_form > $msg_form_char_limit){ + $error_processing_msg = qq{<span style="color:red"><b>Cannot send message : message length must be under $msg_form_char_limit characters.</b></span>}; } elsif (defined $length_msg_form and $length_msg_form eq 0 ){ - $error_processing_msg = q{<span style="color:red"><b>{msg_empty}</b></span>}; + $error_processing_msg = qq{<span style="color:red"><b>Cannot send message : message is empty. You can type up to $msg_form_char_limit characters.</b></span>}; } else { if (defined $length_msg_form and $ENV{REQUEST_METHOD} eq 'POST'){ @@ -55,8 +54,8 @@ else { use Net::SMTP; use Net::SMTPS; my $smtp = Net::SMTPS->new($mailsender_smtp, Port => $mailsender_port, doSSL => 'ssl', Debug_SSL => 0); - my $mymailaddr_escaped = escape_arobase{$mymailaddr}; - my $mailsender_escaped = escape_arobase($mailsender); + my $mymailaddr_escaped = EscapeArobase{$mymailaddr}; + my $mailsender_escaped = EscapeArobase($mailsender); $smtp->auth($mailsender, $mailsender_pw) or die; $smtp->mail($mailsender) or die "Net::SMTP module has broke: $!."; @@ -72,8 +71,10 @@ else { else { die $smtp->message(); } - - unlink "../l/$linkfilename"; + my $f = $0; + if ($f =~ /^([\w]+)$/){ + unlink "$1"; + } print "Location: /merci/index.html\n\n"; } } @@ -91,7 +92,7 @@ print qq{<!DOCTYPE html> <body> <p>{type_msg_below}:</p> <form method="POST"> - <textarea wrap="off" cols="50" rows="30" name="msg"></textarea><br> + <textarea id="msg" wrap="off" cols="50" rows="30" name="msg"></textarea><br> }; if (defined $error_processing_msg){ printf $error_processing_msg; |