#!/usr/bin/env python3
# OVPN Client — Linux kömekçisi.
# pkexec arkaly root hukugy bilen işledilýär we diňe barlanan konfig bilen OpenVPN-i başladýar.
#
#   pkexec /usr/lib/ovpn-client/ovpn-helper [--stop-others] <profile.ovpn> <management_port> <mgmt.pw> [<auth.txt>]
#
# --stop-others: birikmezden öň başga VPN-leri öçürýär (openvpn müşderi prosesleri, WireGuard, NetworkManager VPN,
#                warp-cli / nordvpn / expressvpn / mullvad). OpenVPN SERWER prosesleri (server direktiwasy) degilmeýär.
#
# Howpsuzlyk: konfigdäki skript/plugin/faýl ýazmak opsiýalary ret edilýär, faýllar diňe
# çagyran ulanyjynyň öz bukjasyndan kabul edilýär. DNS üçin diňe ulgamdaky ynamly
# skriptler (update-systemd-resolved / update-resolv-conf) ulanylýar.
import os
import pwd
import re
import shutil
import signal
import stat
import subprocess
import sys
import time

FORBIDDEN = {
    "up", "down", "route-up", "route-pre-down", "ipchange", "learn-address", "tls-verify", "client-connect",
    "client-disconnect", "auth-user-pass-verify", "plugin", "script-security", "log", "log-append", "status",
    "writepid", "cd", "chroot", "config", "tmp-dir", "management", "management-client-user",
    "management-client-group", "management-hold", "management-query-passwords", "daemon", "syslog",
    "replay-persist", "dev-node", "iproute", "user", "group", "up-restart", "down-pre", "service", "echo",
    "persist-tun-script", "route-noexec", "ifconfig-noexec",
}
PATH_OPTS = {
    "ca", "cert", "key", "tls-auth", "tls-crypt", "tls-crypt-v2", "pkcs12", "dh", "extra-certs", "crl-verify",
    "secret", "http-proxy-user-pass", "auth-user-pass", "askpass", "capath",
}


def die(msg, code=2):
    sys.stderr.write("ovpn-helper: " + msg + "\n")
    sys.exit(code)


def caller_uid():
    for k in ("PKEXEC_UID", "SUDO_UID"):
        v = os.environ.get(k)
        if v and v.isdigit():
            return int(v)
    if os.getuid() == 0 and os.environ.get("OVPN_HELPER_ALLOW_ROOT") == "1":
        return 0
    die("pkexec arkaly işlediň")


def check_file(path, uid, home):
    try:
        st = os.lstat(path)
    except OSError:
        die("faýl tapylmady: " + path)
    if stat.S_ISLNK(st.st_mode) or not stat.S_ISREG(st.st_mode):
        die("adaty faýl däl: " + path)
    if st.st_uid != uid:
        die("faýl ulanyja degişli däl: " + path)
    real = os.path.realpath(path)
    if uid != 0 and not (real + "/").startswith(home.rstrip("/") + "/"):
        die("faýl ulanyjynyň bukjasynda däl: " + path)
    if st.st_size > 2_000_000:
        die("faýl gaty uly")
    return real


def validate_config(text):
    inline = None
    for n, raw in enumerate(text.splitlines(), 1):
        line = raw.strip()
        if inline:
            if line.lower() == "</" + inline + ">":
                inline = None
            continue
        if not line or line[0] in "#;":
            continue
        if line.startswith("<") and line.endswith(">") and not line.startswith("</"):
            inline = line[1:-1].strip().lower()
            continue
        parts = line.split()
        opt = parts[0].lower()
        if opt.startswith("--"):
            opt = opt[2:]
        if opt in FORBIDDEN:
            die("rugsat berilmeýän opsiýa (setir %d): %s" % (n, opt))
        if opt in PATH_OPTS and len(parts) > 1 and parts[1] != "[inline]" and not parts[1].isdigit():
            die("faýl ýoly bilen opsiýa rugsat berilmeýär (setir %d): %s" % (n, opt))
        if opt in ("setenv", "setenv-safe") and opt == "setenv" and (len(parts) < 2 or parts[1] != "opt"):
            die("setenv rugsat berilmeýär (setir %d)" % n)
    if inline:
        die("ýapylmadyk blok: <%s>" % inline)


def dns_args():
    sd = "/etc/openvpn/update-systemd-resolved"
    rc = "/etc/openvpn/update-resolv-conf"
    # systemd-resolved hakykatdan işleýärmi (D-Bus + Resolve soketi)?
    resolved = (os.path.exists("/run/systemd/resolve/io.systemd.Resolve") and os.path.exists("/run/dbus/system_bus_socket")
                and "systemd" in os.path.realpath("/etc/resolv.conf"))
    if os.path.isfile(sd) and resolved:
        return ["--script-security", "2", "--up", sd, "--up-restart", "--down", sd, "--down-pre",
                "--dhcp-option", "DOMAIN-ROUTE", "."]
    if os.path.isfile(rc) and shutil.which("resolvconf"):
        return ["--script-security", "2", "--up", rc, "--down", rc, "--down-pre"]
    return ["--script-security", "1"]


def _sh(cmd, timeout=15):
    try:
        r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=timeout, universal_newlines=True)
        return r.returncode, r.stdout or ""
    except Exception:
        return 1, ""


def _cmdline(pid):
    try:
        with open("/proc/%d/cmdline" % pid, "rb") as f:
            return [x.decode("utf-8", "replace") for x in f.read().split(b"\0") if x]
    except OSError:
        return []


def _is_server_proc(args, pid=None):
    """OpenVPN serwer prosesi bolsa True (ony ýapmaýarys)."""
    if "--server" in args or ("--mode" in args and "server" in args):
        return True
    cfg = cd = None
    for i, a in enumerate(args):
        if i + 1 < len(args):
            if a == "--config":
                cfg = args[i + 1]
            elif a == "--cd":
                cd = args[i + 1]
    if cfg is None and len(args) > 1 and not args[1].startswith("-"):
        cfg = args[1]
    if cfg:
        base = cd or "/"
        if not os.path.isabs(base):
            try:
                base = os.path.join(os.readlink("/proc/%d/cwd" % pid), base) if pid else "/"
            except OSError:
                base = "/"
        if not cd and pid and not os.path.isabs(cfg):
            try:
                base = os.readlink("/proc/%d/cwd" % pid)
            except OSError:
                pass
        path = cfg if os.path.isabs(cfg) else os.path.join(base, cfg)
        try:
            with open(path, "r", encoding="utf-8", errors="replace") as f:
                for line in f:
                    if re.match(r"^\s*(server|server-bridge|mode\s+server)(\s|$)", line.lower()):
                        return True
        except OSError:
            pass
    return False


def stop_other_vpns():
    """Başga VPN-leri öçürýär; her öçürilen üçin 'ovpn-helper: stopped ...' setiri çap edýär."""
    stopped = []
    me = os.getpid()
    # 1) başga OpenVPN müşderi prosesleri (systemd bölümleri ilki dolandyryş arkaly)
    victims = []
    for name in os.listdir("/proc"):
        if not name.isdigit() or int(name) == me:
            continue
        pid = int(name)
        try:
            comm = open("/proc/%d/comm" % pid).read().strip()
        except OSError:
            continue
        if comm not in ("openvpn", "openvpn-xor"):
            continue
        args = _cmdline(pid)
        if _is_server_proc(args, pid):
            continue
        victims.append(pid)
    for pid in victims:
        unit = None
        try:
            for line in open("/proc/%d/cgroup" % pid):
                m = re.search(r"/(openvpn(?:-client)?@[^/]+\.service)", line)
                if m:
                    unit = m.group(1)
        except OSError:
            pass
        if unit:
            _sh(["systemctl", "stop", unit])
            stopped.append("systemd " + unit)
    for pid in victims:
        try:
            os.kill(pid, signal.SIGTERM)
            stopped.append("openvpn (pid %d)" % pid)
        except OSError:
            continue
    t0 = time.time()
    while victims and time.time() - t0 < 4:
        victims = [p for p in victims if os.path.exists("/proc/%d" % p)]
        if victims:
            time.sleep(0.2)
    for pid in victims:
        try:
            os.kill(pid, signal.SIGKILL)
        except OSError:
            pass
    # 2) NetworkManager VPN / WireGuard birikmeleri
    nmcli = shutil.which("nmcli")
    if nmcli:
        rc, out = _sh([nmcli, "-t", "-f", "NAME,TYPE", "connection", "show", "--active"])
        for line in out.splitlines():
            name, _, typ = line.rpartition(":")
            if typ in ("vpn", "wireguard") and name:
                _sh([nmcli, "connection", "down", "id", name])
                stopped.append("NetworkManager " + name)
    # 3) wg-quick / wg interfeýsleri
    wg = shutil.which("wg")
    if wg:
        rc, out = _sh([wg, "show", "interfaces"])
        for ifc in out.split():
            if not re.match(r"^[A-Za-z0-9_=+.-]{1,15}$", ifc):
                continue
            q = shutil.which("wg-quick")
            rc = 1
            if q:
                rc, _o = _sh([q, "down", ifc])
            if rc != 0:
                _sh(["ip", "link", "del", ifc])
            stopped.append("WireGuard " + ifc)
    # 4) satyjy CLI-ler
    for exe, st, off, mark in (("warp-cli", ["status"], ["disconnect"], "Status update: Connected"),
                               ("nordvpn", ["status"], ["disconnect"], "Status: Connected"),
                               ("expressvpn", ["status"], ["disconnect"], "Connected to"),
                               ("mullvad", ["status"], ["disconnect"], "Connected")):
        path = shutil.which(exe)
        if not path:
            continue
        rc, out = _sh([path] + st)
        if mark in out and "Disconnected" not in out.split("\n")[0]:
            _sh([path] + off)
            stopped.append(exe)
    for x in stopped:
        print("ovpn-helper: stopped other VPN: " + x)
    sys.stdout.flush()
    return stopped


def main():
    stop_others = False
    if len(sys.argv) > 1 and sys.argv[1] == "--stop-others":
        stop_others = True
        del sys.argv[1]
    if len(sys.argv) not in (4, 5):
        die("ulanyş: ovpn-helper [--stop-others] <profile.ovpn> <port> <mgmt.pw> [auth.txt]")
    if os.geteuid() != 0:
        die("root hukugy gerek (pkexec)")
    uid = caller_uid()
    home = pwd.getpwuid(uid).pw_dir if uid != 0 else "/"
    cfg = check_file(sys.argv[1], uid, home)
    port = sys.argv[2]
    if not port.isdigit() or not 1024 <= int(port) <= 65535:
        die("nädogry port")
    pw = check_file(sys.argv[3], uid, home)
    if os.path.dirname(pw) != os.path.dirname(cfg):
        die("mgmt.pw faýly konfig bilen bir bukjada bolmaly")
    auth = None
    if len(sys.argv) == 5:
        auth = check_file(sys.argv[4], uid, home)
        if os.path.dirname(auth) != os.path.dirname(cfg):
            die("auth faýly konfig bilen bir bukjada bolmaly")
    with open(cfg, "r", encoding="utf-8", errors="replace") as f:
        validate_config(f.read())

    ovpn = shutil.which("openvpn") or next((p for p in ("/usr/sbin/openvpn", "/sbin/openvpn", "/usr/local/sbin/openvpn") if os.path.exists(p)), None)
    if not ovpn:
        die("OpenVPN gurnalmadyk: sudo apt install openvpn", 3)
    args = [ovpn, "--config", cfg, "--management", "127.0.0.1", port, pw, "--management-hold",
            "--setenv", "IV_GUI_VER", "OVPN-Client-Linux", "--verb", "3"]
    args += dns_args()
    if auth:
        args += ["--auth-user-pass", auth, "--auth-nocache"]
    os.chdir("/")
    if stop_others:
        try:
            stop_other_vpns()
        except Exception as e:   # başga VPN-i öçürip bolmasa-da birikmäge päsgel bermeýäris
            print("ovpn-helper: could not stop other VPNs: %s" % e)
    sys.stdout.flush()
    os.execv(ovpn, args)


if __name__ == "__main__":
    main()
