File Coverage

blib/lib/JMX/Jmx4Perl/Agent.pm
Criterion Covered Total %
statement 69 193 35.7
branch 13 104 12.5
condition 2 15 13.3
subroutine 15 26 57.6
pod 4 5 80.0
total 103 343 30.0


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             package JMX::Jmx4Perl::Agent;
3              
4 3     3   1700 use JSON;
  3         12526  
  3         21  
5 3     3   1989 use URI::Escape qw(uri_escape_utf8);
  3         4222  
  3         175  
6 3     3   1465 use HTTP::Request;
  3         54231  
  3         89  
7 3     3   21 use Carp;
  3         6  
  3         176  
8 3     3   18 use strict;
  3         6  
  3         68  
9 3     3   14 use vars qw($VERSION $DEBUG);
  3         6  
  3         149  
10 3     3   18 use base qw(JMX::Jmx4Perl);
  3         5  
  3         751  
11 3     3   23 use JMX::Jmx4Perl::Request;
  3         15  
  3         233  
12 3     3   1410 use JMX::Jmx4Perl::Response;
  3         31  
  3         94  
13 3     3   1415 use JMX::Jmx4Perl::Agent::UserAgent;
  3         13  
  3         149  
14 3     3   23 use Data::Dumper;
  3         10  
  3         8100  
15              
16              
17             $VERSION = $JMX::Jmx4Perl::VERSION;
18              
19             =head1 NAME
20              
21             JMX::Jmx4Perl::Agent - JSON-HTTP based acess to a remote JMX agent
22              
23             =head1 SYNOPSIS
24              
25             my $agent = new JMX::Jmx4Perl(mode=>"agent", url => "http://jeeserver/j4p");
26             my $answer = $agent->get_attribute("java.lang:type=Memory","HeapMemoryUsage");
27             print Dumper($answer);
28              
29             {
30             request => {
31             attribute => "HeapMemoryUsage",
32             name => "java.lang:type=Memory"
33             },
34             status => 200,
35             value => {
36             committed => 18292736,
37             init => 0,
38             max => 532742144,
39             used => 15348352
40             }
41             }
42              
43             =head1 DESCRIPTION
44              
45             This module is not used directly, but via L, which acts as a
46             proxy to this module. You can think of L as the interface which
47             is backed up by this module. Other implementations (e.g.
48              
49             =head1 METHODS
50              
51             =over 4
52              
53             =item $jjagent = JMX::Jmx4Perl::Agent->new(url => $url, ....)
54              
55             Creates a new local agent for a given url
56              
57             =over
58              
59             =item url =>
60              
61             The url where the agent is deployed. This is a mandatory parameter. The url
62             must include the context within the server, which is typically based on the
63             name of the war archive. Example: C for a drop
64             in deployment of the agent in a standard Tomcat's webapp directory.
65              
66             =item timeout =>
67              
68             Timeout in seconds after which a request should be stopped if it not suceeds
69             within this time. This parameter is given through directly to the underlying
70             L
71              
72             =item user => , password =>
73              
74             Credentials to use for the HTTP request
75              
76             =item method =>
77              
78             The HTTP method to use for contacting the agent. Must be either "GET" or
79             "POST". This method is used, if the request to send dosen't specify the method
80             and no other parameters forces a POST context.
81              
82             =item proxy => { http => '', https => '', ... }
83              
84             =item proxy =>
85              
86             =item proxy => { url => }
87              
88             Optional proxy to use
89              
90             =item proxy_user => , proxy_password =>
91              
92             Credentials to use for accessing the proxy
93              
94             =item target
95              
96             Add a target which is used for any request served by this object if not already
97             a target is present in the request. This way you can setup the default target
98             configuration if you are using the agent servlet as a proxy, e.g.
99              
100             ... target => { url => "service:jmx:...", user => "...", password => "..." }
101              
102             =item legacy-escape
103              
104             Before version 1.0 a quite strange escaping scheme is used, when the part of a
105             GET requests contains a slash (/). Starting with 1.0 this scheme has changed,
106             but in order to allow post 1.0 Jmx4perl clients acess pre 1.0 Jolokia agents,
107             this option can be set to true to switch to the old escape mechanism.
108              
109             =back
110              
111             =cut
112              
113             # HTTP Parameters to be used for transmitting the request
114             my @PARAMS = ("maxDepth","maxCollectionSize","maxObjects","ignoreErrors");
115              
116             # Regexp for detecting invalid chars which can not be used securily in pathinfos
117             my $INVALID_PATH_CHARS = qr/%(5C|3F|3B|2F)/i; # \ ? ; /
118              
119             # Init called by parent package within 'new' for specific initialization. See
120             # above for the parameters recognized
121             sub init {
122 5     5 0 11 my $self = shift;
123            
124 5 50       36 croak "No URL provided" unless $self->cfg('url');
125 5         29 my $ua = JMX::Jmx4Perl::Agent::UserAgent->new();
126 5         10483 $ua->jjagent_config($self->{cfg});
127             #push @{ $ua->requests_redirectable }, 'POST';
128 5 50       18 $ua->timeout($self->cfg('timeout')) if $self->cfg('timeout');
129             #print "TO: ",$ua->timeout(),"\n";
130 5         32 $ua->agent("JMX::Jmx4Perl::Agent $VERSION");
131             # $ua->env_proxy;
132 5         351 my $proxy = $self->cfg('proxy');
133 5 50       20 if ($proxy) {
134 0 0       0 my $url = ref($proxy) eq "HASH" ? $proxy->{url} : $proxy;
135 0 0       0 if (ref($url) eq "HASH") {
136 0         0 for my $k (keys %$url) {
137 0         0 $ua->proxy($k,$url->{$k});
138             }
139             } else {
140 0 0       0 if ($self->cfg('url') =~ m|^(.*?)://|) {
141             # Set proxy for URL scheme used
142 0         0 $ua->proxy($1,$url);
143             } else {
144 0         0 $ua->proxy('http',$proxy);
145             }
146             }
147             }
148 5         11 $self->{ua} = $ua;
149 5         12 return $self;
150             }
151              
152             =item $url = $agent->url()
153              
154             Get the base URL for connecting to the agent. You cannot change the URL via this
155             method, it is immutable for a given agent.
156              
157             =cut
158              
159             sub url {
160 0     0 1 0 my $self = shift;
161 0         0 return $self->cfg('url');
162             }
163              
164             =item $resp = $agent->request($request)
165              
166             Implementation of the JMX request as specified in L. It uses a
167             L sent via an L for posting a JSON representation
168             of the request. This method shouldn't be called directly but via
169             L->request().
170              
171             =cut
172              
173             sub request {
174 0     0 1 0 my $self = shift;
175 0 0       0 my @jmx_requests = $self->cfg('target') ? $self->_update_targets(@_) : @_;
176 0         0 my $ua = $self->{ua};
177 0         0 my $http_req = $self->_to_http_request(@jmx_requests);
178 0 0       0 if ($self->{cfg}->{verbose}) {
179 0         0 print $http_req->as_string;
180 0         0 print "===========================================================\n";
181             }
182             #print Dumper($http_req);
183 0         0 my $http_resp = $ua->request($http_req);
184 0         0 my $json_resp = {};
185 0 0       0 if ($self->{cfg}->{verbose}) {
186 0         0 print $http_resp->as_string,"\n";
187 0         0 print "===========================================================\n";
188             }
189 0         0 eval {
190 0         0 $json_resp = from_json($http_resp->content());
191             };
192 0         0 my $json_error = $@;
193 0 0       0 if ($http_resp->is_error) {
    0          
194             return JMX::Jmx4Perl::Response->new
195             (
196             status => $http_resp->code,
197             value => $json_error ? $http_resp->content : $json_resp,
198             error => $json_error ? $self->_prepare_http_error_text($http_resp) :
199 0         0 ref($json_resp) eq "ARRAY" ? join "\n", map { $_->{error} } grep { $_->{error} } @$json_resp : $json_resp->{error},
  0         0  
200             stacktrace => ref($json_resp) eq "ARRAY" ? $self->_extract_stacktraces($json_resp) : $json_resp->{stacktrace},
201 0 0       0 request => @jmx_requests == 1 ? $jmx_requests[0] : \@jmx_requests
    0          
    0          
    0          
    0          
202             );
203             } elsif ($json_error) {
204             # If is not an HTTP-Error and deserialization fails, then we
205             # probably got a wrong URL and get delivered some server side
206             # document (with HTTP code 200)
207 0         0 my $e = $json_error;
208 0         0 $e =~ s/(.*)at .*?line.*$/$1/;
209 0         0 return JMX::Jmx4Perl::Response->new
210             (
211             status => 400,
212             error =>
213             "Error while deserializing JSON answer (Wrong URL ?)\n" . $e,
214             value => $http_resp->content
215             );
216             }
217            
218 0         0 my @responses = ($self->_from_http_response($json_resp,@jmx_requests));
219 0 0 0     0 if (!wantarray && scalar(@responses) == 1) {
220 0         0 return shift @responses;
221             } else {
222 0         0 return @responses;
223             }
224             }
225              
226             =item $encrypted = $agent->encrypt($plain)
227              
228             Encrypt a password which can be used in configuration files in order to
229             obfuscate the clear text password.
230              
231             =cut
232              
233             sub encrypt {
234 0     0 1 0 return "[[" . &JMX::Jmx4Perl::Agent::UserAgent::encrypt(shift) . "]]";
235             }
236              
237              
238             # Create an HTTP-Request for calling the server
239             sub _to_http_request {
240 0     0   0 my $self = shift;
241 0         0 my @reqs = @_;
242 0 0       0 if ($self->_use_GET_request(\@reqs)) {
243             # Old, rest-style
244 0         0 my $url = $self->request_url($reqs[0]);
245 0         0 return HTTP::Request->new(GET => $url);
246             } else {
247 0   0     0 my $url = $self->cfg('url') || croak "No URL provided";
248 0 0       0 $url .= "/" unless $url =~ m|/$|;
249 0         0 my $request = HTTP::Request->new(POST => $url);
250 0 0       0 my $content = to_json(@reqs > 1 ? \@reqs : $reqs[0], { convert_blessed => 1 });
251             #print Dumper($reqs[0],$content);
252 0         0 $request->content($content);
253 0         0 return $request;
254             }
255             }
256              
257             sub _use_GET_request {
258 0     0   0 my $self = shift;
259 0         0 my $reqs = shift;
260 0 0       0 if (@$reqs == 1) {
261 0         0 my $req = $reqs->[0];
262             # For proxy configs and explicite set POST request, get can not be
263             # used
264 0 0       0 return 0 if defined($req->get("target"));
265             #print Dumper($req);
266 0         0 for my $r ($req->method,$self->cfg('method')) {
267 0 0       0 return lc($r) eq "get" if defined($r);
268             }
269             # GET by default
270 0         0 return 1;
271             } else {
272 0         0 return 0;
273             }
274             }
275              
276             # Create one or more response objects for a given request
277             sub _from_http_response {
278 0     0   0 my $self = shift;
279 0         0 my $json_resp = shift;
280 0         0 my @reqs = @_;
281 0 0       0 if (ref($json_resp) eq "HASH") {
    0          
282 0         0 return JMX::Jmx4Perl::Response->new(%{$json_resp},request => $reqs[0]);
  0         0  
283             } elsif (ref($json_resp) eq "ARRAY") {
284 0 0       0 die "Internal: Number of request and responses doesn't match (",scalar(@reqs)," vs. ",scalar(@$json_resp)
285             unless scalar(@reqs) == scalar(@$json_resp);
286            
287 0         0 my @ret = ();
288 0         0 for (my $i=0;$i<@reqs;$i++) {
289 0 0       0 die "Internal: Not a hash --> ",$json_resp->[$i] unless ref($json_resp->[$i]) eq "HASH";
290 0         0 my $response = JMX::Jmx4Perl::Response->new(%{$json_resp->[$i]},request => $reqs[$i]);
  0         0  
291 0         0 push @ret,$response;
292             }
293 0         0 return @ret;
294             } else {
295 0 0       0 die "Internal: Not a hash nor an array but ",ref($json_resp) ? ref($json_resp) : $json_resp;
296             }
297             }
298              
299             # Update targets if not set in request.
300             sub _update_targets {
301 0     0   0 my $self = shift;
302 0         0 my @requests = @_;
303 0         0 my $target = $self->_clone_target;
304 0         0 for my $req (@requests) {
305 0 0       0 $req->{target} = $target unless exists($req->{target});
306             # A request with existing but undefined target removes
307             # any default
308 0 0       0 delete $req->{target} unless defined($req->{target});
309             }
310 0         0 return @requests;
311             }
312              
313             sub _clone_target {
314 0     0   0 my $self = shift;
315 0 0       0 die "Internal: No target set" unless $self->cfg('target');
316 0         0 my $target = { %{$self->cfg('target')} };
  0         0  
317 0 0       0 if ($target->{env}) {
318 0         0 $target->{env} = { %{$target->{env}}};
  0         0  
319             }
320 0         0 return $target;
321             }
322              
323             =item $url = $agent->request_url($request)
324              
325             Generate the URL for accessing the java agent based on a given request.
326              
327             =cut
328              
329             sub request_url {
330 4     4 1 19 my $self = shift;
331 4         8 my $request = shift;
332 4   33     13 my $url = $self->cfg('url') || croak "No base url given in configuration";
333 4 50       15 $url .= "/" unless $url =~ m|/$|;
334            
335 4         15 my $type = $request->get("type");
336 4         8 my $req = $type . "/";
337 4         11 $req .= $self->_escape($request->get("mbean"));
338            
339 4 50       416 if ($type eq READ) {
    50          
    50          
    0          
    0          
340 0         0 $req .= "/" . $self->_escape($request->get("attribute"));
341 0         0 $req .= $self->_extract_path($request->get("path"));
342             } elsif ($type eq WRITE) {
343 0         0 $req .= "/" . $self->_escape($request->get("attribute"));
344 0         0 $req .= "/" . $self->_escape($self->_null_escape($request->get("value")));
345 0         0 $req .= $self->_extract_path($request->get("path"));
346             } elsif ($type eq LIST) {
347             # The (URI escaped) colon after the must be transformed into a slash
348 4         18 $req =~ s|%3A|/|i;
349 4         13 $req .= $self->_extract_path($request->get("path"));
350             } elsif ($type eq EXEC) {
351 0         0 $req .= "/" . $self->_escape($request->get("operation"));
352 0         0 for my $arg (@{$request->get("arguments")}) {
  0         0  
353             # Array refs are sticked together via ","
354 0 0       0 my $a = ref($arg) eq "ARRAY" ? join ",",@{$arg} : $arg;
  0         0  
355 0         0 $req .= "/" . $self->_escape($self->_null_escape($a));
356             }
357             } elsif ($type eq SEARCH) {
358             # Nothing further to append.
359             }
360             # Squeeze multiple slashes
361 4         41 $req =~ s|((?:!/)?/)/*|$1|g;
362             #print "R: $req\n";
363              
364 4 50 33     35 if ($req =~ $INVALID_PATH_CHARS || $request->{use_query}) {
365 0         0 $req = "?p=$req";
366             }
367 4         8 my @params;
368 4         9 for my $k (@PARAMS) {
369 16 50       33 push @params, $k . "=" . $request->get($k)
370             if $request->get($k);
371             }
372 4 0       13 $req .= ($req =~ /\?/ ? "&" : "?") . join("&",@params) if @params;
    50          
373 4         30 return $url . $req;
374             }
375              
376              
377             # =============================================================================
378              
379              
380             # Return an (optional) path which must already be escaped
381             sub _extract_path {
382 4     4   8 my $self = shift;
383 4         6 my $path = shift;
384 4 100       12 return $path ? "/" . $path : "";
385             }
386              
387              
388             # Escaping is simple:
389             # ! --> !!
390             # / --> !/
391             # It is not done by backslashes '\' since often they get magically get
392             # translated into / when part of an URL
393             sub _escape {
394 4     4   9 my $self = shift;
395 4         5 my $input = shift;
396 4 50       10 if ($self->cfg('legacy-escape')) {
397             # Pre 1.0 escaping:
398 0         0 $input =~ s|(/+)|"/" . ('-' x length($1)) . "/"|eg;
  0         0  
399 0         0 $input =~ s|^/-|/^|; # The first slash needs to be escaped (first)
400 0         0 $input =~ s|-/$|+/|; # as well as last slash. They need a special
401             # escape, because two subsequent slashes get
402             # squeezed to one on the server side
403              
404             } else {
405             # Simpler escaping since 1.0:
406 4         10 $input =~ s/!/!!/g;
407 4         13 $input =~ s/\//!\//g;
408             }
409            
410 4         16 return URI::Escape::uri_escape_utf8($input,"^A-Za-z0-9\-_.!~*'()/"); # Added "/" to
411             # default
412             # set. See L
413             }
414              
415             # Escape empty and undef values so that they can be detangled
416             # on the server side
417             sub _null_escape {
418 0     0     my $self = shift;
419 0           my $value = shift;
420 0 0         if (!defined($value)) {
    0          
421 0           return "[null]";
422             } elsif (! length($value)) {
423 0           return "\"\"";
424             } else {
425 0           return $value;
426             }
427             }
428              
429             # Prepare some readable error text
430             sub _prepare_http_error_text {
431 0     0     my $self = shift;
432 0           my $http_resp = shift;
433 0           my $content = $http_resp->content;
434 0           my $error = "Error while fetching ".$http_resp->request->uri." :\n\n" . $http_resp->status_line . "\n";
435 0           chomp $content;
436 0 0 0       if ($content && $content ne $http_resp->status_line) {
437 0           my $error .= "=" x length($http_resp->status_line) . "\n\n";
438 0           my $short = substr($content,0,600);
439 0 0         $error .= $short . (length($short) < length($content) ? "\n\n... [truncated] ...\n\n" : "") . "\n"
440             }
441 0           return $error;
442             }
443              
444             # Extract all stacktraces stored in the given array ref of json responses
445             sub _extract_stacktraces {
446 0     0     my $self = shift;
447 0           my $json_resp = shift;
448 0           my @ret = ();
449 0           for my $j (@$json_resp) {
450 0 0         push @ret,$j->{stacktrace} if $j->{stacktrace};
451             }
452 0 0         return @ret ? (scalar(@ret) == 1 ? $ret[0] : \@ret) : undef;
    0          
453             }
454              
455             =back
456              
457             =cut
458              
459             # ===================================================================
460             # Specialized UserAgent for passing in credentials:
461              
462             =head1 LICENSE
463              
464             This file is part of jmx4perl.
465              
466             Jmx4perl is free software: you can redistribute it and/or modify
467             it under the terms of the GNU General Public License as published by
468             the Free Software Foundation, either version 2 of the License, or
469             (at your option) any later version.
470              
471             jmx4perl is distributed in the hope that it will be useful,
472             but WITHOUT ANY WARRANTY; without even the implied warranty of
473             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
474             GNU General Public License for more details.
475              
476             You should have received a copy of the GNU General Public License
477             along with jmx4perl. If not, see .
478              
479             A commercial license is available as well. Please contact roland@cpan.org for
480             further details.
481              
482             =head1 AUTHOR
483              
484             roland@cpan.org
485              
486             =cut
487              
488             1;