From 339312c92a71a0943ae34aab27b1026bb3479d0d Mon Sep 17 00:00:00 2001 From: Alexander Sulfrian Date: Tue, 21 Jun 2016 02:42:25 +0200 Subject: Make subclasses of Net::Server Spline::Socketmap is now a subclasses of Net::Server and Spline::Socketmap::Srs is a subclass of Spline::Socketmap. This way the Srs stuff is completly independent of the postfix-socketmap protocol handling. The config is located in the instances and configurable with Net::Server command line options or config file. --- Spline/Socketmap.pm | 103 +++++++++++++++++++-------- Spline/Socketmap/Srs.pm | 111 +++++++++++++++++++++++++++++ Spline/Srs.pm | 108 +++++++++++++--------------- srs | 27 ++++--- t/netstring.t | 44 ++++++++++++ t/socketmap.t | 132 ++++++++++++++++++++++++++++++++++ t/srs.t | 184 +++++++++++++++++++++++++++--------------------- 7 files changed, 530 insertions(+), 179 deletions(-) create mode 100644 Spline/Socketmap/Srs.pm create mode 100644 t/netstring.t create mode 100644 t/socketmap.t diff --git a/Spline/Socketmap.pm b/Spline/Socketmap.pm index d677166..af31c31 100644 --- a/Spline/Socketmap.pm +++ b/Spline/Socketmap.pm @@ -2,34 +2,29 @@ package Spline::Socketmap; use strict; use warnings; -use base qw(Net::Server::PreFork); +use Scalar::Util qw( openhandle ); -use base 'Exporter'; +use base qw( Exporter Net::Server::PreFork ); our @EXPORT = qw( ); -our @EXPORT_OK = qw( ); - -our $timeout = 10; -our $handler = undef; - - -sub call_handler($@) { - die 'No handler configured' unless ref($handler) eq 'CODE'; - - return &$handler(@_); +our @EXPORT_OK = qw( + lookup + handle_request + netstring_read + netstring_write + process_request + socketmap_protocol +); + +sub lookup($$$) { + die 'Not implemented'; } sub handle_request($$) { my $self = shift; - my ($data) = @_; + my $data = shift; my ($map, $key) = split(/ /, $data, 2); - my $result = call_handler($map, $key); - if (defined $result) { - $self->reply($result); - } - else { - $self->reply('TEMP Protocol error'); - } + return $self->lookup($map, $key); } sub netstring_read_length($) { @@ -38,9 +33,10 @@ sub netstring_read_length($) { local $/ = ':'; $length = <$fd>; - die "Cannot read netstring length" unless defined($length); + die 'Cannot read netstring length' unless defined($length); chomp $length; + die 'Invalid length' unless $length =~ m/\A\d+\z/; return $length; } @@ -48,9 +44,12 @@ sub netstring_read($) { my $fd = shift; my ($length, $data); + die 'Filehandle required' unless openhandle($fd); + $length = netstring_read_length($fd); if (read($fd, $data, $length) == $length) { - (getc() eq ',') or die "Closing , missing"; + my $char = getc($fd); + die "Closing , missing" if not defined $char or $char ne ','; } else { die 'Received only ' . length($data) . " of $length bytes"; @@ -65,32 +64,47 @@ sub netstring_write($$) { print $fd length($data).':'.$data.','; } -sub process_request($) { +sub socketmap_protocol($$) { my $self = shift; + my $input = shift; + my $result; eval { local $SIG{'ALRM'} = sub { die "Timed Out!\n" }; - alarm($timeout); + alarm($self->{server}->{timeout} // 0); - $self->handle_request(netstring_read(*STDIN)); + if (ref($input) eq 'CODE') { + $input = &$input; + } + $result = $self->handle_request($input); alarm(0); }; my $err = $@; alarm(0); - if ($err) { if ($err =~ /timed out/i) { - $self->reply('TEMP Timeout'); + return 'TEMP Timeout'; } else { chomp $err; - $self->reply("TEMP $err"); + return "TEMP $err"; } } + else { + return $result; + } } -sub reply($$) { +sub recv($) { + my $self = shift; + + my $input = netstring_read(*STDIN); + $self->log(3, $input); + return $input; +} + +sub send($$) { my $self = shift; my ($data) = @_; @@ -98,6 +112,37 @@ sub reply($$) { netstring_write(*STDOUT, $data); } +sub process_request($) { + my $self = shift; + + my $recv = sub { return $self->recv() }; + $self->send($self->socketmap_protocol($recv) // 'TEMP Protocol error'); +} + +sub options($$) { + my $self = shift; + my $prop = $self->{'server'}; + my $template = shift; + + $self->SUPER::options($template); + + # Timeout for one request + $prop->{'timeout'} ||= undef; + $template->{'timeout'} = \$prop->{'timeout'}; +} + +sub post_configure_hook { + my $self = shift; + my $prop = $self->{'server'}; + + if (!defined($prop->{'timeout'}) || $prop->{'timeout'} !~ /^\d+$/) { + $prop->{'timeout'} = 10; + } + elsif ($prop->{'timeout'} < 0) { + $prop->{'timeout'} = 0; + } +} + 1; # vim: set et ts=4: diff --git a/Spline/Socketmap/Srs.pm b/Spline/Socketmap/Srs.pm new file mode 100644 index 0000000..5917c00 --- /dev/null +++ b/Spline/Socketmap/Srs.pm @@ -0,0 +1,111 @@ +package Spline::Socketmap::Srs; + +use strict; +use warnings; + +use base qw( Spline::Socketmap ); +use Spline::Srs; + +sub lookup($$) { + my $self = shift; + my ($map, $key) = @_; + + $self->{srs}->handle($map, $key); +} + +sub options($$) { + my $self = shift; + my $template = shift; + my $prop = $self->{server}; + + $self->SUPER::options($template); + + # single value options + for my $opt (qw(alias secret_file max_age hash_length hash_min ignore_time srsalt_fallback)) { + $template->{$opt} = \$prop->{$opt}; + } + + # array options + for my $opt (qw(excludes secret)) { + if (! defined $prop->{$opt}) { + $prop->{$opt} = []; + } elsif (! ref $prop->{$opt}) { + $prop->{$opt} = [$prop->{$opt}]; + } + $template->{$opt} = $prop->{$opt}; + } + +} + +sub post_configure_hook { + my $self = shift; + my $prop = $self->{'server'}; + + # boolean values + for my $opt (qw(ignore_time srsalt_fallback)) { + if (defined $prop->{$opt}) { + $prop->{$opt} = 1; + } + else { + $prop->{$opt} = 0; + } + } + + # secrets + my @secrets = (); + if (defined $prop->{secret_file}) { + if (open(my $file, '<', $prop->{secret_file})) { + while (<$file>) { + chomp; + push @secrets, $_; + } + close($file); + } + else { + $self->log(1, 'ERROR: Cannot open secret_file: "' . $prop->{secret_file} . '"'); + } + } + + if (defined $prop->{secret} && scalar @{$prop->{secret}} ne 0) { + if (scalar @secrets gt 0) { + $self->log(1, 'WARNING: Using both "--secret" and "--secret-file" will '. + 'use the first line from the file for generating hashes'); + } + + for (@{$prop->{secret}}) { + push @secrets, $_; + } + } + + # default values + my %defaults = ( + max_age => 49, + hash_length => 5, + hash_min => 5, + ); + + for my $opt (keys %defaults) { + if (!defined($prop->{$opt}) || $prop->{$opt} !~ m/^\d+$/ || $prop->{$opt} le 0) { + if (defined $prop->{$opt}) { + $self->log(2, "Invalid value for '$opt', using default value '$defaults{$opt}'"); + } + + $prop->{$opt} = $defaults{$opt}; + } + } + + $self->{srs} = new Spline::Srs({ + alias => $prop->{alias}, + excludes => $prop->{excludes}, + secret => \@secrets, + max_age => $prop->{max_age}, + hash_length => $prop->{hash_length}, + hash_min => $prop->{hash_min}, + ignore_time => $prop->{ignore_time}, + srsalt_fallback => $prop->{srsalt_fallback}, + }); +} + +1; + +# vim: set et ts=4: diff --git a/Spline/Srs.pm b/Spline/Srs.pm index e799245..597e3ed 100644 --- a/Spline/Srs.pm +++ b/Spline/Srs.pm @@ -8,37 +8,44 @@ use Mail::SRS; use base 'Exporter'; our @EXPORT = qw( ); our @EXPORT_OK = qw( - config_set - config_get check_exclude - srs_forward - srs_reverse ); -my $config = { - alias => '', - excludes => [], - secret => '', - max_age => 49, - hash_length => 5, - hash_min => 5, - ignore_time => 0, - srsalt_fallback => 0, -}; - -my $srs = undef; - -sub config_set($$) { - my ($key, $value) = @_; - return unless defined $config->{$key}; - - $config->{$key} = $value; - $srs = undef; -} +sub new { + my $class = shift; + my $opts = shift; + + my $active_secret; + if (ref $opts->{secret} eq 'ARRAY') { + $active_secret = $opts->{secret}->[0]; + } + else { + $active_secret = $opts->{secret}; + } + + if (length($active_secret // '') < 20) { + die 'You need to configure a suitable secret'; + } + + if (!$opts->{alias}) { + die 'You need to configure an alias domain'; + } -sub config_get($) { - my $key = shift; - return $config->{$key}; + my $self = { + alias => $opts->{alias} // '', + excludes => $opts->{excludes} // [], + srsalt_fallback => $opts->{srsalt_fallback} // 0, + srs => new Mail::SRS( + Secret => $opts->{secret} // '', + MaxAge => $opts->{max_age} // 49, + HashLength => $opts->{hash_length} // 5, + HashMin => $opts->{hash_min} // 5, + IgnoreTimestamp => $opts->{ignore_time} // 0, + ), + }; + bless $self, $class; + + return $self; } sub check_exclude($@) { @@ -72,31 +79,29 @@ sub replace_srsalt_chars($) { return $addr; } -sub srs_forward($) { +sub forward($) { + my $self = shift; my $addr = shift; - return $addr if check_exclude($addr, $config->{excludes}); - - check_configured(); - return $srs->forward($addr, $config->{alias}); + return $addr if check_exclude($addr, $self->{excludes}); + return $self->{srs}->forward($addr, $self->{alias}); } -sub srs_reverse($) { +sub reverse($) { + my $self = shift; my $addr = shift; - check_configured(); - - if ($config->{srsalt_fallback}) { + if ($self->{srsalt_fallback}) { my $result; eval { - $result = $srs->reverse($addr); + $result = $self->{srs}->reverse($addr); }; my $err = $@; if ($err) { if ($err =~ m/Invalid hash/) { my $fallback = replace_srsalt_chars($addr); - return $srs->reverse($fallback); + return $self->{srs}->reverse($fallback); } die $err; @@ -105,38 +110,19 @@ sub srs_reverse($) { return $result; } - return $srs->reverse($addr); -} - -sub check_configured() { - return if defined $srs; - - if (length($config->{secret}) < 20) { - die 'You need to configure a suitable secret'; - } - - if (!$config->{alias}) { - die 'You need to configure an alias domain'; - } - - $srs = new Mail::SRS( - Secret => $config->{secret}, - MaxAge => $config->{max_age}, - HashLength => $config->{hash_length}, - HashMin => $config->{hash_min}, - IgnoreTimestamp => $config->{ignore_time}, - ); + return $self->{srs}->reverse($addr); } sub handle($$) { + my $self = shift; my ($map, $key) = @_; my $result; if ($map eq 'forward') { - $result = srs_forward($key); + $result = $self->forward($key); } elsif ($map eq 'reverse') { - $result = srs_reverse($key); + $result = $self->reverse($key); } else { return "PERM Invalid request"; diff --git a/srs b/srs index d7f6dfd..e8a2b32 100755 --- a/srs +++ b/srs @@ -4,18 +4,25 @@ use warnings; use FindBin; use lib $FindBin::Bin; -use Spline::Socketmap; -use Spline::Srs; +use English; + +use Spline::Socketmap::Srs; -$Spline::Socketmap::timeout = 10; -$Spline::Socketmap::handler = sub { - my ($map, $key) = @_; - Spline::Srs::handle($map, $key); +my $defaults = { + syslog_ident => 'srs', + log_level => 1, }; -Spline::Socketmap->run({ - syslog_ident => 'srs', - conf_file => ($< == 0 ? '' : $ENV{"HOME"}) . '/etc/srs.conf', -}); +# Load config file by default if it exists +my $config_file = '/etc/srs.conf'; +$config_file = $ENV{HOME} . $config_file if $UID != 0; +$defaults->{conf_file} = $config_file if -e $config_file; + +# Default to current user (if not root) +$defaults->{user} = $EUID if $EUID != 0; +$defaults->{group} = $EGID if $EGID != 0; + +my $server = Spline::Socketmap::Srs->new(); +$server->run($defaults); # vim: set et ts=4: diff --git a/t/netstring.t b/t/netstring.t new file mode 100644 index 0000000..de0b61e --- /dev/null +++ b/t/netstring.t @@ -0,0 +1,44 @@ +use strict; +use warnings; + +use Test::More tests => 11; +use Test::Exception; + +BEGIN { + use_ok 'Spline::Socketmap', qw( + netstring_read + netstring_write + ) or BAIL_OUT; +} + +sub output($) { + my $data = shift; + + my $output; + open(my $fh, '>', \$output); + netstring_write($fh, $data); + return $output; +} + +sub input($) { + my $data = shift; + + open(my $fh, '<', \$data); + return netstring_read($fh); +} + +# Netstring output +is(output('test'), '4:test,', 'Netstring output'); +is(output(''), '0:,', 'Empty netstring output'); +is(output('. '), '2:. ,', 'Preserve spaces'); + +# Netstring input +is(input('4:test,'), 'test', 'Basic netstring input'); +is(input('0:,'), '', 'Empty netstring input'); +throws_ok { input('4:test') } qr/, missing/, 'Missing final ,'; +throws_ok { input('5:test') } qr/4 of 5 bytes/, 'Data too short'; +throws_ok { input('4test') } qr/Invalid length/, 'Format error'; +throws_ok { input('') } qr/Cannot read .* length/, 'Empty input'; +throws_ok { netstring_read('') } qr/Filehandle required/, 'No filehandle'; + +# vim: set et ts=4: diff --git a/t/socketmap.t b/t/socketmap.t new file mode 100644 index 0000000..a788663 --- /dev/null +++ b/t/socketmap.t @@ -0,0 +1,132 @@ +use strict; +use warnings; + +use Test::More tests => 19; +use Test::Exception; + +BEGIN { + use_ok 'Spline::Socketmap', qw( + lookup + handle_request + socketmap_protocol + process_request + ) or BAIL_OUT; +} + +# Default handler +throws_ok { lookup(undef, 'map', 'key') } qr/Not implemented/, 'No handler'; + +my $socketmap = new Spline::Socketmap(); + +# Parsing socketmap input +my @args; +{ + no warnings 'redefine'; + local *Spline::Socketmap::lookup = sub { shift; @args = @_; return }; + is($socketmap->handle_request('map key with whitespace'), undef, + 'Protocol parsed'); + is_deeply(\@args, ['map', 'key with whitespace'], 'Parsing with whitespace'); +} + +{ + no warnings 'redefine'; + local *Spline::Socketmap::lookup = sub { return 'OK test' }; + is(handle_request($socketmap, 'map key'), 'OK test', 'Result passed'); +} + +# Socketmap protocol handling +{ + no warnings 'redefine'; + local *Spline::Socketmap::lookup = sub { }; + is($socketmap->socketmap_protocol('map key'), undef, 'Pass undef return value'); +} + +{ + no warnings 'redefine'; + local *Spline::Socketmap::lookup = sub { shift; shift; return shift }; + is($socketmap->socketmap_protocol('map key'), 'key', 'Pass return value'); +} + +{ + no warnings 'redefine'; + local *Spline::Socketmap::lookup = sub { sleep 2 }; + $socketmap->{server}->{timeout} = 1; + + is($socketmap->socketmap_protocol('map key'), 'TEMP Timeout', + 'Temporary error on timeout'); +} + +{ + no warnings 'redefine'; + local *Spline::Socketmap::lookup = sub { die 'Other error' }; + like($socketmap->socketmap_protocol('map key'), qr/TEMP Other error/, + 'Temporary error on die'); +} + +# process_request (Net::Server) +my ($input, $output, $log); +my $dummy = {}; +{ + package Dummy; + use base 'Spline::Socketmap'; + sub recv { return $input } + sub send { shift; $output = shift } + sub log { shift; shift; $log = shift } + bless $dummy, 'Dummy'; +} + +{ + no warnings 'redefine'; + local *Dummy::socketmap_protocol = sub { shift; return &{shift()} }; + + $input = 'OK test'; + process_request($dummy); + is($output, 'OK test', 'Send output if defined (ok)'); + + $input = 'NOTFOUND '; + process_request($dummy); + is($output, 'NOTFOUND ', 'Send output if defined (notfound)'); +} + +{ + no warnings 'redefine'; + local *Dummy::socketmap_protocol = sub { }; + + process_request($dummy); + like($output, qr/TEMP .* error/, 'Temporary error if result is undef'); +} + +{ + no warnings 'redefine'; + local *Dummy::recv = sub { sleep 10 }; + $dummy->{server}->{timeout} = 1; + + lives_ok { process_request($dummy) } 'Temporary error with hanging input'; + like($output, qr/TEMP Timeout/, 'Timeout on hanging input'); +} + +# recv +{ + my $recv_data; + open(my $fh, '<', \$recv_data); + local *STDIN = $fh; + + $log = undef; + $recv_data = '7:OK test,'; + is(Spline::Socketmap::recv($dummy), 'OK test', 'Receive works'); + is($log, 'OK test', 'Log data after receiving'); +} + +# send +{ + my $send_data; + open(my $fh, '>', \$send_data); + local *STDOUT = $fh; + + $log = undef; + lives_ok { Spline::Socketmap::send($dummy, 'OK test') } 'Sending works'; + is($send_data, '7:OK test,', 'Send data in netstring format'); + is($log, 'OK test', 'Log data before sending'); +} + +# vim: set et ts=4: diff --git a/t/srs.t b/t/srs.t index a1c8104..b6482b4 100644 --- a/t/srs.t +++ b/t/srs.t @@ -1,30 +1,16 @@ use strict; use warnings; -use Test::More tests => 39; +use Test::More tests => 35; use Test::Exception; use Test::MockModule; BEGIN { use_ok 'Spline::Srs', qw( - config_set - config_get check_exclude - srs_forward - srs_reverse ) or BAIL_OUT; } -# Test config -my $old_value = config_get('hash_length'); -isnt($old_value, undef, 'Config value exists'); -config_set('hash_length', $old_value+1); -isnt(config_get('hash_length'), $old_value, 'Config changed'); - -is(config_get('invalid'), undef, 'Invalid config value is undef'); -config_set('invalid', 'test'); -is(config_get('invalid'), undef, 'Cannot set invalid config values'); - # Testing ignores is(check_exclude('test@example.com',['example.com']), 1, 'Ignore matching domain'); is(check_exclude('test@example.de', ['example.com']), 0, 'Do not ignore non-matching domain'); @@ -32,80 +18,120 @@ is(check_exclude('test@test.example.com', ['example.com']), 0, 'Do not ignore su is(check_exclude('test@test.example.com', ['.example.com']), 1, 'Ignore sub-domain if requested'); # Try without config -throws_ok { srs_forward('something@example.com') } qr/configure.*secret/, 'Secret required'; -config_set('secret', 'abc'); -throws_ok { srs_forward('something@example.com') } qr/configure.*secret/, 'Long secret required'; -config_set('secret', '12345678901234567890'); -throws_ok { srs_forward('something@example.com') } qr/configure.*alias/, 'Alias required'; -config_set('alias', 'something'); -lives_ok { srs_forward('something@example.com') } 'Config complete'; +my %config = (); +throws_ok { new Spline::Srs(\%config) } qr/configure.*secret/, 'Secret required'; +$config{secret} = 'abc'; +throws_ok { new Spline::Srs(\%config) } qr/configure.*secret/, 'Long secret required'; +$config{secret} = '12345678901234567890'; +throws_ok { new Spline::Srs(\%config) } qr/configure.*alias/, 'Alias required'; +$config{alias} = 'something'; +lives_ok { new Spline::Srs(\%config) } 'Config complete'; # Testing ignores with config -config_set('alias', 'domain.invalid'); -is(srs_forward('test@domain.invalid'), 'test@domain.invalid', 'Exclude alias domain by default'); -isnt(srs_forward('test@example.com'), 'test@example.com', 'Not excluding something other'); +$config{alias} = 'domain.invalid'; +{ + my $srs = new Spline::Srs(\%config); + is($srs->forward('test@domain.invalid'), 'test@domain.invalid', + 'Exclude alias domain by default'); + isnt($srs->forward('test@example.com'), 'test@example.com', + 'Not excluding something other'); +} -config_set('excludes', ['example.com']); -is(srs_forward('test@domain.invalid'), 'test@domain.invalid', 'Exclude alias domain by default'); -is(srs_forward('test@example.com'), 'test@example.com', 'Exclude is working'); -isnt(srs_forward('test@test.example.com'), 'test@test.example.com', 'Excluded domain should not match subdomain'); +$config{excludes} = ['example.com']; +{ + my $srs = new Spline::Srs(\%config); + is($srs->forward('test@domain.invalid'), 'test@domain.invalid', + 'Exclude alias domain by default'); + is($srs->forward('test@example.com'), 'test@example.com', + 'Exclude is working'); + isnt($srs->forward('test@test.example.com'), 'test@test.example.com', + 'Excluded domain should not match subdomain'); +} -config_set('excludes', ['.example.com']); -is(srs_forward('test@domain.invalid'), 'test@domain.invalid', 'Exclude alias domain by default'); -is(srs_forward('test@test.example.com'), 'test@test.example.com', 'Exclude subdomains'); -isnt(srs_forward('test@example.com'), 'test@example.com', 'Excluded subdomain should not match domain'); +$config{excludes} = ['.example.com']; +{ + my $srs = new Spline::Srs(\%config); + is($srs->forward('test@domain.invalid'), 'test@domain.invalid', + 'Exclude alias domain by default'); + is($srs->forward('test@test.example.com'), 'test@test.example.com', + 'Exclude subdomains'); + isnt($srs->forward('test@example.com'), 'test@example.com', + 'Excluded subdomain should not match domain'); +} -config_set('excludes', ['example.com', '.example.com']); -is(srs_forward('test@domain.invalid'), 'test@domain.invalid', 'Exclude alias domain by default'); -is(srs_forward('test@test.example.com'), 'test@test.example.com', 'Multiple excludes'); -is(srs_forward('test@example.com'), 'test@example.com', 'Multiple excludes'); +$config{excludes} = ['example.com', '.example.com']; +{ + my $srs = new Spline::Srs(\%config); + is($srs->forward('test@domain.invalid'), 'test@domain.invalid', + 'Exclude alias domain by default'); + is($srs->forward('test@test.example.com'), 'test@test.example.com', + 'Multiple excludes'); + is($srs->forward('test@example.com'), 'test@example.com', + 'Multiple excludes'); +} # SRS Forward -config_set('secret', '12345678901234567890'); -config_set('alias', 'domain.invalid'); -config_set('excludes', undef); -my $result = srs_forward('alex@example.com'); -isnt($result, undef, 'Not undef'); -isnt($result, 'alex@example.com', 'Not rewritten'); -like($result, qr/^SRS0[+=-]/, 'SRS0 Prefix'); -like($result, qr/\@domain\.invalid$/, 'Rewrite to alias-Domain'); - -# Check SRS forward an backwards -my $address = 'alex@example.com'; -my $srs_address = srs_forward($address); -isnt($srs_address, undef, 'Forward works'); -isnt($srs_address, $address, 'Forward works'); -is(srs_reverse($srs_address), $address, 'Reverse maps back to original'); - -# Some checks requires a fixed time for deterministic output +$config{secret} = '12345678901234567890'; +$config{alias} = 'domain.invalid'; +$config{excludes} = undef; +$config{hash_length} = 6; my ($old_srs_address, $broken); { - no warnings 'redefine'; - my $timestamp_create = \&Mail::SRS::timestamp_create; - local *Mail::SRS::timestamp_create = sub { - my $self = shift; - $timestamp_create->($self, 1); - }; - - $old_srs_address = srs_forward($address); - isnt($old_srs_address, $srs_address, 'Time should have changed'); - is($old_srs_address, 'SRS0=R0OuN1=AA=example.com=alex@domain.invalid', 'exakt match with fixed time'); - - # Hash with broken chars - $broken = srs_forward('nk@example.com'); - is($broken, 'SRS0=s/6g+P=AA=example.com=nk@domain.invalid', 'srs address is as expected'); + my $srs = new Spline::Srs(\%config); + my $result = $srs->forward('alex@example.com'); + isnt($result, undef, 'Not undef'); + isnt($result, 'alex@example.com', 'Not rewritten'); + like($result, qr/^SRS0[+=-]/, 'SRS0 Prefix'); + like($result, qr/\@domain\.invalid$/, 'Rewrite to alias-Domain'); + + # Check SRS forward an backwards + my $address = 'alex@example.com'; + my $srs_address = $srs->forward($address); + isnt($srs_address, undef, 'Forward works'); + isnt($srs_address, $address, 'Forward works'); + is($srs->reverse($srs_address), $address, 'Reverse maps back to original'); + + # Some checks requires a fixed time for deterministic output + { + no warnings 'redefine'; + my $timestamp_create = \&Mail::SRS::timestamp_create; + local *Mail::SRS::timestamp_create = sub { + my $self = shift; + $timestamp_create->($self, 1); + }; + + $old_srs_address = $srs->forward($address); + isnt($old_srs_address, $srs_address, 'Time should have changed'); + is($old_srs_address, 'SRS0=R0OuN1=AA=example.com=alex@domain.invalid', + 'exakt match with fixed time'); + + # Hash with broken chars + $broken = $srs->forward('nk@example.com'); + is($broken, 'SRS0=s/6g+P=AA=example.com=nk@domain.invalid', + 'srs address is as expected'); + } + + # Timestamp should timeout (but old timestamps can be ignored) + throws_ok { $srs->reverse($old_srs_address) } qr/Invalid timestamp/, + 'Timestamp should be to old'; } -# Timestamp should timeout (but old timestamps can be ignored) -throws_ok { srs_reverse($old_srs_address) } qr/Invalid timestamp/, 'Timestamp should be to old'; -config_set('ignore_time', 1); -lives_ok { srs_reverse($old_srs_address) } 'Ignore old timestamp'; - -# libsrs_alt uses different base64 chars and should also work -lives_ok { srs_reverse($broken) } 'Reverse with special chars'; -$broken = 'SRS0=s.6g_P=AA=example.com=nk@domain.invalid'; -throws_ok { srs_reverse($broken) } qr/Invalid hash/, 'Reverse with legacy chars'; -config_set('srsalt_fallback', 1); -is(srs_reverse($broken), 'nk@example.com', 'Reverse with legacy chars'); +$config{'ignore_time'} = 1; +{ + my $srs = new Spline::Srs(\%config); + lives_ok { $srs->reverse($old_srs_address) } 'Ignore old timestamp'; + + # libsrs_alt uses different base64 chars and should also work + lives_ok { $srs->reverse($broken) } 'Reverse with special chars'; + $broken = 'SRS0=s.6g_P=AA=example.com=nk@domain.invalid'; + throws_ok { $srs->reverse($broken) } qr/Invalid hash/, + 'Reverse with legacy chars'; +} + +$config{'srsalt_fallback'} = 1; +{ + my $srs = new Spline::Srs(\%config); + is($srs->reverse($broken), 'nk@example.com', 'Reverse with legacy chars'); +} # vim: set et ts=4: -- cgit v1.2.3-1-g7c22