File Coverage

blib/lib/Net/Fastly/Client.pm
Criterion Covered Total %
statement 45 192 23.4
branch 4 58 6.9
condition 3 25 12.0
subroutine 12 33 36.3
pod 5 5 100.0
total 69 313 22.0


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