Source code for pyrad.server

# server.py
#
# Copyright 2003-2004,2007,2016 Wichert Akkerman <wichert@wiggy.net>

import select
import socket
from pyrad import host
from pyrad import packet
import logging


logger = logging.getLogger('pyrad')


[docs]class RemoteHost: """Remote RADIUS capable host we can talk to.""" def __init__(self, address, secret, name, authport=1812, acctport=1813, coaport=3799): """Constructor. :param address: IP address :type address: string :param secret: RADIUS secret :type secret: string :param name: short name (used for logging only) :type name: string :param authport: port used for authentication packets :type authport: integer :param acctport: port used for accounting packets :type acctport: integer :param coaport: port used for CoA packets :type coaport: integer """ self.address = address self.secret = secret self.authport = authport self.acctport = acctport self.coaport = coaport self.name = name
[docs]class ServerPacketError(Exception): """Exception class for bogus packets. ServerPacketError exceptions are only used inside the Server class to abort processing of a packet. """
[docs]class Server(host.Host): """Basic RADIUS server. This class implements the basics of a RADIUS server. It takes care of the details of receiving and decoding requests; processing of the requests should be done by overloading the appropriate methods in derived classes. :ivar hosts: hosts who are allowed to talk to us :type hosts: dictionary of Host class instances :ivar _poll: poll object for network sockets :type _poll: select.poll class instance :ivar _fdmap: map of filedescriptors to network sockets :type _fdmap: dictionary :cvar MaxPacketSize: maximum size of a RADIUS packet :type MaxPacketSize: integer """ MaxPacketSize = 8192 def __init__(self, addresses=[], authport=1812, acctport=1813, coaport=3799, hosts=None, dict=None, auth_enabled=True, acct_enabled=True, coa_enabled=False): """Constructor. :param addresses: IP addresses to listen on :type addresses: sequence of strings :param authport: port to listen on for authentication packets :type authport: integer :param acctport: port to listen on for accounting packets :type acctport: integer :param coaport: port to listen on for CoA packets :type coaport: integer :param hosts: hosts who we can talk to :type hosts: dictionary mapping IP to RemoteHost class instances :param dict: RADIUS dictionary to use :type dict: Dictionary class instance :param auth_enabled: enable auth server (default True) :type auth_enabled: bool :param acct_enabled: enable accounting server (default True) :type acct_enabled: bool :param coa_enabled: enable coa server (default False) :type coa_enabled: bool """ host.Host.__init__(self, authport, acctport, coaport, dict) if hosts is None: self.hosts = {} else: self.hosts = hosts self.auth_enabled = auth_enabled self.authfds = [] self.acct_enabled = acct_enabled self.acctfds = [] self.coa_enabled = coa_enabled self.coafds = [] for addr in addresses: self.BindToAddress(addr)
[docs] def BindToAddress(self, addr): """Add an address to listen to. An empty string indicated you want to listen on all addresses. :param addr: IP address to listen on :type addr: string """ if self.auth_enabled: authfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) authfd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) authfd.bind((addr, self.authport)) self.authfds.append(authfd) if self.acct_enabled: acctfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) acctfd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) acctfd.bind((addr, self.acctport)) self.acctfds.append(acctfd) if self.coa_enabled: coafd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) coafd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) coafd.bind((addr, self.coaport)) self.coafds.append(coafd)
[docs] def HandleAuthPacket(self, pkt): """Authentication packet handler. This is an empty function that is called when a valid authentication packet has been received. It can be overriden in derived classes to add custom behaviour. :param pkt: packet to process :type pkt: Packet class instance """
[docs] def HandleAcctPacket(self, pkt): """Accounting packet handler. This is an empty function that is called when a valid accounting packet has been received. It can be overriden in derived classes to add custom behaviour. :param pkt: packet to process :type pkt: Packet class instance """
[docs] def HandleCoaPacket(self, pkt): """CoA packet handler. This is an empty function that is called when a valid accounting packet has been received. It can be overriden in derived classes to add custom behaviour. :param pkt: packet to process :type pkt: Packet class instance """
[docs] def HandleDisconnectPacket(self, pkt): """CoA packet handler. This is an empty function that is called when a valid accounting packet has been received. It can be overriden in derived classes to add custom behaviour. :param pkt: packet to process :type pkt: Packet class instance """
def _HandleAuthPacket(self, pkt): """Process a packet received on the authentication port. If this packet should be dropped instead of processed a ServerPacketError exception should be raised. The main loop will drop the packet and log the reason. :param pkt: packet to process :type pkt: Packet class instance """ if pkt.source[0] not in self.hosts: raise ServerPacketError('Received packet from unknown host') pkt.secret = self.hosts[pkt.source[0]].secret if pkt.code != packet.AccessRequest: raise ServerPacketError( 'Received non-authentication packet on authentication port') self.HandleAuthPacket(pkt) def _HandleAcctPacket(self, pkt): """Process a packet received on the accounting port. If this packet should be dropped instead of processed a ServerPacketError exception should be raised. The main loop will drop the packet and log the reason. :param pkt: packet to process :type pkt: Packet class instance """ if pkt.source[0] not in self.hosts: raise ServerPacketError('Received packet from unknown host') pkt.secret = self.hosts[pkt.source[0]].secret if pkt.code not in [packet.AccountingRequest, packet.AccountingResponse]: raise ServerPacketError( 'Received non-accounting packet on accounting port') self.HandleAcctPacket(pkt) def _HandleCoaPacket(self, pkt): """Process a packet received on the coa port. If this packet should be dropped instead of processed a ServerPacketError exception should be raised. The main loop will drop the packet and log the reason. :param pkt: packet to process :type pkt: Packet class instance """ if pkt.source[0] not in self.hosts: raise ServerPacketError('Received packet from unknown host') pkt.secret = self.hosts[pkt.source[0]].secret if pkt.code == packet.CoARequest: self.HandleCoaPacket(pkt) elif pkt.code == packet.DisconnectRequest: self.HandleDisconnectPacket(pkt) else: raise ServerPacketError('Received non-coa packet on coa port') def _GrabPacket(self, pktgen, fd): """Read a packet from a network connection. This method assumes there is data waiting for to be read. :param fd: socket to read packet from :type fd: socket class instance :return: RADIUS packet :rtype: Packet class instance """ (data, source) = fd.recvfrom(self.MaxPacketSize) pkt = pktgen(data) pkt.source = source pkt.fd = fd return pkt def _PrepareSockets(self): """Prepare all sockets to receive packets. """ for fd in self.authfds + self.acctfds + self.coafds: self._fdmap[fd.fileno()] = fd self._poll.register(fd.fileno(), select.POLLIN | select.POLLPRI | select.POLLERR) if self.auth_enabled: self._realauthfds = list(map(lambda x: x.fileno(), self.authfds)) if self.acct_enabled: self._realacctfds = list(map(lambda x: x.fileno(), self.acctfds)) if self.coa_enabled: self._realcoafds = list(map(lambda x: x.fileno(), self.coafds))
[docs] def CreateReplyPacket(self, pkt, **attributes): """Create a reply packet. Create a new packet which can be returned as a reply to a received packet. :param pkt: original packet :type pkt: Packet instance """ reply = pkt.CreateReply(**attributes) reply.source = pkt.source return reply
def _ProcessInput(self, fd): """Process available data. If this packet should be dropped instead of processed a PacketError exception should be raised. The main loop will drop the packet and log the reason. This function calls either HandleAuthPacket() or HandleAcctPacket() depending on which socket is being processed. :param fd: socket to read packet from :type fd: socket class instance """ if fd.fileno() in self._realauthfds: pkt = self._GrabPacket(lambda data, s=self: s.CreateAuthPacket(packet=data), fd) self._HandleAuthPacket(pkt) elif fd.fileno() in self._realacctfds: pkt = self._GrabPacket(lambda data, s=self: s.CreateAcctPacket(packet=data), fd) self._HandleAcctPacket(pkt) else: pkt = self._GrabPacket(lambda data, s=self: s.CreateCoAPacket(packet=data), fd) self._HandleCoaPacket(pkt)
[docs] def Run(self): """Main loop. This method is the main loop for a RADIUS server. It waits for packets to arrive via the network and calls other methods to process them. """ self._poll = select.poll() self._fdmap = {} self._PrepareSockets() while True: for (fd, event) in self._poll.poll(): if event == select.POLLIN: try: fdo = self._fdmap[fd] self._ProcessInput(fdo) except ServerPacketError as err: logger.info('Dropping packet: ' + str(err)) except packet.PacketError as err: logger.info('Received a broken packet: ' + str(err)) else: logger.error('Unexpected event in server main loop')