Web

DNS

DNS from the server: A, AAAA, MX, TXT, CNAME, NS, SOA, SRV, CAA and PTR records looked up through the system resolver or one you name, host names resolved, and record text and zone files read into fields.

Version 1.0.0 · 5 functions · 2 crates

#dns#network#email

Functions

What use dns.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use dns_lookup, dns_reverse from dns (Quillo 0.6).

dns_lookup(name: str, record_type: str, server: str, timeout_ms: int) → listor an error

The record_type records ("A", "AAAA", "MX", "TXT", "CNAME", "NS", "SOA", "SRV", "CAA", "PTR"...) of name, asked of server: "" for the system's resolver, "cloudflare", "google", "quad9", or an IP (with :port). Each record is { name, type, ttl, value } plus its fields (preference and exchange for MX, text for TXT...). No records is an empty list.

dns_reverse(ip: str, server: str, timeout_ms: int) → str?or an error

The name an IP address points back to (its PTR record), or null when it has none.

dns_resolve(host: str) → listor an error

The addresses host resolves to as the operating system resolves it (/etc/hosts first, so "localhost" works offline), IPv4 ones first, each once.

dns_parse_record(record_type: str, text: str, origin: str) → anyor an error

A record's text as a zone file writes it (10 mail.example.com. for MX, 0 issue "letsencrypt.org" for CAA, its tag and caa_value) read into its fields, as dns_lookup gives them; names without a final dot are taken relative to origin (empty: the root).

dns_zone_parse(zone: str, origin: str) → listor an error

A zone file (BIND format: $ORIGIN, $TTL, @, parentheses) read into its records, each as dns_lookup gives them, sorted by name and type. origin is used when the file has no $ORIGIN.

Get it

$ quillo install dns               # dns.qwer into this folder$ quillo install dns --example     # and its example, dns.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/dns.qwer$ curl -O https://quillolang.org/plugins/files/dns.qwe$ curl -O https://quillolang.org/plugins/files/dns_sample.zone

Then, in your program, all of it or only what you call:

main.qwe
use dns.qweruse dns_lookup, dns_reverse from dns      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
hickory-resolver = "0.24"hickory-proto = { version = "0.24", features = ["text-parsing"] }

Example

dns.qwe
// A domain checker for a hosting panel: what a customer's zone file says, the records it// publishes, and lookups against live DNS.////   quillo run dns.qwe      then open http://localhost:8080/zone and http://localhost:8080/offline//   http://localhost:8080/lookup/example.com/MX goes to the network.use "dns.qwer"route GET /zone = dns_zone_parse(read_file("dns_sample.zone")?, "")? | map((r) => { name: r.name, type: r.type, value: r.value })route GET /offline = {    localhost: dns_resolve("localhost")?,    mx: dns_parse_record("MX", "10 mx1.mailer.example.", "")?,    srv: dns_parse_record("SRV", "10 60 5060 sip", "shop.example.com.")?,    caa: dns_parse_record("CAA", "0 issue \"letsencrypt.org\"", "")?,    txt: dns_parse_record("TXT", "\"v=spf1 include:_spf.mailer.example\" \" -all\"", "")?,    bad: dns_parse_record("MX", "ten mail", "").error,}route GET /lookup/:name/:type = dns_lookup(name, type, "cloudflare", 3000)?// Does the customer's domain point at us, and can it receive mail?route GET /check/:domain = {    points_here: dns_lookup(domain, "A", "", 3000)? | any((r) => r.address == "203.0.113.10"),    mail: dns_lookup(domain, "MX", "", 3000)? | map((r) => r.exchange),    spf: dns_lookup(domain, "TXT", "", 3000)? | map((r) => r.text) | filter((t) => starts_with(t, "v=spf1")),}
$ quillo run dns.qwe$ curl 'localhost:8080/zone'$ curl 'localhost:8080/offline'

Source

dns.qwer
//! DNS from the server: A, AAAA, MX, TXT, CNAME, NS, SOA, SRV, CAA and PTR records looked up through the system's resolver or one you name (1.1.1.1, 8.8.8.8), host names resolved, and record text and whole zone files read into fields.//! version: 1.0.0//!//! [dependencies]//! hickory-resolver = "0.24"//! hickory-proto = { version = "0.24", features = ["text-parsing"] }use hickory_proto::rr::{Name, RData, Record, RecordType};use hickory_proto::serialize::txt::{Parser, RDataParser};use hickory_resolver::config::{NameServerConfigGroup, ResolverConfig, ResolverOpts};use hickory_resolver::Resolver;use serde_json::{json, Value};use std::net::{IpAddr, ToSocketAddrs};use std::str::FromStr;use std::time::Duration;fn rtype(t: &str) -> Result<RecordType, String> {    RecordType::from_str(&t.trim().to_uppercase()).map_err(|_| format!("unknown record type `{}`", t))}fn fields(r: &RData) -> Value {    match r {        RData::A(a) => json!({ "address": a.to_string() }),        RData::AAAA(a) => json!({ "address": a.to_string() }),        RData::CNAME(n) => json!({ "target": n.to_string() }),        RData::NS(n) => json!({ "host": n.to_string() }),        RData::PTR(n) => json!({ "name": n.to_string() }),        RData::MX(m) => json!({ "preference": m.preference(), "exchange": m.exchange().to_string() }),        RData::SRV(s) => json!({ "priority": s.priority(), "weight": s.weight(), "port": s.port(), "target": s.target().to_string() }),        RData::SOA(s) => json!({ "mname": s.mname().to_string(), "rname": s.rname().to_string(), "serial": s.serial(), "refresh": s.refresh(), "retry": s.retry(), "expire": s.expire(), "minimum": s.minimum() }),        RData::TXT(t) => {            let parts: Vec<String> = t.txt_data().iter().map(|b| String::from_utf8_lossy(b).to_string()).collect();            json!({ "text": parts.concat(), "strings": parts })        }        RData::CAA(c) => json!({ "critical": c.issuer_critical(), "tag": c.tag().as_str(), "caa_value": c.value().to_string().trim_matches('"').to_string() }),        _ => json!({}),    }}fn record(r: &Record) -> Value {    let mut v = json!({ "name": r.name().to_string(), "type": r.record_type().to_string(), "ttl": r.ttl() });    if let Some(d) = r.data() {        v["value"] = json!(d.to_string());        if let Value::Object(f) = fields(d) {            v.as_object_mut().unwrap().extend(f);        }    }    v}fn resolver(server: &str, timeout_ms: i64) -> Result<Resolver, String> {    let mut opts = ResolverOpts::default();    opts.timeout = Duration::from_millis(timeout_ms.clamp(100, 30_000) as u64);    opts.attempts = 2;    let config = match server.trim() {        "" | "system" => return Resolver::from_system_conf().map_err(|e| format!("the system's DNS settings: {}", e)),        "cloudflare" => ResolverConfig::cloudflare(),        "google" => ResolverConfig::google(),        "quad9" => ResolverConfig::quad9(),        s => {            let (ip, port) = match s.parse::<std::net::SocketAddr>() {                Ok(a) => (a.ip(), a.port()),                Err(_) => (s.parse::<IpAddr>().map_err(|_| format!("server `{}` is not an IP, ip:port, system, cloudflare, google or quad9", s))?, 53),            };            ResolverConfig::from_parts(None, vec![], NameServerConfigGroup::from_ips_clear(&[ip], port, true))        }    };    Resolver::new(config, opts).map_err(|e| e.to_string())}/// The `record_type` records ("A", "AAAA", "MX", "TXT", "CNAME", "NS", "SOA", "SRV", "CAA", "PTR"...) of `name`, asked of/// `server`: "" for the system's resolver, "cloudflare", "google", "quad9", or an IP (with `:port`). Each record is/// `{ name, type, ttl, value }` plus its fields (`preference` and `exchange` for MX, `text` for TXT...). No records is an empty list.pub fn dns_lookup(name: &str, record_type: &str, server: &str, timeout_ms: i64) -> Result<Vec<Value>, String> {    let t = rtype(record_type)?;    let r = resolver(server, timeout_ms)?;    match r.lookup(name.trim(), t) {        Ok(l) => Ok(l.records().iter().filter(|r| r.record_type() == t || t == RecordType::ANY).map(record).collect()),        Err(e) if matches!(e.kind(), hickory_resolver::error::ResolveErrorKind::NoRecordsFound { .. }) => Ok(vec![]),        Err(e) => Err(format!("{} {}: {}", name, record_type, e)),    }}/// The name an IP address points back to (its PTR record), or null when it has none.pub fn dns_reverse(ip: &str, server: &str, timeout_ms: i64) -> Result<Option<String>, String> {    let addr: IpAddr = ip.trim().parse().map_err(|_| format!("`{}` is not an IP address", ip))?;    let r = resolver(server, timeout_ms)?;    match r.reverse_lookup(addr) {        Ok(l) => Ok(l.iter().next().map(|n| n.to_string().trim_end_matches('.').to_string())),        Err(e) if matches!(e.kind(), hickory_resolver::error::ResolveErrorKind::NoRecordsFound { .. }) => Ok(None),        Err(e) => Err(e.to_string()),    }}/// The addresses `host` resolves to as the operating system resolves it (`/etc/hosts` first, so "localhost" works offline),/// IPv4 ones first, each once.pub fn dns_resolve(host: &str) -> Result<Vec<String>, String> {    let mut ips: Vec<IpAddr> = (host.trim(), 0).to_socket_addrs().map_err(|e| format!("{}: {}", host, e))?.map(|a| a.ip()).collect();    ips.sort_by_key(|ip| (ip.is_ipv6(), *ip));    ips.dedup();    Ok(ips.iter().map(|i| i.to_string()).collect())}/// A record's text as a zone file writes it (`10 mail.example.com.` for MX, `0 issue "letsencrypt.org"` for CAA, its `tag` and `caa_value`) read into its/// fields, as `dns_lookup` gives them; names without a final dot are taken relative to `origin` (empty: the root).pub fn dns_parse_record(record_type: &str, text: &str, origin: &str) -> Result<Value, String> {    let t = rtype(record_type)?;    let o = if origin.trim().is_empty() { None } else { Some(Name::from_str(origin.trim()).map_err(|e| e.to_string())?) };    let tokens: Vec<String> = tokenize(text);    let rd = RData::parse(t, tokens.iter().map(|s| s.as_str()), o.as_ref()).map_err(|e| format!("{} `{}`: {}", record_type, text, e))?;    let mut v = json!({ "type": t.to_string(), "value": rd.to_string() });    if let Value::Object(f) = fields(&rd) {        v.as_object_mut().unwrap().extend(f);    }    Ok(v)}/// Splits record text as a zone file does: on spaces, keeping "quoted strings" together without their quotes.fn tokenize(s: &str) -> Vec<String> {    let mut out = vec![];    let mut cur = String::new();    let (mut quoted, mut had) = (false, false);    let mut chars = s.chars().peekable();    while let Some(c) = chars.next() {        match c {            '\\' if quoted => {                if let Some(n) = chars.next() {                    cur.push(n);                }            }            '"' => {                quoted = !quoted;                had = true;            }            c if c.is_whitespace() && !quoted => {                if !cur.is_empty() || had {                    out.push(std::mem::take(&mut cur));                }                had = false;            }            c => cur.push(c),        }    }    if !cur.is_empty() || had {        out.push(cur);    }    out}/// A zone file (BIND format: `$ORIGIN`, `$TTL`, `@`, parentheses) read into its records, each as `dns_lookup` gives them, sorted/// by name and type. `origin` is used when the file has no `$ORIGIN`.pub fn dns_zone_parse(zone: &str, origin: &str) -> Result<Vec<Value>, String> {    let o = if origin.trim().is_empty() { None } else { Some(Name::from_str(origin.trim()).map_err(|e| e.to_string())?) };    let (_, sets) = Parser::new(zone, None, o).parse().map_err(|e| format!("the zone: {}", e))?;    Ok(sets.values().flat_map(|s| s.records_without_rrsigs().map(record).collect::<Vec<_>>()).collect())}
DNS blocklists dnsbl.qwer · 1.0.0Ask several IP and domain blocklists at once, addresses reversed by octet or IPv6 nibble, with return codes named, TXT reasons read, and a list's refusal, failure or silence told apart from a listing.emailnetworkdnsabuse4 views · 8 downloadsDKIM, SPF and DMARC email_auth.qwer · 1.0.0Messages signed and checked with DKIM (RSA and Ed25519), SPF records read and evaluated for a sending IP, and DMARC policies applied, with DNS records given as a map so it runs offline.emailsigningdnssecurity6 views · 5 downloadsDMARC reports dmarc_report.qwer · 1.0.0Read DMARC aggregate reports from their XML or their gzip and zip attachments, with a limit on what they unpack to, into records and summaries by source and alignment that show who sends as your domain and what fails.emaildnsreportsxml2 views · 6 downloadsCSS inliner css_inline.qwer · 1.0.0CSS inlined into HTML for email: style blocks and extra CSS written into each element's style attribute, with nothing ever fetched.emailcsshtmltemplates7 views · 6 downloadsDomains domains.qwer · 1.0.0Domain names read by the Public Suffix List: registrable domain, subdomain and suffix, same-site checks for redirects and cookies, and international names to punycode and back.urlsdnscookiessecurityi18n7 views · 7 downloadsLink finder link_extract.qwer · 1.0.0URLs and emails found in plain text, with trailing punctuation handled, and the text turned into safe HTML with clickable, nofollow links.textlinkscommentsemailhtml7 views · 6 downloads
Thread
78Useful? Like it, or add to its thread below.

Thread 0 posts

On DNS: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.