auto-violator.sh 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245
  1. #!/usr/bin/env bash
  2. # auto-vibrator.sh — piss slop that barely works <3
  3. #
  4. # Usage:
  5. # ./scripts/auto-violator.sh # fetch all, show summary
  6. # ./scripts/auto-violator.sh porkbun # porkbun only
  7. # ./scripts/auto-violator.sh inwx # inwx only
  8. # ./scripts/auto-violator.sh --raw # output raw TLD lists (one per line)
  9. # ./scripts/auto-violator.sh --toml # output TOML-ready arrays
  10. # ./scripts/auto-violator.sh --diff # compare against current Lists.toml
  11. # ./scripts/auto-violator.sh --template # generate Lists.toml into violator-workdir/
  12. # ./scripts/auto-violator.sh --probe # probe unknown TLDs for WHOIS servers
  13. #
  14. # Outputs:
  15. # violator-workdir/cache/ — cached API responses
  16. # violator-workdir/pdom.txt — purchasable TLDs with known WHOIS/RDAP servers
  17. # violator-workdir/sdom.txt — TLDs where no WHOIS server could be found
  18. # violator-workdir/Lists.toml — generated Lists.toml (never overwrites project root)
  19. #
  20. # Config: scripts/violator.conf
  21. #
  22. # Notes : yea parts of this is ai slop, didnt make it myself oooo scary,
  23. # but most of the rust i did myself just didnt feel like doing
  24. # this at 4am and it somewhat works
  25. # Correction : The initial porkbun fetching was mostly me but porkbun
  26. # lacked many domains so yea
  27. set -euo pipefail
  28. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  29. PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
  30. LISTS_TOML="$PROJECT_DIR/Lists.toml"
  31. WORK_DIR="$SCRIPT_DIR/violator-workdir"
  32. CACHE_DIR="$WORK_DIR/cache"
  33. CONF_FILE="$SCRIPT_DIR/violator.conf"
  34. PDOM_FILE="$WORK_DIR/pdom.txt"
  35. SDOM_FILE="$WORK_DIR/sdom.txt"
  36. OUTPUT_TOML="$WORK_DIR/Lists.toml"
  37. mkdir -p "$CACHE_DIR"
  38. RED='\033[0;31m'
  39. GREEN='\033[0;32m'
  40. YELLOW='\033[1;33m'
  41. CYAN='\033[0;36m'
  42. BOLD='\033[1m'
  43. NC='\033[0m'
  44. # ═══════════════════════════════════════════════════════════════════════════
  45. # Config parser
  46. # ═══════════════════════════════════════════════════════════════════════════
  47. # Read a value from violator.conf
  48. # Usage: conf_get section key [default]
  49. conf_get() {
  50. local section="$1" key="$2" default="${3:-}"
  51. if [[ ! -f "$CONF_FILE" ]]; then
  52. echo "$default"
  53. return
  54. fi
  55. awk -v section="$section" -v key="$key" -v default="$default" '
  56. /^\[/ { in_section = ($0 == "[" section "]") ; next }
  57. in_section && /^[[:space:]]*#/ { next }
  58. in_section && /^[[:space:]]*$/ { next }
  59. in_section {
  60. # key = value
  61. split($0, kv, "=")
  62. gsub(/^[[:space:]]+|[[:space:]]+$/, "", kv[1])
  63. gsub(/^[[:space:]]+|[[:space:]]+$/, "", kv[2])
  64. if (kv[1] == key) { print kv[2]; found=1; exit }
  65. }
  66. END { if (!found) print default }
  67. ' "$CONF_FILE"
  68. }
  69. # Read all keys from a section (returns "key value" lines)
  70. conf_section_keys() {
  71. local section="$1"
  72. if [[ ! -f "$CONF_FILE" ]]; then
  73. return
  74. fi
  75. awk -v section="$section" '
  76. /^\[/ { in_section = ($0 == "[" section "]") ; next }
  77. in_section && /^[[:space:]]*#/ { next }
  78. in_section && /^[[:space:]]*$/ { next }
  79. in_section {
  80. split($0, kv, "=")
  81. gsub(/^[[:space:]]+|[[:space:]]+$/, "", kv[1])
  82. gsub(/^[[:space:]]+|[[:space:]]+$/, "", kv[2])
  83. if (kv[1] != "" && kv[2] != "") print kv[1], kv[2]
  84. }
  85. ' "$CONF_FILE"
  86. }
  87. # Read a bare-value section (values are just words, possibly multiline)
  88. conf_section_values() {
  89. local section="$1"
  90. if [[ ! -f "$CONF_FILE" ]]; then
  91. return
  92. fi
  93. awk -v section="$section" '
  94. /^\[/ { in_section = ($0 == "[" section "]") ; next }
  95. in_section && /^[[:space:]]*#/ { next }
  96. in_section && /^[[:space:]]*$/ { next }
  97. in_section { print }
  98. ' "$CONF_FILE"
  99. }
  100. # Read the tlds field from a [list.NAME] section (may be multiline, indented continuation)
  101. conf_list_tlds() {
  102. local list_name="$1"
  103. local section="list.${list_name}"
  104. if [[ ! -f "$CONF_FILE" ]]; then
  105. return
  106. fi
  107. awk -v section="$section" '
  108. /^\[/ { in_section = ($0 == "[" section "]"); next }
  109. in_section && /^[[:space:]]*#/ { next }
  110. in_section && /^[[:space:]]*$/ { next }
  111. in_section {
  112. split($0, kv, "=")
  113. gsub(/^[[:space:]]+|[[:space:]]+$/, "", kv[1])
  114. if (kv[1] == "tlds") { in_tlds=1; gsub(/^[^=]*=/, ""); print; next }
  115. if (in_tlds && /^[[:space:]]/) { print; next }
  116. in_tlds=0
  117. }
  118. ' "$CONF_FILE" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//'
  119. }
  120. # Get all [list.*] section names
  121. conf_list_names() {
  122. if [[ ! -f "$CONF_FILE" ]]; then
  123. echo "standard decent swiss country two three four long all"
  124. return
  125. fi
  126. grep -oE '^\[list\.[a-z0-9_]+\]' "$CONF_FILE" | sed 's/\[list\.//;s/\]//'
  127. }
  128. # Load skip TLDs from config
  129. load_skip_tlds() {
  130. local skip
  131. skip=$(conf_section_values "skip_tlds" | tr '\n' ' ')
  132. if [[ -z "$skip" ]]; then
  133. skip="bl bq eh mf gb bv sj kp hm"
  134. fi
  135. echo "$skip"
  136. }
  137. # Load whois overrides from config
  138. load_whois_overrides() {
  139. conf_section_keys "whois_overrides"
  140. }
  141. # ═══════════════════════════════════════════════════════════════════════════
  142. # Fetchers
  143. # ═══════════════════════════════════════════════════════════════════════════
  144. # ─── Porkbun ────────────────────────────────────────────────────────────────
  145. fetch_porkbun() {
  146. local cache="$CACHE_DIR/porkbun.json"
  147. local max_age=86400
  148. if [[ -f "$cache" ]]; then
  149. local age=$(( $(date +%s) - $(stat -f%m "$cache" 2>/dev/null || stat -c%Y "$cache" 2>/dev/null || echo 0) ))
  150. if (( age < max_age )); then
  151. echo "$cache"
  152. return 0
  153. fi
  154. fi
  155. echo -e "${CYAN}Fetching Porkbun pricing API...${NC}" >&2
  156. if curl -sf -X POST "https://api.porkbun.com/api/json/v3/pricing/get" \
  157. -H "Content-Type: application/json" \
  158. -d '{}' \
  159. -o "$cache" 2>/dev/null; then
  160. echo "$cache"
  161. else
  162. echo -e "${RED}Failed to fetch Porkbun data${NC}" >&2
  163. return 1
  164. fi
  165. }
  166. parse_porkbun() {
  167. local json_file="$1"
  168. if command -v jq &>/dev/null; then
  169. jq -r '.pricing // {} | keys[]' "$json_file" 2>/dev/null | sort -u
  170. else
  171. grep -o '"[a-z][a-z0-9.-]*":{' "$json_file" | sed 's/"//g; s/:{//' | sort -u
  172. fi
  173. }
  174. # ─── INWX ───────────────────────────────────────────────────────────────────
  175. fetch_inwx() {
  176. local cache="$CACHE_DIR/inwx.csv"
  177. local max_age=86400
  178. if [[ -f "$cache" ]]; then
  179. local age=$(( $(date +%s) - $(stat -f%m "$cache" 2>/dev/null || stat -c%Y "$cache" 2>/dev/null || echo 0) ))
  180. if (( age < max_age )); then
  181. echo "$cache"
  182. return 0
  183. fi
  184. fi
  185. echo -e "${CYAN}Fetching INWX pricelist CSV...${NC}" >&2
  186. if curl -sf "https://www.inwx.ch/en/domain/pricelist/vat/1/file/csv" \
  187. -o "$cache" 2>/dev/null; then
  188. echo "$cache"
  189. else
  190. echo -e "${YELLOW}Could not fetch INWX${NC}" >&2
  191. return 1
  192. fi
  193. }
  194. parse_inwx() {
  195. local csv_file="$1"
  196. sed 's/;.*//' "$csv_file" | tr -d '"' | grep -E '^[a-z][a-z0-9]*$' | sort -u
  197. }
  198. # ─── OVH ────────────────────────────────────────────────────────────────────
  199. fetch_ovh() {
  200. local cache="$CACHE_DIR/ovh.json"
  201. local max_age=86400
  202. if [[ -f "$cache" ]]; then
  203. local age=$(( $(date +%s) - $(stat -f%m "$cache" 2>/dev/null || stat -c%Y "$cache" 2>/dev/null || echo 0) ))
  204. if (( age < max_age )); then
  205. echo "$cache"
  206. return 0
  207. fi
  208. fi
  209. echo -e "${CYAN}Fetching OVH domain extensions...${NC}" >&2
  210. if curl -sf "https://www.ovh.com/engine/apiv6/domain/extensions" \
  211. -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" \
  212. -o "$cache" 2>/dev/null; then
  213. echo "$cache"
  214. else
  215. echo -e "${YELLOW}Could not fetch OVH extensions${NC}" >&2
  216. return 1
  217. fi
  218. }
  219. parse_ovh() {
  220. local json_file="$1"
  221. if command -v jq &>/dev/null; then
  222. jq -r '.[]' "$json_file" 2>/dev/null | grep -vE '\.' | sort -u
  223. else
  224. grep -oE '"[a-z]{2,20}"' "$json_file" | tr -d '"' | grep -vE '\.' | sort -u
  225. fi
  226. }
  227. # ─── DomainOffer.net ────────────────────────────────────────────────────────
  228. fetch_domainoffer() {
  229. local cache="$CACHE_DIR/domainoffer.html"
  230. local max_age=86400
  231. if [[ -f "$cache" ]]; then
  232. local age=$(( $(date +%s) - $(stat -f%m "$cache" 2>/dev/null || stat -c%Y "$cache" 2>/dev/null || echo 0) ))
  233. if (( age < max_age )); then
  234. echo "$cache"
  235. return 0
  236. fi
  237. fi
  238. echo -e "${CYAN}Fetching DomainOffer.net price compare...${NC}" >&2
  239. if curl -sf "https://domainoffer.net/price-compare" \
  240. -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" \
  241. -o "$cache" 2>/dev/null; then
  242. echo "$cache"
  243. else
  244. echo -e "${YELLOW}Could not fetch DomainOffer.net${NC}" >&2
  245. return 1
  246. fi
  247. }
  248. parse_domainoffer() {
  249. local html_file="$1"
  250. local parsed_cache="$CACHE_DIR/domainoffer-tlds.txt"
  251. # Return cached parsed list if newer than HTML
  252. if [[ -f "$parsed_cache" && "$parsed_cache" -nt "$html_file" ]]; then
  253. cat "$parsed_cache"
  254. return
  255. fi
  256. local result=""
  257. if command -v python3 &>/dev/null; then
  258. result=$(python3 -c "
  259. import re, json, sys
  260. with open('$html_file') as f:
  261. html = f.read()
  262. m = re.search(r'var domainData = (\[.*?\]);', html, re.DOTALL)
  263. if not m:
  264. sys.exit(0)
  265. for entry in json.loads(m.group(1)):
  266. tld = entry[0].lstrip('.').lower() if isinstance(entry, list) and entry else ''
  267. if '.' not in tld and re.match(r'^[a-z][a-z0-9]*$', tld):
  268. print(tld)
  269. " | sort -u)
  270. else
  271. result=$(grep -oE '\["[a-z][a-z0-9]*",' "$html_file" | sed 's/\["//;s/",//' | sort -u)
  272. fi
  273. # Cache the parsed result
  274. echo "$result" > "$parsed_cache"
  275. echo "$result"
  276. }
  277. # ─── tld-list.com ───────────────────────────────────────────────────────────
  278. fetch_tldlist() {
  279. local cache="$CACHE_DIR/tldlist-basic.txt"
  280. local max_age=86400
  281. if [[ -f "$cache" ]]; then
  282. local age=$(( $(date +%s) - $(stat -f%m "$cache" 2>/dev/null || stat -c%Y "$cache" 2>/dev/null || echo 0) ))
  283. if (( age < max_age )); then
  284. echo "$cache"
  285. return 0
  286. fi
  287. fi
  288. echo -e "${CYAN}Fetching tld-list.com basic list...${NC}" >&2
  289. if curl -sf "https://tld-list.com/df/tld-list-basic.csv" \
  290. -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" \
  291. -o "$cache" 2>/dev/null; then
  292. echo "$cache"
  293. else
  294. echo -e "${YELLOW}Could not fetch tld-list.com${NC}" >&2
  295. return 1
  296. fi
  297. }
  298. parse_tldlist() {
  299. local file="$1"
  300. tr -d '\r' < "$file" | grep -E '^[a-z][a-z0-9]*$' | sort -u
  301. }
  302. # ─── IANA root zone ─────────────────────────────────────────────────────────
  303. fetch_iana() {
  304. local cache="$CACHE_DIR/iana-tlds.txt"
  305. local max_age=604800
  306. if [[ -f "$cache" ]]; then
  307. local age=$(( $(date +%s) - $(stat -f%m "$cache" 2>/dev/null || stat -c%Y "$cache" 2>/dev/null || echo 0) ))
  308. if (( age < max_age )); then
  309. echo "$cache"
  310. return 0
  311. fi
  312. fi
  313. echo -e "${CYAN}Fetching IANA TLD list...${NC}" >&2
  314. if curl -sf "https://data.iana.org/TLD/tlds-alpha-by-domain.txt" -o "$cache" 2>/dev/null; then
  315. echo "$cache"
  316. else
  317. echo -e "${RED}Failed to fetch IANA list${NC}" >&2
  318. return 1
  319. fi
  320. }
  321. parse_iana() {
  322. local file="$1"
  323. tail -n +2 "$file" | tr '[:upper:]' '[:lower:]' | sort -u
  324. }
  325. parse_iana_cctlds() {
  326. local file="$1"
  327. tail -n +2 "$file" | tr '[:upper:]' '[:lower:]' | grep -E '^[a-z]{2}$' | sort -u
  328. }
  329. # ─── RDAP bootstrap ─────────────────────────────────────────────────────────
  330. fetch_rdap() {
  331. local cache="$CACHE_DIR/rdap-dns.json"
  332. local max_age=86400
  333. if [[ -f "$cache" ]]; then
  334. local age=$(( $(date +%s) - $(stat -f%m "$cache" 2>/dev/null || stat -c%Y "$cache" 2>/dev/null || echo 0) ))
  335. if (( age < max_age )); then
  336. echo "$cache"
  337. return 0
  338. fi
  339. fi
  340. echo -e "${CYAN}Fetching RDAP bootstrap...${NC}" >&2
  341. if curl -sf "https://data.iana.org/rdap/dns.json" -o "$cache" 2>/dev/null; then
  342. echo "$cache"
  343. else
  344. echo -e "${RED}Failed to fetch RDAP bootstrap${NC}" >&2
  345. return 1
  346. fi
  347. }
  348. parse_rdap_tlds() {
  349. local json_file="$1"
  350. if command -v jq &>/dev/null; then
  351. jq -r '.services[][] | .[]' "$json_file" 2>/dev/null | grep -v '^http' | tr '[:upper:]' '[:lower:]' | sort -u
  352. else
  353. grep -oE '"[a-z]{2,20}"' "$json_file" | tr -d '"' | sort -u
  354. fi
  355. }
  356. # ─── WHOIS server list ──────────────────────────────────────────────────────
  357. fetch_whois_servers() {
  358. local cache="$CACHE_DIR/tld_serv_list.txt"
  359. local max_age=604800
  360. if [[ -f "$cache" ]]; then
  361. local age=$(( $(date +%s) - $(stat -f%m "$cache" 2>/dev/null || stat -c%Y "$cache" 2>/dev/null || echo 0) ))
  362. if (( age < max_age )); then
  363. echo "$cache"
  364. return 0
  365. fi
  366. fi
  367. echo -e "${CYAN}Fetching WHOIS server list...${NC}" >&2
  368. if curl -sf "https://raw.githubusercontent.com/rfc1036/whois/next/tld_serv_list" -o "$cache" 2>/dev/null; then
  369. echo "$cache"
  370. else
  371. echo -e "${YELLOW}Could not fetch WHOIS server list${NC}" >&2
  372. return 1
  373. fi
  374. }
  375. get_whois_server() {
  376. local tld="$1"
  377. local serv_file="$2"
  378. local line
  379. line=$(grep -E "^\\.${tld}[[:space:]]" "$serv_file" 2>/dev/null | head -1)
  380. if [[ -z "$line" ]]; then
  381. echo ""
  382. return
  383. fi
  384. local server
  385. server=$(echo "$line" | awk '{
  386. for (i=NF; i>=2; i--) {
  387. if ($i ~ /^[a-z0-9].*\.[a-z]/) { print $i; exit }
  388. }
  389. }')
  390. if [[ "$server" == "NONE" || "$server" == "ARPA" || -z "$server" || "$server" == http* ]]; then
  391. echo ""
  392. else
  393. echo "$server"
  394. fi
  395. }
  396. get_iana_whois_server() {
  397. local tld="$1"
  398. curl -s "https://www.iana.org/domains/root/db/${tld}.html" 2>/dev/null \
  399. | sed -n 's/.*WHOIS Server:<\/b> *\([^ <]*\).*/\1/p' \
  400. | head -1
  401. }
  402. # ═══════════════════════════════════════════════════════════════════════════
  403. # WHOIS Probe — try common server patterns for unknown TLDs
  404. # ═══════════════════════════════════════════════════════════════════════════
  405. probe_whois_server() {
  406. local tld="$1"
  407. local timeout_s="$2"
  408. local patterns_str="$3"
  409. # Split patterns on comma
  410. IFS=',' read -ra patterns <<< "$patterns_str"
  411. for pattern in "${patterns[@]}"; do
  412. pattern=$(echo "$pattern" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
  413. local server="${pattern//\{\}/$tld}"
  414. # Perl socket connect — Timeout covers both DNS resolution and TCP connect
  415. # unlike nc -w which only covers TCP and lets DNS hang forever on macOS
  416. if perl -e 'use IO::Socket::INET; exit(0) if IO::Socket::INET->new(PeerAddr=>$ARGV[0],PeerPort=>43,Timeout=>$ARGV[1]); exit(1)' "$server" "$timeout_s" 2>/dev/null; then
  417. echo "$server"
  418. return 0
  419. fi
  420. done
  421. return 1
  422. }
  423. # Worker function for parallel probing — writes result to a file and prints live
  424. _probe_worker() {
  425. local tld="$1" timeout_s="$2" patterns="$3" result_dir="$4" idx="$5" total="$6"
  426. local server
  427. if server=$(probe_whois_server "$tld" "$timeout_s" "$patterns"); then
  428. echo "${tld}:${server}" > "${result_dir}/${tld}.found"
  429. echo -e " [${idx}/${total}] ${GREEN}✓${NC} ${tld} → ${server}" >&2
  430. else
  431. touch "${result_dir}/${tld}.miss"
  432. echo -e " [${idx}/${total}] ${RED}✗${NC} ${tld}" >&2
  433. fi
  434. }
  435. run_whois_probes() {
  436. local sdom_file="$1"
  437. local pdom_file="$2"
  438. local probe_enabled
  439. probe_enabled=$(conf_get "whois_probe" "enabled" "true")
  440. if [[ "$probe_enabled" != "true" ]]; then
  441. echo -e " ${YELLOW}WHOIS probing disabled in config${NC}" >&2
  442. return
  443. fi
  444. local timeout_s
  445. timeout_s=$(conf_get "whois_probe" "timeout" "2")
  446. local patterns
  447. patterns=$(conf_get "whois_probe" "patterns" "whois.nic.{}, whois.{}, whois.registry.{}")
  448. local max_jobs
  449. max_jobs=$(conf_get "whois_probe" "parallel" "10")
  450. if [[ ! -f "$sdom_file" ]] || [[ ! -s "$sdom_file" ]]; then
  451. return
  452. fi
  453. # Count patterns for info
  454. local pattern_count=0
  455. IFS=',' read -ra _pats <<< "$patterns"
  456. pattern_count=${#_pats[@]}
  457. unset _pats
  458. local total
  459. total=$(wc -l < "$sdom_file" | tr -d ' ')
  460. echo -e " ${CYAN}Probing ${total} TLDs (${pattern_count} patterns, ${timeout_s}s timeout, ${max_jobs} parallel)${NC}" >&2
  461. # Use workdir for temp results so they're visible
  462. local result_dir="$WORK_DIR/probe-tmp"
  463. rm -rf "$result_dir"
  464. mkdir -p "$result_dir"
  465. # Read all TLDs into array
  466. local -a tld_list=()
  467. while IFS= read -r tld; do
  468. [[ -n "$tld" ]] && tld_list+=("$tld")
  469. done < "$sdom_file"
  470. # Launch all jobs with max_jobs concurrency
  471. # Each worker prints its own result immediately to stderr
  472. local running=0 idx=0
  473. for tld in "${tld_list[@]}"; do
  474. ((idx++)) || true
  475. _probe_worker "$tld" "$timeout_s" "$patterns" "$result_dir" "$idx" "$total" &
  476. ((running++)) || true
  477. # Throttle: when we hit max_jobs, wait for any one to finish
  478. if (( running >= max_jobs )); then
  479. wait -n 2>/dev/null || wait
  480. ((running--)) || true
  481. fi
  482. done
  483. # Wait for remaining
  484. wait
  485. # Collect results: append found to pdom, rebuild sdom
  486. local found=0
  487. local new_sdom=""
  488. for tld in "${tld_list[@]}"; do
  489. if [[ -f "${result_dir}/${tld}.found" ]]; then
  490. cat "${result_dir}/${tld}.found" >> "$pdom_file"
  491. ((found++)) || true
  492. else
  493. new_sdom+="${tld}"$'\n'
  494. fi
  495. done
  496. # Cleanup
  497. rm -rf "$result_dir"
  498. # Rewrite sdom with remaining unknowns
  499. echo -n "$new_sdom" | sort -u > "$sdom_file"
  500. if (( found > 0 )); then
  501. sort -u -o "$pdom_file" "$pdom_file"
  502. echo -e " ${GREEN}Probing complete: found ${found}/${total} WHOIS servers${NC}" >&2
  503. else
  504. echo -e " ${YELLOW}Probing complete: no new servers (0/${total})${NC}" >&2
  505. fi
  506. }
  507. # ═══════════════════════════════════════════════════════════════════════════
  508. # Helpers
  509. # ═══════════════════════════════════════════════════════════════════════════
  510. parse_current_lists() {
  511. local list_name="${1:-all}"
  512. if [[ ! -f "$LISTS_TOML" ]]; then
  513. echo -e "${RED}No Lists.toml found at $LISTS_TOML${NC}" >&2
  514. return 1
  515. fi
  516. awk -v list="$list_name" '
  517. $0 ~ "^"list" *= *\\[" { found=1; next }
  518. found && /^\]/ { exit }
  519. found && /^[[:space:]]*\[/ { exit }
  520. found {
  521. gsub(/["\t,]/, " ")
  522. n = split($0, parts, " ")
  523. for (i=1; i<=n; i++) {
  524. if (parts[i] != "") {
  525. sub(/:.*/, "", parts[i])
  526. print parts[i]
  527. }
  528. }
  529. }
  530. ' "$LISTS_TOML" | sort -u
  531. }
  532. to_toml_array() {
  533. local tlds=()
  534. while IFS= read -r tld; do
  535. [[ -z "$tld" ]] && continue
  536. tlds+=("$tld")
  537. done
  538. local line='\t'
  539. local first=true
  540. for tld in "${tlds[@]}"; do
  541. local entry="\"$tld\""
  542. if $first; then
  543. line+="$entry"
  544. first=false
  545. else
  546. local test_line="$line, $entry"
  547. if (( ${#test_line} > 78 )); then
  548. echo -e "$line,"
  549. line="\t$entry"
  550. else
  551. line+=", $entry"
  552. fi
  553. fi
  554. done
  555. [[ -n "$line" ]] && echo -e "$line,"
  556. }
  557. filter_cctlds() {
  558. grep -E '^[a-z]{2}$'
  559. }
  560. filter_short_tlds() {
  561. grep -E '^[a-z]{2,6}$'
  562. }
  563. SKIP_TLDS=""
  564. filter_skip() {
  565. if [[ -z "$SKIP_TLDS" ]]; then
  566. cat
  567. return
  568. fi
  569. local skip_pattern
  570. # trim whitespace and convert spaces to regex alternation
  571. skip_pattern=$(echo "$SKIP_TLDS" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr -s ' ' '|')
  572. if [[ -z "$skip_pattern" ]]; then
  573. cat
  574. return
  575. fi
  576. grep -vE "^($skip_pattern)$"
  577. }
  578. # ═══════════════════════════════════════════════════════════════════════════
  579. # Template generation (Lists.toml)
  580. # ═══════════════════════════════════════════════════════════════════════════
  581. generate_template() {
  582. local all_registrar_tlds="$1"
  583. local rdap_tlds="$2"
  584. local source_summary="$3"
  585. # Fetch WHOIS server list
  586. local whois_serv_file=""
  587. if whois_serv_file=$(fetch_whois_servers 2>/dev/null); then
  588. true
  589. fi
  590. # Load manual whois overrides from config
  591. local -A manual_whois=()
  592. while IFS=' ' read -r tld server; do
  593. [[ -z "$tld" ]] && continue
  594. manual_whois["$tld"]="$server"
  595. done < <(load_whois_overrides)
  596. local buyable_tlds
  597. buyable_tlds=$(echo "$all_registrar_tlds" | filter_skip | sort -u)
  598. local buyable_count
  599. buyable_count=$(echo "$buyable_tlds" | grep -c . || echo 0)
  600. # Build annotated TLD list: "tld" or "tld:whois_server"
  601. local annotated_all=()
  602. local annotated_cc=()
  603. local rdap_hit=0 whois_hit=0 bare_hit=0
  604. local pdom_entries=()
  605. local sdom_entries=()
  606. while IFS= read -r tld; do
  607. [[ -z "$tld" ]] && continue
  608. local entry=""
  609. # Check manual override first
  610. if [[ -n "${manual_whois[$tld]:-}" ]]; then
  611. entry="${tld}:${manual_whois[$tld]}"
  612. ((whois_hit++)) || true
  613. pdom_entries+=("$entry")
  614. elif echo "$rdap_tlds" | grep -qx "$tld" 2>/dev/null; then
  615. entry="$tld"
  616. ((rdap_hit++)) || true
  617. pdom_entries+=("$tld")
  618. else
  619. local server=""
  620. if [[ -n "$whois_serv_file" ]]; then
  621. server=$(get_whois_server "$tld" "$whois_serv_file")
  622. fi
  623. if [[ -n "$server" ]]; then
  624. entry="${tld}:${server}"
  625. ((whois_hit++)) || true
  626. pdom_entries+=("$entry")
  627. else
  628. entry="$tld"
  629. ((bare_hit++)) || true
  630. sdom_entries+=("$tld")
  631. fi
  632. fi
  633. annotated_all+=("$entry")
  634. local base_tld="${tld%%:*}"
  635. if [[ "$base_tld" =~ ^[a-z]{2}$ ]]; then
  636. annotated_cc+=("$entry")
  637. fi
  638. done <<< "$buyable_tlds"
  639. # Write pdom.txt and sdom.txt
  640. printf '%s\n' "${pdom_entries[@]}" | sort -u > "$PDOM_FILE"
  641. printf '%s\n' "${sdom_entries[@]}" | sort -u > "$SDOM_FILE"
  642. echo -e "${CYAN}Building template...${NC}" >&2
  643. echo -e " ${GREEN}${rdap_hit}${NC} TLDs with RDAP (direct lookup)" >&2
  644. echo -e " ${YELLOW}${whois_hit}${NC} TLDs with WHOIS override" >&2
  645. echo -e " ${RED}${bare_hit}${NC} TLDs with no known server (will probe)" >&2
  646. echo -e " ${CYAN}pdom.txt:${NC} $(wc -l < "$PDOM_FILE" | tr -d ' ') entries" >&2
  647. echo -e " ${CYAN}sdom.txt:${NC} $(wc -l < "$SDOM_FILE" | tr -d ' ') entries" >&2
  648. echo "" >&2
  649. # Run WHOIS probe on sdom entries if enabled
  650. if [[ "$DO_PROBE" == true ]]; then
  651. run_whois_probes "$SDOM_FILE" "$PDOM_FILE"
  652. # Reload pdom into annotated_all (probed ones now have servers)
  653. if [[ -s "$PDOM_FILE" ]]; then
  654. local probed_tlds
  655. probed_tlds=$(cat "$PDOM_FILE")
  656. # Rebuild annotated arrays with probed data
  657. annotated_all=()
  658. annotated_cc=()
  659. while IFS= read -r entry; do
  660. [[ -z "$entry" ]] && continue
  661. annotated_all+=("$entry")
  662. local base="${entry%%:*}"
  663. if [[ "$base" =~ ^[a-z]{2}$ ]]; then
  664. annotated_cc+=("$entry")
  665. fi
  666. done <<< "$probed_tlds"
  667. # Add remaining sdom entries (no server found) unless --strict
  668. if [[ -s "$SDOM_FILE" && "$STRICT" != true ]]; then
  669. while IFS= read -r tld; do
  670. [[ -z "$tld" ]] && continue
  671. annotated_all+=("$tld")
  672. if [[ "$tld" =~ ^[a-z]{2}$ ]]; then
  673. annotated_cc+=("$tld")
  674. fi
  675. done < "$SDOM_FILE"
  676. fi
  677. fi
  678. fi
  679. # --strict without --probe: filter out sdom entries from annotated arrays
  680. if [[ "$STRICT" == true && "$DO_PROBE" != true && -s "$SDOM_FILE" ]]; then
  681. local -A sdom_set=()
  682. while IFS= read -r tld; do
  683. [[ -n "$tld" ]] && sdom_set[$tld]=1
  684. done < "$SDOM_FILE"
  685. local filtered_all=() filtered_cc=()
  686. for ann in "${annotated_all[@]}"; do
  687. local base="${ann%%:*}"
  688. if [[ -z "${sdom_set[$base]:-}" ]]; then
  689. filtered_all+=("$ann")
  690. if [[ "$base" =~ ^[a-z]{2}$ ]]; then
  691. filtered_cc+=("$ann")
  692. fi
  693. fi
  694. done
  695. local stripped=$(( ${#annotated_all[@]} - ${#filtered_all[@]} ))
  696. annotated_all=("${filtered_all[@]}")
  697. annotated_cc=("${filtered_cc[@]}")
  698. echo -e " ${YELLOW}--strict:${NC} removed $stripped TLDs with no working server" >&2
  699. fi
  700. # ── Build list output from config ──────────────────────────────────
  701. # Annotate a curated tld list with whois overrides
  702. annotate_tld() {
  703. local bare_tld="$1"
  704. for ann in "${annotated_all[@]}"; do
  705. local ann_base="${ann%%:*}"
  706. if [[ "$ann_base" == "$bare_tld" ]]; then
  707. echo "$ann"
  708. return
  709. fi
  710. done
  711. echo "$bare_tld"
  712. }
  713. filter_annotated_by_length() {
  714. local min="$1" max="$2"
  715. for ann in "${annotated_all[@]}"; do
  716. local base="${ann%%:*}"
  717. local len=${#base}
  718. if (( len >= min && len <= max )); then
  719. echo "$ann"
  720. fi
  721. done
  722. }
  723. filter_annotated_cctlds() {
  724. for ann in "${annotated_all[@]}"; do
  725. local base="${ann%%:*}"
  726. if [[ "$base" =~ ^[a-z]{2}$ ]]; then
  727. echo "$ann"
  728. fi
  729. done
  730. }
  731. # ── Output Lists.toml ──────────────────────────────────────────────
  732. local date_str
  733. date_str=$(date +%Y-%m-%d)
  734. # Build description comments from config
  735. local list_descriptions=""
  736. while IFS= read -r name; do
  737. local desc
  738. desc=$(conf_get "list.${name}" "description" "$name")
  739. list_descriptions+="# ${name}$(printf '%*s' $((10 - ${#name})) '')— ${desc}"$'\n'
  740. done < <(conf_list_names)
  741. cat <<HEADER
  742. # Lists.toml — Built-in TLD lists for hoardom
  743. # Auto-generated on ${date_str} from ${source_summary}
  744. #
  745. # Format:
  746. # "tld" — TLD has RDAP support, lookup works directly
  747. # "tld:whois.server" — No RDAP: use this WHOIS server for fallback
  748. #
  749. # ${buyable_count} purchasable TLDs (handshake/sub-TLDs excluded)
  750. # ${rdap_hit} have RDAP, ${whois_hit} need WHOIS override, ${bare_hit} will auto-probe
  751. #
  752. # Lists:
  753. ${list_descriptions}
  754. HEADER
  755. # Generate each list from config
  756. while IFS= read -r name; do
  757. local type
  758. type=$(conf_get "list.${name}" "type" "curated")
  759. case "$type" in
  760. curated)
  761. local tlds_str
  762. tlds_str=$(conf_list_tlds "$name")
  763. echo "${name} = ["
  764. for bare_tld in $tlds_str; do
  765. annotate_tld "$bare_tld"
  766. done | to_toml_array
  767. echo "]"
  768. echo ""
  769. ;;
  770. filter)
  771. local min_len max_len country_only
  772. min_len=$(conf_get "list.${name}" "min_length" "2")
  773. max_len=$(conf_get "list.${name}" "max_length" "99")
  774. country_only=$(conf_get "list.${name}" "country_only" "false")
  775. echo "${name} = ["
  776. if [[ "$country_only" == "true" ]]; then
  777. filter_annotated_cctlds | to_toml_array
  778. else
  779. filter_annotated_by_length "$min_len" "$max_len" | to_toml_array
  780. fi
  781. echo "]"
  782. echo ""
  783. ;;
  784. all)
  785. echo "${name} = ["
  786. printf '%s\n' "${annotated_all[@]}" | to_toml_array
  787. echo "]"
  788. echo ""
  789. ;;
  790. esac
  791. done < <(conf_list_names)
  792. }
  793. # ═══════════════════════════════════════════════════════════════════════════
  794. # Main
  795. # ═══════════════════════════════════════════════════════════════════════════
  796. main() {
  797. local mode="summary"
  798. local source="all"
  799. local all_sources=false
  800. DO_PROBE=false
  801. STRICT=false
  802. # Load skip TLDs from config
  803. SKIP_TLDS=$(load_skip_tlds)
  804. for arg in "$@"; do
  805. case "$arg" in
  806. --raw) mode="raw" ;;
  807. --toml) mode="toml" ;;
  808. --diff) mode="diff" ;;
  809. --template) mode="template" ;;
  810. --probe) DO_PROBE=true ;;
  811. --strict) STRICT=true ;;
  812. --all-sources) all_sources=true ;;
  813. porkbun) source="porkbun" ;;
  814. inwx) source="inwx" ;;
  815. ovh) source="ovh" ;;
  816. domainoffer) source="domainoffer" ;;
  817. iana) source="iana" ;;
  818. rdap) source="rdap" ;;
  819. tldlist) source="tldlist" ;;
  820. --help|-h)
  821. echo "Usage: $0 [source] [--raw|--toml|--diff|--template] [--probe] [--strict] [--all-sources]"
  822. echo ""
  823. echo "Sources: porkbun, ovh, inwx, domainoffer, iana, rdap, tldlist"
  824. echo ""
  825. echo "Flags:"
  826. echo " --raw Output raw TLD list (one per line)"
  827. echo " --toml Output TOML-ready arrays"
  828. echo " --diff Compare against current Lists.toml"
  829. echo " --template Generate Lists.toml into violator-workdir/"
  830. echo " --probe Probe unknown TLDs for WHOIS servers"
  831. echo " --strict Remove TLDs with no working server from output"
  832. echo " --all-sources Include tld-list.com for extra coverage"
  833. echo ""
  834. echo "Config: $CONF_FILE"
  835. echo "Workdir: $WORK_DIR"
  836. exit 0 ;;
  837. esac
  838. done
  839. local porkbun_tlds="" inwx_tlds="" ovh_tlds="" domainoffer_tlds="" iana_tlds="" rdap_tlds="" tldlist_tlds=""
  840. local porkbun_count=0 inwx_count=0 ovh_count=0 domainoffer_count=0 iana_count=0 rdap_count=0 tldlist_count=0
  841. # Template mode needs all sources
  842. if [[ "$mode" == "template" ]]; then
  843. source="all"
  844. fi
  845. # ── Fetch from selected sources ──
  846. if [[ "$source" == "all" || "$source" == "porkbun" ]]; then
  847. if porkbun_file=$(fetch_porkbun); then
  848. porkbun_tlds=$(parse_porkbun "$porkbun_file")
  849. porkbun_count=$(echo "$porkbun_tlds" | grep -c . || true)
  850. fi
  851. fi
  852. if [[ "$source" == "all" || "$source" == "ovh" ]]; then
  853. if ovh_file=$(fetch_ovh); then
  854. ovh_tlds=$(parse_ovh "$ovh_file")
  855. ovh_count=$(echo "$ovh_tlds" | grep -c . || true)
  856. fi
  857. fi
  858. if [[ "$source" == "all" || "$source" == "inwx" ]]; then
  859. if inwx_file=$(fetch_inwx 2>/dev/null); then
  860. inwx_tlds=$(parse_inwx "$inwx_file")
  861. inwx_count=$(echo "$inwx_tlds" | grep -c . || true)
  862. fi
  863. fi
  864. if [[ "$source" == "all" || "$source" == "domainoffer" ]]; then
  865. if domainoffer_file=$(fetch_domainoffer 2>/dev/null); then
  866. domainoffer_tlds=$(parse_domainoffer "$domainoffer_file")
  867. domainoffer_count=$(echo "$domainoffer_tlds" | grep -c . || true)
  868. fi
  869. fi
  870. if [[ "$source" == "all" || "$source" == "iana" ]]; then
  871. if iana_file=$(fetch_iana); then
  872. iana_tlds=$(parse_iana "$iana_file")
  873. iana_count=$(echo "$iana_tlds" | grep -c . || true)
  874. fi
  875. fi
  876. if [[ "$source" == "all" || "$source" == "rdap" ]]; then
  877. if rdap_file=$(fetch_rdap); then
  878. rdap_tlds=$(parse_rdap_tlds "$rdap_file")
  879. rdap_count=$(echo "$rdap_tlds" | grep -c . || true)
  880. fi
  881. fi
  882. if [[ "$all_sources" == true || "$source" == "tldlist" ]]; then
  883. if tldlist_file=$(fetch_tldlist); then
  884. tldlist_tlds=$(parse_tldlist "$tldlist_file")
  885. tldlist_count=$(echo "$tldlist_tlds" | grep -c . || true)
  886. fi
  887. fi
  888. # ── Filter porkbun: no handshake, no sub-TLDs ──
  889. local porkbun_filtered=""
  890. if [[ -n "$porkbun_tlds" ]]; then
  891. local porkbun_file="$CACHE_DIR/porkbun.json"
  892. if command -v jq &>/dev/null && [[ -f "$porkbun_file" ]]; then
  893. porkbun_filtered=$(jq -r '
  894. .pricing // {} | to_entries[] |
  895. select(.key | contains(".") | not) |
  896. select(.value.specialType // "" | test("handshake") | not) |
  897. .key
  898. ' "$porkbun_file" 2>/dev/null | sort -u)
  899. else
  900. porkbun_filtered=$(echo "$porkbun_tlds" | grep -v '\.' | sort -u)
  901. fi
  902. fi
  903. # ── Merge all registrar TLDs ──
  904. local registrar_tlds
  905. registrar_tlds=$(echo -e "${porkbun_filtered}\n${ovh_tlds}\n${inwx_tlds}\n${domainoffer_tlds}" | grep -E '^[a-z]' | sort -u | filter_skip)
  906. if [[ "$all_sources" == true && -n "$tldlist_tlds" ]]; then
  907. local tldlist_extra
  908. tldlist_extra=$(comm -23 <(echo "$tldlist_tlds") <(echo "$registrar_tlds") 2>/dev/null || true)
  909. local extra_count
  910. extra_count=$(echo "$tldlist_extra" | grep -c . || echo 0)
  911. echo -e " ${YELLOW}tld-list.com:${NC} $extra_count TLDs with no registrar pricing (excluded)" >&2
  912. fi
  913. local all_tlds="$registrar_tlds"
  914. # Also include IANA ccTLDs with RDAP/WHOIS
  915. if [[ -n "$iana_tlds" ]]; then
  916. local iana_cctlds
  917. iana_cctlds=$(echo "$iana_tlds" | filter_cctlds | filter_skip)
  918. local whois_serv_file_extra=""
  919. if [[ -f "$CACHE_DIR/tld_serv_list.txt" ]]; then
  920. whois_serv_file_extra="$CACHE_DIR/tld_serv_list.txt"
  921. elif whois_serv_file_extra=$(fetch_whois_servers 2>/dev/null); then
  922. true
  923. fi
  924. local iana_extra=0
  925. while IFS= read -r cctld; do
  926. [[ -z "$cctld" ]] && continue
  927. if echo "$registrar_tlds" | grep -qx "$cctld" 2>/dev/null; then
  928. continue
  929. fi
  930. if echo "$rdap_tlds" | grep -qx "$cctld" 2>/dev/null; then
  931. all_tlds=$(echo -e "${all_tlds}\n${cctld}")
  932. ((iana_extra++)) || true
  933. continue
  934. fi
  935. if [[ -n "$whois_serv_file_extra" ]]; then
  936. local srv
  937. srv=$(get_whois_server "$cctld" "$whois_serv_file_extra")
  938. if [[ -n "$srv" ]]; then
  939. all_tlds=$(echo -e "${all_tlds}\n${cctld}")
  940. ((iana_extra++)) || true
  941. fi
  942. fi
  943. done <<< "$iana_cctlds"
  944. all_tlds=$(echo "$all_tlds" | sort -u)
  945. if (( iana_extra > 0 )); then
  946. echo -e " ${CYAN}IANA adds${NC} $iana_extra ccTLDs with RDAP/WHOIS not at any registrar" >&2
  947. fi
  948. fi
  949. local all_cctlds
  950. all_cctlds=$(echo "$all_tlds" | filter_cctlds)
  951. # Always generate pdom.txt / sdom.txt (even outside template mode)
  952. generate_pdom_sdom() {
  953. local whois_serv_file=""
  954. if whois_serv_file=$(fetch_whois_servers 2>/dev/null); then true; fi
  955. local -A manual_whois=()
  956. while IFS=' ' read -r tld server; do
  957. [[ -z "$tld" ]] && continue
  958. manual_whois["$tld"]="$server"
  959. done < <(load_whois_overrides)
  960. local pdom_list=() sdom_list=()
  961. while IFS= read -r tld; do
  962. [[ -z "$tld" ]] && continue
  963. if [[ -n "${manual_whois[$tld]:-}" ]]; then
  964. pdom_list+=("${tld}:${manual_whois[$tld]}")
  965. elif echo "$rdap_tlds" | grep -qx "$tld" 2>/dev/null; then
  966. pdom_list+=("$tld")
  967. else
  968. local server=""
  969. if [[ -n "$whois_serv_file" ]]; then
  970. server=$(get_whois_server "$tld" "$whois_serv_file")
  971. fi
  972. if [[ -n "$server" ]]; then
  973. pdom_list+=("${tld}:${server}")
  974. else
  975. sdom_list+=("$tld")
  976. fi
  977. fi
  978. done <<< "$all_tlds"
  979. printf '%s\n' "${pdom_list[@]}" | sort -u > "$PDOM_FILE"
  980. if (( ${#sdom_list[@]} > 0 )); then
  981. printf '%s\n' "${sdom_list[@]}" | sort -u > "$SDOM_FILE"
  982. else
  983. > "$SDOM_FILE"
  984. fi
  985. # Run probes on sdom if enabled
  986. if [[ "$DO_PROBE" == true ]]; then
  987. run_whois_probes "$SDOM_FILE" "$PDOM_FILE"
  988. fi
  989. }
  990. # Build source summary
  991. local sources_used=()
  992. [[ $porkbun_count -gt 0 ]] && sources_used+=("Porkbun")
  993. [[ $ovh_count -gt 0 ]] && sources_used+=("OVH")
  994. [[ $inwx_count -gt 0 ]] && sources_used+=("INWX")
  995. [[ $domainoffer_count -gt 0 ]] && sources_used+=("DomainOffer")
  996. local source_summary joined
  997. joined=$(printf " + %s" "${sources_used[@]}")
  998. joined="${joined:3}"
  999. source_summary="${joined} + RDAP bootstrap + WHOIS server list"
  1000. case "$mode" in
  1001. raw)
  1002. generate_pdom_sdom
  1003. echo "$all_tlds"
  1004. ;;
  1005. toml)
  1006. generate_pdom_sdom
  1007. echo -e "${BOLD}# Purchasable TLDs from all registrars ($(echo "$all_tlds" | wc -l | tr -d ' ') total)${NC}"
  1008. echo "all_registrars = ["
  1009. echo "$all_tlds" | to_toml_array
  1010. echo "]"
  1011. echo ""
  1012. echo "# Country-code TLDs (purchasable)"
  1013. echo "cctlds = ["
  1014. echo "$all_cctlds" | to_toml_array
  1015. echo "]"
  1016. ;;
  1017. diff)
  1018. generate_pdom_sdom
  1019. echo -e "${BOLD}Comparing registrar data vs current Lists.toml${NC}"
  1020. echo ""
  1021. local current_all current_country
  1022. current_all=$(parse_current_lists "all")
  1023. current_country=$(parse_current_lists "country")
  1024. if [[ -n "$all_tlds" ]]; then
  1025. local missing_from_all
  1026. missing_from_all=$(comm -23 <(echo "$all_tlds" | filter_short_tlds | sort) <(echo "$current_all" | sort) 2>/dev/null || true)
  1027. if [[ -n "$missing_from_all" ]]; then
  1028. local mc
  1029. mc=$(echo "$missing_from_all" | wc -l | tr -d ' ')
  1030. echo -e "${YELLOW}TLDs at registrars but NOT in our 'all' list ($mc):${NC}"
  1031. echo "$missing_from_all" | tr '\n' ' '
  1032. echo "" && echo ""
  1033. fi
  1034. local missing_cc
  1035. missing_cc=$(comm -23 <(echo "$all_cctlds" | sort) <(echo "$current_country" | sort) 2>/dev/null || true)
  1036. if [[ -n "$missing_cc" ]]; then
  1037. local mcc
  1038. mcc=$(echo "$missing_cc" | wc -l | tr -d ' ')
  1039. echo -e "${YELLOW}ccTLDs at registrars but NOT in 'country' list ($mcc):${NC}"
  1040. echo "$missing_cc" | tr '\n' ' '
  1041. echo "" && echo ""
  1042. fi
  1043. local extra
  1044. extra=$(comm -13 <(echo "$all_tlds" | sort) <(echo "$current_all" | sort) 2>/dev/null || true)
  1045. if [[ -n "$extra" ]]; then
  1046. local ec
  1047. ec=$(echo "$extra" | wc -l | tr -d ' ')
  1048. echo -e "${CYAN}TLDs in our 'all' list but NOT at any registrar ($ec):${NC}"
  1049. echo "$extra" | tr '\n' ' '
  1050. echo "" && echo ""
  1051. fi
  1052. fi
  1053. if [[ -n "$rdap_tlds" && -n "$current_all" ]]; then
  1054. local no_rdap
  1055. no_rdap=$(comm -23 <(echo "$current_all" | sort) <(echo "$rdap_tlds" | sort) 2>/dev/null || true)
  1056. if [[ -n "$no_rdap" ]]; then
  1057. local nrc
  1058. nrc=$(echo "$no_rdap" | wc -l | tr -d ' ')
  1059. echo -e "${RED}TLDs in our lists with NO RDAP server ($nrc) — need WHOIS fallback:${NC}"
  1060. echo "$no_rdap" | tr '\n' ' '
  1061. echo ""
  1062. fi
  1063. fi
  1064. ;;
  1065. template)
  1066. generate_template "$registrar_tlds" "$rdap_tlds" "$source_summary" > "$OUTPUT_TOML"
  1067. echo -e " ${GREEN}Lists.toml written to:${NC} ${OUTPUT_TOML}" >&2
  1068. echo -e " ${GREEN}pdom.txt:${NC} $(wc -l < "$PDOM_FILE" | tr -d ' ') purchasable TLDs with servers" >&2
  1069. echo -e " ${GREEN}sdom.txt:${NC} $(wc -l < "$SDOM_FILE" | tr -d ' ') TLDs with no known server" >&2
  1070. ;;
  1071. summary)
  1072. generate_pdom_sdom
  1073. echo -e "${BOLD}═══ TLD Source Summary ═══${NC}"
  1074. echo ""
  1075. [[ $porkbun_count -gt 0 ]] && echo -e " ${GREEN}Porkbun${NC} $(echo "$porkbun_filtered" | grep -c . || echo 0) TLDs ($(echo "$porkbun_filtered" | filter_cctlds | wc -l | tr -d ' ') ccTLDs)"
  1076. [[ $ovh_count -gt 0 ]] && echo -e " ${GREEN}OVH${NC} $ovh_count TLDs ($(echo "$ovh_tlds" | filter_cctlds | wc -l | tr -d ' ') ccTLDs)"
  1077. [[ $inwx_count -gt 0 ]] && echo -e " ${GREEN}INWX${NC} $inwx_count TLDs ($(echo "$inwx_tlds" | filter_cctlds | wc -l | tr -d ' ') ccTLDs)"
  1078. [[ $domainoffer_count -gt 0 ]] && echo -e " ${GREEN}DomainOffer${NC} $domainoffer_count TLDs ($(echo "$domainoffer_tlds" | filter_cctlds | wc -l | tr -d ' ') ccTLDs)"
  1079. [[ $tldlist_count -gt 0 ]] && echo -e " ${GREEN}tld-list.com${NC} $tldlist_count TLDs (community registry)"
  1080. [[ $iana_count -gt 0 ]] && echo -e " ${GREEN}IANA${NC} $iana_count TLDs"
  1081. [[ $rdap_count -gt 0 ]] && echo -e " ${GREEN}RDAP${NC} $rdap_count TLDs with lookup servers"
  1082. echo ""
  1083. if [[ $porkbun_count -gt 0 && $ovh_count -gt 0 ]]; then
  1084. local ovh_unique inwx_unique domainoffer_unique
  1085. ovh_unique=$(comm -23 <(echo "$ovh_tlds" | sort) <(echo "$porkbun_filtered" | sort) | wc -l | tr -d ' ')
  1086. echo -e " ${CYAN}OVH adds${NC} $ovh_unique TLDs not on Porkbun"
  1087. if [[ $inwx_count -gt 0 ]]; then
  1088. inwx_unique=$(comm -23 <(echo "$inwx_tlds" | sort) <(echo -e "${porkbun_filtered}\n${ovh_tlds}" | sort -u) | wc -l | tr -d ' ')
  1089. echo -e " ${CYAN}INWX adds${NC} $inwx_unique TLDs not on Porkbun/OVH"
  1090. fi
  1091. if [[ $domainoffer_count -gt 0 ]]; then
  1092. domainoffer_unique=$(comm -23 <(echo "$domainoffer_tlds" | sort) <(echo -e "${porkbun_filtered}\n${ovh_tlds}\n${inwx_tlds}" | sort -u) | wc -l | tr -d ' ')
  1093. echo -e " ${CYAN}DomainOffer adds${NC} $domainoffer_unique TLDs not on Porkbun/OVH/INWX"
  1094. fi
  1095. echo ""
  1096. fi
  1097. echo -e " ${BOLD}Merged purchasable:${NC} $(echo "$all_tlds" | wc -l | tr -d ' ') TLDs"
  1098. echo -e " ${BOLD}Merged ccTLDs:${NC} $(echo "$all_cctlds" | wc -l | tr -d ' ')"
  1099. echo -e " ${BOLD}pdom.txt:${NC} $(wc -l < "$PDOM_FILE" | tr -d ' ') with known servers"
  1100. echo -e " ${BOLD}sdom.txt:${NC} $(wc -l < "$SDOM_FILE" | tr -d ' ') with no known server"
  1101. echo ""
  1102. echo -e " Workdir: ${CYAN}$WORK_DIR${NC}"
  1103. echo -e " Config: ${CYAN}$CONF_FILE${NC}"
  1104. echo -e " Use ${BOLD}--diff${NC} to compare against Lists.toml"
  1105. echo -e " Use ${BOLD}--toml${NC} to output TOML-ready arrays"
  1106. echo -e " Use ${BOLD}--template${NC} to generate Lists.toml into workdir"
  1107. echo -e " Use ${BOLD}--probe${NC} to probe unknown TLDs for WHOIS servers"
  1108. echo -e " Use ${BOLD}--all-sources${NC} to also fetch tld-list.com"
  1109. echo -e " Use ${BOLD}--raw${NC} for raw TLD list (one per line)"
  1110. ;;
  1111. esac
  1112. }
  1113. main "$@"