domainhack 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- domainhack/__init__.py +5 -0
- domainhack/__main__.py +5 -0
- domainhack/adapters/__init__.py +0 -0
- domainhack/adapters/_circuit.py +105 -0
- domainhack/adapters/_http.py +22 -0
- domainhack/adapters/_registration.py +128 -0
- domainhack/adapters/_stderr.py +16 -0
- domainhack/adapters/_text_sink.py +33 -0
- domainhack/adapters/_throttle.py +150 -0
- domainhack/adapters/brand_data.py +49 -0
- domainhack/adapters/brand_report_render.py +222 -0
- domainhack/adapters/cached_registrar.py +340 -0
- domainhack/adapters/composite_writer.py +27 -0
- domainhack/adapters/console_writer.py +77 -0
- domainhack/adapters/csv_writer.py +79 -0
- domainhack/adapters/dns_confirming_writer.py +72 -0
- domainhack/adapters/dns_resolver.py +120 -0
- domainhack/adapters/file_word_source.py +51 -0
- domainhack/adapters/iana_tlds.py +110 -0
- domainhack/adapters/json_writer.py +51 -0
- domainhack/adapters/pacing.py +46 -0
- domainhack/adapters/rdap_bootstrap.py +223 -0
- domainhack/adapters/rdap_registrar.py +328 -0
- domainhack/adapters/registrar_catalog.py +120 -0
- domainhack/adapters/registrar_router.py +72 -0
- domainhack/adapters/registry_sources.py +192 -0
- domainhack/adapters/tqdm_progress.py +101 -0
- domainhack/adapters/whois_registrar.py +386 -0
- domainhack/cli/__init__.py +0 -0
- domainhack/cli/app.py +800 -0
- domainhack/cli/name_cmd.py +222 -0
- domainhack/cli/tlds_cmd.py +96 -0
- domainhack/data/hack_words.json +20 -0
- domainhack/data/iana_tlds.txt +1438 -0
- domainhack/data/rdap_dns.json +1 -0
- domainhack/data/registry_sources.json +2367 -0
- domainhack/data/second_level.json +28 -0
- domainhack/data/tld_presets.json +7 -0
- domainhack/domain/__init__.py +0 -0
- domainhack/domain/entities.py +287 -0
- domainhack/domain/label_rules.py +96 -0
- domainhack/domain/parking.py +81 -0
- domainhack/ports/__init__.py +0 -0
- domainhack/ports/dns_lookup.py +11 -0
- domainhack/ports/known_tlds.py +21 -0
- domainhack/ports/progress.py +32 -0
- domainhack/ports/registrar.py +27 -0
- domainhack/ports/result_cache.py +19 -0
- domainhack/ports/result_writer.py +13 -0
- domainhack/ports/word_source.py +9 -0
- domainhack/py.typed +0 -0
- domainhack/usecases/__init__.py +0 -0
- domainhack/usecases/brand_candidates.py +195 -0
- domainhack/usecases/brand_report.py +271 -0
- domainhack/usecases/check_domains.py +287 -0
- domainhack/usecases/confirm_dns.py +64 -0
- domainhack/usecases/estimate_run.py +80 -0
- domainhack/usecases/filter_words.py +37 -0
- domainhack/usecases/generate_range.py +107 -0
- domainhack/usecases/lanes.py +193 -0
- domainhack/usecases/rank_candidates.py +106 -0
- domainhack-0.2.0.dist-info/METADATA +501 -0
- domainhack-0.2.0.dist-info/RECORD +67 -0
- domainhack-0.2.0.dist-info/WHEEL +5 -0
- domainhack-0.2.0.dist-info/entry_points.txt +2 -0
- domainhack-0.2.0.dist-info/licenses/LICENSE +21 -0
- domainhack-0.2.0.dist-info/top_level.txt +1 -0
domainhack/cli/app.py
ADDED
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import codecs
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Hashable, Iterable, Sequence
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Protocol
|
|
9
|
+
|
|
10
|
+
from domainhack import __version__
|
|
11
|
+
from domainhack.adapters._circuit import HostCircuitBreaker
|
|
12
|
+
from domainhack.adapters.cached_registrar import (
|
|
13
|
+
AVAILABLE_TTL_SECONDS,
|
|
14
|
+
CachedRegistrarClient,
|
|
15
|
+
CacheTtlPolicy,
|
|
16
|
+
)
|
|
17
|
+
from domainhack.adapters.composite_writer import CompositeResultWriter
|
|
18
|
+
from domainhack.adapters.console_writer import ConsoleResultWriter
|
|
19
|
+
from domainhack.adapters.csv_writer import CsvResultWriter
|
|
20
|
+
from domainhack.adapters.dns_confirming_writer import DnsConfirmingWriter
|
|
21
|
+
from domainhack.adapters.dns_resolver import DnsPythonLookup
|
|
22
|
+
from domainhack.adapters.file_word_source import DEFAULT_ENCODING, FileWordSource, WordListError
|
|
23
|
+
from domainhack.adapters.json_writer import JsonResultWriter
|
|
24
|
+
from domainhack.adapters.pacing import lane_for, pacing_for
|
|
25
|
+
from domainhack.adapters.registrar_catalog import build_registrar_for
|
|
26
|
+
from domainhack.adapters.registrar_router import RegistrarFactory, RegistrarRouter
|
|
27
|
+
from domainhack.adapters.registry_sources import describe_unsupported
|
|
28
|
+
from domainhack.adapters.tqdm_progress import TqdmProgressReporter
|
|
29
|
+
from domainhack.domain.entities import TLD, DomainHack
|
|
30
|
+
from domainhack.ports.dns_lookup import DnsLookup
|
|
31
|
+
from domainhack.ports.progress import NullProgressReporter, ProgressReporter
|
|
32
|
+
from domainhack.ports.registrar import RegistrarClient
|
|
33
|
+
from domainhack.ports.result_writer import ResultWriter
|
|
34
|
+
from domainhack.usecases.check_domains import (
|
|
35
|
+
DEFAULT_PARALLEL,
|
|
36
|
+
CheckDomainsUseCase,
|
|
37
|
+
CheckSummary,
|
|
38
|
+
LaneKey,
|
|
39
|
+
)
|
|
40
|
+
from domainhack.usecases.confirm_dns import ConfirmWithDns
|
|
41
|
+
from domainhack.usecases.estimate_run import (
|
|
42
|
+
GUARDRAIL_MAX_QUERIES,
|
|
43
|
+
Pacing,
|
|
44
|
+
RunEstimate,
|
|
45
|
+
estimate_run,
|
|
46
|
+
format_duration,
|
|
47
|
+
)
|
|
48
|
+
from domainhack.usecases.filter_words import FilterWordsUseCase
|
|
49
|
+
from domainhack.usecases.generate_range import (
|
|
50
|
+
MAX_RANGE_LENGTH,
|
|
51
|
+
RangeCandidatesUseCase,
|
|
52
|
+
RangeWordSource,
|
|
53
|
+
)
|
|
54
|
+
from domainhack.usecases.rank_candidates import CandidateOrder, RankCandidatesUseCase
|
|
55
|
+
|
|
56
|
+
if TYPE_CHECKING:
|
|
57
|
+
from domainhack.cli.name_cmd import NameServices
|
|
58
|
+
|
|
59
|
+
OUTPUT_FORMATS = ("csv", "json")
|
|
60
|
+
_FORMAT_BY_SUFFIX = {".csv": "csv", ".json": "json", ".jsonl": "json"}
|
|
61
|
+
|
|
62
|
+
EXIT_OK = 0
|
|
63
|
+
EXIT_FAILURE = 1
|
|
64
|
+
EXIT_USAGE = 2
|
|
65
|
+
EXIT_INTERRUPTED = 130 # 128 + SIGINT, the shell convention
|
|
66
|
+
|
|
67
|
+
CONTACT_ENV = "DOMAINHACK_CONTACT"
|
|
68
|
+
|
|
69
|
+
_EPILOG = f"""\
|
|
70
|
+
exit status:
|
|
71
|
+
{EXIT_OK} completed and every check succeeded
|
|
72
|
+
{EXIT_FAILURE} completed but some checks ended in ERROR, or the run failed
|
|
73
|
+
(unreadable word list, unwritable output, no supported TLD...)
|
|
74
|
+
{EXIT_USAGE} usage error (bad option or value)
|
|
75
|
+
{EXIT_INTERRUPTED} interrupted (Ctrl-C); results checked so far are kept
|
|
76
|
+
|
|
77
|
+
Results go to stdout; errors, warnings, progress and the summary go to stderr.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
_RANGE_END_RE = re.compile(r"[a-z]+")
|
|
81
|
+
_EMAIL_RE = re.compile(r"[^@\s]+@[^@\s]+\.[^@\s]+")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class RegistrarCatalog(Protocol):
|
|
85
|
+
"""Builds the registrar client for one TLD, or None when unsupported.
|
|
86
|
+
|
|
87
|
+
The production catalog is ``build_registrar_for``; ``main`` and
|
|
88
|
+
``cmd_check`` accept another one (tests pass a fake).
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __call__(
|
|
92
|
+
self,
|
|
93
|
+
tld: TLD,
|
|
94
|
+
*,
|
|
95
|
+
delay: float,
|
|
96
|
+
breaker: HostCircuitBreaker | None,
|
|
97
|
+
contact: str | None,
|
|
98
|
+
) -> RegistrarClient | None: ...
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class CliError(Exception):
|
|
102
|
+
"""A runtime failure reported as a single ``error:`` line (exit status 1)."""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class OutputFormatError(ValueError):
|
|
106
|
+
"""The output file format could not be determined (a usage error)."""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class NoSupportedTLDError(CliError):
|
|
110
|
+
"""None of the requested TLDs has a registrar able to check it."""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def parse_tld_list(value: str) -> list[TLD]:
|
|
114
|
+
"""Parse ``"to,io,com.ar"`` into TLDs: lowercased, deduplicated, order preserved.
|
|
115
|
+
|
|
116
|
+
Raises ``argparse.ArgumentTypeError`` (an argparse usage error) when the list
|
|
117
|
+
is empty or any suffix is not a valid TLD.
|
|
118
|
+
"""
|
|
119
|
+
tlds: list[TLD] = []
|
|
120
|
+
for raw in value.split(","):
|
|
121
|
+
suffix = raw.strip().lstrip(".").lower()
|
|
122
|
+
if not suffix:
|
|
123
|
+
continue
|
|
124
|
+
try:
|
|
125
|
+
tld = TLD(suffix)
|
|
126
|
+
except ValueError as exc:
|
|
127
|
+
raise argparse.ArgumentTypeError(f"invalid TLD: {raw.strip()!r}") from exc
|
|
128
|
+
if tld not in tlds:
|
|
129
|
+
tlds.append(tld)
|
|
130
|
+
if not tlds:
|
|
131
|
+
raise argparse.ArgumentTypeError("at least one TLD is required")
|
|
132
|
+
return tlds
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _selected_tlds(args: argparse.Namespace) -> list[TLD]:
|
|
136
|
+
"""The TLDs requested on the command line (accepts a raw string for convenience)."""
|
|
137
|
+
value: str | list[TLD] = args.tld
|
|
138
|
+
return parse_tld_list(value) if isinstance(value, str) else list(value)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _non_negative_float(value: str) -> float:
|
|
142
|
+
try:
|
|
143
|
+
number = float(value)
|
|
144
|
+
except ValueError:
|
|
145
|
+
raise argparse.ArgumentTypeError(f"not a number: {value!r}") from None
|
|
146
|
+
if not 0 <= number < float("inf"): # also rejects nan
|
|
147
|
+
raise argparse.ArgumentTypeError(f"must be a finite number >= 0, got {value!r}")
|
|
148
|
+
return number
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _non_negative_int(value: str) -> int:
|
|
152
|
+
try:
|
|
153
|
+
number = int(value)
|
|
154
|
+
except ValueError:
|
|
155
|
+
raise argparse.ArgumentTypeError(f"not an integer: {value!r}") from None
|
|
156
|
+
if number < 0:
|
|
157
|
+
raise argparse.ArgumentTypeError(f"must be >= 0, got {value!r}")
|
|
158
|
+
return number
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _positive_int(value: str) -> int:
|
|
162
|
+
number = _non_negative_int(value)
|
|
163
|
+
if number < 1:
|
|
164
|
+
raise argparse.ArgumentTypeError(f"must be >= 1, got {value!r}")
|
|
165
|
+
return number
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _range_max(value: str) -> int:
|
|
169
|
+
try:
|
|
170
|
+
number = int(value)
|
|
171
|
+
except ValueError:
|
|
172
|
+
raise argparse.ArgumentTypeError(f"not an integer: {value!r}") from None
|
|
173
|
+
if not 1 <= number <= MAX_RANGE_LENGTH:
|
|
174
|
+
raise argparse.ArgumentTypeError(f"must be between 1 and {MAX_RANGE_LENGTH}, got {value!r}")
|
|
175
|
+
return number
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _range_end(value: str) -> str:
|
|
179
|
+
if not _RANGE_END_RE.fullmatch(value):
|
|
180
|
+
raise argparse.ArgumentTypeError(f"must be lowercase letters a-z, got {value!r}")
|
|
181
|
+
return value
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _encoding(value: str) -> str:
|
|
185
|
+
try:
|
|
186
|
+
return codecs.lookup(value).name
|
|
187
|
+
except LookupError:
|
|
188
|
+
raise argparse.ArgumentTypeError(f"unknown encoding: {value!r}") from None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _contact(value: str) -> str:
|
|
192
|
+
# Printable ASCII only: the value goes verbatim into an HTTP header.
|
|
193
|
+
if not (value.isascii() and value.isprintable() and _EMAIL_RE.fullmatch(value)):
|
|
194
|
+
raise argparse.ArgumentTypeError(f"not an email address: {value!r}")
|
|
195
|
+
return value
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _add_encoding_option(parser: argparse.ArgumentParser) -> None:
|
|
199
|
+
parser.add_argument(
|
|
200
|
+
"--encoding",
|
|
201
|
+
type=_encoding,
|
|
202
|
+
default=DEFAULT_ENCODING,
|
|
203
|
+
help="Word list encoding (default: utf-8); undecodable bytes are an error",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def add_run_options(parser: argparse.ArgumentParser) -> None:
|
|
208
|
+
"""Options shared by every command that queries registries (``check``, ``name``):
|
|
209
|
+
pacing, parallelism, progress, cache and contact."""
|
|
210
|
+
parser.add_argument(
|
|
211
|
+
"--delay",
|
|
212
|
+
type=_non_negative_float,
|
|
213
|
+
default=1.0,
|
|
214
|
+
help="Seconds between requests (default: 1.0)",
|
|
215
|
+
)
|
|
216
|
+
parser.add_argument(
|
|
217
|
+
"--no-progress",
|
|
218
|
+
action="store_true",
|
|
219
|
+
help="Hide the progress bar (auto-hidden when stderr is not a TTY)",
|
|
220
|
+
)
|
|
221
|
+
parser.add_argument(
|
|
222
|
+
"--parallel",
|
|
223
|
+
type=_positive_int,
|
|
224
|
+
default=DEFAULT_PARALLEL,
|
|
225
|
+
metavar="N",
|
|
226
|
+
help="Check up to N registry hosts at once, never more than one request per "
|
|
227
|
+
f"host (default: {DEFAULT_PARALLEL}); results print as they complete. "
|
|
228
|
+
"--parallel 1 checks one domain at a time, in order",
|
|
229
|
+
)
|
|
230
|
+
parser.add_argument("--no-cache", action="store_true", help="Disable the result cache")
|
|
231
|
+
parser.add_argument(
|
|
232
|
+
"--cache-ttl",
|
|
233
|
+
type=_non_negative_float,
|
|
234
|
+
default=None,
|
|
235
|
+
metavar="HOURS",
|
|
236
|
+
help="Never reuse cached results older than HOURS (default: no cap). Without it, "
|
|
237
|
+
"taken names are kept until their expiration date (at most 90 days; 30 days "
|
|
238
|
+
"when unknown), and dropping names for 24 hours",
|
|
239
|
+
)
|
|
240
|
+
parser.add_argument(
|
|
241
|
+
"--cache-ttl-available",
|
|
242
|
+
type=_non_negative_float,
|
|
243
|
+
default=AVAILABLE_TTL_SECONDS / 3600,
|
|
244
|
+
metavar="HOURS",
|
|
245
|
+
help="Reuse cached AVAILABLE results younger than HOURS "
|
|
246
|
+
f"(default: {AVAILABLE_TTL_SECONDS / 3600:g})",
|
|
247
|
+
)
|
|
248
|
+
parser.add_argument(
|
|
249
|
+
"--cache-path",
|
|
250
|
+
type=Path,
|
|
251
|
+
default=None,
|
|
252
|
+
help="SQLite cache file (default: $XDG_CACHE_HOME/domainhack/results.sqlite3)",
|
|
253
|
+
)
|
|
254
|
+
parser.add_argument(
|
|
255
|
+
"--contact",
|
|
256
|
+
type=_contact,
|
|
257
|
+
default=os.environ.get(CONTACT_ENV) or None,
|
|
258
|
+
metavar="EMAIL",
|
|
259
|
+
help="Send EMAIL as the HTTP From header on RDAP requests so registry "
|
|
260
|
+
f"operators can reach you (default: ${CONTACT_ENV})",
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
265
|
+
parser = argparse.ArgumentParser(
|
|
266
|
+
prog="domainhack",
|
|
267
|
+
description="Find domain hacks hiding in real words.",
|
|
268
|
+
epilog=_EPILOG,
|
|
269
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
270
|
+
)
|
|
271
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
272
|
+
parser.add_argument(
|
|
273
|
+
"--tld",
|
|
274
|
+
type=parse_tld_list,
|
|
275
|
+
default="to",
|
|
276
|
+
metavar="TLD[,TLD...]",
|
|
277
|
+
help="TLD suffix, or a comma-separated list like to,io,com.ar (default: to)",
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
281
|
+
|
|
282
|
+
# filter subcommand
|
|
283
|
+
filt = sub.add_parser("filter", help="Filter words that form domain hacks")
|
|
284
|
+
filt.add_argument("file", type=Path, help="Path to word list file")
|
|
285
|
+
filt.add_argument("--min-length", type=_non_negative_int, default=0, help="Minimum word length")
|
|
286
|
+
_add_encoding_option(filt)
|
|
287
|
+
|
|
288
|
+
# check subcommand
|
|
289
|
+
chk = sub.add_parser(
|
|
290
|
+
"check",
|
|
291
|
+
help="Check domain availability",
|
|
292
|
+
epilog=_EPILOG,
|
|
293
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
294
|
+
)
|
|
295
|
+
add_run_options(chk)
|
|
296
|
+
chk.add_argument("--show-taken", action="store_true", help="Also print taken domains")
|
|
297
|
+
chk.add_argument(
|
|
298
|
+
"--show-dropping",
|
|
299
|
+
action="store_true",
|
|
300
|
+
help="Also print taken domains in redemption or pending delete, which may "
|
|
301
|
+
"soon be free (implied by --show-taken)",
|
|
302
|
+
)
|
|
303
|
+
chk.add_argument("--dry-run", action="store_true", help="List domains without checking")
|
|
304
|
+
|
|
305
|
+
source_group = chk.add_mutually_exclusive_group(required=True)
|
|
306
|
+
source_group.add_argument("--file", type=Path, help="Word list file (list mode)")
|
|
307
|
+
source_group.add_argument(
|
|
308
|
+
"--range-max",
|
|
309
|
+
type=_range_max,
|
|
310
|
+
help=f"Max combination length, 1-{MAX_RANGE_LENGTH} (range mode)",
|
|
311
|
+
)
|
|
312
|
+
chk.add_argument("--range-end", type=_range_end, default=None, help="Stop at this combination")
|
|
313
|
+
chk.add_argument(
|
|
314
|
+
"--order",
|
|
315
|
+
choices=[o.value for o in CandidateOrder],
|
|
316
|
+
default=None,
|
|
317
|
+
help="Check order in list mode: score = shortest SLD, then shortest word, then "
|
|
318
|
+
"alphabetical (default); alpha = alphabetical; input = word-list order. "
|
|
319
|
+
"Range mode is always generated shortest-first (input)",
|
|
320
|
+
)
|
|
321
|
+
chk.add_argument(
|
|
322
|
+
"--limit",
|
|
323
|
+
type=_positive_int,
|
|
324
|
+
default=None,
|
|
325
|
+
metavar="N",
|
|
326
|
+
help="Check at most N domains per TLD, taken in --order",
|
|
327
|
+
)
|
|
328
|
+
chk.add_argument(
|
|
329
|
+
"--keep-order",
|
|
330
|
+
action="store_true",
|
|
331
|
+
help="With --parallel > 1, print and save results in check order "
|
|
332
|
+
"instead of completion order",
|
|
333
|
+
)
|
|
334
|
+
chk.add_argument(
|
|
335
|
+
"--yes",
|
|
336
|
+
action="store_true",
|
|
337
|
+
help=f"Run a brute-force check estimated at more than {GUARDRAIL_MAX_QUERIES:,} queries",
|
|
338
|
+
)
|
|
339
|
+
_add_encoding_option(chk)
|
|
340
|
+
chk.add_argument("--output", type=Path, default=None, help="Also save results to FILE")
|
|
341
|
+
chk.add_argument(
|
|
342
|
+
"--format",
|
|
343
|
+
choices=OUTPUT_FORMATS,
|
|
344
|
+
default=None,
|
|
345
|
+
help="Output file format (default: inferred from --output extension)",
|
|
346
|
+
)
|
|
347
|
+
chk.add_argument(
|
|
348
|
+
"--confirm-dns",
|
|
349
|
+
action="store_true",
|
|
350
|
+
help="Also look up each available or taken name in public DNS (NS, A/AAAA). "
|
|
351
|
+
"An available name with NS records prints as 'AVAILABLE?': do not trust it as free",
|
|
352
|
+
)
|
|
353
|
+
# Imported here: name_cmd imports this module's helpers.
|
|
354
|
+
from domainhack.cli import name_cmd, tlds_cmd
|
|
355
|
+
|
|
356
|
+
name_cmd.register(sub, epilog=_EPILOG)
|
|
357
|
+
tlds_cmd.register(sub, epilog=_EPILOG)
|
|
358
|
+
return parser
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _validate_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> None:
|
|
362
|
+
"""Cross-option checks argparse cannot express; exits with status 2 on error."""
|
|
363
|
+
if args.command != "check":
|
|
364
|
+
return
|
|
365
|
+
if args.range_max is not None and args.order not in (None, CandidateOrder.INPUT.value):
|
|
366
|
+
parser.error(
|
|
367
|
+
f"--order {args.order} needs --file; range mode is generated shortest-first "
|
|
368
|
+
"and too large to sort"
|
|
369
|
+
)
|
|
370
|
+
if args.range_end is None:
|
|
371
|
+
return
|
|
372
|
+
if args.range_max is None:
|
|
373
|
+
parser.error("--range-end requires --range-max")
|
|
374
|
+
if len(args.range_end) > args.range_max:
|
|
375
|
+
parser.error(f"--range-end {args.range_end!r} is longer than --range-max {args.range_max}")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _word_source(args: argparse.Namespace, path: Path) -> FileWordSource:
|
|
379
|
+
return FileWordSource(path, encoding=getattr(args, "encoding", DEFAULT_ENCODING))
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def cmd_filter(args: argparse.Namespace) -> int:
|
|
383
|
+
"""Print matching words.
|
|
384
|
+
|
|
385
|
+
With a single TLD the output is one word per line (unchanged, pipeable into
|
|
386
|
+
``check --file``). With several TLDs each match is printed as
|
|
387
|
+
``word -> sld.tld``, one line per (word, TLD) match, since a word can match
|
|
388
|
+
more than one TLD.
|
|
389
|
+
"""
|
|
390
|
+
tlds = _selected_tlds(args)
|
|
391
|
+
source = _word_source(args, args.file)
|
|
392
|
+
use_case = FilterWordsUseCase(source, tlds, min_length=args.min_length)
|
|
393
|
+
multi = len(tlds) > 1
|
|
394
|
+
for hack in use_case.execute():
|
|
395
|
+
print(f"{hack.word} -> {hack.display}" if multi else hack.word)
|
|
396
|
+
_warn_skipped(use_case.skipped)
|
|
397
|
+
return EXIT_OK
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _warn_skipped(count: int) -> None:
|
|
401
|
+
"""One stderr line about candidates dropped by label validation."""
|
|
402
|
+
if count:
|
|
403
|
+
print(f"skipped {count} invalid candidates", file=sys.stderr)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _build_candidates(
|
|
407
|
+
args: argparse.Namespace, tlds: TLD | Sequence[TLD]
|
|
408
|
+
) -> FilterWordsUseCase | RangeCandidatesUseCase:
|
|
409
|
+
"""Candidate source: (word, TLD) matches in list mode, SLD x TLD in range mode.
|
|
410
|
+
|
|
411
|
+
Invalid labels are skipped and counted in the returned use case's ``skipped``.
|
|
412
|
+
"""
|
|
413
|
+
tld_list: tuple[TLD, ...] = (tlds,) if isinstance(tlds, TLD) else tuple(tlds)
|
|
414
|
+
if args.file:
|
|
415
|
+
return FilterWordsUseCase(_word_source(args, args.file), tld_list)
|
|
416
|
+
range_source = RangeWordSource(args.range_max, end_at=args.range_end)
|
|
417
|
+
return RangeCandidatesUseCase(range_source, tld_list)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _order(args: argparse.Namespace) -> CandidateOrder:
|
|
421
|
+
"""``--order``; by default ``score`` in list mode and ``input`` in range mode."""
|
|
422
|
+
value: str | None = getattr(args, "order", None)
|
|
423
|
+
if value is not None:
|
|
424
|
+
return CandidateOrder(value)
|
|
425
|
+
return CandidateOrder.SCORE if args.file else CandidateOrder.INPUT
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _build_ranker(args: argparse.Namespace, tlds: Sequence[TLD]) -> RankCandidatesUseCase:
|
|
429
|
+
return RankCandidatesUseCase(_order(args), limit=getattr(args, "limit", None), tlds=tlds)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _build_domains(args: argparse.Namespace, tlds: TLD | Sequence[TLD]) -> Iterable[DomainHack]:
|
|
433
|
+
"""Domains to check, in check order and capped by ``--limit`` per TLD.
|
|
434
|
+
|
|
435
|
+
List mode yields every (word, TLD) match, best first by default. Range
|
|
436
|
+
mode is lazy and SLD-major (a.to, a.io, b.to, b.io, ...).
|
|
437
|
+
"""
|
|
438
|
+
tld_list: tuple[TLD, ...] = (tlds,) if isinstance(tlds, TLD) else tuple(tlds)
|
|
439
|
+
return _build_ranker(args, tld_list).execute(_build_candidates(args, tld_list).execute())
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _resolve_output_format(output: Path, fmt: str | None) -> str:
|
|
443
|
+
"""Return the explicit format, or infer it from the output file extension."""
|
|
444
|
+
if fmt is not None:
|
|
445
|
+
return fmt
|
|
446
|
+
inferred = _FORMAT_BY_SUFFIX.get(output.suffix.lower())
|
|
447
|
+
if inferred is None:
|
|
448
|
+
raise OutputFormatError(
|
|
449
|
+
f"Cannot infer output format from {output.name!r}; "
|
|
450
|
+
"use a .csv/.json/.jsonl extension or pass --format {csv,json}"
|
|
451
|
+
)
|
|
452
|
+
return inferred
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _build_writer(args: argparse.Namespace) -> ResultWriter:
|
|
456
|
+
console = ConsoleResultWriter(
|
|
457
|
+
show_taken=args.show_taken, show_dropping=getattr(args, "show_dropping", False)
|
|
458
|
+
)
|
|
459
|
+
output: Path | None = getattr(args, "output", None)
|
|
460
|
+
if output is None:
|
|
461
|
+
return console
|
|
462
|
+
fmt = _resolve_output_format(output, getattr(args, "format", None))
|
|
463
|
+
return CompositeResultWriter([console, build_file_writer(output, fmt)])
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def build_file_writer(output: Path, fmt: str) -> ResultWriter:
|
|
467
|
+
"""The CSV or JSON Lines writer for ``output``; CliError when it cannot be opened."""
|
|
468
|
+
try:
|
|
469
|
+
return CsvResultWriter(output) if fmt == "csv" else JsonResultWriter(output)
|
|
470
|
+
except OSError as exc:
|
|
471
|
+
raise CliError(f"cannot write output file '{output}': {exc.strerror or exc}") from exc
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _range_totals(args: argparse.Namespace, tlds: Sequence[TLD]) -> dict[TLD, int]:
|
|
475
|
+
"""Range mode: exact candidates per TLD, capped by ``--limit``."""
|
|
476
|
+
range_source = RangeWordSource(args.range_max, end_at=args.range_end)
|
|
477
|
+
totals = RangeCandidatesUseCase(range_source, tlds).totals_by_tld()
|
|
478
|
+
limit: int | None = getattr(args, "limit", None)
|
|
479
|
+
if limit is not None:
|
|
480
|
+
totals = {tld: min(count, limit) for tld, count in totals.items()}
|
|
481
|
+
return totals
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _progress_total(args: argparse.Namespace, tlds: Sequence[TLD] | None = None) -> int | None:
|
|
485
|
+
"""Exact total in range mode (valid SLD x TLD pairs, capped by ``--limit``).
|
|
486
|
+
|
|
487
|
+
None (indeterminate bar) in file mode: the total is only known once the
|
|
488
|
+
list has been read (``cmd_check`` counts it when ``--order`` sorts it).
|
|
489
|
+
"""
|
|
490
|
+
if args.file:
|
|
491
|
+
return None
|
|
492
|
+
tld_list = list(tlds) if tlds is not None else _selected_tlds(args)
|
|
493
|
+
return sum(_range_totals(args, tld_list).values())
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _build_progress(args: argparse.Namespace) -> ProgressReporter:
|
|
497
|
+
if getattr(args, "no_progress", False):
|
|
498
|
+
return NullProgressReporter()
|
|
499
|
+
return TqdmProgressReporter()
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _registrar_factory(
|
|
503
|
+
args: argparse.Namespace, catalog: RegistrarCatalog | None = None
|
|
504
|
+
) -> RegistrarFactory:
|
|
505
|
+
"""Map a TLD to a fresh RegistrarClient, or None when no registrar supports it."""
|
|
506
|
+
build: RegistrarCatalog = catalog if catalog is not None else build_registrar_for
|
|
507
|
+
delay: float = args.delay
|
|
508
|
+
contact: str | None = getattr(args, "contact", None)
|
|
509
|
+
# One breaker per run: a host that stops answering is skipped for every TLD it serves.
|
|
510
|
+
breaker = HostCircuitBreaker()
|
|
511
|
+
|
|
512
|
+
def factory(tld: TLD) -> RegistrarClient | None:
|
|
513
|
+
return build(tld, delay=delay, breaker=breaker, contact=contact)
|
|
514
|
+
|
|
515
|
+
return factory
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _build_router(
|
|
519
|
+
args: argparse.Namespace, catalog: RegistrarCatalog | None = None
|
|
520
|
+
) -> RegistrarRouter:
|
|
521
|
+
return RegistrarRouter(_registrar_factory(args, catalog))
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _build_registrar(
|
|
525
|
+
args: argparse.Namespace,
|
|
526
|
+
router: RegistrarRouter | None = None,
|
|
527
|
+
*,
|
|
528
|
+
catalog: RegistrarCatalog | None = None,
|
|
529
|
+
) -> RegistrarClient:
|
|
530
|
+
"""The TLD router, wrapped in the result cache unless ``--no-cache``.
|
|
531
|
+
|
|
532
|
+
The cache is keyed by fqdn, so a single cache serves every TLD.
|
|
533
|
+
"""
|
|
534
|
+
registrar: RegistrarClient = router if router is not None else _build_router(args, catalog)
|
|
535
|
+
if getattr(args, "no_cache", False):
|
|
536
|
+
return registrar
|
|
537
|
+
return CachedRegistrarClient(
|
|
538
|
+
registrar, path=getattr(args, "cache_path", None), ttl=_cache_ttl_policy(args)
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _lane_key(args: argparse.Namespace, router: RegistrarRouter) -> LaneKey:
|
|
543
|
+
"""One lane per registry host (see ``pacing.lane_for``), resolved once per TLD."""
|
|
544
|
+
lanes: dict[TLD, Hashable] = {}
|
|
545
|
+
|
|
546
|
+
def lane(domain: DomainHack) -> Hashable:
|
|
547
|
+
tld = domain.tld
|
|
548
|
+
if tld not in lanes:
|
|
549
|
+
lanes[tld] = lane_for(router.client_for(tld), tld)
|
|
550
|
+
return lanes[tld]
|
|
551
|
+
|
|
552
|
+
return lane
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _cache_ttl_policy(args: argparse.Namespace) -> CacheTtlPolicy:
|
|
556
|
+
"""``--cache-ttl-available`` sets the AVAILABLE TTL; ``--cache-ttl`` caps every TTL."""
|
|
557
|
+
cap_hours: float | None = getattr(args, "cache_ttl", None)
|
|
558
|
+
available_hours: float = getattr(args, "cache_ttl_available", AVAILABLE_TTL_SECONDS / 3600)
|
|
559
|
+
return CacheTtlPolicy(
|
|
560
|
+
available=available_hours * 3600,
|
|
561
|
+
cap=None if cap_hours is None else cap_hours * 3600,
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _estimate_range_run(
|
|
566
|
+
args: argparse.Namespace, router: RegistrarRouter, tlds: Sequence[TLD]
|
|
567
|
+
) -> RunEstimate:
|
|
568
|
+
delay: float = args.delay
|
|
569
|
+
|
|
570
|
+
def pacing(tld: TLD) -> Pacing:
|
|
571
|
+
return pacing_for(router.client_for(tld), tld, delay)
|
|
572
|
+
|
|
573
|
+
return estimate_run(_range_totals(args, tlds), pacing)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _range_guardrail(
|
|
577
|
+
args: argparse.Namespace, router: RegistrarRouter, tlds: Sequence[TLD]
|
|
578
|
+
) -> int | None:
|
|
579
|
+
"""Print the cost of a brute-force run; refuse a large one without ``--yes``.
|
|
580
|
+
|
|
581
|
+
Returns EXIT_USAGE when the run is refused, None when it may go ahead.
|
|
582
|
+
"""
|
|
583
|
+
estimate = _estimate_range_run(args, router, tlds)
|
|
584
|
+
hosts = ", ".join(h.host for h in estimate.hosts)
|
|
585
|
+
count = len(estimate.hosts)
|
|
586
|
+
parallel: int = getattr(args, "parallel", 1)
|
|
587
|
+
lanes = min(parallel, count)
|
|
588
|
+
# The estimate is the busiest host's share: a lower bound whether hosts
|
|
589
|
+
# are checked in parallel lanes or take turns (each host is paced on its own).
|
|
590
|
+
how = f"; {lanes} hosts in parallel, one request at a time each" if lanes > 1 else ""
|
|
591
|
+
_stderr(
|
|
592
|
+
f"Estimated {estimate.queries:,} queries to {count} host{'s' if count != 1 else ''} "
|
|
593
|
+
f"({hosts}): at least {format_duration(estimate.seconds)} at the current pacing{how}."
|
|
594
|
+
)
|
|
595
|
+
if not estimate.exceeds() or getattr(args, "yes", False):
|
|
596
|
+
return None
|
|
597
|
+
_stderr(
|
|
598
|
+
f"error: refusing to send more than {GUARDRAIL_MAX_QUERIES:,} queries without --yes; "
|
|
599
|
+
"registries ask clients not to bulk-query. Narrow the run with --range-end "
|
|
600
|
+
"or --limit, preview it with --dry-run, or pass --yes."
|
|
601
|
+
)
|
|
602
|
+
return EXIT_USAGE
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _supported_tlds(router: RegistrarRouter, tlds: Sequence[TLD]) -> list[TLD]:
|
|
606
|
+
"""Keep TLDs the router can check; warn on stderr about the others.
|
|
607
|
+
|
|
608
|
+
Raises NoSupportedTLDError when none is supported.
|
|
609
|
+
"""
|
|
610
|
+
supported = [tld for tld in tlds if router.supports(tld)]
|
|
611
|
+
unsupported = [tld for tld in tlds if tld not in supported]
|
|
612
|
+
if unsupported and supported:
|
|
613
|
+
for message in describe_unsupported(t.suffix for t in unsupported):
|
|
614
|
+
print(f"warning: {message}; skipping", file=sys.stderr)
|
|
615
|
+
if not supported:
|
|
616
|
+
router.close()
|
|
617
|
+
raise NoSupportedTLDError("; ".join(describe_unsupported(t.suffix for t in unsupported)))
|
|
618
|
+
return supported
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _confirming_writer(
|
|
622
|
+
args: argparse.Namespace, writer: ResultWriter, dns_lookup: DnsLookup | None
|
|
623
|
+
) -> ResultWriter:
|
|
624
|
+
"""With ``--confirm-dns``, wrap ``writer`` so results get DNS evidence as they stream."""
|
|
625
|
+
if not getattr(args, "confirm_dns", False):
|
|
626
|
+
return writer
|
|
627
|
+
lookup = dns_lookup if dns_lookup is not None else DnsPythonLookup()
|
|
628
|
+
return DnsConfirmingWriter(writer, ConfirmWithDns(lookup))
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def cmd_check(
|
|
632
|
+
args: argparse.Namespace,
|
|
633
|
+
*,
|
|
634
|
+
catalog: RegistrarCatalog | None = None,
|
|
635
|
+
dns_lookup: DnsLookup | None = None,
|
|
636
|
+
) -> int:
|
|
637
|
+
"""Check availability; ``catalog`` defaults to ``build_registrar_for``.
|
|
638
|
+
|
|
639
|
+
``dns_lookup`` (for ``--confirm-dns``) defaults to ``DnsPythonLookup``.
|
|
640
|
+
"""
|
|
641
|
+
tlds = _selected_tlds(args)
|
|
642
|
+
|
|
643
|
+
if args.dry_run:
|
|
644
|
+
# The same candidates, order and limit as a real run.
|
|
645
|
+
candidates = _build_candidates(args, tlds)
|
|
646
|
+
for domain in _build_ranker(args, tlds).execute(candidates.execute()):
|
|
647
|
+
print(f" {domain.display} (word: {domain.word!r})")
|
|
648
|
+
_warn_skipped(candidates.skipped)
|
|
649
|
+
return EXIT_OK
|
|
650
|
+
|
|
651
|
+
# Fail fast on local problems, before any network work starts.
|
|
652
|
+
if getattr(args, "output", None) is not None:
|
|
653
|
+
_resolve_output_format(args.output, getattr(args, "format", None))
|
|
654
|
+
if args.file:
|
|
655
|
+
_word_source(args, args.file).check_readable()
|
|
656
|
+
|
|
657
|
+
router = _build_router(args, catalog)
|
|
658
|
+
tlds = _supported_tlds(router, tlds)
|
|
659
|
+
try:
|
|
660
|
+
refused = None if args.file else _range_guardrail(args, router, tlds)
|
|
661
|
+
if refused is not None:
|
|
662
|
+
router.close()
|
|
663
|
+
return refused
|
|
664
|
+
candidates = _build_candidates(args, tlds)
|
|
665
|
+
ranker = _build_ranker(args, tlds)
|
|
666
|
+
domains: Iterable[DomainHack] = ranker.execute(candidates.execute())
|
|
667
|
+
total = _progress_total(args, tlds)
|
|
668
|
+
if ranker.materializes:
|
|
669
|
+
# Sorting reads the whole list anyway (before any output file is
|
|
670
|
+
# opened); counting it gives the progress bar an exact total.
|
|
671
|
+
domains = list(domains)
|
|
672
|
+
total = len(domains)
|
|
673
|
+
writer = _confirming_writer(args, _build_writer(args), dns_lookup)
|
|
674
|
+
except BaseException:
|
|
675
|
+
router.close()
|
|
676
|
+
raise
|
|
677
|
+
|
|
678
|
+
with _build_registrar(args, router) as registrar:
|
|
679
|
+
progress = _build_progress(args)
|
|
680
|
+
# Workers call the router directly; the cache is read and written on
|
|
681
|
+
# this thread only, so cache hits never wait for a lane.
|
|
682
|
+
cache = registrar if isinstance(registrar, CachedRegistrarClient) else None
|
|
683
|
+
use_case = CheckDomainsUseCase(
|
|
684
|
+
router,
|
|
685
|
+
writer,
|
|
686
|
+
progress,
|
|
687
|
+
cache=cache,
|
|
688
|
+
parallel=getattr(args, "parallel", 1),
|
|
689
|
+
lane_key=_lane_key(args, router),
|
|
690
|
+
keep_order=getattr(args, "keep_order", False),
|
|
691
|
+
)
|
|
692
|
+
summary = use_case.execute(domains, total=total)
|
|
693
|
+
_warn_skipped(candidates.skipped)
|
|
694
|
+
conflicts = writer.conflicts if isinstance(writer, DnsConfirmingWriter) else 0
|
|
695
|
+
return _report(summary, dns_conflicts=conflicts)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _report(summary: CheckSummary, *, dns_conflicts: int = 0) -> int:
|
|
699
|
+
"""Print the run summary on stderr and map it to an exit status."""
|
|
700
|
+
_report_registries(summary)
|
|
701
|
+
if dns_conflicts:
|
|
702
|
+
names, verb = ("name", "has") if dns_conflicts == 1 else ("names", "have")
|
|
703
|
+
_stderr(
|
|
704
|
+
f"warning: {dns_conflicts} {names} the registry reported available {verb} "
|
|
705
|
+
"NS records in DNS (marked 'AVAILABLE?'); do not count on them being free"
|
|
706
|
+
)
|
|
707
|
+
if summary.interrupted:
|
|
708
|
+
_stderr(
|
|
709
|
+
f"Interrupted after {summary.checked} checks "
|
|
710
|
+
f"({summary.available} available, {summary.errors} errors)."
|
|
711
|
+
)
|
|
712
|
+
return EXIT_INTERRUPTED
|
|
713
|
+
dropping = f" ({summary.dropping} dropping)" if summary.dropping else ""
|
|
714
|
+
_stderr(
|
|
715
|
+
f"\nDone. Checked {summary.checked} domains: {summary.available} available, "
|
|
716
|
+
f"{summary.taken} taken{dropping}, {summary.errors} errors."
|
|
717
|
+
)
|
|
718
|
+
return EXIT_FAILURE if summary.errors else EXIT_OK
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _report_registries(summary: CheckSummary) -> None:
|
|
722
|
+
"""Name every TLD whose checks failed, so an unreachable registry is never silent."""
|
|
723
|
+
for tld in summary.tlds_with_errors:
|
|
724
|
+
errors = f"{tld.errors} error{'s' if tld.errors != 1 else ''}"
|
|
725
|
+
if tld.unreachable:
|
|
726
|
+
_stderr(f"warning: registry for .{tld.suffix} did not respond ({errors}); re-run later")
|
|
727
|
+
else:
|
|
728
|
+
_stderr(
|
|
729
|
+
f"warning: registry for .{tld.suffix} failed {tld.errors} of {tld.checked} "
|
|
730
|
+
"checks; re-run later for the rest"
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _stderr(message: str) -> None:
|
|
735
|
+
print(message, file=sys.stderr)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _silence_stdout() -> None:
|
|
739
|
+
"""After EPIPE (``domainhack ... | head``), keep Python's exit-time flush quiet."""
|
|
740
|
+
try:
|
|
741
|
+
devnull = os.open(os.devnull, os.O_WRONLY)
|
|
742
|
+
os.dup2(devnull, sys.stdout.fileno())
|
|
743
|
+
except (OSError, ValueError):
|
|
744
|
+
pass
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def main(
|
|
748
|
+
argv: Sequence[str] | None = None,
|
|
749
|
+
*,
|
|
750
|
+
catalog: RegistrarCatalog | None = None,
|
|
751
|
+
dns_lookup: DnsLookup | None = None,
|
|
752
|
+
services: "NameServices | None" = None,
|
|
753
|
+
) -> int:
|
|
754
|
+
"""Run the CLI and return its exit status (see the ``--help`` epilog).
|
|
755
|
+
|
|
756
|
+
``catalog`` replaces ``build_registrar_for`` (the registrar catalog) for
|
|
757
|
+
``check`` and ``name``; ``dns_lookup`` replaces ``DnsPythonLookup`` (for
|
|
758
|
+
``check --confirm-dns``); ``services`` replaces the ``name`` command's TLD
|
|
759
|
+
list and DNS lookup.
|
|
760
|
+
"""
|
|
761
|
+
parser = build_parser()
|
|
762
|
+
try:
|
|
763
|
+
args = parser.parse_args(argv)
|
|
764
|
+
_validate_args(parser, args)
|
|
765
|
+
except SystemExit as exc: # usage error (2), or --help / --version (0)
|
|
766
|
+
return exc.code if isinstance(exc.code, int) else EXIT_USAGE
|
|
767
|
+
|
|
768
|
+
try:
|
|
769
|
+
if args.command == "filter":
|
|
770
|
+
return cmd_filter(args)
|
|
771
|
+
if args.command == "name":
|
|
772
|
+
from domainhack.cli import name_cmd
|
|
773
|
+
|
|
774
|
+
return name_cmd.cmd_name(args, catalog=catalog, services=services)
|
|
775
|
+
if args.command == "tlds":
|
|
776
|
+
from domainhack.cli import tlds_cmd
|
|
777
|
+
|
|
778
|
+
return tlds_cmd.cmd_tlds(args)
|
|
779
|
+
return cmd_check(args, catalog=catalog, dns_lookup=dns_lookup)
|
|
780
|
+
except OutputFormatError as exc:
|
|
781
|
+
parser.print_usage(sys.stderr)
|
|
782
|
+
_stderr(f"{parser.prog}: error: {exc}")
|
|
783
|
+
return EXIT_USAGE
|
|
784
|
+
except KeyboardInterrupt:
|
|
785
|
+
_stderr("Interrupted.")
|
|
786
|
+
return EXIT_INTERRUPTED
|
|
787
|
+
except BrokenPipeError:
|
|
788
|
+
_silence_stdout()
|
|
789
|
+
return EXIT_FAILURE
|
|
790
|
+
except (CliError, WordListError) as exc:
|
|
791
|
+
_stderr(f"error: {exc}")
|
|
792
|
+
return EXIT_FAILURE
|
|
793
|
+
except OSError as exc:
|
|
794
|
+
where = f"{exc.filename}: " if exc.filename else ""
|
|
795
|
+
_stderr(f"error: {where}{exc.strerror or exc}")
|
|
796
|
+
return EXIT_FAILURE
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
if __name__ == "__main__":
|
|
800
|
+
raise SystemExit(main())
|