File Coverage

blib/lib/CGI/AuthRegister.pm
Criterion Covered Total %
statement 18 504 3.5
branch 0 202 0.0
condition 0 39 0.0
subroutine 6 40 15.0
pod 2 33 6.0
total 26 818 3.1


line stmt bran cond sub pod time code
1             #
2             #+
3             # file: AuthRegister.pm
4             # CGI::AuthRegister - Simple CGI Authentication and Registration in Perl
5             # (c) 2012-14 Vlado Keselj http://web.cs.dal.ca/~vlado
6             # $Date: $
7             # $Id: $
8             #-
9              
10             package CGI::AuthRegister;
11 1     1   5558 use strict;
  1         2  
  1         43  
12             #
13             #+
14 1     1   5 use vars qw($NAME $ABSTRACT $VERSION);
  1         3  
  1         80  
15             $NAME = 'AuthRegister';
16             $ABSTRACT = 'Simple CGI Authentication and Registration in Perl';
17             $VERSION = '1.0';
18             #-
19 1     1   807169 use CGI qw(:standard);
  1         29679  
  1         9  
20             # Useful diagnostics:
21             # use CGI qw(:standard :Carp -debug);
22             # use CGI::Carp 'fatalsToBrowser';
23             # use diagnostics; # verbose error messages
24             # use strict; # check for mistakes
25 1     1   5172 use Carp;
  1         8  
  1         115  
26             require Exporter;
27 1     1   6 use vars qw(@ISA @EXPORT);
  1         2  
  1         107  
28             @ISA = qw(Exporter);
29             @EXPORT = qw($Error $SessionId $SiteId $SiteName $User
30             $UserEmail $SendLogs $LogReport
31             analyze_cookie header_delete_cookie header_session_cookie
32             import_dir_and_config login logout
33             require_https require_login send_email_reminder
34             get_user get_user_by_userid set_new_session store_log
35             );
36              
37 1         8529 use vars qw($Email_from $Email_bcc $Error $ErrorInternal $LogReport $Sendmail
38 1     1   5 $Session $SessionId $SiteId $SiteName $Ticket $User $UserEmail $SendLogs);
  1         3  
39             # $Error = ''; # Appended error messages, OK to be sent to user
40             # $ErrorInternal = ''; # Appended internal error messages, intended
41             # for administrator
42             # $LogReport = ''; # Collecting some important log events if needed
43             # $Session = ''; # Session data structure
44             # $SessionId = ''; # Session identifier, generated
45             $SiteId = 'Site'; # Site identifier, used in cookies and emails
46             $SiteName = 'Site'; # Site name, can include spaces
47             # $Ticket = ''; # Session ticket for security, generated
48             # $User = ''; # User data structure
49             # $UserEmail = ''; # User email address
50             # $SendLogs = ''; # If true, send logs by email to admin ($Email_bcc)
51              
52             $Email_from = ''; # Example: $SiteId.' ';
53             $Email_bcc = ''; # Example: $SiteId.' Bcc ';
54              
55             $Sendmail = "/usr/lib/sendmail"; # Sendmail with full path
56              
57             # Functions
58             sub putfile($@);
59              
60             ########################################################################
61             # Configuration
62             # sets site id as the base directory name; imports configuration.pl if exists
63             sub import_dir_and_config {
64 0     0 1   my $base = `pwd`; $base =~ /\/([^\/]*)$/; $base = $1; $base =~ s/\s+$//;
  0            
  0            
  0            
65 0           $SiteId = $SiteName = $base;
66 0 0         if (-r 'configuration.pl') { package main; require 'configuration.pl'; }
  0            
67             }
68              
69             ########################################################################
70             # HTTPS Connection and Cookies Management
71              
72             # Check that the connection is HTTPS and if not, redirect to HTTPS.
73             # It must be done before script produces any output.
74             sub require_https {
75 0 0   0 0   if ($ENV{'HTTPS'} ne 'on') {
76 0           print "Status: 301 Moved Permanently\nLocation: https://$ENV{SERVER_NAME}$ENV{SCRIPT_NAME}\n\n";
77 0           exit 0;
78             }
79             }
80              
81             # If not logged in, ask for userid/email and password. Catches ?logout
82             # request as well. Allows parentheses in userid/email for login, which are
83             # removed. This allows users to use auxiliary comments with userid, so that
84             # browser can distinguish passwords.
85             sub require_login {
86 0     0 0   my $title = "Login Page for Site: $SiteId";
87 0           my $HTMLstart = "$title

$title

\n";
88 0           my $Formstart = "
";
89 0           my $Back = "Click here for the main page.\n";
90 0           my $LoginForm = "

Please log in to access the site:
\n".$Formstart.

91             "\n\n".
Userid or email:".
92             textfield(-name=>"userid")."
".
93             "Password:".password_field(-name=>"password")."
94             '
 '.
95             "
\n";
96 0           my $SendResetForm = "

If you forgot your password, it may be possible to ".

97             "retrieve it by email:
\n".$Formstart."Email: ".
98             textfield(-name=>"email_pw_send")."\n".
99             ''.
100             "\n".
101             "Or, you can reqest password to be reset and sent to you:
\n".
102             $Formstart."Email: ".textfield(-name=>"email_reset")."\n".
103             ''.
104             "\n";
105              
106 0           &analyze_cookie;
107 0 0 0       if ($SessionId ne '' && param('keywords') eq 'logout') {
108 0           logout(); print header_delete_cookie(), $HTMLstart,
  0            
109 0           "

You are logged out.\n", $LoginForm, $SendResetForm; exit; }

110              
111 0 0         if ($SessionId ne '') { print header(); return 1; }
  0            
  0            
112              
113 0           my $Request_type = param('request_type');
114              
115 0 0         if ($Request_type eq 'Login') {
    0          
    0          
116 0           my $email = param('userid'); my $password = param('password');
  0            
117 0           $email =~ s/\(.*\)//g; $email =~ s/\s+$//; $email =~ s/^\s+//;
  0            
  0            
118              
119 0 0         if (! &login($email, $password) ) { # checks for userid and email
120 0           print header(), $HTMLstart, "Unsuccessful login!\n";
121 0           print $LoginForm, $SendResetForm; exit;
  0            
122             }
123 0           else { print header_session_cookie(); return 1; }
  0            
124             }
125             elsif ($Request_type eq 'Send_Password') {
126 0           &send_email_reminder(param('email_pw_send'), 'raw');
127 0           print header(), $HTMLstart, "You should receive password reminder if ".
128             "your email is registered at this site.\n".
129             "If you do not receive remider, you can contact the administrator.\n",
130             $LoginForm, $SendResetForm;
131 0           $LogReport.=$Error; &store_log;
  0            
132 0           exit;
133             }
134             elsif ($Request_type eq 'Reset_Password') {
135 0           &reset_and_send_email_reminder(param('email_reset'), 'raw');
136 0           print header(), $HTMLstart, "You should receive new password if ".
137             "your email is registered at this site.\n".
138             "If you do not receive remider, you can contact the administrator.\n",
139 0           $LoginForm, $SendResetForm; exit;
140             }
141             else { # should be: $Request_type eq ''
142 0           print header(), $HTMLstart, $LoginForm, $SendResetForm; exit; }
  0            
143            
144 0           die; # Not supposed to be reached
145             }
146              
147             # Requires session (i.e., to be logged in). Otherwise, makes redirection.
148             sub require_session {
149 0     0 0   my %args=@_; my $defaultcgi = 'index.cgi';
  0            
150 0 0 0       if (exists($args{-redirect}) && $args{-redirect} ne '' &&
      0        
151             $args{-redirect} ne $ENV{SCRIPT_NAME})
152 0           { $defaultcgi = $args{-redirect} }
153 0 0 0       if (exists($args{-back}) && $args{-back}) {
154 0           $defaultcgi.="?goto=$args{-back}";
155             }
156 0           &analyze_cookie;
157 0 0         if ($SessionId eq '') {
158 0 0         if ($ENV{SCRIPT_NAME} eq $defaultcgi) {
159 0           print CGI::header(), CGI::start_html, CGI::h1("147-ERR:Login required");
160 0           exit; }
161 0           print CGI::redirect(-uri=>$defaultcgi); exit;
  0            
162             }
163             }
164              
165             # Prepare HTTP header. If SessionId is not empty, generate cookie with
166             # the sessionid and ticket.
167             sub header_session_cookie {
168 0     0 0   my %args=@_; my $redirect=$args{-redirect};
  0            
169 0 0         if ($redirect ne '') {
170 0 0         if ($SessionId eq '') { return redirect(-uri=>$redirect) }
  0            
171             else {
172 0           return redirect(-uri=>$redirect,-cookie=>
173             cookie(-name=>$SiteId,
174             -value=>"$SessionId $Ticket"));
175             }
176             } else {
177 0 0         if ($SessionId eq '') { return header } else
  0            
178 0           { return header(-cookie=>cookie(-name=>$SiteId,
179             -value=>"$SessionId $Ticket")) }
180             }
181             }
182              
183             # Delete cookie after logging out. Return string.
184             sub header_delete_cookie {
185 0     0 0   return header(-cookie=>cookie(-name=>$SiteId, -value=>'', -expires=>"now")) }
186              
187             # Analyze cookie to detect session, and check the ticket as well. It
188             # should be called at the beginning of a script. $SessionId and
189             # $Ticket are set to empty string if not successful. The information
190             # about the session is stored in db/sessions.d/$SessionId/session.info
191             # file. The structures $Session and $User are set if successful.
192             sub analyze_cookie {
193 0     0 1   my $c = cookie(-name=>$SiteId); # sessionid and ticket
194 0 0         if ($c eq '') { $SessionId = $Ticket = ''; return; }
  0            
  0            
195 0           ($SessionId, $Ticket) = split(/\s+/, $c);
196 0 0 0       if ($SessionId !~ /^[\w.:-]+$/ or $Ticket !~ /^\w+$/)
197 0           { $User = $SessionId = $Ticket = ''; return; }
  0            
198              
199             # check validity of session and set user variables
200 0           my $sessioninfofile = "db/sessions.d/$SessionId/session.info";
201 0 0         if (!-f $sessioninfofile) { $SessionId = $Ticket = ''; return; }
  0            
  0            
202 0           my $se = &read_db_record("file=$sessioninfofile");
203 0 0 0       if (!ref($se) or $Ticket ne $se->{'Ticket'})
204 0           { $User = $SessionId = $Ticket = ''; return; }
  0            
205 0           $Session = $se;
206 0           $UserEmail = $se->{email};
207 0           $User = &get_user_by_email($UserEmail);
208 0 0         if ($Error ne '') { $User = $SessionId = $Ticket = ''; return; }
  0            
  0            
209             }
210              
211             ########################################################################
212             # Session Management
213              
214             # params: $email, opt: pwstore type: md5 raw
215             sub reset_password {
216 0 0   0 0   my $email = shift; my $pwstore = shift; $pwstore = 'md5' if $pwstore eq '';
  0            
  0            
217 0           my $password = &random_password(6);
218 0 0         if (!-f 'db/passwords') {
219 0           putfile 'db/passwords', ''; chmod 0600, 'db/passwords' }
  0            
220 0 0         if (!&lock_mkdir('db/passwords')) { $Error.="95-ERR:\n"; return ''; }
  0            
  0            
221 0 0         local *PH; open(PH,"db/passwords") or croak($!);
  0            
222 0           my $content = '';
223 0           while () {
224 0           my ($e,$p) = split;
225 0 0         $content .= $_ if $e ne $email;
226             }
227 0           close(PH);
228 0           $content .= "$email ";
229 0 0         if ($pwstore eq 'raw') { $content.="raw:$password" }
  0 0          
230 0           elsif($pwstore eq 'md5') { $content.="md5:".md5_base64($password) }
231             #else { $content.="md5:".md5_base64($password) }
232 0           else { $content.="raw:$password" }
233 0           $content .= "\n";
234 0           putfile 'db/passwords', $content; chmod 0600, 'db/passwords';
  0            
235 0           &unlock_mkdir('db/passwords');
236 0           return $password;
237             }
238              
239             sub md5_base64 {
240 0     0 0   my $arg=shift; require Digest::MD5; return Digest::MD5::md5_base64($arg); }
  0            
  0            
241              
242             sub random_password {
243 0 0   0 0   my $n = shift; $n = 8 unless $n > 0;
  0            
244 0           my @chars = (2..9, 'a'..'k', 'm'..'z', 'A'..'N', 'P'..'Z',
245             qw(, . / ? ; : - = + ! @ $ % *) );
246 0           return join('', map { $chars[rand($#chars+1)] } (1..$n));
  0            
247             }
248              
249             # removes session file and return the appropriate HTTP header
250             sub logout {
251 0 0   0 0   if ($Session eq '') { $Error.= "217-ERR: No session to log out\n"; return; }
  0            
  0            
252 0 0         if (!-d "db/sessions.d/$SessionId") { $Error.="218-ERR: No session dir\n" }
  0            
253             else {
254 0           unlink(); rmdir("db/sessions.d/$SessionId"); }
  0            
255 0           $LogReport.=$Error."User $UserEmail logged out."; &store_log;
  0            
256 0           $Session = $SessionId = $Ticket = '';
257 0           return 1;
258             }
259              
260             # The first parameter can be an userid and email. (diff by @)
261             sub login {
262 0     0 0   my $email = shift; my $password = shift;
  0            
263 0           $email = lc $email; my $userid;
  0            
264 0 0         if ($email !~ /@/) { $userid=$email; $email=''; }
  0            
  0            
265 0 0         if ($email ne '') {
266 0 0         if (!&emailcheckok($email)) {
267 0           $Error.="242-ERR:Incorrect email address format"; return; }
  0            
268             #my $u = &get_user_by_email($email);
269 0           my $u = &get_user_unique('email', $email);
270 0 0         if ($u eq '') { $Error.='245-ERR:Email not registered'; return; }
  0            
  0            
271 0           $userid = $u->{userid};
272 0           $User = $u;
273             } else {
274 0 0         if ($userid eq '') { $Error.="249-ERR:Empty userid"; return; }
  0            
  0            
275             #my $u = &get_user_by_userid($userid);
276 0           my $u = &get_user_unique('userid', $userid);
277 0 0         if ($u eq '') { $Error.='252-ERR:Not exist-unique'; return; }
  0            
  0            
278 0           $email = $u->{email};
279 0           $User = $u;
280             }
281              
282 0 0         if (!password_check($User, $password)) {
283 0           $Error.="258:Invalid password\n"; return ''; }
  0            
284              
285 0           &set_new_session($User);
286 0           $LogReport.="User $UserEmail logged in.\n"; &store_log;
  0            
287 0           return 1;
288             }
289              
290             sub set_new_session {
291 0     0 0   my $u = shift; my $email = $u->{email};
  0            
292 0 0 0       mkdir('db', 0700) or croak unless -d 'db';
293 0 0 0       mkdir('db/sessions.d', 0700) or croak unless -d 'db/sessions.d';
294              
295 0           my $sessionid = $email."______"; $sessionid =~ /.*?(\w).*?(\w).*?(\w).*?(\w).*?(\w).*?(\w)/;
  0            
296 0           $sessionid = $1.$2.$3.$4.$5; $^T =~ /\d{6}$/; $sessionid.= "_$&";
  0            
  0            
297 0 0         if (! mkdir("db/sessions.d/$sessionid", 0700)) {
298 0   0       my $cnt=1; for(;$cnt<100 and !mkdir("db/sessions.d/${sessionid}_$cnt", 0700); ++$cnt) {}
  0            
299 0 0         croak "Cannot create sessions!" if $cnt == 100;
300 0           $sessionid = "${sessionid}_$cnt";
301             }
302 0           $SessionId = $sessionid; $Ticket = &random_name;
  0            
303 0           putfile("db/sessions.d/$SessionId/session.info",
304             "SessionId:$SessionId\nTicket:$Ticket\nemail:$email\n");
305 0           $UserEmail = $email;
306 0           return $SessionId;
307             }
308              
309             # Return 1 if OK, '' otherwise
310             sub password_check {
311 0     0 0   my $u = shift; my $password = shift; my $pwstored = &find_password($u->{email});
  0            
  0            
312 0 0         if ($pwstored =~ /^raw:/) {
313 0 0         $pwstored=$'; return ( ($pwstored eq $password) ? 1 : '' ); }
  0            
314 0 0         if ($pwstored =~ /^md5:/) {
315 0 0         $pwstored=$'; return ( ($pwstored eq md5_base64($password)) ? 1 : ''); }
  0            
316 0           $Error.="316-ERR:PWCheck error($pwstored)\n"; $ErrorInternal="AuthRegister:$Error"; return '';
  0            
  0            
317             }
318              
319             sub find_password {
320 0     0 0   my $email = shift; my $pwfile = "db/passwords";
  0            
321 0           $email = lc $email;
322 0 0         if (!-f $pwfile) { putfile $pwfile, ''; chmod 0600, $pwfile }
  0            
  0            
323 0 0         if (!&lock_mkdir($pwfile)) { $Error.="309-ERR:\n"; return ''; }
  0            
  0            
324 0 0         local *PH; if (!open(PH,$pwfile)) {
  0            
325 0           &unlock_mkdir($pwfile);
326 0           $Error.="312-ERR: Cannot open ($pwfile):$!\n"; return ''; }
  0            
327 0           while () {
328 0           my ($e,$p) = split; $e = lc $e;
  0            
329 0 0         if ($e eq $email) { close(PH); &unlock_mkdir($pwfile); return $p; }
  0            
  0            
  0            
330             }
331 0           $Error.="NOTFOUND($email)";
332 0           close(PH); &unlock_mkdir($pwfile); return '';
  0            
  0            
333             }
334              
335             sub random_name {
336 0 0   0 0   my $n = shift; $n = 8 unless $n > 0;
  0            
337 0           my @chars = (0..9, 'a'..'z', 'A'..'Z');
338 0           return join('', map { $chars[rand($#chars+1)] } (1..$n));
  0            
339             }
340              
341             sub store_log {
342 0 0   0 0   if($#_>=-1) { $LogReport.=$_[0] }
  0            
343 0 0         return if $LogReport eq '';
344 0 0         if ($SendLogs) { &send_email_to_admin('Log entry', $LogReport) }
  0            
345 0           $LogReport = '';
346             }
347              
348             ########################################################################
349             # Email communication
350              
351             # params: $email, opt: 'raw' or 'md5' to generate passord
352             sub reset_and_send_email_reminder {
353 0     0 0   my $email = shift; my $pwstore = shift;
  0            
354 0           $email=lc $email; $email =~ s/\s/ /g;
  0            
355 0 0         if ($email eq '') {
356 0           $Error.="328-ERR:No e-mail provided to send password\n"; return; }
  0            
357 0 0         if (!emailcheckok($email)) {
358 0           $Error.="330-ERR:Invalid e-mail address provided($email)\n"; return; }
  0            
359 0           my $user = get_user_unique('email',$email);
360 0 0         if ($user eq '') {
361 0           $Error.="333-ERR: No user with email ($email)\n"; return; }
  0            
362 0           my $pw = &reset_password($email, $pwstore);
363 0           &send_email_reminder1($email, $pw);
364 0           return 1;
365             }
366              
367             # params: $email, opt: 'raw' or 'md5' to generate new password if not found
368             sub send_email_reminder {
369 0     0 0   my $email = shift; my $pwstore = shift;
  0            
370 0           $email=lc $email; $email =~ s/\s/ /g;
  0            
371 0 0         if ($email eq '') {
372 0           $Error.="356-ERR:No e-mail provided to send password\n"; return; }
  0            
373 0 0         if (!emailcheckok($email)) {
374 0           $Error.="358-ERR:Invalid e-mail address provided($email)\n"; return; }
  0            
375 0           my $user = get_user_by_email($email);
376 0 0         if ($user eq '') {
377 0           $Error.="361-ERR: No user with email ($email)\n"; return; }
  0            
378 0           my $pw = find_password($email);
379 0 0         if ($pw =~ /^raw:/) { $pw = $' }
  0 0          
380 0           elsif ($pw ne '') { $Error.="364-ERR:Cannot retrieve password\n"; return; }
  0            
381 0           else { $pw = &reset_password($email, $pwstore) }
382              
383 0           &send_email_reminder1($email, $pw);
384 0           return 1;
385             }
386              
387             sub send_email_reminder1 {
388 0     0 0   my $email = shift; my $pw = shift;
  0            
389 0           my $httpslogin = "https://$ENV{SERVER_NAME}$ENV{SCRIPT_NAME}";
390              
391 0           my $msg = "Hi,\n\nYour email and password for the $SiteId site is:\n\n".
392             "Email: $email\nPassword: $pw\n\n".
393             "You can log in at:\n\n$httpslogin\n\n\n".
394             # "$HttpsBaseLink/login.cgi\n\n\n".
395             "Best regards,\n$SiteId Admin\n";
396 0           &send_email_to($email, "Subject: $SiteId Password Reminder", $msg);
397             }
398              
399             sub send_email_to_admin {
400 0     0 0   my $subject = shift; my $msg1 = shift;
  0            
401 0           $subject =~ s/\s+/ /g;
402 0           $subject = "Subject: [$SiteId System Report] $subject";
403 0 0         return if $Email_bcc eq '';
404 0           my $msg = '';
405 0 0         $msg.="From: $Email_from\n" unless $Email_from eq '';
406 0           $msg.="To: $Email_bcc\n";
407 0           $msg.="$subject\n\n$msg1";
408 0           &_send_email($msg);
409             }
410              
411             sub send_email_to {
412 0 0   0 0   my $email = shift; croak unless &emailcheckok($email);
  0            
413 0           my $subject = shift; $subject =~ s/[\n\r]/ /g;
  0            
414 0 0         if ($subject !~ /^Subject: /) { $subject = "Subject: $subject" }
  0            
415 0           my $msg1 = shift;
416              
417 0           my $msg = '';
418 0 0         $msg.="From: $Email_from\n" unless $Email_from eq '';
419 0           $msg.="To: $email\n";
420 0 0         $msg.="Bcc: $Email_bcc\n" unless $Email_bcc eq '';
421 0           $msg.="$subject\n\n$msg1";
422 0           &_send_email($msg);
423             }
424              
425             sub _send_email {
426 0     0     my $fullmessage = shift;
427 0 0         if (! -x $Sendmail) {
428 0           $Error.="390-ERR:No sendmail ($Sendmail)\n"; return ''; }
  0            
429 0           local *S;
430 0 0         if (!open(S,"|$Sendmail -ti")) {
431 0           $Error.="393-ERR:Cannot run sendmail:$!\n"; return ''; }
  0            
432 0           print S $fullmessage; close(S);
  0            
433             }
434              
435             ########################################################################
436             # Data checks
437              
438             sub emailcheckok {
439 0     0 0   my $email = shift;
440 0 0         if ($email =~ /^[a-zA-Z][\w\.+-]*[a-zA-Z0-9+-]@
441             [a-zA-Z0-9][\w\.-]*[a-zA-Z0-9]\.[a-zA-Z][a-zA-Z\.]*[a-zA-Z]$/x)
442 0           { return 1 }
443 0           return '';
444             }
445              
446             sub useridcheckok {
447 0 0   0 0   my $userid = shift; return 1 if $userid=~/^[a-zA-Z0-9-]+$/; return ''; }
  0            
  0            
448              
449             # DB related functions
450              
451             # Uses file db/users.db. Empty string returned if unsuccessful, with
452             # error message appended to $Error.
453             sub get_user_by_email {
454 0     0 0   my $email = shift;
455 0 0         if (!-f 'db/users.db')
456 0           { $Error.= "454-ERR: no file db/users.db\n"; return; }
  0            
457 0           my @db = @{ &read_db('file=db/users.db') };
  0            
458 0 0         for my $r (@db) { if (lc($email) eq lc($r->{email})) { return $User=$r } }
  0            
  0            
459 0           $Error.="457-ERR: no user with email ($email)\n"; return $User='';
  0            
460             }
461              
462             sub get_user_by_userid {
463 0     0 0   my $userid = shift;
464 0 0         if (!-f 'db/users.db')
465 0           { $Error.= "463-ERR: no file db/users.db\n"; return; }
  0            
466 0           my @db = @{ &read_db('file=db/users.db') };
  0            
467 0 0         for my $r (@db) { if ($userid eq $r->{userid}) { return $User=$r } }
  0            
  0            
468 0           $Error.="466-ERR: no user with userid ($userid)."; return $User='';
  0            
469             }
470              
471             sub get_user {
472 0     0 0   my $k = shift; my $v = shift;
  0            
473 0 0         if (!-f 'db/users.db')
474 0           { $Error.= "472-ERR: no file db/users.db\n"; return; }
  0            
475 0           my @db = @{ &read_db('file=db/users.db') };
  0            
476 0           for my $r (@db)
477 0 0 0       { if (exists($r->{$k}) && $v eq $r->{$k}) { return $User=$r } }
  0            
478 0           $Error.="476-ERR: no user with key=($k) v=($v)."; return $User='';
  0            
479             }
480              
481             # Get user by a key,value, but make sure there is exactly one such user
482             # Normalizes whitespace and case insensitive
483             sub get_user_unique {
484 0     0 0   my $k = shift; my $v = shift;
  0            
485 0 0         if (!-f 'db/users.db')
486 0           { $Error.= "455-ERR: no file db/users.db\n"; return ''; }
  0            
487 0           my @db = @{ &read_db('file=db/users.db') };
  0            
488 0           $v=~s/^\s+//; $v=~s/\s+$//; $v=~s/\s+/ /g; $v = lc $v;
  0            
  0            
  0            
489 0 0 0       if ($k eq '' or $v eq '')
490 0           { $Error.="461-ERR:Empty k or v ($k:$v)\n"; return ''; }
  0            
491 0           my $u = '';
492 0           for my $r (@db) {
493 0 0         next unless exists($r->{$k}); my $v1 = $r->{$k};
  0            
494 0           $v1=~s/^\s+//; $v1=~s/\s+$//; $v1=~s/\s+/ /g; $v1 = lc $v1;
  0            
  0            
  0            
495 0 0         next unless $v eq $v1;
496 0 0         if ($u eq '') { $u = $r; next; }
  0            
  0            
497 0           $Error.= "467-ERR: double user key ($k:$v)\n"; return '';
  0            
498             }
499 0 0         return $User=$u unless $u eq '';
500 0           $Error.="470-ERR: no user with key ($k:$v)\n"; return '';
  0            
501             }
502              
503             # Read DB records in the RFC822-like style (to add reference).
504             sub read_db {
505 0     0 0   my $arg = shift;
506 0 0         if ($arg =~ /^file=/) {
507 0 0         my $f = $'; if (!&lock_mkdir($f)) { return '' }
  0            
  0            
508 0 0         local *F; open(F, $f) or die "cannot open $f:$!";
  0            
509 0           $arg = join('', ); close(F); &unlock_mkdir($f);
  0            
  0            
510             }
511              
512 0           my $db = [];
513 0           while ($arg) {
514 0           $arg =~ s/^\s*(#.*\s*)*//; # allow comments betwen records
515 0           my $record;
516 0 0         if ($arg =~ /\n\n+/) { $record = "$`\n"; $arg = $'; }
  0            
  0            
517 0           else { $record = $arg; $arg = ''; }
  0            
518 0           my $r = {};
519 0           while ($record) {
520 0           while ($record =~ /^(.*)(\\\n|\n[ \t]+)(.*)/)
521 0           { $record = "$1 $3$'" }
522 0 0         $record =~ /^([^\n:]*):(.*)\n/ or die;
523 0           my $k = $1; my $v = $2; $record = $';
  0            
  0            
524 0 0         if (exists($r->{$k})) {
525 0           my $c = 0;
526 0           while (exists($r->{"$k-$c"})) { ++$c }
  0            
527 0           $k = "$k-$c";
528             }
529 0           $r->{$k} = $v;
530             }
531 0           push @{ $db }, $r;
  0            
532             }
533 0           return $db;
534             }
535              
536             # Read one DB record in the RFC822-like style (to add reference).
537             sub read_db_record {
538 0     0 0   my $arg = shift;
539 0 0         if ($arg =~ /^file=/) {
540 0 0         my $f = $'; local *F; open(F, $f) or die "cannot open $f:$!";
  0            
  0            
541 0           $arg = join('', ); close(F);
  0            
542             }
543              
544 0           while ($arg =~ s/^(\s*|\s*#.*)\n//) {} # allow comments before record
545 0           my $record;
546 0 0         if ($arg =~ /\n\n+/) { $record = "$`\n"; $arg = $'; }
  0            
  0            
547 0           else { $record = $arg; $arg = ''; }
  0            
548 0           my $r = {};
549 0           while ($record) {
550 0           while ($record =~ /^(.*)(\\\n|\n[ \t]+)(.*)/)
551 0           { $record = "$1 $3$'" }
552 0 0         $record =~ /^([^\n:]*):(.*)\n/ or die;
553 0           my $k = $1; my $v = $2; $record = $';
  0            
  0            
554 0 0         if (exists($r->{$k})) {
555 0           my $c = 0;
556 0           while (exists($r->{"$k-$c"})) { ++$c }
  0            
557 0           $k = "$k-$c";
558             }
559 0           $r->{$k} = $v;
560             }
561 0           return $r;
562             }
563              
564             sub putfile($@) {
565 0     0 0   my $f = shift; local *F;
  0            
566 0 0         if (!open(F, ">$f")) { $Error.="325-ERR:Cannot write ($f):$!\n"; return; }
  0            
  0            
567 0           for (@_) { print F } close(F);
  0            
  0            
568             }
569              
570             ########################################################################
571             # Simple file locking using mkdir
572              
573             # Exlusive locking using mkdir
574             # lock_mkdir($fname); # return 1=success ''=fail
575             sub lock_mkdir {
576 0     0 0   my $fname = shift; my $lockd = "$fname.lock"; my $locked;
  0            
  0            
577             # First, hopefully most usual case
578 0 0 0       if (!-e $lockd && ($locked = mkdir($lockd,0700))) { return $locked }
  0            
579 0           my $tryfor=10; #sec
580 0           $locked = ''; # flag
581 0           for (my $i=0; $i<2*$tryfor; ++$i) {
582 0           select(undef,undef,undef,0.5); # wait for 0.5 sec
583 0 0         !-e $lockd && ($locked = mkdir($lockd,0700));
584 0 0         if ($locked) { return $locked }
  0            
585             }
586 0           $Error.="393-ERR:Could not lock file ($fname)\n"; return $locked;
  0            
587             }
588              
589             # Unlock using mkdir
590             # unlock_mkdir($fname); # return 1=success ''=fail or no lock
591             sub unlock_mkdir {
592 0     0 0   my $fname = shift; my $lockd = "$fname.lock";
  0            
593 0 0         if (!-e $lockd) { $Error.="400-ERR:No lock on ($fname)\n"; return '' }
  0            
  0            
594 0 0         if (-d $lockd) { return rmdir($lockd) }
  0            
595 0 0 0       if (-f $lockd or -l $lockd) { unlink($lockd) }
  0            
596 0           $Error.="403-ERR:Unknown error"; return '';
  0            
597             }
598              
599             1;
600              
601             __END__