* Add enclair_db option to UserDB.pm. Allows logging of enclair password
[interchange.git] / lib / Vend / Session.pm
1 # Vend::Session - Interchange session routines
2 #
3 # $Id: Session.pm,v 2.31 2007-08-20 18:29:10 kwalsh Exp $
4
5 # Copyright (C) 2002-2007 Interchange Development Group
6 # Copyright (C) 1996-2002 Red Hat, Inc.
7 #
8 # This program was originally based on Vend 0.2 and 0.3
9 # Copyright 1995 by Andrew M. Wilcox <amw@wilcoxsolutions.com>
10 #
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public
22 # License along with this program; if not, write to the Free
23 # Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
24 # MA  02110-1301  USA.
25
26 package Vend::Session;
27 require Exporter;
28
29 use vars qw($VERSION);
30 $VERSION = substr(q$Revision: 2.31 $, 10);
31
32 @ISA = qw(Exporter);
33
34 @EXPORT = qw(
35
36 check_save
37 dump_sessions
38 expire_sessions
39 close_session
40 get_session
41 init_session
42 is_retired
43 new_session
44 put_session
45 retire_id
46 session_name
47 tie_static_dbm
48
49 );
50
51 use strict;
52 use Fcntl;
53 use Vend::Util;
54
55 require Vend::SessionFile;
56
57 BEGIN {
58         if($Global::GDBM) {
59                 require GDBM_File;
60                 import GDBM_File;
61         }
62         if($Global::DB_File) {
63                 require DB_File;
64                 import DB_File;
65         }
66         if($Global::DBI) {
67                 require Vend::SessionDB;
68         }
69 }
70
71 my (%Session_class);
72 my ($Session_open, $File_sessions, $Lock_sessions);
73
74
75 # Selects based on initial config
76
77 %Session_class = (
78 # $File_sessions, $Lock_sessions, &$Session_open
79 GDBM => [ 0, 1, sub {
80                         $::Instance->{DB_object} =
81                                 tie(%Vend::SessionDBM,
82                                         'GDBM_File',
83                                         $Vend::Cfg->{SessionDatabase} . ".gdbm",
84                                         &GDBM_WRCREAT,
85                                         $Vend::Cfg->{FileCreationMask}
86                         );
87                         die "Could not tie to $Vend::Cfg->{SessionDatabase}: $!\n"
88                                 unless defined $::Instance->{DB_object};
89                 },
90         ],
91 DB_File => [ 0, 1, sub {
92                                         tie(
93                                                 %Vend::SessionDBM,
94                                                 'DB_File',
95                                                 $Vend::Cfg->{SessionDatabase} . ".db",
96                                                 &O_RDWR|&O_CREAT,
97                                                 $Vend::Cfg->{FileCreationMask}
98                                         )
99                                         or die "Could not tie to $Vend::Cfg->{SessionDatabase}: $!\n";
100                                 },
101 ],
102
103 DBI => [ 0, 0, sub {
104                                 return 1 if $::Instance->{DB_sessions};
105                                 tie (
106                                         %Vend::SessionDBM,
107                                         'Vend::SessionDB',
108                                         $Vend::Cfg->{SessionDB}
109                                 )
110                                 or die "Could not tie to $Vend::Cfg->{SessionDB}: $!\n";
111                                 $::Instance->{DB_sessions} = 1;
112                         },
113                 ],
114
115 File => [ 1, 0, sub {
116                                 tie(
117                                         %Vend::SessionDBM,
118                                         'Vend::SessionFile',
119                                         $Vend::Cfg->{SessionDatabase}
120                                 )
121                                 or die "Could not tie to $Vend::Cfg->{SessionDatabase}: $!\n";
122                         },
123                 ],
124
125
126 NFS => [ 1, 0, sub {
127                                 tie(
128                                         %Vend::SessionDBM,
129                                         'Vend::SessionFile',
130                                         $Vend::Cfg->{SessionDatabase},
131                                         1,
132                                 )
133                                 or die "Could not tie to $Vend::Cfg->{SessionDatabase}: $!\n";
134                         },
135                 ],
136
137 );
138
139 # SESSIONS implemented using DBM
140
141 sub get_session {
142         my $seed = shift;
143
144         if($seed and ! $Vend::SessionID) {
145 #::logDebug("received seed=$seed");
146                 $Vend::SessionID = $seed;
147         }
148         $Vend::SessionName = session_name() if ! $Vend::SessionName;
149 #::logDebug("session name now $Vend::SessionName");
150
151         $Vend::HaveSession = 0;
152         open_session();
153         my $new;
154         $new = read_session($seed) unless $Vend::ExternalProgram;
155         unless($File_sessions) {
156                 lock_session();
157                 close_session();
158         }
159         $Vend::HaveSession = 1;
160         return ($new || 0);
161 }
162
163 sub put_session {
164         return unless $Vend::HaveSession;
165         unless($File_sessions) {
166                 open_session();
167                 write_session();
168                 unlock_session();
169                 close_session();
170         }
171         else {
172                 write_session();
173         }
174         $Vend::HaveSession = 0;
175 }
176
177 sub open_session {
178         return 1 if defined $Vend::SessionOpen;
179         ($File_sessions, $Lock_sessions, $Session_open) 
180                 = @{$Session_class{ $Vend::Cfg->{SessionType} || 'File' }};
181         if (! defined $File_sessions) {
182                 ($File_sessions, $Lock_sessions, $Session_open) = @{$Session_class{File}};
183         }
184 #::logDebug("open_session: File_sessions=$File_sessions Sub=$Session_open");
185         if($Lock_sessions) {
186                 open(Vend::SessionLock, "+>>$Vend::Cfg->{SessionLockFile}")
187                         or die "Could not open lock file '$Vend::Cfg->{SessionLockFile}': $!\n";
188                 lockfile(\*Vend::SessionLock, 1, 1)
189                         or die "Could not lock '$Vend::Cfg->{SessionLockFile}': $!\n";
190         }
191         
192         &$Session_open;
193         $Vend::SessionOpen = 1;
194
195 }
196
197 sub count_ip {
198         my ($inc) = @_;
199
200         # Immediate return if RobotLimit is not defined
201         my $index = $Vend::Cfg->{RobotLimit} or return 0;
202         $index *= -1;
203
204
205         my $ip = $CGI::remote_addr;
206         $ip =~ s/\W/_/g;
207
208         my $dir = "$Vend::Cfg->{ScratchDir}/addr_ctr";
209         my $fn = Vend::Util::get_filename($ip, 2, 1, $dir);
210
211
212         # Unlink the "counter" file if applicable.
213
214         if(-f $fn) {
215                 my $grace = $::Limit->{ip_session_expire} || 60;
216                 my @st = stat(_);
217                 my $mtime = (time() - $st[9]) / 60;
218                 if($mtime > $grace) {
219 #::logDebug("ip $ip session limit expired due to '$mtime' > '$grace' minutes");
220                     unlink $fn;
221                 }
222         }
223
224         my $lfn = $fn . '.lockout';
225
226
227         # Unlink the lockout file if applicable, otherwise lock.
228
229         if(-f $lfn) {
230                 my $grace = $::Limit->{robot_expire} || 1;
231                 my @st = stat(_);
232                 my $mtime = (time() - $st[9]) / 86400;
233                 if($mtime > $grace) {
234 #::logDebug("ip $ip allowed back in due to '$mtime' > '$grace' days");
235                         unlink $lfn;
236                 } else {
237                         return 1;
238                 }
239         }
240
241
242         # Append a new timestamp to the counter file (if applicable)
243
244         timecard_stamp($fn) if $inc;
245
246
247         # Get timestamp from timecard file and see if it's expired yet.
248
249         my $rtime;
250         return 0 unless $rtime = timecard_read($fn, $index);
251         my $grace = $::Limit->{ip_session_expire} || 60;
252         return 0 if time - $rtime > $grace * 60;
253
254 #::logDebug("ip $ip locked out due to too many new sessions in the last $grace minutes");
255         # Create the lockout file
256         open(FH, '>', $lfn) or die "Can't create $lfn: $!";
257         close FH;
258
259         return 1;
260 }
261
262 sub is_retired {
263         my $id = shift;
264         mkdir "$Vend::Cfg->{ScratchDir}/retired", 0777
265                 unless -d "$Vend::Cfg->{ScratchDir}/retired";
266         my $fn = Vend::Util::get_filename($id, 2, 1, "$Vend::Cfg->{ScratchDir}/retired");
267         return -f $fn ? 1 : 0;
268 }
269
270 sub retire_id {
271         my $id = shift;
272         return unless $id =~ /^\w+$/;
273         mkdir "$Vend::Cfg->{ScratchDir}/retired", 0777
274                 unless -d "$Vend::Cfg->{ScratchDir}/retired";
275         my $fn = Vend::Util::get_filename($id, 2, 1, "$Vend::Cfg->{ScratchDir}/retired");
276         open(TMPRET, ">$fn")
277                 or die "retire id open: $!\n";
278         close(TMPRET);
279         return;
280 }
281
282 sub new_session {
283     my($seed) = @_;
284     my($name);
285
286 #::logDebug ("new session id=$Vend::SessionID  name=$Vend::SessionName seed=$seed");
287         open_session();
288     for (;;) {
289                 unless (defined $seed) {
290                         $Vend::SessionID = random_string($::Limit->{session_id_length} ||= 8);
291                         undef $Vend::CookieID;
292                 }
293                 undef $seed;
294                 if (is_retired($Vend::SessionID)) {
295                         retire_id($Vend::SessionID);
296                         next;
297                 }
298                 $name = session_name();
299                 unless ($File_sessions) { 
300                         lock_session($name);
301                         last unless defined $Vend::SessionDBM{$name};
302                         unlock_session($name);
303                 }
304                 else {
305                         last unless exists $Vend::SessionDBM{$name};
306                 }
307     }
308         $Vend::new_session = 1;
309         count_ip(1) if $Vend::Cfg->{RobotLimit};
310         undef $Vend::Cookie;
311     $Vend::SessionName = $name;
312     init_session();
313 #::logDebug("init_session $Vend::SessionName is: " . ::uneval($Vend::Session));
314 #::logDebug("init_session $Vend::SessionName");
315         $Vend::HaveSession = 1;
316         return if $File_sessions || $::Instance->{DB_sessions};
317         write_session();
318         close_session();
319         return;
320 }
321
322 sub close_session {
323 #::logDebug ("try to close session id=$Vend::SessionID  name=$Vend::SessionName");
324         return 0 unless defined $Vend::SessionOpen;
325
326         unless($::Instance->{DB_sessions}) {
327                 undef $::Instance->{DB_object};
328                 undef $File_sessions;
329                 untie %Vend::SessionDBM
330                         or die "Could not close $Vend::Cfg->{SessionDatabase}: $!\n";
331                 undef $Vend::SessionOpen;
332         }
333         else {
334                 undef $::Instance->{DB_sessions};
335                 untie %Vend::SessionDBM
336                         or die "Could not close $Vend::Cfg->{SessionDatabase}: $!\n";
337                 undef $Vend::SessionOpen;
338         }
339         
340         return 1 unless $Lock_sessions;
341
342         unlockfile(\*Vend::SessionLock)
343                 or die "Could not unlock '$Vend::Cfg->{SessionLockFile}': $!\n";
344     close(Vend::SessionLock)
345                 or die "Could not close '$Vend::Cfg->{SessionLockFile}': $!\n";
346         undef $Vend::SessionOpen;
347         return 1;
348 }
349
350 sub write_session {
351     my($s);
352 #::logDebug ("write session id=$Vend::SessionID  name=$Vend::SessionName\n");
353         my $time = time;
354     $Vend::Session->{'time'} = $time;
355         delete $Vend::Session->{values}->{mv_credit_card_number};
356     my $save = delete $Vend::Session->{'user'};
357
358         delete @{$::Scratch}{@Vend::TmpScratch};
359
360         $Vend::Session->{username} = $Vend::username;
361         $Vend::Session->{admin} = $Vend::admin;
362         $Vend::Session->{groups} = $Vend::groups;
363         $Vend::Session->{superuser} = $Vend::superuser;
364         $Vend::Session->{login_table} = $Vend::login_table;
365     $s = ! $File_sessions ? uneval_fast($Vend::Session) : $Vend::Session;
366 #::logDebug("writing \$s of length " . length($s) . " to SessionDB");
367     $Vend::SessionDBM{$Vend::SessionName} = $s or 
368                 die "Data was not stored in SessionDBM\n";
369     $Vend::Session->{'user'} = $save;
370 }
371
372 sub unlock_session {
373 #::logDebug ("unlock session id=$Vend::SessionID  name=$Vend::SessionName\n");
374         my $name = shift;
375         $name ||= $Vend::SessionName;
376         delete $Vend::SessionDBM{'LOCK_' . $Vend::SessionName}
377                 unless $File_sessions;
378 }
379
380 sub lock_session {
381         return 1 if $File_sessions;
382         my $name = shift;
383         $name ||= $Vend::SessionName;
384 #::logDebug ("lock session id=$Vend::SessionID  name=$Vend::SessionName\n");
385         my $lockname = "LOCK_$name";
386         my ($tried, $locktime, $sleepleft, $pid, $now, $left);
387         $tried = 0;
388
389         LOCKLOOP: {
390                 my $lv;
391                 if (defined ($lv = $Vend::SessionDBM{$lockname}) ) {
392                         ($locktime, $pid) = split /:/, $lv, 2;
393                 }
394                 $now = time;
395                 if(defined $locktime and $locktime) {
396                         $left = $now - $locktime;
397                         if ( $left > $Global::HammerLock ) {
398                                 $Vend::SessionDBM{$lockname} = "$now:$$";
399                                 logError("Hammered session lock %s left by PID %s" , $lockname, $pid );
400                                 return 1;
401                         }
402                         elsif ($left < 0) {
403                                 my $m = <<EOF;
404 lock_session: Time earlier than lock time for $lockname
405 left by PID %s.
406 EOF
407                                 logError($m, $pid);
408                                 die errmsg("Locking error!\n", '');
409                         }
410                         else {
411                                 unless ($tried) {
412                                         $sleepleft = 1;
413                                         $tried = 1;
414                                 }
415                                 else {
416                                         $sleepleft = int($left / 2);
417                                         if ($sleepleft < 3) {
418                                                 $sleepleft = $left;
419                                         }
420                                 }
421                                 if($::Instance->{DB_sessions}) {
422                                         sleep $sleepleft;
423                                         read_session();
424                                 }
425                                 else {
426                                         close_session();
427                                         sleep $sleepleft;
428                                         open_session();
429                                         read_session();
430                                 }
431                                 next LOCKLOOP;
432                         }
433                 }
434                 else {
435                         $Vend::SessionDBM{$lockname} = "$now:$$";
436                         return 1;
437                 }
438         } #LOCKLOOP
439
440         # Should never get here
441         return undef;
442 }
443
444 sub read_session {
445         my $seed = shift;
446     my($s);
447
448 #::logDebug ("read session id=$Vend::SessionID  name=$Vend::SessionName\n");
449         $s = $Vend::SessionDBM{$Vend::SessionName}
450                 or $Global::Variable->{MV_SESSION_READ_RETRY}
451                 and do {
452                         my $i = 0;
453                         my $tries = $Global::Variable->{MV_SESSION_READ_RETRY} + 0 || 5;
454                         while($i++ < $tries) {
455                                 ::logDebug("retrying session read on undef, try $i");
456                                 $s = $Vend::SessionDBM{$Vend::SessionName};
457                                 next unless $s;
458                                 ::logDebug("Session re-read successfully on try $i");
459                                 last;
460                         }
461                 };
462                 
463 #::logDebug ("Session:\n$s\n");
464         return new_session($seed) unless $s;
465     $Vend::Session = ref $s ? $s : evalr($s);
466     die "Could not eval '$s' from session dbm: $@\n" if $@;
467
468         $Vend::Session->{host} = $CGI::host;
469
470         $Vend::username    = $Vend::Session->{username};
471         $Vend::admin       = $Vend::Session->{admin};
472         $Vend::superuser   = $Vend::Session->{superuser};
473         $Vend::groups      = $Vend::Session->{groups};
474         $Vend::login_table = $Vend::Session->{login_table};
475
476         $Vend::Session->{arg}  = $Vend::Argument;
477
478     $::Values   = $Vend::Session->{values};
479     $::Scratch  = $Vend::Session->{scratch};
480     $::Carts    = $Vend::Session->{carts};
481     $::Discounts = $Vend::Session->{discount};
482     $Vend::Interpolate::Tmp ||= {};
483     $::Control  = $Vend::Interpolate::Tmp->{control} = [];
484         tie $Vend::Items, 'Vend::Cart';
485 }
486
487
488 ## SESSIONS
489
490 my $joiner = $Global::Windows ? '_' : ':';
491
492 sub session_name {
493     my($host, $user, $fn, $proxy);
494
495         return $Vend::SessionID if $::Instance->{ExternalCookie};
496
497         if(defined $CGI::user and $CGI::user) {
498                 $host = escape_chars($CGI::user);
499         }
500         elsif($CGI::cookieuser) {
501                 $host = $CGI::cookieuser;
502         }
503         elsif($CGI::cookiehost) {
504                 $host = $CGI::cookiehost;
505         }
506         else {
507                 $host = $CGI::host;
508                 $proxy = index($host,"proxy");
509                 $host = substr($host,$proxy)
510                         if ($proxy >= 0);
511                 $host = escape_chars($host);
512         }
513 #::logDebug ("name session user=$CGI::user host=$host ($CGI::host)\n");
514     $fn = $Vend::SessionID . $joiner . $host;
515 #::logDebug ("name session id=$Vend::SessionID  name=$fn\n");
516     $fn;
517 }
518
519
520 sub init_session {
521         undef $Vend::username;
522         undef $Vend::admin;
523         undef $Vend::groups;
524         undef $Vend::superuser;
525         undef $Vend::login_table;
526     $Vend::Session = {
527                 'host'          => $CGI::host,
528                 'ohost'         => $CGI::remote_addr,
529                 'arg'           => $Vend::Argument,
530                 'browser'       => $CGI::useragent,
531                 'referer'       => $CGI::referer,
532                 'spider'        => $Vend::Robot,
533                 'scratch'       => { %{$Vend::Cfg->{ScratchDefault}} },
534                 'values'        => { %{$Vend::Cfg->{ValuesDefault}} },
535                 'carts'         => {main => []},
536                 'levies'        => {main => []},
537                 'discount_space'        => {main => {}},
538     };
539         $Vend::Session->{shost} = $CGI::remote_addr
540                 if $CGI::secure;
541         $::Values     = $Vend::Session->{values};
542         $::Scratch        = $Vend::Session->{scratch};
543         $::Scratch->{mv_locale} ||= $Vend::Cfg->{DefaultLocale};
544         $::Carts          = $Vend::Session->{carts};
545         tie $Vend::Items, 'Vend::Cart';
546         $::Values->{mv_shipmode} = $Vend::Cfg->{DefaultShipping}
547                 if ! defined $::Values->{mv_shipmode};
548         $::Discounts
549                 = $Vend::Session->{discount}
550                 = $Vend::Session->{discount_space}{main};
551         if(my $macro = $Vend::Cfg->{SpecialSub}{init_session}) {
552                 Vend::Dispatch::run_macro(
553                                 $macro,
554                                 $Vend::Session,
555                         );
556         }
557 }
558
559 sub dump_sessions {
560         my($called) = @_;
561     my($session_name, $s);
562         die "Can't dump file-based sessions.\n" if $File_sessions;
563         my $pretty;
564
565         eval {  require Data::Dumper;
566                         $Data::Dumper::Indent = 3;
567                         $Data::Dumper::Terse = 1; };
568         $pretty = $@ ? 0 : 1;
569     open_session();
570     while(($session_name, $s) = each %Vend::SessionDBM) {
571                 next if $session_name eq 'dumpprog:DUMP';
572                 next if $session_name =~ /^LOCK_/;
573                 if(defined $called) {
574                         next unless $session_name =~ /$called/;
575                 }
576                 if ($pretty or defined $Storable::VERSION) {
577                         my $ref = evalr $s;
578                         $s = uneval($ref);
579                 }
580                 print "$session_name $s\n\n";
581     }
582     close_session();
583 }
584
585 sub reorg {
586         return unless $::Instance->{DB_object};
587         GDBM_File::reorganize($::Instance->{DB_object});
588         GDBM_File::sync($::Instance->{DB_object});
589 }
590
591 sub expire_sessions {
592         my ($reorg) = @_;
593     my($time, $session_name, $s, $session, @delete);
594
595     $time = time;
596     open_session();
597     while(($session_name, $s) = each %Vend::SessionDBM) {
598
599                 # Lock records
600                 if ($session_name =~ /^LOCK_/) {;
601                         delete $Vend::SessionDBM{$session_name}
602                                 unless ($File_sessions or $s);
603                         next;
604                 }
605
606                 # Session markers
607                 if ($session_name =~ /^\w{8}$/) {
608                         $session = evalr ($s);
609                         die "Could not eval '$s' from session dbm: $@\n" if $@;
610                         next if keys %$session;   # Don't remove if has session marker
611                         push @delete, $session_name;
612                 }
613
614                 $session = evalr($s);
615                 die "Could not eval '$s' from session dbm: $@\n" if $@;
616                 next if check_save($time);
617                 if ( (! defined $session) ||
618                          $time - $session->{'time'} > $Vend::Cfg->{SessionExpire}) {
619                         push @delete, $session_name;
620                 }
621     }
622     foreach $session_name (@delete) {
623                 delete $Vend::SessionDBM{$session_name};
624                 delete $Vend::SessionDBM{"LOCK_$session_name"}
625                                 if ! $File_sessions && $Vend::SessionDBM{"LOCK_$session_name"};
626                 my $file = $session_name;
627                 $file =~ s/:.*//;
628                 opendir(Vend::DELDIR, $Vend::Cfg->{ScratchDir}) ||
629                         die "Could not open configuration directory $Vend::Cfg->{ScratchDir}: $!\n";
630                 my @files = grep /^$file/, readdir(Vend::DELDIR);
631                 for(@files) {
632                         unlink "$Vend::Cfg->{ScratchDir}/$_";
633                 }
634                 closedir(Vend::DELDIR);
635     }
636         reorg() if $reorg;
637     close_session();
638 }
639
640 sub check_save {
641         my($time) = (@_);
642         my $expire;
643
644         $time = $time || time();
645
646         if(defined $::Values->{mv_expire_time}) {
647                 $expire = $::Values->{mv_expire_time};
648                 unless($expire =~ /^\d{6,}$/) {
649                         $expire = Vend::Config::time_to_seconds($expire);
650                 }
651         }
652         $expire = $Vend::Cfg->{SaveExpire} unless $expire;
653
654         $Vend::Session->{'expire'} = $Vend::Expire = $time + $expire;
655
656         return ($expire > $time);
657 }       
658
659 1;
660
661 __END__