Last active
October 22, 2015 18:11
-
-
Save darioguarascio/21e7c188b467154e013b to your computer and use it in GitHub Desktop.
Laravel command to add/remove/list a domain IPs part of the CloudFlare round-robin ip Failover system
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
use Illuminate\Console\Command; | |
use Symfony\Component\Console\Input\InputOption; | |
use Symfony\Component\Console\Input\InputArgument; | |
use \Requests; | |
/* | |
|-------------------------------------------------------------------------- | |
| Cloudflare domain IPv4 A-type manager | |
|-------------------------------------------------------------------------- | |
| | |
| This Laravel command performs actions on domains only (ie: example.com | |
| but not sub.example.com) adding, deleting or listing associated ips. | |
| | |
| `php artisan cloudflar:failover example.com list` | |
| | |
| `php artisan cloudflar:failover example.com add 127.0.0.1` | |
| | |
| `php artisan cloudflar:failover example.com delete 99.99.99.99` | |
*/ | |
class CloudflareFailover extends Command { | |
/** | |
* The console command name. | |
* | |
* @var string | |
*/ | |
protected $name = 'cloudflare:failover'; | |
/** | |
* The console command description. | |
* | |
* @var string | |
*/ | |
protected $description = 'Add or delete IP from Cloudflare domain'; | |
/** | |
* Cloudflare API Endpoint | |
* | |
* @return void | |
*/ | |
protected $apiEndpoint = "https://www.cloudflare.com/api_json.html"; | |
public function __construct() | |
{ | |
parent::__construct(); | |
Log::listen(function($level, $message, $context) | |
{ | |
$this->info(sprintf("[%s] %s", $level, $message)); | |
}); | |
} | |
private function _storeSet($key,$value) { | |
Cache::forever('cloudflare.'.$key, $value); | |
} | |
private function _storeGet($key, $default = 0) { | |
return Cache::get('cloudflare.'.$key, $default); | |
} | |
/** | |
* Cloudflare user email as optional argument or read from config file | |
* | |
* @return string | |
*/ | |
private function _cfEmail() { | |
return $this->option('email') ? $this->option('email') : Config::get('cloudflare.email'); | |
} | |
/** | |
* Cloudflare user token as optional argument or read from config file | |
* Token can be found here: https://www.cloudflare.com/my-account | |
* | |
* @return string | |
*/ | |
private function _cfToken() { | |
return $this->option('token') ? $this->option('token') : Config::get('cloudflare.token'); | |
} | |
/** | |
* Fail threshold - Number of checks to tolerate before taking action | |
* | |
* @return string | |
*/ | |
private function _cfFails() { | |
return $this->option('fails') ? $this->option('fails') : Config::get('cloudflare.fail-threshold'); | |
} | |
/** | |
* Add an A record to a domain | |
* | |
* @return array | |
*/ | |
private function _addRecord($host, $ip) { | |
$query = array( | |
"email" => $this->_cfEmail(), | |
"tkn" => $this->_cfToken(), | |
"z" => $host, | |
"a" => "rec_new", | |
"type" => "A", | |
"name" => $host, | |
"content" => $ip, | |
"ttl" => 3600, | |
); | |
try { | |
$req = Requests::get($this->apiEndpoint.'?'.http_build_query($query, '', '&')); | |
$data = json_decode($req->body); | |
return array( | |
$data->result, | |
$data->msg ); | |
} catch (Exception $e) { | |
throw $e; | |
} | |
} | |
/** | |
* Delete an A record from a domain | |
* | |
* @return array | |
*/ | |
private function _delRecord($host, $id) { | |
$query = array( | |
"email" => $this->_cfEmail(), | |
"tkn" => $this->_cfToken(), | |
"z" => $host, | |
"a" => "rec_delete", | |
"id" => $id | |
); | |
try { | |
$req = Requests::get($this->apiEndpoint.'?'.http_build_query($query, '', '&')); | |
$data = json_decode($req->body); | |
return array( | |
$data->result, | |
$data->msg ); | |
} catch (Exception $e) { | |
throw $e; | |
} | |
} | |
/** | |
* Get all the A-type record for a domain | |
* | |
* @return array | |
*/ | |
private function _getRecords($host) { | |
$query = array( | |
"email" => $this->_cfEmail(), | |
"tkn" => $this->_cfToken(), | |
"z" => $host, | |
"a" => "rec_load_all" | |
); | |
try { | |
$result = array(); | |
$req = Requests::get($this->apiEndpoint.'?'.http_build_query($query, '', '&')); | |
$data = json_decode($req->body); | |
foreach ($data->response->recs->objs as $entry) { | |
if ($entry->name == $host && $entry->type == 'A') { | |
$result[ $entry->rec_id ] = $entry->content; | |
} | |
} | |
return $result; | |
} catch (Exception $e) { | |
throw $e; | |
} | |
} | |
protected function defaultCheck($ip) { | |
Log::debug('Running defaultCheck('.$ip.') => TRUE'); | |
return true; | |
} | |
protected function randomCheck($ip) { | |
$result = rand(0,1); | |
Log::debug('Running randomCheck('.$ip.') => ' . $result); | |
return (bool) $result; | |
} | |
protected function falseCheck($ip) { | |
Log::debug('Running randomCheck('.$ip.') => FALSE'); | |
return false; | |
} | |
protected function addToPool($ip) { | |
$target = $this->argument('target'); | |
$result = $this->_addRecord($target, $ip); | |
if ($result[0] == 'success') { | |
Log::debug(sprintf("Ip %s added to %s", $ip, $target)); | |
return true; | |
} else { | |
Log::debug($result[1]); | |
return false; | |
} | |
} | |
protected function deleteFromPool($ip) { | |
$target = $this->argument('target'); | |
$list = $this->_getRecords($target); | |
if (count($list) == 1 && !$this->option('unsafe')) { | |
$this->error(sprintf("%s contains only 1 ip, and it will not be deleted. To force deletion, use --unsafe option.", $target)); | |
} | |
else { | |
foreach ($list as $id => $addr) { | |
if ($addr == $ip) { | |
$result = $this->_delRecord($target, $id); | |
if ($result[0] == 'success') { | |
Log::debug(sprintf("Ip %s deleted from %s", $ip, $target) ); | |
return true; | |
} else { | |
Log::debug($this->error($result[1])); | |
return false; | |
} | |
return; | |
} | |
} | |
Log::debug(sprintf("Ip %s not found in the pool for %s", $ip, $target)); | |
return true; | |
} | |
} | |
private function getFailedChecks($ip){ | |
$k = sprintf("%s.failedChecks.%s", $this->argument('target'), $ip); | |
return $this->_storeGet($k, 0); | |
} | |
private function setFailedChecks($ip, $n){ | |
$k = sprintf("%s.failedChecks.%s", $this->argument('target'), $ip); | |
return $this->_storeSet($k, $n); | |
} | |
private function setRemovedState($ip, $state = 1){ | |
$k = sprintf("%s.removed.%s", $this->argument('target'), $ip); | |
return $this->_storeSet($k, $state); | |
} | |
private function getRemovedState($ip){ | |
$k = sprintf("%s.removed.%s", $this->argument('target'), $ip); | |
return $this->_storeGet($k,0); | |
} | |
/** | |
* Execute the console command. | |
* | |
* @return void | |
*/ | |
public function fire() | |
{ | |
$run = $this->_storeGet('fire'); | |
$this->_storeSet('fire', ++$run); | |
Log::info(sprintf("Run %s times", $run )); | |
$target = $this->argument('target'); | |
switch ($this->argument('action')) { | |
case "delete": | |
$ip = $this->argument('arg_ip'); | |
$this->deleteFromPool($ip); | |
break; | |
case "add": | |
$ip = $this->argument('arg_ip'); | |
break; | |
case "check": | |
$list = $this->option('checkIp'); | |
$check = array($this, $this->option('checkMethod') . 'Check' ); | |
foreach ($list as $addr) { | |
Log::info('Start checking ip: ' . $addr . ' using ' . $this->option('checkMethod') ); | |
$result = call_user_func_array($check, array( $addr )); | |
$failedChecks = $this->getFailedChecks($addr); | |
if (!$result) { | |
$failedChecks++; | |
Log::warning(sprintf("Check for ip %s failed %s times", $addr, $failedChecks)); | |
if ($failedChecks > $this->_cfFails()) { | |
if ($this->getRemovedState($addr)) { | |
Log::critical(sprintf("Ip %s still removed from the pool after %s checks", $addr, $failedChecks)); | |
} else { | |
Log::alert(sprintf("Ip %s fails passed the threshold, removing from CloudFlare pool", $addr, $failedChecks)); | |
$del = $this->deleteFromPool($addr); | |
if ($del === true ){ | |
$this->setRemovedState($addr); | |
} else { | |
Log::error($del); | |
} | |
} | |
} | |
$this->setFailedChecks($addr, $failedChecks); | |
} else { | |
if ($failedChecks > 0) { | |
Log::notice(sprintf("Check for ip %s returned OK after %s checks.", $addr, $failedChecks)); | |
if ($this->getRemovedState($addr)) { | |
Log::alert(sprintf("Adding %s to %s pool...", $addr, $target)); | |
if ($this->addToPool($addr)) { | |
$this->setRemovedState($addr, 0); | |
} | |
} | |
} | |
$this->setFailedChecks($addr, 0); | |
} | |
} | |
break; | |
case "list": | |
default: | |
$list = $this->_getRecords($target); | |
if (count($list) == 0) { | |
$this->info(sprintf("%s has no ips", $target)); | |
} else { | |
foreach ($list as $id => $ip) { | |
$this->info(sprintf("%s has %s", $target, $ip)); | |
} | |
} | |
} | |
} | |
/** | |
* Get the console command arguments. | |
* @return array | |
*/ | |
protected function getArguments() | |
{ | |
return array( | |
array('target', InputArgument::REQUIRED, 'Target domain on CloudFlare'), | |
array('action', InputArgument::OPTIONAL, 'Command to perform (add, delete, list)', 'list'), | |
array('arg_ip', InputArgument::OPTIONAL, 'Ip to add or delete', null), | |
); | |
} | |
/** | |
* Get the console command options. | |
* | |
* @return array | |
*/ | |
protected function getOptions() | |
{ | |
return array( | |
array('unsafe', null, InputOption::VALUE_OPTIONAL, 'On deletions, skip checking if the ip is the only one in the pool', false), | |
array('email', null, InputOption::VALUE_OPTIONAL, 'Cloudflare email address', false), | |
array('token', null, InputOption::VALUE_OPTIONAL, 'Cloudflare auth token', false), | |
array('fails', null, InputOption::VALUE_OPTIONAL, 'Fails threshold', false), | |
array('checkMethod', null, InputOption::VALUE_OPTIONAL, 'IP Check method', 'default'), | |
array('checkIp', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'IP to check', array()), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment