mirror of
https://github.com/waytotheweb/scripts.git
synced 2026-03-29 19:47:06 +00:00
534 lines
16 KiB
Perl
534 lines
16 KiB
Perl
###############################################################################
|
|
# 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;
|