#!/usr/bin/env python3
"""Servidor DHCP mínimo para ver discover, offer, request y ack (RFC 2131).

Es material de clase, no un servidor de verdad: una sola subred, sin renovar
concesiones y sin revisar conflictos. Responde siempre por broadcast.

Uso: dhcp_server.py <interfaz> <dirección del servidor> <primer host> <último host>
Ejemplo: dhcp_server.py eth1 10.200.50.1 10.200.50.100 10.200.50.110
"""
import ipaddress
import socket
import struct
import sys

BOOTREQUEST, BOOTREPLY = 1, 2
MAGIC_COOKIE = b"\x63\x82\x53\x63"
DISCOVER, OFFER, REQUEST, ACK = 1, 2, 3, 5
LEASE_SECONDS = 600
NETMASK = "255.255.255.0"
MESSAGE_NAMES = {1: "DISCOVER", 2: "OFFER", 3: "REQUEST", 5: "ACK"}


def parse_options(data):
    """Devuelve las opciones DHCP como un diccionario código -> bytes."""
    options = {}
    i = 0
    while i < len(data) and data[i] != 255:
        if data[i] == 0:  # relleno
            i += 1
            continue
        code, length = data[i], data[i + 1]
        options[code] = data[i + 2 : i + 2 + length]
        i += 2 + length
    return options


def build_reply(request, message_type, offered, server):
    """Arma la respuesta copiando xid, flags y chaddr del pedido."""
    header = struct.pack("!BBBB", BOOTREPLY, 1, 6, 0) + request[4:12]
    header += b"\0" * 4 + socket.inet_aton(offered)  # ciaddr, yiaddr
    header += socket.inet_aton(server) + b"\0" * 4  # siaddr, giaddr
    header += request[28:44] + b"\0" * 192  # chaddr, sname y file
    options = bytes([53, 1, message_type])
    options += bytes([54, 4]) + socket.inet_aton(server)
    options += bytes([51, 4]) + struct.pack("!I", LEASE_SECONDS)
    options += bytes([1, 4]) + socket.inet_aton(NETMASK)
    options += bytes([3, 4]) + socket.inet_aton(server)  # router por defecto
    return header + MAGIC_COOKIE + options + b"\xff"


def main():
    if len(sys.argv) != 5:
        sys.exit(__doc__)
    device, server, first, last = sys.argv[1:]
    pool = [str(ipaddress.ip_address(n)) for n in range(
        int(ipaddress.ip_address(first)), int(ipaddress.ip_address(last)) + 1)]
    leases = {}  # MAC -> dirección

    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, device.encode())
    sock.bind(("0.0.0.0", 67))

    while True:
        data, _ = sock.recvfrom(1500)
        if len(data) < 240 or data[0] != BOOTREQUEST or data[236:240] != MAGIC_COOKIE:
            continue
        options = parse_options(data[240:])
        kind = options.get(53, b"\0")[0]
        mac = data[28:34]
        if kind == DISCOVER:
            free = [a for a in pool if a not in leases.values()]
            if mac not in leases and not free:
                print("sin direcciones libres", flush=True)
                continue
            leases.setdefault(mac, free[0] if free else None)
            answer = OFFER
        elif kind == REQUEST and mac in leases:
            answer = ACK
        else:
            continue
        print(f"{MESSAGE_NAMES[kind]} de {mac.hex(':')} -> "
              f"{MESSAGE_NAMES[answer]} {leases[mac]}", flush=True)
        reply = build_reply(data, answer, leases[mac], server)
        sock.sendto(reply, ("255.255.255.255", 68))


if __name__ == "__main__":
    main()
