File: //usr/local/cwaf/modules/CPAN/lib/Comodo/CWAF/ClientAPI.pm
package Comodo::CWAF::ClientAPI;
use strict qw(vars refs);
our (%conf);
BEGIN {
our (@ISA, @EXPORT);
use Exporter;
use LWP;
use LWP::Simple;
use JSON;
use Digest::MD5;
use File::Path qw(remove_tree);
use utf8;
# Load CWAF modules
use Comodo::CWAF::Main;# qw(do_log %conf get_backupdir get_workdir is_apache is_litespeed is_nginx is_cpanel is_plesk is_webmin is_directadmin);
use Comodo::CWAF::Platform;
@ISA = qw(Exporter);
@EXPORT = qw(download_rules get_cwafapi_json test_credentials get_http_content get_http_file make_backup_cpanel_plugin make_backup_rules make_restore_rules make_restore_cpanel_plugin check_downloaded_file get_remote_rules_version send_feedback check_rules_version make_dir_clean get_updater_version get_local_rules_version set_workdir make_symlink get_client_version get_web_platform get_available_version is_dir_empty fix_permissions get_useragent);
}
# &check_downloaded_file($path_to_file,$md5sum_of_file)
# check that downloaded file is valid
# RETURN:
# 0 - error
# 1 - all ok
sub check_downloaded_file($;$) {
my ($filename,$md5sum_orig) = @_;
my ($md5sum);
# check file is exists and can be read
if(-e $filename && -r $filename) {
# get file md5 sum
get_md5sum_file($filename,\$md5sum);
do_log("$filename original md5sum - $md5sum_orig",10);
do_log("$filename local md5sum - $md5sum",10);
# check md5 sum of original and downloaded file
if($md5sum eq $md5sum_orig) {
do_log("file successfully saved ($filename)",10);
return 1;
}
else {
do_log("WARN: downloaded file is broken,invalid crc",5);
}
}
else {
do_log("WARN: file doesn't exist or isn't readable ($filename)",5);
}
return 0;
}
# &get_md5sum_file ($filename, \$md5_sum_of_file_scalarref)
# make MD5 sum of file
# RETURN:
# 0 - if got error
# 1 - all ok
sub get_md5sum_file($$) {
my ($filename, $md5sum) = @_;
my ($md5,$md5result);
unless( -r $filename ) {
do_log("can't open file to get md5sum ($filename)",10);
return 0;
}
open(FILE, "<$filename");
# enable binary mode
binmode(FILE);
$md5 = Digest::MD5->new;
$md5result = $md5->addfile(FILE);
# save md5 sum
$$md5sum = $md5result->hexdigest;
}
# &make_restore_cpanel_plugin
# restore cpanel plugin from backup
# RETURN:
# 0 - if got error
# 1 - all ok
sub make_restore_cpanel_plugin {
my ($cgidir,$plugin_cgi,$var);
# build path to cgidir
$cgidir = join('/', $conf{'cwaf_path'}, 'tmp/cwaf');
# build path to addod_cwaf.cgi
$plugin_cgi = join('/', $conf{'cwaf_path'} , 'tmp/addon_cwaf.cgi');
# check system() return code
unless( system("cp -fR $cgidir " . $conf{'cgi_dir'}) ) {
do_log("WARN: can't copy cgidir from backup directory",5);
return 0;
}
return 0 unless ( &make_dir_clean($cgidir,$cgidir) );
# check system() return code
unless( system("cp -f $plugin_cgi " . $conf{'cgi_dir'}) ) {
do_log("WARN: can't copy plugin from backup directory",5);
return 0;
}
return 1;
}
# &make_restore_rules
# restore CWAF rules from backup
# RETURN: no return
sub make_restore_rules {
my $work_dir1 = $conf{'cwaf_path'} . '/tmp/rules/workdir1';
my $work_dir2 = $conf{'cwaf_path'} . '/tmp/rules/workdir2';
my $cur_workdir = get_workdir();
my $new_workdir = &get_backupdir();
# no conf files in dir
return 0 unless (scalar glob("$new_workdir/rules/*.conf"));
make_dir_clean('all',$cur_workdir);
set_workdir($new_workdir);
make_symlink($new_workdir);
return 1;
}
sub make_dir_clean($;$) {
my ($type,$path) = @_;
my ($dirname);
if($type eq "rules") {
$dirname = join('/',$conf{'cwaf_path'},$conf{'rules_dir'});
}
elsif($type eq "backuprules") {
$dirname = join('/',$conf{'cwaf_path'},'tmp',$conf{'rules_dir'});
}
else {
$dirname = $path if($path);
}
do_log("prepare to remove directory $dirname",10);
if(-d $dirname) {
do_log("remove directory $dirname",10);
remove_tree($dirname,{ keep_root => 1, error => \my $err });
if(@$err) {
do_log("can't clean $dirname directory",10);
return 0;
}
}
# if got error while purge directory => exit
return 1;
}
# &get_cwafapi_json($request_hashref,$response_hashref)
# send data to CWAF, check received data and save it
# return:
# 0 - if got error or timeout
# 1 - if all ok
#
sub get_cwafapi_json($$) {
my ($request, $response) = @_;
my ($var, %req, %resp, $json, $result);
# build request
%req = ( 'url' => $conf{'cwaf_url'},
'post' => {
'login' => $conf{'cwaf_login'},
'password' => $conf{'cwaf_passwd'}
}
);
# parse input %request and add it keys & values to %req.
while (my ($k, $v) = each(%$request)){
# build complete request
$req{'post'}{$k} = $v if($v);
}
# try to make connection to http-server
$var = get_http_content(\%req,\%resp);
if($var) {
do_log("parse JSON from CWAF server",10);
$json = new JSON;
# try to convert JSON-data to hash.
eval { $result = $json->allow_nonref->utf8->decode( $resp{'content'} ); };
if($@) {
do_log('WARN: malformed JSON data',9);
return 0;
}
do_log("got answer from CWAF ($result->{'emsg'})",10);
if(defined($result->{'status'})) {
# save result hash-ref
do_log('save response',10);
while (my ($k, $v) = each(%$result)) { $response->{$k} = $v; }
return 1;
}
else {
do_log("got error",10);
}
}
else {
do_log("can't connect to CWAF server",10);
$response->{'status'} = 0;
$response->{'r_code'} = $resp{'r_code'};
$response->{'x-error-reason'} = $resp{'x-error-reason'};
}
return 0;
}
# &test_cwafapi_credentials($login,$passwd)
# send data to CWAF, check received data and save it
# return:
# (0, api_response) - if got error or timeout
# (1, api_response) - if all ok
#
sub test_credentials($$) {
my ($login, $passwd) = @_;
my ($var, %req, %resp);
# build request
%req = (
'act' => 'info',
'url' => $conf{'cwaf_url'},
'post' => {
'login' => $login,
'password' => $passwd,
}
);
# try to make connection to http-server
$var = get_http_content(\%req,\%resp);
if($var) {
return (1, "$resp{'r_code'} $resp{'x-error-reason'}");
} else {
return (0, "$resp{'r_code'} $resp{'x-error-reason'}");
}
}
# &get_http_content($request_hashref,$response_hashref)
# download file to spooldir, return answer to $response_hashref
# return: 0 - if got error or timeout
# 1 - if all ok
sub get_http_content($$) {
my ($req,$resp) = @_;
my ($response,$lwp_timeout,$lwp,$s_flag);
# set defaukt timeout
$lwp_timeout = defined($conf{'http_timeout'}) ? $conf{'http_timeout'} : 60;
# disable hostname check in certificates(HTTPS)
$ENV{'PERL_LWP_SSL_VERIFY_HOSTNAME'} = 0;
# $lwp = LWP::UserAgent->new();
# enable this line for IO::Socket::SSL > 1.950 < 1.988-1
$lwp = LWP::UserAgent->new(ssl_opts => { verify_hostname => 0, SSL_verify_mode => 0 });
$lwp->timeout($lwp_timeout);
$lwp->cookie_jar({
file => $conf{'http_cookie_file'},
autosave => 1
});
# new user agent
#$lwp->agent('Mozilla/5.0 (compatible; ComodoCWAFbot/1.0; +http://www.comodo.com/)');
#$lwp->agent('CWAF_Client/'.get_client_version());
$lwp->agent(&get_useragent());
$lwp->default_header('Accept-Language' => "en-us,en;q=0.5");
$lwp->default_header('Accept' => "text/html;q=0.9,*/*;q=0.8");
$lwp->default_header('Accept-Charset' => "ISO-8859-1,utf-8;q=0.7,*;q=0.7");
# set save to file flag, if not set - try to decode content
$s_flag = grep (':content_file', @{ $req->{'content'} } ) ? 1 : 0;
do_log("lwp_params: timeout=$lwp_timeout sec, save_to_file flag: $s_flag", 10);
# add web paltform flag 0 - apache, 1 - litespeed, 2 - nginx, 4 - modsecurity 3
$req->{'post'}{'source'} = '0' if &is_apache();
$req->{'post'}{'source'} = '1' if &is_litespeed();
$req->{'post'}{'source'} = '2' if &is_nginx();
$req->{'post'}{'source'} = '4' if &is_modsec3(); # is_nginx3()
# make POST request
$response = $lwp->post( $req->{'url'}, $req->{'post'}, @{$req->{'content'}} );
# save return code & content type
$resp->{'r_code'} = $response->code;
$resp->{'content-type'} = $response->content_type;
if($response->header('X-Error-Reason')) {
$resp->{'x-error-reason'} = $response->header('X-Error-Reason');
} else {
$resp->{'x-error-reason'} = $response->message;
}
# save MD5 hash for downloaded file
$resp->{'md5_orig'} = $response->header('X-Arch-MD5') if($response->header('X-Arch-MD5'));
# save filename of download file
if($response->header('Content-Disposition')) {
$resp->{'filename'} = $1 if($response->header('Content-Disposition') =~ /filename=\"([^"]+)\"/);
}
# success 200 OK
if($response->code =~ "200" || $response->code =~ "304") {
# if not set save flag - not try check decoded_content
return 1 if($s_flag);
if( defined($response->decoded_content) ) {
do_log('normalize content', 10);
eval {
$resp->{'content'} = $response->decoded_content(default_charset => 'utf-8');
};
if($@) {
do_log('unknown charset in http response, truncate body', 10);
$resp->{'content'} = '';
}
}
return 1;
}
return 0;
}
# &get_http_file($url,$file)
# download file from $url to $file
# return: HTTP error code (200 is ok)
sub get_http_file($$) {
my ($url,$file) = @_;
my ($response,$lwp_timeout,$lwp);
do_log("Downloading file url=$url, save to file: $file", 10);
# set default timeout
$lwp_timeout = defined($conf{'http_timeout'}) ? $conf{'http_timeout'} : 60;
# disable hostname check in certificates(HTTPS)
$ENV{'PERL_LWP_SSL_VERIFY_HOSTNAME'} = 0;
# $lwp = LWP::UserAgent->new();
# enable this line for IO::Socket::SSL > 1.950 < 1.988-1
$lwp = LWP::UserAgent->new(ssl_opts => { verify_hostname => 0, SSL_verify_mode => 0 });
$lwp->timeout($lwp_timeout);
$lwp->cookie_jar({
file => $conf{'http_cookie_file'},
autosave => 1
});
# new user agent
#$lwp->agent('Mozilla/5.0 (compatible; ComodoCWAFbot/1.0; +http://www.comodo.com/)');
#$lwp->agent('CWAF_Client/'.get_client_version());
$lwp->agent(&get_useragent());
$lwp->default_header('Accept-Language' => "en-us,en;q=0.5");
$lwp->default_header('Accept' => "text/html;q=0.9,*/*;q=0.8");
$lwp->default_header('Accept-Charset' => "ISO-8859-1,utf-8;q=0.7,*;q=0.7");
# make file request
$response = $lwp->get( $url, ':content_file' => $file );
return $response->code;
}
# &get_remote_rules_version()
# download file to spooldir, return answer to $response_hashref
# return:
# 0 - error, if all ok - rules version
sub get_remote_rules_version() {
return Comodo::CWAF::DAdmin::da_get_available_version() if(&is_directadmin());
my (%req,%resp);
do_log("try to get data from CWAF server",10);
# add argument "act" to POST method
$req{'act'} = 'info';
get_cwafapi_json(\%req,\%resp);
# if broken json - exit
return (0, $resp{'x-error-reason'}) unless( $resp{'status'} );
return ($resp{'info'}{'current_version'});
}
# send feedback to CWAF server
# send_feedback($feedback_hashref)
# RETURN:
# 0 - error, 1 - ok
#
sub send_feedback($) {
my ($feedback) = @_;
my (%resp,%action_descr);
do_log("try to send feedback to CWAF server",10);
# add argument "act" to request
$feedback->{'act'} = 'feedback';
# add client version if absent
$feedback->{'client_version'} = &get_client_version() unless $feedback->{'client_version'};
get_cwafapi_json($feedback,\%resp);
# if broken json - exit
return $resp{'status'};
}
# check exists rules version entered in feedback panel
# check_rules_version($version)
# RETURN:
# 0 - error, 1 - ok
#
sub check_rules_version($) {
my ($version) = @_;
my (%req,%resp);
# if version not contains digits -> exit
return 0 unless( $version =~ /^([0-9\.]+)$/ );
%req = ( 'act' => 'info', 'version' => $version );
if( get_cwafapi_json(\%req,\%resp) ) {
return 1 if( int($resp{'info'}{'version'}) == int($version) );
}
return 0;
}
# &get_updater_version()
# return updater version
# RETURN: version or undef(if updater can't run)
sub get_updater_version{
my $var = `$conf{'cwaf_path'}/$conf{'updater_bin'} --version` or undef;
unless( defined($var) ) {
do_log("ERROR: can't get updater version: $!",1);
return undef;
}
chomp($var);
return $var;
}
# get version of client available
# get_available_version
# RETURN:
# 0 - error, if all ok - available client version
sub get_available_version() {
my $version_url = $conf{'script_version'};
my (%req, %resp, $file, $remote_file, $var, $version);
unless(defined($version_url)) {
do_log("ERROR: script_version is not defined in configuration", 0);
return (0, 'internal error');
}
#%req = ( 'url' => $version_url );
$remote_file = $conf{'cwaf_path'}.'/tmp/remote.dat';
if($var = get_http_file($version_url, $remote_file)) {
if(open($file, "< ${remote_file}")) {
while ($version = <$file>) {
chomp($version);
$version =~ s/^\s+|\s+$//g;
if(defined($version) && $version ne "") {
last;
}
}
close($file);
unlink($remote_file);
}
#$version = $resp{'content'};
# check if version file downloaded
if (defined($version)) {
# return 0 if version not contains digits
return (0, $resp{'x-error-reason'}) unless( $version =~ /^([0-9\.]+)$/ );
return $version;
}
}
return (0, $resp{'x-error-reason'});
}
# get version of local client
# get_client_version
# RETURN:
# 0 - error, if all ok - local client version
sub get_client_version() {
my ($file, $retval, $ver_file);
$ver_file = $conf{'cwaf_path'}."/etc/version.dat";
if(open($file, "< ${ver_file}")) {
while ($retval = <$file>) {
chomp($retval);
$retval =~ s/^\s+|\s+$//g;
if(defined($retval) && $retval ne "") {
last;
}
}
close($file);
if (defined($retval)) {
# return 0 if version not contains digits
return 0 unless( $retval =~ /^([0-9\.]+)$/ );
return $retval;
}
}
return 0;
}
# get installed web platform
# get_web_platform
# RETURN:
# installed web platform
sub get_web_platform() {
if(defined($conf{'cwaf_platform'})) {
return 'Apache' if &is_apache();
return 'LiteSpeed' if &is_litespeed();
return 'Nginx' if &is_nginx();
return 'Unknown web platform: '.$conf{'cwaf_platform'};
} else {
return 'Apache';
}
}
# get version of local rules
# get_rules_version
# RETURN:
# 0 - error, if all ok - rules version
sub get_local_rules_version() {
my ($file, $var, $rv_file);
$rv_file = $conf{'cwaf_path'}."/rules/rules.dat";
if(open($file, "< ${rv_file}")) {
$var = <$file>;
chomp($var);
close($file);
return $var;
}
return 0;
}
# &is_dir_empty($checkdir)
# check if $checkdir is empty
#
# RETURN: 0 or 1
sub is_dir_empty($) {
my ($checkdir) = @_;
if(opendir(DIR,$checkdir)){
my @files = grep { !m/\A\.{1,2}\Z/} readdir(DIR);
closedir(DIR);
return @files ? 0 : 1;
} else {
do_log("Can't check directory $checkdir ($!)", 10);
return 0;
}
}
# &set_workdir($path_to_working_directory)
# set current working dir
#
# RETURN: 1 - ok, 0 - error
sub set_workdir {
my ($workdir) = @_;
if( open(FILE,">".$conf{'cwaf_path'} . "/tmp/workdir.dat") ) {
do_log("set work directory ($workdir)",10);
print FILE "$workdir\n";
close(FILE);
return 1;
}
return 0;
}
# &make_symlink($)
# make symlink "working directory" -> "project directory"
#
# RETURN: none
sub make_symlink($) {
my ($work_dir) = @_;
my ($symlink_result);
my ($yaml_symlink, $rules_symlink);
$yaml_symlink = $conf{'cwaf_path'} . '/etc/yml';
$rules_symlink = $conf{'cwaf_path'}.'/'.$conf{'rules_dir'};
# delete symlinks
if( -l $yaml_symlink ) {
unlink($yaml_symlink);
} else {
if( -f $yaml_symlink ) {
rename $yaml_symlink, $yaml_symlink . '.backup';
do_log("Found ancient version of YAML dir. Renaming to $yaml_symlink.backup",10);
}
}
if( -l $rules_symlink ) {
unlink($rules_symlink);
} else {
if( -f $rules_symlink ) {
rename $rules_symlink, $rules_symlink . '.backup';
do_log("Found ancient version of rules dir. Renaming to $rules_symlink.backup",10);
}
}
# try to create symlink to YAML configuration directory
$symlink_result = symlink("$work_dir/yml", $yaml_symlink);
unless($symlink_result) {
do_log("can't create symlink to yml $work_dir/yml($!)",10);
return 0;
}
# try to create symlink to rules directory
$symlink_result = symlink("$work_dir/rules", $rules_symlink);
unless($symlink_result) {
do_log("can't create symlink to rules $work_dir/rules($!)",10);
return 0;
}
return 1;
}
# &fix_permissions()
# fix permissions for all webpanels excluding cPanel
#
# RETURN: none
sub fix_permissions() {
# this is cPanel, exiting
return if(&is_cpanel());
if(&is_plesk()) {
system("chown -R psaadm $conf{'cwaf_path'}");
system("chown -R psaadm $conf{'log_dir'}");
}
if(&is_directadmin()) {
system("chown -R cwaf_plugin:cwaf_plugin $conf{'cwaf_path'}");
system("chown -R cwaf_plugin:cwaf_plugin $conf{'log_dir'}");
}
# satandalone GUI
if(&is_standalone_gui()) {
system("chown -R $conf{'web_user'} $conf{'cwaf_path'}");
system("chown -R $conf{'web_user'} $conf{'log_dir'}");
}
#system("chown -R user $conf{'cwaf_path'}") if(&is_webmin());
&fix_wrapper();
}
sub fix_wrapper() {
return if(&is_cpanel() || &is_webmin());
return unless (-f "$conf{'cwaf_path'}/scripts/suid");
system("chown -R root $conf{'cwaf_path'}/scripts/suid");
system("chmod 4111 $conf{'cwaf_path'}/scripts/suid");
}
### Checker Functions For Hosting Platform/ Web Server ###
# return cpanel hosting platform version string or 0.0
sub get_cpanel_version() {
my $ver = run_shellcmd('/usr/local/cpanel/cpanel -V | sed -r \'s=^([0-9.]+)(.*)=\1=g\'');
$ver =~ /^\d+\.\d+$/ ? return $ver : return '0.0';
}
# return plesk hosting platform version string or 0.0
sub get_plesk_version() {
my $ver = run_shellcmd('/usr/sbin/plesk version | grep "Product version" | grep -oE "[0-9\.]{4}"');
$ver =~ /^\d+\.\d+$/ ? return $ver : return '0.0';
}
# return webmin hosting platform version string or 0.0
sub get_webmin_version() {
my $ver = run_shellcmd('cat /etc/webmin/version');
$ver =~ /^\d+\.\d+$/ ? return $ver : return '0.0';
}
# return directadmin hosting platform version string or 0.0
sub get_directadmin_version() {
my $ver = call_wrapper_out('directadmin_version');
if ($ver =~ m/^v\.(.+)\.\d$/) {
$ver = "$1";
}
$ver =~ /^\d+\.\d+$/ ? return $ver : return '0.0';
}
# return hosting platform version string or Unknown
sub get_wh_panel_version() {
return 'cPanel/'.&get_cpanel_version() if &is_cpanel();
return 'Plesk/'.&get_plesk_version() if &is_plesk();
return 'Webmin/'.&get_webmin_version() if &is_webmin();
return 'DirectAdmin/'.&get_directadmin_version() if &is_directadmin();
return 'Standalone';
}
sub get_apache_version {
my $apachectl_bin = &get_apachectl();
return '0.0.0' unless ($apachectl_bin);
my $ver = run_shellcmd("$apachectl_bin -v 2>&1 | grep '^Server version: Apache' | sed -r 's=^(Server version.+)(/)([0-9.]+)(.*)=\\3=g'");
$ver =~ /^\d+\.\d+\.\d+$/ ? return $ver : return '0.0.0';
}
sub get_litespeed_version {
my $ver = '0.0.0';
my $lsver = call_wrapper_out('litespeed_version');
if ($lsver =~ m/^LiteSpeed\/(.+)/) {
$ver = "$1";
}
$ver =~ /^\d+\.\d+(.*)$/ ? return $ver : return '0.0.0';
}
sub get_nginx_version {
my $nginx_bin = &get_nginx_binary();
return '0.0.0' unless $nginx_bin;
my $ver = run_shellcmd($nginx_bin.' -v 2>&1 | grep -F "nginx version" |'." sed -r -e 's/^nginx version: nginx\\/(.*)/\\1/'");
$ver =~ /^\d+\.\d+\.\d+(.*)$/ ? return $ver : return '0.0.0';
}
# return web server version string or Unknown
sub get_webserver_version() {
return 'Apache/'.&get_apache_version() if &is_apache();
return 'LiteSpeed/'.&get_litespeed_version() if &is_litespeed();
return 'Nginx/'.&get_nginx_version() if &is_nginx();
return 'Unknown';
}
# return User-Agent string
sub get_useragent() {
my $ua = 'CWAF_Client/'.get_client_version().' ('.get_webserver_version().'; '.get_wh_panel_version().') Rules/'.get_local_rules_version();
do_log("User-Agent: $ua", 11);
return $ua;
}
1;