summaryrefslogtreecommitdiff
path: root/stacks/daemon/banlist.py
blob: 061ed1f237c5ed5e515c8b94a4268d8985ba04ce (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
"""Simple AX.25 source ban list for max25d — silent RX drop."""
from __future__ import annotations

import re
import sys
import threading
from pathlib import Path
from typing import Optional

DEFAULT_BANS_FILE = Path("/etc/max25/bans.txt")

_AX25_UI_SRC_RE = re.compile(r"\[AX25 UI ([^>]+)>")


def extract_ax25_source(line: str) -> Optional[str]:
    """Return source callsign from a formatted AX.25 UI RX line, or None."""
    match = _AX25_UI_SRC_RE.search(line)
    if not match:
        return None
    return match.group(1).strip().upper()


def callsign_banned(ban_entry: str, source: str) -> bool:
    """Match ban entry against an incoming source callsign."""
    ban = ban_entry.strip().upper()
    src = source.strip().upper()
    if not ban or not src:
        return False
    if "-" in ban:
        return ban == src
    return ban == src.split("-", 1)[0]


class BanList:
    """Persistent set of banned AX.25 source addresses."""

    def __init__(self, path: Path | str = DEFAULT_BANS_FILE) -> None:
        self.path = Path(path)
        self._lock = threading.Lock()
        self._entries: set[str] = set()
        self.load()

    def load(self) -> None:
        entries: set[str] = set()
        try:
            if self.path.is_file():
                text = self.path.read_text(encoding="utf-8")
                for raw in text.splitlines():
                    line = raw.split("#", 1)[0].strip().upper()
                    if line:
                        entries.add(line)
        except OSError as exc:
            print(f"max25d: banlist load failed ({self.path}): {exc}", file=sys.stderr)
        with self._lock:
            self._entries = entries

    def save(self) -> None:
        lines = sorted(self._entries)
        content = "\n".join(lines)
        if content:
            content += "\n"
        self.path.parent.mkdir(parents=True, exist_ok=True)
        self.path.write_text(content, encoding="utf-8")

    def list(self) -> list[str]:
        with self._lock:
            return sorted(self._entries)

    def add(self, callsign: str) -> None:
        call = callsign.strip().upper()
        with self._lock:
            self._entries.add(call)
            self.save()

    def remove(self, callsign: str) -> bool:
        call = callsign.strip().upper()
        with self._lock:
            if call not in self._entries:
                return False
            self._entries.remove(call)
            self.save()
            return True

    def is_banned(self, source: str) -> bool:
        src = source.strip().upper()
        if not src:
            return False
        with self._lock:
            return any(callsign_banned(entry, src) for entry in self._entries)
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com