# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Try to find our DHCP server(s)."""

if __name__ == "__main__":
    import os
    import sys
    try:
        import sigma
    except ImportError:
        sys.path.append(os.path.join(os.pardir, os.pardir))
    del sys
    del os

import os
import re
import time
import glob
import datetime
import platform
import logging

from sigma.gather import gathering

logger = logging.getLogger("gather.gather_dhcpserver")

def parse_dhclient_leases(table, f):
    """Parse a Linux-style leases file."""
    # We do a superficial parse: records are enclosed in "lease {" and "}".
    # The line we look for is "option dhcp-server-identifier <ipaddr>;".
    # The expiration date/time is given in a line "expire <N> <date> <time>;".
    # We ignore records whose expiration date is over a week ago.
    # There are a lot of dups, so we keep track of the ones we've seen.
    today = datetime.date.fromtimestamp(time.time())
    one_week_ago = today - datetime.timedelta(days=7)
    expiration_cutoff = one_week_ago.strftime("%Y/%m/%d")
    expired = server = None
    seen = []
    in_record = False
    for line in f:
        if not in_record:
            if re.match(r"^\s*lease\s*{", line):
                in_record = True
                expired = False
                server = None
        elif re.match(r"^\s*}", line):
            in_record = False
            if server and not expired and server not in seen:
                table.add([server])
                seen.append(server)
        else:
            m = re.match(
                r"^\s*option\s+dhcp-server-identifier\s+([\d.]+)\s*;", line)
            if m:
                server = m.group(1)
            else:
                m = re.match(r"^\s*expire\s+\S+\s+(\S+)\s+\S+\s*;", line)
                if m:
                    date = m.group(1)
                    expired = date < expiration_cutoff
                    #print "%s = %s < %s" % (expired, date, expiration_cutoff)

def parse_hpux_dhcpclient_data(table):
    """Parse the file containing the lease information.

    According to man dhcpclient, this is /etc/dhcpclient.data and the
    format uses the format

        <code> <length> <data>

    where <code> and <length> are decimal integers; <data> is
    whatever, newline-terminated.  (Apparently <length> refers to the
    size of the data in some binary structure, not in the file.)

    We assume that the data fields making up one record (codes 00-24)
    occur consecutively and in numerical order.

    The codes we look for are 07 (lease_expiration) and 16
    (server_addr).

    As for Linux, we keep only those server_addr values whose
    corresponding lease_expiration is less than a week ago.  Also,
    like Linux, we skip duplicates.

    """
    fn = "/etc/dhcpclient.data"
    if not os.path.exists(fn):
        #print "No file", fn
        return
    try:
        f = open(fn)
    except IOError as err:
        #print "Can't open", fn, ":", err
        return
    try:
        seen = []
        expiration_cutoff = time.time() - 7*24*3600 # One week ago
        lease_expiration = server_addr = None
        for line in f:
            m = re.match(r"^\s*(\d+)\s+\d+\s+(.*)$", line)
            if m:
                code, data = m.group(1, 2)
                code = int(code)
                if code == 7:
                    try:
                        lease_expiration = int(data)
                    except ValueError:
                        pass
                elif code == 16:
                    server_addr = data
                    if (lease_expiration is not None and
                        lease_expiration >= expiration_cutoff and
                        server_addr not in seen):
                        seen.append(server_addr)
                        table.add([server_addr])
                    lease_expiration = server_addr = None
                        
    finally:
        f.close()

def do_osx(table):
    regex = re.compile(r"\s*Server Identifier:\s+([\d.]+)\s*$")
    pipe = os.popen("/usr/sbin/system_profiler SPNetworkDataType", "r")
    try:
        for line in pipe:
            m = regex.match(line)
            if m:
                table.add([m.group(1)])
    finally:
        pipe.close()

def do_aix(table):
    # XXX Look for lease expiration?
    regex = re.compile(r"\s+option\s54\s(\d+\s\d+\s\d+\s\d+)\s*$")
    try:
        f = open("/etc/dhcpc.opt")
    except IOError as err:
        return # No file -- must not have a DHCP client daemon running
    try:
        for line in f:
            m = regex.match(line)
            if m:
                ip = ".".join(m.group(1).split())
                table.add([ip])
    finally:
        f.close()

def Gather(collector):
    table = collector.table("dhcpserver", ["server_ip"])
    basicos = platform.uname()[0]
    if basicos in [ "Microsoft", "Microsoft Windows" ]:
        basicos = "Windows"
    if basicos == "Linux":
        # This pattern seems to work for Red Hat 9, at any rate.
        for leases_file in glob.glob("/var/lib/dhcp/dhclient*.leases"):
            try:
                f = open(leases_file, 'r')
            except os.error as err:
                continue
            try:
                parse_dhclient_leases(table, f)
            finally:
                f.close()
        # If there are no files, then we assume there is no DHCP server.
    elif basicos == "Windows":
        from win32com.client import GetObject
        try:
            items = GetObject("WinMgMts://./root/cimv2").InstancesOf("Win32_NetworkAdapterConfiguration")
            for item in items:
                ip = item.Properties_("DHCPServer").Value
                if ip:
                    table.add([ip])        
        except:
            logger.exception("dhcpserver gather failed")
    elif basicos == "SunOS":
        cmd = "/sbin/dhcpinfo ServerID 2>/dev/null"
        server = gathering.cmdoutput(cmd).rstrip()
        table.add([server])
    elif basicos == "HP-UX":
        parse_hpux_dhcpclient_data(table)
    elif basicos == "Darwin":
        do_osx(table)
    elif basicos == "AIX":
        do_aix(table)
    else:
        raise RuntimeError("can't determine DHCP server on the platform "
        + basicos)

def _test():
    from sigma.gather import gathering
    gathering.test(Gather)

if __name__ == "__main__":
    _test()
