#!/usr/bin/env bash

# Global variables
url="https://www.googleapis.com/customsearch/v1"
start="&start="
userAgent="GooFuzz/$version"
version="2.0"

## Used to store the dork
optimizedDork=""

## API Keys Management
apiKey=""
cxId=""
# Arrays for storing CX ID and API Key pairs
allApiKeys=()
allCxIds=()
keyIndex=0

## Effects
colorBold="\e[1m"
colorNormal="\e[0m"

# Functions
## Dependency check
function checkDependencies() {
    if ! command -v jq &> /dev/null; then
        echo -e "${colorBold}[!]${colorNormal} Error: The 'jq' tool is not installed."
        printf "Please install it (e.g., sudo apt install jq).\n"
        exit 1
    fi
}

## Load API Keys
function loadKeys() {
    local input="$1"
    
    if [ -f "$input" ]; then
        local line
        while IFS=',' read -r cx key; do
            cx=$(echo "$cx" | xargs)
            key=$(echo "$key" | xargs)

            if [ -n "$cx" ] && [ -n "$key" ]; then
                allCxIds+=("$cx")
                allApiKeys+=("$key")
            fi
        done < "$input"
    else
        if [[ "$input" =~ "," ]]; then
            IFS=',' read -r cx key <<< "$input"
            cxId=$(echo "$cx" | xargs)
            keylistInput=$(echo "$key" | xargs)
        fi
        
        if [ -z "$cxId" ] && [ -n "$keylistInput" ] && [ -n "$cxIdSingle" ]; then
            allCxIds+=("$cxIdSingle")
            allApiKeys+=("$keylistInput")
        elif [ -z "$cxId" ] && [ -n "$cx" ] && [ -n "$key" ]; then
             allCxIds+=("$cx")
             allApiKeys+=("$key")
        fi
    fi

    if [ ${#allApiKeys[@]} -gt 0 ]; then
        apiKey="${allApiKeys[0]}"
        cxId="${allCxIds[0]}"
    else
        echo -e "${colorBold}[!]${colorNormal} Error: No valid CX ID or API Key pairs found."
        exit 1
    fi
}

## Get Next API Key
function getNextKey() {
    keyIndex=$((keyIndex + 1))

    if [ $keyIndex -lt ${#allApiKeys[@]} ]; then
        apiKey="${allApiKeys[$keyIndex]}"
        cxId="${allCxIds[$keyIndex]}"
        echo -e "${colorBold}[!]${colorNormal} API Key exhausted. Rotating to pair #${keyIndex} (CX: $cxId)..."
        return 0
    else
        return 1
    fi
}

## Usage (MODIFICADA)
function usage {
    printf "\nUsage:\n"
    printf "    -h                        Display this help message.\n"
    printf "    -k <FILE>                 Specify a FILE with CX_ID,API_KEY pairs, one per line.\n"
    printf "    -w <DICTIONARY>           Specify a DICTIONARY, PATHS or FILES.\n"
    printf "    -e <EXTENSION>            Specify comma-separated extensions.\n"
    printf "    -t <TARGET>               Specify a DOMAIN or IP Address.\n"
    printf "    -p <PAGES>                Specify the number of PAGES (Default: 1).\n"
    printf "    -x <EXCLUSIONS>           EXCLUDES targets (comma-separated or file).\n"
    printf "    -d <DELAY>                Delay in seconds between requests.\n"
    printf "    -s                        Lists subdomains of the specified domain.\n"
    printf "    -c <TEXT>                 Specify relevant content (comma-separated or file).\n"
    printf "    -o <FILENAME>             Export the results to a file (results only).\n"
    printf "    -r <PROXY>                Specify an [protocol://]host[:port] proxy.\n"
    printf "\n"
    printf "Examples:\n"
    printf "    GooFuzz -t site.com -k keys_file.txt -e pdf,doc,bak\n"
    printf "    GooFuzz -t site.com -k keys_file.txt -s -p 10 -d 5 -o GooFuzz-subdomains.txt\n"
    printf "    GooFuzz -t site.com -k keys_file.txt -w config.php,admin,/images/\n"
    printf "    GooFuzz -t site.com -k keys_file.txt -w wordlist.txt\n"
    printf "    GooFuzz -t site.com -k keys_file.txt -w login.html -x dev.site.com\n"   
    printf "    GooFuzz -t site.com -k keys_file.txt -w admin.html -x exclusion_list.txt\n"
    printf "    GooFuzz -t site.com -k keys_file.txt -c P@ssw0rd!\n"
    printf "    GooFuzz -t site.com -k keys_file.txt -e pdf -r http://proxy.example.com:8080\n"   
    exit 0
}

## Checking parameters
function parametersCheck(){
    if [[ ${OPTARG} =~ ^- ]]; then
        showError
    fi
}

## Show Banner
function showBanner(){
    printf "*********************************************************\n"
    printf "* GooFuzz v.${version} - The Power of Google Dorks             *\n"
    printf "*********************************************************\n"
}

## Show Full Banner
function showFullBanner(){
    printf "*********************************************************\n"
    printf "* GooFuzz v.${version} - The Power of Google Dorks             *\n"
    printf "*                                                       *\n"
    printf "* David Utón (@David_Uton)                              *\n"
    printf "*********************************************************\n"
}

## Show errors
function showError(){
    echo -e "${colorBold}[!]${colorNormal} Error, missing or invalid argument."
    usage
}

## Show not found
function notFound(){
    local current_extension="$1"

    if [ -n "$extension" ]; then
        echo -e "\nSorry, no results found for extensions: ${colorBold}${current_extension}${colorNormal}.\n"
    elif [ -n "$dictionary" ]; then
        echo -e "\nSorry, no results found for dictionary: ${colorBold}${dictionary}${colorNormal}.\n"
    elif [ -n "$subdomain" ]; then
        echo -e "\nSorry, no subdomains found for ${colorBold}${target}${colorNormal}"
    elif [ -n "$contents" ]; then
        printf "\nSorry, no results found.\n"
    fi
}

## Show content in files
function showContentInFile(){
    if [ -n "$contents" ]; then
        echo -e "Files found containing: ${colorBold}${contents}${colorNormal}"
    fi
}

## Exit GooFuzz
trap ctrl_c INT

function ctrl_c(){
    echo -e "${colorBold}[!]${colorNormal} Exiting GooFuzz..."
    exit 1
}

## API limit check
function checkApiLimit(){
    local hasError=$(echo "$apiResponse" | jq 'has("error")' 2>/dev/null)

    if [ "$hasError" = "true" ]; then
        errorMessage=$(echo "$apiResponse" | jq -r '.error.message // "Unknown error."')
        errorReason=$(echo "$apiResponse" | jq -r '.error.errors[0].reason // ""')

        if [[ "$errorMessage" == *"limitExceeded"* ]] || [[ "$errorReason" == *"dailyLimitExceeded"* ]]; then
            echo -e "${colorBold}[!]${colorNormal} ERROR: Google API limit exceeded for current key."
            if ! getNextKey; then
                echo -e "${colorBold}[!]${colorNormal} ERROR: All API Keys in the list have been exhausted. Logging out."
                exit 1
            fi
            return 1
        elif [[ "$errorMessage" == *"Key"* ]] || [[ "$errorMessage" == *"Identity"* ]] || [[ "$errorMessage" == *"not used in project"* ]]; then
            echo -e "${colorBold}[!]${colorNormal} ERROR: Invalid API key, CX ID, or API disabled. Trying next pair..."
            if ! getNextKey; then
                echo -e "${colorBold}[!]${colorNormal} ERROR: All API Keys have failed or are invalid. Logging out."
                exit 1
            fi
            return 1
        else
            echo -e "${colorBold}[!]${colorNormal} API error: $errorMessage"
            exit 1
        fi
    fi
    return 0
}

## Exclusions
function exclusionsCheck(){
    excludeTargets=""
    local multi=0

    if [ -f "$exclusions" ]; then
        for exclusion in $(cat "$exclusions"); do
            excludeTargets+=" -site:${exclusion}"
        done

    elif [[ "$exclusions" =~ "," ]]; then
        local excludeTargetsList=$(echo "$exclusions" | tr ',' ' ')

        local is_first=true
        for exclusion in $excludeTargetsList; do
            if [ "$is_first" = true ]; then
                excludeTargets+="-site:${exclusion}"
                is_first=false
            else
                excludeTargets+=" -site:${exclusion}"
            fi
        done

    else
        excludeTargets=" -site:${exclusions}"
    fi
}

## Search the contents of files
function contentsCheck(){
    inFile=""
    local multi=0

    if [ -f "$contents" ]; then
        for content in $(cat "$contents"); do
            if [[ $multi -eq 1 ]]; then
                inFile+=" OR "
            fi
            inFile+="\"${content}\""
            multi=1
        done
    elif [[ "$contents" =~ "," ]]; then
        inFile=$(echo "$contents" | sed 's/,/" OR "/g' | sed 's/^/"/' | sed 's/$/"/')
    else
        inFile="\"${contents}\""
    fi
}

## Search words in URL
function inurlCheck(){
    inUrl=""
    local multi=0

    if [ -f "$dictionary" ]; then
        for word in $(cat "$dictionary"); do
            if [[ $multi -eq 1 ]]; then
                inUrl+=" OR "
            fi
            inUrl+="inurl:\"${word}\""
            multi=1
        done
    elif [[ "$dictionary" =~ "," ]]; then
        inUrl=$(echo "$dictionary" | sed 's/,/" OR "/g' | sed 's/^/inurl:"/' | sed 's/$/"/')
    else
        inUrl="inurl:\"${dictionary}\""
    fi
}

## Calculate sending requests
function calcRequests(){
    if [[ -z $pages ]] || [[ $pages -eq 0 ]]; then
        return $totalRequests
    else
        let totalRequests=$totalRequests*$pages
        return $totalRequests
    fi
}

## Delay Control
function delayControl(){
    if [[ -n "$delay" ]]; then
        sleep "$delay"
    fi
}

## Request
function requestRun(){
    # Reset variables
    requestStorage=""
    page=0

    pages=${pages:-1}

    while [ $page -lt $pages ]; do
        local keyRotated=false

        while true; do
            let pageNum=($page*10)+1

            local dorkQuery=""
            local targetDork="site:${target}"

            if [ -n "$optimizedDork" ]; then
                dorkQuery="${targetDork}+${optimizedDork}"

            elif [ -n "$extension" ]; then
                dorkQuery="${targetDork}+filetype:${extension}"

            elif [ -n "$dictionary" ]; then
                dorkQuery="${targetDork}+inurl:\"${dictionary}\""

            elif [ -n "$subdomain" ]; then
                dorkQuery="site:*.${target}+-site:www.${target}"
                targetDork=""

            elif [ -n "$contents" ]; then
                dorkQuery="${targetDork}+${inFile}"
            fi

            # Exclusions
            if [ -n "$excludeTargets" ]; then
                if [ -z "$subdomain" ]; then
                    dorkQuery+="${excludeTargets}"
                else
                    dorkQuery+=" ${excludeTargets}"
                fi
            fi

            queryCodificado=$(echo "$dorkQuery" | sed 's/ /+/g' | sed 's/:"/:"/g')

            apiResponse=$(curl -s -k -x "$proxy" -A "$userAgent" \
                                 "${url}?key=${apiKey}&cx=${cxId}&q=${queryCodificado}${start}${pageNum}")

            # API check limit
            if ! checkApiLimit; then
                keyRotated=true
                continue
            fi
            break
        done

        if [ "$keyRotated" = true ]; then
            continue
        fi

        request=$(echo "$apiResponse" | jq -r '.items[] | .link' 2>/dev/null)

        if [ -z "$request" ]; then
            break
        fi

        # Request storage
        if [ -z "$requestStorage" ]; then
            requestStorage="$request"
        else
            requestStorage+="\n$request"
        fi

        # Pages Incremental
        ((page++))

        # Delay Control
        delayControl
    done
}

## GooFuzz Dictionary Attack
function dictionaryAttack(){
    echo -e "\nTarget: ${colorBold}${target}${colorNormal}"

    local dorkInput="$dictionary"
    local wordsList=""
    local numWords=0
    local all_results=""
    local is_file=false

    if [ -f "$dorkInput" ]; then
        is_file=true
        wordsList=$(cat "$dorkInput" | tr '\n' ' ')

    elif [[ "$dorkInput" =~ "," ]]; then
        wordsList=$(echo "$dorkInput" | tr ',' ' ')
    else
        requestRun
        if [ -n "$requestStorage" ]; then
            printf "\n===================================================================\n"
            echo -e "Directories and files found from: ${colorBold}${dictionary}${colorNormal}"
            printf "===================================================================\n"
            echo -e "$requestStorage" | sort -u
        else
            notFound
        fi
        exit 1
    fi

    for word in $wordsList; do
        ((numWords++))
    done

    if [ "$is_file" = true ] || [ $numWords -gt 5 ]; then
        for word in $wordsList; do
            dictionary="$word"

            # Send request
            requestRun

            if [ -n "$requestStorage" ]; then
                printf "\n===================================================================\n"
                echo -e "Dork: inurl:\"${colorBold}${word}${colorNormal}\""
                printf "===================================================================\n"
                echo -e "$requestStorage" | sort -u

                if [ -n "$all_results" ]; then all_results+="\n"; fi
                all_results+="$requestStorage"

                if [ -n "$outputFile" ]; then
                    echo -e "\nDork: inurl:\"$word\"" >> "$outputFile"
                    echo "-------------------------------------------------------------------" >> "$outputFile"
                    echo -e "$requestStorage" | sort -u >> "$outputFile"
                fi
            fi

            dictionary="$dorkInput"
        done

    else
        # Consolidated mode
        local dork_parts=""
        local is_first=true

        for word in $wordsList; do
            if [ "$is_first" = false ]; then
                dork_parts+=" OR "
            fi
            dork_parts+="inurl:\"${word}\""
            is_first=false
        done

        optimizedDork="$dork_parts"
        requestRun

        if [ -n "$requestStorage" ]; then
            printf "\n===================================================================\n"
            echo -e "Directories and files found: ${colorBold}${dorkInput}${colorNormal}"
            printf "===================================================================\n"
            echo -e "$requestStorage" | sort -u
            all_results="$requestStorage"
        fi
    fi

    if [ -z "$all_results" ]; then
        notFound "$dorkInput"
    fi

    exit 1
}

## GooFuzz Extension Attack
function extensionAttack(){
    echo -e "\nTarget: ${colorBold}${target}${colorNormal}"

    local dorkInput="$extension"
    local extensionsList=""
    local numExt=0
    local all_results=""
    local is_file=false

    if [ -f "$dorkInput" ]; then
        is_file=true
        extensionsList=$(cat "$dorkInput" | tr '\n' ' ')

    elif [[ "$dorkInput" =~ "," ]]; then
        extensionsList=$(echo "$dorkInput" | tr ',' ' ')
    else
        # Send request
        requestRun
        if [ -n "$requestStorage" ]; then
            printf "\n===================================================================\n"
            echo -e "Extension: ${colorBold}${extension}${colorNormal}"
            showContentInFile
            printf "===================================================================\n"
            echo -e "$requestStorage" | sort -u
        else
            notFound
        fi
        exit 1
    fi

    for ext in $extensionsList; do
        ((numExt++))
    done

    if [ "$is_file" = true ] || [ $numExt -gt 5 ]; then
        for ext in $extensionsList; do
            extension="$ext"

            # Send request
            requestRun

            if [ -n "$requestStorage" ]; then
                printf "\n===================================================================\n"
                echo -e "Extension: ${colorBold}${ext}${colorNormal}"
                showContentInFile
                printf "===================================================================\n"
                echo -e "$requestStorage" | sort -u

                if [ -n "$all_results" ]; then all_results+="\n"; fi
                all_results+="$requestStorage"

                if [ -n "$outputFile" ]; then
                    echo -e "\nExtension: $ext" >> "$outputFile"
                    echo "-------------------------------------------------------------------" >> "$outputFile"
                    echo -e "$requestStorage" | sort -u >> "$outputFile"
                fi
            fi

            extension="$dorkInput"
        done

    else
        # Consolidate mode
        local dork_parts=""
        local is_first=true

        for ext in $extensionsList; do
            if [ "$is_first" = false ]; then
                dork_parts+=" OR "
            fi
            dork_parts+="filetype:${ext}"
            is_first=false
        done

        optimizedDork="$dork_parts"
        requestRun

        if [ -n "$requestStorage" ]; then
            printf "\n===================================================================\n"
            echo -e "Extensions: ${colorBold}${dorkInput}${colorNormal}"
            showContentInFile
            printf "===================================================================\n"
            echo -e "$requestStorage" | sort -u
            all_results="$requestStorage"
        fi
    fi

    if [ -z "$all_results" ]; then
        notFound "$dorkInput"
    fi

    exit 1
}


## GooFuzz Subdomains Attack
function subdomainAttack(){
    echo -e "\nTarget: ${colorBold}${target}${colorNormal}"

    # Send request
    requestRun

    if [ -n "$requestStorage" ]; then

        local subdomains=$(echo -e "$requestStorage" | \
                                 sed -E 's/https?:\/\///' | \
                                 sed -E 's/\/.*$//' | \
                                 sort -u)

        printf "\n===================================================================\n"
        printf "Subdomains found:\n"
        printf "===================================================================\n"
        echo "$subdomains"

        if [ -n "$outputFile" ]; then
            echo -e "\nSubdomains found for: $target" >> "$outputFile"
            echo "-------------------------------------------------------------------" >> "$outputFile"
            echo "$subdomains" >> "$outputFile"
        fi
    else
        notFound
    fi
    exit 1
}

## GooFuzz Contents Attack
function contentsAttack(){
    echo -e "\nTarget: ${colorBold}${target}${colorNormal}"

    # Send request
    requestRun

    if [ -n "$requestStorage" ]; then
        printf "\n===================================================================\n"
        echo -e "Files found containing: ${colorBold}${contents}${colorNormal}"
        printf "===================================================================\n"
        echo -e "$requestStorage" | sort -u

        if [ -n "$outputFile" ]; then
            echo -e "\nFiles found containing: $contents" >> "$outputFile"
            echo "-------------------------------------------------------------------" >> "$outputFile"
            echo -e "$requestStorage" | sort -u >> "$outputFile"
        fi
    else
        notFound
    fi
}

# Script execute
checkDependencies

## Options
while getopts :k:p:x:c:d:w:e:o:t:r:sh option; do
    case ${option} in
        h)
            showFullBanner
            usage
            break
            exit 1
        ;;
        k)
            parametersCheck
            keylistInput=${OPTARG}
        ;;
        p)
            parametersCheck
            pages=${OPTARG}
        ;;
        x)
            parametersCheck
            exclusions=${OPTARG}
            exclusionsCheck
        ;;
        c)
            parametersCheck
            contents=${OPTARG}
            contentsCheck
        ;;
        d)
            parametersCheck
            delay=${OPTARG}
        ;;
        w)
            parametersCheck
            dictionary=${OPTARG}
        ;;
        e)
            parametersCheck
            extension=${OPTARG}
        ;;
        o)
            parametersCheck
            outputFile=${OPTARG}
        ;;
        t)
            parametersCheck
            target=${OPTARG}
        ;;
        s)
            parametersCheck
            subdomain=on
        ;;
        r)
            parametersCheck
            proxy=${OPTARG}
        ;;
        *)
            showFullBanner
            showError
        ;;
    esac
done

## API Keys Check
if [ -z "$keylistInput" ]; then
    echo -e "${colorBold}[!]${colorNormal} Error: A file with CX ID and API Key pairs is required. Use the -k option."
    usage
fi

loadKeys "$keylistInput"

## Continue
showBanner

if [ -n "$target" ] && [ -n "$dictionary" ] && [ -z "$subdomain" ] && [ -z "$extension" ] && [ -z "$contents" ]; then
    dictionaryAttack

elif [ -n "$target" ] && [ -n "$extension" ] && [ -z "$subdomain" ] && [ -z "$dictionary" ]; then
    extensionAttack

elif [ -n "$target" ] && [ -n "$subdomain" ] && [ -z "$extension" ] && [ -z "$dictionary" ] && [ -z "$contents" ]; then
    subdomainAttack

elif [ -n "$target" ] && [ -n "$contents" ] && [ -z "$subdomain" ] && [ -z "$dictionary" ]; then
    contentsAttack
    exit 1
else
    showError
fi