###############################################################################
# Copyright (C) 2006-2025 Jonathan Michaelson
#
# https://github.com/waytotheweb/scripts
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, see <https://www.gnu.org/licenses>.
###############################################################################
# no critic (RequireUseWarnings, ProhibitExplicitReturnUndef, ProhibitMixedBooleanOperators, RequireBriefOpen)
# start main
package ConfigServer::CloudFlare;

use strict;
use lib '/usr/local/csf/lib';
use Carp;
use Fcntl qw(:DEFAULT :flock);
use JSON::Tiny();
use LWP::UserAgent;
use Time::Local();
use ConfigServer::Config;
use ConfigServer::Slurp qw(slurp);
use ConfigServer::Logger qw(logfile);

use Exporter qw(import);
our $VERSION     = 1.00;
our @ISA         = qw(Exporter);
our @EXPORT_OK   = qw();

my $config = ConfigServer::Config->loadconfig();
my %config = $config->config();

my $slurpreg = ConfigServer::Slurp->slurpreg;
my $cleanreg = ConfigServer::Slurp->cleanreg;

my %args;
$args{"content-type"} = "application/json";

if ($config{DEBUG} >= 2) {
	require Data::Dumper;
	import Data::Dumper;
}

if (-e "/usr/local/cpanel/version") {
	require YAML::Tiny;
}

# end main
###############################################################################
# start action
sub action {
	my $action = shift;
	my $ip = shift;
	my $mode = shift;
	my $id = shift;
	my $domainlist = shift;
	my $allowany = shift;

	my $status;
	my $return;

	if ($config{DEBUG} == 1) {logfile("Debug: CloudFlare - [$action] [$ip] [$mode] [$id] [$domainlist] [$allowany]")}
	unless ($config{URLGET}) {
		logfile("CloudFlare: URLGET must be set to 1 to use LWP for this feature");
		return;
	}

	if ($action eq "remove") {
		my @newfile;
		sysopen (my $TEMP, "/var/lib/csf/cloudflare.temp", O_RDWR | O_CREAT);
		flock($TEMP, LOCK_EX);
		my $hit;
		while (my $line = <$TEMP>) {
			chomp $line;
			my ($rip,$mode,$user,$raccount,$rapikey,$rid,$time) = split(/\|/,$line);

			if ($ip eq $rip) {
				$args{"X-Auth-Email"} = $raccount;
				$args{"X-Auth-Key"} = $rapikey;

				$status = &remove($ip,$mode,$rid);
				logfile($status." ($user)");
				$hit = 1;
			} else {
				push @newfile, $line;
			}
		}
		if ($hit) {
			seek ($TEMP, 0, 0);
			truncate ($TEMP, 0);
			foreach my $line (@newfile) {
				print $TEMP $line."\n";
			}
		}
		close ($TEMP);
	} else {
		my %authlist;
		my %domains;
		foreach my $domain (split(/\,/,$domainlist)) {
			$domain =~ s/\s//g;
			if ($domain eq "") {next}
			$domain =~ s/^www\.//;
			$domains{$domain} = 1;
		}

		my $scope = &getscope();

		foreach my $user (keys %{$scope->{user}}) {
			if ($allowany and ($scope->{user}{$user}{domain} eq "any" or $scope->{user}{$user}{any})) {
				$authlist{$scope->{user}{$user}{account}}{apikey} = $scope->{user}{$user}{apikey};
				$authlist{$scope->{user}{$user}{account}}{user} = $user;
			}

			foreach my $domain (keys %domains) {
				if ($scope->{domain}{$domain}{user} eq $user) {
					$authlist{$scope->{domain}{$domain}{account}}{apikey} = $scope->{domain}{$domain}{apikey};
					$authlist{$scope->{domain}{$domain}{account}}{user} = $scope->{domain}{$domain}{user};
				}
				foreach my $userdomain (keys %{$scope->{user}{$user}{domain}}) {
					if ($user eq $domain and $scope->{user}{$user}{domain}{$userdomain} ne "") {
						$authlist{$scope->{user}{$user}{account}}{apikey} = $scope->{user}{$user}{apikey};
						$authlist{$scope->{user}{$user}{account}}{user} = $user;
					}
				}
			}
		}

		my @list;
		foreach my $account (sort keys %authlist) {
			$args{"X-Auth-Email"} = $account;
			$args{"X-Auth-Key"} = $authlist{$account}{apikey};
			my $user = $authlist{$account}{user};

			if ($action eq "deny") {
				my ($id,$status) = &block($ip);
				logfile($status." ($user)");
				sysopen (my $TEMP, "/var/lib/csf/cloudflare.temp", O_WRONLY | O_APPEND | O_CREAT);
				flock($TEMP, LOCK_EX);
				print $TEMP "$ip|$mode|$user|$account|$authlist{$account}{apikey}|$id|".time."\n";
				close ($TEMP);
			}
			elsif ($action eq "allow") {
				my ($id,$status) = &whitelist($ip);
				logfile($status." ($user)");
				sysopen (my $TEMP, "/var/lib/csf/cloudflare.temp", O_WRONLY | O_APPEND | O_CREAT);
				flock($TEMP, LOCK_EX);
				print $TEMP "$ip|$mode|$user|$account|$authlist{$account}{apikey}|$id|".time."\n";
				close ($TEMP);
			}
			elsif ($action eq "del") {
				my $status = &remove($ip,$mode);
				print "csf - $status ($user)\n";
			}
			elsif ($action eq "add") {
				my $id;
				my $status;
				if ($mode eq "block") {($id,$status) = &block($ip)}
				if ($mode eq "challenge") {($id,$status) = &challenge($ip)}
				if ($mode eq "whitelist") {($id,$status) = &whitelist($ip)}
				print "csf - $status ($user)\n";
			}
			elsif ($action eq "getlist") {
				push @list, &getlist($user);
			}
		}
		if ($action eq "getlist") {return @list}
	}

	return;
}
# end action
###############################################################################
# start block
sub block {
	my $ip = shift;
	my $target = &checktarget($ip);

	my $block->{mode} = $config{CF_BLOCK};
	$block->{configuration}->{target} = $target;
	$block->{configuration}->{value} = $ip;
	$block->{notes} = "csf $config{CF_BLOCK}";

	my $content;
	eval {
		local $SIG{__DIE__} = undef;
		$content = JSON::Tiny::encode_json($block);
	};

	my $ua = LWP::UserAgent->new;
	my $res = $ua->post('https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules', %args, Content => $content);

	if ($res->is_success) {
		my $id = JSON::Tiny::decode_json($res->content);
		return $id->{result}->{id},"CloudFlare: $config{CF_BLOCK} $target $ip";
	} else {
		if ($config{DEBUG} == 1) {print "Debug: ".$res->content."\n"}
		elsif ($config{DEBUG} >= 2) {
			eval {
				local $SIG{__DIE__} = undef;
				print Dumper(JSON::Tiny::decode_json($res->content));
			};
		}
		return "CloudFlare: [$ip] $config{CF_BLOCK} failed: ".$res->status_line;
	}
}
# end block
###############################################################################
# start whitelist
sub whitelist {
	my $ip = shift;
	my $target = &checktarget($ip);

	my $whitelist->{mode} = "whitelist";
	$whitelist->{configuration}->{target} = $target;
	$whitelist->{configuration}->{value} = $ip;
	$whitelist->{notes} = "csf whitelist";

	my $content;
	eval {
		local $SIG{__DIE__} = undef;
		$content = JSON::Tiny::encode_json($whitelist);
	};

	my $ua = LWP::UserAgent->new;
	my $res = $ua->post('https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules', %args, Content => $content);

	if ($res->is_success) {
		my $id = JSON::Tiny::decode_json($res->content);
		return $id->{result}->{id}, "CloudFlare: whitelisted $target $ip";
	} else {
		if ($config{DEBUG} == 1) {print "Debug: ".$res->content."\n"}
		elsif ($config{DEBUG} >= 2) {
			eval {
				local $SIG{__DIE__} = undef;
				print Dumper(JSON::Tiny::decode_json($res->content));
			};
		}
		return "CloudFlare: [$ip] whitelist failed: ".$res->status_line;
	}
}
# end whitelist
###############################################################################
# start challenge
sub challenge {
	my $ip = shift;
	my $target = &checktarget($ip);

	my $challenge->{mode} = "challenge";
	$challenge->{configuration}->{target} = $target;
	$challenge->{configuration}->{value} = $ip;
	$challenge->{notes} = "csf challenge";

	my $content;
	eval {
		local $SIG{__DIE__} = undef;
		$content = JSON::Tiny::encode_json($challenge);
	};

	my $ua = LWP::UserAgent->new;
	my $res = $ua->post('https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules', %args, Content => $content);

	if ($res->is_success) {
		my $id = JSON::Tiny::decode_json($res->content);
		return $id->{result}->{id}, "CloudFlare: challenged $target $ip";
	} else {
		if ($config{DEBUG} == 1) {print "Debug: ".$res->content."\n"}
		elsif ($config{DEBUG} >= 2) {
			eval {
				local $SIG{__DIE__} = undef;
				print Dumper(JSON::Tiny::decode_json($res->content));
			};
		}
		return "CloudFlare: [$ip] challenge failed: ".$res->status_line;
	}
}
# end challenge
###############################################################################
# start add
sub add {
	my $ip = shift;
	my $mode = shift;
	my $target = &checktarget($ip);

	my $add->{mode} = $mode;
	$add->{configuration}->{target} = $target;
	$add->{configuration}->{value} = $ip;

	my $content;
	eval {
		local $SIG{__DIE__} = undef;
		$content = JSON::Tiny::encode_json($add);
	};

	my $ua = LWP::UserAgent->new;
	my $res = $ua->post('https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules', %args, Content => $content);

	if ($res->is_success) {
		my $id = JSON::Tiny::decode_json($res->content);
		return $id->{result}->{id}, "CloudFlare: $mode added $target $ip";
	} else {
		if ($config{DEBUG} == 1) {print "Debug: ".$res->content."\n"}
		elsif ($config{DEBUG} >= 2) {
			eval {
				local $SIG{__DIE__} = undef;
				print Dumper(JSON::Tiny::decode_json($res->content));
			};
		}
		return "CloudFlare: [$ip] $mode failed: ".$res->status_line;
	}
}
# end whitelist
###############################################################################
# start remove
sub remove {
	my $ip = shift;
	my $mode = shift;
	my $id = shift;
	my $target = &checktarget($ip);

	if ($id eq "") {
		$id = getid($ip,$mode);
		if ($id =~ /CloudFlare:/) {return $id}
		if ($id eq "") {return "CloudFlare: [$ip] remove failed: id not found"}
	}

	my $ua = LWP::UserAgent->new;
	my $res = $ua->delete('https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules/'.$id, %args);

	if ($res->is_success) {
		return "CloudFlare: removed $target $ip";
	} else {
		if ($config{DEBUG} == 1) {print "Debug: ".$res->content."\n"}
		elsif ($config{DEBUG} >= 2) {
			eval {
				local $SIG{__DIE__} = undef;
				print Dumper(JSON::Tiny::decode_json($res->content));
			};
		}
		return "CloudFlare: [$ip] [$id] remove failed: ".$res->status_line;
	}
}
# end remove
###############################################################################
# start getid
sub getid {
	my $ip = shift;
	my $mode = shift;
	my $target = &checktarget($ip);

	my $ua = LWP::UserAgent->new;
	my $res = $ua->get('https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules?page=1&per_page=100&configuration.target='.$target.'&configuration.value='.$ip.'&match=all&order=mode&direction=desc', %args);

	if ($res->is_success) {
		my $result = JSON::Tiny::decode_json($res->content);
		my $entry = @{$result->{result}}[0];
		return $entry->{id};
	} else {
		if ($config{DEBUG} == 1) {print "Debug: ".$res->content."\n"}
		elsif ($config{DEBUG} >= 2) {
			eval {
				local $SIG{__DIE__} = undef;
				print Dumper(JSON::Tiny::decode_json($res->content));
			};
		}
		return "CloudFlare: [$ip] id [$mode] failed: ".$res->status_line;
	}
}
# end getid
###############################################################################
# start getlist
sub getlist {
	my $domain = shift;

	my %ips;
	my $page = 1;
	my $pages = 1;
	my $result;

	my $ua = LWP::UserAgent->new;

	while (1) {
		my $res = $ua->get('https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules?page='.$page.'&per_page=100&order=created_on&direction=asc&match=all', %args);
		if ($res->is_success) {
			my $result = JSON::Tiny::decode_json($res->content);

			$pages = $result->{result_info}->{total_pages};
			foreach my $entry (@{$result->{result}}) {
				if ($entry->{configuration}->{target} eq "ip" or $entry->{configuration}->{target} eq "country" or $entry->{configuration}->{target} eq "ip_range") {
					my ($date, $time) = split /T/ => $entry->{created_on};
					my ($year, $mon, $mday) = split /-/ => $date;
					$year -= 1900;
					$mon -= 1;
					my ($hour, $min, $sec) = split /:/ => $time;
					my $timelocal = Time::Local::timelocal($sec, $min, $hour, $mday, $mon, $year);

					$ips{$entry->{configuration}->{value}}{notes} = $entry->{notes};
					$ips{$entry->{configuration}->{value}}{mode} = $entry->{mode};
					$ips{$entry->{configuration}->{value}}{created_on} = $timelocal;
					$ips{$entry->{configuration}->{value}}{domain} = $domain;
					$ips{$entry->{configuration}->{value}}{success} = 1;
				}
			}
		} else {
			if ($config{DEBUG} >= 2) {
				eval {
					local $SIG{__DIE__} = undef;
					print Dumper(JSON::Tiny::decode_json($res->content));
				};
			}
			$ips{$domain}{success} = 0;
			$ips{$domain}{domain} = "CloudFlare: list failed for ($domain): ".$res->status_line;
			return \%ips;
		}
		$page++;
		if ($pages < $page) {last}
	}
	return \%ips;
}
# end getlist
###############################################################################
# start getscope
sub getscope {
	my %scope;
	my %disabled;
	my %any;
	my @entries = slurp("/etc/csf/csf.cloudflare");
	foreach my $line (@entries) {
		if ($line =~ /^Include\s*(.*)$/) {
			my @incfile = slurp($1);
			push @entries,@incfile;
		}
	}
	foreach my $line (@entries) {
		$line =~ s/$cleanreg//g;
		if ($line eq "") {next}
		if ($line =~ /^\s*\#|Include/) {next}

		my @setting = split(/\:/,$line);

		if ($setting[0] eq "DOMAIN") {
			my $domain = $setting[1];
			my $user = $setting[3];
			my $account = $setting[5];
			my $apikey = $setting[7];

			$scope{domain}{$domain}{account} = $account;
			$scope{domain}{$domain}{apikey} = $apikey;
			$scope{domain}{$domain}{user} = $user;
			$scope{user}{$user}{account} = $account;
			$scope{user}{$user}{apikey} = $apikey;
			$scope{user}{$user}{domain}{$domain} = $domain;
			if ($domain eq "any") {$scope{user}{$user}{any} = 1}
		}
		if ($setting[0] eq "DISABLE") {
			$disabled{$setting[1]} = 1;
		}
		if ($setting[0] eq "ANY") {
			$any{$setting[1]} = 1;
		}
	}
	if ($config{CF_CPANEL}) {
		my %userdomains;
		my %accounts;
		my %creds;

		open (my $IN, "<","/etc/userdomains");
		flock ($IN, LOCK_SH);
		my @localusers = <$IN>;
		close ($IN);
		chomp @localusers;
		foreach my $line (@localusers) {
			my ($domain,$user) = split(/\:\s*/,$line,2);
			$userdomains{$domain} = $user;
			$accounts{$user} = 1;
		}

		foreach my $user (keys %accounts) {
			if ($disabled{$user}) {next}
			my $userhome = (getpwnam($user))[7];

			if (-e "$userhome/.cpanel/datastore/cloudflare_data.yaml") {
				my $yaml = YAML::Tiny->read("$userhome/.cpanel/datastore/cloudflare_data.yaml");
				if ($yaml->[0]->{client_api_key} ne "") {
					$creds{$user}{account} = $yaml->[0]->{cloudflare_email};
					$creds{$user}{apikey} = $yaml->[0]->{client_api_key};
				}
			}
		}

		foreach my $domain (keys %userdomains) {
			my $user = $userdomains{$domain};
			if ($disabled{$user}) {next}
			if ($creds{$user}{apikey} ne "") {
				$scope{domain}{$domain}{account} = $creds{$user}{account};
				$scope{domain}{$domain}{apikey} = $creds{$user}{apikey};
				$scope{domain}{$domain}{user} = $user;
				$scope{user}{$user}{account} = $creds{$user}{account};
				$scope{user}{$user}{apikey} = $creds{$user}{apikey};
				$scope{user}{$user}{domain}{$domain} = $domain;
				if ($any{$user}) {
					$scope{domain}{any}{account} = $creds{$user}{account};
					$scope{domain}{any}{apikey} = $creds{$user}{apikey};
					$scope{domain}{any}{user} = $user;
					$scope{user}{$user}{domain}{any} = "any";
					$scope{user}{$user}{any} = 1;
				}
			}
		}
	}
	return \%scope;
}
# end getscope
###############################################################################
# start checktarget
sub checktarget {
	my $arg = shift;
	if ($arg =~ /^\w\w$/) {return "country"}
	elsif ($arg =~ /\/16$/) {return "ip_range"}
	elsif ($arg =~ /\/24$/) {return "ip_range"}
	else {return "ip"}
}
# end checktarget
###############################################################################
1;
