From 13c68ce71390463f200b02280bf5e064224eb58c Mon Sep 17 00:00:00 2001 From: "Jannis M. Hoffmann" Date: Tue, 3 Nov 2020 17:01:16 +0100 Subject: propper process coordination for session cleanup --- lib/JWebmail/Plugin/ServerSideSessionData.pm | 135 +++++++++++++++++++-------- 1 file changed, 97 insertions(+), 38 deletions(-) diff --git a/lib/JWebmail/Plugin/ServerSideSessionData.pm b/lib/JWebmail/Plugin/ServerSideSessionData.pm index 9890358..e86a248 100644 --- a/lib/JWebmail/Plugin/ServerSideSessionData.pm +++ b/lib/JWebmail/Plugin/ServerSideSessionData.pm @@ -1,99 +1,151 @@ -package JWebmail::Plugin::ServerSideSessionData; +package JWebmail::Plugin::ServerSideSessionData v1.1.0; use Mojo::Base 'Mojolicious::Plugin'; use Mojo::JSON qw(decode_json encode_json); use Mojo::File; -use constant { - S_KEY => 's3d.key', -}; +use Fcntl ':DEFAULT', ':seek'; +use Time::HiRes 'sleep'; -has '_session_directory'; -sub session_directory { my $self = shift; @_ ? $self->_session_directory(Mojo::File->new(@_)) : $self->_session_directory } +use constant S_KEY => 's3d.key'; +use constant CLEANUP_FILE_NAME => 'cleanup'; +use constant LOCK_ITER => 5; +use constant ADVANCE_ON_FAILURE => 10; # seconds to retry to acquire the lock +has 'session_directory'; has 'expiration'; has 'cleanup_interval'; -has '_cleanup'; -sub cleanup { +has 'next_cleanup' => 0; + + +# read und potentially update file return bool +# needs atomic lock file +# the file contains a single timestamp +sub _rw_cleanup_file { my $self = shift; - if (@_) { - return $self->_cleanup(@_); + my $time = shift; + + my $lock_name = $self->session_directory->child(CLEANUP_FILE_NAME . ".lock"); + my $info_name = $self->session_directory->child(CLEANUP_FILE_NAME . ".info"); + + my ($lock, $ctr, $rmlock); + until (sysopen($lock, $lock_name, O_WRONLY | O_CREAT | O_EXCL)) { + die "unexpected error '$!'" unless $! eq 'File exists'; + if ($ctr > LOCK_ITER) { + open($lock, '<', $lock_name) or die "unexpected error '$!'"; + my $pid = <$lock>; + close $lock; + chomp $pid; + if (!$rmlock && (!$pid || !-e "/proc/$pid")) { + $lock_name->remove; + $rmlock = 1; + next; + } + $self->next_cleanup($time + ADVANCE_ON_FAILURE); + return 0; + } + sleep(0.01); # TODO: better spin locking + } continue { + $ctr++; } - else { - if ($self->_cleanup < time) { + $lock->say($$); + $lock->close; + + my $ret = eval { + use autodie; + + open(my $info, -e $info_name ? '+<' : '+>', $info_name); + my $next_time = $info->getline; + $next_time = 0 unless ($next_time//'') =~ /^\d+$/; + chomp $next_time; + if ($next_time > $time) { + $info->close; + $self->next_cleanup($next_time); return 0; } else { - $self->_cleanup(time + $self->cleanup_interval); + $info->truncate(0); + $info->seek(0, SEEK_SET); + $info->say($time + $self->cleanup_interval); + $info->close; + $self->next_cleanup($time + $self->cleanup_interval); return 1; } - } + }; + $lock_name->remove; + return $ret; } -sub s3d { +sub cleanup_files { my $self = shift; - my $c = shift; - # cleanup old sessions - if ($self->cleanup) { - my $t = time; + my $t = time; + if ($self->next_cleanup < $t && $self->_rw_cleanup_file($t)) { for ($self->session_directory->list->each) { if ( $_->stat->mtime + $self->expiration < $t ) { $_->remove; } } } +} + + +sub s3d { + my $self = shift; + my $c = shift; + my ($key, $val) = @_; + + # cleanup old sessions + $self->cleanup_files; my $file = $self->session_directory->child($c->session(S_KEY) || $c->req->request_id . $$); if (-e $file) { if ($file->stat->mtime + $self->expiration < time) { - $file->remove; + truncate $file, 0; } else { $file->touch; } } - my $data = decode_json($file->slurp) if (-s $file); + elsif (defined $val) { + $file->touch; + $file->chmod(0600); + $c->session(S_KEY, $file->basename); + } - my ($key, $val) = @_; + my $data = decode_json($file->slurp) if (-s $file); if (defined $val) { # set - unless (-e $file) { - $c->session(S_KEY, $file->basename); - } $data = ref $data ? $data : {}; $data->{$key} = $val; - #$file->spurt(encode_json $data); - open(my $f, '>', $file) or die "$!"; - chmod 0600, $f; - $f->say(encode_json $data); - close($f); + $file->spurt(encode_json $data, "\n"); } else { # get return defined $key ? $data->{$key} : $data; } -}; +} sub register { my ($self, $app, $conf) = @_; - $self->session_directory($conf->{directory} || "/tmp/" . $app->moniker); + $self->session_directory(Mojo::File->new($conf->{directory} || "/tmp/" . $app->moniker)); $self->expiration($conf->{expiration} || $app->sessions->default_expiration); $self->cleanup_interval($conf->{cleanup_interval} || $self->expiration); - $self->cleanup(time + $self->cleanup_interval); unless (-d $self->session_directory) { mkdir($self->session_directory) or $! ? die "failed to create directory: $!" : 1; } + $self->cleanup_files; + $app->helper( s3d => sub { $self->s3d(@_) } ); } @@ -110,7 +162,7 @@ ServeSideSessionData - Stores session data on the server (alias SSSD or S3D) =head1 SYNOPSIS - $app->plugin('ServeSideSessionData'); + $app->plugin('ServeSideSessionData', {expiration => 20*60}); $c->s3d(data => 'Hello, S3D'); $c->s3d('data'); @@ -118,21 +170,28 @@ ServeSideSessionData - Stores session data on the server (alias SSSD or S3D) =head1 DESCRIPTION Store data temporarily on the server. -The only protetction on the server are struct user access rights. +The only protection on the server are strict user access rights +so you need to still be careful with your secrets. =head1 OPTIONS =head2 directory +directory where session data is stored + default C<< 'tmp/' . $app->moniker >> =head2 expiration -default session expiration +how long is a server side session valid in seconds (calculated after last access) + +defaults to session expiration =head2 cleanup_interval -default session expiration +a recurring time interval when old session data gets cleaned up + +defaults to expiration =head1 HELPERS @@ -144,4 +203,4 @@ Stores and retrieves values. $c->s3d('data'); $c->s3d->{data}; -=cut \ No newline at end of file +=cut -- cgit v1.2.3