🔒 Repository is read-only – file editing is disabled.

PaganLinux/tmp-http-urlaudit2.py main

200 linii Raw ← Powrót
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())