Source code for pyrad.curved

# curved.py
#
# Copyright 2002 Wichert Akkerman <wichert@wiggy.net>

"""Twisted integration code.

RADIUS servers built on Twisted: :class:`RADIUSAccess` for authentication
and :class:`RADIUSAccounting` for accounting requests, to be passed to
``reactor.listenUDP``. Twisted is not a dependency of pyrad and has to be
installed to use this module.
"""

__docformat__ = "restructuredtext en"

from twisted.internet import protocol
from twisted.internet import reactor
from twisted.python import log
import sys
from pyrad import dictionary
from pyrad import host
from pyrad import packet
from pyrad.server import CheckMessageAuthenticator
from pyrad.server import ServerPacketError
from pyrad.server import _HostAddresses


[docs] class PacketError(Exception): """A received packet is dropped. Raised by :meth:`RADIUSAccess.processPacket` and :meth:`RADIUSAccounting.processPacket` for packets of the wrong type. A :meth:`~RADIUS.processPacket` override may raise it to drop a packet: the drop is logged as one line instead of a traceback. This is not :class:`pyrad.packet.PacketError`, which it shadows in this module; packet handlers may raise either, or :class:`pyrad.server.ServerPacketError`. """
[docs] class RADIUS(host.Host, protocol.DatagramProtocol): """Base class of the Twisted RADIUS servers. Receives and decodes packets, checks that they come from a known host, verifies them and passes them to :meth:`processPacket`. Use :class:`RADIUSAccess` or :class:`RADIUSAccounting` and override :meth:`processPacket`. Before :meth:`processPacket` is called, ``pkt.secret`` is set to the secret of the host and ``pkt.source`` to its ``(host, port)``. The Message-Authenticator of an Access-Request has been verified (and with ``enforce_ma`` required), and with ``enable_pkt_verify`` the Request Authenticator of an Accounting-Request. """ def __init__(self, hosts=None, dict=None, enforce_ma=False, enable_pkt_verify=True): """ :param hosts: hosts allowed to send packets, mapping their IP address to a :class:`pyrad.server.RemoteHost` or to the secret. IPv4 clients of a socket bound to ``'::'`` are looked up by their IPv4 address. Unlike :class:`pyrad.server.Server`, there is no ``'0.0.0.0'`` entry for all other hosts: packets from other hosts are dropped. :type hosts: dict[str, pyrad.server.RemoteHost or bytes] :param dict: RADIUS dictionary used to decode the packets, by default an empty dictionary :type dict: pyrad.dictionary.Dictionary :param enforce_ma: drop Access-Requests without a Message-Authenticator (an invalid Message-Authenticator is always dropped) :type enforce_ma: bool :param enable_pkt_verify: drop Accounting-Requests with an invalid Request Authenticator :type enable_pkt_verify: bool """ if dict is None: dict = dictionary.Dictionary() host.Host.__init__(self, dict=dict) if hosts is None: hosts = {} self.hosts = hosts self.enforce_ma = enforce_ma self.enable_pkt_verify = enable_pkt_verify
[docs] def processPacket(self, pkt): """Process a received packet. Override it and call the base method first, which raises :class:`PacketError` for packets of the wrong type in :class:`RADIUSAccess` and :class:`RADIUSAccounting`. To answer, create a reply with ``pkt.CreateReply()`` and send it with ``self.transport.write(reply.ReplyPacket(), pkt.source)``. If no reply is sent, the request is dropped. Raise :class:`PacketError` to drop the packet with a one-line log message; other exceptions are logged with a traceback. :param pkt: received packet :type pkt: pyrad.packet.Packet """ pass
[docs] def createPacket(self, **kwargs): """Create the packet object for a received datagram. :class:`RADIUSAccess` creates a :class:`pyrad.packet.AuthPacket` and :class:`RADIUSAccounting` a :class:`pyrad.packet.AcctPacket`. The base method raises :exc:`NotImplementedError`; a class that doesn't override it receives generic :class:`pyrad.packet.Packet` objects. :param kwargs: arguments for the packet class, e.g. ``packet`` :return: the packet :rtype: pyrad.packet.Packet """ raise NotImplementedError('Attempted to use a pure base class')
def _createPacket(self, **kwargs): if type(self).createPacket is RADIUS.createPacket: # createPacket is not implemented, use a generic packet return self.CreatePacket(**kwargs) pkt = self.createPacket(**kwargs) if pkt is None: # an override written for 2.5.4, which never called createPacket return self.CreatePacket(**kwargs) return pkt def _HostSecret(self, key): """Return the secret of a hosts entry. The entry is a RemoteHost or the secret itself. Returns None if it has none (e.g. hosts is a set). """ try: entry = self.hosts[key] except (TypeError, KeyError): return None if isinstance(entry, bytes): return entry return getattr(entry, 'secret', None) def _verifyPacket(self, pkt): """Verify a received request the way pyrad.server.Server does. :raises pyrad.server.ServerPacketError: if the packet should be dropped :raises pyrad.packet.PacketError: if the packet should be dropped """ if pkt.code == packet.AccessRequest: CheckMessageAuthenticator(pkt, self.enforce_ma) elif pkt.code == packet.AccountingRequest and self.enable_pkt_verify: if not packet.AcctPacket.VerifyAcctRequest(pkt): raise packet.PacketError('Packet verification failed') def datagramReceived(self, datagram, source): host, port = source[:2] try: pkt = self._createPacket(packet=datagram) except packet.PacketError as err: log.msg('Dropping invalid packet: ' + str(err)) return except Exception as err: # not authenticated yet, log one line like Server._GrabPacket log.msg('Dropping invalid packet: %s: %s' % (type(err).__name__, err)) return key = next((k for k in _HostAddresses(host) if k in self.hosts), None) if key is None: log.msg('Dropping packet from unknown host ' + host) return secret = self._HostSecret(key) if secret is None: log.msg('Dropping packet from %s: no secret in its hosts entry' % host) return pkt.secret = secret pkt.source = (host, port) try: self._verifyPacket(pkt) except (ServerPacketError, packet.PacketError) as err: log.msg('Dropping packet from %s: %s' % (host, str(err))) return try: self.processPacket(pkt) except (PacketError, packet.PacketError, ServerPacketError) as err: log.msg('Dropping packet from %s: %s' % (host, str(err))) except Exception: # e.g. a ValueError from an attribute decoder or a handler bug log.err(None, 'Error processing packet from %s' % host)
[docs] class RADIUSAccess(RADIUS): """Twisted RADIUS authentication server. Override :meth:`processPacket` and call the base method first, which drops all packets except Access-Requests. The User-Password is not verified: decrypt it with :meth:`pkt.PwDecrypt <pyrad.packet.AuthPacket.PwDecrypt>`. """
[docs] def createPacket(self, **kwargs): return self.CreateAuthPacket(**kwargs)
[docs] def processPacket(self, pkt): if pkt.code != packet.AccessRequest: raise PacketError( 'non-AccessRequest packet on authentication socket')
[docs] class RADIUSAccounting(RADIUS): """Twisted RADIUS accounting server. Override :meth:`processPacket` and call the base method first, which drops all packets except Accounting-Requests. """
[docs] def createPacket(self, **kwargs): return self.CreateAcctPacket(**kwargs)
[docs] def processPacket(self, pkt): if pkt.code != packet.AccountingRequest: raise PacketError( 'non-AccountingRequest packet on accounting socket')
if __name__ == '__main__': # demo only: without hosts every packet is dropped as from an unknown # host log.startLogging(sys.stdout, 0) reactor.listenUDP(1812, RADIUSAccess()) reactor.listenUDP(1813, RADIUSAccounting()) reactor.run()