File Coverage

blib/lib/cPanel/PublicAPI.pm
Criterion Covered Total %
statement 200 349 57.3
branch 85 202 42.0
condition 53 133 39.8
subroutine 26 33 78.7
pod 15 15 100.0
total 379 732 51.7


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