File Coverage

blib/lib/Browsermob/Proxy.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Browsermob::Proxy;
2             $Browsermob::Proxy::VERSION = '0.15';
3             # ABSTRACT: Perl client for the proxies created by the Browsermob server
4 7     7   123520 use Moo;
  7         38564  
  7         41  
5 7     7   5100 use Carp;
  7         10  
  7         414  
6 7     7   1391 use JSON;
  7         22454  
  7         38  
7 7     7   7450 use Net::HTTP::Spore;
  0            
  0            
8             use Net::HTTP::Spore::Middleware::DefaultParams;
9              
10              
11             my $spec = {
12             name => 'BrowserMob Proxy',
13             formats => ['json'],
14             version => '0.01',
15             # server name and port are constructed in the _spore builder
16             # base_url => '/proxy',
17             methods => {
18             get_proxies => {
19             method => 'GET',
20             path => '/',
21             description => 'Get a list of ports attached to ProxyServer instances managed by ProxyManager'
22             },
23             create => {
24             method => 'POST',
25             path => '/',
26             optional_params => [
27             'port'
28             ],
29             description => 'Create a new proxy. Returns a JSON object {"port": your_port} on success"'
30             },
31             delete_proxy => {
32             method => 'DELETE',
33             path => '/:port',
34             required_params => [
35             'port'
36             ],
37             description => 'Shutdown the proxy and close the port'
38             },
39             create_new_har => {
40             method => 'PUT',
41             path => '/:port/har',
42             optional_params => [
43             'initialPageRef',
44             'captureHeaders',
45             'captureContent',
46             'captureBinaryContent'
47             ],
48             required_params => [
49             'port'
50             ],
51             description => 'creates a new HAR attached to the proxy and returns the HAR content if there was a previous HAR.'
52             },
53             retrieve_har => {
54             method => 'GET',
55             path => '/:port/har',
56             required_params => [
57             'port'
58             ],
59             description => 'returns the JSON/HAR content representing all the HTTP traffic passed through the proxy'
60             },
61             auth_basic => {
62             method => 'POST',
63             path => '/:port/auth/basic/:domain',
64             required_params => [
65             'port',
66             'domain'
67             ],
68             description => 'Sets automatic basic authentication for the specified domain'
69             },
70             filter_request => {
71             method => 'POST',
72             path => '/:port/filter/request',
73             required_params => [
74             'port'
75             ],
76             description => 'Modify request/payload with javascript'
77             },
78             set_timeout => {
79             method => 'PUT',
80             path => '/:port/timeout',
81             required_params => [
82             'port',
83             ],
84             optional_params => [
85             'requestTimeout',
86             'readTimeout',
87             'connectionTimeout',
88             'dnsCacheTimeout'
89             ],
90             description => 'Handles different proxy timeouts'
91             }
92             }
93             };
94              
95              
96             has server_addr => (
97             is => 'rw',
98             default => sub { '127.0.0.1' }
99             );
100              
101              
102              
103             has server_port => (
104             is => 'rw',
105             default => sub { 8080 }
106             );
107              
108              
109             has port => (
110             is => 'rw',
111             lazy => 1,
112             predicate => 'has_port',
113             default => sub { '' }
114             );
115              
116              
117             has trace => (
118             is => 'ro',
119             default => sub { 0 }
120             );
121              
122             has mock => (
123             is => 'rw',
124             lazy => 1,
125             predicate => 'has_mock',
126             default => sub { '' }
127             );
128              
129             has _spore => (
130             is => 'ro',
131             lazy => 1,
132             handles => [keys %{ $spec->{methods} }],
133             builder => sub {
134             my $self = shift;
135             my $client = Net::HTTP::Spore->new_from_string(
136             to_json($self->_spec),
137             trace => $self->trace
138             );
139              
140             $self->_set_middlewares($client, 'json');
141              
142             return $client;
143             }
144             );
145              
146             around filter_request => sub {
147             my ($orig, $self, @args) = @_;
148             my $client = $self->_spore;
149             $self->_set_middlewares($client, 'text');
150             $orig->($self, @args);
151             $self->_set_middlewares($client, 'json');
152             };
153              
154             sub _set_middlewares {
155             my ($self, $client, $type) = @_;
156             $client->reset_middlewares;
157              
158             if ($type eq 'json') {
159             $client->enable('Format::JSON');
160             }
161             elsif ($type eq 'text') {
162             $client->enable('Format::Text');
163             }
164              
165             if ($self->has_port) {
166             $client->enable('DefaultParams', default_params => {
167             port => $self->port
168             });
169             }
170              
171             if ($self->has_mock) {
172             # The Mock middleware ignores any middleware enabled after
173             # it; make sure to enable everything else first.
174             $client->enable('Mock', tests => $self->mock);
175             }
176             }
177              
178             has _spec => (
179             is => 'ro',
180             lazy => 1,
181             builder => sub {
182             my $self = shift;
183             $spec->{base_url} = 'http://' . $self->server_addr . ':' . $self->server_port . '/proxy';
184             return $spec;
185             }
186             );
187              
188             sub BUILD {
189             my ($self, $args) = @_;
190             my $res = $self->create;
191              
192             unless ($self->has_port) {
193             $self->port($res->body->{port});
194             $self->_set_middlewares( $self->_spore, 'json' );
195             }
196             }
197              
198              
199             sub new_har {
200             my ($self, $initial_page_ref) = @_;
201             my $payload = {};
202              
203             croak "You need to create a proxy first!" unless $self->has_port;
204             if (defined $initial_page_ref) {
205             $payload->{initialPageRef} = $initial_page_ref;
206             }
207              
208             $self->_spore->create_new_har(payload => $payload);
209             }
210              
211              
212             sub har {
213             my ($self) = @_;
214              
215             croak "You need to create a proxy first!" unless $self->has_port;
216             return $self->_spore->retrieve_har->body;
217             }
218              
219              
220             sub selenium_proxy {
221             my ($self, $initiate_manually) = @_;
222             $self->new_har unless $initiate_manually;
223              
224             return {
225             proxyType => 'manual',
226             httpProxy => 'http://' . $self->server_addr . ':' . $self->port,
227             sslProxy => 'http://' . $self->server_addr . ':' . $self->port
228             };
229             }
230              
231              
232             sub firefox_proxy {
233             my ($self, $initiate_manually) = @_;
234             $self->new_har unless $initiate_manually;
235              
236             return {
237             'network.proxy.type' => 1,
238             'network.proxy.http' => $self->server_addr,
239             'network.proxy.http_port' => $self->port,
240             'network.proxy.ssl' => $self->server_addr,
241             'network.proxy.ssl_port' => $self->port
242             };
243             }
244              
245              
246             sub ua_proxy {
247             my ($self, $initiate_manually) = @_;
248             $self->new_har unless $initiate_manually;
249              
250             return ('http', 'http://' . $self->server_addr . ':' . $self->port);
251             }
252              
253              
254             sub set_env_proxy {
255             my ($self, $initiate_manually) = @_;
256             $self->new_har unless $initiate_manually;
257              
258             $ENV{http_proxy} = 'http://' . $self->server_addr . ':' . $self->port;
259             $ENV{https_proxy} = 'http://' . $self->server_addr . ':' . $self->port;
260             $ENV{ssl_proxy} = 'http://' . $self->server_addr . ':' . $self->port;
261             }
262              
263              
264             sub add_basic_auth {
265             my ($self, $args) = @_;
266             foreach (qw/domain username password/) {
267             croak "$_ is a required parameter for add_basic_auth"
268             unless exists $args->{$_};
269             }
270              
271             $self->auth_basic(
272             domain => delete $args->{domain},
273             payload => $args
274             );
275             }
276              
277              
278             sub set_request_header {
279             my ($self, $header, $value) = @_;
280             croak 'Please pass a ($header, $value) as arguments when setting a header'
281             unless $header and $value;
282              
283             $self->_set_header('request', $header, $value);
284             }
285              
286             sub _set_header {
287             my ($self, $type, $header, $value) = @_;
288              
289             $self->filter_request(
290             payload => "
291             $type.headers().remove('$header');
292             $type.headers().add('$header', '$value');
293             "
294             );
295             }
296              
297              
298              
299             sub DEMOLISH {
300             my ($self, $gd) = @_;
301             return if $gd;
302              
303             eval { $self->delete_proxy };
304             warn $@ if $@ and $self->trace;
305             }
306              
307             1;
308              
309             __END__
310              
311             =pod
312              
313             =encoding UTF-8
314              
315             =head1 NAME
316              
317             Browsermob::Proxy - Perl client for the proxies created by the Browsermob server
318              
319             =for markdown [![Build Status](https://travis-ci.org/gempesaw/Browsermob-Proxy.svg?branch=master)](https://travis-ci.org/gempesaw/Browsermob-Proxy)
320              
321             =head1 VERSION
322              
323             version 0.15
324              
325             =head1 SYNOPSIS
326              
327             Standalone:
328              
329             my $proxy = Browsermob::Proxy->new(
330             server_port => 9090
331             # port => 9092
332             );
333              
334             print $proxy->port;
335             $proxy->new_har('Google');
336             # create network traffic across your port
337             $proxy->har; # returns a HAR as a hashref, converted from JSON
338              
339             with L<Browsermob::Server>:
340              
341             my $server = Browsermob::Server->new(
342             server_port => 9090
343             );
344             $server->start; # ignore if your server is already running
345              
346             my $proxy = $server->create_proxy;
347             $proxy->new_har('proxy from server!');
348              
349             =head1 DESCRIPTION
350              
351             From L<http://bmp.lightbody.net/>:
352              
353             =over 4
354              
355             BrowserMob proxy is based on technology developed in the Selenium open
356             source project and a commercial load testing and monitoring service
357             originally called BrowserMob and now part of Neustar.
358              
359             It can capture performance data for web apps (via the HAR format), as
360             well as manipulate browser behavior and traffic, such as whitelisting
361             and blacklisting content, simulating network traffic and latency, and
362             rewriting HTTP requests and responses.
363              
364             =back
365              
366             This module is a Perl client interface to interact with the server and
367             its proxies. It uses L<Net::HTTP::Spore>. You can use
368             L<Browsermob::Server> to manage the server itself in addition to using
369             this module to handle the proxies.
370              
371             =head2 INSTALLATION
372              
373             We depend on L<Net::HTTP::Spore> to set up our communication with the
374             Browsermob server. Unfortunately, there hasn't been a recent release
375             and due to breaking changes in new versions of its dependencies, you
376             might run in to problems installing its current CPAN version
377             v0.06. And, thus installing this module may be difficult.
378              
379             We're using a fork of L<Net::HTTP::Spore> that is kept slightly ahead
380             of master with the bug fixes merged in; installation via
381             L<App::cpanminus> looks like:
382              
383             cpanm git://github.com/gempesaw/net-http-spore.git@build/master
384              
385             =head1 ATTRIBUTES
386              
387             =head2 server_addr
388              
389             Optional: specify where the proxy server is; defaults to 127.0.0.1
390              
391             my $proxy = Browsermob::Proxy->new(server_addr => '127.0.0.1');
392              
393             =head2 server_port
394              
395             Optional: Indicate at what port we should expect a Browsermob Server
396             to be running; defaults to 8080
397              
398             my $proxy = Browsermob::Proxy->new(server_port => 8080);
399              
400             =head2 port
401              
402             Optional: When instantiating a proxy, you can choose the proxy port on
403             your own, or let the server automatically assign you an unused port.
404              
405             my $proxy = Browsermob::Proxy->new(port => 9091);
406              
407             =head2 trace
408              
409             Set Net::HTTP::Spore's trace option; defaults to 0; set it to 1 to see
410             headers and 2 to see headers and responses. This can only be set during
411             construction; changing it afterwards will have no impact.
412              
413             my $proxy = Browsermob::Proxy->new( trace => 2 );
414              
415             =head1 METHODS
416              
417             =head2 new_har
418              
419             After creating a proxy, C<new_har> creates a new HAR attached to the
420             proxy and returns the HAR content if there was a previous one. If no
421             argument is passed, the initial page ref will be "Page 1"; you can
422             also pass a string to choose your own initial page ref.
423              
424             $proxy->new_har;
425             $proxy->new_har('Google');
426              
427             This convenience method is just a helper around the actual endpoint
428             method C</create_new_har>; it uses the defaults of not capturing
429             headers, request/response bodies, or binary content. If you'd like to
430             capture those items, you can use C<create_new_har> as follows:
431              
432             $proxy->create_new_har(
433             payload => {
434             initialPageRef => 'payload is optional'
435             },
436             captureHeaders => 'true',
437             captureContent => 'true',
438             captureBinaryContent => 'true'
439             );
440              
441             =head2 har
442              
443             After creating a proxy and initiating a L<new_har>, you can retrieve
444             the contents of the current HAR with this method. It returns a hashref
445             HAR, and may in the future return an isntance of L<Archive::HAR>.
446              
447             my $har = $proxy->har;
448             print Dumper $har->{log}->{entries}->[0];
449              
450             =head2 selenium_proxy
451              
452             Generate the proper capabilities for use in the constructor of a new
453             Selenium::Remote::Driver object.
454              
455             my $proxy = Browsermob::Proxy->new;
456             my $driver = Selenium::Remote::Driver->new(
457             browser_name => 'chrome',
458             proxy => $proxy->selenium_proxy
459             );
460             $driver->get('http://www.google.com');
461             print Dumper $proxy->har;
462              
463             N.B.: C<selenium_proxy> will AUTOMATICALLY call L</new_har> for you
464             initiating an unnamed har, unless you pass it something truthy.
465              
466             my $proxy = Browsermob::Proxy->new;
467             my $driver = Selenium::Remote::Driver->new(
468             browser_name => 'chrome',
469             proxy => $proxy->selenium_proxy(1)
470             );
471             # later
472             $proxy->new_har;
473             $driver->get('http://www.google.com');
474             print Dumper $proxy->har;
475              
476             =head2 firefox_proxy
477              
478             Generate a hash with the proper keys and values that for use in
479             setting preferences for a
480             L<Selenium::Remote::Driver::Firefox::Profile>. This method returns a
481             hashref; dereference it when you pass it to
482             L<Selenium::Remote::Driver::Firefox::Profile/set_preference>:
483              
484             my $profile = Selenium::Remote::Driver::Firefox::Profile->new;
485              
486             my $firefox_pref = $proxy->firefox_proxy;
487             $profile->set_preference( %{ $firefox_pref } );
488              
489             my $driver = Selenium::Remote::Driver->new_from_caps(
490             desired_capabilities => {
491             browserName => 'Firefox',
492             firefox_profile => $profile->_encode
493             }
494             );
495              
496             N.B.: C<firefox_proxy> will AUTOMATICALLY call L</new_har> for you
497             initiating an unnamed har, unless you pass it something truthy.
498              
499             =head2 ua_proxy
500              
501             Generate the proper arguments for the proxy method of
502             L<LWP::UserAgent>. By default, C<ua_proxy> will initiate a new har for
503             you automatically, the same as L</selenium_proxy> does. If you want to
504             initialize the har yourself, pass in something truthy.
505              
506             my $proxy = Browsermob::Proxy->new;
507             my $ua = LWP::UserAgent->new;
508             $ua->proxy($proxy->ua_proxy);
509              
510             =head2 set_env_proxy
511              
512             Export to C<%ENV> the properties of this proxy's port. This can be
513             used in tandem with <LWP::UserAgent/env_proxy>. This will set the
514             appropriate environment variables, and then your C<$ua> will pick it
515             up when its C<env_proxy> method is invoked aftewards. As usual, this
516             will create a new HAR unless you deliberately inhibit it.
517              
518             $proxy->set_env_proxy;
519             $ua->env_proxy;
520              
521             In particular, we set C<http_proxy>, C<https_proxy>, and C<ssl_proxy>
522             to the appropriate server and port by defining them as keys in C<%ENV>.
523              
524             =head2 add_basic_auth
525              
526             Set up automatic Basic authentication for a specified domain. Accepts
527             as input a HASHREF with the keys C<domain>, C<username>, and
528             C<password>. For example,
529              
530             $proxy->add_basic_auth({
531             domain => '.google.com',
532             username => 'username',
533             password => 'password'
534             });
535              
536             =head2 set_request_header ( $header, $value )
537              
538             Takes two STRINGs as arguments. (Unhelpfully) returns a
539             Net::HTTP::Spore::Response. With this method, we will remove the
540             specified C<$header> from every request the proxy sees, and replace it
541             with the C<$header> C<$value> pair that you pass in.
542              
543             $proxy->set_request_header( 'User-Agent', 'superwoman' );
544              
545             Under the covers, we are using L</filter_request> with a Javascript
546             Rhino payload.
547              
548             =head2 set_timeout ( $timeoutType => $milliseconds )
549              
550             Set different time outs on the instantiated proxy. You can set
551             multiple timeouts at once, if you like.
552              
553             $proxy->timeout(
554             requestTimeout => 5000,
555             readTimeout => 6000
556             );
557              
558             =over 4
559              
560             =item *
561              
562             requestTimeout
563              
564             Request timeout in milliseconds. A timeout value of -1 is interpreted
565             as infinite timeout. It equals -1 by default.
566              
567             =item *
568              
569             readTimeout
570              
571             Read timeout is the timeout for waiting for data or, put differently,
572             a maximum period inactivity between two consecutive data packets. A
573             timeout value of zero is interpreted as an infinite timeout. It equals
574             60000 by default.
575              
576             =item *
577              
578             connectionTimeout
579              
580             Determines the timeout in milliseconds until a connection is
581             established. A timeout value of zero is interpreted as an infinite
582             timeout. It eqauls 60000 by default.
583              
584             =item *
585              
586             dnsCacheTimeout
587              
588             Sets the maximum length of time that records will be stored in this
589             Cache. A nonpositive value disables this feature (that is, sets no
590             limit). It equals 0 by default.
591              
592             =back
593              
594             =head2 delete_proxy
595              
596             Delete the proxy off of the server, shutting down the port. Although
597             we do try to do this in our DEMOLISH method, we can't do anything if
598             the C<$proxy> object is kept around during global destruction. If
599             you're noticing that your BMP server has leftover proxies, you should
600             start either explicitly C<undef>ing the `$proxy` object or invoking
601             this method.
602              
603             # calls ->delete_proxy in our DEMOLISH method, explicitly not
604             # during global destruction!
605             undef $proxy;
606              
607             # manually delete the proxy from the BMP server
608             $proxy->delete_proxy;
609              
610             After deleting the proxy, invoking any other method will probably lead
611             to a C<die> from inside the Net::HTTP::Spore module somewhere.
612              
613             =head1 SEE ALSO
614              
615             Please see those modules/websites for more information related to this module.
616              
617             =over 4
618              
619             =item *
620              
621             L<http://bmp.lightbody.net/|http://bmp.lightbody.net/>
622              
623             =item *
624              
625             L<https://github.com/lightbody/browsermob-proxy|https://github.com/lightbody/browsermob-proxy>
626              
627             =item *
628              
629             L<Browsermob::Server|Browsermob::Server>
630              
631             =back
632              
633             =head1 BUGS
634              
635             Please report any bugs or feature requests on the bugtracker website
636             https://github.com/gempesaw/Browsermob-Proxy/issues
637              
638             When submitting a bug or request, please include a test-file or a
639             patch to an existing test-file that illustrates the bug or desired
640             feature.
641              
642             =head1 AUTHOR
643              
644             Daniel Gempesaw <gempesaw@gmail.com>
645              
646             =head1 COPYRIGHT AND LICENSE
647              
648             This software is copyright (c) 2014 by Daniel Gempesaw.
649              
650             This is free software; you can redistribute it and/or modify it under
651             the same terms as the Perl 5 programming language system itself.
652              
653             =cut