Created
September 5, 2025 18:40
-
-
Save tomasbasham/d8f4e2fcc17844a0e66a5b89bbde464c to your computer and use it in GitHub Desktop.
Naive implementation of a RDAP client.
This file contains hidden or 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
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # The general approach is outlined in the following StackOverflow approach | |
| # https://stackoverflow.com/a/60584381/1613695 | |
| # This script was generated with AI to demonstrate the approach. It is certainly | |
| # not producton ready. | |
| # Configuration | |
| IANA_RDAP_URL="http://data.iana.org/rdap/dns.json" | |
| CACHE_FILE="/tmp/rdap_dns_cache.json" | |
| CACHE_DURATION=3600 # 1 hour in seconds | |
| # Colours for output | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| BLUE='\033[0;34m' | |
| NC='\033[0m' # No Colour | |
| usage() { | |
| cat << EOF | |
| Usage: $0 <domain> [tld] | |
| Perform RDAP lookup for a domain by finding the authoritative RDAP server. | |
| Arguments: | |
| domain The domain name to lookup (e.g., example.com or just example) | |
| tld The TLD (e.g., com, org.uk) - optional if domain includes TLD | |
| Examples: | |
| $0 example.com | |
| $0 example com | |
| $0 example.co.uk | |
| $0 example co.uk | |
| The script will automatically attempt to extract the TLD from the domain | |
| if only one argument is provided. | |
| EOF | |
| } | |
| log() { | |
| echo -e "${BLUE}[INFO]${NC} $1" >&2 | |
| } | |
| warn() { | |
| echo -e "${YELLOW}[WARN]${NC} $1" >&2 | |
| } | |
| error() { | |
| echo -e "${RED}[ERROR]${NC} $1" >&2 | |
| } | |
| success() { | |
| echo -e "${GREEN}[SUCCESS]${NC} $1" >&2 | |
| } | |
| # Function to extract TLD from domain using common patterns | |
| # This is a simplified approach - not perfect but covers most cases | |
| extract_tld() { | |
| local domain="$1" | |
| # Handle common multi-part TLDs first | |
| if [[ "$domain" =~ \.(co\.uk|org\.uk|me\.uk|ltd\.uk|plc\.uk|net\.uk)$ ]]; then | |
| echo "${BASH_REMATCH[1]}" | |
| return | |
| elif [[ "$domain" =~ \.(com\.au|net\.au|org\.au|edu\.au|gov\.au|asn\.au|id\.au)$ ]]; then | |
| echo "${BASH_REMATCH[1]}" | |
| return | |
| elif [[ "$domain" =~ \.(co\.nz|net\.nz|org\.nz|govt\.nz|mil\.nz|iwi\.nz|geek\.nz|gen\.nz|kiwi\.nz|maori\.nz|school\.nz)$ ]]; then | |
| echo "${BASH_REMATCH[1]}" | |
| return | |
| elif [[ "$domain" =~ \.(com\.br|net\.br|org\.br|gov\.br|edu\.br|mil\.br)$ ]]; then | |
| echo "${BASH_REMATCH[1]}" | |
| return | |
| fi | |
| # Handle single-part TLDs | |
| if [[ "$domain" =~ \.([a-zA-Z0-9-]+)$ ]]; then | |
| echo "${BASH_REMATCH[1]}" | |
| return | |
| fi | |
| # If no TLD found, return empty | |
| echo "" | |
| } | |
| # Function to get domain name without TLD | |
| get_domain_name() { | |
| local full_domain="$1" | |
| local tld="$2" | |
| # Remove the TLD from the end | |
| echo "${full_domain%.$tld}" | |
| } | |
| # Function to fetch and cache RDAP DNS data | |
| get_rdap_data() { | |
| local cache_file="$1" | |
| # Check if cache exists and is fresh | |
| if [[ -f "$cache_file" ]]; then | |
| local cache_age=$(($(date +%s) - $(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null || echo 0))) | |
| if [[ $cache_age -lt $CACHE_DURATION ]]; then | |
| log "Using cached RDAP data" | |
| cat "$cache_file" | |
| return | |
| fi | |
| fi | |
| log "Fetching RDAP data from IANA..." | |
| if curl -sf "$IANA_RDAP_URL" > "$cache_file"; then | |
| cat "$cache_file" | |
| else | |
| error "Failed to fetch RDAP data from IANA" | |
| exit 1 | |
| fi | |
| } | |
| # Function to find RDAP server for a given TLD | |
| find_rdap_server() { | |
| local tld="$1" | |
| local rdap_data="$2" | |
| log "Looking for RDAP server for TLD: $tld" | |
| # Use jq to find the RDAP server for the given TLD | |
| local server=$(echo "$rdap_data" | jq -r --arg tld "$tld" ' | |
| .services[] | | |
| select(.[0][] | test("^" + $tld + "$"; "i")) | | |
| .[1][0] | |
| ') | |
| if [[ -n "$server" && "$server" != "null" ]]; then | |
| echo "$server" | |
| else | |
| echo "" | |
| fi | |
| } | |
| # Function to extract related links from RDAP response | |
| extract_related_links() { | |
| local rdap_response="$1" | |
| echo "$rdap_response" | jq -r ' | |
| .links[]? | | |
| select(.rel == "related") | | |
| .href | |
| ' 2>/dev/null || echo "" | |
| } | |
| # Function to merge RDAP responses | |
| merge_rdap_responses() { | |
| local base_response="$1" | |
| local additional_response="$2" | |
| # Simple merge - just combine the objects, additional data wins for conflicts | |
| echo "$base_response $additional_response" | jq -s '.[0] * .[1]' | |
| } | |
| # Function to perform recursive RDAP lookup | |
| perform_recursive_rdap_lookup() { | |
| local domain="$1" | |
| local rdap_server="$2" | |
| local visited_urls="$3" | |
| local depth="${4:-0}" | |
| local max_depth=5 | |
| # Prevent infinite recursion | |
| if [[ $depth -ge $max_depth ]]; then | |
| warn "Maximum recursion depth ($max_depth) reached, stopping" | |
| echo "{}" | |
| return 0 | |
| fi | |
| # Construct the RDAP URL | |
| local rdap_url="${rdap_server%/}/domain/$domain" | |
| # Check if we've already visited this URL to prevent loops | |
| if [[ "$visited_urls" == *"$rdap_url"* ]]; then | |
| warn "Already visited $rdap_url, skipping to prevent loop" | |
| echo "{}" | |
| return 0 | |
| fi | |
| # Add current URL to visited list | |
| visited_urls="$visited_urls $rdap_url" | |
| log "Performing RDAP lookup (depth $depth): $rdap_url" | |
| # Perform the RDAP lookup | |
| local rdap_response | |
| if rdap_response=$(curl -sf -H "Accept: application/rdap+json" "$rdap_url" 2>/dev/null); then | |
| log "RDAP lookup successful at depth $depth" | |
| # Extract related links for recursive lookups | |
| local related_links | |
| related_links=$(extract_related_links "$rdap_response") | |
| # Start with the base response | |
| local merged_response="$rdap_response" | |
| # Perform recursive lookups for related links | |
| if [[ -n "$related_links" ]]; then | |
| while IFS= read -r link; do | |
| if [[ -n "$link" && "$link" != "null" ]]; then | |
| log "Following related link at depth $depth: $link" | |
| # Perform recursive lookup | |
| local recursive_response | |
| if recursive_response=$(curl -sf -H "Accept: application/rdap+json" "$link" 2>/dev/null); then | |
| log "Successfully retrieved data from related link" | |
| # Merge the responses | |
| merged_response=$(merge_rdap_responses "$merged_response" "$recursive_response") | |
| # Check for further related links in the recursive response | |
| local further_links | |
| further_links=$(extract_related_links "$recursive_response") | |
| if [[ -n "$further_links" ]]; then | |
| while IFS= read -r further_link; do | |
| if [[ -n "$further_link" && "$further_link" != "null" && "$visited_urls" != *"$further_link"* ]]; then | |
| log "Following further related link at depth $((depth + 1)): $further_link" | |
| local further_response | |
| if further_response=$(curl -sf -H "Accept: application/rdap+json" "$further_link" 2>/dev/null); then | |
| merged_response=$(merge_rdap_responses "$merged_response" "$further_response") | |
| visited_urls="$visited_urls $further_link" | |
| else | |
| warn "Failed to retrieve data from further link: $further_link" | |
| fi | |
| fi | |
| done <<< "$further_links" | |
| fi | |
| else | |
| warn "Failed to retrieve data from related link: $link" | |
| fi | |
| fi | |
| done <<< "$related_links" | |
| fi | |
| echo "$merged_response" | |
| return 0 | |
| else | |
| error "RDAP lookup failed for $domain at $rdap_url" | |
| return 1 | |
| fi | |
| } | |
| # Function to perform RDAP lookup (wrapper for backward compatibility) | |
| perform_rdap_lookup() { | |
| local domain="$1" | |
| local rdap_server="$2" | |
| local result | |
| if result=$(perform_recursive_rdap_lookup "$domain" "$rdap_server" "" 0); then | |
| echo "$result" | |
| success "RDAP lookup completed successfully with all related data" | |
| return 0 | |
| else | |
| return 1 | |
| fi | |
| } | |
| # Main function | |
| main() { | |
| local domain_input="$1" | |
| local tld_input="${2:-}" | |
| local full_domain="" | |
| local tld="" | |
| local domain_name="" | |
| # Determine domain and TLD | |
| if [[ -n "$tld_input" ]]; then | |
| # TLD provided separately | |
| domain_name="$domain_input" | |
| tld="$tld_input" | |
| full_domain="$domain_name.$tld" | |
| else | |
| # Extract TLD from domain | |
| full_domain="$domain_input" | |
| tld=$(extract_tld "$full_domain") | |
| if [[ -z "$tld" ]]; then | |
| error "Could not extract TLD from '$full_domain'. Please provide TLD as second argument." | |
| usage | |
| exit 1 | |
| fi | |
| domain_name=$(get_domain_name "$full_domain" "$tld") | |
| fi | |
| log "Domain: $domain_name" | |
| log "TLD: $tld" | |
| log "Full domain: $full_domain" | |
| # Get RDAP data | |
| local rdap_data=$(get_rdap_data "$CACHE_FILE") | |
| # Find RDAP server | |
| local rdap_server=$(find_rdap_server "$tld" "$rdap_data") | |
| if [[ -z "$rdap_server" ]]; then | |
| error "No RDAP server found for TLD: $tld" | |
| exit 1 | |
| fi | |
| success "Found RDAP server: $rdap_server" | |
| # Perform RDAP lookup | |
| perform_rdap_lookup "$full_domain" "$rdap_server" | |
| } | |
| # Check dependencies | |
| for cmd in curl jq; do | |
| if ! command -v "$cmd" &> /dev/null; then | |
| error "Required command '$cmd' not found. Please install it." | |
| exit 1 | |
| fi | |
| done | |
| # Parse arguments | |
| if [[ $# -eq 0 || "$1" == "-h" || "$1" == "--help" ]]; then | |
| usage | |
| exit 0 | |
| fi | |
| if [[ $# -gt 2 ]]; then | |
| error "Too many arguments provided" | |
| usage | |
| exit 1 | |
| fi | |
| # Run main function | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment