From 37ed7ba8d4542164636fc5a389e1add749925b52 Mon Sep 17 00:00:00 2001 From: Michal Suchanek Date: Aug 23 2022 07:17:48 +0000 Subject: MyBS: Save hoarded cookies to disk The performance of the OBS SSH authentication system is very bad, and can be overwhelmed by about 1 authentication/s. With osc saving cookies to disk this is not seen as problem. Saving cookies to disk in MyBS should work around the authentication system performance problem until it's resolved. The design ensures that processes competing for authentication use the same cookie once one become available rether than authenticating independently, overwhelming the authentication service. - Reading cookie file is lockless, file update atomic with mv - Requesting auth & writing out obtained cookie is locked - To be able to break stale lock the lockfile is empty, cookie is saved to a separate tmeporary file Cookie file contains the whole Set-Cookie header content. It would be possible to add support for multiple cookies but OBS only ever sets one cookie so multiple cookies are not supported. Signed-off-by: Michal Suchanek --- diff --git a/scripts/lib/SUSE/MyBS.pm b/scripts/lib/SUSE/MyBS.pm index 2af821b..fa1e3bf 100644 --- a/scripts/lib/SUSE/MyBS.pm +++ b/scripts/lib/SUSE/MyBS.pm @@ -10,13 +10,23 @@ use XML::Parser; use XML::Writer; use HTTP::Request; use File::Temp qw(tempfile); +use File::Copy qw(move); +use File::Basename; +use File::Path qw/make_path/; use Config::IniFiles; use Digest::MD5; use IPC::Open2; use MIME::Base64; +use Fcntl; +use POSIX qw(strftime); +use Errno; use SUSE::MyBS::Buildresults; +my $cookiefile = "~/.local/state/MyBS/cookie"; +my $lockfile = $cookiefile . ".lock"; +my $locktime = 300; + sub new { my ($class, $api_url) = @_; my $self = { }; @@ -147,6 +157,8 @@ sub new { bless($self, $class); + $self->load_cookie(); + if ($self->{url}->scheme eq "https" && $self->{url}->host eq "api.suse.de" && $self->{ua}->can('ssl_opts')) { @@ -166,6 +178,112 @@ sub new { return $self; } +sub check_cookie { + my $self = $_[0]; + my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat($cookiefile); + # printf("Loaded cookie from %s, current cookie time is %s\n", $self->{cookietime} ? $self->{cookietime} : 'none', $mtime ? $mtime : 'none'); + if ($mtime && (!$self->{cookietime} || ($self->{cookietime} < $mtime))) { + $self->load_cookie(); + return 1; + } + return undef; +} + +sub load_cookie { + my $self = $_[0]; + my $home = $ENV{'HOME'} . "/"; + $cookiefile =~ s,^~[/],$home,; + my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat($cookiefile); + $self->{cookietime} = $mtime; + if (-r $cookiefile) { + my $cookie = do { + open(my $fh, $cookiefile); + local $/ = undef; + <$fh>; + }; + # print "Loading "; + $self->process_cookie($cookie); + } +} + +sub process_cookie { + my ($self, $cookie) = @_; + my @cookie = split /\s*([^=;]+)\s*=\s*([^=;]+)\s*;?/, $cookie; + # print "Cookie: $cookie[1]=$cookie[2]\n"; + $self->{cookies}{$cookie[1]} = $cookie[2]; +} + +sub open_cookie { + my $self = $_[0]; + my $home = $ENV{'HOME'} . "/"; + $lockfile =~ s,^~[/],$home,; + my $dir = dirname($lockfile); + make_path($dir); + my $fh, my $filename; + my $ret = sysopen $fh, $lockfile, O_CREAT|O_EXCL|O_WRONLY, 0644; + if ($ret) { + ($fh, $filename) = tempfile( 'cookie-XXXXXX', DIR => $dir); + } else { + if (!$!{EEXIST}) { + die "Cannot access $lockfile: $!\n"; + } + } + return undef unless $ret; + $self->{lock} = $fh; + $self->{cookiefile} = $filename; + return $fh; +} + +sub lock_cookie { + my $self = $_[0]; + die "Deadlock: Already locked!\n" if $self->{lock}; + my $ret = $self->open_cookie(); + my $delay = 1; + while (!$ret) { + my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat($lockfile); + if ($mtime) { + my $age = time() - $mtime; + my $remain = $locktime - $age; + if ($remain > 0) { + sleep((($remain > 0) && ($remain < $delay)) ? $remain : $delay); + $delay *= 2; + } else { + my $date = strftime("%F %R", localtime($mtime)); + print STDERR "Cookie file $cookiefile locked for $age seconds since $date\n"; + unlink "$lockfile"; + } + } else { + if (!$!{ENOENT}) { + die "Cannot access $lockfile: $!\n"; + } + $ret = $self->open_cookie(); + } + } +} + +sub DESTROY { + my $self = $_[0]; + + if ($self->{lock}) { + unlink($lockfile, $self->{cookiefile}); + } +} + +sub save_cookie { + my ($self, $cookie) = @_; + my $fh = $self->{lock}; + + print $fh $cookie; + + close($fh); + + move($self->{cookiefile}, $cookiefile); + unlink($lockfile); + + $self->{lock} = undef; + $self->{cookiefile} = undef; +} + sub ssh_auth { my $self = $_[0]; my $now = time(); @@ -205,11 +323,12 @@ sub api { @cookies = (@cookies, "$cookie=" . $cookies->{$cookie}); } my $cookies = join("; ", @cookies); - #print "Cookies: $cookies\n"; + # print "Cookies: $cookies\n"; $req->header("Cookie" => $cookies); } else { if (exists($self->{sshkey})) { - $req->header("Authorization", $self->ssh_auth()); + $self->lock_cookie(); + $req->header("Authorization", $self->ssh_auth()) unless $self->check_cookie(); } } if ($data) { @@ -220,6 +339,11 @@ sub api { #print STDERR "req: " . $req->as_string() . "\n"; #$self->{ua}->add_handler(request_send => sub { my($req, $ua, $handler) = @_; print STDERR "req: " . $req->as_string() . "\n"; return; m_method => "GET"}); my $res = $self->{ua}->request($req); + if (($res->code == 401) && (keys % { $cookies } ) && exists($self->{sshkey})) { + $self->lock_cookie(); + $req->header("Authorization", $self->ssh_auth()) unless $self->check_cookie(); + $res = $self->{ua}->request($req); + } if ($res->code != 200) { #print STDERR $res->as_string(); die "$method $path: @{[$res->message()]} (HTTP @{[$res->code()]})\n"; @@ -227,9 +351,8 @@ sub api { my $headers = $res->headers(); my $cookie = $headers->{'set-cookie'}; if ($cookie) { - my @cookie = split /\s*([^=;]+)\s*=\s*([^=;]+)\s*;?/, $cookie; - #print "Cookie: $cookie[1]=$cookie[2]\n"; - $self->{cookies}{$cookie[1]} = $cookie[2]; + $self->process_cookie($cookie); + $self->save_cookie($cookie); } return $res->content();