CodeCommitsIssuesPull requestsActionsInsightsSecurity
2d94a3e447917255d1b5f4790b30b16fc9ff308d

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

Cpanel/CloudFlare.pm

555lines · modecode

1package Cpanel::CloudFlare;
2
3# cpanel - Cpanel/CloudFlare.pm Copyright(c) 2011 CloudFlare, Inc.
4# All rights Reserved.
5# copyright@cloudflare.com http://cloudflare.com
6# @author ian@cloudflare.com
7# This code is subject to the cPanel license. Unauthorized copying is prohibited
8
9use Cpanel::DnsUtils::UsercPanel ();
10use Cpanel::AdminBin ();
11use Cpanel::Locale ();
12use Cpanel::Logger ();
13use Cpanel::UrlTools ();
14use Cpanel::SocketIP ();
15use Cpanel::Encoder::URI ();
16use Cpanel::AcctUtils ();
17use Cpanel::DomainLookup ();
18use Cpanel::DataStore ();
19
20use Socket ();
21use JSON::Syck ();
22use strict;
23
24my $logger = Cpanel::Logger->new();
25my $locale;
26my $cf_config_file = "/usr/local/cpanel/etc/cloudflare.json";
27my $cf_data_file = "/usr/local/cpanel/etc/cloudflare_data.yaml";
28my $cf_host_key;
29my $cf_host_name;
30my $cf_host_uri;
31my $cf_user_name;
32my $cf_user_uri;
33my $cf_host_port;
34my $cf_host_prefix;
35my $has_ssl;
36my $cf_debug_mode;
37my $cf_global_data = {};
38
39my %KEYMAP = ( 'line' => 'Line', 'ttl' => 'ttl', 'name' => 'name',
40 'class' => 'class', 'address' => 'address', 'type' => 'type',
41 'txtdata' => 'txtdata', 'preference' => 'preference', 'exchange' => 'exchange' );
42
43## Initialize vars here.
44sub CloudFlare_init {
45 my $data = JSON::Syck::LoadFile($cf_config_file);
46 $cf_host_key = $data->{"host_key"};
47 $cf_host_name = $data->{"host_name"};
48 $cf_host_uri = $data->{"host_uri"};
49 $cf_host_port = $data->{"host_port"};
50 $cf_host_prefix = $data->{"host_prefix"};
51 $cf_debug_mode = $data->{"debug"};
52 $cf_user_name = $data->{"user_name"};
53 $cf_user_uri = $data->{"user_uri"};
54
55 ## Load the global statshot of who is on CF.
56 if( Cpanel::DataStore::load_ref( $cf_data_file, $cf_global_data ) ) {
57 #$logger->info("Successfully loaded cf data");
58 } else {
59 $cf_global_data = {"cf_zones" => {}};
60 $logger->warn( "Failed to load cf data -- storing blank data");
61 Cpanel::DataStore::store_ref($cf_data_file, $cf_global_data);
62 }
63
64 eval { use Net::SSLeay qw(post_https make_headers make_form); $has_ssl = 1 };
65 if ( !$has_ssl ) {
66 $logger->warn("Failed to load Net::SSLeay: $@.\nDisabling functionality until fixed.");
67 }
68}
69
70
71# Can only be called with json or xml api because it uses
72# a non-standard return
73sub api2_user_create {
74 my %OPTS = @_;
75
76 if (!$cf_host_key) {
77 $logger->info("Missing cf_host_key! Define this in $cf_config_file.");
78 return [];
79 }
80
81 # Use a random string as a password.
82 my $password = crypt(int(rand(10000000)), time);
83 $logger->info("Createing Cloudflare user for " . $OPTS{"email"} . " -- " . $password);
84
85 ## Otherwise, try to create this user.
86 my $args = {
87 "host" => $cf_host_name,
88 "uri" => $cf_host_uri,
89 "port" => $cf_host_port,
90 "query" => {
91 "act" => "user_create",
92 "host_key" => $cf_host_key,
93 "cloudflare_email" => $OPTS{"email"},
94 "cloudflare_pass" => $password
95 },
96 };
97
98 my $result = __https_post_req->($args);
99 return JSON::Syck::Load($result);
100}
101
102# Can only be called with json or xml api because it uses
103# a non-standard return
104sub api2_user_lookup {
105 my %OPTS = @_;
106
107 if (!$cf_host_key) {
108 $logger->info("Missing cf_host_key! Define this in $cf_config_file.");
109 return [];
110 }
111
112 if ( !$has_ssl ) {
113 $logger->info("No SSL Configured");
114 return [{"result"=>"error",
115 "msg" => "CloudFlare is disabled until Net::SSLeay is installed on this server."}];
116 }
117
118
119 ## Otherwise, try to log this user in.
120 my $login_args = {
121 "host" => $cf_host_name,
122 "uri" => $cf_host_uri,
123 "port" => $cf_host_port,
124 "query" => {
125 "act" => "user_lookup",
126 "host_key" => $cf_host_key,
127 "cloudflare_email" => $OPTS{"email"}
128 },
129 };
130
131 my $result = __https_post_req->($login_args);
132 return JSON::Syck::Load($result);
133}
134
135## Pulls certain stats for the passed in zone.
136sub api2_get_stats {
137 my %OPTS = @_;
138
139 if (!$OPTS{"user_api_key"}) {
140 $logger->info("Missing user_api_key!");
141 return [];
142 }
143
144 if ( !$has_ssl ) {
145 $logger->info("No SSL Configured");
146 return [{"result"=>"error",
147 "msg" => "CloudFlare is disabled until Net::SSLeay is installed on this server."}];
148 }
149
150 ## Otherwise, pull this users stats.
151 my $stats_args = {
152 "host" => $cf_user_name,
153 "uri" => $cf_user_uri,
154 "port" => $cf_host_port,
155 "query" => {
156 "a" => "stats",
157 "z" => $OPTS{"zone_name"},
158 "tkn" => $OPTS{"user_api_key"},
159 "u" => $OPTS{"user_email"},
160 "interval" => 30, # 30 = last 7 days, 20 = last 30 days 40 = last 24 hours
161 },
162 };
163
164 my $result = __https_post_req->($stats_args);
165 return JSON::Syck::Load($result);
166}
167
168sub api2_edit_cf_setting {
169 my %OPTS = @_;
170
171 if (!$OPTS{"user_api_key"}) {
172 $logger->info("Missing user_api_key!");
173 return [];
174 }
175
176 if ( !$has_ssl ) {
177 $logger->info("No SSL Configured");
178 return [{"result"=>"error",
179 "msg" => "CloudFlare is disabled until Net::SSLeay is installed on this server."}];
180 }
181
182 ## Otherwise, pull this users stats.
183 my $stats_args = {
184 "host" => $cf_user_name,
185 "uri" => $cf_user_uri,
186 "port" => $cf_host_port,
187 "query" => {
188 "a" => $OPTS{"a"},
189 "z" => $OPTS{"zone_name"},
190 "tkn" => $OPTS{"user_api_key"},
191 "u" => $OPTS{"user_email"},
192 "v" => $OPTS{"v"},
193 },
194 };
195
196 my $result = __https_post_req->($stats_args);
197 return JSON::Syck::Load($result);
198}
199
200# Can only be called with json or xml api because it uses
201# a non-standard return
202sub api2_zone_set {
203 my %OPTS = @_;
204
205 if (!$cf_host_key || !$cf_host_prefix) {
206 $logger->info("Missing cf_host_key or $cf_host_prefix! Define these in $cf_config_file.");
207 return [];
208 }
209
210 my $domain = ".".$OPTS{"zone_name"}.".";
211 my $subs = $OPTS{"subdomains"};
212 $subs =~ s/${domain}//g;
213
214 ## Unpack the mapping from recs to lines (ugg, this is SOOO BAAD)
215 my $recs2lines = JSON::Syck::Load($OPTS{"cf_recs"});
216
217 ## Set up the zone_set args.
218 my $login_args = {
219 "host" => $cf_host_name,
220 "uri" => $cf_host_uri,
221 "port" => $cf_host_port,
222 "query" => {
223 "act" => "zone_set",
224 "host_key" => $cf_host_key,
225 "user_key" => $OPTS{"user_key"},
226 "zone_name" => $OPTS{"zone_name"},
227 "resolve_to" => $cf_host_prefix . "." . $OPTS{"zone_name"},
228 "subdomains" => $subs
229 },
230 };
231
232 if (!$subs) {
233 $login_args->{"query"}->{"act"} = "zone_delete";
234 $cf_global_data->{"cf_zones"}->{$OPTS{"zone_name"}} = 0
235 }
236
237 my $result = JSON::Syck::Load(__https_post_req->($login_args));
238
239 ## Args for updating local DNS.
240 my %zone_args = ("domain" => $OPTS{"zone_name"},
241 "class" => "IN",
242 "type" => "CNAME",
243 "name" => $cf_host_prefix,
244 "ttl" => 1400,
245 "cname" => $OPTS{"zone_name"},
246 );
247
248 my $is_cf = 0;
249
250 ## If we get an error, do nothing and return the error to the user.
251 if ($result->{"result"} eq "error") {
252 $logger->info("CloudFlare Error: " . $result->{"msg"});
253 } else {
254 ## Otherwise, update the dns for this zone.
255 my $dom = ".".$OPTS{"zone_name"};
256
257 ## First Make sure that the resolve to is set.
258 my $res = Cpanel::AdminBin::adminfetchnocache( 'zone', '', 'ADD', 'storable', $OPTS{"zone_name"},
259 __serialize_request( \%zone_args ) );
260
261 ## If there's a delete, remove this record from being CF.
262 if ($OPTS{"old_line"}) {
263 $zone_args{"line"} = $OPTS{"old_line"};
264 $zone_args{"name"} = $OPTS{"old_rec"};
265 $zone_args{"name"} =~ s/$domain//g;
266 $zone_args{"cname"} = $OPTS{"zone_name"};
267 $res = Cpanel::AdminBin::adminfetchnocache( 'zone', '', 'EDIT', 'storable', $OPTS{"zone_name"},
268 __serialize_request( \%zone_args ) );
269 }
270
271 ## Now, go ahead and update all of the CF enabled recs.
272 foreach my $ft (keys %{$result->{"response"}->{"forward_tos"}}) {
273 $zone_args{"line"} = $recs2lines->{$ft."."};
274 $zone_args{"name"} = $ft;
275 $zone_args{"name"} =~ s/$dom//g;
276 $zone_args{"cname"} = $result->{"response"}->{"forward_tos"}->{$ft};
277
278 $res = Cpanel::AdminBin::adminfetchnocache( 'zone', '', 'EDIT', 'storable', $OPTS{"zone_name"},
279 __serialize_request( \%zone_args ) );
280 if (!$res->{"status"}) {
281 ## Try again, bumping up the line by 1. -- no idea why this works.
282 $zone_args{"line"}++;
283 $res = Cpanel::AdminBin::adminfetchnocache( 'zone', '', 'EDIT', 'storable', $OPTS{"zone_name"},
284 __serialize_request( \%zone_args ) );
285 }
286
287 if (!$res->{"status"}) {
288 $logger->info("Failed to set DNS for CloudFlare record $ft!");
289 $logger->info(JSON::Syck::Dump($res));
290 $result->{"result"} = "error";
291 $result->{"msg"} = $res->{"statusmsg"};
292 }
293
294 ## Note that if at least one rec is on, this zone is on CF.
295 $cf_global_data->{"cf_zones"}->{$OPTS{"zone_name"}} = 1;
296 $is_cf = 1;
297 }
298 }
299
300 if (!$is_cf) {
301 $cf_global_data->{"cf_zones"}->{$OPTS{"zone_name"}} = 0;
302 }
303
304 ## Save the updated global data arg.
305 Cpanel::DataStore::store_ref($cf_data_file, $cf_global_data);
306
307 return $result;
308}
309
310sub api2_zone_delete {
311 my %OPTS = @_;
312
313 if (!$cf_host_key || !$cf_host_prefix) {
314 $logger->info("Missing cf_host_key or $cf_host_prefix! Define these in $cf_config_file.");
315 return [];
316 }
317
318 my $domain = ".".$OPTS{"zone_name"}.".";
319
320 ## Unpack the mapping from recs to lines (ugg, this is SOOO BAAD)
321 my $recs2lines = JSON::Syck::Load($OPTS{"cf_recs"});
322
323 ## Set up the zone_set args.
324 my $login_args = {
325 "host" => $cf_host_name,
326 "uri" => $cf_host_uri,
327 "port" => $cf_host_port,
328 "query" => {
329 "act" => "zone_delete",
330 "host_key" => $cf_host_key,
331 "user_key" => $OPTS{"user_key"},
332 "zone_name" => $OPTS{"zone_name"}
333 },
334 };
335
336 $cf_global_data->{"cf_zones"}->{$OPTS{"zone_name"}} = 0;
337
338 my $result = JSON::Syck::Load(__https_post_req->($login_args));
339
340 ## Args for updating local DNS.
341 my %zone_args = ("domain" => $OPTS{"zone_name"},
342 "class" => "IN",
343 "type" => "CNAME",
344 "name" => $cf_host_prefix,
345 "ttl" => 1400,
346 "cname" => $OPTS{"zone_name"},
347 );
348
349 ## If we get an error, do nothing and return the error to the user.
350 if ($result->{"result"} eq "error") {
351 $logger->info("CloudFlare Error: " . $result->{"msg"});
352 } else {
353 ## Otherwise, update the dns for this zone.
354 my $dom = ".".$OPTS{"zone_name"};
355
356 ## Loop over list of subs, removing from CF.
357 my $res;
358 foreach my $linecom (split(/,/, $OPTS{"subdomains"})) {
359 my @line = split(':', $linecom);
360 $zone_args{"line"} = $line[1];
361 $zone_args{"name"} = $line[0];
362 $zone_args{"name"} =~ s/$domain//g;
363 $zone_args{"cname"} = $OPTS{"zone_name"};
364 $res = Cpanel::AdminBin::adminfetchnocache( 'zone', '', 'EDIT', 'storable', $OPTS{"zone_name"},
365 __serialize_request( \%zone_args ) );
366 }
367 }
368
369 ## Save the updated global data arg.
370 Cpanel::DataStore::store_ref($cf_data_file, $cf_global_data);
371
372 return $result;
373}
374
375sub api2_fetchzone {
376 my $raw = __fetchzone(@_);
377 my $results = [];
378 my %OPTS = @_;
379 my $domain = $OPTS{'domain'}.".";
380
381 foreach my $res (@{$raw->{"record"}}) {
382 if ((($res->{"type"} eq "CNAME") || ($res->{"type"} eq "A")) &&
383 ($res->{"name"} !~ /(^direct|^ssh|^ftp|ssl|mx|ns[^.]*|imap[^.]*|pop[^.]*|smtp[^.]*|mail[^.]*|mx[^.]*|exchange[^.]*|smtp[^.]*|google[^.]*|secure|sftp|svn|git|irc|email|mobilemail|pda|webmail|^e\.|video|vid|vids|sites|calendar|svn|cvs|git|cpanel|panel|repo|webstats|local|localhost|$cf_host_prefix)/) &&
384 ($res->{"name"} ne $domain) &&
385 ($res->{"cname"} !~ /google.com/)){
386 if ($res->{"cname"} =~ /cdn.cloudflare.net$/) {
387 $res->{"cloudflare"} = 1;
388 } else {
389 $res->{"cloudflare"} = 0;
390 }
391 push @$results, $res;
392 }
393 }
394 return $results;
395}
396
397sub api2_getbasedomains {
398 my $res = Cpanel::DomainLookup::api2_getbasedomains(@_);
399 my $has_cf = 0;
400 foreach my $dom (@$res) {
401 if ($cf_global_data->{"cf_zones"}->{$dom->{"domain"}}) {
402 $dom->{"cloudflare"} = 1;
403 $has_cf = 1;
404 } else {
405 $dom->{"cloudflare"} = 0;
406 }
407 }
408 return {"has_cf" => $has_cf, "res" => $res};
409}
410
411sub api2 {
412 my $func = shift;
413
414 my %API;
415
416 $API{'user_create'}{'func'} = 'api2_user_create';
417 $API{'user_create'}{'engine'} = 'hasharray';
418 $API{'user_lookup'}{'func'} = 'api2_user_lookup';
419 $API{'user_lookup'}{'engine'} = 'hasharray';
420 $API{'zone_set'}{'func'} = 'api2_zone_set';
421 $API{'zone_set'}{'engine'} = 'hasharray';
422 $API{'zone_delete'}{'func'} = 'api2_zone_delete';
423 $API{'zone_delete'}{'engine'} = 'hasharray';
424 $API{'fetchzone'}{'func'} = 'api2_fetchzone';
425 $API{'fetchzone'}{'engine'} = 'hasharray';
426 $API{'getbasedomains'}{'func'} = 'api2_getbasedomains';
427 $API{'getbasedomains'}{'engine'} = 'hasharray';
428 $API{'zone_get_stats'}{'func'} = 'api2_get_stats';
429 $API{'zone_get_stats'}{'engine'} = 'hasharray';
430 $API{'zone_edit_cf_setting'}{'func'} = 'api2_edit_cf_setting';
431 $API{'zone_edit_cf_setting'}{'engine'} = 'hasharray';
432
433 return ( \%{ $API{$func} } );
434}
435
436########## Internal Functions Defined Below #########
437
438sub __fetchzone {
439 my %OPTS = @_;
440 my $domain = $OPTS{'domain'};
441 my $results = Cpanel::AdminBin::adminfetchnocache( 'zone', '', 'FETCH', 'storable', $domain, ($OPTS{'customonly'} ? 1 : 0) );
442
443 if ( ref $results->{'record'} eq 'ARRAY' ) {
444 for(0 .. $#{ $results->{'record'} }) {
445 $results->{'record'}->[$_]->{'record'} = ($results->{'record'}->[$_]->{'address'} || $results->{'record'}->[$_]->{'cname'} || $results->{'record'}->[$_]->{'txtdata'});
446 $results->{'record'}->[$_]->{'line'} = ($results->{'record'}->[$_]->{'Line'});
447 }
448 foreach my $key ( keys %KEYMAP ) {
449 if ( exists $OPTS{$key} && defined $OPTS{$key} ) {
450 my %MULTITYPES = map { $_ => undef } split(/[\|\,]/, $OPTS{$key});
451 @{ $results->{'record'} } = grep { exists $MULTITYPES{$_->{ $KEYMAP{$key} }} } @{ $results->{'record'} };
452 }
453 }
454 }
455
456 return $results;
457}
458
459sub __https_post_req {
460 my ( $args_hr ) = @_;
461 if ($args_hr->{'port'} ne "443") {
462 ## Downgrade to http
463 return __http_post_req($args_hr);
464 } else {
465 my ( $args_hr ) = @_;
466 my ($page, $response, %reply_headers)
467 = post_https($args_hr->{'host'}, $args_hr->{'port'}, $args_hr->{'uri'}, '',
468 make_form(%{$args_hr->{'query'}}));
469 if ($cf_debug_mode) {
470 $logger->info($response);
471 $logger->info($page);
472 }
473 return $page;
474 }
475}
476
477sub __http_post_req {
478 my ( $args_hr ) = @_;
479 my $query = $args_hr->{'query'};
480 if ( ref $args_hr->{'query'} eq 'HASH' ) {
481 $query = '';
482 foreach my $key ( keys %{ $args_hr->{'query'} } ) {
483 if ( ref $args_hr->{'query'}{$key} eq 'ARRAY' ) {
484 for my $val ( @{ $args_hr->{'query'}{$key} } ) {
485 $query .=
486 $query
487 ? "&$key=" . Cpanel::Encoder::URI::uri_encode_str($val)
488 : "$key=" . Cpanel::Encoder::URI::uri_encode_str($val);
489 }
490 }
491 else {
492 $query .=
493 $query
494 ? "&$key=" . Cpanel::Encoder::URI::uri_encode_str( $args_hr->{'query'}{$key} )
495 : "$key=" . Cpanel::Encoder::URI::uri_encode_str( $args_hr->{'query'}{$key} );
496 }
497 }
498 }
499
500 my $postdata_len = length($query);
501
502 if ($cf_debug_mode) {
503 $logger->info($query);
504 }
505
506 my $proto = getprotobyname('tcp');
507 return unless defined $proto;
508
509 socket( my $socket_fh, &Socket::AF_INET, &Socket::SOCK_STREAM, $proto );
510 return unless $socket_fh;
511
512 my $iaddr = gethostbyname($args_hr->{'host'});
513 my $port = $args_hr->{'port'};
514
515 return unless ( defined $iaddr && defined $port );
516
517 my $sin = Socket::sockaddr_in( $port, $iaddr );
518 return unless defined $sin;
519
520 my $result = "";
521 if ( connect( $socket_fh, $sin ) ) {
522 send $socket_fh, "POST /$args_hr->{'uri'} HTTP/1.0\nContent-Length: $postdata_len\nContent-Type: application/x-www-form-urlencoded\nHost: $args_hr->{'host'}\n\n$query", 0;
523
524 my $in_header = 1;
525 while (<$socket_fh>) {
526 if ( /^\n$/ || /^\r\n$/ || /^$/ ) {
527 $in_header = 0;
528 next;
529 }
530
531 if (!$in_header) {
532 $result .= $_;
533 }
534 }
535 }
536
537 close $socket_fh;
538
539 if ($cf_debug_mode) {
540 $logger->info($result);
541 }
542
543 return $result;
544}
545
546sub __serialize_request {
547 my $opt_ref = shift;
548 my @KEYLIST;
549 foreach my $opt ( keys %$opt_ref ) {
550 push @KEYLIST, Cpanel::Encoder::URI::uri_encode_str($opt) . '=' . Cpanel::Encoder::URI::uri_encode_str( $opt_ref->{$opt} );
551 }
552 return join( '&', @KEYLIST );
553}
554
5551; # Ah, perl.