File Coverage

blib/lib/WebService/HIBP.pm
Criterion Covered Total %
statement 138 138 100.0
branch 36 36 100.0
condition n/a
subroutine 22 22 100.0
pod 9 9 100.0
total 205 205 100.0


line stmt bran cond sub pod time code
1             package WebService::HIBP;
2              
3 2     2   133178 use strict;
  2         15  
  2         59  
4 2     2   9 use warnings;
  2         4  
  2         61  
5 2     2   1316 use JSON();
  2         25141  
  2         57  
6 2     2   868 use URI::Escape();
  2         2625  
  2         46  
7 2     2   1296 use LWP::UserAgent();
  2         88191  
  2         56  
8 2     2   1053 use Digest::SHA();
  2         6121  
  2         51  
9 2     2   1135 use Encode();
  2         20486  
  2         52  
10 2     2   14 use HTTP::Status();
  2         4  
  2         29  
11 2     2   1081 use Unicode::Normalize();
  2         4112  
  2         69  
12 2     2   992 use WebService::HIBP::Breach();
  2         5  
  2         38  
13 2     2   835 use WebService::HIBP::Paste();
  2         5  
  2         2589  
14              
15             our $VERSION = '0.14';
16              
17 8     8   29 sub _LENGTH_OF_PASSWORD_PREFIX { return 5; }
18              
19             sub new {
20 4     4 1 1417 my ( $class, %params ) = @_;
21 4         10 my $self = {};
22 4         10 bless $self, $class;
23 4         15 $self->{url} = 'https://haveibeenpwned.com/api/v2/';
24 4         12 $self->{password_url} = 'https://api.pwnedpasswords.com/range/';
25 4 100       18 if ( $params{user_agent} ) {
26 2         7 $self->{ua} = $params{user_agent};
27             }
28             else {
29             $self->{ua} =
30 2         19 LWP::UserAgent->new( agent => "WebService-HIBP/$VERSION " );
31 2         3171 $self->{ua}->env_proxy();
32             }
33 4         4513 return $self;
34             }
35              
36             sub _get {
37 28     28   109 my ( $self, $url ) = @_;
38 28         191 my $response = $self->{ua}->get($url);
39 28         5576748 $self->{last_response} = $response;
40 28         124 return $response;
41             }
42              
43             sub last_request {
44 4     4 1 1298 my ($self) = @_;
45 4 100       15 if ( defined $self->{last_response} ) {
46 3         12 return $self->{last_response}->request();
47             }
48 1         7 return;
49             }
50              
51             sub last_response {
52 5     5 1 334 my ($self) = @_;
53 5 100       22 if ( defined $self->{last_response} ) {
54 4         19 return $self->{last_response};
55             }
56 1         4 return;
57             }
58              
59             sub data_classes {
60 3     3 1 1161 my ($self) = @_;
61 3         13 my $url = $self->{url} . 'dataclasses';
62 3         11 my $response = $self->_get($url);
63 3 100       13 if ( $response->is_success() ) {
64 1         19 my $json = JSON::decode_json( $response->decoded_content() );
65 1         617 my @classes;
66 1         3 foreach my $class ( @{$json} ) {
  1         4  
67 118         174 push @classes, $class;
68             }
69 1         28 return @classes;
70             }
71             else {
72 2         27 Carp::croak( "Failed to retrieve $url:" . $response->status_line() );
73             }
74             }
75              
76             sub breach {
77 4     4 1 4002314 my ( $self, $name ) = @_;
78 4         43 my $url = $self->{url} . 'breach/' . $name;
79 4         19 my $response = $self->_get($url);
80 4 100       17 if ( $response->is_success() ) {
    100          
81 1         379 my $json = JSON::decode_json( $response->decoded_content() );
82 1         160 return WebService::HIBP::Breach->new( %{$json} );
  1         13  
83             }
84             elsif ( $response->code() == HTTP::Status::HTTP_NOT_FOUND() ) {
85 1         21 return ();
86             }
87             else {
88 2         45 Carp::croak( "Failed to retrieve $url:" . $response->status_line() );
89             }
90             }
91              
92             sub pastes {
93 4     4 1 4002877 my ( $self, $account ) = @_;
94             my $url =
95 4         85 $self->{url} . 'pasteaccount/' . URI::Escape::uri_escape($account);
96 4         194 my $response = $self->_get($url);
97 4 100       16 if ( $response->is_success() ) {
    100          
98 1         15 my $json = JSON::decode_json( $response->decoded_content() );
99 1         303 my @pastes;
100 1         2 foreach my $paste ( @{$json} ) {
  1         4  
101 66         82 push @pastes, WebService::HIBP::Paste->new( %{$paste} );
  66         202  
102             }
103 1         42 return @pastes;
104             }
105             elsif ( $response->code() == HTTP::Status::HTTP_NOT_FOUND() ) {
106 1         35 return ();
107             }
108             else {
109 2         46 Carp::croak( "Failed to retrieve $url:" . $response->status_line() );
110             }
111             }
112              
113             sub breaches {
114 5     5 1 4002828 my ( $self, %parameters ) = @_;
115 5         51 my $url = $self->{url} . 'breaches';
116 5 100       38 if ( $parameters{domain} ) {
117 2         22 $url .= '?domain=' . URI::Escape::uri_escape( $parameters{domain} );
118             }
119 5         85 my $response = $self->_get($url);
120 5 100       22 if ( $response->is_success() ) {
121 3         51 my $json = JSON::decode_json( $response->decoded_content() );
122 3         3532 my @breaches;
123 3         6 foreach my $breach ( @{$json} ) {
  3         9  
124 364         429 push @breaches, WebService::HIBP::Breach->new( %{$breach} );
  364         1402  
125             }
126 3         727 return @breaches;
127             }
128             else {
129 2         30 Carp::croak( "Failed to retrieve $url:" . $response->status_line() );
130             }
131             }
132              
133             sub account {
134 7     7 1 8003416 my ( $self, $account, %parameters ) = @_;
135             my $url =
136 7         115 $self->{url} . 'breachedaccount/' . URI::Escape::uri_escape($account);
137 7         460 my @filters;
138 7 100       47 if ( $parameters{unverified} ) {
139 1         7 push @filters, 'includeUnverified=true';
140             }
141 7 100       34 if ( $parameters{truncate} ) {
142 1         6 push @filters, 'truncateResponse=true';
143             }
144 7 100       31 if ( $parameters{domain} ) {
145             push @filters,
146 1         7 'domain=' . URI::Escape::uri_escape( $parameters{domain} );
147             }
148 7 100       47 if (@filters) {
149 3         125 $url .= q[?] . join q[&], @filters;
150             }
151 7         38 my $response = $self->_get($url);
152 7 100       37 if ( $response->is_success() ) {
    100          
153 4         72 my $json = JSON::decode_json( $response->decoded_content() );
154 4         2979 my @breaches;
155 4         14 foreach my $breach ( @{$json} ) {
  4         14  
156 229         321 push @breaches, WebService::HIBP::Breach->new( %{$breach} );
  229         925  
157             }
158 4         220 return @breaches;
159             }
160             elsif ( $response->code() == HTTP::Status::HTTP_NOT_FOUND() ) {
161 1         30 return ();
162             }
163             else {
164 2         44 Carp::croak( "Failed to retrieve $url:" . $response->status_line() );
165             }
166             }
167              
168             sub password {
169 5     5 1 1309 my ( $self, $password ) = @_;
170 5         57 my $normalised = Unicode::Normalize::NFD($password)
171             ; # No API documentation on normalisation, chose NFD based on compatibility with the front end at https://haveibeenpwned.com/Passwords.
172 5         34 my $sha1 =
173             uc Digest::SHA::sha1_hex( Encode::encode( 'UTF-8', $normalised, 1 ) );
174 5         452 my $url = $self->{password_url} . substr $sha1, 0,
175             _LENGTH_OF_PASSWORD_PREFIX();
176 5         18 my $response = $self->_get($url);
177 5 100       24 if ( $response->is_success() ) {
178 3         48 my $remainder = substr $sha1, _LENGTH_OF_PASSWORD_PREFIX();
179 3         16 foreach my $line ( split /\r\n/smx, $response->decoded_content() ) {
180 1140         6898 my ( $pwned, $count ) = split /:/smx, $line;
181 1140 100       2126 if ( $pwned eq $remainder ) {
182 1         26 return $count;
183             }
184             }
185 2         67 return 0;
186             }
187             else {
188 2         48 Carp::croak( "Failed to retrieve $url:" . $response->status_line() );
189             }
190             }
191              
192             1; # End of WebService::HIBP
193             __END__