File Coverage

blib/lib/Net/Fastly/Client.pm
Criterion Covered Total %
statement 48 198 24.2
branch 4 58 6.9
condition 3 25 12.0
subroutine 13 35 37.1
pod 6 6 100.0
total 74 322 22.9


line stmt bran cond sub pod time code
1             package Net::Fastly::Client;
2              
3 4     4   29 use strict;
  4         6  
  4         111  
4 4     4   19 use warnings;
  4         8  
  4         109  
5 4     4   21 use Carp qw( carp );
  4         7  
  4         211  
6 4     4   2815 use JSON::XS;
  4         29928  
  4         5025  
7              
8             =head1 NAME
9              
10             Net::Fastly::Client - communicate with the Fastly HTTP API
11              
12             =head1 SYNOPSIS
13              
14             =head1 PROXYING
15              
16             There are two ways to proxy:
17              
18             The first method is to pass a proxy option into the constructor
19              
20             my $client = Net::Fastly::Client->new(user => $username, password => $password, proxy => "http://localhost:8080");
21            
22             The second is to set your C environment variable. So, in Bash
23              
24             % export https_proxy=http://localhost:8080
25            
26             or in CSH or TCSH
27              
28             % setenv https_proxy=http://localhost:8080
29              
30             =head1 METHODS
31              
32             =cut
33              
34              
35             =head2 new
36              
37             Create a new Fastly user agent. Options are
38              
39             =over 4
40              
41             =item user
42              
43             The login to use
44              
45             =item password
46              
47             Your password
48              
49             =item api_key
50              
51             Alternatively use the API Key (only some commands are available)
52              
53             =item proxy
54              
55             Optionally pass in an https proxy to use.
56              
57             =back
58              
59              
60             =cut
61             sub new {
62 1     1 1 2 my $class = shift;
63 1         4 my %opts = @_;
64 1         3 my $self = bless \%opts, $class;
65            
66 1   50     25 my $base = $opts{base_url} ||= "api.fastly.com";
67 1   50     7 my $port = $opts{base_port} ||= 80;
68 1   33     10 $self->{user} ||= $self->{username};
69 1         10 $self->{_ua} = Net::Fastly::Client::UserAgent->new($base, $port, $opts{proxy});
70 1 50       7 return $self unless $self->fully_authed;
71              
72             # If we're fully authed (i.e username and password ) then we need to log in
73 0         0 my $res = $self->_ua->_post('/login', {}, user => $self->{user}, password => $self->{password});
74              
75 0         0 carp "DEPRECATION WARNING: Username/password authentication is deprecated and will not be available starting September 2020; please migrate to API tokens as soon as possible.";
76              
77 0 0       0 unless ($res->is_success) {
78 0 0 0     0 die "You must have IO::Socket::SSL or Crypt::SSLeay installed in order to do SSL requests\n" if $res->code == 501 && $res->status_line =~ /Protocol scheme 'https' is not supported/;
79 0 0       0 die "Unauthorized" unless $res->is_success;
80             }
81 0         0 my $content = decode_json($res->decoded_content);
82 0         0 $self->{_cookie} = $res->header('set-cookie');
83 0 0       0 return wantarray ? ($self, $content->{user}, $content->{customer}) : $self;
84             }
85              
86 0     0   0 sub _ua { shift->{_ua} }
87              
88             =head2 authed
89              
90             Whether or not we're authed at all by either API key or username & password
91              
92             =cut
93             sub authed {
94 0     0 1 0 my $self = shift;
95 0 0       0 $self->key_authed || $self->fully_authed;
96             }
97              
98             =head2 key_authed
99              
100             Whether or not we're authed by API key
101              
102             =cut
103             sub key_authed {
104 0     0 1 0 my $self = shift;
105             defined $self->{api_key}
106 0         0 }
107              
108             =head2 fully_authed
109              
110             Whether or not we're authed by username & password
111              
112             =cut
113             sub fully_authed {
114 1     1 1 3 my $self = shift;
115 1 50       13 defined $self->{user} && defined $self->{password};
116             }
117              
118             =head2 set_customer
119              
120             Set the current customer to act as.
121              
122             B: this will only work if you're an admin
123              
124             =cut
125             sub set_customer {
126 0     0 1 0 my $self = shift;
127 0         0 my $id = shift;
128 0         0 $self->{explicit_customer} = $id;
129             }
130              
131             =head2 timeout
132              
133             Get or set the timeout value in seconds. The default value is 180 seconds.
134              
135             =cut
136              
137             sub timeout {
138 0     0 1 0 my $self = shift;
139 0         0 $self->_ua->_ua->timeout(@_);
140             }
141              
142             # Get stuff from the stats API
143             sub _get_stats {
144 0     0   0 my $self = shift;
145 0         0 my $content = $self->_get(@_);
146 0 0       0 die $content->{msg} unless $content->{status} eq 'success';
147 0         0 return $content->{data};
148             }
149              
150             sub _get {
151 0     0   0 my $self = shift;
152 0         0 my $path = shift;
153 0         0 my %opts = @_;
154 0   0     0 my $headers = delete $opts{headers} || {};
155              
156 0         0 my $res = $self->_ua->_get($path, $self->_headers($headers), %opts);
157 0 0       0 return undef if 404 == $res->code;
158 0 0       0 $self->_raise_error($res) unless $res->is_success;
159 0         0 my $content = decode_json($res->decoded_content);
160 0         0 return $content;
161             }
162              
163             sub _post {
164 0     0   0 my $self = shift;
165 0         0 my $path = shift;
166 0         0 my %params = @_;
167 0   0     0 my $headers = delete $params{headers} || {};
168              
169 0         0 my $res = $self->_ua->_post($path, $self->_headers($headers), %params);
170 0 0       0 $self->_raise_error($res) unless $res->is_success;
171 0         0 my $content = decode_json($res->decoded_content);
172 0         0 return $content;
173             }
174              
175             sub _purge {
176 0     0   0 my $self = shift;
177 0         0 my $url = shift;
178 0         0 my %params = @_;
179 0   0     0 my $headers = delete $params{headers} || {};
180              
181 0         0 my $method = "_purge";
182 0 0       0 if ($self->{use_old_purge_method}) {
183 0         0 $method = "_post";
184 0         0 $url = "/purge/$url";
185             }
186              
187 0         0 my $res = $self->_ua->$method($url, $self->_headers($headers), %params);
188 0 0       0 $self->_raise_error($res) unless $res->is_success;
189 0         0 my $content = decode_json($res->decoded_content);
190 0         0 return $content;
191             }
192              
193             sub _put {
194 0     0   0 my $self = shift;
195 0         0 my $path = shift;
196 0         0 my %params = @_;
197 0   0     0 my $headers = delete $params{headers} || {};
198              
199 0         0 my $res = $self->_ua->_put($path, $self->_headers($headers), %params);
200 0 0       0 $self->_raise_error($res) unless $res->is_success;
201 0         0 my $content = decode_json($res->decoded_content);
202 0         0 return $content;
203             }
204              
205             sub _delete {
206 0     0   0 my $self = shift;
207 0         0 my $path = shift;
208 0         0 my %params = @_;
209 0   0     0 my $headers = delete $params{headers} || {};
210              
211 0         0 my $res = $self->_ua->_delete($path, $self->_headers($headers));
212 0 0       0 $self->_raise_error($res) unless $res->is_success;
213 0         0 return 1;
214             }
215              
216             sub _headers {
217 0     0   0 my $self = shift;
218 0         0 my $extras = shift;
219 0 0       0 my $params = $self->fully_authed ? { 'Cookie' => $self->{_cookie} } : { 'Fastly-Key' => $self->{api_key} };
220 0 0       0 $params->{'Fastly-Explicit-Customer'} = $self->{explicit_customer} if defined $self->{explicit_customer};
221 0         0 $params->{'Content-Accept'} = 'application/json';
222 0         0 $params->{'User-Agent'} = "fastly-perl-v$Net::Fastly::VERSION";
223 0         0 while (my ($key, $value) = each %$extras) {
224 0 0       0 $params->{$key} = $value if defined $value;
225             }
226 0         0 return $params;
227             }
228              
229             sub _raise_error {
230 0     0   0 my $self = shift;
231 0         0 my $res = shift;
232              
233 0         0 my $content = eval { decode_json($res->decoded_content) };
  0         0  
234 0 0 0     0 my $message = $content ? $content->{detail} || $content->{msg} : $res->status_line." ".$res->decoded_content;
235 0         0 die "$message\n";
236             }
237              
238              
239             package Net::Fastly::Client::UserAgent;
240              
241 4     4   36 use strict;
  4         19  
  4         103  
242 4     4   2435 use URI;
  4         18849  
  4         134  
243 4     4   2785 use LWP::UserAgent;
  4         158711  
  4         462  
244             BEGIN { # Compatibility fix for older versions of HTTP::Request::Common
245 4     4   2127 require HTTP::Request::Common;
246 4 50       9275 if (HTTP::Request::Common->can('DELETE')) {
    0          
247 4         3178 HTTP::Request::Common->import(qw(GET HEAD PUT POST DELETE));
248             } elsif (my $_simple_req = HTTP::Request::Common->can('_simple_req')) {
249 0         0 HTTP::Request::Common->import(qw(GET HEAD PUT POST));
250 0         0 *DELETE = sub { $_simple_req->('DELETE', @_) };
  0         0  
251             } else {
252 0         0 die << 'END'
253             "DELETE" is not exported by the HTTP::Request::Common module
254             and its underlying _simple_req() method is not available.
255             END
256             }
257             }
258              
259             sub new {
260 1     1   3 my $class = shift;
261 1         2 my $base = shift;
262 1         2 my $port = shift;
263 1         4 my $proxy = shift;
264 1         14 my $ua = Net::Fastly::UA->new;
265 1 50       3024 if ($proxy) {
266 0         0 $ua->proxy('https', $proxy);
267             } else {
268 1         10 $ua->env_proxy;
269             }
270 1         18475 return bless { _base => $base, _port => $port, _ua => $ua }, $class;
271            
272             }
273              
274 0     0     sub _ua { shift->{_ua} }
275              
276             sub _get {
277 0     0     my $self = shift;
278 0           my $path = shift;
279 0           my $headers = shift;
280 0           my %params = @_;
281 0           my $url = $self->_make_url($path, %params);
282 0           return $self->_ua->request(GET $url, %$headers);
283             }
284              
285             sub _post {
286 0     0     my $self = shift;
287 0           my $path = shift;
288 0           my $headers = shift;
289 0           my %params = @_;
290 0           my $url = $self->_make_url($path);
291 0           return $self->_ua->request(POST $url, [_make_params(%params)], %$headers);
292             }
293              
294             sub _purge {
295 0     0     my $self = shift;
296 0           my $url = shift;
297 0           my $headers = shift;
298 0           my %params = @_;
299 0           return $self->_ua->request(HTTP::Request->new("PURGE", $url, [%$headers]));
300             }
301              
302             sub _put {
303 0     0     my $self = shift;
304 0           my $path = shift;
305 0           my $headers = shift;
306 0           my %params = @_;
307 0           $headers->{content_type} = "application/x-www-form-urlencoded";
308 0           my $url = $self->_make_url($path);
309 0           my $uri = URI->new('http');
310 0           $uri->query_form(_make_params(%params));
311 0   0       return $self->_ua->request(PUT $url, %$headers, Content => $uri->query || "");
312             }
313              
314             sub _delete {
315 0     0     my $self = shift;
316 0           my $path = shift;
317 0           my $headers = shift;
318 0           my %params = @_;
319 0           my $url = $self->_make_url($path, %params);
320 0           return $self->_ua->request(DELETE $url, %$headers);
321             }
322              
323             sub _make_url {
324 0     0     my $self = shift;
325 0           my $base = $self->{_base};
326 0           my $port = $self->{_port};
327 0           my $path = shift;
328 0           my %params = @_;
329              
330 0           my $prot = "https:";
331 0 0         if ($base =~ s!^(https?:)//!!) {
332 0           $prot = $1;
333             }
334 0           my $url = URI->new($prot);
335 0           $url->host($base);
336 0 0         $url->port($port) if $port != 80;
337 0           $url->path($path);
338 0 0         $url->query_form(_make_params(%params)) if keys %params;
339 0           return $url;
340             }
341              
342             sub _make_params {
343 0     0     my %in = @_;
344 0           my %out;
345            
346 0           foreach my $key (keys %in) {
347 0           my $value = $in{$key};
348 0 0         next unless defined $value;
349 0 0         unless (ref($value) eq 'HASH') {
350 0           $out{$key} = $value;
351 0           next;
352             }
353 0           foreach my $sub_key (keys %$value) {
354 0           $out{$key."[".$sub_key."]"} = $value->{$sub_key};
355             }
356             }
357 0           return %out;
358             }
359              
360             package Net::Fastly::UA;
361              
362 4     4   35 use base qw(LWP::UserAgent);
  4         9  
  4         355  
363 4     4   1966 use LWP::Protocol::https;
  4         401454  
  4         580  
364             our $DEBUG=0;
365              
366             sub request {
367 0     0     my $self = shift;
368 0           my $req = shift;
369 0 0         print $req->as_string."\n------------------------\n\n" if $DEBUG;
370 0           my $res = $self->SUPER::request($req);
371 0 0         print $res->as_string."\n------------------------\n\n\n\n\n" if $DEBUG;
372 0           return $res;
373             }
374             1;