File Coverage

blib/lib/cPanel/PublicAPI.pm
Criterion Covered Total %
statement 202 351 57.5
branch 84 200 42.0
condition 55 135 40.7
subroutine 27 34 79.4
pod 15 15 100.0
total 383 735 52.1


line stmt bran cond sub pod time code
1             package cPanel::PublicAPI;
2              
3             # Copyright 2019 cPanel, L.L.C.
4             # All rights reserved.
5             # http://cpanel.net
6             #
7             # Redistribution and use in source and binary forms, with or without
8             # modification, are permitted provided that the following conditions are met:
9             #
10             # 1. Redistributions of source code must retain the above copyright notice,
11             # this list of conditions and the following disclaimer.
12             #
13             # 2. Redistributions in binary form must reproduce the above copyright notice,
14             # this list of conditions and the following disclaimer in the documentation
15             # and/or other materials provided with the distribution.
16             #
17             # 3. Neither the name of the owner nor the names of its contributors may be
18             # used to endorse or promote products derived from this software without
19             # specific prior written permission.
20             #
21             # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
22             # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23             # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24             # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25             # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26             # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27             # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28             # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29             # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30             # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31              
32             our $VERSION = '2.8';
33              
34 6     6   433570 use strict;
  6         57  
  6         178  
35 6     6   34 use Carp ();
  6         12  
  6         93  
36 6     6   3128 use MIME::Base64 ();
  6         4273  
  6         150  
37 6     6   4357 use HTTP::Tiny ();
  6         314754  
  6         202  
38 6     6   3286 use HTTP::CookieJar ();
  6         194144  
  6         27521  
39              
40             our %CFG;
41              
42             my %PORT_DB = (
43             'whostmgr' => {
44             'ssl' => 2087,
45             'plaintext' => 2086,
46             },
47             'cpanel' => {
48             'ssl' => 2083,
49             'plaintext' => 2082,
50             },
51             'webmail' => {
52             'ssl' => 2096,
53             'plaintext' => 2095,
54             },
55             );
56              
57             sub _create_http_tiny {
58 13     13   64 return HTTP::Tiny->new(@_);
59             }
60              
61             sub new {
62 14     14 1 46605 my ( $class, %OPTS ) = @_;
63              
64 14         36 my $self = {};
65 14         31 bless( $self, $class );
66              
67 14   100     99 $self->{'debug'} = $OPTS{'debug'} || 0;
68 14   100     55 $self->{'timeout'} = $OPTS{'timeout'} || 300;
69 14 100       39 $self->{'usessl'} = exists $OPTS{'usessl'} ? $OPTS{'usessl'} : 1;
70              
71 14 100       43 if ( exists $OPTS{'ip'} ) {
    100          
72 2         7 $self->{'ip'} = $OPTS{'ip'};
73             }
74             elsif ( exists $OPTS{'host'} ) {
75 1         4 $self->{'host'} = $OPTS{'host'};
76             }
77             else {
78 11         23 $self->{'ip'} = '127.0.0.1';
79             }
80              
81 14   100     58 my $ua_creator = $OPTS{'http_tiny_creator'} || \&_create_http_tiny;
82              
83             $self->{'ua'} = $ua_creator->(
84             agent => "cPanel::PublicAPI/$VERSION ",
85             verify_SSL => ( exists $OPTS{'ssl_verify_mode'} ? $OPTS{'ssl_verify_mode'} : 1 ),
86             keep_alive => ( exists $OPTS{'keepalive'} ? int $OPTS{'keepalive'} : 0 ),
87 14 50       88 timeout => $self->{'timeout'},
    50          
88             );
89              
90 14 100 66     1341 if ( exists $OPTS{'error_log'} && $OPTS{'error_log'} ne 'STDERR' ) {
91 4 50       199 if ( !open( $self->{'error_fh'}, '>>', $OPTS{'error_log'} ) ) {
92 0         0 print STDERR "Unable to open $OPTS{'error_log'} for writing, defaulting to STDERR for error logging: $@\n";
93 0         0 $self->{'error_fh'} = \*STDERR;
94             }
95             }
96             else {
97 10         28 $self->{'error_fh'} = \*STDERR;
98             }
99              
100 14 100       40 if ( $OPTS{'user'} ) {
101 2         7 $self->{'user'} = $OPTS{'user'};
102 2 50       9 $self->debug("Using user param from object creation") if $self->{'debug'};
103             }
104             else {
105 12 50       1197 $self->{'user'} = exists $INC{'Cpanel/PwCache.pm'} ? ( Cpanel::PwCache::getpwuid($>) )[0] : ( getpwuid($>) )[0];
106 12 100       78 $self->debug("Setting user based on current uid ($>)") if $self->{'debug'};
107             }
108              
109 14 100 100     51 if ( exists $OPTS{'api_token'} && exists $OPTS{'accesshash'} ) {
110 1         11 $self->error('You cannot specify both an accesshash and an API token');
111 1         18 die $self->{'error'};
112             }
113              
114             # Allow the user to specify an api_token instead of an accesshash.
115             # Though, it will just act as a synonym.
116 13 100       35 $OPTS{'accesshash'} = $OPTS{'api_token'} if $OPTS{'api_token'};
117              
118 13 100 66     109 if ( ( !exists( $OPTS{'pass'} ) || $OPTS{'pass'} eq '' ) && ( !exists $OPTS{'accesshash'} || $OPTS{'accesshash'} eq '' ) ) {
    100 66        
      100        
119 9 50       791 my $homedir = exists $INC{'Cpanel/PwCache.pm'} ? ( Cpanel::PwCache::getpwuid($>) )[7] : ( getpwuid($>) )[7];
120 9 100       51 $self->debug("Attempting to detect correct authentication credentials") if $self->{'debug'};
121              
122 9 50 33     202 if ( -e $homedir . '/.accesshash' ) {
    50 33        
      33        
      33        
123 0         0 local $/;
124 0 0       0 if ( open( my $hash_fh, '<', $homedir . '/.accesshash' ) ) {
125 0         0 $self->{'accesshash'} = readline($hash_fh);
126 0         0 $self->{'accesshash'} =~ s/[\r\n]+//g;
127 0         0 close($hash_fh);
128 0 0       0 $self->debug("Got accesshash from $homedir/.accesshash") if $self->{'debug'};
129             }
130             else {
131 0 0       0 $self->debug("Failed to fetch accesshash from $homedir/.accesshash") if $self->{'debug'};
132             }
133             }
134             elsif ( exists $ENV{'REMOTE_PASSWORD'} && $ENV{'REMOTE_PASSWORD'} && $ENV{'REMOTE_PASSWORD'} ne '__HIDDEN__' && exists $ENV{'SERVER_SOFTWARE'} && $ENV{'SERVER_SOFTWARE'} =~ /^cpsrvd/ ) {
135 9 100       31 $self->debug("Got user password from the REMOTE_PASSWORD environment variables.") if $self->{'debug'};
136 9         41 $self->{'pass'} = $ENV{'REMOTE_PASSWORD'};
137             }
138             else {
139 0         0 Carp::confess('pass, accesshash, or api_token is a required parameter');
140             }
141             }
142             elsif ( $OPTS{'pass'} ) {
143 2         15 $self->{'pass'} = $OPTS{'pass'};
144 2 50       10 $self->debug("Using pass param from object creation") if $self->{'debug'};
145             }
146             else {
147 2         11 $OPTS{'accesshash'} =~ s/[\r\n]//;
148 2         7 $self->{'accesshash'} = $OPTS{'accesshash'};
149 2 50       7 $self->debug("Using accesshash param from object creation") if $self->{'debug'};
150             }
151              
152 13         58 $self->_update_operating_mode();
153              
154 13         52 return $self;
155             }
156              
157             sub set_debug {
158 1     1 1 573 my $self = shift;
159 1         5 $self->{'debug'} = int shift;
160             }
161              
162             sub user {
163 1     1 1 531 my $self = shift;
164 1         5 $self->{'user'} = shift;
165             }
166              
167             sub pass {
168 1     1 1 527 my $self = shift;
169 1         5 $self->{'pass'} = shift;
170 1         3 delete $self->{'accesshash'};
171 1         4 $self->_update_operating_mode();
172             }
173              
174             sub accesshash {
175 2     2 1 790 my $self = shift;
176 2         5 $self->{'accesshash'} = shift;
177 2         6 delete $self->{'pass'};
178 2         5 $self->_update_operating_mode();
179             }
180              
181             sub api_token {
182 1     1 1 799 return shift->accesshash(@_);
183             }
184              
185             sub whm_api {
186 9     9 1 5617 my ( $self, $call, $formdata, $format ) = @_;
187 9 100       35 $self->_init_serializer() if !exists $cPanel::PublicAPI::CFG{'serializer'};
188 9 50 33     46 if ( !defined $call || $call eq '' ) {
189 0         0 $self->error("A call was not defined when called cPanel::PublicAPI::whm_api_request()");
190             }
191 9 50 100     41 if ( defined $format && $format ne 'xml' && $format ne 'json' && $format ne 'ref' ) {
      66        
      33        
192 0         0 $self->error("cPanel::PublicAPI::whm_api_request() was called with an invalid data format, the only valid format are 'json', 'ref' or 'xml'");
193             }
194              
195 9   100     28 $formdata ||= {};
196 9 100       32 if ( ref $formdata ) {
    100          
197 7         28 $formdata = { 'api.version' => 1, %$formdata };
198             }
199             elsif ( $formdata !~ /(^|&)api\.version=/ ) {
200 1         4 $formdata = "api.version=1&$formdata";
201             }
202              
203 9         16 my $query_format;
204 9 100       21 if ( defined $format ) {
205 2         18 $query_format = $format;
206             }
207             else {
208 7         13 $query_format = $CFG{'serializer'};
209             }
210              
211 9         26 my $uri = "/$query_format-api/$call";
212              
213 9         29 my ( $status, $statusmsg, $data ) = $self->api_request( 'whostmgr', $uri, 'POST', $formdata );
214 9         26511 return $self->_parse_returndata(
215             {
216             'caller' => 'whm_api',
217             'data' => $data,
218             'format' => $format,
219             'call' => $call
220             }
221             );
222             }
223              
224             sub api_request {
225 0     0 1 0 my ( $self, $service, $uri, $method, $formdata, $headers ) = @_;
226              
227 0   0     0 $formdata ||= '';
228 0   0     0 $method ||= 'GET';
229 0   0     0 $headers ||= {};
230              
231 0 0       0 $self->debug("api_request: ( $self, $service, $uri, $method, $formdata, $headers )") if $self->{'debug'};
232              
233 0 0       0 $self->_init() if !exists $CFG{'init'};
234              
235 0         0 undef $self->{'error'};
236 0   0     0 my $timeout = $self->{'timeout'} || 300;
237              
238 0         0 my $orig_alarm = 0;
239 0         0 my $page;
240              
241 0         0 my $port = $self->_determine_port_for_service($service);
242 0 0       0 $self->debug("Found port for service $service to be $port (usessl=$self->{'usessl'})") if $self->{'debug'};
243              
244 0         0 eval {
245 0   0     0 $self->{'remote_server'} = $self->{'ip'} || $self->{'host'};
246 0         0 $self->_validate_connection_settings();
247 0 0       0 if ( $self->{'operating_mode'} eq 'session' ) {
248 0 0 0     0 $self->_establish_session($service) if !( $self->{'security_tokens'}->{$service} && $self->{'cookie_jars'}->{$service} );
249 0         0 $self->{'ua'}->cookie_jar( $self->{'cookie_jars'}->{$service} );
250             }
251              
252 0         0 my $remote_server = $self->{'remote_server'};
253 0         0 my $attempts = 0;
254 0         0 my $finished_request = 0;
255 0         0 my $hassigpipe;
256              
257             local $SIG{'ALRM'} = sub {
258 0     0   0 $self->error('Connection Timed Out');
259 0         0 die $self->{'error'};
260 0         0 };
261              
262 0     0   0 local $SIG{'PIPE'} = sub { $hassigpipe = 1; };
  0         0  
263 0         0 $orig_alarm = alarm($timeout);
264              
265 0 0       0 $formdata = $self->format_http_query($formdata) if ref $formdata;
266              
267 0 0       0 my $scheme = $self->{'usessl'} ? "https" : "http";
268 0         0 my $url = "$scheme://$remote_server:$port";
269 0 0       0 if ( $self->{'operating_mode'} eq 'session' ) {
270 0         0 my $security_token = $self->{'security_tokens'}->{$service};
271 0         0 $url .= '/' . $self->{'security_tokens'}->{$service} . $uri;
272             }
273             else {
274 0         0 $url .= $uri;
275             }
276              
277 0         0 my $content;
278 0 0 0     0 if ( $method eq 'POST' || $method eq 'PUT' ) {
279 0         0 $content = $formdata;
280             }
281             else {
282 0         0 $url .= "?$formdata";
283             }
284 0 0       0 $self->debug("URL: $url") if $self->{'debug'};
285              
286 0 0       0 if ( !ref $headers ) {
287 0         0 my @lines = split /\r\n/, $headers;
288 0         0 $headers = {};
289 0         0 foreach my $line (@lines) {
290 0 0       0 last unless length $line;
291 0         0 my ( $key, $value ) = split /:\s*/, $line, 2;
292 0 0       0 next unless length $key;
293 0   0     0 $headers->{$key} ||= [];
294 0         0 push @{ $headers->{$key} }, $value;
  0         0  
295             }
296             }
297              
298 0 0       0 if ($self->{'operating_mode'} eq 'accesshash') {
299 0 0       0 my $token_app = ($service eq 'whostmgr') ? 'whm' : $service;
300              
301             $headers->{'Authorization'} = sprintf(
302             '%s %s:%s',
303             $token_app,
304             $self->{'user'},
305 0         0 $self->{'accesshash'},
306             );
307             }
308              
309 0         0 my $options = {
310             headers => $headers,
311             };
312 0 0       0 $options->{'content'} = $content if defined $content;
313 0         0 my $ua = $self->{'ua'};
314 0         0 while ( ++$attempts < 3 ) {
315 0         0 $hassigpipe = 0;
316 0         0 my $response = $ua->request( $method, $url, $options );
317 0 0       0 if ( $response->{'status'} == 599 ) {
318 0         0 $self->error("Could not connect to $url: $response->{'content'}");
319 0         0 die $self->{'error'}; #exit eval
320             }
321              
322 0 0       0 if ($hassigpipe) { next; } # http spec says to reconnect
  0         0  
323 0         0 my %HEADERS;
324 0 0       0 if ( $self->{'debug'} ) {
325 0         0 %HEADERS = %{ $response->{'headers'} };
  0         0  
326 0         0 foreach my $header ( keys %HEADERS ) {
327 0         0 $self->debug("HEADER[$header]=[$HEADERS{$header}]");
328             }
329 0 0 0     0 if ( exists $HEADERS{'transfer-encoding'} && $HEADERS{'transfer-encoding'} =~ /chunked/i ) {
    0          
330 0         0 $self->debug("READ TYPE=chunked");
331             }
332             elsif ( defined $HEADERS{'content-length'} ) {
333 0         0 $self->debug("READ TYPE=content-length");
334             }
335             else {
336 0         0 $self->debug("READ TYPE=close");
337             }
338             }
339              
340 0 0       0 if ( !$response->{'success'} ) {
341 0         0 $self->error("Server Error from $remote_server: $response->{'status'} $response->{'reason'}");
342             }
343              
344 0         0 $page = $response->{'content'};
345              
346 0         0 $finished_request = 1;
347 0         0 last;
348             }
349              
350 0 0 0     0 if ( !$finished_request && !$self->{'error'} ) {
351 0         0 $self->error("The request could not be completed after the maximum attempts");
352             }
353              
354             };
355 0 0 0     0 if ( $self->{'debug'} && $@ ) {
356 0         0 warn $@;
357             }
358              
359 0         0 alarm($orig_alarm); # Reset with parent's alarm value
360              
361 0 0       0 return ( $self->{'error'} ? 0 : 1, $self->{'error'}, \$page );
362             }
363              
364             sub establish_tfa_session {
365 0     0 1 0 my ( $self, $service, $tfa_token ) = @_;
366 0 0       0 if ( $self->{'operating_mode'} ne 'session' ) {
367 0         0 $self->error("2FA-authenticated sessions are not supported when using accesshash keys or API tokens");
368 0         0 die $self->{'error'};
369             }
370 0 0 0     0 if ( !( $service && $tfa_token ) ) {
371 0         0 $self->error("You must specify the service name, and the 2FA token in order to establish a 2FA-authenticated session");
372 0         0 die $self->{'error'};
373             }
374              
375 0         0 undef $self->{'cookie_jars'}->{$service};
376 0         0 undef $self->{'security_tokens'}->{$service};
377 0         0 return $self->_establish_session( $service, $tfa_token );
378             }
379              
380             sub _validate_connection_settings {
381 0     0   0 my $self = shift;
382              
383 0 0       0 if ( !$self->{'user'} ) {
384 0         0 $self->error("You must specify a user to login as.");
385 0         0 die $self->{'error'};
386             }
387              
388 0 0       0 if ( !$self->{'remote_server'} ) {
389 0         0 $self->error("You must set a host to connect to. (missing 'host' and 'ip' parameter)");
390 0         0 die $self->{'error'};
391             }
392             }
393              
394             sub _update_operating_mode {
395 16     16   31 my $self = shift;
396              
397 16 100       48 if ( exists $self->{'accesshash'} ) {
    50          
398 4         15 $self->{'accesshash'} =~ s/[\r\n]//g;
399 4         12 $self->{'operating_mode'} = 'accesshash';
400             }
401             elsif ( exists $self->{'pass'} ) {
402 12         26 $self->{'operating_mode'} = 'session';
403              
404             # This is called whenever the pass or accesshash is changed,
405             # so we reset the cookie jars, and tokens on such changes
406 12         45 $self->{'cookie_jars'} = { map { $_ => undef } keys %PORT_DB };
  36         97  
407 12         36 $self->{'security_tokens'} = { map { $_ => undef } keys %PORT_DB };
  36         81  
408             }
409             else {
410 0         0 $self->error('You must specify an accesshash, API token, or password');
411 0         0 die $self->{'error'};
412             }
413             }
414              
415             sub _establish_session {
416 0     0   0 my ( $self, $service, $tfa_token ) = @_;
417              
418 0 0       0 return if $self->{'operating_mode'} ne 'session';
419 0 0 0     0 return if $self->{'security_tokens'}->{$service} && $self->{'cookie_jars'}->{$service};
420              
421 0         0 $self->{'cookie_jars'}->{$service} = HTTP::CookieJar->new();
422 0         0 $self->{'ua'}->cookie_jar( $self->{'cookie_jars'}->{$service} );
423              
424 0         0 my $port = $self->_determine_port_for_service($service);
425 0 0       0 my $scheme = $self->{'usessl'} ? "https" : "http";
426 0         0 my $url = "$scheme://$self->{'remote_server'}:$port/login";
427             my $resp = $self->{'ua'}->post_form(
428             $url,
429             {
430             'user' => $self->{'user'},
431 0 0       0 'pass' => $self->{'pass'},
432             ( $tfa_token ? ( 'tfa_token' => $tfa_token ) : () ),
433             },
434             );
435              
436 0 0       0 if ( my $security_token = ( split /\//, $resp->{'headers'}->{'location'} )[1] ) {
437 0         0 $self->{'security_tokens'}->{$service} = $security_token;
438 0         0 $self->debug("Established $service session");
439 0         0 return 1;
440             }
441              
442 0         0 my $details = $resp->{'reason'};
443 0 0       0 $details .= " ($resp->{'content'})" if $resp->{'status'} == 599;
444              
445 0         0 $self->error("Failed to establish session and parse security token: $resp->{'status'} $details");
446              
447 0         0 die $self->{'error'};
448             }
449              
450             sub _determine_port_for_service {
451 0     0   0 my ( $self, $service ) = @_;
452              
453 0         0 my $port;
454 0 0       0 if ( $self->{'usessl'} ) {
455 0 0       0 $port = $service =~ /^\d+$/ ? $service : $PORT_DB{$service}{'ssl'};
456             }
457             else {
458 0 0       0 $port = $service =~ /^\d+$/ ? $service : $PORT_DB{$service}{'plaintext'};
459             }
460 0         0 return $port;
461             }
462              
463             sub cpanel_api1_request {
464 7     7 1 4885 my ( $self, $service, $cfg, $formdata, $format ) = @_;
465              
466 7         12 my $query_format;
467 7 100       23 if ( defined $format ) {
468 2         4 $query_format = $format;
469             }
470             else {
471 5         13 $query_format = $CFG{'serializer'};
472             }
473              
474 7 50       19 $self->_init_serializer() if !exists $cPanel::PublicAPI::CFG{'serializer'};
475 7         11 my $count = 0;
476 7 100       24 if ( ref $formdata eq 'ARRAY' ) {
477 3         5 $formdata = { map { ( 'arg-' . $count++ ) => $_ } @{$formdata} };
  4         16  
  3         7  
478             }
479 7         13 foreach my $cfg_item ( keys %{$cfg} ) {
  7         22  
480 18         53 $formdata->{ 'cpanel_' . $query_format . 'api_' . $cfg_item } = $cfg->{$cfg_item};
481             }
482 7         21 $formdata->{ 'cpanel_' . $query_format . 'api_apiversion' } = 1;
483              
484 7 50 33     40 my ( $status, $statusmsg, $data ) = $self->api_request( $service, '/' . $query_format . '-api/cpanel', ( ( scalar keys %$formdata < 10 && _total_form_length( $formdata, 1024 ) < 1024 ) ? 'GET' : 'POST' ), $formdata );
485              
486 7         20829 return $self->_parse_returndata(
487             {
488             'caller' => 'cpanel_api1',
489             'data' => $data,
490             'format' => $format,
491             }
492             );
493             }
494              
495             sub cpanel_api2_request {
496 5     5 1 3646 my ( $self, $service, $cfg, $formdata, $format ) = @_;
497 5 50       17 $self->_init_serializer() if !exists $cPanel::PublicAPI::CFG{'serializer'};
498              
499 5         11 my $query_format;
500 5 100       13 if ( defined $format ) {
501 2         5 $query_format = $format;
502             }
503             else {
504 3         9 $query_format = $CFG{'serializer'};
505             }
506              
507 5         8 foreach my $cfg_item ( keys %{$cfg} ) {
  5         16  
508 10         31 $formdata->{ 'cpanel_' . $query_format . 'api_' . $cfg_item } = $cfg->{$cfg_item};
509             }
510 5         16 $formdata->{ 'cpanel_' . $query_format . 'api_apiversion' } = 2;
511 5 50 33     29 my ( $status, $statusmsg, $data ) = $self->api_request( $service, '/' . $query_format . '-api/cpanel', ( ( scalar keys %$formdata < 10 && _total_form_length( $formdata, 1024 ) < 1024 ) ? 'GET' : 'POST' ), $formdata );
512              
513 5         13784 return $self->_parse_returndata(
514             {
515             'caller' => 'cpanel_api2',
516             'data' => $data,
517             'format' => $format,
518             }
519             );
520             }
521              
522             sub _parse_returndata {
523 21     21   49 my ( $self, $opts_hr ) = @_;
524              
525 21 50       49 if ( $self->{'error'} ) {
    50          
526 0         0 die $self->{'error'};
527             }
528 21         87 elsif ( ${ $opts_hr->{'data'} } =~ m/tfa_login_form/ ) {
529 0         0 $self->error("Two-Factor Authentication enabled on the account. Establish a session with the security token, or disable 2FA on the account");
530 0         0 die $self->{'error'};
531             }
532              
533 21 100 66     97 if ( defined $opts_hr->{'format'} && ( $opts_hr->{'format'} eq 'json' || $opts_hr->{'format'} eq 'xml' ) ) {
      66        
534 6         11 return ${ $opts_hr->{'data'} };
  6         32  
535             }
536             else {
537 15         23 my $parsed_data;
538 15         21 eval { $parsed_data = $CFG{'api_decode_func'}->( ${ $opts_hr->{'data'} } ); };
  15         22  
  15         110  
539 15 50       42 if ( !ref $parsed_data ) {
540 0         0 $self->error("There was an issue with parsing the following response from cPanel or WHM: [data=[${$opts_hr->{'data'}}]]");
  0         0  
541 0         0 die $self->{'error'};
542             }
543              
544 15         52 my $error_check_dt = {
545             'whm_api' => \&_check_whm_api_errors,
546             'cpanel_api1' => \&_check_cpanel_api1_errors,
547             'cpanel_api2' => \&_check_cpanel_api2_errors,
548             };
549 15         60 return $error_check_dt->{ $opts_hr->{'caller'} }->( $self, $opts_hr->{'call'}, $parsed_data );
550             }
551             }
552              
553             sub _check_whm_api_errors {
554 7     7   51 my ( $self, $call, $parsed_data ) = @_;
555              
556 7 100 66     48 if (
      33        
      66        
557             ( exists $parsed_data->{'error'} && $parsed_data->{'error'} =~ /Unknown App Requested/ ) || # xml-api v0 version
558             ( exists $parsed_data->{'metadata'}->{'reason'} && $parsed_data->{'metadata'}->{'reason'} =~ /Unknown app\s+(?:\(.+\))?\s+requested/ ) # xml-api v1 version
559             ) {
560 1         15 $self->error("cPanel::PublicAPI::whm_api was called with the invalid API call of: $call.");
561 1         7 return;
562             }
563 6         35 return $parsed_data;
564             }
565              
566             sub _check_cpanel_api1_errors {
567 5     5   18 my ( $self, undef, $parsed_data ) = @_;
568 5 50 33     25 if (
      66        
569             exists $parsed_data->{'event'}->{'reason'} && (
570             $parsed_data->{'event'}->{'reason'} =~ /failed: Undefined subroutine/ || # pre-11.44 error message
571             $parsed_data->{'event'}->{'reason'} =~ m/failed: Can\'t use string/ # 11.44+ error message
572             )
573             ) {
574 1         10 $self->error( "cPanel::PublicAPI::cpanel_api1_request was called with the invalid API1 call of: " . $parsed_data->{'module'} . '::' . $parsed_data->{'func'} );
575 1         6 return;
576             }
577 4         26 return $parsed_data;
578             }
579              
580             sub _check_cpanel_api2_errors {
581 3     3   11 my ( $self, undef, $parsed_data ) = @_;
582              
583 3 100 66     18 if ( exists $parsed_data->{'cpanelresult'}->{'error'} && $parsed_data->{'cpanelresult'}->{'error'} =~ /Could not find function/ ) { # xml-api v1 version
584 1         8 $self->error( "cPanel::PublicAPI::cpanel_api2_request was called with the invalid API2 call of: " . $parsed_data->{'cpanelresult'}->{'module'} . '::' . $parsed_data->{'cpanelresult'}->{'func'} );
585 1         6 return;
586             }
587 2         14 return $parsed_data;
588             }
589              
590             sub _total_form_length {
591 12     12   22 my $data = shift;
592 12         19 my $max = shift;
593 12         17 my $size = 0;
594 12         21 foreach my $key ( keys %{$data} ) {
  12         25  
595 46 50       113 return 1024 if ( ( $size += ( length($key) + 2 + length( $data->{$key} ) ) ) >= 1024 );
596             }
597 12         71 return $size;
598             }
599              
600             sub _init_serializer {
601 2 50   2   605 return if exists $CFG{'serializer'};
602 2         5 my $self = shift; #not required
603 2         13 foreach my $serializer (
604              
605             #module, key (cpanel api uri), deserializer function name
606             [ 'JSON::Syck', 'json', 'Load' ],
607             [ 'JSON', 'json', 'decode_json' ],
608             [ 'JSON::XS', 'json', 'decode_json' ],
609             [ 'JSON::PP', 'json', 'decode_json' ],
610             ) {
611 6         16 my $serializer_module = $serializer->[0];
612 6         8 my $serializer_key = $serializer->[1];
613 6         360 eval " require $serializer_module; ";
614 6 100       6608 if ( !$@ ) {
615 2 50 33     25 $self->debug("loaded serializer: $serializer_module") if $self && ref $self && $self->{'debug'};
      33        
616 2         9 $CFG{'serializer'} = $CFG{'parser_key'} = $serializer_key;
617 2         8 $CFG{'serializer_module'} = $CFG{'parser_module'} = $serializer_module;
618 2         18 $CFG{'api_decode_func'} = $serializer_module->can($serializer->[2]);
619              
620 2         8 last;
621             }
622             else {
623 4 50 33     79 $self->debug("Failed to load serializer: $serializer_module: @_") if $self && ref $self && $self->{'debug'};
      33        
624             }
625             }
626 2 50       11 if ($@) {
627 0         0 Carp::confess("Unable to find a module capable of deserializing the api response.");
628             }
629             }
630              
631             sub _init {
632 1 50   1   583 return if exists $CFG{'init'};
633 1         3 my $self = shift; #not required
634 1         4 $CFG{'init'} = 1;
635              
636             # moved this over to a pattern to allow easy change of deps
637 1         6 foreach my $encoder (
638             [ 'Cpanel/Encoder/URI.pm', 'Cpanel::Encoder::URI', 'uri_encode_str' ],
639             [ 'URI/Escape.pm', 'URI::Escape', 'uri_escape' ],
640             ) {
641 2         5 my $module_path = $encoder->[0];
642 2         5 my $module = $encoder->[1];
643 2         4 my $funcname = $encoder->[2];
644 2         5 eval { require $module_path; };
  2         820  
645              
646 2 100       1527 if ( !$@ ) {
647 1 50 33     13 $self->debug("loaded encoder: $module_path") if $self && ref $self && $self->{'debug'};
      33        
648 1         13 $CFG{'uri_encoder_func'} = $module->can($funcname);
649 1         5 last;
650             }
651             else {
652 1 50 33     15 $self->debug("failed to load encoder: $module_path") if $self && ref $self && $self->{'debug'};
      33        
653             }
654             }
655 1 50       6 if ($@) {
656 0         0 Carp::confess("Unable to find a module capable of encoding api requests.");
657             }
658             }
659              
660             sub error {
661 5     5 1 337 my ( $self, $msg ) = @_;
662 5         10 print { $self->{'error_fh'} } $msg . "\n";
  5         72  
663 5         22 $self->{'error'} = $msg;
664             }
665              
666             sub debug {
667 3     3 1 11 my ( $self, $msg ) = @_;
668 3         4 print { $self->{'error_fh'} } "debug: " . $msg . "\n";
  3         26  
669             }
670              
671             sub format_http_headers {
672 1     1 1 811 my ( $self, $headers ) = @_;
673 1 50       6 if ( ref $headers ) {
674 1 50       3 return '' if !scalar keys %{$headers};
  1         6  
675 1 50       3 return join( "\r\n", map { $_ ? ( $_ . ': ' . $headers->{$_} ) : () } keys %{$headers} ) . "\r\n";
  1         11  
  1         5  
676             }
677 0         0 return $headers;
678             }
679              
680             sub format_http_query {
681 1     1 1 1824 my ( $self, $formdata ) = @_;
682 1 50       5 if ( ref $formdata ) {
683 1         3 return join( '&', map { $CFG{'uri_encoder_func'}->($_) . '=' . $CFG{'uri_encoder_func'}->( $formdata->{$_} ) } sort keys %{$formdata} );
  2         48  
  1         8  
684             }
685 0           return $formdata;
686             }
687