File Coverage

blib/lib/WebService/HIBP.pm
Criterion Covered Total %
statement 132 132 100.0
branch 30 30 100.0
condition n/a
subroutine 21 21 100.0
pod 9 9 100.0
total 192 192 100.0


line stmt bran cond sub pod time code
1             package WebService::HIBP;
2              
3 2     2   118082 use strict;
  2         10  
  2         49  
4 2     2   9 use warnings;
  2         3  
  2         51  
5 2     2   1131 use JSON();
  2         20899  
  2         44  
6 2     2   747 use URI::Escape();
  2         2137  
  2         40  
7 2     2   1044 use LWP::UserAgent();
  2         79758  
  2         44  
8 2     2   966 use Digest::SHA();
  2         5069  
  2         41  
9 2     2   910 use Encode();
  2         17461  
  2         45  
10 2     2   921 use Unicode::Normalize();
  2         3506  
  2         56  
11 2     2   791 use WebService::HIBP::Breach();
  2         4  
  2         31  
12 2     2   633 use WebService::HIBP::Paste();
  2         4  
  2         2075  
13              
14             our $VERSION = '0.12';
15              
16 8     8   29 sub _LENGTH_OF_PASSWORD_PREFIX { return 5; }
17              
18             sub new {
19 4     4 1 2342 my ( $class, %params ) = @_;
20 4         57 my $self = {};
21 4         14 bless $self, $class;
22 4         15 $self->{url} = 'https://haveibeenpwned.com/api/v2/';
23 4         10 $self->{password_url} = 'https://api.pwnedpasswords.com/range/';
24 4 100       18 if ( $params{user_agent} ) {
25 2         6 $self->{ua} = $params{user_agent};
26             }
27             else {
28             $self->{ua} =
29 2         21 LWP::UserAgent->new( agent => "WebService-HIBP/$VERSION " );
30 2         2802 $self->{ua}->env_proxy();
31             }
32 4         4186 return $self;
33             }
34              
35             sub _get {
36 24     24   63 my ( $self, $url ) = @_;
37 24         181 my $response = $self->{ua}->get($url);
38 24         3995676 $self->{last_response} = $response;
39 24         159 return $response;
40             }
41              
42             sub last_request {
43 4     4 1 1536 my ($self) = @_;
44 4 100       16 if ( defined $self->{last_response} ) {
45 3         9 return $self->{last_response}->request();
46             }
47 1         7 return;
48             }
49              
50             sub last_response {
51 2     2 1 496 my ($self) = @_;
52 2 100       7 if ( defined $self->{last_response} ) {
53 1         6 return $self->{last_response};
54             }
55 1         5 return;
56             }
57              
58             sub data_classes {
59 3     3 1 1098 my ($self) = @_;
60 3         11 my $url = $self->{url} . 'dataclasses';
61 3         10 my $response = $self->_get($url);
62 3 100       10 if ( $response->is_success() ) {
63 1         16 my $json = JSON::decode_json( $response->decoded_content() );
64 1         463 my @classes;
65 1         1 foreach my $class ( @{$json} ) {
  1         4  
66 117         140 push @classes, $class;
67             }
68 1         20 return @classes;
69             }
70             else {
71 2         22 Carp::croak( "Failed to retrieve $url:" . $response->status_line() );
72             }
73             }
74              
75             sub breach {
76 3     3 1 2006196 my ( $self, $name ) = @_;
77 3         13 my $url = $self->{url} . 'breach/' . $name;
78 3         11 my $response = $self->_get($url);
79 3 100       14 if ( $response->is_success() ) {
80 1         15 my $json = JSON::decode_json( $response->decoded_content() );
81 1         447 return WebService::HIBP::Breach->new( %{$json} );
  1         14  
82             }
83             else {
84 2         21 Carp::croak( "Failed to retrieve $url:" . $response->status_line() );
85             }
86             }
87              
88             sub pastes {
89 3     3 1 2001878 my ( $self, $account ) = @_;
90             my $url =
91 3         25 $self->{url} . 'pasteaccount/' . URI::Escape::uri_escape($account);
92 3         124 my $response = $self->_get($url);
93 3 100       13 if ( $response->is_success() ) {
94 1         16 my $json = JSON::decode_json( $response->decoded_content() );
95 1         704 my @pastes;
96 1         2 foreach my $paste ( @{$json} ) {
  1         4  
97 64         70 push @pastes, WebService::HIBP::Paste->new( %{$paste} );
  64         166  
98             }
99 1         33 return @pastes;
100             }
101             else {
102 2         26 Carp::croak( "Failed to retrieve $url:" . $response->status_line() );
103             }
104             }
105              
106             sub breaches {
107 4     4 1 2002475 my ( $self, %parameters ) = @_;
108 4         19 my $url = $self->{url} . 'breaches';
109 4 100       23 if ( $parameters{domain} ) {
110 1         8 $url .= '?domain=' . URI::Escape::uri_escape( $parameters{domain} );
111             }
112 4         35 my $response = $self->_get($url);
113 4 100       18 if ( $response->is_success() ) {
114 2         30 my $json = JSON::decode_json( $response->decoded_content() );
115 2         3253 my @breaches;
116 2         7 foreach my $breach ( @{$json} ) {
  2         6  
117 358         387 push @breaches, WebService::HIBP::Breach->new( %{$breach} );
  358         1535  
118             }
119 2         674 return @breaches;
120             }
121             else {
122 2         25 Carp::croak( "Failed to retrieve $url:" . $response->status_line() );
123             }
124             }
125              
126             sub account {
127 6     6 1 6002391 my ( $self, $account, %parameters ) = @_;
128             my $url =
129 6         63 $self->{url} . 'breachedaccount/' . URI::Escape::uri_escape($account);
130 6         343 my @filters;
131 6 100       34 if ( $parameters{unverified} ) {
132 1         3 push @filters, 'includeUnverified=true';
133             }
134 6 100       36 if ( $parameters{truncate} ) {
135 1         5 push @filters, 'truncateResponse=true';
136             }
137 6 100       25 if ( $parameters{domain} ) {
138             push @filters,
139 1         80 'domain=' . URI::Escape::uri_escape( $parameters{domain} );
140             }
141 6 100       31 if (@filters) {
142 3         15 $url .= q[?] . join q[&], @filters;
143             }
144 6         24 my $response = $self->_get($url);
145 6 100       29 if ( $response->is_success() ) {
146 4         58 my $json = JSON::decode_json( $response->decoded_content() );
147 4         1866 my @breaches;
148 4         10 foreach my $breach ( @{$json} ) {
  4         12  
149 220         244 push @breaches, WebService::HIBP::Breach->new( %{$breach} );
  220         732  
150             }
151 4         204 return @breaches;
152             }
153             else {
154 2         30 Carp::croak( "Failed to retrieve $url:" . $response->status_line() );
155             }
156             }
157              
158             sub password {
159 5     5 1 1027 my ( $self, $password ) = @_;
160 5         51 my $normalised = Unicode::Normalize::NFD($password)
161             ; # No API documentation on normalisation, chose NFD based on compatibility with the front end at https://haveibeenpwned.com/Passwords.
162 5         31 my $sha1 =
163             uc Digest::SHA::sha1_hex( Encode::encode( 'UTF-8', $normalised, 1 ) );
164 5         356 my $url = $self->{password_url} . substr $sha1, 0,
165             _LENGTH_OF_PASSWORD_PREFIX();
166 5         13 my $response = $self->_get($url);
167 5 100       30 if ( $response->is_success() ) {
168 3         50 my $remainder = substr $sha1, _LENGTH_OF_PASSWORD_PREFIX();
169 3         16 foreach my $line ( split /\r\n/smx, $response->decoded_content() ) {
170 1140         10415 my ( $pwned, $count ) = split /:/smx, $line;
171 1140 100       1855 if ( $pwned eq $remainder ) {
172 1         22 return $count;
173             }
174             }
175 2         57 return 0;
176             }
177             else {
178 2         32 Carp::croak( "Failed to retrieve $url:" . $response->status_line() );
179             }
180             }
181              
182             1; # End of WebService::HIBP
183             __END__