File Coverage

blib/lib/WebService/HIBP.pm
Criterion Covered Total %
statement 122 142 85.9
branch 29 38 76.3
condition n/a
subroutine 22 22 100.0
pod 9 9 100.0
total 182 211 86.2


line stmt bran cond sub pod time code
1             package WebService::HIBP;
2              
3 2     2   132136 use strict;
  2         13  
  2         59  
4 2     2   12 use warnings;
  2         4  
  2         45  
5 2     2   1413 use JSON();
  2         24556  
  2         54  
6 2     2   863 use URI::Escape();
  2         2572  
  2         47  
7 2     2   1284 use LWP::UserAgent();
  2         88906  
  2         56  
8 2     2   1044 use Digest::SHA();
  2         6180  
  2         49  
9 2     2   1148 use Encode();
  2         20363  
  2         47  
10 2     2   14 use HTTP::Status();
  2         6  
  2         29  
11 2     2   1073 use Unicode::Normalize();
  2         4033  
  2         59  
12 2     2   989 use WebService::HIBP::Breach();
  2         6  
  2         39  
13 2     2   806 use WebService::HIBP::Paste();
  2         5  
  2         2584  
14              
15             our $VERSION = '0.15';
16              
17 8     8   34 sub _LENGTH_OF_PASSWORD_PREFIX { return 5; }
18              
19             sub new {
20 4     4 1 1434 my ( $class, %params ) = @_;
21 4         13 my $self = {};
22 4         11 bless $self, $class;
23 4         16 $self->{url} = 'https://haveibeenpwned.com/api/v3/';
24 4         12 $self->{password_url} = 'https://api.pwnedpasswords.com/range/';
25 4 100       26 if ( $params{user_agent} ) {
26 2         6 $self->{ua} = $params{user_agent};
27             }
28             else {
29             $self->{ua} =
30 2         24 LWP::UserAgent->new( agent => "WebService-HIBP/$VERSION " );
31 2         3272 $self->{ua}->env_proxy();
32             }
33 4 50       4632 if ( $params{api_key} ) {
34 0         0 $self->{ua}->default_header('hibp-api-key' => $params{api_key});
35             }
36 4         78 return $self;
37             }
38              
39             sub _get {
40 21     21   66 my ( $self, $url ) = @_;
41 21         142 my $response = $self->{ua}->get($url);
42 21         3775440 $self->{last_response} = $response;
43 21         99 return $response;
44             }
45              
46             sub last_request {
47 4     4 1 1418 my ($self) = @_;
48 4 100       18 if ( defined $self->{last_response} ) {
49 3         13 return $self->{last_response}->request();
50             }
51 1         6 return;
52             }
53              
54             sub last_response {
55 3     3 1 356 my ($self) = @_;
56 3 100       14 if ( defined $self->{last_response} ) {
57 2         9 return $self->{last_response};
58             }
59 1         7 return;
60             }
61              
62             sub data_classes {
63 3     3 1 1219 my ($self) = @_;
64 3         12 my $url = $self->{url} . 'dataclasses';
65 3         11 my $response = $self->_get($url);
66 3 100       14 if ( $response->is_success() ) {
67 1         21 my $json = JSON::decode_json( $response->decoded_content() );
68 1         609 my @classes;
69 1         4 foreach my $class ( @{$json} ) {
  1         5  
70 119         176 push @classes, $class;
71             }
72 1         37 return @classes;
73             }
74             else {
75 2         30 Carp::croak( "Failed to retrieve $url:" . $response->status_line() );
76             }
77             }
78              
79             sub breach {
80 4     4 1 4003021 my ( $self, $name ) = @_;
81 4         38 my $url = $self->{url} . 'breach/' . $name;
82 4         24 my $response = $self->_get($url);
83 4 100       18 if ( $response->is_success() ) {
    100          
84 1         19 my $json = JSON::decode_json( $response->decoded_content() );
85 1         610 return WebService::HIBP::Breach->new( %{$json} );
  1         18  
86             }
87             elsif ( $response->code() == HTTP::Status::HTTP_NOT_FOUND() ) {
88 1         28 return ();
89             }
90             else {
91 2         47 Carp::croak( "Failed to retrieve $url:" . $response->status_line() );
92             }
93             }
94              
95             sub pastes {
96 2     2 1 1246 my ( $self, $account ) = @_;
97             my $url =
98 2         14 $self->{url} . 'pasteaccount/' . URI::Escape::uri_escape($account);
99 2         61 my $response = $self->_get($url);
100 2 50       10 if ( $response->is_success() ) {
    50          
101 0         0 my $json = JSON::decode_json( $response->decoded_content() );
102 0         0 my @pastes;
103 0         0 foreach my $paste ( @{$json} ) {
  0         0  
104 0         0 push @pastes, WebService::HIBP::Paste->new( %{$paste} );
  0         0  
105             }
106 0         0 return @pastes;
107             }
108             elsif ( $response->code() == HTTP::Status::HTTP_NOT_FOUND() ) {
109 0         0 return ();
110             }
111             else {
112 2         67 Carp::croak( "Failed to retrieve $url:" . $response->status_line() );
113             }
114             }
115              
116             sub breaches {
117 5     5 1 4010720 my ( $self, %parameters ) = @_;
118 5         52 my $url = $self->{url} . 'breaches';
119 5 100       37 if ( $parameters{domain} ) {
120 2         29 $url .= '?domain=' . URI::Escape::uri_escape( $parameters{domain} );
121             }
122 5         196 my $response = $self->_get($url);
123 5 100       27 if ( $response->is_success() ) {
124 3         52 my $json = JSON::decode_json( $response->decoded_content() );
125 3         4476 my @breaches;
126 3         10 foreach my $breach ( @{$json} ) {
  3         11  
127 402         532 push @breaches, WebService::HIBP::Breach->new( %{$breach} );
  402         1765  
128             }
129 3         482 return @breaches;
130             }
131             else {
132 2         30 Carp::croak( "Failed to retrieve $url:" . $response->status_line() );
133             }
134             }
135              
136             sub account {
137 2     2 1 790 my ( $self, $account, %parameters ) = @_;
138             my $url =
139 2         16 $self->{url} . 'breachedaccount/' . URI::Escape::uri_escape($account);
140 2         66 my @filters;
141 2 50       8 if ( $parameters{unverified} ) {
142 0         0 push @filters, 'includeUnverified=true';
143             } else {
144 2         6 push @filters, 'includeUnverified=false';
145             }
146 2 50       7 if ( $parameters{truncate} ) {
147 0         0 push @filters, 'truncateResponse=true';
148             } else {
149 2         6 push @filters, 'truncateResponse=false';
150             }
151 2 50       8 if ( $parameters{domain} ) {
152             push @filters,
153 0         0 'domain=' . URI::Escape::uri_escape( $parameters{domain} );
154             }
155 2 50       12 if (@filters) {
156 2         77 $url .= q[?] . join q[&], @filters;
157             }
158 2         11 my $response = $self->_get($url);
159 2 50       10 if ( $response->is_success() ) {
    50          
160 0         0 my $json = JSON::decode_json( $response->decoded_content() );
161 0         0 my @breaches;
162 0         0 foreach my $breach ( @{$json} ) {
  0         0  
163 0         0 push @breaches, WebService::HIBP::Breach->new( %{$breach} );
  0         0  
164             }
165 0         0 return @breaches;
166             }
167             elsif ( $response->code() == HTTP::Status::HTTP_NOT_FOUND() ) {
168 0         0 return ();
169             }
170             else {
171 2         47 Carp::croak( "Failed to retrieve $url:" . $response->status_line() );
172             }
173             }
174              
175             sub password {
176 5     5 1 1580 my ( $self, $password ) = @_;
177 5         63 my $normalised = Unicode::Normalize::NFD($password)
178             ; # No API documentation on normalisation, chose NFD based on compatibility with the front end at https://haveibeenpwned.com/Passwords.
179 5         36 my $sha1 =
180             uc Digest::SHA::sha1_hex( Encode::encode( 'UTF-8', $normalised, 1 ) );
181 5         635 my $url = $self->{password_url} . substr $sha1, 0,
182             _LENGTH_OF_PASSWORD_PREFIX();
183 5         23 my $response = $self->_get($url);
184 5 100       26 if ( $response->is_success() ) {
185 3         53 my $remainder = substr $sha1, _LENGTH_OF_PASSWORD_PREFIX();
186 3         18 foreach my $line ( split /\r\n/smx, $response->decoded_content() ) {
187 1149         7409 my ( $pwned, $count ) = split /:/smx, $line;
188 1149 100       2235 if ( $pwned eq $remainder ) {
189 1         26 return $count;
190             }
191             }
192 2         65 return 0;
193             }
194             else {
195 2         34 Carp::croak( "Failed to retrieve $url:" . $response->status_line() );
196             }
197             }
198              
199             1; # End of WebService::HIBP
200             __END__