125 lines
4.3 KiB
Plaintext
125 lines
4.3 KiB
Plaintext
|
|
#!/data/data/com.termux/files/usr/bin/python3
|
||
|
|
"""mdns: unified CLI for mDNS resolve / browse / publish / reverse / cache.
|
||
|
|
|
||
|
|
Loads lib/mdns_tools/ from the repo root. Usable both from an install into
|
||
|
|
~/.local/bin (installer sets a symlink pointing at the repo checkout) and
|
||
|
|
directly from a clone.
|
||
|
|
"""
|
||
|
|
import argparse
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
_ROOT = Path(__file__).resolve().parent.parent
|
||
|
|
sys.path.insert(0, str(_ROOT / "lib"))
|
||
|
|
|
||
|
|
from mdns_tools import __version__ # noqa: E402
|
||
|
|
|
||
|
|
|
||
|
|
def _cmd_resolve(a: argparse.Namespace) -> int:
|
||
|
|
from mdns_tools import resolve as _r, cache as _c
|
||
|
|
ip = _r.resolve(a.host, family=a.family, timeout=a.timeout,
|
||
|
|
retries=a.retries, use_cache=not a.no_cache)
|
||
|
|
if ip:
|
||
|
|
print(ip)
|
||
|
|
return 0
|
||
|
|
if a.stale:
|
||
|
|
ip = _c.get_stale(a.host if a.host.endswith(".local") else f"{a.host}.local",
|
||
|
|
family=a.family)
|
||
|
|
if ip:
|
||
|
|
print(ip)
|
||
|
|
return 0
|
||
|
|
print(f"error: could not resolve '{a.host}'", file=sys.stderr)
|
||
|
|
return 1
|
||
|
|
|
||
|
|
|
||
|
|
def _cmd_reverse(a: argparse.Namespace) -> int:
|
||
|
|
from mdns_tools import reverse as _rev
|
||
|
|
name = _rev.reverse(a.ip, timeout=a.timeout, retries=a.retries)
|
||
|
|
if name:
|
||
|
|
print(name)
|
||
|
|
return 0
|
||
|
|
print(f"error: no PTR for {a.ip}", file=sys.stderr)
|
||
|
|
return 1
|
||
|
|
|
||
|
|
|
||
|
|
def _cmd_browse(a: argparse.Namespace) -> int:
|
||
|
|
from mdns_tools import browse as _br # loads zeroconf
|
||
|
|
if a.list_types:
|
||
|
|
for t in _br.list_types(duration=a.duration):
|
||
|
|
print(t)
|
||
|
|
return 0
|
||
|
|
items = _br.browse(service_type=a.type, duration=a.duration)
|
||
|
|
if a.json:
|
||
|
|
print(json.dumps(items, indent=2))
|
||
|
|
return 0
|
||
|
|
for it in items:
|
||
|
|
addrs = ", ".join(it.get("addresses") or []) or "-"
|
||
|
|
print(f'{it["type"]:<30} {it.get("server","-"):<28} {addrs}:{it.get("port","-")}')
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
def _cmd_publish(a: argparse.Namespace) -> int:
|
||
|
|
from mdns_tools import publish as _p # loads zeroconf
|
||
|
|
_p.run(config_path=a.config, hostname=a.hostname, verbose=not a.quiet)
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
def _cmd_cache(a: argparse.Namespace) -> int:
|
||
|
|
from mdns_tools import cache as _c
|
||
|
|
if a.op == "list":
|
||
|
|
for k, v in _c.all_entries().items():
|
||
|
|
host, fam = k.split("|") if "|" in k else (k, "v4")
|
||
|
|
print(f'{host:<40} {fam} {v.get("ip"):<40} ttl={v.get("ttl")}')
|
||
|
|
elif a.op == "clear":
|
||
|
|
n = _c.clear(a.host)
|
||
|
|
print(f"removed {n} entries")
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> int:
|
||
|
|
p = argparse.ArgumentParser(prog="mdns", description="mDNS / DNS-SD tools")
|
||
|
|
p.add_argument("--version", action="version", version=f"mdns {__version__}")
|
||
|
|
sub = p.add_subparsers(dest="cmd", required=True)
|
||
|
|
|
||
|
|
r = sub.add_parser("resolve", help="hostname -> IP")
|
||
|
|
r.add_argument("host")
|
||
|
|
r.add_argument("--family", choices=["v4", "v6"], default="v4")
|
||
|
|
r.add_argument("--timeout", type=float, default=1.5)
|
||
|
|
r.add_argument("--retries", type=int, default=2)
|
||
|
|
r.add_argument("--no-cache", action="store_true")
|
||
|
|
r.add_argument("--stale", action="store_true", help="accept expired cache as last resort")
|
||
|
|
r.set_defaults(func=_cmd_resolve)
|
||
|
|
|
||
|
|
rv = sub.add_parser("reverse", help="IP -> hostname (PTR)")
|
||
|
|
rv.add_argument("ip")
|
||
|
|
rv.add_argument("--timeout", type=float, default=1.5)
|
||
|
|
rv.add_argument("--retries", type=int, default=2)
|
||
|
|
rv.set_defaults(func=_cmd_reverse)
|
||
|
|
|
||
|
|
b = sub.add_parser("browse", help="discover services on the LAN")
|
||
|
|
b.add_argument("--type", help="e.g. _ssh._tcp.local. (default: all types)")
|
||
|
|
b.add_argument("--duration", type=float, default=3.0)
|
||
|
|
b.add_argument("--json", action="store_true")
|
||
|
|
b.add_argument("--list-types", action="store_true")
|
||
|
|
b.set_defaults(func=_cmd_browse)
|
||
|
|
|
||
|
|
pu = sub.add_parser("publish", help="advertise this device + services")
|
||
|
|
pu.add_argument("--config", help="path to services YAML/JSON")
|
||
|
|
pu.add_argument("--hostname")
|
||
|
|
pu.add_argument("--quiet", action="store_true")
|
||
|
|
pu.set_defaults(func=_cmd_publish)
|
||
|
|
|
||
|
|
ca = sub.add_parser("cache", help="inspect/clear the resolver cache")
|
||
|
|
ca.add_argument("op", choices=["list", "clear"])
|
||
|
|
ca.add_argument("host", nargs="?")
|
||
|
|
ca.set_defaults(func=_cmd_cache)
|
||
|
|
|
||
|
|
args = p.parse_args()
|
||
|
|
return args.func(args)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(main())
|