| blib/lib/Finance/Bank/Natwest/Connection.pm | |||
|---|---|---|---|
| Criterion | Covered | Total | % |
| statement | 99 | 105 | 94.2 |
| branch | 36 | 58 | 62.0 |
| condition | 9 | 24 | 37.5 |
| subroutine | 15 | 15 | 100.0 |
| pod | 0 | 3 | 0.0 |
| total | 159 | 205 | 77.5 |
| line | stmt | bran | cond | sub | pod | time | code |
|---|---|---|---|---|---|---|---|
| 1 | package Finance::Bank::Natwest::Connection; | ||||||
| 2 | 3 | 3 | 1544 | use strict; | |||
| 3 | 8 | ||||||
| 3 | 116 | ||||||
| 3 | 3 | 3 | 16 | use vars qw( $VERSION ); | |||
| 3 | 6 | ||||||
| 3 | 142 | ||||||
| 4 | 3 | 3 | 16 | use Carp; | |||
| 3 | 5 | ||||||
| 3 | 199 | ||||||
| 5 | 3 | 3 | 16 | use LWP::UserAgent; | |||
| 3 | 6 | ||||||
| 3 | 171 | ||||||
| 6 | |||||||
| 7 | $VERSION = '0.04'; | ||||||
| 8 | |||||||
| 9 | require Finance::Bank::Natwest; | ||||||
| 10 | |||||||
| 11 | 3 | 3 | 14 | use constant POSS_PIN => { first => 0, second => 1, third => 2, fourth => 3 }; | |||
| 3 | 7 | ||||||
| 3 | 335 | ||||||
| 12 | 3 | 5400 | use constant POSS_PASS => | ||||
| 13 | { first => 0, second => 1, third => 2, fourth => 3, fifth => 4, | ||||||
| 14 | sixth => 5, seventh => 6, eighth => 7, ninth => 8, tenth => 9, | ||||||
| 15 | eleventh => 10, twelfth => 11, thirteenth => 12, fourteenth => 13, | ||||||
| 16 | fifteenth => 14, sixteenth => 15, seventeenth => 16, | ||||||
| 17 | eighteenth => 17, nineteenth => 18, twentieth => 19 | ||||||
| 18 | 3 | 3 | 15 | }; | |||
| 3 | 4 | ||||||
| 19 | |||||||
| 20 | |||||||
| 21 | sub new{ | ||||||
| 22 | 31 | 31 | 0 | 8216 | my ($class, %opts) = @_; | ||
| 23 | |||||||
| 24 | 31 | 74 | my $self = bless {}, $class; | ||||
| 25 | |||||||
| 26 | 31 | 66 | 126 | $self->{url_base} = $opts{url_base} || Finance::Bank::Natwest->url_base; | |||
| 27 | |||||||
| 28 | 31 | 78 | $self->_set_credentials( %opts ); | ||||
| 29 | 4 | 20 | $self->_new_ua( %opts ); | ||||
| 30 | |||||||
| 31 | 4 | 26 | return $self; | ||||
| 32 | } | ||||||
| 33 | |||||||
| 34 | sub _new_ua{ | ||||||
| 35 | 4 | 4 | 11 | my ($self, %opts) = @_; | |||
| 36 | |||||||
| 37 | 4 | 5 | my %proxy; | ||||
| 38 | |||||||
| 39 | 4 | 50 | 13 | if (exists $opts{proxy}) { | |||
| 40 | 0 | 0 | $proxy{env_proxy} = 0; | ||||
| 41 | 0 | 0 | 0 | 0 | $proxy{proxy} = $opts{proxy} if | ||
| 42 | $opts{proxy} ne 'no' and $opts{proxy} ne 'env'; | ||||||
| 43 | 0 | 0 | 0 | $proxy{env_proxy} = 1 if $opts{proxy} eq 'env'; | |||
| 44 | } else { | ||||||
| 45 | 4 | 10 | $proxy{env_proxy} = 1; | ||||
| 46 | } | ||||||
| 47 | |||||||
| 48 | 4 | 43 | $self->{ua} = LWP::UserAgent->new( | ||||
| 49 | env_proxy => $proxy{env_proxy}, | ||||||
| 50 | keep_alive => 1, | ||||||
| 51 | timeout => 30, | ||||||
| 52 | cookie_jar => {}, | ||||||
| 53 | requests_redirectable => [ 'GET', 'HEAD', 'POST' ], | ||||||
| 54 | agent => "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)" | ||||||
| 55 | ); | ||||||
| 56 | |||||||
| 57 | 4 | 50 | 32 | $self->{ua}->proxy('https', $proxy{proxy}) if exists $proxy{proxy}; | |||
| 58 | } | ||||||
| 59 | |||||||
| 60 | sub _set_credentials{ | ||||||
| 61 | 31 | 31 | 60 | my ($self, %opts) = @_; | |||
| 62 | |||||||
| 63 | 31 | 100 | 171 | croak "Must provide either a premade credentials object or ". | |||
| 64 | "a class name together with options, stopped" if | ||||||
| 65 | !exists $opts{credentials}; | ||||||
| 66 | |||||||
| 67 | 25 | 100 | 57 | if (ref($opts{credentials})) { | |||
| 68 | 9 | 100 | 66 | croak "Can't accept credential options if supplying a premade ". | |||
| 69 | "credentials object, stopped" if | ||||||
| 70 | exists $opts{credentials_options}; | ||||||
| 71 | |||||||
| 72 | 3 | 100 | 10 | croak "Not a valid credentials object, stopped" unless | |||
| 73 | $self->_isa_credentials($opts{credentials}); | ||||||
| 74 | |||||||
| 75 | 1 | 3 | $self->{credentials} = $opts{credentials}; | ||||
| 76 | } else { | ||||||
| 77 | 16 | 100 | 115 | croak "Must provide credential options unless suppying a premade ". | |||
| 78 | "credentials object, stopped" if | ||||||
| 79 | !exists $opts{credentials_options}; | ||||||
| 80 | |||||||
| 81 | 8 | 25 | $self->{credentials} = | ||||
| 82 | $self->_new_credentials( | ||||||
| 83 | $opts{credentials}, $opts{credentials_options} | ||||||
| 84 | ); | ||||||
| 85 | }; | ||||||
| 86 | } | ||||||
| 87 | |||||||
| 88 | sub _new_credentials{ | ||||||
| 89 | 8 | 8 | 20 | my ($self, $class, $options) = @_; | |||
| 90 | |||||||
| 91 | 8 | 50 | 63 | croak "Invalid class name, stopped" if | |||
| 92 | $class !~ /^(?:\w|::)+$/; | ||||||
| 93 | |||||||
| 94 | 8 | 18 | my $full_class = "Finance::Bank::Natwest::CredentialsProvider::$class"; | ||||
| 95 | |||||||
| 96 | 8 | 895 | eval "local \$SIG{'__DIE__'}; | ||||
| 97 | local \$SIG{'__WARN__'}; | ||||||
| 98 | require $full_class; | ||||||
| 99 | "; | ||||||
| 100 | 8 | 100 | 51 | croak "Not a valid credentials class, stopped" | |||
| 101 | if $@; | ||||||
| 102 | |||||||
| 103 | 7 | 50 | 20 | croak "Not a valid credentials class, stopped" | |||
| 104 | unless $self->_isa_credentials($full_class); | ||||||
| 105 | |||||||
| 106 | { | ||||||
| 107 | 7 | 8 | local $Carp::CarpLevel = $Carp::CarpLevel + 1; | ||||
| 7 | 14 | ||||||
| 108 | 7 | 8 | return $full_class->new(%{$options}); | ||||
| 7 | 33 | ||||||
| 109 | } | ||||||
| 110 | } | ||||||
| 111 | |||||||
| 112 | sub _isa_credentials{ | ||||||
| 113 | 10 | 10 | 17 | my ($self, $credentials) = @_; | |||
| 114 | |||||||
| 115 | 10 | 23 | my @required_subs = qw( new get_start get_stop get_identity get_pinpass ); | ||||
| 116 | |||||||
| 117 | 10 | 19 | foreach my $sub (@required_subs) { | ||||
| 118 | 42 | 100 | 45 | return unless defined eval { | |||
| 119 | 42 | 91 | local $SIG{'__DIE__'}; | ||||
| 120 | 42 | 94 | local $SIG{'__WARN__'}; | ||||
| 121 | 42 | 364 | $credentials->can($sub); | ||||
| 122 | }; | ||||||
| 123 | } | ||||||
| 124 | |||||||
| 125 | 8 | 33 | return 1; | ||||
| 126 | } | ||||||
| 127 | |||||||
| 128 | sub login{ | ||||||
| 129 | 3 | 3 | 0 | 6 | my ($self) = @_; | ||
| 130 | |||||||
| 131 | 3 | 4 | my $page; | ||||
| 132 | |||||||
| 133 | 3 | 8 | $self->{login_ok} = 0; | ||||
| 134 | 3 | 7 | $self->{in_login} = 1; | ||||
| 135 | 3 | 5 | delete $self->{rb_id}; | ||||
| 136 | |||||||
| 137 | 3 | 14 | $self->{credentials}->get_start(); | ||||
| 138 | |||||||
| 139 | 3 | 11 | my $identity = $self->{credentials}->get_identity(); | ||||
| 140 | |||||||
| 141 | 3 | 25 | ($self->{rb_id}, $page) = $self->post( 'logon.asp', | ||||
| 142 | { DBIDa => $identity->{dob}, DBIDb => $identity->{uid}, | ||||||
| 143 | radType => '', scriptingon => 'yup' } ); | ||||||
| 144 | |||||||
| 145 | 3 | 50 | 475 | croak "Error during login process. " . | |||
| 146 | "The website is temporarily unavailable, stopped" if | ||||||
| 147 | $page =~ m|Service Temporarily Unvailable|i; | ||||||
| 148 | |||||||
| 149 | 3 | 50 | 19 | croak "Error during login process, stopped" if | |||
| 150 | $page =~ m| .*? |i; |
||||||
| 151 | |||||||
| 152 | 3 | 50 | 33 | croak "Error during login process. " . | |||
| 153 | "Current page cannot be recognised, stopped" unless | ||||||
| 154 | $page =~ m# | ||||||
| 155 | Please \s enter \s the \s | ||||||
| 156 | ([a-z]{5,6}), \s ([a-z]{5,6}) \s and \s ([a-z]{5,6}) \s | ||||||
| 157 | digits \s from \s your \s (?:Security \s Number|PIN): | ||||||
| 158 | #ix; | ||||||
| 159 | |||||||
| 160 | 3 | 50 | 33 | 56 | croak "Error during login process. " . | ||
| 33 | |||||||
| 161 | "Unrecognised pin request ($1, $2, $3), stopped" unless | ||||||
| 162 | exists POSS_PIN->{$1} && | ||||||
| 163 | exists POSS_PIN->{$2} && | ||||||
| 164 | exists POSS_PIN->{$3}; | ||||||
| 165 | |||||||
| 166 | 3 | 14 | my $pin_digits = [ POSS_PIN->{$1}, POSS_PIN->{$2}, POSS_PIN->{$3} ]; | ||||
| 167 | |||||||
| 168 | 3 | 50 | 29 | croak "Error during login process. " . | |||
| 169 | "Current page cannot be recognised, stopped" unless | ||||||
| 170 | $page =~ m| | ||||||
| 171 | Please \s enter \s the \s | ||||||
| 172 | ([a-z]{5,11}), \s ([a-z]{5,11}) \s and \s ([a-z]{5,11}) \s | ||||||
| 173 | characters \s from \s your \s Password: | ||||||
| 174 | |ix; | ||||||
| 175 | |||||||
| 176 | 3 | 50 | 33 | 37 | croak "Error during login process. " . | ||
| 33 | |||||||
| 177 | "Unrecognised password request ($1, $2, $3), stopped" unless | ||||||
| 178 | exists POSS_PASS->{$1} && | ||||||
| 179 | exists POSS_PASS->{$2} && | ||||||
| 180 | exists POSS_PASS->{$3}; | ||||||
| 181 | |||||||
| 182 | 3 | 14 | my $pass_chars = [ POSS_PASS->{$1}, POSS_PASS->{$2}, POSS_PASS->{$3} ]; | ||||
| 183 | |||||||
| 184 | 3 | 25 | my $pinpass = $self->{credentials}->get_pinpass( $pin_digits, $pass_chars ); | ||||
| 185 | 3 | 19 | $self->{credentials}->get_stop(); | ||||
| 186 | |||||||
| 187 | 3 | 37 | $page = $self->post('Logon-PinPass.asp', | ||||
| 188 | { pin1 => $pinpass->{pin}[0], | ||||||
| 189 | pin2 => $pinpass->{pin}[1], | ||||||
| 190 | pin3 => $pinpass->{pin}[2], | ||||||
| 191 | pass1 => $pinpass->{password}[0], | ||||||
| 192 | pass2 => $pinpass->{password}[1], | ||||||
| 193 | pass3 => $pinpass->{password}[2], | ||||||
| 194 | buttonComplete => 'Submitted', | ||||||
| 195 | buttonFinish => 'Finish' } ); | ||||||
| 196 | |||||||
| 197 | 3 | 50 | 169 | $page = $self->post('LogonMessage.asp', { buttonOK => 'Next' }) if | |||
| 198 | $page =~ m|LogonMessage\.asp|i; | ||||||
| 199 | |||||||
| 200 | 3 | 50 | 14 | croak "Error during login process, stopped" if | |||
| 201 | $page =~ m| .*? |i; |
||||||
| 202 | |||||||
| 203 | 3 | 7 | $self->{login_ok} = 1; | ||||
| 204 | 3 | 22 | delete $self->{in_login}; | ||||
| 205 | } | ||||||
| 206 | |||||||
| 207 | sub post{ | ||||||
| 208 | 8 | 8 | 0 | 13 | my $self = shift; | ||
| 209 | |||||||
| 210 | 8 | 100 | 33 | 62 | $self->login(@_) | ||
| 211 | if !$self->{login_ok} and !exists $self->{in_login}; | ||||||
| 212 | |||||||
| 213 | 8 | 26 | my $resp = $self->_post(@_); | ||||
| 214 | |||||||
| 215 | 8 | 50 | 23 | if ($self->_check_expired($resp)) { | |||
| 216 | 0 | 0 | $self->_login(@_); | ||||
| 217 | |||||||
| 218 | 0 | 0 | $resp = $self->_post(@_); | ||||
| 219 | 0 | 0 | 0 | croak "Error talking to nwolb. " . | |||
| 220 | "Session has timed out even though only just logged in, stopped" | ||||||
| 221 | if $self->_check_expired($resp); | ||||||
| 222 | } | ||||||
| 223 | |||||||
| 224 | 8 | 50 | 894 | return unless defined wantarray; | |||
| 225 | |||||||
| 226 | 8 | 100 | 49 | if (wantarray) { | |||
| 227 | 3 | 18 | return (($resp->base->path_segments)[2], $resp->content); | ||||
| 228 | } else { | ||||||
| 229 | 5 | 22 | return $resp->content; | ||||
| 230 | } | ||||||
| 231 | } | ||||||
| 232 | |||||||
| 233 | sub _check_expired{ | ||||||
| 234 | 8 | 8 | 14 | my ($self, $resp) = @_; | |||
| 235 | |||||||
| 236 | 8 | 45 | return lc(($resp->base->path_segments)[-1]) eq 'exit.asp'; | ||||
| 237 | } | ||||||
| 238 | |||||||
| 239 | sub _post{ | ||||||
| 240 | 8 | 8 | 10 | my $self = shift; | |||
| 241 | 8 | 11 | my $url = shift; | ||||
| 242 | 8 | 9 | my $full_url; | ||||
| 243 | |||||||
| 244 | 8 | 100 | 28 | if (exists $self->{rb_id}) { | |||
| 245 | 5 | 19 | $full_url = $self->{url_base} . $self->{rb_id} . '/' . $url; | ||||
| 246 | } else { | ||||||
| 247 | 3 | 9 | $full_url = $self->{url_base} . $url; | ||||
| 248 | } | ||||||
| 249 | |||||||
| 250 | 8 | 87 | my $resp = $self->{ua}->post($full_url, @_); | ||||
| 251 | |||||||
| 252 | 8 | 50 | 35511 | croak "Error talking to nwolb: " . $resp->message . ", stopped" | |||
| 253 | if !$resp->is_success; | ||||||
| 254 | |||||||
| 255 | 8 | 50 | 66 | 541 | croak "Unknown error talking to nwolb, stopped" | ||
| 256 | if !exists $self->{in_login} and | ||||||
| 257 | lc($resp->base->as_string) ne lc($full_url); | ||||||
| 258 | |||||||
| 259 | 8 | 237 | return $resp; | ||||
| 260 | } | ||||||
| 261 | |||||||
| 262 | 1; |