🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
#!/usr/bin/env python3
"""Audyt http:// -> https:// w polach source/sources/url receptur. Wersja POPRAWNA.
Poprzednia wersja miala blad: sondowala URL http:// i wynik zapisywala jako wynik
https. Tutaj jawnie sondowane sa OBA warianty, przez curl (ten sam klient, ktorego
uzywa pagbuild), z cache i zapisem przyrostowym.
Klasyfikacja kazdego wystapienia:
SAFE_HTTPS - https dziala, a tresc wyglada na ta sama co http -> podmienic
SAFE_HTTPS_UNVERIFIED - https dziala, ale http nie (nie ma z czym porownac)
CONTENT_DIFF - https dziala, ale inne ctype/clen niz http -> NIE podmieniac automatycznie
HTTP_ONLY - tylko http dziala -> NIE podmieniac
BOTH_DEAD - oba nie dzialaja -> zrodlo martwe
UNRESOLVED - nie rozwiazano placeholderow
"""
import collections
import concurrent.futures as cf
import json
import os
import re
import subprocess
import sys
import threading
import yaml
R = "/var/lib/pagan-sync/recipes"
OUT = "/tmp/https-audit.json"
CACHE = "/tmp/curl-probe-cache.json"
KEYS = ("source", "sources", "url")
PH = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
UA = "curl/8.7.1"
FMT = "%{http_code}|%{size_download}|%{content_type}|%{url_effective}"
_lock = threading.Lock()
cache = {}
def as_list(v):
if v is None:
return []
return v if isinstance(v, list) else [v]
def expand(s, d):
missing = []
def rep(m):
k = m.group(1)
if k in d and not isinstance(d[k], (dict, list)):
return str(d[k])
missing.append(k)
return m.group(0)
return PH.sub(rep, s), bool(missing)
def to_https(u):
return "https://" + u[len("http://"):]
def curl_once(url, head, timeout=25):
args = ["curl", "-sS", "-o", "/dev/null", "-L", "--max-time", str(timeout),
"-A", UA, "-w", FMT]
if head:
args.append("-I")
else:
args += ["-r", "0-0"]
args.append(url)
p = subprocess.run(args, capture_output=True, text=True)
out = (p.stdout or "").strip().splitlines()
if not out:
return {"status": 0, "ok": False, "curl_rc": p.returncode, "err": (p.stderr or "").strip()[:120]}
parts = out[-1].split("|")
if len(parts) != 4:
return {"status": 0, "ok": False, "curl_rc": p.returncode, "err": "bad-format"}
code = int(parts[0]) if parts[0].isdigit() else 0
return {
"status": code,
"ok": 200 <= code < 400,
"size": parts[1],
"ctype": parts[2],
"final": parts[3],
"curl_rc": p.returncode,
}
def probe(url):
"""HEAD, potem (jesli HEAD odrzucony) GET z Range 0-0."""
r = curl_once(url, head=True)
if r["ok"]:
return r
if r["status"] in (400, 403, 405, 501) or r["status"] == 0:
r2 = curl_once(url, head=False)
if r2["ok"]:
return r2
if r2["status"] and r2["status"] not in (0,):
return r2
# zostaw wersje z HEAD, jesli GET tez padl (zwykle URLError/TLS)
return r2 if r2["status"] else r
return r
def cached_probe(url):
with _lock:
if url in cache:
return cache[url]
res = probe(url)
with _lock:
cache[url] = res
return res
def save_cache():
with _lock:
snap = dict(cache)
tmp = CACHE + ".tmp"
json.dump(snap, open(tmp, "w"))
os.replace(tmp, CACHE)
def main():
global cache
if os.path.exists(CACHE):
try:
cache = json.load(open(CACHE))
except Exception:
cache = {}
occ = []
for dp, _d, fs in os.walk(R):
if "PAGBUILD.yaml" not in fs:
continue
p = os.path.join(dp, "PAGBUILD.yaml")
rel = p[len(R) + 1:]
try:
d = yaml.safe_load(open(p, encoding="utf-8")) or {}
except Exception:
continue
for k in KEYS:
for v in as_list(d.get(k)):
s = str(v)
if s.startswith("http://"):
ex, unres = expand(s, d)
occ.append((rel, k, s, ex, unres))
http_urls = sorted({o[3] for o in occ if not o[4]})
https_urls = [to_https(u) for u in http_urls]
print(f"wystapien http://: {len(occ)} | unikalnych http URL: {len(http_urls)}", flush=True)
todo_h = [u for u in http_urls if u not in cache]
todo_s = [u for u in https_urls if u not in cache]
print(f"do sondowania: http={len(todo_h)} https={len(todo_s)}", flush=True)
def run_pass(urls, label):
done = 0
t = 0
with cf.ThreadPoolExecutor(max_workers=12) as pool:
futs = {pool.submit(cached_probe, u): u for u in urls}
for fut in cf.as_completed(futs):
done += 1
if done % 50 == 0:
save_cache()
print(f" [{label}] {done}/{len(urls)}", flush=True)
save_cache()
run_pass(todo_h, "http ")
run_pass(todo_s, "https")
out = collections.defaultdict(list)
for rel, k, raw, ex, unres in occ:
if unres:
out["UNRESOLVED"].append({"recipe": rel, "key": k, "raw": raw,
"expanded": ex, "detail": "placeholdery"})
continue
h = cache.get(ex, {})
s = cache.get(to_https(ex), {})
hok, sok = h.get("ok", False), s.get("ok", False)
if sok and hok:
same = (h.get("size") == s.get("size") and h.get("ctype") == s.get("ctype"))
cls = "SAFE_HTTPS" if same else "CONTENT_DIFF"
elif sok and not hok:
cls = "SAFE_HTTPS_UNVERIFIED"
elif hok and not sok:
cls = "HTTP_ONLY"
else:
cls = "BOTH_DEAD"
out[cls].append({"recipe": rel, "key": k, "raw": raw, "expanded": ex,
"http": h, "https": s})
json.dump(out, open(OUT, "w"), indent=1, ensure_ascii=False)
print()
for cls in ("SAFE_HTTPS", "SAFE_HTTPS_UNVERIFIED", "CONTENT_DIFF", "HTTP_ONLY",
"BOTH_DEAD", "UNRESOLVED"):
print(f"{cls:24s}: {len(out.get(cls, []))}")
print(f"\nzapisano {OUT}")
if __name__ == "__main__":
sys.exit(main())