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; |