CodeCommitsIssuesPull requestsActionsInsightsSecurity
ba441348a14258e3d8a4d55d082889474c3e25f3

Branches

Tags

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

Clone

HTTPS

Download ZIP

Cpanel/CloudFlare.pm

692lines · modepreview

package Cpanel::CloudFlare;

# cpanel - Cpanel/CloudFlare.pm                   Copyright(c) 2011 CloudFlare, Inc.
#                                                               All rights Reserved.
# copyright@cloudflare.com                                      http://cloudflare.com
# @author ian@cloudflare.com
# This code is subject to the cPanel license. Unauthorized copying is prohibited

use Cpanel::DnsUtils::UsercPanel ();
use Cpanel::AdminBin             ();
use Cpanel::Locale               ();
use Cpanel::Logger               ();
use Cpanel::UrlTools             ();
use Cpanel::SocketIP             ();
use Cpanel::Encoder::URI         ();
use Cpanel::AcctUtils            ();
use Cpanel::DomainLookup         ();
use Cpanel::DataStore            ();

use Socket                       ();
use JSON::Syck                   ();
use Digest::MD5 qw(md5_hex);
use strict;

my $logger = Cpanel::Logger->new();
my $locale;
my $cf_config_file = "/usr/local/cpanel/etc/cloudflare.json";
my $cf_data_file_name = ".cpanel/datastore/cloudflare_data.yaml";
my $cf_old_data_file_name = "/usr/local/cpanel/etc/cloudflare_data.yaml";
my $cf_data_file;
my $cf_host_key;
my $cf_host_name;
my $cf_host_uri;
my $cf_user_name;
my $cf_user_uri;
my $cf_host_port;
my $cf_host_on_cloud_msg;
my $cf_host_prefix;
my $has_ssl;
my $cf_debug_mode;
my $hoster_name;
my $cf_cp_version;
my $cf_global_data = {};
my $DEFAULT_HOSTER_NAME = "your web hosting provider";

my %KEYMAP = ( 'line' => 'Line', 'ttl' => 'ttl', 'name' => 'name', 
               'class' => 'class', 'address' => 'address', 'type' => 'type', 
               'txtdata' => 'txtdata', 'preference' => 'preference', 'exchange' => 'exchange' );

## Initialize vars here.
sub CloudFlare_init { 
    my $data = JSON::Syck::LoadFile($cf_config_file);

    $cf_host_key = $data->{"host_key"};
    $cf_host_name = $data->{"host_name"};
    $cf_host_uri = $data->{"host_uri"};
    $cf_host_port = $data->{"host_port"};
    $cf_host_prefix = $data->{"host_prefix"};
    $cf_debug_mode = $data->{"debug"};
    $cf_user_name = $data->{"user_name"};
    $cf_user_uri = $data->{"user_uri"};
    $cf_cp_version = $data->{"cp_version"};
    $hoster_name = $data->{"host_formal_name"};
    $cf_host_on_cloud_msg = ($data->{"cloudflare_on_message"})? $data->{"cloudflare_on_message"}: "";
    if (!$hoster_name) {
        $hoster_name = $DEFAULT_HOSTER_NAME;
    }

    eval { use Net::SSLeay qw(post_https make_headers make_form); $has_ssl = 1 };
    if ( !$has_ssl ) {
        $logger->warn("Failed to load Net::SSLeay: $@.\nDisabling functionality until fixed.");
    }

    ## Load the api key.    
    if (-x "/usr/local/cpanel/bin/apikeywrap") {
        my $response=`/usr/local/cpanel/bin/apikeywrap $cf_host_key`;
        chomp $response;
        $cf_host_key = $response;
    }
}

# Can only be called with json or xml api because it uses
# a non-standard return
sub api2_user_create {
    my %OPTS = @_;

    if (!$cf_host_key) {
        $logger->info("Missing cf_host_key! Define this in $cf_config_file.");
        return [];
    }

    __load_data_file($OPTS{"homedir"});
    # Use a random string as a password.
    my $password = ($OPTS{"password"})? $OPTS{"password"}: crypt(int(rand(10000000)), time);
    $logger->info("Creating Cloudflare user for " . $OPTS{"email"} . " -- " . $password);
    $cf_global_data->{"cf_user_tokens"}->{$OPTS{"user"}} = md5_hex($OPTS{"user"} . $cf_host_key);
    $logger->info("Making user token: " . $cf_global_data->{"cf_user_tokens"}->{$OPTS{"user"}});
    
    ## Otherwise, try to create this user.
    my $args = {
        "host" => $cf_host_name,
        "uri" => $cf_host_uri,
        "port" => $cf_host_port,
        "query" => {
            "act" => "user_create",
            "host_key" => $cf_host_key,
            "cloudflare_email" => $OPTS{"email"},
            "cloudflare_pass" => $password,
            "unique_id" => $cf_global_data->{"cf_user_tokens"}->{$OPTS{"user"}},
        },
    };

    Cpanel::DataStore::store_ref($cf_data_file, $cf_global_data);
    my $result = __https_post_req->($args);  
    return JSON::Syck::Load($result);
}

# Can only be called with json or xml api because it uses
# a non-standard return
sub api2_user_lookup {
    my %OPTS = @_;

    __load_data_file($OPTS{"homedir"}, $OPTS{"user"});
    if (!$cf_host_key) {
        $logger->info("Missing cf_host_key! Define this in $cf_config_file.");
        return [];
    }

    if ( !$has_ssl ) { 
        $logger->info("No SSL Configured");
        return [{"result"=>"error", 
                 "msg" => "CloudFlare is disabled until Net::SSLeay is installed on this server."}];
    }

    if ($cf_global_data->{"cf_user_tokens"}->{$OPTS{"user"}}) {
        if ($cf_debug_mode) {
            $logger->info("Using user token");
        }
        my $login_args = {
            "host" => $cf_host_name,
            "uri" => $cf_host_uri,
            "port" => $cf_host_port,
            "query" => {
                "act" => "user_lookup",
                "host_key" => $cf_host_key,
                "unique_id" => $cf_global_data->{"cf_user_tokens"}->{$OPTS{"user"}},
            },
        };

        my $result = JSON::Syck::Load(__https_post_req->($login_args));
        $result->{"on_cloud_message"} = $cf_host_on_cloud_msg;
        return ($result);
    } else {
        if ($cf_debug_mode) {
            $logger->info("Using user email");
        }
        my $login_args = {
            "host" => $cf_host_name,
            "uri" => $cf_host_uri,
            "port" => $cf_host_port,
            "query" => {
                "act" => "user_lookup",
                "host_key" => $cf_host_key,
                "cloudflare_email" => $OPTS{"email"},
            },
        };

        my $result = JSON::Syck::Load(__https_post_req->($login_args));
        $result->{"on_cloud_message"} = $cf_host_on_cloud_msg;
        return ($result);
    }
}

## Pulls certain stats for the passed in zone.
sub api2_get_stats {
    my %OPTS = @_;

    if (!$OPTS{"user_api_key"}) {
        $logger->info("Missing user_api_key!");
        return [];
    }

    if ( !$has_ssl ) { 
        $logger->info("No SSL Configured");
        return [{"result"=>"error", 
                 "msg" => "CloudFlare is disabled until Net::SSLeay is installed on this server."}];
    }

    ## Otherwise, pull this users stats.
    my $stats_args = {
        "host" => $cf_user_name,
        "uri" => $cf_user_uri,
        "port" => $cf_host_port,
        "query" => {
            "a" => "stats",
            "z" => $OPTS{"zone_name"},
            "tkn" => $OPTS{"user_api_key"},
            "u" => $OPTS{"user_email"},
            "interval" => 30, # 30 = last 7 days, 20 = last 30 days 40 = last 24 hours
        },
    };

    my $result = __https_post_req->($stats_args);
    return JSON::Syck::Load($result);
}

sub api2_edit_cf_setting {
    my %OPTS = @_;

    if (!$OPTS{"user_api_key"}) {
        $logger->info("Missing user_api_key!");
        return [];
    }

    if ( !$has_ssl ) { 
        $logger->info("No SSL Configured");
        return [{"result"=>"error", 
                 "msg" => "CloudFlare is disabled until Net::SSLeay is installed on this server."}];
    }

    ## Otherwise, pull this users stats.
    my $stats_args = {
        "host" => $cf_user_name,
        "uri" => $cf_user_uri,
        "port" => $cf_host_port,
        "query" => {
            "a" => $OPTS{"a"},
            "z" => $OPTS{"zone_name"},
            "tkn" => $OPTS{"user_api_key"},
            "u" => $OPTS{"user_email"},
            "v" => $OPTS{"v"},
        },
    };

    my $result = __https_post_req->($stats_args);
    return JSON::Syck::Load($result);
}

# Can only be called with json or xml api because it uses
# a non-standard return
sub api2_zone_set {
    my %OPTS = @_;

    if (!$cf_host_key || !$cf_host_prefix) {
        $logger->info("Missing cf_host_key or $cf_host_prefix! Define these in $cf_config_file.");
        return [];
    }

    __load_data_file($OPTS{"homedir"});
    my $domain = ".".$OPTS{"zone_name"}.".";
    my $subs = $OPTS{"subdomains"};
    $subs =~ s/${domain}//g;

    ## Unpack the mapping from recs to lines (ugg, this is SOOO BAAD)
    my $recs2lines = JSON::Syck::Load($OPTS{"cf_recs"});
    
    ## Set up the zone_set args.
    my $login_args = {
        "host" => $cf_host_name,
        "uri" => $cf_host_uri,
        "port" => $cf_host_port,
        "query" => {
            "act" => "zone_set",
            "host_key" => $cf_host_key,
            "user_key" => $OPTS{"user_key"},
            "zone_name" => $OPTS{"zone_name"},
            "resolve_to" => $cf_host_prefix . "." . $OPTS{"zone_name"},
            "subdomains" => $subs
        },
    };

    if (!$subs) {
        $login_args->{"query"}->{"act"} = "zone_delete";
        $cf_global_data->{"cf_zones"}->{$OPTS{"zone_name"}} = 0
    }

    my $result = JSON::Syck::Load(__https_post_req->($login_args));
    
    ## Args for updating local DNS.
    my %zone_args = ("domain" => $OPTS{"zone_name"},
                     "class" => "IN",
                     "type" => "CNAME",
                     "name" => $cf_host_prefix,
                     "ttl" => 1400,
                     "cname" => $OPTS{"zone_name"},
        );

    my $is_cf = 0;

    ## If we get an error, do nothing and return the error to the user.
    if ($result->{"result"} eq "error") {
        $logger->info("CloudFlare Error: " . $result->{"msg"});
    } else {
        ## Otherwise, update the dns for this zone.
        my $dom = ".".$OPTS{"zone_name"};
             
        ## First Make sure that the resolve to is set.
        my $res = Cpanel::AdminBin::adminfetchnocache( 'zone', '', 'ADD', 'storable', $OPTS{"zone_name"},
                                                       __serialize_request( \%zone_args ) ); 
        
        ## If there's a delete, remove this record from being CF.
        if ($OPTS{"old_line"}) {
            $zone_args{"line"} = $OPTS{"old_line"};
            $zone_args{"name"} = $OPTS{"old_rec"};
            $zone_args{"name"} =~ s/$domain//g;
            $zone_args{"cname"} = $OPTS{"zone_name"};
            $res = Cpanel::AdminBin::adminfetchnocache( 'zone', '', 'EDIT', 'storable', $OPTS{"zone_name"},
                                                        __serialize_request( \%zone_args ) );
        }
        
        ## Now, go ahead and update all of the CF enabled recs.
        foreach my $ft (keys %{$result->{"response"}->{"forward_tos"}}) {
            $zone_args{"line"} = $recs2lines->{$ft."."};
            $zone_args{"name"} = $ft;
            $zone_args{"name"} =~ s/$dom//g;
            $zone_args{"cname"} = $result->{"response"}->{"forward_tos"}->{$ft};

            $res = Cpanel::AdminBin::adminfetchnocache( 'zone', '', 'EDIT', 'storable', $OPTS{"zone_name"},
                                                        __serialize_request( \%zone_args ) );
            if (!$res->{"status"}) {
                ## Try again, bumping up the line by 1. -- no idea why this works.
                $zone_args{"line"}++;
                $res = Cpanel::AdminBin::adminfetchnocache( 'zone', '', 'EDIT', 'storable', $OPTS{"zone_name"},
                                                            __serialize_request( \%zone_args ) );
            }

            if (!$res->{"status"}) {
                $logger->info("Failed to set DNS for CloudFlare record $ft!");
                $logger->info(JSON::Syck::Dump($res));
                $result->{"result"} = "error";
                $result->{"msg"} = $res->{"statusmsg"};
            }

            ## Note that if at least one rec is on, this zone is on CF.
            $cf_global_data->{"cf_zones"}->{$OPTS{"zone_name"}} = 1;
            $is_cf = 1;   
        }
    }

    if (!$is_cf) {
       $cf_global_data->{"cf_zones"}->{$OPTS{"zone_name"}} = 0;
    }

    ## Save the updated global data arg.
    Cpanel::DataStore::store_ref($cf_data_file, $cf_global_data);

    return $result;
}

sub api2_zone_delete {
    my %OPTS = @_;

    if (!$cf_host_key || !$cf_host_prefix) {
        $logger->info("Missing cf_host_key or $cf_host_prefix! Define these in $cf_config_file.");
        return [];
    }

    __load_data_file($OPTS{"homedir"});
    my $domain = ".".$OPTS{"zone_name"}.".";

    ## Unpack the mapping from recs to lines (ugg, this is SOOO BAAD)
    my $recs2lines = JSON::Syck::Load($OPTS{"cf_recs"});
    
    ## Set up the zone_set args.
    my $login_args = {
        "host" => $cf_host_name,
        "uri" => $cf_host_uri,
        "port" => $cf_host_port,
        "query" => {
            "act" => "zone_delete",
            "host_key" => $cf_host_key,
            "user_key" => $OPTS{"user_key"},
            "zone_name" => $OPTS{"zone_name"}
        },
    };

    $cf_global_data->{"cf_zones"}->{$OPTS{"zone_name"}} = 0;

    my $result = JSON::Syck::Load(__https_post_req->($login_args));
    
    ## Args for updating local DNS.
    my %zone_args = ("domain" => $OPTS{"zone_name"},
                     "class" => "IN",
                     "type" => "CNAME",
                     "name" => $cf_host_prefix,
                     "ttl" => 1400,
                     "cname" => $OPTS{"zone_name"},
        );

    ## If we get an error, do nothing and return the error to the user.
    if ($result->{"result"} eq "error") {
        $logger->info("CloudFlare Error: " . $result->{"msg"});
    } else {
        ## Otherwise, update the dns for this zone.
        my $dom = ".".$OPTS{"zone_name"};
             
        ## Loop over list of subs, removing from CF.
        my $res;
        foreach my $linecom (split(/,/, $OPTS{"subdomains"})) {
            my @line = split(':', $linecom);
            $zone_args{"line"} = $line[1];
            $zone_args{"name"} = $line[0];
            $zone_args{"name"} =~ s/$domain//g;
            $zone_args{"cname"} = $OPTS{"zone_name"};
            $res = Cpanel::AdminBin::adminfetchnocache( 'zone', '', 'EDIT', 'storable', $OPTS{"zone_name"},
                                                        __serialize_request( \%zone_args ) );
        }
    }

    ## Save the updated global data arg.
    Cpanel::DataStore::store_ref($cf_data_file, $cf_global_data);

    return $result;
}

sub api2_fetchzone {
    my $raw = __fetchzone(@_);
    my $results = [];
    my %OPTS    = @_;
    my $domain = $OPTS{'domain'}.".";

    foreach my $res (@{$raw->{"record"}}) {
        if ((($res->{"type"} eq "CNAME") || ($res->{"type"} eq "A")) &&
            ($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)/) &&
            ($res->{"name"} ne $domain) &&
            ($res->{"cname"} !~ /google.com/)){
            if ($res->{"cname"} =~ /cdn.cloudflare.net$/) {
                $res->{"cloudflare"} = 1;
            } else {
                $res->{"cloudflare"} = 0;
            }
            push @$results, $res; 
        }
    }
    return $results;
}

sub api2_getbasedomains {        
    my %OPTS = @_;
    __load_data_file($OPTS{"homedir"}, $OPTS{"user"});
    my $res = Cpanel::DomainLookup::api2_getbasedomains(@_);
    my $has_cf = 0;
    foreach my $dom (@$res) {
        if ($cf_global_data->{"cf_zones"}->{$dom->{"domain"}}) {
            $dom->{"cloudflare"} = 1;
            $has_cf = 1;
        } else {
            $dom->{"cloudflare"} = 0;
        }
    }
    return {"has_cf" => $has_cf, "res" => $res, "hoster" => $hoster_name};
}

sub api2 {
    my $func = shift;

    my %API;

    $API{'user_create'}{'func'}                        = 'api2_user_create';
    $API{'user_create'}{'engine'}                      = 'hasharray';
    $API{'user_lookup'}{'func'}                        = 'api2_user_lookup';
    $API{'user_lookup'}{'engine'}                      = 'hasharray';
    $API{'zone_set'}{'func'}                           = 'api2_zone_set';
    $API{'zone_set'}{'engine'}                         = 'hasharray';
    $API{'zone_delete'}{'func'}                        = 'api2_zone_delete';
    $API{'zone_delete'}{'engine'}                      = 'hasharray';
    $API{'fetchzone'}{'func'}                          = 'api2_fetchzone';
    $API{'fetchzone'}{'engine'}                        = 'hasharray';
    $API{'getbasedomains'}{'func'}                     = 'api2_getbasedomains';
    $API{'getbasedomains'}{'engine'}                   = 'hasharray';
    $API{'zone_get_stats'}{'func'}                     = 'api2_get_stats';
    $API{'zone_get_stats'}{'engine'}                   = 'hasharray';
    $API{'zone_edit_cf_setting'}{'func'}               = 'api2_edit_cf_setting';
    $API{'zone_edit_cf_setting'}{'engine'}             = 'hasharray';

    return ( \%{ $API{$func} } );
}

## Run the auto-update here
sub check_auto_update {

    print "Checking CloudFlare for latest version\n";
    CloudFlare_init();
    print "Current Version: $cf_cp_version\n";

    my $check_args = {
        "host" => $cf_host_name,
        "uri" => $cf_host_uri,
        "port" => $cf_host_port,
        "query" => {
            "act" => "cpanel_info",
            "host_key" => $cf_host_key,
        },
    };

    my $result = JSON::Syck::Load(__https_post_req->($check_args));
    print "Latest Version: " . $result->{"response"}{"cpanel_latest"} . "\n";
    print "Latest SHA1: " . $result->{"response"}{"cpanel_sha1"} . "\n";

    if ($result->{"response"}{"cpanel_latest"} > $cf_cp_version) {
        print "Downloading the latest version.\n";
        `curl -L https://github.com/cloudflare/CloudFlare-CPanel/tarball/master > /tmp/cloudflare.tar.gz`;
        if (`sha1sum /tmp/cloudflare.tar.gz | grep $result->{"response"}{"cpanel_sha1"}`) {
            print "Valid Checksum\n";
        } else {
            print "Checksum failed, aborting upgrade\n";
            unlink("/tmp/cloudflare.tar.gz");
        }
    } else {
        print "You already have the latest version.\n";
    }
}

## Returns a list of all users and zones on CF.
sub list_active_zones {
    if ($>) {
        die("must be root to run.");
    }

    CloudFlare_init();
    my %zones;
    foreach my $user (`ls /home`) {
        chomp $user;
        if ( -e "/home/$user/$cf_data_file_name") {
            __load_data_file("/home/$user", $user);
            foreach my $zone (keys %{$cf_global_data->{"cf_zones"}}) {
                if ($cf_global_data->{"cf_zones"}{$zone}) {
                    $zones{$zone} = 1;
                }
            }
        }
    }

    print "The following zones are currently active on CloudFlare:\n";
    foreach my $zone (keys %zones) {
        print "$zone\n";
    }
}

########## Internal Functions Defined Below #########

sub __load_data_file {
    my $home_dir = shift;
    my $user = shift;
    $cf_data_file = $home_dir . "/" . $cf_data_file_name;    
    if( Cpanel::DataStore::load_ref($cf_data_file, $cf_global_data ) ) {
        if ($cf_debug_mode) {
            $logger->info("Successfully loaded cf data -- $cf_data_file");
        }
    } else {
        ## Try to load the data from the old default data file (if it exists)
        if (-e $cf_old_data_file_name) {
            $logger->info( "Failed to load cf data -- Trying to copy from $cf_old_data_file_name for $user");
            my $tmp_data = {};
            Cpanel::DataStore::load_ref($cf_old_data_file_name, $tmp_data);
            $cf_global_data->{"cf_user_tokens"}->{$user} = $tmp_data->{"cf_user_tokens"}->{$user};
            $cf_global_data->{"cf_zones"} = $tmp_data->{"cf_zones"};

        } else {
            $cf_global_data = {"cf_zones" => {}};
            $logger->info( "Failed to load cf data -- storing blank data at $cf_data_file");
        }
        Cpanel::DataStore::store_ref($cf_data_file, $cf_global_data);
        chmod 0600, $cf_data_file;
    }
}

sub __fetchzone {
    my %OPTS    = @_;
    my $domain  = $OPTS{'domain'};
    my $results = Cpanel::AdminBin::adminfetchnocache( 'zone', '', 'FETCH', 'storable', $domain, ($OPTS{'customonly'} ? 1 : 0)  );

    if ( ref $results->{'record'} eq 'ARRAY' ) {
        for(0 .. $#{ $results->{'record'} }) {
            $results->{'record'}->[$_]->{'record'} = ($results->{'record'}->[$_]->{'address'} || $results->{'record'}->[$_]->{'cname'} || $results->{'record'}->[$_]->{'txtdata'});
            $results->{'record'}->[$_]->{'line'} = ($results->{'record'}->[$_]->{'Line'});
        }
        foreach my $key ( keys %KEYMAP ) {
            if ( exists $OPTS{$key} && defined $OPTS{$key} ) {
                my %MULTITYPES = map { $_ => undef } split(/[\|\,]/, $OPTS{$key});
                @{ $results->{'record'} } = grep { exists $MULTITYPES{$_->{ $KEYMAP{$key} }} } @{ $results->{'record'} };
            }
        }
    }

    return $results;
}

sub __https_post_req {
    my ( $args_hr ) = @_;
    if ($args_hr->{'port'} ne "443") {
        ## Downgrade to http
        return __http_post_req($args_hr);
    } else {
        my ( $args_hr ) = @_;
        my ($page, $response, %reply_headers)
            = post_https($args_hr->{'host'}, $args_hr->{'port'}, $args_hr->{'uri'}, '', 
                         make_form(%{$args_hr->{'query'}}));
        if ($cf_debug_mode) {
            $logger->info($response);
            $logger->info($page);
        }
        return $page;
    }
}

sub __http_post_req {
    my ( $args_hr ) = @_;
    my $query = $args_hr->{'query'};
    if ( ref $args_hr->{'query'} eq 'HASH' ) {
        $query = '';
        foreach my $key ( keys %{ $args_hr->{'query'} } ) {
            if ( ref $args_hr->{'query'}{$key} eq 'ARRAY' ) {
                for my $val ( @{ $args_hr->{'query'}{$key} } ) {
                    $query .=
                        $query
                        ? "&$key=" . Cpanel::Encoder::URI::uri_encode_str($val)
                        : "$key=" . Cpanel::Encoder::URI::uri_encode_str($val);
                }
            }
            else {
                $query .=
                    $query
                    ? "&$key=" . Cpanel::Encoder::URI::uri_encode_str( $args_hr->{'query'}{$key} )
                    : "$key=" . Cpanel::Encoder::URI::uri_encode_str( $args_hr->{'query'}{$key} );
            }
        }
    }
    
    my $postdata_len = length($query);

    if ($cf_debug_mode) {
        $logger->info($query);
    }    

    my $proto = getprotobyname('tcp');
    return unless defined $proto;

    socket( my $socket_fh, &Socket::AF_INET, &Socket::SOCK_STREAM, $proto );
    return unless $socket_fh;

    my $iaddr = gethostbyname($args_hr->{'host'});
    my $port = $args_hr->{'port'};
    
    return unless ( defined $iaddr && defined $port );

    my $sin = Socket::sockaddr_in( $port, $iaddr );
    return unless defined $sin;
    
    my $result = "";
    if ( connect( $socket_fh, $sin ) ) {
        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;
        
        my $in_header = 1;
        while (<$socket_fh>) {
            if ( /^\n$/ || /^\r\n$/ || /^$/ ) {
                $in_header = 0;
                next;
            }
            
            if (!$in_header) {
                $result .=  $_;
            }
        }
    }

    close $socket_fh;

    if ($cf_debug_mode) {
        $logger->info($result);
    }

    return $result;
}

sub __serialize_request {
    my $opt_ref = shift;
    my @KEYLIST;
    foreach my $opt ( keys %$opt_ref ) {
        push @KEYLIST, Cpanel::Encoder::URI::uri_encode_str($opt) . '=' . Cpanel::Encoder::URI::uri_encode_str( $opt_ref->{$opt} );
    }
    return join( '&', @KEYLIST );
}

## Are we running from the command line?
if ($ARGV[0] eq "check") {
    check_auto_update();
} elsif ($ARGV[0] eq "list") {
    list_active_zones();
}

1; # Ah, perl.