File Coverage

blib/lib/cPanel/PublicAPI.pm
Criterion Covered Total %
statement 202 351 57.5
branch 85 202 42.0
condition 55 135 40.7
subroutine 27 34 79.4
pod 15 15 100.0
total 384 737 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.7';
33              
34 6     6   421554 use strict;
  6         50  
  6         141  
35 6     6   26 use Carp ();
  6         10  
  6         81  
36 6     6   3468 use MIME::Base64 ();
  6         4309  
  6         123  
37 6     6   3914 use HTTP::Tiny ();
  6         270868  
  6         165  
38 6     6   2559 use HTTP::CookieJar ();
  6         150163  
  6         24320  
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   42 return HTTP::Tiny->new(@_);
59             }
60              
61             sub new {
62 14     14 1 40069 my ( $class, %OPTS ) = @_;
63              
64 14         27 my $self = {};
65 14         25 bless( $self, $class );
66              
67 14   100     63 $self->{'debug'} = $OPTS{'debug'} || 0;
68 14   100     42 $self->{'timeout'} = $OPTS{'timeout'} || 300;
69 14 100       33 $self->{'usessl'} = exists $OPTS{'usessl'} ? $OPTS{'usessl'} : 1;
70              
71 14 100       31 if ( exists $OPTS{'ip'} ) {
    100          
72 2         5 $self->{'ip'} = $OPTS{'ip'};
73             }
74             elsif ( exists $OPTS{'host'} ) {
75 1         3 $self->{'host'} = $OPTS{'host'};
76             }
77             else {
78 11         15 $self->{'ip'} = '127.0.0.1';
79             }
80              
81 14   100     45 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       63 timeout => $self->{'timeout'},
    50          
88             );
89              
90 14 100 66     1179 if ( exists $OPTS{'error_log'} && $OPTS{'error_log'} ne 'STDERR' ) {
91 4 50       178 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         21 $self->{'error_fh'} = \*STDERR;
98             }
99              
100 14 100       33 if ( $OPTS{'user'} ) {
101 2         7 $self->{'user'} = $OPTS{'user'};
102 2 50       7 $self->debug("Using user param from object creation") if $self->{'debug'};
103             }
104             else {
105 12 50       1073 $self->{'user'} = exists $INC{'Cpanel/PwCache.pm'} ? ( Cpanel::PwCache::getpwuid($>) )[0] : ( getpwuid($>) )[0];
106 12 100       98 $self->debug("Setting user based on current uid ($>)") if $self->{'debug'};
107             }
108              
109 14 100 100     41 if ( exists $OPTS{'api_token'} && exists $OPTS{'accesshash'} ) {
110 1         8 $self->error('You cannot specify both an accesshash and an API token');
111 1         14 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       31 $OPTS{'accesshash'} = $OPTS{'api_token'} if $OPTS{'api_token'};
117              
118 13 100 66     98 if ( ( !exists( $OPTS{'pass'} ) || $OPTS{'pass'} eq '' ) && ( !exists $OPTS{'accesshash'} || $OPTS{'accesshash'} eq '' ) ) {
    100 66        
      100        
119 9 50       652 my $homedir = exists $INC{'Cpanel/PwCache.pm'} ? ( Cpanel::PwCache::getpwuid($>) )[7] : ( getpwuid($>) )[7];
120 9 100       42 $self->debug("Attempting to detect correct authentication credentials") if $self->{'debug'};
121              
122 9 50 33     158 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       37 $self->debug("Got user password from the REMOTE_PASSWORD environment variables.") if $self->{'debug'};
136 9         33 $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         11 $self->{'pass'} = $OPTS{'pass'};
144 2 50       7 $self->debug("Using pass param from object creation") if $self->{'debug'};
145             }
146             else {
147 2         9 $OPTS{'accesshash'} =~ s/[\r\n]//;
148 2         35 $self->{'accesshash'} = $OPTS{'accesshash'};
149 2 50       7 $self->debug("Using accesshash param from object creation") if $self->{'debug'};
150             }
151              
152 13         40 $self->_update_operating_mode();
153              
154 13         42 return $self;
155             }
156              
157             sub set_debug {
158 1     1 1 427 my $self = shift;
159 1         4 $self->{'debug'} = int shift;
160             }
161              
162             sub user {
163 1     1 1 422 my $self = shift;
164 1         3 $self->{'user'} = shift;
165             }
166              
167             sub pass {
168 1     1 1 440 my $self = shift;
169 1         4 $self->{'pass'} = shift;
170 1         2 delete $self->{'accesshash'};
171 1         4 $self->_update_operating_mode();
172             }
173              
174             sub accesshash {
175 2     2 1 614 my $self = shift;
176 2         5 $self->{'accesshash'} = shift;
177 2         4 delete $self->{'pass'};
178 2         5 $self->_update_operating_mode();
179             }
180              
181             sub api_token {
182 1     1 1 632 return shift->accesshash(@_);
183             }
184              
185             sub whm_api {
186 9     9 1 3719 my ( $self, $call, $formdata, $format ) = @_;
187 9 100       28 $self->_init_serializer() if !exists $cPanel::PublicAPI::CFG{'serializer'};
188 9 50 33     36 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     30 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     24 $formdata ||= {};
196 9 100       24 if ( ref $formdata ) {
    100          
197 7         24 $formdata = { 'api.version' => 1, %$formdata };
198             }
199             elsif ( $formdata !~ /(^|&)api\.version=/ ) {
200 1         3 $formdata = "api.version=1&$formdata";
201             }
202              
203 9         12 my $query_format;
204 9 100       16 if ( defined $format ) {
205 2         3 $query_format = $format;
206             }
207             else {
208 7         12 $query_format = $CFG{'serializer'};
209             }
210              
211 9         18 my $uri = "/$query_format-api/$call";
212              
213 9         24 my ( $status, $statusmsg, $data ) = $self->api_request( 'whostmgr', $uri, 'POST', $formdata );
214 9         16209 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   24 my $self = shift;
396              
397 16 100       39 if ( exists $self->{'accesshash'} ) {
    50          
398 4         14 $self->{'accesshash'} =~ s/[\r\n]//g;
399 4         21 $self->{'operating_mode'} = 'accesshash';
400             }
401             elsif ( exists $self->{'pass'} ) {
402 12         21 $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         81  
407 12         28 $self->{'security_tokens'} = { map { $_ => undef } keys %PORT_DB };
  36         72  
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 3067 my ( $self, $service, $cfg, $formdata, $format ) = @_;
465              
466 7         9 my $query_format;
467 7 100       17 if ( defined $format ) {
468 2         3 $query_format = $format;
469             }
470             else {
471 5         7 $query_format = $CFG{'serializer'};
472             }
473              
474 7 50       18 $self->_init_serializer() if !exists $cPanel::PublicAPI::CFG{'serializer'};
475 7         11 my $count = 0;
476 7 100       19 if ( ref $formdata eq 'ARRAY' ) {
477 3         5 $formdata = { map { ( 'arg-' . $count++ ) => $_ } @{$formdata} };
  4         13  
  3         7  
478             }
479 7         11 foreach my $cfg_item ( keys %{$cfg} ) {
  7         19  
480 18         40 $formdata->{ 'cpanel_' . $query_format . 'api_' . $cfg_item } = $cfg->{$cfg_item};
481             }
482 7         17 $formdata->{ 'cpanel_' . $query_format . 'api_apiversion' } = 1;
483              
484 7 50 33     34 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         12895 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 2216 my ( $self, $service, $cfg, $formdata, $format ) = @_;
497 5 50       15 $self->_init_serializer() if !exists $cPanel::PublicAPI::CFG{'serializer'};
498              
499 5         6 my $query_format;
500 5 100       12 if ( defined $format ) {
501 2         3 $query_format = $format;
502             }
503             else {
504 3         6 $query_format = $CFG{'serializer'};
505             }
506              
507 5         7 foreach my $cfg_item ( keys %{$cfg} ) {
  5         12  
508 10         27 $formdata->{ 'cpanel_' . $query_format . 'api_' . $cfg_item } = $cfg->{$cfg_item};
509             }
510 5         10 $formdata->{ 'cpanel_' . $query_format . 'api_apiversion' } = 2;
511 5 50 33     26 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         8488 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   41 my ( $self, $opts_hr ) = @_;
524              
525 21 50       44 if ( $self->{'error'} ) {
    50          
526 0         0 die $self->{'error'};
527             }
528 21         67 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     70 if ( defined $opts_hr->{'format'} && ( $opts_hr->{'format'} eq 'json' || $opts_hr->{'format'} eq 'xml' ) ) {
      66        
534 6         9 return ${ $opts_hr->{'data'} };
  6         27  
535             }
536             else {
537 15         16 my $parsed_data;
538 15 50       21 eval { $parsed_data = $CFG{'serializer_can_deref'} ? $CFG{'api_serializer_obj'}->( $opts_hr->{'data'} ) : $CFG{'api_serializer_obj'}->( ${ $opts_hr->{'data'} } ); };
  15         30  
  15         81  
539 15 50       38 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         47 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         44 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   13 my ( $self, $call, $parsed_data ) = @_;
555              
556 7 100 66     36 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         11 $self->error("cPanel::PublicAPI::whm_api was called with the invalid API call of: $call.");
561 1         5 return;
562             }
563 6         27 return $parsed_data;
564             }
565              
566             sub _check_cpanel_api1_errors {
567 5     5   13 my ( $self, undef, $parsed_data ) = @_;
568 5 50 33     23 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         5 $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         20 return $parsed_data;
578             }
579              
580             sub _check_cpanel_api2_errors {
581 3     3   8 my ( $self, undef, $parsed_data ) = @_;
582              
583 3 100 66     14 if ( exists $parsed_data->{'cpanelresult'}->{'error'} && $parsed_data->{'cpanelresult'}->{'error'} =~ /Could not find function/ ) { # xml-api v1 version
584 1         6 $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         4 return;
586             }
587 2         11 return $parsed_data;
588             }
589              
590             sub _total_form_length {
591 12     12   19 my $data = shift;
592 12         14 my $max = shift;
593 12         14 my $size = 0;
594 12         13 foreach my $key ( keys %{$data} ) {
  12         24  
595 46 50       89 return 1024 if ( ( $size += ( length($key) + 2 + length( $data->{$key} ) ) ) >= 1024 );
596             }
597 12         54 return $size;
598             }
599              
600             sub _init_serializer {
601 2 50   2   666 return if exists $CFG{'serializer'};
602 2         5 my $self = shift; #not required
603 2         22 foreach my $serializer (
604              
605             #module, key (cpanel api uri), deserializer function, deserializer understands references
606             [ 'JSON::Syck', 'json', \&JSON::Syck::Load, 0 ],
607             [ 'JSON', 'json', \&JSON::decode_json, 0 ],
608             [ 'JSON::XS', 'json', \&JSON::XS::decode_json, 0 ],
609             [ 'JSON::PP', 'json', \&JSON::PP::decode_json, 0 ],
610             ) {
611 6         12 my $serializer_module = $serializer->[0];
612 6         9 my $serializer_key = $serializer->[1];
613 6         8 $CFG{'api_serializer_obj'} = $serializer->[2];
614 6         11 $CFG{'serializer_can_deref'} = $serializer->[3];
615 6         366 eval " require $serializer_module; ";
616 6 100       5103 if ( !$@ ) {
617 2 50 33     19 $self->debug("loaded serializer: $serializer_module") if $self && ref $self && $self->{'debug'};
      33        
618 2         6 $CFG{'serializer'} = $CFG{'parser_key'} = $serializer_key;
619 2         7 $CFG{'serializer_module'} = $CFG{'parser_module'} = $serializer_module;
620 2         4 last;
621             }
622             else {
623 4 50 33     32 $self->debug("Failed to load serializer: $serializer_module: @_") if $self && ref $self && $self->{'debug'};
      33        
624             }
625             }
626 2 50       10 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   485 return if exists $CFG{'init'};
633 1         3 my $self = shift; #not required
634 1         3 $CFG{'init'} = 1;
635              
636             # moved this over to a pattern to allow easy change of deps
637 1         7 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         6 my $module = $encoder->[0];
642 2         5 my $function = $encoder->[1];
643 2         5 eval { require $module; };
  2         62422  
644              
645 2 100       1290 if ( !$@ ) {
646 1 50 33     15 $self->debug("loaded encoder: $module") if $self && ref $self && $self->{'debug'};
      33        
647 1         3 $CFG{'uri_encoder_func'} = $function;
648 1         3 last;
649             }
650             else {
651 1 50 33     22 $self->debug("failed to load encoder: $module") if $self && ref $self && $self->{'debug'};
      33        
652             }
653             }
654 1 50       7 if ($@) {
655 0         0 Carp::confess("Unable to find a module capable of encoding api requests.");
656             }
657             }
658              
659             sub error {
660 5     5 1 282 my ( $self, $msg ) = @_;
661 5         8 print { $self->{'error_fh'} } $msg . "\n";
  5         79  
662 5         18 $self->{'error'} = $msg;
663             }
664              
665             sub debug {
666 3     3 1 8 my ( $self, $msg ) = @_;
667 3         4 print { $self->{'error_fh'} } "debug: " . $msg . "\n";
  3         21  
668             }
669              
670             sub format_http_headers {
671 1     1 1 649 my ( $self, $headers ) = @_;
672 1 50       5 if ( ref $headers ) {
673 1 50       2 return '' if !scalar keys %{$headers};
  1         4  
674 1 50       3 return join( "\r\n", map { $_ ? ( $_ . ': ' . $headers->{$_} ) : () } keys %{$headers} ) . "\r\n";
  1         8  
  1         3  
675             }
676 0         0 return $headers;
677             }
678              
679             sub format_http_query {
680 1     1 1 1830 my ( $self, $formdata ) = @_;
681 1 50       4 if ( ref $formdata ) {
682 1         3 return join( '&', map { $CFG{'uri_encoder_func'}->($_) . '=' . $CFG{'uri_encoder_func'}->( $formdata->{$_} ) } sort keys %{$formdata} );
  2         39  
  1         5  
683             }
684 0           return $formdata;
685             }
686