2021-09-27 18:32:28 +02:00

263 lines
7.4 KiB
Python
Executable File

#! /usr/bin/env python3
import base64
import csv
import os
import subprocess
import sys
from glob import glob
INCOMPATIBLE_WITH_LEGACY_VERSIONS = ["cira-family", "cira-private", "cira-protected"]
CURRENT_DIR = "v3"
LEGACY_DIR = "v2"
HISTORIC_DIR = "v1"
MINISIGN_PK = "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3"
class Entry:
name = None
description = None
stamps = None
def __init__(self, name, description, stamps):
self.name = name
self.description = description
self.stamps = stamps
@staticmethod
def parse(raw_entry):
description = ""
stamps = []
lines = raw_entry.strip().splitlines()
if len(lines) < 2:
return None
name = lines[0].strip()
previous_was_blank = False
for line in lines[1:]:
line = line.strip()
if previous_was_blank is True and line == "":
continue
previous_was_blank = False
if line.startswith("sdns://"):
stamps.append(line)
else:
description = description + line + "\n"
description = description.strip()
if len(name) < 2 or len(description) < 10 or len(stamps) < 1:
return None
return Entry(name, description, stamps)
def format(self):
out = "## " + self.name + "\n\n"
out = out + self.description + "\n\n"
for stamp in self.stamps:
out = out + stamp + "\n"
return out
def format_legacy(self):
out = "## " + self.name + "\n\n"
out = out + self.description + "\n\n"
out = out + self.stamps[0] + "\n"
return out
def csv_entry(self):
parsed = DNSCryptStamp.parse(self.stamps[0])
if parsed is None:
return None
version = 2
if self.name.find("cisco") >= 0:
version = 1
dnssec = "no"
nolog = "no"
namecoin = "no"
if parsed.dnssec:
dnssec = "yes"
if parsed.nolog:
nolog = "yes"
csv_entry = [
self.name,
self.name,
self.description.splitlines(False)[0],
None,
None,
None,
version,
dnssec,
nolog,
namecoin,
parsed.addr,
parsed.provider,
parsed.pk,
None,
]
return csv_entry
class DNSCryptStamp:
dnssec = False
nolog = False
nofilter = False
addr = None
pk = None
provider = None
@staticmethod
def parse(stamp):
bin = base64.urlsafe_b64decode(stamp.removeprefix("sdns://") + "==")
i = 0
if bin[i] != 0x01:
return None
i = i + 1
parsed = DNSCryptStamp()
props = bin[i]
parsed.dnssec = not not ((props >> 0) & 1)
parsed.nolog = not not ((props >> 1) & 1)
parsed.nofilter = not not ((props >> 2) & 1)
i = i + 8
addr_len = bin[i]
i = i + 1
parsed.addr = bin[i : i + addr_len].decode("utf-8")
i = i + addr_len
pk_len = bin[i]
i = i + 1
if pk_len != 32:
return None
hpk = bin[i : i + pk_len].hex().upper()
hpks = []
for j in range(0, 16):
hpks.append(hpk[j * 4 : j * 4 + 4])
parsed.pk = ":".join(hpks)
i = i + pk_len
provider_len = bin[i]
i = i + 1
parsed.provider = bin[i : i + provider_len].decode("utf-8")
i = i + provider_len
return parsed
def process(md_path, signatures_to_update):
md_legacy_path = LEGACY_DIR + "/" + os.path.basename(md_path)
csv_historic_path = HISTORIC_DIR + "/" + "dnscrypt-resolvers.csv"
print("\n[" + md_path + "]")
entries = {}
previous_content = ""
out = ""
out_legacy = """
# *** THIS LIST IS FOR OLD DNSCRYPT-PROXY VERSIONS ***
Version 2 of the list is for dnscrypt-proxy <= 2.0.42 users.
If you are running up-to-date software, replace `/v2/` with `/v3/` in the sources URLs
of the `dnscrypt-proxy.toml` file (relevant lines start with `urls = ['https://...']`
and are present in the `[sources]` section).
THIS LIST IS AUTOMATICALLY GENERATED AS A SUBSET OF THE V3 LIST. DO NOT EDIT IT MANUALLY.
If you want to contribute changes to a resolvers list, only edit files from the `v3` directory.
--
"""
csv_entries = []
with open(md_path) as f:
previous_content = f.read()
c = previous_content.split("\n## ")
out = out + c[0].strip() + "\n\n"
raw_entries = c[1:]
for i in range(0, len(raw_entries)):
entry = Entry.parse(raw_entries[i])
if not entry:
print("Invalid entry: [" + raw_entries[i] + "]", file=sys.stderr)
continue
if entry.name in entries:
print("Duplicate entry: [" + entry.name + "]", file=sys.stderr)
entries[entry.name] = entry
for name in sorted(entries.keys()):
entry = entries[name]
out = out + "\n" + entry.format() + "\n"
if not name in INCOMPATIBLE_WITH_LEGACY_VERSIONS:
out_legacy = out_legacy + "\n" + entry.format_legacy() + "\n"
if os.path.basename(md_path) == "public-resolvers.md":
for name in sorted(entries.keys()):
entry = entries[name]
csv_entry = entry.csv_entry()
if csv_entry:
csv_entries.append(entry.csv_entry())
if out == previous_content:
print("No changes")
else:
with open(md_path + ".tmp", "wt") as f:
f.write(out)
os.rename(md_path + ".tmp", md_path)
# Legacy
if (
os.path.basename(md_path) == "odoh-relays.md"
or os.path.basename(md_path) == "odoh-servers.md"
):
md_legacy_path = md_path
else:
with open(md_legacy_path) as f:
previous_content = f.read()
if out_legacy == previous_content:
print("No changes to the legacy version")
else:
with open(md_legacy_path + ".tmp", "wt") as f:
f.write(out_legacy)
os.rename(md_legacy_path + ".tmp", md_legacy_path)
# Historic
if len(csv_entries) != 0:
with open(csv_historic_path, "wt") as f:
w = csv.writer(f, dialect="unix", quoting=csv.QUOTE_MINIMAL)
w.writerow(
[
"Name",
"Full name",
"Description",
"Location",
"Coordinates",
"URL",
"Version",
"DNSSEC validation",
"No logs",
"Namecoin",
"Resolver address",
"Provider name",
"Provider public key",
"Provider public key TXT record",
]
)
for csv_entry in csv_entries:
w.writerow(csv_entry)
# Signatures
for path in [md_path, md_legacy_path, csv_historic_path]:
try:
subprocess.run(
["minisign", "-V", "-P", MINISIGN_PK, "-m", path], check=True
)
except subprocess.CalledProcessError:
signatures_to_update.append(path)
signatures_to_update = []
for md_path in glob(CURRENT_DIR + "/*.md"):
process(md_path, signatures_to_update)
if signatures_to_update:
subprocess.run(["minisign", "-Sm", *signatures_to_update])