diff --git a/README.md b/README.md index 1851865..2afc677 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,10 @@ Usage: cloudflare check zone - curl - php (php-cli) 5.x -# Code Stewardship -* Originally created by @bAndie91 -* Taken over by @jordanttrizz on 01/26/2022 \ No newline at end of file + +## DONATE + +Support me to improve cloudflare-cli + + + diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..cb174d5 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.2.1 \ No newline at end of file diff --git a/cf-cli b/cf-cli new file mode 120000 index 0000000..99820b4 --- /dev/null +++ b/cf-cli @@ -0,0 +1 @@ +cloudflare.sh \ No newline at end of file diff --git a/cf-domain-search b/cf-domain-search new file mode 120000 index 0000000..a48b65c --- /dev/null +++ b/cf-domain-search @@ -0,0 +1 @@ +cf-domain-search.sh \ No newline at end of file diff --git a/cf-domain-search.sh b/cf-domain-search.sh new file mode 100755 index 0000000..9e0c1b3 --- /dev/null +++ b/cf-domain-search.sh @@ -0,0 +1,163 @@ +#!/bin/bash +# -- A simple script to search for domains in your Cloudflare account +# -- Requires jq (https://stedolan.github.io/jq/) +# -- Usage: ./domain-search.sh +# -- Example: ./domain-search.sh example.com or ./domain-search.sh -f domains.txt + +# -- Variables +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" # -- Get current script directory +CACHE_FILE="$HOME/.cf-domain-search.cache" + +# -- Check if jq is installed +if ! [ -x "$(command -v jq)" ]; then + echo 'Error: jq is not installed.' >&2 + exit 1 +fi + +# -- usage +usage() { + USAGE=\ +"Usage: ./domain-search.sh (-f file.txt|) [-h] [-t] [-d] + + Example: ./domain-search.sh example.com or ./domain-search.sh -f domains.txt + + Options + -h, --help Display this help and exit + -f, --file File containing list of domains to search for + -t, --test Test file containing list of domains to search for + -d, --debug Enable debug mode + -c, --cache Enable cache mode + +" + echo "$USAGE" +} + +# -- Debug +_debug () { + [[ $DEBUG ]] && echo "DEBUG: $1" +} +_success () { echo -e "\033[0;42m${*} \e[0m"; } +_fail () { echo -e "\e[41m\e[97m${*} \e[0m"; } + +# -- All args +ALL_ARGS="${*}" + +# -- Process arguments + POSITIONAL=() + while [[ $# -gt 0 ]] + do + key="$1" + + case $key in + -h|--help) + HELP=YES + shift # past argument + ;; + -t|--test) + TEST="1" + shift # past argument + ;; + -f|--file) + DOMAIN_FILE="$2" + shift # past argument + shift # past value + ;; + -c|--cache) + CACHE=1 + shift # past argument + ;; + -d|--debug) + DEBUG="1" + shift # past argument + ;; + *) # unknown option + POSITIONAL+=("$1") # save it in an array for later + shift # past argument + ;; + esac + done + set -- "${POSITIONAL[@]}" # restore positional parameters + + +# -- All args +_debug "ALL_ARGS: $ALL_ARGS" +_debug "\$1: $1" +_debug "TEST: $TEST" +_debug "DOMAIN_FILE: $DOMAIN_FILE" +_debug "DEBUG: $DEBUG" +[[ -n $1 ]] && DOMAIN="$1" + +# -- Check if argument is passed +if [[ -z "$DOMAIN" && -z $DOMAIN_FILE ]]; then + usage + echo "Error: No domain specified" + exit 1 +elif [[ $HELP ]]; then + usage + exit 1 +fi + + +# -------------------- +# -- Main +# -------------------- + +echo "Getting list of domains using ./cloudflare" +if [[ $TEST ]]; then + CF_DOMAINS=$(cat $SCRIPT_DIR/tests/domains.txt) + _debug "CF_DOMAINS: $CF_DOMAINS" +else + if [[ $CACHE ]]; then + if [[ -f $CACHE_FILE ]]; then + # -- Check if cache file is older than an hour + if [[ $(find $CACHE_FILE -mmin +60) ]]; then + echo "-- Cache file is older than an hour, deleting cache file and pulling in fresh cache" + rm $CACHE_FILE + CF_DOMAINS=$($SCRIPT_DIR/cloudflare list zones | awk '{print $1}') + echo "-- Caching domains to $HOME/.cf-domain-search.cache" + echo "$CF_DOMAINS" > $HOME/.cf-domain-search.cache + else + echo "-- Cache file is not older than an hour, using cache file" + CF_DOMAINS=$(cat $CACHE_FILE) + fi + else + echo "-- Cache file does not exist, pulling in fresh cache" + CF_DOMAINS=$($SCRIPT_DIR/cloudflare list zones | awk '{print $1}') + echo "-- Caching domains to $HOME/.cf-domain-search.cache" + echo "$CF_DOMAINS" > $HOME/.cf-domain-search.cache + fi + else + echo "-- Grabbing domains from Cloudflare API" + CF_DOMAINS=$($SCRIPT_DIR/cloudflare list zones | awk '{print $1}') + fi +fi +echo "" + +if [[ $DOMAIN_FILE ]]; then + if [[ -f $DOMAIN_FILE ]]; then + # -- Search for matching domains from $FILE in Cloudflare account + echo "Search for matching domains from the file $DOMAIN_FILE in Cloudflare account" + for domain in $(cat $DOMAIN_FILE); do + SEARCH=$(grep '^'$domain'$' <<< "${CF_DOMAINS[@]}") + if [[ $? == 0 ]]; then + _success "$domain - FOUND" + else + _fail "$domain - NOTFOUND" + fi + done + else + echo "Error: $DOMAIN_FILE does not exist" + exit 1 + fi +else + # -- Search for matching domain from stdin in Cloudflare account + echo "Search for matching domain $DOMAIN in Cloudflare account" + SEARCH=$(echo "${CF_DOMAINS[@]}" | grep "$DOMAIN") + if [[ $? == 0 ]]; then + _success "$DOMAIN - FOUND" + else + _fail "$DOMAIN - NOTFOUND" + fi +fi + + diff --git a/cloudflare b/cloudflare deleted file mode 100755 index 40c64ec..0000000 --- a/cloudflare +++ /dev/null @@ -1,998 +0,0 @@ -#!/usr/bin/env bash - -debug=0 -details=0 -quiet=0 -NL=$'\n' -TA=$'\t' -APIv4_ENDPOINT=https://api.cloudflare.com/client/v4 -usage_text=\ -"Usage: cloudflare [Options] -Options: - --details, -d Display detailed info where possible - --debug, -D Display API debugging info - --quiet, -q Less verbose - -E - -T -Environment variables: - CF_ACCOUNT - email address (as -E option) - CF_TOKEN - API token (as -T option) -Enter \"cloudflare help\" to list available commands." - - -die() -{ - if [ -n "$1" ] - then - echo "$1" >&2 - fi - exit ${2:-1} -} - -if [ $BASH_VERSINFO -lt 4 ] -then - die "Sorry, you need at least bash 4.0 to run this script." 1 -fi - -is_debug() -{ - [ "$debug" = 1 ] -} -is_quiet() -{ - [ "$quiet" = 1 ] -} - -is_integer() -{ - expr "$1" : '[0-9]\+$' >/dev/null -} - -is_hex() -{ - expr "$1" : '[0-9a-fA-F]\+$' >/dev/null -} - - -call_cf_v4() -{ - # Invocation: call_cf_v4 [PARAMETERS] [-- JSON-DECODER-ARGS] - local method path formtype exitcode querystring page per_page - declare -a curl_opts - curl_opts=() - - method=${1^^} - shift - path=$1 - shift - - if [ "$method" != POST -o "${1:0:1}" = '{' ] - then - curl_opts+=(-H "Content-Type: application/json") - formtype=data - else - formtype=form - fi - if [ "$method" = GET ] - then - curl_opts+=(--get) - fi - - while [ -n "$1" ] - do - if [ ."$1" = .-- ] - then - shift - break - else - curl_opts+=(--$formtype "$1") - fi - shift - done - if [ -z "$1" ] - then - set -- '&?success?"success"?"failed"' - fi - - page=1 - per_page=50 - while true - do - querystring="?page=$page&per_page=$per_page" - if is_debug - then - echo "<<< curl -X $method ${curl_opts[*]} $APIv4_ENDPOINT$path$querystring" >&2 - fi - - output=`curl -sS -H "X-Auth-Email: $CF_ACCOUNT" -H "X-Auth-Key: $CF_TOKEN" \ - -X "$method" "${curl_opts[@]}" \ - "$APIv4_ENDPOINT$path$querystring" | json_decode "$@"` - exitcode=$? - sed -e '/^!/d' <<<"$output" - - if grep -qE '^!has_more' <<<"$output" - then - let page++ - else - break - fi - done - return $exitcode -} - -json_decode() -{ - # Parameter Synatx - # - # .key1.key11.key111 - # dive into array - # %format - # set output formatting - # table - # display as a table - # ,mod1,mod2,... - # see modifiers - # &mod1&mod2&... - # modifiers per line - # - # - # Modifier Synatx - # - # ?modCondition?modTrue?modFalse - # tenary expression - # ||key1||key2||key3||... - # find a true-ish value - # key.subkey.subsubkey - # dive into array - # !key - # implode non-zero elements of key - # !!key1 key2 key3 ... - # implode values of keys if they are not false - # $v) - { - $o[] = (is_int($k) ? "" : "$k=") . repr_array($v, true); - } - if(count($o) > 1 and $brackets) - return "[" . implode(",", $o) . "]"; - else - return implode(",", $o); - } - else - return $a; - } - function pfmt($fmt, &$array, $care_null=1) - { - if(preg_match("/^\?(.*?)\?(.*?)\?(.*)/", $fmt, $grp)) - { - $out = pfmt($grp[1], $array, 0) ? pfmt($grp[2], $array) : pfmt($grp[3], $array); - } - elseif(preg_match("/^!!(.*)/", $fmt, $grp)) - { - $out = implode(",", array_filter(preg_split("/\s+/", $grp[1]), function($k) use($array){ return !!$array[$k]; })); - } - elseif(preg_match("/^!(.*)/", $fmt, $grp)) - { - $out = implode(",", array_keys(array_filter($array[$grp[1]], "notzero"))); - } - elseif(preg_match("/^<(.*)/", $fmt, $grp)) - { - $code = $grp[1]; - extract($array, EXTR_SKIP); - $out = eval("return $code;"); - } - elseif(preg_match("/^\x22(.*?)\x22/", $fmt, $grp)) - { - $out = $grp[1]; - } - elseif(preg_match("/^@(.*?)@(.*)/", $fmt, $grp)) - { - $out = substr($array[$grp[2]], 0, -strlen(".".$array[$grp[1]])); - if($out == "") $out = "@"; - } - elseif(preg_match("/^\|\|/", $fmt)) - { - while(preg_match("/^\|\|(.*?)(\|\|.*|$)/", $fmt, $grp)) - { - if(pfmt($grp[1], $array, 0) != false or !preg_match("/^\|\|/", $grp[2])) - { - $out = pfmt($grp[1], $array, $care_null); - break; - } - $fmt = $grp[2]; - } - } - elseif(preg_match("/(.+?)\.(.+)/", $fmt, $grp)) - { - if(is_array(@$array[$grp[1]])) - $out = pfmt($grp[2], $array[$grp[1]], $care_null); - else - $out = NULL; - } - else - { - /* Fix Cludflare´s DNS notation. - We must use FQDN with the trailing dot if no $ORIGIN declared. */ - if(in_array(@$array["type"], explode(",", "CNAME,MX,NS,SRV")) and isset($array["content"]) and substr($array["content"], -1) != ".") - { - $array["content"] .= "."; - } - if(is_array(@$array[$fmt])) - { - $out = repr_array($array[$fmt]); - } - else - { - $out = $care_null ? (array_key_exists($fmt, $array) ? (isset($array[$fmt]) ? $array[$fmt] : "NULL" ) : "NA") : @$array[$fmt]; - } - } - return $out; - } - - $data0 = json_decode(file_get_contents("php://stdin"), true); - if('$debug') file_put_contents("php://stderr", var_export($data0, 1)); - if(@$data0["result"] == "error") - { - echo $data0["msg"] . "\n"; - exit(2); - } - if(array_key_exists("success", $data0) and !$data0["success"]) - { - function prnt_error($e) - { - printf("E%s: %s\n", $e["code"], $e["message"]); - foreach((array)@$e["error_chain"] as $e) prnt_error($e); - } - foreach($data0["errors"] as $e) prnt_error($e); - exit(2); - } - - if(isset($data0["result_info"]["page"]) and $data0["result_info"]["page"] < $data0["result_info"]["total_pages"]) - { - echo "!has_more\n"; - } - - array_shift($argv); - $data = $data0; - foreach($argv as $param) - { - if($param == "") - { - continue; - } - if(substr($param, 0, 1) == ".") - { - $data = $data0; - foreach(explode(".", $param) as $p) - { - if($p != "") - { - if(array_key_exists($p, $data)) - { - if($p == "objs" and @$data["has_more"]) - { - echo "!has_more\n"; - echo "!count=", $data["count"], "\n"; - } - $data = $data[$p]; - } - else - { - $data = array(); - break; - } - } - } - } - if(substr($param, 0, 1) == "%") - { - $outfmt = substr($param, 1); - } - if($param == "table") - { - ksort($data); - $maxlength = 0; - foreach($data as $key=>$elem) - { - if(strlen($key) > $maxlength) - { - $maxlength = strlen($key); - } - } - foreach($data as $key=>$elem) - { - printf("%-".$maxlength."s\t%s\n", $key, (string)$elem); - } - } - if(substr($param, 0, 1) == ",") - { - foreach($data as $key=>$elem) - { - $out = array(); - foreach(preg_split("/(?&2 - - for zname_zid in `call_cf_v4 GET /zones -- .result %"%s:%s$NL" ,name,id` - do - zone=${zname_zid%%:*} - zone=${zone,,} - zid=${zname_zid##*:} - if [[ "$record_name" =~ ^((.*)\.|)$zone$ ]] - then - subdomain=${BASH_REMATCH[2]} - zone_id=$zid - break - fi - done - [ -z "$zone_id" ] && { is_quiet || echo >&2; return 2; } - is_quiet || echo -n "$zone, searching record ... " >&2 - - rec_found=0 - oldIFS=$IFS - IFS=$NL - for test_record in `call_cf_v4 GET /zones/$zone_id/dns_records -- .result ,name,type,id,ttl,content` - do - IFS=$oldIFS - set -- $test_record - test_record_name=$1 - shift - - if [ "$test_record_name" = "$record_name" ] - then - test_record_type=$1 - shift - test_record_id=$1 - shift - test_record_ttl=$1 - shift - test_record_content=$* - - if [ \( -z "$record_type" -o "$test_record_type" = "$record_type" \) -a \( -z "$record_oldcontent" -o "$test_record_content" = "$record_oldcontent" \) ] - then - let rec_found++ - [ $rec_found -gt 1 ] && { is_quiet || echo >&2; return 4; } - - record_type=$test_record_type - record_id=$test_record_id - record_ttl=$test_record_ttl - record_content=$test_record_content - if [ "$first_match" = 1 ] - then - # accept first matching record - break - fi - fi - fi - IFS=$NL - done - IFS=$oldIFS - - is_quiet || echo "$record_id" >&2 - [ -z "$record_id" ] && return 3 - - return 0 -} - -get_zone_id() -{ - zone_id=`call_cf_v4 GET /zones name="$1" -- .result ,id` - if [ -z "$zone_id" ] - then - die "No such DNS zone found" - fi -} - - - - -while [ -n "$1" ] -do - case "$1" in - -E) shift - CF_ACCOUNT=$1;; - -T) shift - CF_TOKEN=$1;; - -D|--debug) - debug=1;; - -d|--detail|--detailed|--details) - details=1;; - -q|--quiet) - quiet=1;; - -h|--help) - die "$usage_text" 0 - ;; - --) shift - break;; - -*) false;; - *) break;; - esac - shift -done - - -if [ -z "$CF_ACCOUNT" ] -then - die "No \$CF_ACCOUNT set. - -$usage_text" -fi -if [ -z "$CF_TOKEN" ] -then - die "No \$CF_TOKEN set. - -$usage_text" -fi - - -if [ -z "$1" ] -then - die "$usage_text" 1 -fi - - - - -cmd1=$1 -shift - -case "$cmd1" in -list|show) - cmd2=$1 - shift - case "$cmd2" in - zone|zones) - call_cf_v4 GET /zones -- .result %"%s$TA%s$TA#%s$TA%s$TA%s$NL" ,name,status,id,original_name_servers,name_servers - ;; - setting|settings) - [ -z "$1" ] && die "Usage: cloudflare $cmd1 settings " - if is_hex "$1" - then - zone_id=$1 - else - get_zone_id "$1" - fi - if [ "$details" = 1 ] - then - fieldspec=,id,value,'?editable?"Editable"?""','?modified_on?<",, mod: $modified_on"?""' - else - fieldspec=,id,value,\"\",\"\" - fi - call_cf_v4 GET /zones/$zone_id/settings -- .result %"%-30s %s$TA%s%s$NL" "$fieldspec" - ;; - record|records) - [ -z "$1" ] && die "Usage: cloudflare $cmd1 records " - if is_hex "$1" - then - zone_id=$1 - else - get_zone_id "$1" - fi - call_cf_v4 GET /zones/$zone_id/dns_records -- .result %"%-20s %11s %-8s %s %s$TA; %s #%s$NL" \ - ',@zone_name@name,?<$ttl==1?"auto"?ttl,type,||priority||data.priority||"",content,!!proxiable proxied locked,id' - ;; - listing|listings|blocking|blockings) - call_cf_v4 GET /user/firewall/access_rules/rules -- .result %"%s$TA%s$TA%s$TA# %s$NL" ',<$configuration["value"],mode,modified_on,notes' - ;; - *) - die "Parameters: - zones, settings, records, listing" - ;; - esac - ;; - -add) - cmd2=$1 - shift - case "$cmd2" in - record) - [ $# -lt 4 ] && die "Usage: cloudflare add record [ttl] [prio] [service] [protocol] [weight] [port] - domain zone to register the record in, see 'show zones' command - one of: A, AAAA, CNAME, MX, NS, SRV, TXT, SPF, LOC - subdomain name, or \"@\" to refer to the domain's root - IP address for A, AAAA - FQDN for CNAME, MX, NS, SRV - any text for TXT, spf definition text for SPF - coordinates for LOC (see RFC 1876 section 3) - [ttl] Time To Live, 1 = auto - [prio] required only by MX and SRV records, enter \"10\" if unsure - These ones are only for SRV records: - [service] service name, eg. \"sip\" - [protocol] tcp, udp, tls - [weight] relative weight for records with the same priority - [port] layer-4 port number" - - zone=$1 - shift - type=${1^^} - shift - name=$1 - shift - - content=$1 - ttl=$2 - prio=$3 - service=$4 - protocol=$5 - weight=$6 - port=$7 - - [ -n "$ttl" ] || ttl=1 - [ -n "$prio" ] || prio=10 - get_zone_id "$zone" - - - case "$type" in - MX) - call_cf_v4 POST /zones/$zone_id/dns_records "{\"type\":\"$type\",\"name\":\"$name\",\"content\":\"$content\",\"ttl\":$ttl,\"priority\":$prio}" - ;; - LOC) - locdata='' - data_separated=1 - if [ -n "${content//[! ]/}" ] - then - data_separated=0 - set -- $content - fi - for key in lat_degrees lat_minutes lat_seconds lat_direction \ - long_degrees long_minutes long_seconds long_direction \ - altitude size precision_horz precision_vert - do - value=$1 - value=${value%m} - locdata="$locdata${locdata:+,}\"$key\":\"$value\"" - shift - done - if [ $data_separated = 1 ] - then - ttl=${1:-1} - fi - call_cf_v4 POST /zones/$zone_id/dns_records "{\"type\":\"$type\",\"ttl\":$ttl,\"name\":\"$name\",\"data\":{$locdata}}" - ;; - SRV) - [ "${service:0:1}" = _ ] || service="_$service" - [ "${protocol:0:1}" = _ ] || protocol="_$protocol" - [ -n "$weight" ] || weight=1 - target=$content - - call_cf_v4 POST /zones/$zone_id/dns_records "{ - \"type\":\"$type\", - \"ttl\":$ttl, - \"data\":{ - \"service\":\"$service\", - \"proto\":\"$protocol\", - \"name\":\"$name\", - \"priority\":$prio, - \"weight\":$weight, - \"port\":$port, - \"target\":\"$target\" - } - }" - ;; - *) - call_cf_v4 POST /zones/$zone_id/dns_records "{\"type\":\"$type\",\"name\":\"$name\",\"content\":\"$content\",\"ttl\":$ttl}" - ;; - esac - ;; - - whitelist|blacklist|block|challenge) - trg=$1 - trg_type='' - shift - notes=$* - case "$cmd2" in - whitelist) mode=whitelist;; - blacklist|block) mode=block;; - challenge) mode=challenge;; - esac - - if expr "$trg" : '[0-9\.]\+$' >/dev/null - then - trg_type=ip - elif expr "$trg" : '[0-9\.]\+/[0-9]\+$' >/dev/null - then - trg_type=ip_range - elif expr "$trg" : '[A-Z]\+$' >/dev/null - then - trg_type=country - fi - [ -z "$trg" -o -z "$trg_type" ] && die "Usage: cloudflare add [] [] [note]" - - call_cf_v4 POST /user/firewall/access_rules/rules mode=$mode configuration[target]="$trg_type" configuration[value]="$trg" notes="$notes" - ;; - - zone) - if [ $# != 1 ] - then - die "Usage: cloudflare add zone " - fi - call_cf_v4 POST /zones "{\"name\":\"$1\",\"jump_start\":true}" -- .result '&<"status: $status"' - ;; - - *) - die "Parameters: - zone, record, whitelist, blacklist, challenge" - esac - ;; - -delete) - cmd2=$1 - shift - case "$cmd2" in - record) - prm1=$1 - prm2=$2 - shift - shift - - if [ ${#prm2} = 32 ] && is_hex "$prm2" - then - if [ -n "$1" ] - then - die "Unknown parameters: $@" - fi - if is_hex "$prm1" - then - zone_id=$prm1 - else - get_zone_id "$prm1" - fi - record_id=$prm2 - - else - record_type='' - first_match=0 - - [ -z "$prm1" ] && die "Usage: cloudflare delete record [ [ | first] | [|] ]" - - if [ "$prm2" = first ] - then - first_match=1 - else - record_type=${prm2^^} - fi - - findout_record "$prm1" "$record_type" "$first_match" - case $? in - 0) true;; - 2) die "No suitable DNS zone found for \`$prm1'";; - 3) die "DNS record \`$prm1' not found";; - 4) die "Ambiguous record spec: \`$prm1'";; - *) die "Internal error";; - esac - fi - - call_cf_v4 DELETE /zones/$zone_id/dns_records/$record_id - ;; - - listing) - [ -z "$1" ] && die "Usage: cloudflare delete listing [] [first]" - call_cf_v4 GET /user/firewall/access_rules/rules -- .result ,id,configuration.value,notes |\ - while read ruleid trg notes - do - if [ "$ruleid" = "$1" -o "$trg" = "$1" ] || grep -qF "$1" <<<"$notes" - then - call_cf_v4 DELETE /user/firewall/access_rules/rules/$ruleid - if [ "$2" = first ] - then - break - fi - fi - done - ;; - - zone) - if [ $# != 1 ] - then - die "Usage: cloudflare delete zone " - fi - get_zone_id "$1" - call_cf_v4 DELETE /zones/$zone_id - ;; - - *) - die "Parameters: - zone, record, listing" - esac - ;; - -change|set) - cmd2=$1 - shift - case "$cmd2" in - zone) - [ -z "$1" ] && die "Usage: cloudflare $cmd1 zone [ [ ... ]]" - [ -z "$2" ] && die "Settings: - security_level [under_attack | high | medium | low | essentially_off] - cache_level [aggressive | basic | simplified] - rocket_loader [on | off | manual] - minify - development_mode [on | off] - mirage [on | off] - ipv6 [on | off] -Other: see output of 'show zone' command" - get_zone_id "$1" - shift - setting_items='' - - declare -A map - map[sec_lvl]=security_level - map[cache_lvl]=cache_level - map[rocket_ldr]=rocket_loader - map[async]=rocket_loader - map[devmode]=development_mode - map[dev_mode]=development_mode - - while [ -n "$1" ] - do - setting=$1 - shift - [ -n "${map[$setting]}" ] && setting=${map[$setting]} - setting_value=$1 - shift - - case "$setting" in - minify) - css=off - html=off - js=off - for s in ${setting_value//,/ } - do - case "$s" in - css|html|js) eval $s=on;; - *) die "E.g: cloudflare $cmd1 zone minify css,html,js" - esac - done - setting_value="{\"css\":\"$css\",\"html\":\"$html\",\"js\":\"$js\"}" - ;; - esac - - if [ "${setting_value:0:1}" != '{' ] - then - setting_value="\"$setting_value\"" - fi - setting_items="$setting_items${setting_items:+,}{\"id\":\"$setting\",\"value\":$setting_value}" - done - - call_cf_v4 PATCH /zones/$zone_id/settings "{\"items\":[$setting_items]}" - ;; - - record) - str1="Usage: cloudflare $cmd1 record [type | first | oldcontent ] [ [ ... ]] -You must enter \"type\" and the record type (A, MX, ...) when the record name is ambiguous, -or enter \"first\" to modify the first matching record in the zone, -or enter \"oldcontent\" and the exact content of the record you want to modify if there are more records with the same name and type. -Settings: - newname Rename the record - newtype Change type - content See description in 'add record' command - ttl See description in 'add record' command - proxied Turn CF proxying on/off" - [ -z "$1" ] && die "$str1" - record_name=$1 - shift - record_type='' - first_match=0 - record_oldcontent='' - - while [ -n "$1" ] - do - case "$1" in - first) first_match=1;; - type) shift; record_type=${1^^};; - oldcontent) shift; record_oldcontent=$1;; - *) break;; - esac - shift - done - - if [ -z "$1" ] - then - die "$str1" - fi - - findout_record "$record_name" "$record_type" "$first_match" "$record_oldcontent" - e=$? - case $e in - 0) true;; - 2) die "No suitable DNS zone found for \`$record_name'";; - 3) is_quiet && die || die "DNS record \`$record_name' not found";; - 4) die "Ambiguous record name: \`$record_name'";; - *) die "Internal error";; - esac - - record_content_esc=${record_content//\"/\\\"} - old_data="\"name\":\"$record_name\",\"type\":\"$record_type\",\"ttl\":$record_ttl,\"content\":\"$record_content_esc\"" - new_data='' - while [ -n "$1" ] - do - setting=$1 - shift - value=$1 - shift - - [ "$setting" = service_mode ] && setting=proxied - [ "$setting" = newtype -o "$setting" = new_type ] && setting=type - [ "$setting" = newname -o "$setting" = new_name ] && setting=name - [ "$setting" = newcontent ] && setting=content - if [ "$setting" = proxied ] - then - value=${value,,} - [ "$value" = on -o "$value" = 1 ] && value=true - [ "$value" = off -o "$value" = 0 ] && value=false - fi - [ "$setting" = type ] && value=${value^^} - - if [ "$setting" != content ] && ( expr "$value" : '[0-9]\+$' >/dev/null || expr "$value" : '[0-9]\+\.[0-9]\+$' >/dev/null || [ "$value" = true -o "$value" = false ] ) - then - value_escq=$value - else - value_escq=\"${value//\"/\\\"}\" - fi - new_data="$new_data${new_data:+,}\"$setting\":$value_escq" - done - - call_cf_v4 PUT /zones/$zone_id/dns_records/$record_id "{$old_data,$new_data}" - ;; - - *) - die "Parameters: - zone, record" - esac - ;; - -clear) - case "$1" in - cache) - shift - [ -z "$1" ] && die "Usage: cloudflare clear cache " - get_zone_id "$1" - call_cf_v4 DELETE /zones/$zone_id/purge_cache '{"purge_everything":true}' - ;; - *) - die "Parameters: - cache" - ;; - esac - ;; - -check) - case "$1" in - zone) - shift - [ -z "$1" ] && die "Usage: cloudflare check zone " - get_zone_id "$1" - call_cf_v4 PUT /zones/$zone_id/activation_check - ;; - *) - die "Parameters: - zone" - ;; - esac - ;; - -invalidate) - if [ -n "$1" ] - then - urls='' - zone_id='' - for url in "$@" - do - urls="${urls:+$urls,}\"$url\"" - if [ -z "$zone_id" ] - then - if [[ "$url" =~ ^([^:]+:)?/*([^:/]+) ]] - then - re_grps=${#BASH_REMATCH[@]} - domain=${BASH_REMATCH[re_grps-1]} - while true - do - zone_id=`get_zone_id "$domain" 2>/dev/null; echo "$zone_id"` - if [ -n "$zone_id" ] - then - break - fi - parent=${domain#*.} - if [ "$parent" = "$domain" ] - then - break - fi - domain=$parent - done - fi - fi - done - if [ -z "$zone_id" ] - then - die "Zone name could not be figured out." - fi - call_cf_v4 DELETE /zones/$zone_id/purge_cache "{\"files\":[$urls]}" - else - die "Usage: cloudflare invalidate [url-2 [url-3 [...]]]" - fi - ;; - -json) - json_decode "$@" - ;; - -*|help) - die "Commands: - show, add, delete, change, clear, invalidate, check" - ;; -esac - diff --git a/cloudflare b/cloudflare new file mode 120000 index 0000000..99820b4 --- /dev/null +++ b/cloudflare @@ -0,0 +1 @@ +cloudflare.sh \ No newline at end of file diff --git a/cloudflare.sh b/cloudflare.sh new file mode 100755 index 0000000..8ddd49c --- /dev/null +++ b/cloudflare.sh @@ -0,0 +1,1383 @@ +#!/usr/bin/env bash + +# --- +# cloudflare-cli v1.0.1 +# --- + +# -------------------------------------------------- # +# -- Variables +# -------------------------------------------------- # +VERSION=1.1.2-alpha +DEBUG=0 +details=0 +quiet=0 +NL=$'\n' +TA=$'\t' +CF_API_ENDPOINT=https://api.cloudflare.com/client/v4 +APIv4_ENDPOINT=$CF_API_ENDPOINT # Remove eventually + +# -- Colors +RED="\e[31m" +GREEN="\e[32m" +CYAN="\e[36m" +BLUEBG="\e[44m" +YELLOWBG="\e[43m" +GREENBG="\e[42m" +DARKGREYBG="\e[100m" +ECOL="\e[0m" + +# -- HELP_VERSION +HELP_VERSION=\ +"Version: $VERSION +" + +# -- HELP +HELP=\ +" +Help + +Usage: cloudflare [Options] + +${HELP_OPTIONS} + +Commands: + + list - Show information about an object + zone + zones + settings + records + access-lists + + add - Create Object + zone + record + whitelist + blacklist + challenge + + delete - Delete Objects + zone + record + listing + + change - Change Object + zone + record + + clear - Clear cache + everything + invalidate + + help - Full help + +Environment variables: + CF_ACCOUNT - email address (as -E option) + CF_TOKEN - API token (as -T option) + +Configuration file for credentials: + Create a file in \$HOME/.cloudflare with both CF_ACCOUNT and CF_TOKEN defined. + + CF_ACCOUNT=example@example.com + CF_TOKEN= + +${HELP_EXAMPLES} + +${HELP_VERSION} + +Enter \"cloudflare help\" to list available commands." + +# -- HELP_CMDS +HELP_CMDS=\ +"Commands: + list, add, delete, change, clear, invalidate, check + +list zone, zones, settings, records, listing +add zone, record, whitelist, blacklist, challenge +delete zone, record, listing +change zone, record +clear cache" + +# -- HELP_OPTIONS +HELP_OPTIONS=\ +"Options: + + --details, -d Display detailed info where possible + --debug, -D Display API debugging info + --quiet, -q Less verbose + -E + -T " + +# -- USAGE +HELP_USAGE=\ +"Usage: cloudflare [Options] + +${HELP_CMDS} + +${HELP_OPTIONS} +${HELP_EXAMPLES} +${HELP_VERSION} + +Enter \"cloudflare help\" to list available commands." + +# -- EXAMPLES +HELP_EXAMPLES=\ +"Examples: + +$ cloudflare show settings example.net +advanced_ddos off +always_online on +automatic_https_rewrites off +... + +$ cloudflare show records example.net +www auto CNAME example.net. ; proxiable,proxied #IDSTRING +@ auto A 198.51.100.1 ; proxiable,proxied #IDSTRING +* 3600 A 198.51.100.2 ; #IDSTRING +... + +$ cloudflare show records example.net +www auto CNAME example.net. ; proxiable,proxied #IDSTRING +@ auto A 198.51.100.1 ; proxiable,proxied #IDSTRING +* 3600 A 198.51.100.2 ; #IDSTRING +..." + + +HELP_SHOW=\ +"${HELP_CMDS} + +Usage: cloudflare list [zones|zone |settings |records |access-lists ] + + Commands: + zones -List all zones under account. + zone -List basic information for . + settings -List settings for + records -List records for + access-lists -List access lists for + + Options: + domain zone to register the record in, see 'show zones' command + +${HELP_VERSION}" + +HELP_ADD_RECORD=\ +"${HELP_CMDS} + +Usage: cloudflare add record [ttl] [prio | proxied] [service] [protocol] [weight] [port] + domain zone to register the record in, see 'show zones' command + one of: A, AAAA, CNAME, MX, NS, SRV, TXT (Contain in double quotes ""), SPF, LOC + subdomain name, or \"@\" to refer to the domain's root + IP address for A, AAAA + FQDN for CNAME, MX, NS, SRV + any text for TXT, spf definition text for SPF + coordinates for LOC (see RFC 1876 section 3) +Additional Options + [ttl] Time To Live, 1 = auto + MX records: + [prio] required only by MX and SRV records, enter \"10\" if unsure + A or CNAME records: + [proxied] Proxied, true or false. For A or CNAME records only. + SRV records: + [service] service name, eg. \"sip\" + [protocol] tcp, udp, tls + [weight] relative weight for records with the same priority + [port] layer-4 port number + +${HELP_VERSION}" + + +# -------------------------------------------------- # +# -- Functions +# -------------------------------------------------- # + +# -- help +HELP () { +cmd1=$1 +shift + case "$cmd1" in + # -- usage + usage|USAGE) + echo "$HELP_USAGE" + ;; + + # -- help + help|HELP) + echo "$HELP" + ;; + + # -- add + add) + cmd2="$2" + case "$cmd2" in + record) + echo "$HELP_ADD_RECORD" + ;; + esac + ;; + show) + echo "$HELP_SHOW" + ;; +esac +} + +# ---------------------- +# -- Messaging Functions +# ---------------------- +_error () { + echo -e "${RED}** ERROR ** - $1 ${ECOL}" +} + +_success () { + echo -e "${GREEN}** SUCCESS ** - $@ ${ECOL}" +} + +_running () { + echo -e "${BLUEBG}${@}${ECOL}" +} + +_creating () { + echo -e "${DARKGREYBG}${@}${ECOL}" +} + +_separator () { + echo -e "${YELLOWBG}****************${ECOL}" +} + +_debug () { + if [ $DEBUG == "1" ]; then + echo -e "${CYAN}** DEBUG: $@${ECOL}" + fi +} + +# -- die +_die() { + if [ -n "$1" ]; then + _error "$1" + fi + exit ${2:-1} +} + +# -- check_bash - check version of bash +check_bash () { + # - Check bash version and die if not at least 4.0 + if [ $BASH_VERSINFO -lt 4 ]; then + _die "Sorry, you need at least bash 4.0 to run this script." 1 + fi +} + +# -- is_* functions +is_debug() { [ "$DEBUG" = 1 ]; } +is_quiet() { [ "$quiet" = 1 ]; } +is_integer() { expr "$1" : '[0-9]\+$' >/dev/null; } +is_hex() { expr "$1" : '[0-9a-fA-F]\+$' >/dev/null; } + +# -- CURL_CF - New Cloudflare api call +# +# Invocation - CURL_CF [PARAMETERS] [-- JSON_DECODER-ARGS] +# Example - call_cf_v4 GET /zones -- .result %"%s$TA%s$TA#%s$TA%s$TA%s$NL" ,name,status,id,original_name_servers,name_servers +# --------------------------------------------------------------------------- +CURL_CF() { + # Variables + local CURL_METHOD API_PATH FORMTYPE EXITCODE QUERYSTRING PAGE PER_PAGE + declare -a CURL_OPTS + CURL_OPTS=() + + # Params + CURL_METHOD=${1^^} # Set $CURL_METHOD to all uppercase + shift + API_PATH=$1 + shift + + # Set content-type and form type + # -o = OPTION NAME + # ${1:0:1} get first characater of $1 + # if first character of $1 is { then set $FORMTYPE to form + if [ "$CURL_METHOD" != POST -o "${1:0:1}" = '{' ]; then + CURL_OPTS+=(-H "Content-Type: application/json") + FORMTYPE=data + else + FORMTYPE=form + fi + _debug "Formtype: $FORMTYPE" + + # If GET method then set CURL_OPTS --get + if [[ "$CURL_METHOD" = GET ]];then + CURL_OPTS+=(--get) + fi + _debug "CURL_OPTS: $CURL_OPTS" + + # If $1 contains -- do nothing else set curl_opts to (--$FORMTYPE "$1") + while [ -n "$1" ]; do + if [ ."$1" = .-- ]; then + shift + break + else + CURL_OPTS+=(--$FORMTYPE "$1") + fi + shift + done + _debug "CURL_OPTS: $CURL_OPTS" + + # if $1 is zero, ? + if [ -z "$1" ];then + set -- '&?success?"success"?"failed"' + _debug "$1" + fi + + # Cloudflare page and results options + PAGE=1 + PER_PAGE=100 + + while true;do + QUERY_STRING="?page=${PAGE}&per_page=${PER_PAGE}" +# if is_debug; then +# echo "<<< curl -X $CURL_METHOD ${CURL_OPTS[*]} ${CF_API_ENDPOINT}${API_API_PATH}${QUERYSTRING}" >&2 +# fi + +# if [[ $DEBUG == "1" ]];then set -x;fi + CURL_OUTPUT=$(curl -sS -H "X-Auth-Email: ${CF_ACCOUNT}" \ + -H "X-Auth-Key: ${CF_TOKEN}" \ + -X "${CURL_METHOD}" \ + "${CURL_OPTS[@]}" \ + "${CF_API_ENDPOINT}${API_PATH}${QUERYSTRING}" | json_decode "$@") +# if [[ $DEBUG == "1" ]];then set +x;fi + + EXIT_CODE=$? +# _debug "Curl OUTPUT: $CURL_OUTPUT" + _debug "Exit Code: $EXIT_CODE" + + sed -e '/^!/d' <<<"$CURL_OUTPUT" + + if [[ $(grep -qE '^!has_more' <<< "$CURL_OUTPUT") ]]; then + _debug "More pages" + let PAGE++ + else + _debug "No more pages" + break + fi + done + return $EXIT_CODE +} + +# -- call_cf_v4 - Main call to cloudflare using curl +call_cf_v4() { + # Invocation: call_cf_v4 [PARAMETERS] [-- JSON-DECODER-ARGS] + local method path formtype exitcode querystring page per_page + declare -a curl_opts + curl_opts=() + + method=${1^^} + shift + path=$1 + shift + + if [ "$method" != POST -o "${1:0:1}" = '{' ] + then + curl_opts+=(-H "Content-Type: application/json") + formtype=data + else + formtype=form + fi + if [ "$method" = GET ] + then + curl_opts+=(--get) + fi + + while [ -n "$1" ] + do + if [ ."$1" = .-- ] + then + shift + break + else + curl_opts+=(--$formtype "$1") + fi + shift + done + if [ -z "$1" ] + then + set -- '&?success?"success"?"failed"' + fi + + page=1 + per_page=50 + while true + do + querystring="?page=$page&per_page=$per_page" + if is_debug + then + echo "<<< curl -X $method ${curl_opts[*]} $APIv4_ENDPOINT$path$querystring" >&2 + fi + + output=`curl -sS -H "X-Auth-Email: $CF_ACCOUNT" -H "X-Auth-Key: $CF_TOKEN" \ + -X "$method" "${curl_opts[@]}" \ + "$APIv4_ENDPOINT$path$querystring" | json_decode "$@"` + exitcode=$? + sed -e '/^!/d' <<<"$output" + + if grep -qE '^!has_more' <<<"$output" + then + let page++ + else + break + fi + done + return $exitcode +} + +# -- json_decode - php code to decode json +json_decode() +{ + # Parameter Synatx + # + # .key1.key11.key111 + # dive into array + # %format + # set output formatting + # table + # display as a table + # ,mod1,mod2,... + # see modifiers + # &mod1&mod2&... + # modifiers per line + # + # + # Modifier Synatx + # + # ?modCondition?modTrue?modFalse + # tenary expression + # ||key1||key2||key3||... + # find a true-ish value + # key.subkey.subsubkey + # dive into array + # !key + # implode non-zero elements of key + # !!key1 key2 key3 ... + # implode values of keys if they are not false + # $v) + { + $o[] = (is_int($k) ? "" : "$k=") . repr_array($v, true); + } + if(count($o) > 1 and $brackets) + return "[" . implode(",", $o) . "]"; + else + return implode(",", $o); + } + else + return $a; + } + function pfmt($fmt, &$array, $care_null=1) + { + if(preg_match("/^\?(.*?)\?(.*?)\?(.*)/", $fmt, $grp)) + { + $out = pfmt($grp[1], $array, 0) ? pfmt($grp[2], $array) : pfmt($grp[3], $array); + } + elseif(preg_match("/^!!(.*)/", $fmt, $grp)) + { + $out = implode(",", array_filter(preg_split("/\s+/", $grp[1]), function($k) use($array){ return !!$array[$k]; })); + } + elseif(preg_match("/^!(.*)/", $fmt, $grp)) + { + $out = implode(",", array_keys(array_filter($array[$grp[1]], "notzero"))); + } + elseif(preg_match("/^<(.*)/", $fmt, $grp)) + { + $code = $grp[1]; + extract($array, EXTR_SKIP); + $out = eval("return $code;"); + } + elseif(preg_match("/^\x22(.*?)\x22/", $fmt, $grp)) + { + $out = $grp[1]; + } + elseif(preg_match("/^@(.*?)@(.*)/", $fmt, $grp)) + { + $out = substr($array[$grp[2]], 0, -strlen(".".$array[$grp[1]])); + if($out == "") $out = "@"; + } + elseif(preg_match("/^\|\|/", $fmt)) + { + while(preg_match("/^\|\|(.*?)(\|\|.*|$)/", $fmt, $grp)) + { + if(pfmt($grp[1], $array, 0) != false or !preg_match("/^\|\|/", $grp[2])) + { + $out = pfmt($grp[1], $array, $care_null); + break; + } + $fmt = $grp[2]; + } + } + elseif(preg_match("/(.+?)\.(.+)/", $fmt, $grp)) + { + if(is_array(@$array[$grp[1]])) + $out = pfmt($grp[2], $array[$grp[1]], $care_null); + else + $out = NULL; + } + else + { + /* Fix Cludflare´s DNS notation. + We must use FQDN with the trailing dot if no $ORIGIN declared. */ + if(in_array(@$array["type"], explode(",", "CNAME,MX,NS,SRV")) and isset($array["content"]) and substr($array["content"], -1) != ".") + { + $array["content"] .= "."; + } + if(is_array(@$array[$fmt])) + { + $out = repr_array($array[$fmt]); + } + else + { + $out = $care_null ? (array_key_exists($fmt, $array) ? (isset($array[$fmt]) ? $array[$fmt] : "NULL" ) : "NA") : @$array[$fmt]; + } + } + return $out; + } + + $data0 = json_decode(file_get_contents("php://stdin"), true); + if('$DEBUG') file_put_contents("php://stderr", var_export($data0, 1)); + if(@$data0["result"] == "error") + { + echo $data0["msg"] . "\n"; + exit(2); + } + if(array_key_exists("success", $data0) and !$data0["success"]) + { + function prnt_error($e) + { + printf("E%s: %s\n", $e["code"], $e["message"]); + foreach((array)@$e["error_chain"] as $e) prnt_error($e); + } + foreach($data0["errors"] as $e) prnt_error($e); + exit(2); + } + + if(isset($data0["result_info"]["page"]) and $data0["result_info"]["page"] < $data0["result_info"]["total_pages"]) + { + echo "!has_more\n"; + } + + array_shift($argv); + $data = $data0; + foreach($argv as $param) + { + if($param == "") + { + continue; + } + if(substr($param, 0, 1) == ".") + { + $data = $data0; + foreach(explode(".", $param) as $p) + { + if($p != "") + { + if(array_key_exists($p, $data)) + { + if($p == "objs" and @$data["has_more"]) + { + echo "!has_more\n"; + echo "!count=", $data["count"], "\n"; + } + $data = $data[$p]; + } + else + { + $data = array(); + break; + } + } + } + } + if(substr($param, 0, 1) == "%") + { + $outfmt = substr($param, 1); + } + if($param == "table") + { + ksort($data); + $maxlength = 0; + foreach($data as $key=>$elem) + { + if(strlen($key) > $maxlength) + { + $maxlength = strlen($key); + } + } + foreach($data as $key=>$elem) + { + printf("%-".$maxlength."s\t%s\n", $key, (string)$elem); + } + } + if(substr($param, 0, 1) == ",") + { + foreach($data as $key=>$elem) + { + $out = array(); + foreach(preg_split("/(?&2 + + for zname_zid in `call_cf_v4 GET /zones -- .result %"%s:%s$NL" ,name,id` + do + zone=${zname_zid%%:*} + zone=${zone,,} + zid=${zname_zid##*:} + if [[ "$record_name" =~ ^((.*)\.|)$zone$ ]] + then + subdomain=${BASH_REMATCH[2]} + zone_id=$zid + break + fi + done + [ -z "$zone_id" ] && { is_quiet || echo >&2; return 2; } + is_quiet || echo -n "$zone, searching record ... " >&2 + + rec_found=0 + oldIFS=$IFS + IFS=$NL + for test_record in `call_cf_v4 GET /zones/$zone_id/dns_records -- .result ,name,type,id,ttl,content` + do + IFS=$oldIFS + set -- $test_record + test_record_name=$1 + shift + + if [ "$test_record_name" = "$record_name" ] + then + test_record_type=$1 + shift + test_record_id=$1 + shift + test_record_ttl=$1 + shift + test_record_content=$* + + if [ \( -z "$record_type" -o "$test_record_type" = "$record_type" \) -a \( -z "$record_oldcontent" -o "$test_record_content" = "$record_oldcontent" \) ] + then + let rec_found++ + [ $rec_found -gt 1 ] && { is_quiet || echo >&2; return 4; } + + record_type=$test_record_type + record_id=$test_record_id + record_ttl=$test_record_ttl + record_content=$test_record_content + if [ "$first_match" = 1 ] + then + # accept first matching record + break + fi + fi + fi + IFS=$NL + done + IFS=$oldIFS + + is_quiet || echo "$record_id" >&2 + [ -z "$record_id" ] && return 3 + + return 0 +} + +# -- get_zone_id - get Cloudflare zone id +get_zone_id() +{ + zone_id=`call_cf_v4 GET /zones name="$1" -- .result ,id` + if [ -z "$zone_id" ] + then + _error "No such DNS zone found" + _die + fi +} + +# ------------ +# -- Main loop +# ------------ + +# -- Check for options +while [ -n "$1" ] +do + case "$1" in + -E) shift + CF_ACCOUNT=$1;; + -T) shift + CF_TOKEN=$1;; + -D|--debug) + DEBUG=1;; + -d|--detail|--detailed|--details) + details=1;; + -q|--quiet) + quiet=1;; + -h|--help) + _error "$USAGE" 0 + _die + ;; + --) shift + break;; + -*) false;; + *) break;; + esac + shift +done + +# -- Check for .cloudflare credentials + +if [ ! -f "$HOME/.cloudflare" ] + then + echo "No .cloudflare file." + if [ -z "$CF_ACCOUNT" ] + then + _error "No \$CF_ACCOUNT set." + HELP usage + _die + fi + if [ -z "$CF_TOKEN" ] + then + _error "No \$CF_TOKEN set." + HELP usage + _die + fi +else + if is_debug; then echo "Found .cloudflare file."; fi + source $HOME/.cloudflare + if is_debug; then echo "Sourced CF_ACCOUNT: $CF_ACCOUNT CF_TOKEN: $CF_TOKEN"; fi + + if [ -z "$CF_ACCOUNT" ] + then + _error "No \$CF_ACCOUNT set in config." + HELP usage + _die + fi + if [ -z "$CF_TOKEN" ] + then + _error "No \$CF_TOKEN set in config. + + $USAGE" + fi +fi + +# -- Check for command +if [ -z "$1" ]; then + HELP usage + _die "Missing arguments" 1 +fi + + +# -- run commands +CMD1=$1 +shift + +case "$CMD1" in +# -------------------------- +# -- show command @SHOW +# -------------------------- +show|list) + CMD2=$1 + shift + case "$CMD2" in + + # -- zone + zone) + # -- Invocation - CURL_CF [PARAMETERS] [-- JSON_DECODER-ARGS] + #CURL_CF GET /zone -- + echo "test" + ;; + # -- zone + zones) + # -- Max per page=1000 and max results = 2000 + # TODO figure out how to get all zones in one call, or warn there is more than 1000 and add an option for second set of results etc. + #call_cf_v4 GET /zones -- .result %"%s$TA%s$TA#%s$TA%s$TA%s$NL" ,name,status,id,original_name_servers,name_servers + CURL_CF GET '/zones?per_page=1000' -- .result %"%s$TA%s$TA#%s$TA%s$TA%s$NL" ,name,status,id,original_name_servers,name_servers + ;; + + # -- settings + setting|settings) + [ -z "$1" ] && _error "Usage: cloudflare $CMD1 settings " + if is_hex "$1" + then + zone_id=$1 + else + get_zone_id "$1" + fi + if [ "$details" = 1 ] + then + fieldspec=,id,value,'?editable?"Editable"?""','?modified_on?<",, mod: $modified_on"?""' + else + fieldspec=,id,value,\"\",\"\" + fi + call_cf_v4 GET /zones/$zone_id/settings -- .result %"%-30s %s$TA%s%s$NL" "$fieldspec" + ;; + + # -- record + record|records) + [ -z "$1" ] && _error "Usage: cloudflare $CMD1 records " + if is_hex "$1" + then + zone_id=$1 + else + get_zone_id "$1" + fi + call_cf_v4 GET /zones/$zone_id/dns_records -- .result %"%-20s %11s %-8s %s %s$TA; %s #%s$NL" \ + ',@zone_name@name,?<$ttl==1?"auto"?ttl,type,||priority||data.priority||"",content,!!proxiable proxied locked,id' + ;; + + # -- access-rules + access-rules|listing|listings|blocking|blockings) + call_cf_v4 GET /user/firewall/access_rules/rules -- .result %"%s$TA%s$TA%s$TA# %s$NL" ',<$configuration["value"],mode,modified_on,notes' + ;; + + # -- no command catchall + *) + HELP show + if [[ -n $CMD2 ]]; then + _die "Unknown command $CMD2" 1 + else + _die "No command provided" 1 + fi + ;; + esac + ;; + +# ------------------- +# -- add command @ADD +# ------------------- +add) + CMD2=$1 + shift + case "$CMD2" in + record) + [ $# -lt 4 ] && _error "Missing arguments"; HELP add record; + + zone=$1 + shift + type=${1^^} + shift + name=$1 + shift + content=$1 + ttl=$2 + if [[ "$type" == "A" ]] || [[ "$type" == "CNAME" ]]; then + proxied=$3 + elif [[ "$type" == "MX" ]] || [[ "$type" == "SRV" ]]; then + prio=$3 + fi + service=$4 + protocol=$5 + weight=$6 + port=$7 + + [ -n "$proxied" ] || proxied=true + [ -n "$ttl" ] || ttl=1 + [ -n "$prio" ] || prio=10 + if [[ $content =~ ^127.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] && [[ "$type" == "A" ]]; then _error "Can't proxy 127.0.0.0/8 using an A record"; fi + get_zone_id "$zone" + + + case "$type" in + MX) + call_cf_v4 POST /zones/$zone_id/dns_records "{\"type\":\"$type\",\"name\":\"$name\",\"content\":\"$content\",\"ttl\":$ttl,\"priority\":$prio}" + ;; + LOC) + locdata='' + data_separated=1 + if [ -n "${content//[! ]/}" ] + then + data_separated=0 + set -- $content + fi + for key in lat_degrees lat_minutes lat_seconds lat_direction \ + long_degrees long_minutes long_seconds long_direction \ + altitude size precision_horz precision_vert + do + value=$1 + value=${value%m} + locdata="$locdata${locdata:+,}\"$key\":\"$value\"" + shift + done + if [ $data_separated = 1 ] + then + ttl=${1:-1} + fi + call_cf_v4 POST /zones/$zone_id/dns_records "{\"type\":\"$type\",\"ttl\":$ttl,\"name\":\"$name\",\"data\":{$locdata}}" + ;; + SRV) + [ "${service:0:1}" = _ ] || service="_$service" + [ "${protocol:0:1}" = _ ] || protocol="_$protocol" + [ -n "$weight" ] || weight=1 + target=$content + + call_cf_v4 POST /zones/$zone_id/dns_records "{ + \"type\":\"$type\", + \"ttl\":$ttl, + \"data\":{ + \"service\":\"$service\", + \"proto\":\"$protocol\", + \"name\":\"$name\", + \"priority\":$prio, + \"weight\":$weight, + \"port\":$port, + \"target\":\"$target\" + } + }" + ;; + TXT) + call_cf_v4 POST /zones/$zone_id/dns_records "{\"type\":\"$type\",\"name\":\"$name\",\"content\":\"$content\",\"ttl\":$ttl}" + ;; + A) + call_cf_v4 POST /zones/$zone_id/dns_records "{\"type\":\"$type\",\"name\":\"$name\",\"content\":\"$content\",\"ttl\":$ttl,\"proxied\":$proxied}" + ;; + CNAME) + call_cf_v4 POST /zones/$zone_id/dns_records "{\"type\":\"$type\",\"name\":\"$name\",\"content\":\"$content\",\"ttl\":$ttl,\"proxied\":$proxied}" + ;; + *) + call_cf_v4 POST /zones/$zone_id/dns_records "{\"type\":\"$type\",\"name\":\"$name\",\"content\":\"$content\",\"ttl\":$ttl}" + ;; + esac + ;; + + whitelist|blacklist|block|challenge) + trg=$1 + trg_type='' + shift + notes=$* + case "$CMD2" in + whitelist) mode=whitelist;; + blacklist|block) mode=block;; + challenge) mode=challenge;; + esac + + if expr "$trg" : '[0-9\.]\+$' >/dev/null + then + trg_type=ip + elif expr "$trg" : '[0-9\.]\+/[0-9]\+$' >/dev/null + then + trg_type=ip_range + elif expr "$trg" : '[A-Z]\+$' >/dev/null + then + trg_type=country + fi + [ -z "$trg" -o -z "$trg_type" ] && die "Usage: cloudflare add [] [] [note]" + + call_cf_v4 POST /user/firewall/access_rules/rules mode=$mode configuration[target]="$trg_type" configuration[value]="$trg" notes="$notes" + ;; + + zone) + if [ $# != 1 ] + then + die "Usage: cloudflare add zone " + fi + call_cf_v4 POST /zones "{\"name\":\"$1\",\"jump_start\":true}" -- .result '&<"status: $status"' + ;; + + *) + die "Parameters: + zone, record, whitelist, blacklist, challenge" + esac + ;; + +# ----------------- +# -- delete command +# ----------------- +delete) + CMD2=$1 + shift + case "$CMD2" in + record) + prm1=$1 + prm2=$2 + shift + shift + + if [ ${#prm2} = 32 ] && is_hex "$prm2" + then + if [ -n "$1" ] + then + die "Unknown parameters: $@" + fi + if is_hex "$prm1" + then + zone_id=$prm1 + else + get_zone_id "$prm1" + fi + record_id=$prm2 + + else + record_type='' + first_match=0 + + [ -z "$prm1" ] && die "Usage: cloudflare delete record [ [ | first] | [|] ]" + + if [ "$prm2" = first ] + then + first_match=1 + else + record_type=${prm2^^} + fi + + findout_record "$prm1" "$record_type" "$first_match" + case $? in + 0) true;; + 2) die "No suitable DNS zone found for \`$prm1'";; + 3) die "DNS record \`$prm1' not found";; + 4) die "Ambiguous record spec: \`$prm1'";; + *) die "Internal error";; + esac + fi + + call_cf_v4 DELETE /zones/$zone_id/dns_records/$record_id + ;; + + listing) + [ -z "$1" ] && die "Usage: cloudflare delete listing [] [first]" + call_cf_v4 GET /user/firewall/access_rules/rules -- .result ,id,configuration.value,notes |\ + while read ruleid trg notes + do + if [ "$ruleid" = "$1" -o "$trg" = "$1" ] || grep -qF "$1" <<<"$notes" + then + call_cf_v4 DELETE /user/firewall/access_rules/rules/$ruleid + if [ "$2" = first ] + then + break + fi + fi + done + ;; + + zone) + if [ $# != 1 ] + then + die "Usage: cloudflare delete zone " + fi + get_zone_id "$1" + call_cf_v4 DELETE /zones/$zone_id + ;; + + *) + die "Parameters: + zone, record, listing" + esac + ;; + +# --------------------- +# -- change|set command +# --------------------- +change|set) + CMD2=$1 + shift + case "$CMD2" in + zone) + [ -z "$1" ] && die "Usage: cloudflare $CMD1 zone [ [ ... ]]" + [ -z "$2" ] && die "Settings: + security_level [under_attack | high | medium | low | essentially_off] + cache_level [aggressive | basic | simplified] + rocket_loader [on | off | manual] + minify + development_mode [on | off] + mirage [on | off] + ipv6 [on | off] +Other: see output of 'show zone' command" + get_zone_id "$1" + shift + setting_items='' + + declare -A map + map[sec_lvl]=security_level + map[cache_lvl]=cache_level + map[rocket_ldr]=rocket_loader + map[async]=rocket_loader + map[devmode]=development_mode + map[dev_mode]=development_mode + + while [ -n "$1" ] + do + setting=$1 + shift + [ -n "${map[$setting]}" ] && setting=${map[$setting]} + setting_value=$1 + shift + + case "$setting" in + minify) + css=off + html=off + js=off + for s in ${setting_value//,/ } + do + case "$s" in + css|html|js) eval $s=on;; + *) die "E.g: cloudflare $CMD1 zone minify css,html,js" + esac + done + setting_value="{\"css\":\"$css\",\"html\":\"$html\",\"js\":\"$js\"}" + ;; + esac + + if [ "${setting_value:0:1}" != '{' ] + then + setting_value="\"$setting_value\"" + fi + setting_items="$setting_items${setting_items:+,}{\"id\":\"$setting\",\"value\":$setting_value}" + done + + call_cf_v4 PATCH /zones/$zone_id/settings "{\"items\":[$setting_items]}" + ;; + + record) + str1="Usage: cloudflare $CMD1 record [type | first | oldcontent ] [ [ ... ]] +You must enter \"type\" and the record type (A, MX, ...) when the record name is ambiguous, +or enter \"first\" to modify the first matching record in the zone, +or enter \"oldcontent\" and the exact content of the record you want to modify if there are more records with the same name and type. +Settings: + newname Rename the record + newtype Change type + content See description in 'add record' command + ttl See description in 'add record' command + proxied Turn CF proxying on/off" + [ -z "$1" ] && die "$str1" + record_name=$1 + shift + record_type='' + first_match=0 + record_oldcontent='' + + while [ -n "$1" ] + do + case "$1" in + first) first_match=1;; + type) shift; record_type=${1^^};; + oldcontent) shift; record_oldcontent=$1;; + *) break;; + esac + shift + done + + if [ -z "$1" ] + then + die "$str1" + fi + + findout_record "$record_name" "$record_type" "$first_match" "$record_oldcontent" + e=$? + case $e in + 0) true;; + 2) die "No suitable DNS zone found for \`$record_name'";; + 3) is_quiet && die || die "DNS record \`$record_name' not found";; + 4) die "Ambiguous record name: \`$record_name'";; + *) die "Internal error";; + esac + + record_content_esc=${record_content//\"/\\\"} + old_data="\"name\":\"$record_name\",\"type\":\"$record_type\",\"ttl\":$record_ttl,\"content\":\"$record_content_esc\"" + new_data='' + while [ -n "$1" ] + do + setting=$1 + shift + value=$1 + shift + + [ "$setting" = service_mode ] && setting=proxied + [ "$setting" = newtype -o "$setting" = new_type ] && setting=type + [ "$setting" = newname -o "$setting" = new_name ] && setting=name + [ "$setting" = newcontent ] && setting=content + if [ "$setting" = proxied ] + then + value=${value,,} + [ "$value" = on -o "$value" = 1 ] && value=true + [ "$value" = off -o "$value" = 0 ] && value=false + fi + [ "$setting" = type ] && value=${value^^} + + if [ "$setting" != content ] && ( expr "$value" : '[0-9]\+$' >/dev/null || expr "$value" : '[0-9]\+\.[0-9]\+$' >/dev/null || [ "$value" = true -o "$value" = false ] ) + then + value_escq=$value + else + value_escq=\"${value//\"/\\\"}\" + fi + new_data="$new_data${new_data:+,}\"$setting\":$value_escq" + done + + call_cf_v4 PUT /zones/$zone_id/dns_records/$record_id "{$old_data,$new_data}" + ;; + + *) + die "Parameters: + zone, record" + esac + ;; +# ---------------- +# -- clear command +# ---------------- +clear) + case "$1" in + all-cache) + shift + [ -z "$1" ] && die "Usage: cloudflare clear cache " + get_zone_id "$1" + call_cf_v4 DELETE /zones/$zone_id/purge_cache '{"purge_everything":true}' + ;; + *) + die "Parameters: + cache" + ;; + esac + ;; + +# ---------------- +# -- check command +# ---------------- +check) + case "$1" in + zone) + shift + [ -z "$1" ] && die "Usage: cloudflare check zone " + get_zone_id "$1" + call_cf_v4 PUT /zones/$zone_id/activation_check + ;; + *) + die "Parameters: + zone" + ;; + esac + ;; + +# --------------------- +# -- invalidate command +# --------------------- +invalidate) + if [ -n "$1" ] + then + urls='' + zone_id='' + for url in "$@" + do + urls="${urls:+$urls,}\"$url\"" + if [ -z "$zone_id" ] + then + if [[ "$url" =~ ^([^:]+:)?/*([^:/]+) ]] + then + re_grps=${#BASH_REMATCH[@]} + domain=${BASH_REMATCH[re_grps-1]} + while true + do + zone_id=`get_zone_id "$domain" 2>/dev/null; echo "$zone_id"` + if [ -n "$zone_id" ] + then + break + fi + parent=${domain#*.} + if [ "$parent" = "$domain" ] + then + break + fi + domain=$parent + done + fi + fi + done + if [ -z "$zone_id" ] + then + die "Zone name could not be figured out." + fi + call_cf_v4 DELETE /zones/$zone_id/purge_cache "{\"files\":[$urls]}" + else + die "Usage: cloudflare invalidate [url-2 [url-3 [...]]]" + fi + ;; + +# --------------- +# -- json command +# --------------- +json) + json_decode "$@" + ;; + +# --------------- +# -- help command +# --------------- +help) + HELP usage + ;; +*) + HELP usage + _die "No Command provided." 1 + ;; +esac + diff --git a/tests/domain-search.txt b/tests/domain-search.txt new file mode 100644 index 0000000..3e8c293 --- /dev/null +++ b/tests/domain-search.txt @@ -0,0 +1,5 @@ +concha.com +samoyede.com +cow.com +testing.com +qweqwe.com \ No newline at end of file diff --git a/tests/domains.txt b/tests/domains.txt new file mode 100644 index 0000000..e5fa291 --- /dev/null +++ b/tests/domains.txt @@ -0,0 +1,15 @@ +ibero-mesornis.com +nothofagus.com +berycomorphi.com +a.com +manta.com +jubilant.com +finger.com +astraddle.com +pogonia.com +concha.com +samoyede.com +cow.com +molucella.com +spark.com +qwe.com \ No newline at end of file