pax_global_header00006660000000000000000000000064146522646410014524gustar00rootroot0000000000000052 comment=69e9178da9a8dda764ba832c8f57071b31ac8f07 pyrad-2.4/000077500000000000000000000000001465226464100125105ustar00rootroot00000000000000pyrad-2.4/PKG-INFO000066400000000000000000000077201465226464100136130ustar00rootroot00000000000000Metadata-Version: 1.1 Name: pyrad Version: 2.4 Summary: RADIUS tools Home-page: https://github.com/pyradius/pyrad Author: Istvan Ruzman, Christian Giese Author-email: istvan@ruzman.eu, developer@gicnet.de License: BSD Description: .. image:: https://travis-ci.org/pyradius/pyrad.svg?branch=master :target: https://travis-ci.org/pyradius/pyrad .. image:: https://coveralls.io/repos/github/pyradius/pyrad/badge.svg?branch=master :target: https://coveralls.io/github/pyradius/pyrad?branch=master .. image:: https://img.shields.io/pypi/v/pyrad.svg :target: https://pypi.python.org/pypi/pyrad .. image:: https://img.shields.io/pypi/pyversions/pyrad.svg :target: https://pypi.python.org/pypi/pyrad .. image:: https://img.shields.io/pypi/dm/pyrad.svg :target: https://pypi.python.org/pypi/pyrad .. image:: https://readthedocs.org/projects/pyrad/badge/?version=latest :target: http://pyrad.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. image:: https://img.shields.io/pypi/l/pyrad.svg :target: https://pypi.python.org/pypi/pyrad Introduction ============ pyrad is an implementation of a RADIUS client/server as described in RFC2865. It takes care of all the details like building RADIUS packets, sending them and decoding responses. Here is an example of doing a authentication request:: from __future__ import print_function from pyrad.client import Client from pyrad.dictionary import Dictionary import pyrad.packet srv = Client(server="localhost", secret=b"Kah3choteereethiejeimaeziecumi", dict=Dictionary("dictionary")) # create request req = srv.CreateAuthPacket(code=pyrad.packet.AccessRequest, User_Name="wichert", NAS_Identifier="localhost") req["User-Password"] = req.PwCrypt("password") # send request reply = srv.SendPacket(req) if reply.code == pyrad.packet.AccessAccept: print("access accepted") else: print("access denied") print("Attributes returned by server:") for i in reply.keys(): print("%s: %s" % (i, reply[i])) Requirements & Installation =========================== pyrad requires Python 2.7, or Python 3.6 or later Installing is simple; pyrad uses the standard distutils system for installing Python modules:: python setup.py install Author, Copyright, Availability =============================== pyrad was written by Wichert Akkerman and is maintained by Christian Giese (GIC-de) and Istvan Ruzman (Istvan91). This project is licensed under a BSD license. Copyright and license information can be found in the LICENSE.txt file. The current version and documentation can be found on pypi: https://pypi.org/project/pyrad/ Bugs and wishes can be submitted in the pyrad issue tracker on github: https://github.com/pyradius/pyrad/issues Keywords: radius,authentication Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: System :: Systems Administration :: Authentication/Directory pyrad-2.4/README.rst000066400000000000000000000051271465226464100142040ustar00rootroot00000000000000.. image:: https://travis-ci.org/pyradius/pyrad.svg?branch=master :target: https://travis-ci.org/pyradius/pyrad .. image:: https://coveralls.io/repos/github/pyradius/pyrad/badge.svg?branch=master :target: https://coveralls.io/github/pyradius/pyrad?branch=master .. image:: https://img.shields.io/pypi/v/pyrad.svg :target: https://pypi.python.org/pypi/pyrad .. image:: https://img.shields.io/pypi/pyversions/pyrad.svg :target: https://pypi.python.org/pypi/pyrad .. image:: https://img.shields.io/pypi/dm/pyrad.svg :target: https://pypi.python.org/pypi/pyrad .. image:: https://readthedocs.org/projects/pyrad/badge/?version=latest :target: http://pyrad.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. image:: https://img.shields.io/pypi/l/pyrad.svg :target: https://pypi.python.org/pypi/pyrad Introduction ============ pyrad is an implementation of a RADIUS client/server as described in RFC2865. It takes care of all the details like building RADIUS packets, sending them and decoding responses. Here is an example of doing a authentication request:: from __future__ import print_function from pyrad.client import Client from pyrad.dictionary import Dictionary import pyrad.packet srv = Client(server="localhost", secret=b"Kah3choteereethiejeimaeziecumi", dict=Dictionary("dictionary")) # create request req = srv.CreateAuthPacket(code=pyrad.packet.AccessRequest, User_Name="wichert", NAS_Identifier="localhost") req["User-Password"] = req.PwCrypt("password") # send request reply = srv.SendPacket(req) if reply.code == pyrad.packet.AccessAccept: print("access accepted") else: print("access denied") print("Attributes returned by server:") for i in reply.keys(): print("%s: %s" % (i, reply[i])) Requirements & Installation =========================== pyrad requires Python 2.7, or Python 3.6 or later Installing is simple; pyrad uses the standard distutils system for installing Python modules:: python setup.py install Author, Copyright, Availability =============================== pyrad was written by Wichert Akkerman and is maintained by Christian Giese (GIC-de) and Istvan Ruzman (Istvan91). This project is licensed under a BSD license. Copyright and license information can be found in the LICENSE.txt file. The current version and documentation can be found on pypi: https://pypi.org/project/pyrad/ Bugs and wishes can be submitted in the pyrad issue tracker on github: https://github.com/pyradius/pyrad/issues pyrad-2.4/pyproject.toml000077500000000000000000000020601465226464100154250ustar00rootroot00000000000000[build-system] requires = ["poetry>=1.0"] build-backend = "poetry.masonry.api" [tool.poetry] name = "pyrad" version= "2.4" readme = "README.rst" license = "BSD-3-Clause" description="RADIUS tools" authors = [ "Istvan Ruzman ", "Christian Giese ", ] keywords = ["AAA", "accounting", "authentication", "authorization", "RADIUS"] classifiers = [ "Development Status :: 6 - Mature", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Systems Administration :: Authentication/Directory", ] include = [ "example/*" ] [tool.poetry.urls] repository = "https://github.com/pyradius/pyrad" [tool.poetry.dependencies] python = "^2.7 || ^3.6" six = "^1.15.0" netaddr = "^0.8" [tool.poetry.dev-dependencies] nose = "^0.10.0b1" pyrad-2.4/pyrad.egg-info/000077500000000000000000000000001465226464100153215ustar00rootroot00000000000000pyrad-2.4/pyrad.egg-info/PKG-INFO000066400000000000000000000077201465226464100164240ustar00rootroot00000000000000Metadata-Version: 1.1 Name: pyrad Version: 2.4 Summary: RADIUS tools Home-page: https://github.com/pyradius/pyrad Author: Istvan Ruzman, Christian Giese Author-email: istvan@ruzman.eu, developer@gicnet.de License: BSD Description: .. image:: https://travis-ci.org/pyradius/pyrad.svg?branch=master :target: https://travis-ci.org/pyradius/pyrad .. image:: https://coveralls.io/repos/github/pyradius/pyrad/badge.svg?branch=master :target: https://coveralls.io/github/pyradius/pyrad?branch=master .. image:: https://img.shields.io/pypi/v/pyrad.svg :target: https://pypi.python.org/pypi/pyrad .. image:: https://img.shields.io/pypi/pyversions/pyrad.svg :target: https://pypi.python.org/pypi/pyrad .. image:: https://img.shields.io/pypi/dm/pyrad.svg :target: https://pypi.python.org/pypi/pyrad .. image:: https://readthedocs.org/projects/pyrad/badge/?version=latest :target: http://pyrad.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. image:: https://img.shields.io/pypi/l/pyrad.svg :target: https://pypi.python.org/pypi/pyrad Introduction ============ pyrad is an implementation of a RADIUS client/server as described in RFC2865. It takes care of all the details like building RADIUS packets, sending them and decoding responses. Here is an example of doing a authentication request:: from __future__ import print_function from pyrad.client import Client from pyrad.dictionary import Dictionary import pyrad.packet srv = Client(server="localhost", secret=b"Kah3choteereethiejeimaeziecumi", dict=Dictionary("dictionary")) # create request req = srv.CreateAuthPacket(code=pyrad.packet.AccessRequest, User_Name="wichert", NAS_Identifier="localhost") req["User-Password"] = req.PwCrypt("password") # send request reply = srv.SendPacket(req) if reply.code == pyrad.packet.AccessAccept: print("access accepted") else: print("access denied") print("Attributes returned by server:") for i in reply.keys(): print("%s: %s" % (i, reply[i])) Requirements & Installation =========================== pyrad requires Python 2.7, or Python 3.6 or later Installing is simple; pyrad uses the standard distutils system for installing Python modules:: python setup.py install Author, Copyright, Availability =============================== pyrad was written by Wichert Akkerman and is maintained by Christian Giese (GIC-de) and Istvan Ruzman (Istvan91). This project is licensed under a BSD license. Copyright and license information can be found in the LICENSE.txt file. The current version and documentation can be found on pypi: https://pypi.org/project/pyrad/ Bugs and wishes can be submitted in the pyrad issue tracker on github: https://github.com/pyradius/pyrad/issues Keywords: radius,authentication Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: System :: Systems Administration :: Authentication/Directory pyrad-2.4/pyrad.egg-info/SOURCES.txt000066400000000000000000000006641465226464100172130ustar00rootroot00000000000000README.rst pyproject.toml setup.cfg setup.py pyrad/__init__.py pyrad/bidict.py pyrad/client.py pyrad/client_async.py pyrad/curved.py pyrad/dictfile.py pyrad/dictionary.py pyrad/host.py pyrad/packet.py pyrad/proxy.py pyrad/server.py pyrad/server_async.py pyrad/tools.py pyrad.egg-info/PKG-INFO pyrad.egg-info/SOURCES.txt pyrad.egg-info/dependency_links.txt pyrad.egg-info/requires.txt pyrad.egg-info/top_level.txt pyrad.egg-info/zip-safepyrad-2.4/pyrad.egg-info/dependency_links.txt000066400000000000000000000000011465226464100213670ustar00rootroot00000000000000 pyrad-2.4/pyrad.egg-info/requires.txt000066400000000000000000000000141465226464100177140ustar00rootroot00000000000000six netaddr pyrad-2.4/pyrad.egg-info/top_level.txt000066400000000000000000000000061465226464100200470ustar00rootroot00000000000000pyrad pyrad-2.4/pyrad.egg-info/zip-safe000066400000000000000000000000011465226464100167510ustar00rootroot00000000000000 pyrad-2.4/pyrad/000077500000000000000000000000001465226464100136275ustar00rootroot00000000000000pyrad-2.4/pyrad/__init__.py000066400000000000000000000026311465226464100157420ustar00rootroot00000000000000"""Python RADIUS client code. pyrad is an implementation of a RADIUS client as described in RFC2865. It takes care of all the details like building RADIUS packets, sending them and decoding responses. Here is an example of doing a authentication request:: import pyrad.packet from pyrad.client import Client from pyrad.dictionary import Dictionary srv = Client(server="radius.my.domain", secret="s3cr3t", dict = Dictionary("dicts/dictionary", "dictionary.acc")) req = srv.CreatePacket(code=pyrad.packet.AccessRequest, User_Name = "wichert", NAS_Identifier="localhost") req["User-Password"] = req.PwCrypt("password") reply = srv.SendPacket(req) if reply.code = =pyrad.packet.AccessAccept: print "access accepted" else: print "access denied" print "Attributes returned by server:" for i in reply.keys(): print "%s: %s" % (i, reply[i]) This package contains four modules: - client: RADIUS client code - dictionary: RADIUS attribute dictionary - packet: a RADIUS packet as send to/from servers - tools: utility functions """ __docformat__ = 'epytext en' __author__ = 'Christian Giese ' __url__ = 'http://pyrad.readthedocs.io/en/latest/?badge=latest' __copyright__ = 'Copyright 2002-2020 Wichert Akkerman and Christian Giese. All rights reserved.' __version__ = '2.4' __all__ = ['client', 'dictionary', 'packet', 'server', 'tools', 'dictfile'] pyrad-2.4/pyrad/bidict.py000066400000000000000000000015461465226464100154450ustar00rootroot00000000000000# bidict.py # # Bidirectional map class BiDict(object): def __init__(self): self.forward = {} self.backward = {} def Add(self, one, two): self.forward[one] = two self.backward[two] = one def __len__(self): return len(self.forward) def __getitem__(self, key): return self.GetForward(key) def __delitem__(self, key): if key in self.forward: del self.backward[self.forward[key]] del self.forward[key] else: del self.forward[self.backward[key]] del self.backward[key] def GetForward(self, key): return self.forward[key] def HasForward(self, key): return key in self.forward def GetBackward(self, key): return self.backward[key] def HasBackward(self, key): return key in self.backward pyrad-2.4/pyrad/client.py000066400000000000000000000175021465226464100154640ustar00rootroot00000000000000# client.py # # Copyright 2002-2007 Wichert Akkerman __docformat__ = "epytext en" import hashlib import select import socket import time import six import struct from pyrad import host from pyrad import packet EAP_CODE_REQUEST = 1 EAP_CODE_RESPONSE = 2 EAP_TYPE_IDENTITY = 1 class Timeout(Exception): """Simple exception class which is raised when a timeout occurs while waiting for a RADIUS server to respond.""" class Client(host.Host): """Basic RADIUS client. This class implements a basic RADIUS client. It can send requests to a RADIUS server, taking care of timeouts and retries, and validate its replies. :ivar retries: number of times to retry sending a RADIUS request :type retries: integer :ivar timeout: number of seconds to wait for an answer :type timeout: float """ def __init__(self, server, authport=1812, acctport=1813, coaport=3799, secret=six.b(''), dict=None, retries=3, timeout=5): """Constructor. :param server: hostname or IP address of RADIUS server :type server: string :param authport: port to use for authentication packets :type authport: integer :param acctport: port to use for accounting packets :type acctport: integer :param coaport: port to use for CoA packets :type coaport: integer :param secret: RADIUS secret :type secret: string :param dict: RADIUS dictionary :type dict: pyrad.dictionary.Dictionary """ host.Host.__init__(self, authport, acctport, coaport, dict) self.server = server self.secret = secret self._socket = None self.retries = retries self.timeout = timeout self._poll = select.poll() def bind(self, addr): """Bind socket to an address. Binding the socket used for communicating to an address can be usefull when working on a machine with multiple addresses. :param addr: network address (hostname or IP) and port to bind to :type addr: host,port tuple """ self._CloseSocket() self._SocketOpen() self._socket.bind(addr) def _SocketOpen(self): try: family = socket.getaddrinfo(self.server, 'www')[0][0] except: family = socket.AF_INET if not self._socket: self._socket = socket.socket(family, socket.SOCK_DGRAM) self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self._poll.register(self._socket, select.POLLIN) def _CloseSocket(self): if self._socket: self._poll.unregister(self._socket) self._socket.close() self._socket = None def CreateAuthPacket(self, **args): """Create a new RADIUS packet. This utility function creates a new RADIUS packet which can be used to communicate with the RADIUS server this client talks to. This is initializing the new packet with the dictionary and secret used for the client. :return: a new empty packet instance :rtype: pyrad.packet.AuthPacket """ return host.Host.CreateAuthPacket(self, secret=self.secret, **args) def CreateAcctPacket(self, **args): """Create a new RADIUS packet. This utility function creates a new RADIUS packet which can be used to communicate with the RADIUS server this client talks to. This is initializing the new packet with the dictionary and secret used for the client. :return: a new empty packet instance :rtype: pyrad.packet.Packet """ return host.Host.CreateAcctPacket(self, secret=self.secret, **args) def CreateCoAPacket(self, **args): """Create a new RADIUS packet. This utility function creates a new RADIUS packet which can be used to communicate with the RADIUS server this client talks to. This is initializing the new packet with the dictionary and secret used for the client. :return: a new empty packet instance :rtype: pyrad.packet.Packet """ return host.Host.CreateCoAPacket(self, secret=self.secret, **args) def _SendPacket(self, pkt, port): """Send a packet to a RADIUS server. :param pkt: the packet to send :type pkt: pyrad.packet.Packet :param port: UDP port to send packet to :type port: integer :return: the reply packet received :rtype: pyrad.packet.Packet :raise Timeout: RADIUS server does not reply """ self._SocketOpen() for attempt in range(self.retries): if attempt and pkt.code == packet.AccountingRequest: if "Acct-Delay-Time" in pkt: pkt["Acct-Delay-Time"] = \ pkt["Acct-Delay-Time"][0] + self.timeout else: pkt["Acct-Delay-Time"] = self.timeout now = time.time() waitto = now + self.timeout self._socket.sendto(pkt.RequestPacket(), (self.server, port)) while now < waitto: ready = self._poll.poll((waitto - now) * 1000) if ready: rawreply = self._socket.recv(4096) else: now = time.time() continue try: reply = pkt.CreateReply(packet=rawreply) if pkt.VerifyReply(reply, rawreply): return reply except packet.PacketError: pass now = time.time() raise Timeout def SendPacket(self, pkt): """Send a packet to a RADIUS server. :param pkt: the packet to send :type pkt: pyrad.packet.Packet :return: the reply packet received :rtype: pyrad.packet.Packet :raise Timeout: RADIUS server does not reply """ if isinstance(pkt, packet.AuthPacket): if pkt.auth_type == 'eap-md5': # Creating EAP-Identity password = pkt[2][0] if 2 in pkt else pkt[1][0] pkt[79] = [struct.pack('!BBHB%ds' % len(password), EAP_CODE_RESPONSE, packet.CurrentID, len(password) + 5, EAP_TYPE_IDENTITY, password)] reply = self._SendPacket(pkt, self.authport) if ( reply and reply.code == packet.AccessChallenge and pkt.auth_type == 'eap-md5' ): # Got an Access-Challenge eap_code, eap_id, eap_size, eap_type, eap_md5 = struct.unpack( '!BBHB%ds' % (len(reply[79][0]) - 5), reply[79][0] ) # Sending back an EAP-Type-MD5-Challenge # Thank god for http://www.secdev.org/python/eapy.py client_pw = pkt[2][0] if 2 in pkt else pkt[1][0] md5_challenge = hashlib.md5( struct.pack('!B', eap_id) + client_pw + eap_md5[1:] ).digest() pkt[79] = [ struct.pack('!BBHBB', 2, eap_id, len(md5_challenge) + 6, 4, len(md5_challenge)) + md5_challenge ] # Copy over Challenge-State pkt[24] = reply[24] reply = self._SendPacket(pkt, self.authport) return reply elif isinstance(pkt, packet.CoAPacket): return self._SendPacket(pkt, self.coaport) else: return self._SendPacket(pkt, self.acctport) pyrad-2.4/pyrad/client_async.py000066400000000000000000000341171465226464100166620ustar00rootroot00000000000000# client_async.py # # Copyright 2018-2020 Geaaru gmail.com> __docformat__ = "epytext en" from datetime import datetime import asyncio import six import logging import random from pyrad.packet import Packet, AuthPacket, AcctPacket, CoAPacket class DatagramProtocolClient(asyncio.Protocol): def __init__(self, server, port, logger, client, retries=3, timeout=30): self.transport = None self.port = port self.server = server self.logger = logger self.retries = retries self.timeout = timeout self.client = client # Map of pending requests self.pending_requests = {} # Use cryptographic-safe random generator as provided by the OS. random_generator = random.SystemRandom() self.packet_id = random_generator.randrange(0, 256) self.timeout_future = None async def __timeout_handler__(self): try: while True: req2delete = [] now = datetime.now() next_weak_up = self.timeout # noinspection PyShadowingBuiltins for id, req in self.pending_requests.items(): secs = (req['send_date'] - now).seconds if secs > self.timeout: if req['retries'] == self.retries: self.logger.debug('[%s:%d] For request %d execute all retries', self.server, self.port, id) req['future'].set_exception( TimeoutError('Timeout on Reply') ) req2delete.append(id) else: # Send again packet req['send_date'] = now req['retries'] += 1 self.logger.debug('[%s:%d] For request %d execute retry %d', self.server, self.port, id, req['retries']) self.transport.sendto(req['packet'].RequestPacket()) elif next_weak_up > secs: next_weak_up = secs # noinspection PyShadowingBuiltins for id in req2delete: # Remove request for map del self.pending_requests[id] await asyncio.sleep(next_weak_up) except asyncio.CancelledError: pass def send_packet(self, packet, future): if packet.id in self.pending_requests: raise Exception('Packet with id %d already present' % packet.id) # Store packet on pending requests map self.pending_requests[packet.id] = { 'packet': packet, 'creation_date': datetime.now(), 'retries': 0, 'future': future, 'send_date': datetime.now() } # In queue packet raw on socket buffer self.transport.sendto(packet.RequestPacket()) def connection_made(self, transport): self.transport = transport socket = transport.get_extra_info('socket') self.logger.info( '[%s:%d] Transport created with binding in %s:%d', self.server, self.port, socket.getsockname()[0], socket.getsockname()[1] ) pre_loop = asyncio.get_event_loop() asyncio.set_event_loop(loop=self.client.loop) # Start asynchronous timer handler self.timeout_future = asyncio.ensure_future( self.__timeout_handler__() ) asyncio.set_event_loop(loop=pre_loop) def error_received(self, exc): self.logger.error('[%s:%d] Error received: %s', self.server, self.port, exc) def connection_lost(self, exc): if exc: self.logger.warn('[%s:%d] Connection lost: %s', self.server, self.port, str(exc)) else: self.logger.info('[%s:%d] Transport closed', self.server, self.port) # noinspection PyUnusedLocal def datagram_received(self, data, addr): try: reply = Packet(packet=data, dict=self.client.dict) if reply and reply.id in self.pending_requests: req = self.pending_requests[reply.id] packet = req['packet'] reply.dict = packet.dict reply.secret = packet.secret if packet.VerifyReply(reply, data): req['future'].set_result(reply) # Remove request for map del self.pending_requests[reply.id] else: self.logger.warn('[%s:%d] Ignore invalid reply for id %d. %s', self.server, self.port, reply.id) else: self.logger.warn('[%s:%d] Ignore invalid reply: %s', self.server, self.port, data) except Exception as exc: self.logger.error('[%s:%d] Error on decode packet: %s', self.server, self.port, exc) async def close_transport(self): if self.transport: self.logger.debug('[%s:%d] Closing transport...', self.server, self.port) self.transport.close() self.transport = None if self.timeout_future: self.timeout_future.cancel() await self.timeout_future self.timeout_future = None def create_id(self): self.packet_id = (self.packet_id + 1) % 256 return self.packet_id def __str__(self): return 'DatagramProtocolClient(server?=%s, port=%d)' % (self.server, self.port) # Used as protocol_factory def __call__(self): return self class ClientAsync: """Basic RADIUS client. This class implements a basic RADIUS client. It can send requests to a RADIUS server, taking care of timeouts and retries, and validate its replies. :ivar retries: number of times to retry sending a RADIUS request :type retries: integer :ivar timeout: number of seconds to wait for an answer :type timeout: integer """ # noinspection PyShadowingBuiltins def __init__(self, server, auth_port=1812, acct_port=1813, coa_port=3799, secret=six.b(''), dict=None, loop=None, retries=3, timeout=30, logger_name='pyrad'): """Constructor. :param server: hostname or IP address of RADIUS server :type server: string :param auth_port: port to use for authentication packets :type auth_port: integer :param acct_port: port to use for accounting packets :type acct_port: integer :param coa_port: port to use for CoA packets :type coa_port: integer :param secret: RADIUS secret :type secret: string :param dict: RADIUS dictionary :type dict: pyrad.dictionary.Dictionary :param loop: Python loop handler :type loop: asyncio event loop """ if not loop: self.loop = asyncio.get_event_loop() else: self.loop = loop self.logger = logging.getLogger(logger_name) self.server = server self.secret = secret self.retries = retries self.timeout = timeout self.dict = dict self.auth_port = auth_port self.protocol_auth = None self.acct_port = acct_port self.protocol_acct = None self.protocol_coa = None self.coa_port = coa_port async def initialize_transports(self, enable_acct=False, enable_auth=False, enable_coa=False, local_addr=None, local_auth_port=None, local_acct_port=None, local_coa_port=None): task_list = [] if not enable_acct and not enable_auth and not enable_coa: raise Exception('No transports selected') if enable_acct and not self.protocol_acct: self.protocol_acct = DatagramProtocolClient( self.server, self.acct_port, self.logger, self, retries=self.retries, timeout=self.timeout ) bind_addr = None if local_addr and local_acct_port: bind_addr = (local_addr, local_acct_port) acct_connect = self.loop.create_datagram_endpoint( self.protocol_acct, reuse_port=True, remote_addr=(self.server, self.acct_port), local_addr=bind_addr ) task_list.append(acct_connect) if enable_auth and not self.protocol_auth: self.protocol_auth = DatagramProtocolClient( self.server, self.auth_port, self.logger, self, retries=self.retries, timeout=self.timeout ) bind_addr = None if local_addr and local_auth_port: bind_addr = (local_addr, local_auth_port) auth_connect = self.loop.create_datagram_endpoint( self.protocol_auth, reuse_port=True, remote_addr=(self.server, self.auth_port), local_addr=bind_addr ) task_list.append(auth_connect) if enable_coa and not self.protocol_coa: self.protocol_coa = DatagramProtocolClient( self.server, self.coa_port, self.logger, self, retries=self.retries, timeout=self.timeout ) bind_addr = None if local_addr and local_coa_port: bind_addr = (local_addr, local_coa_port) coa_connect = self.loop.create_datagram_endpoint( self.protocol_coa, reuse_port=True, remote_addr=(self.server, self.coa_port), local_addr=bind_addr ) task_list.append(coa_connect) await asyncio.ensure_future( asyncio.gather( *task_list, return_exceptions=False, ), loop=self.loop ) # noinspection SpellCheckingInspection async def deinitialize_transports(self, deinit_coa=True, deinit_auth=True, deinit_acct=True): if self.protocol_coa and deinit_coa: await self.protocol_coa.close_transport() del self.protocol_coa self.protocol_coa = None if self.protocol_auth and deinit_auth: await self.protocol_auth.close_transport() del self.protocol_auth self.protocol_auth = None if self.protocol_acct and deinit_acct: await self.protocol_acct.close_transport() del self.protocol_acct self.protocol_acct = None # noinspection PyPep8Naming def CreateAuthPacket(self, **args): """Create a new RADIUS packet. This utility function creates a new RADIUS packet which can be used to communicate with the RADIUS server this client talks to. This is initializing the new packet with the dictionary and secret used for the client. :return: a new empty packet instance :rtype: pyrad.packet.Packet """ if not self.protocol_auth: raise Exception('Transport not initialized') return AuthPacket(dict=self.dict, id=self.protocol_auth.create_id(), secret=self.secret, **args) # noinspection PyPep8Naming def CreateAcctPacket(self, **args): """Create a new RADIUS packet. This utility function creates a new RADIUS packet which can be used to communicate with the RADIUS server this client talks to. This is initializing the new packet with the dictionary and secret used for the client. :return: a new empty packet instance :rtype: pyrad.packet.Packet """ if not self.protocol_acct: raise Exception('Transport not initialized') return AcctPacket(id=self.protocol_acct.create_id(), dict=self.dict, secret=self.secret, **args) # noinspection PyPep8Naming def CreateCoAPacket(self, **args): """Create a new RADIUS packet. This utility function creates a new RADIUS packet which can be used to communicate with the RADIUS server this client talks to. This is initializing the new packet with the dictionary and secret used for the client. :return: a new empty packet instance :rtype: pyrad.packet.Packet """ if not self.protocol_acct: raise Exception('Transport not initialized') return CoAPacket(id=self.protocol_coa.create_id(), dict=self.dict, secret=self.secret, **args) # noinspection PyPep8Naming # noinspection PyShadowingBuiltins def CreatePacket(self, id, **args): if not id: raise Exception('Missing mandatory packet id') return Packet(id=id, dict=self.dict, secret=self.secret, **args) # noinspection PyPep8Naming def SendPacket(self, pkt): """Send a packet to a RADIUS server. :param pkt: the packet to send :type pkt: pyrad.packet.Packet :return: Future related with packet to send :rtype: asyncio.Future """ ans = asyncio.Future(loop=self.loop) if isinstance(pkt, AuthPacket): if not self.protocol_auth: raise Exception('Transport not initialized') self.protocol_auth.send_packet(pkt, ans) elif isinstance(pkt, AcctPacket): if not self.protocol_acct: raise Exception('Transport not initialized') self.protocol_acct.send_packet(pkt, ans) elif isinstance(pkt, CoAPacket): if not self.protocol_coa: raise Exception('Transport not initialized') self.protocol_coa.send_packet(pkt, ans) else: raise Exception('Unsupported packet') return ans pyrad-2.4/pyrad/curved.py000066400000000000000000000042341465226464100154740ustar00rootroot00000000000000# curved.py # # Copyright 2002 Wichert Akkerman """Twisted integration code """ __docformat__ = 'epytext 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 class PacketError(Exception): """Exception class for bogus packets PacketError exceptions are only used inside the Server class to abort processing of a packet. """ class RADIUS(host.Host, protocol.DatagramProtocol): def __init__(self, hosts={}, dict=dictionary.Dictionary()): host.Host.__init__(self, dict=dict) self.hosts = hosts def processPacket(self, pkt): pass def createPacket(self, **kwargs): raise NotImplementedError('Attempted to use a pure base class') def datagramReceived(self, datagram, source): host, port = source try: pkt = self.CreatePacket(packet=datagram) except packet.PacketError as err: log.msg('Dropping invalid packet: ' + str(err)) return if host not in self.hosts: log.msg('Dropping packet from unknown host ' + host) return pkt.source = (host, port) try: self.processPacket(pkt) except PacketError as err: log.msg('Dropping packet from %s: %s' % (host, str(err))) class RADIUSAccess(RADIUS): def createPacket(self, **kwargs): self.CreateAuthPacket(**kwargs) def processPacket(self, pkt): if pkt.code != packet.AccessRequest: raise PacketError( 'non-AccessRequest packet on authentication socket') class RADIUSAccounting(RADIUS): def createPacket(self, **kwargs): self.CreateAcctPacket(**kwargs) def processPacket(self, pkt): if pkt.code != packet.AccountingRequest: raise PacketError( 'non-AccountingRequest packet on authentication socket') if __name__ == '__main__': log.startLogging(sys.stdout, 0) reactor.listenUDP(1812, RADIUSAccess()) reactor.listenUDP(1813, RADIUSAccounting()) reactor.run() pyrad-2.4/pyrad/dictfile.py000066400000000000000000000055571465226464100160000ustar00rootroot00000000000000# dictfile.py # # Copyright 2009 Kristoffer Gronlund """ Dictionary File Implements an iterable file format that handles the RADIUS $INCLUDE directives behind the scene. """ import os import six class _Node(object): """Dictionary file node A single dictionary file. """ __slots__ = ('name', 'lines', 'current', 'length', 'dir') def __init__(self, fd, name, parentdir): self.lines = fd.readlines() self.length = len(self.lines) self.current = 0 self.name = os.path.basename(name) path = os.path.dirname(name) if os.path.isabs(path): self.dir = path else: self.dir = os.path.join(parentdir, path) def Next(self): if self.current >= self.length: return None self.current += 1 return self.lines[self.current - 1] class DictFile(object): """Dictionary file class An iterable file type that handles $INCLUDE directives internally. """ __slots__ = ('stack') def __init__(self, fil): """ @param fil: a dictionary file to parse @type fil: string or file """ self.stack = [] self.__ReadNode(fil) def __ReadNode(self, fil): node = None parentdir = self.__CurDir() if isinstance(fil, six.string_types): fname = None if os.path.isabs(fil): fname = fil else: fname = os.path.join(parentdir, fil) fd = open(fname, "rt") node = _Node(fd, fil, parentdir) fd.close() else: node = _Node(fil, '', parentdir) self.stack.append(node) def __CurDir(self): if self.stack: return self.stack[-1].dir else: return os.path.realpath(os.curdir) def __GetInclude(self, line): line = line.split("#", 1)[0].strip() tokens = line.split() if tokens and tokens[0].upper() == '$INCLUDE': return " ".join(tokens[1:]) else: return None def Line(self): """Returns line number of current file """ if self.stack: return self.stack[-1].current else: return -1 def File(self): """Returns name of current file """ if self.stack: return self.stack[-1].name else: return '' def __iter__(self): return self def __next__(self): while self.stack: line = self.stack[-1].Next() if line == None: self.stack.pop() else: inc = self.__GetInclude(line) if inc: self.__ReadNode(inc) else: return line raise StopIteration next = __next__ # BBB for python <3 pyrad-2.4/pyrad/dictionary.py000066400000000000000000000340441465226464100163530ustar00rootroot00000000000000# dictionary.py # # Copyright 2002,2005,2007,2016 Wichert Akkerman """ RADIUS uses dictionaries to define the attributes that can be used in packets. The Dictionary class stores the attribute definitions from one or more dictionary files. Dictionary files are textfiles with one command per line. Comments are specified by starting with a # character, and empty lines are ignored. The commands supported are:: ATTRIBUTE [] specify an attribute and its type VALUE specify a value attribute VENDOR specify a vendor ID BEGIN-VENDOR begin definition of vendor attributes END-VENDOR end definition of vendor attributes The datatypes currently supported are: +---------------+----------------------------------------------+ | type | description | +===============+==============================================+ | string | ASCII string | +---------------+----------------------------------------------+ | ipaddr | IPv4 address | +---------------+----------------------------------------------+ | date | 32 bits UNIX | +---------------+----------------------------------------------+ | octets | arbitrary binary data | +---------------+----------------------------------------------+ | abinary | ascend binary data | +---------------+----------------------------------------------+ | ipv6addr | 16 octets in network byte order | +---------------+----------------------------------------------+ | ipv6prefix | 18 octets in network byte order | +---------------+----------------------------------------------+ | integer | 32 bits unsigned number | +---------------+----------------------------------------------+ | signed | 32 bits signed number | +---------------+----------------------------------------------+ | short | 16 bits unsigned number | +---------------+----------------------------------------------+ | byte | 8 bits unsigned number | +---------------+----------------------------------------------+ | tlv | Nested tag-length-value | +---------------+----------------------------------------------+ | integer64 | 64 bits unsigned number | +---------------+----------------------------------------------+ These datatypes are parsed but not supported: +---------------+----------------------------------------------+ | type | description | +===============+==============================================+ | ifid | 8 octets in network byte order | +---------------+----------------------------------------------+ | ether | 6 octets of hh:hh:hh:hh:hh:hh | | | where 'h' is hex digits, upper or lowercase. | +---------------+----------------------------------------------+ """ from pyrad import bidict from pyrad import tools from pyrad import dictfile from copy import copy import logging __docformat__ = 'epytext en' DATATYPES = frozenset(['string', 'ipaddr', 'integer', 'date', 'octets', 'abinary', 'ipv6addr', 'ipv6prefix', 'short', 'byte', 'signed', 'ifid', 'ether', 'tlv', 'integer64']) class ParseError(Exception): """Dictionary parser exceptions. :ivar msg: Error message :type msg: string :ivar linenumber: Line number on which the error occurred :type linenumber: integer """ def __init__(self, msg=None, **data): self.msg = msg self.file = data.get('file', '') self.line = data.get('line', -1) def __str__(self): str = '' if self.file: str += self.file if self.line > -1: str += '(%d)' % self.line if self.file or self.line > -1: str += ': ' str += 'Parse error' if self.msg: str += ': %s' % self.msg return str class Attribute(object): def __init__(self, name, code, datatype, is_sub_attribute=False, vendor='', values=None, encrypt=0, has_tag=False): if datatype not in DATATYPES: raise ValueError('Invalid data type') self.name = name self.code = code self.type = datatype self.vendor = vendor self.encrypt = encrypt self.has_tag = has_tag self.values = bidict.BiDict() self.sub_attributes = {} self.parent = None self.is_sub_attribute = is_sub_attribute if values: for (key, value) in values.items(): self.values.Add(key, value) class Dictionary(object): """RADIUS dictionary class. This class stores all information about vendors, attributes and their values as defined in RADIUS dictionary files. :ivar vendors: bidict mapping vendor name to vendor code :type vendors: bidict :ivar attrindex: bidict mapping :type attrindex: bidict :ivar attributes: bidict mapping attribute name to attribute class :type attributes: bidict """ def __init__(self, dict=None, *dicts): """ :param dict: path of dictionary file or file-like object to read :type dict: string or file :param dicts: list of dictionaries :type dicts: sequence of strings or files """ self.vendors = bidict.BiDict() self.vendors.Add('', 0) self.attrindex = bidict.BiDict() self.attributes = {} self.defer_parse = [] if dict: self.ReadDictionary(dict) for i in dicts: self.ReadDictionary(i) def __len__(self): return len(self.attributes) def __getitem__(self, key): return self.attributes[key] def __contains__(self, key): return key in self.attributes has_key = __contains__ def __ParseAttribute(self, state, tokens): if not len(tokens) in [4, 5]: raise ParseError( 'Incorrect number of tokens for attribute definition', name=state['file'], line=state['line']) vendor = state['vendor'] has_tag = False encrypt = 0 if len(tokens) >= 5: def keyval(o): kv = o.split('=') if len(kv) == 2: return (kv[0], kv[1]) else: return (kv[0], None) options = [keyval(o) for o in tokens[4].split(',')] for (key, val) in options: if key == 'has_tag': has_tag = True elif key == 'encrypt': if val not in ['1', '2', '3']: raise ParseError( 'Illegal attribute encryption: %s' % val, file=state['file'], line=state['line']) encrypt = int(val) if (not has_tag) and encrypt == 0: vendor = tokens[4] if not self.vendors.HasForward(vendor): if vendor == "concat": # ignore attributes with concat (freeradius compat.) return None else: raise ParseError('Unknown vendor ' + vendor, file=state['file'], line=state['line']) (attribute, code, datatype) = tokens[1:4] codes = code.split('.') # Codes can be sent as hex, or octal or decimal string representations. tmp = [] for c in codes: if c.startswith('0x'): tmp.append(int(c, 16)) elif c.startswith('0o'): tmp.append(int(c, 8)) else: tmp.append(int(c, 10)) codes = tmp is_sub_attribute = (len(codes) > 1) if len(codes) == 2: code = int(codes[1]) parent_code = int(codes[0]) elif len(codes) == 1: code = int(codes[0]) parent_code = None else: raise ParseError('nested tlvs are not supported') datatype = datatype.split("[")[0] if datatype not in DATATYPES: raise ParseError('Illegal type: ' + datatype, file=state['file'], line=state['line']) if vendor: if is_sub_attribute: key = (self.vendors.GetForward(vendor), parent_code, code) else: key = (self.vendors.GetForward(vendor), code) else: if is_sub_attribute: key = (parent_code, code) else: key = code self.attrindex.Add(attribute, key) self.attributes[attribute] = Attribute(attribute, code, datatype, is_sub_attribute, vendor, encrypt=encrypt, has_tag=has_tag) if datatype == 'tlv': # save attribute in tlvs state['tlvs'][code] = self.attributes[attribute] if is_sub_attribute: # save sub attribute in parent tlv and update their parent field state['tlvs'][parent_code].sub_attributes[code] = attribute self.attributes[attribute].parent = state['tlvs'][parent_code] def __ParseValue(self, state, tokens, defer): if len(tokens) != 4: raise ParseError('Incorrect number of tokens for value definition', file=state['file'], line=state['line']) (attr, key, value) = tokens[1:] try: adef = self.attributes[attr] except KeyError: if defer: self.defer_parse.append((copy(state), copy(tokens))) return raise ParseError('Value defined for unknown attribute ' + attr, file=state['file'], line=state['line']) if adef.type in ['integer', 'signed', 'short', 'byte', 'integer64']: value = int(value, 0) value = tools.EncodeAttr(adef.type, value) self.attributes[attr].values.Add(key, value) def __ParseVendor(self, state, tokens): if len(tokens) not in [3, 4]: raise ParseError( 'Incorrect number of tokens for vendor definition', file=state['file'], line=state['line']) # Parse format specification, but do # nothing about it for now if len(tokens) == 4: fmt = tokens[3].split('=') if fmt[0] != 'format': raise ParseError( "Unknown option '%s' for vendor definition" % (fmt[0]), file=state['file'], line=state['line']) try: (t, l) = tuple(int(a) for a in fmt[1].split(',')) if t not in [1, 2, 4] or l not in [0, 1, 2]: raise ParseError( 'Unknown vendor format specification %s' % (fmt[1]), file=state['file'], line=state['line']) except ValueError: raise ParseError( 'Syntax error in vendor specification', file=state['file'], line=state['line']) (vendorname, vendor) = tokens[1:3] self.vendors.Add(vendorname, int(vendor, 0)) def __ParseBeginVendor(self, state, tokens): if len(tokens) != 2: raise ParseError( 'Incorrect number of tokens for begin-vendor statement', file=state['file'], line=state['line']) vendor = tokens[1] if not self.vendors.HasForward(vendor): raise ParseError( 'Unknown vendor %s in begin-vendor statement' % vendor, file=state['file'], line=state['line']) state['vendor'] = vendor def __ParseEndVendor(self, state, tokens): if len(tokens) != 2: raise ParseError( 'Incorrect number of tokens for end-vendor statement', file=state['file'], line=state['line']) vendor = tokens[1] if state['vendor'] != vendor: raise ParseError( 'Ending non-open vendor' + vendor, file=state['file'], line=state['line']) state['vendor'] = '' def ReadDictionary(self, file): """Parse a dictionary file. Reads a RADIUS dictionary file and merges its contents into the class instance. :param file: Name of dictionary file to parse or a file-like object :type file: string or file-like object """ fil = dictfile.DictFile(file) state = {} state['vendor'] = '' state['tlvs'] = {} self.defer_parse = [] for line in fil: state['file'] = fil.File() state['line'] = fil.Line() line = line.split('#', 1)[0].strip() tokens = line.split() if not tokens: continue key = tokens[0].upper() if key == 'ATTRIBUTE': self.__ParseAttribute(state, tokens) elif key == 'VALUE': self.__ParseValue(state, tokens, True) elif key == 'VENDOR': self.__ParseVendor(state, tokens) elif key == 'BEGIN-VENDOR': self.__ParseBeginVendor(state, tokens) elif key == 'END-VENDOR': self.__ParseEndVendor(state, tokens) for state, tokens in self.defer_parse: key = tokens[0].upper() if key == 'VALUE': self.__ParseValue(state, tokens, False) self.defer_parse = [] pyrad-2.4/pyrad/host.py000066400000000000000000000070531465226464100151630ustar00rootroot00000000000000# host.py # # Copyright 2003,2007 Wichert Akkerman from pyrad import packet class Host(object): """Generic RADIUS capable host. :ivar dict: RADIUS dictionary :type dict: pyrad.dictionary.Dictionary :ivar authport: port to listen on for authentication packets :type authport: integer :ivar acctport: port to listen on for accounting packets :type acctport: integer """ def __init__(self, authport=1812, acctport=1813, coaport=3799, dict=None): """Constructor :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 dict: RADIUS dictionary :type dict: pyrad.dictionary.Dictionary """ self.dict = dict self.authport = authport self.acctport = acctport self.coaport = coaport def CreatePacket(self, **args): """Create a new RADIUS packet. This utility function creates a new RADIUS authentication packet which can be used to communicate with the RADIUS server this client talks to. This is initializing the new packet with the dictionary and secret used for the client. :return: a new empty packet instance :rtype: pyrad.packet.Packet """ return packet.Packet(dict=self.dict, **args) def CreateAuthPacket(self, **args): """Create a new authentication RADIUS packet. This utility function creates a new RADIUS authentication packet which can be used to communicate with the RADIUS server this client talks to. This is initializing the new packet with the dictionary and secret used for the client. :return: a new empty packet instance :rtype: pyrad.packet.AuthPacket """ return packet.AuthPacket(dict=self.dict, **args) def CreateAcctPacket(self, **args): """Create a new accounting RADIUS packet. This utility function creates a new accouting RADIUS packet which can be used to communicate with the RADIUS server this client talks to. This is initializing the new packet with the dictionary and secret used for the client. :return: a new empty packet instance :rtype: pyrad.packet.AcctPacket """ return packet.AcctPacket(dict=self.dict, **args) def CreateCoAPacket(self, **args): """Create a new CoA RADIUS packet. This utility function creates a new CoA RADIUS packet which can be used to communicate with the RADIUS server this client talks to. This is initializing the new packet with the dictionary and secret used for the client. :return: a new empty packet instance :rtype: pyrad.packet.CoAPacket """ return packet.CoAPacket(dict=self.dict, **args) def SendPacket(self, fd, pkt): """Send a packet. :param fd: socket to send packet with :type fd: socket class instance :param pkt: packet to send :type pkt: Packet class instance """ fd.sendto(pkt.Packet(), pkt.source) def SendReplyPacket(self, fd, pkt): """Send a packet. :param fd: socket to send packet with :type fd: socket class instance :param pkt: packet to send :type pkt: Packet class instance """ fd.sendto(pkt.ReplyPacket(), pkt.source) pyrad-2.4/pyrad/packet.py000066400000000000000000001015231465226464100154520ustar00rootroot00000000000000# packet.py # # Copyright 2002-2005,2007 Wichert Akkerman # # A RADIUS packet as defined in RFC 2138 from collections import OrderedDict import struct try: import secrets random_generator = secrets.SystemRandom() except ImportError: import random random_generator = random.SystemRandom() import hmac import sys if sys.version_info >= (3, 0): hmac_new = lambda *x, **y: hmac.new(*x, digestmod='MD5', **y) else: hmac_new = hmac.new try: import hashlib md5_constructor = hashlib.md5 except ImportError: # BBB for python 2.4 import md5 md5_constructor = md5.new import six from pyrad import tools # Packet codes AccessRequest = 1 AccessAccept = 2 AccessReject = 3 AccountingRequest = 4 AccountingResponse = 5 AccessChallenge = 11 StatusServer = 12 StatusClient = 13 DisconnectRequest = 40 DisconnectACK = 41 DisconnectNAK = 42 CoARequest = 43 CoAACK = 44 CoANAK = 45 # Current ID CurrentID = random_generator.randrange(1, 255) class PacketError(Exception): pass class Packet(OrderedDict): """Packet acts like a standard python map to provide simple access to the RADIUS attributes. Since RADIUS allows for repeated attributes the value will always be a sequence. pyrad makes sure to preserve the ordering when encoding and decoding packets. There are two ways to use the map intereface: if attribute names are used pyrad take care of en-/decoding data. If the attribute type number (or a vendor ID/attribute type tuple for vendor attributes) is used you work with the raw data. Normally you will not use this class directly, but one of the :obj:`AuthPacket` or :obj:`AcctPacket` classes. """ def __init__(self, code=0, id=None, secret=six.b(''), authenticator=None, **attributes): """Constructor :param dict: RADIUS dictionary :type dict: pyrad.dictionary.Dictionary class :param secret: secret needed to communicate with a RADIUS server :type secret: string :param id: packet identification number :type id: integer (8 bits) :param code: packet type code :type code: integer (8bits) :param packet: raw packet to decode :type packet: string """ OrderedDict.__init__(self) self.code = code if id is not None: self.id = id else: self.id = CreateID() if not isinstance(secret, six.binary_type): raise TypeError('secret must be a binary string') self.secret = secret if authenticator is not None and \ not isinstance(authenticator, six.binary_type): raise TypeError('authenticator must be a binary string') self.authenticator = authenticator self.message_authenticator = None self.raw_packet = None if 'dict' in attributes: self.dict = attributes['dict'] if 'packet' in attributes: self.raw_packet = attributes['packet'] self.DecodePacket(self.raw_packet) if 'message_authenticator' in attributes: self.message_authenticator = attributes['message_authenticator'] for (key, value) in attributes.items(): if key in [ 'dict', 'fd', 'packet', 'message_authenticator', ]: continue key = key.replace('_', '-') self.AddAttribute(key, value) def add_message_authenticator(self): self.message_authenticator = True # Maintain a zero octets content for md5 and hmac calculation. self['Message-Authenticator'] = 16 * six.b('\00') if self.id is None: self.id = self.CreateID() if self.authenticator is None and self.code == AccessRequest: self.authenticator = self.CreateAuthenticator() self._refresh_message_authenticator() def get_message_authenticator(self): self._refresh_message_authenticator() return self.message_authenticator def _refresh_message_authenticator(self): hmac_constructor = hmac_new(self.secret) # Maintain a zero octets content for md5 and hmac calculation. self['Message-Authenticator'] = 16 * six.b('\00') attr = self._PktEncodeAttributes() header = struct.pack('!BBH', self.code, self.id, (20 + len(attr))) hmac_constructor.update(header[0:4]) if self.code in (AccountingRequest, DisconnectRequest, CoARequest, AccountingResponse): hmac_constructor.update(16 * six.b('\00')) else: # NOTE: self.authenticator on reply packet is initialized # with request authenticator by design. # For AccessAccept, AccessReject and AccessChallenge # it is needed use original Authenticator. # For AccessAccept, AccessReject and AccessChallenge # it is needed use original Authenticator. if self.authenticator is None: raise Exception('No authenticator found') hmac_constructor.update(self.authenticator) hmac_constructor.update(attr) self['Message-Authenticator'] = hmac_constructor.digest() def verify_message_authenticator(self, secret=None, original_authenticator=None, original_code=None): """Verify packet Message-Authenticator. :return: False if verification failed else True :rtype: boolean """ if self.message_authenticator is None: raise Exception('No Message-Authenticator AVP present') prev_ma = self['Message-Authenticator'] # Set zero bytes for Message-Authenticator for md5 calculation if secret is None and self.secret is None: raise Exception('Missing secret for HMAC/MD5 verification') if secret: key = secret else: key = self.secret # If there's a raw packet, use that to calculate the expected # Message-Authenticator. While the Packet class keeps multiple # instances of an attribute grouped together in the attribute list, # other applications may not. Using _PktEncodeAttributes to get # the attributes could therefore end up changing the attribute order # because of the grouping Packet does, which would cause # Message-Authenticator verification to fail. Using the raw packet # instead, if present, ensures the verification is done using the # attributes exactly as sent. if self.raw_packet: attr = self.raw_packet[20:] attr = attr.replace(prev_ma[0], 16 * six.b('\00')) else: self['Message-Authenticator'] = 16 * six.b('\00') attr = self._PktEncodeAttributes() header = struct.pack('!BBH', self.code, self.id, (20 + len(attr))) hmac_constructor = hmac_new(key) hmac_constructor.update(header) if self.code in (AccountingRequest, DisconnectRequest, CoARequest, AccountingResponse): if original_code is None or original_code != StatusServer: # TODO: Handle Status-Server response correctly. hmac_constructor.update(16 * six.b('\00')) elif self.code in (AccessAccept, AccessChallenge, AccessReject): if original_authenticator is None: if self.authenticator: # NOTE: self.authenticator on reply packet is initialized # with request authenticator by design. original_authenticator = self.authenticator else: raise Exception('Missing original authenticator') hmac_constructor.update(original_authenticator) else: # On Access-Request and Status-Server use dynamic authenticator hmac_constructor.update(self.authenticator) hmac_constructor.update(attr) self['Message-Authenticator'] = prev_ma[0] return prev_ma[0] == hmac_constructor.digest() def CreateReply(self, **attributes): """Create a new packet as a reply to this one. This method makes sure the authenticator and secret are copied over to the new instance. """ return Packet(id=self.id, secret=self.secret, authenticator=self.authenticator, dict=self.dict, **attributes) def _DecodeValue(self, attr, value): if attr.values.HasBackward(value): return attr.values.GetBackward(value) else: return tools.DecodeAttr(attr.type, value) def _EncodeValue(self, attr, value): result = '' if attr.values.HasForward(value): result = attr.values.GetForward(value) else: result = tools.EncodeAttr(attr.type, value) if attr.encrypt == 2: # salt encrypt attribute result = self.SaltCrypt(result) return result def _EncodeKeyValues(self, key, values): if not isinstance(key, str): return (key, values) if not isinstance(values, (list, tuple)): values = [values] key, _, tag = key.partition(":") attr = self.dict.attributes[key] key = self._EncodeKey(key) if tag: tag = struct.pack('B', int(tag)) if attr.type == "integer": return (key, [tag + self._EncodeValue(attr, v)[1:] for v in values]) else: return (key, [tag + self._EncodeValue(attr, v) for v in values]) else: return (key, [self._EncodeValue(attr, v) for v in values]) def _EncodeKey(self, key): if not isinstance(key, str): return key attr = self.dict.attributes[key] if attr.vendor and not attr.is_sub_attribute: #sub attribute keys don't need vendor return (self.dict.vendors.GetForward(attr.vendor), attr.code) else: return attr.code def _DecodeKey(self, key): """Turn a key into a string if possible""" if self.dict.attrindex.HasBackward(key): return self.dict.attrindex.GetBackward(key) return key def AddAttribute(self, key, value): """Add an attribute to the packet. :param key: attribute name or identification :type key: string, attribute code or (vendor code, attribute code) tuple :param value: value :type value: depends on type of attribute """ attr = self.dict.attributes[key.partition(':')[0]] (key, value) = self._EncodeKeyValues(key, value) if attr.is_sub_attribute: tlv = self.setdefault(self._EncodeKey(attr.parent.name), {}) encoded = tlv.setdefault(key, []) else: encoded = self.setdefault(key, []) encoded.extend(value) def get(self, key, failobj=None): try: res = self.__getitem__(key) except KeyError: res = failobj return res def __getitem__(self, key): if not isinstance(key, six.string_types): return OrderedDict.__getitem__(self, key) values = OrderedDict.__getitem__(self, self._EncodeKey(key)) attr = self.dict.attributes[key] if attr.type == 'tlv': # return map from sub attribute code to its values res = {} for (sub_attr_key, sub_attr_val) in values.items(): sub_attr_name = attr.sub_attributes[sub_attr_key] sub_attr = self.dict.attributes[sub_attr_name] for v in sub_attr_val: res.setdefault(sub_attr_name, []).append(self._DecodeValue(sub_attr, v)) return res else: res = [] for v in values: res.append(self._DecodeValue(attr, v)) return res def __contains__(self, key): try: return OrderedDict.__contains__(self, self._EncodeKey(key)) except KeyError: return False has_key = __contains__ def __delitem__(self, key): OrderedDict.__delitem__(self, self._EncodeKey(key)) def __setitem__(self, key, item): if isinstance(key, six.string_types): (key, item) = self._EncodeKeyValues(key, item) OrderedDict.__setitem__(self, key, item) else: OrderedDict.__setitem__(self, key, item) def keys(self): return [self._DecodeKey(key) for key in OrderedDict.keys(self)] @staticmethod def CreateAuthenticator(): """Create a packet authenticator. All RADIUS packets contain a sixteen byte authenticator which is used to authenticate replies from the RADIUS server and in the password hiding algorithm. This function returns a suitable random string that can be used as an authenticator. :return: valid packet authenticator :rtype: binary string """ data = [] for _ in range(16): data.append(random_generator.randrange(0, 256)) if six.PY3: return bytes(data) else: return ''.join(chr(b) for b in data) def CreateID(self): """Create a packet ID. All RADIUS requests have a ID which is used to identify a request. This is used to detect retries and replay attacks. This function returns a suitable random number that can be used as ID. :return: ID number :rtype: integer """ return random_generator.randrange(0, 256) def ReplyPacket(self): """Create a ready-to-transmit authentication reply packet. Returns a RADIUS packet which can be directly transmitted to a RADIUS server. This differs with Packet() in how the authenticator is calculated. :return: raw packet :rtype: string """ assert(self.authenticator) assert(self.secret is not None) if self.message_authenticator: self._refresh_message_authenticator() attr = self._PktEncodeAttributes() header = struct.pack('!BBH', self.code, self.id, (20 + len(attr))) authenticator = md5_constructor(header[0:4] + self.authenticator + attr + self.secret).digest() return header + authenticator + attr def VerifyReply(self, reply, rawreply=None): if reply.id != self.id: return False if rawreply is None: rawreply = reply.ReplyPacket() attr = reply._PktEncodeAttributes() # The Authenticator field in an Accounting-Response packet is called # the Response Authenticator, and contains a one-way MD5 hash # calculated over a stream of octets consisting of the Accounting # Response Code, Identifier, Length, the Request Authenticator field # from the Accounting-Request packet being replied to, and the # response attributes if any, followed by the shared secret. The # resulting 16 octet MD5 hash value is stored in the Authenticator # field of the Accounting-Response packet. hash = md5_constructor(rawreply[0:4] + self.authenticator + rawreply[20:] + self.secret).digest() if hash != rawreply[4:20]: return False return True def _PktEncodeAttribute(self, key, value): if isinstance(key, tuple): value = struct.pack('!L', key[0]) + \ self._PktEncodeAttribute(key[1], value) key = 26 return struct.pack('!BB', key, (len(value) + 2)) + value def _PktEncodeTlv(self, tlv_key, tlv_value): tlv_attr = self.dict.attributes[self._DecodeKey(tlv_key)] curr_avp = six.b('') avps = [] max_sub_attribute_len = max(map(lambda item: len(item[1]), tlv_value.items())) for i in range(max_sub_attribute_len): sub_attr_encoding = six.b('') for (code, datalst) in tlv_value.items(): if i < len(datalst): sub_attr_encoding += self._PktEncodeAttribute(code, datalst[i]) # split above 255. assuming len of one instance of all sub tlvs is lower than 255 if (len(sub_attr_encoding) + len(curr_avp)) < 245: curr_avp += sub_attr_encoding else: avps.append(curr_avp) curr_avp = sub_attr_encoding avps.append(curr_avp) tlv_avps = [] for avp in avps: value = struct.pack('!BB', tlv_attr.code, (len(avp) + 2)) + avp tlv_avps.append(value) if tlv_attr.vendor: vendor_avps = six.b('') for avp in tlv_avps: vendor_avps += struct.pack( '!BBL', 26, (len(avp) + 6), self.dict.vendors.GetForward(tlv_attr.vendor) ) + avp return vendor_avps else: return b''.join(tlv_avps) def _PktEncodeAttributes(self): result = six.b('') for (code, datalst) in self.items(): attribute = self.dict.attributes.get(self._DecodeKey(code)) if attribute and attribute.type == 'tlv': result += self._PktEncodeTlv(code, datalst) else: for data in datalst: result += self._PktEncodeAttribute(code, data) return result def _PktDecodeVendorAttribute(self, data): # Check if this packet is long enough to be in the # RFC2865 recommended form if len(data) < 6: return [(26, data)] (vendor, atype, length) = struct.unpack('!LBB', data[:6])[0:3] attribute = self.dict.attributes.get(self._DecodeKey((vendor, atype))) try: if attribute and attribute.type == 'tlv': self._PktDecodeTlvAttribute((vendor, atype), data[6:length + 4]) tlvs = [] # tlv is added to the packet inside _PktDecodeTlvAttribute else: tlvs = [((vendor, atype), data[6:length + 4])] except: return [(26, data)] sumlength = 4 + length while len(data) > sumlength: try: atype, length = struct.unpack('!BB', data[sumlength:sumlength+2])[0:2] except: return [(26, data)] tlvs.append(((vendor, atype), data[sumlength+2:sumlength+length])) sumlength += length return tlvs def _PktDecodeTlvAttribute(self, code, data): sub_attributes = self.setdefault(code, {}) loc = 0 while loc < len(data): atype, length = struct.unpack('!BB', data[loc:loc+2])[0:2] sub_attributes.setdefault(atype, []).append(data[loc+2:loc+length]) loc += length def DecodePacket(self, packet): """Initialize the object from raw packet data. Decode a packet as received from the network and decode it. :param packet: raw packet :type packet: string""" try: (self.code, self.id, length, self.authenticator) = \ struct.unpack('!BBH16s', packet[0:20]) except struct.error: raise PacketError('Packet header is corrupt') if len(packet) != length: raise PacketError('Packet has invalid length') if length > 8192: raise PacketError('Packet length is too long (%d)' % length) self.clear() packet = packet[20:] while packet: try: (key, attrlen) = struct.unpack('!BB', packet[0:2]) except struct.error: raise PacketError('Attribute header is corrupt') if attrlen < 2: raise PacketError( 'Attribute length is too small (%d)' % attrlen) value = packet[2:attrlen] attribute = self.dict.attributes.get(self._DecodeKey(key)) if key == 26: for (key, value) in self._PktDecodeVendorAttribute(value): self.setdefault(key, []).append(value) elif key == 80: # POST: Message Authenticator AVP is present. self.message_authenticator = True self.setdefault(key, []).append(value) elif attribute and attribute.type == 'tlv': self._PktDecodeTlvAttribute(key,value) else: self.setdefault(key, []).append(value) packet = packet[attrlen:] def SaltCrypt(self, value): """Salt Encryption :param value: plaintext value :type password: unicode string :return: obfuscated version of the value :rtype: binary string """ if isinstance(value, six.text_type): value = value.encode('utf-8') if self.authenticator is None: # self.authenticator = self.CreateAuthenticator() self.authenticator = 16 * six.b('\x00') random_value = 32768 + random_generator.randrange(0, 32767) if six.PY3: salt_raw = struct.pack('!H', random_value ) salt = chr(salt_raw[0]) + chr(salt_raw[1]) else: salt = struct.pack('!H', random_value ) salt = chr(ord(salt[0]) | 1 << 7)+salt[1] result = six.b(salt) length = struct.pack("B", len(value)) buf = length + value if len(buf) % 16 != 0: buf += six.b('\x00') * (16 - (len(buf) % 16)) last = self.authenticator + six.b(salt) while buf: hash = md5_constructor(self.secret + last).digest() if six.PY3: for i in range(16): result += bytes((hash[i] ^ buf[i],)) else: for i in range(16): result += chr(ord(hash[i]) ^ ord(buf[i])) last = result[-16:] buf = buf[16:] return result class AuthPacket(Packet): def __init__(self, code=AccessRequest, id=None, secret=six.b(''), authenticator=None, auth_type='pap', **attributes): """Constructor :param code: packet type code :type code: integer (8bits) :param id: packet identification number :type id: integer (8 bits) :param secret: secret needed to communicate with a RADIUS server :type secret: string :param dict: RADIUS dictionary :type dict: pyrad.dictionary.Dictionary class :param packet: raw packet to decode :type packet: string """ Packet.__init__(self, code, id, secret, authenticator, **attributes) self.auth_type = auth_type def CreateReply(self, **attributes): """Create a new packet as a reply to this one. This method makes sure the authenticator and secret are copied over to the new instance. """ return AuthPacket(AccessAccept, self.id, self.secret, self.authenticator, dict=self.dict, auth_type=self.auth_type, **attributes) def RequestPacket(self): """Create a ready-to-transmit authentication request packet. Return a RADIUS packet which can be directly transmitted to a RADIUS server. :return: raw packet :rtype: string """ if self.authenticator is None: self.authenticator = self.CreateAuthenticator() if self.id is None: self.id = self.CreateID() if self.message_authenticator: self._refresh_message_authenticator() attr = self._PktEncodeAttributes() if self.auth_type == 'eap-md5': header = struct.pack( '!BBH16s', self.code, self.id, (20 + 18 + len(attr)), self.authenticator ) digest = hmac_new( self.secret, header + attr + struct.pack('!BB16s', 80, struct.calcsize('!BB16s'), b''), ).digest() return ( header + attr + struct.pack('!BB16s', 80, struct.calcsize('!BB16s'), digest) ) header = struct.pack('!BBH16s', self.code, self.id, (20 + len(attr)), self.authenticator) return header + attr def PwDecrypt(self, password): """Obfuscate a RADIUS password. RADIUS hides passwords in packets by using an algorithm based on the MD5 hash of the packet authenticator and RADIUS secret. This function reverses the obfuscation process. :param password: obfuscated form of password :type password: binary string :return: plaintext password :rtype: unicode string """ buf = password pw = six.b('') last = self.authenticator while buf: hash = md5_constructor(self.secret + last).digest() if six.PY3: for i in range(16): pw += bytes((hash[i] ^ buf[i],)) else: for i in range(16): pw += chr(ord(hash[i]) ^ ord(buf[i])) (last, buf) = (buf[:16], buf[16:]) while pw.endswith(six.b('\x00')): pw = pw[:-1] return pw.decode('utf-8') def PwCrypt(self, password): """Obfuscate password. RADIUS hides passwords in packets by using an algorithm based on the MD5 hash of the packet authenticator and RADIUS secret. If no authenticator has been set before calling PwCrypt one is created automatically. Changing the authenticator after setting a password that has been encrypted using this function will not work. :param password: plaintext password :type password: unicode string :return: obfuscated version of the password :rtype: binary string """ if self.authenticator is None: self.authenticator = self.CreateAuthenticator() if isinstance(password, six.text_type): password = password.encode('utf-8') buf = password if len(password) % 16 != 0: buf += six.b('\x00') * (16 - (len(password) % 16)) result = six.b('') last = self.authenticator while buf: hash = md5_constructor(self.secret + last).digest() if six.PY3: for i in range(16): result += bytes((hash[i] ^ buf[i],)) else: for i in range(16): result += chr(ord(hash[i]) ^ ord(buf[i])) last = result[-16:] buf = buf[16:] return result def VerifyChapPasswd(self, userpwd): """ Verify RADIUS ChapPasswd :param userpwd: plaintext password :type userpwd: str :return: is verify ok :rtype: bool """ if not self.authenticator: self.authenticator = self.CreateAuthenticator() if isinstance(userpwd, six.text_type): userpwd = userpwd.strip().encode('utf-8') chap_password = tools.DecodeOctets(self.get(3)[0]) if len(chap_password) != 17: return False chapid = chap_password[0] if six.PY3: chapid = chr(chapid).encode('utf-8') password = chap_password[1:] challenge = self.authenticator if 'CHAP-Challenge' in self: challenge = self['CHAP-Challenge'][0] return password == md5_constructor(chapid + userpwd + challenge).digest() def VerifyAuthRequest(self): """Verify request authenticator. :return: True if verification failed else False :rtype: boolean """ assert(self.raw_packet) hash = md5_constructor(self.raw_packet[0:4] + 16 * six.b('\x00') + self.raw_packet[20:] + self.secret).digest() return hash == self.authenticator class AcctPacket(Packet): """RADIUS accounting packets. This class is a specialization of the generic :obj:`Packet` class for accounting packets. """ def __init__(self, code=AccountingRequest, id=None, secret=six.b(''), authenticator=None, **attributes): """Constructor :param dict: RADIUS dictionary :type dict: pyrad.dictionary.Dictionary class :param secret: secret needed to communicate with a RADIUS server :type secret: string :param id: packet identification number :type id: integer (8 bits) :param code: packet type code :type code: integer (8bits) :param packet: raw packet to decode :type packet: string """ Packet.__init__(self, code, id, secret, authenticator, **attributes) def CreateReply(self, **attributes): """Create a new packet as a reply to this one. This method makes sure the authenticator and secret are copied over to the new instance. """ return AcctPacket(AccountingResponse, self.id, self.secret, self.authenticator, dict=self.dict, **attributes) def VerifyAcctRequest(self): """Verify request authenticator. :return: False if verification failed else True :rtype: boolean """ assert(self.raw_packet) hash = md5_constructor(self.raw_packet[0:4] + 16 * six.b('\x00') + self.raw_packet[20:] + self.secret).digest() return hash == self.authenticator def RequestPacket(self): """Create a ready-to-transmit authentication request packet. Return a RADIUS packet which can be directly transmitted to a RADIUS server. :return: raw packet :rtype: string """ if self.id is None: self.id = self.CreateID() if self.message_authenticator: self._refresh_message_authenticator() attr = self._PktEncodeAttributes() header = struct.pack('!BBH', self.code, self.id, (20 + len(attr))) self.authenticator = md5_constructor(header[0:4] + 16 * six.b('\x00') + attr + self.secret).digest() ans = header + self.authenticator + attr return ans class CoAPacket(Packet): """RADIUS CoA packets. This class is a specialization of the generic :obj:`Packet` class for CoA packets. """ def __init__(self, code=CoARequest, id=None, secret=six.b(''), authenticator=None, **attributes): """Constructor :param dict: RADIUS dictionary :type dict: pyrad.dictionary.Dictionary class :param secret: secret needed to communicate with a RADIUS server :type secret: string :param id: packet identification number :type id: integer (8 bits) :param code: packet type code :type code: integer (8bits) :param packet: raw packet to decode :type packet: string """ Packet.__init__(self, code, id, secret, authenticator, **attributes) def CreateReply(self, **attributes): """Create a new packet as a reply to this one. This method makes sure the authenticator and secret are copied over to the new instance. """ return CoAPacket(CoAACK, self.id, self.secret, self.authenticator, dict=self.dict, **attributes) def VerifyCoARequest(self): """Verify request authenticator. :return: False if verification failed else True :rtype: boolean """ assert(self.raw_packet) hash = md5_constructor(self.raw_packet[0:4] + 16 * six.b('\x00') + self.raw_packet[20:] + self.secret).digest() return hash == self.authenticator def RequestPacket(self): """Create a ready-to-transmit CoA request packet. Return a RADIUS packet which can be directly transmitted to a RADIUS server. :return: raw packet :rtype: string """ attr = self._PktEncodeAttributes() if self.id is None: self.id = self.CreateID() header = struct.pack('!BBH', self.code, self.id, (20 + len(attr))) self.authenticator = md5_constructor(header[0:4] + 16 * six.b('\x00') + attr + self.secret).digest() if self.message_authenticator: self._refresh_message_authenticator() attr = self._PktEncodeAttributes() self.authenticator = md5_constructor(header[0:4] + 16 * six.b('\x00') + attr + self.secret).digest() return header + self.authenticator + attr def CreateID(): """Generate a packet ID. :return: packet ID :rtype: 8 bit integer """ global CurrentID CurrentID = (CurrentID + 1) % 256 return CurrentID pyrad-2.4/pyrad/proxy.py000066400000000000000000000046661465226464100153760ustar00rootroot00000000000000# proxy.py # # Copyright 2005,2007 Wichert Akkerman # # A RADIUS proxy as defined in RFC 2138 from pyrad.server import ServerPacketError from pyrad.server import Server from pyrad import packet import select import socket class Proxy(Server): """Base class for RADIUS proxies. This class extends tha RADIUS server class with the capability to handle communication with other RADIUS servers as well. :ivar _proxyfd: network socket used to communicate with other servers :type _proxyfd: socket class instance """ def _PrepareSockets(self): Server._PrepareSockets(self) self._proxyfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self._fdmap[self._proxyfd.fileno()] = self._proxyfd self._poll.register(self._proxyfd.fileno(), (select.POLLIN | select.POLLPRI | select.POLLERR)) def _HandleProxyPacket(self, pkt): """Process a packet received on the reply socket. If this packet should be dropped instead of processed a :obj:`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.AccessAccept, packet.AccessReject, packet.AccountingResponse]: raise ServerPacketError('Received non-response on proxy socket') def _ProcessInput(self, fd): """Process available data. 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. This function calls either :obj:`HandleAuthPacket`, :obj:`HandleAcctPacket` or :obj:`_HandleProxyPacket` depending on which socket is being processed. :param fd: socket to read packet from :type fd: socket class instance :param pkt: packet to process :type pkt: Packet class instance """ if fd.fileno() == self._proxyfd.fileno(): pkt = self._GrabPacket( lambda data, s=self: s.CreatePacket(packet=data), fd) self._HandleProxyPacket(pkt) else: Server._ProcessInput(self, fd) pyrad-2.4/pyrad/server.py000066400000000000000000000305371465226464100155170ustar00rootroot00000000000000# server.py # # Copyright 2003-2004,2007,2016 Wichert Akkerman import select import socket from pyrad import host from pyrad import packet import logging logger = logging.getLogger('pyrad') 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 class ServerPacketError(Exception): """Exception class for bogus packets. ServerPacketError exceptions are only used inside the Server class to abort processing of a packet. """ 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) def _GetAddrInfo(self, addr): """Use getaddrinfo to lookup all addresses for each address. Returns a list of tuples or an empty list: [(family, address)] :param addr: IP address to lookup :type addr: string """ results = set() try: tmp = socket.getaddrinfo(addr, 'www') except socket.gaierror: return [] for el in tmp: results.add((el[0], el[4][0])) return results 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 """ addrFamily = self._GetAddrInfo(addr) for (family, address) in addrFamily: if self.auth_enabled: authfd = socket.socket(family, socket.SOCK_DGRAM) authfd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) authfd.bind((address, self.authport)) self.authfds.append(authfd) if self.acct_enabled: acctfd = socket.socket(family, socket.SOCK_DGRAM) acctfd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) acctfd.bind((address, self.acctport)) self.acctfds.append(acctfd) if self.coa_enabled: coafd = socket.socket(family, socket.SOCK_DGRAM) coafd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) coafd.bind((address, self.coaport)) self.coafds.append(coafd) 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 """ 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 """ 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 """ 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 _AddSecret(self, pkt): """Add secret to packets received and raise ServerPacketError for unknown hosts. :param pkt: packet to process :type pkt: Packet class instance """ if pkt.source[0] in self.hosts: pkt.secret = self.hosts[pkt.source[0]].secret elif '0.0.0.0' in self.hosts: pkt.secret = self.hosts['0.0.0.0'].secret else: raise ServerPacketError('Received packet from unknown host') 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 """ self._AddSecret(pkt) 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 """ self._AddSecret(pkt) 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 """ self._AddSecret(pkt) 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)) 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 self.auth_enabled and fd.fileno() in self._realauthfds: pkt = self._GrabPacket(lambda data, s=self: s.CreateAuthPacket(packet=data), fd) self._HandleAuthPacket(pkt) elif self.acct_enabled and fd.fileno() in self._realacctfds: pkt = self._GrabPacket(lambda data, s=self: s.CreateAcctPacket(packet=data), fd) self._HandleAcctPacket(pkt) elif self.coa_enabled: pkt = self._GrabPacket(lambda data, s=self: s.CreateCoAPacket(packet=data), fd) self._HandleCoaPacket(pkt) else: raise ServerPacketError('Received packet for unknown handler') 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') pyrad-2.4/pyrad/server_async.py000066400000000000000000000271661465226464100167200ustar00rootroot00000000000000# server_async.py # # Copyright 2018-2019 Geaaru import asyncio import logging import traceback from abc import abstractmethod, ABCMeta from enum import Enum from datetime import datetime from pyrad.packet import Packet, AccessAccept, AccessReject, \ AccountingRequest, AccountingResponse, \ DisconnectACK, DisconnectNAK, DisconnectRequest, CoARequest, \ CoAACK, CoANAK, AccessRequest, AuthPacket, AcctPacket, CoAPacket, \ PacketError from pyrad.server import ServerPacketError class ServerType(Enum): Auth = 'Authentication' Acct = 'Accounting' Coa = 'Coa' class DatagramProtocolServer(asyncio.Protocol): def __init__(self, ip, port, logger, server, server_type, hosts, request_callback): self.transport = None self.ip = ip self.port = port self.logger = logger self.server = server self.hosts = hosts self.server_type = server_type self.request_callback = request_callback def connection_made(self, transport): self.transport = transport self.logger.info('[%s:%d] Transport created', self.ip, self.port) def connection_lost(self, exc): if exc: self.logger.warn('[%s:%d] Connection lost: %s', self.ip, self.port, str(exc)) else: self.logger.info('[%s:%d] Transport closed', self.ip, self.port) def send_response(self, reply, addr): self.transport.sendto(reply.ReplyPacket(), addr) def datagram_received(self, data, addr): self.logger.debug('[%s:%d] Received %d bytes from %s', self.ip, self.port, len(data), addr) receive_date = datetime.utcnow() if addr[0] in self.hosts: remote_host = self.hosts[addr[0]] elif '0.0.0.0' in self.hosts: remote_host = self.hosts['0.0.0.0'] else: self.logger.warn('[%s:%d] Drop package from unknown source %s', self.ip, self.port, addr) return try: self.logger.debug('[%s:%d] Received from %s packet: %s', self.ip, self.port, addr, data.hex()) req = Packet(packet=data, dict=self.server.dict) except Exception as exc: self.logger.error('[%s:%d] Error on decode packet: %s', self.ip, self.port, exc) return try: if req.code in (AccountingResponse, AccessAccept, AccessReject, CoANAK, CoAACK, DisconnectNAK, DisconnectACK): raise ServerPacketError('Invalid response packet %d' % req.code) elif self.server_type == ServerType.Auth: if req.code != AccessRequest: raise ServerPacketError('Received non-auth packet on auth port') req = AuthPacket(secret=remote_host.secret, dict=self.server.dict, packet=data) if self.server.enable_pkt_verify: if req.VerifyAuthRequest(): raise PacketError('Packet verification failed') elif self.server_type == ServerType.Coa: if req.code != DisconnectRequest and req.code != CoARequest: raise ServerPacketError('Received non-coa packet on coa port') req = CoAPacket(secret=remote_host.secret, dict=self.server.dict, packet=data) if self.server.enable_pkt_verify: if req.VerifyCoARequest(): raise PacketError('Packet verification failed') elif self.server_type == ServerType.Acct: if req.code != AccountingRequest: raise ServerPacketError('Received non-acct packet on acct port') req = AcctPacket(secret=remote_host.secret, dict=self.server.dict, packet=data) if self.server.enable_pkt_verify: if req.VerifyAcctRequest(): raise PacketError('Packet verification failed') # Call request callback self.request_callback(self, req, addr) except Exception as exc: if self.server.debug: self.logger.exception('[%s:%d] Error for packet from %s', self.ip, self.port, addr) else: self.logger.error('[%s:%d] Error for packet from %s: %s', self.ip, self.port, addr, exc) process_date = datetime.utcnow() self.logger.debug('[%s:%d] Request from %s processed in %d ms', self.ip, self.port, addr, (process_date-receive_date).microseconds/1000) def error_received(self, exc): self.logger.error('[%s:%d] Error received: %s', self.ip, self.port, exc) async def close_transport(self): if self.transport: self.logger.debug('[%s:%d] Close transport...', self.ip, self.port) self.transport.close() self.transport = None def __str__(self): return 'DatagramProtocolServer(ip=%s, port=%d)' % (self.ip, self.port) # Used as protocol_factory def __call__(self): return self class ServerAsync(metaclass=ABCMeta): def __init__(self, auth_port=1812, acct_port=1813, coa_port=3799, hosts=None, dictionary=None, loop=None, logger_name='pyrad', enable_pkt_verify=False, debug=False): if not loop: self.loop = asyncio.get_event_loop() else: self.loop = loop self.logger = logging.getLogger(logger_name) if hosts is None: self.hosts = {} else: self.hosts = hosts self.auth_port = auth_port self.auth_protocols = [] self.acct_port = acct_port self.acct_protocols = [] self.coa_port = coa_port self.coa_protocols = [] self.dict = dictionary self.enable_pkt_verify = enable_pkt_verify self.debug = debug def __request_handler__(self, protocol, req, addr): try: if protocol.server_type == ServerType.Acct: self.handle_acct_packet(protocol, req, addr) elif protocol.server_type == ServerType.Auth: self.handle_auth_packet(protocol, req, addr) elif protocol.server_type == ServerType.Coa and \ req.code == CoARequest: self.handle_coa_packet(protocol, req, addr) elif protocol.server_type == ServerType.Coa and \ req.code == DisconnectRequest: self.handle_disconnect_packet(protocol, req, addr) else: self.logger.error('[%s:%s] Unexpected request found', protocol.ip, protocol.port) except Exception as exc: if self.debug: self.logger.exception('[%s:%s] Unexpected error', protocol.ip, protocol.port) else: self.logger.error('[%s:%s] Unexpected error: %s', protocol.ip, protocol.port, exc) def __is_present_proto__(self, ip, port): if port == self.auth_port: for proto in self.auth_protocols: if proto.ip == ip: return True elif port == self.acct_port: for proto in self.acct_protocols: if proto.ip == ip: return True elif port == self.coa_port: for proto in self.coa_protocols: if proto.ip == ip: return True return False # noinspection PyPep8Naming @staticmethod def CreateReplyPacket(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) return reply async def initialize_transports(self, enable_acct=False, enable_auth=False, enable_coa=False, addresses=None): task_list = [] if not enable_acct and not enable_auth and not enable_coa: raise Exception('No transports selected') if not addresses or len(addresses) == 0: addresses = ['127.0.0.1'] # noinspection SpellCheckingInspection for addr in addresses: if enable_acct and not self.__is_present_proto__(addr, self.acct_port): protocol_acct = DatagramProtocolServer( addr, self.acct_port, self.logger, self, ServerType.Acct, self.hosts, self.__request_handler__ ) bind_addr = (addr, self.acct_port) acct_connect = self.loop.create_datagram_endpoint( protocol_acct, reuse_port=True, local_addr=bind_addr ) self.acct_protocols.append(protocol_acct) task_list.append(acct_connect) if enable_auth and not self.__is_present_proto__(addr, self.auth_port): protocol_auth = DatagramProtocolServer( addr, self.auth_port, self.logger, self, ServerType.Auth, self.hosts, self.__request_handler__ ) bind_addr = (addr, self.auth_port) auth_connect = self.loop.create_datagram_endpoint( protocol_auth, reuse_port=True, local_addr=bind_addr ) self.auth_protocols.append(protocol_auth) task_list.append(auth_connect) if enable_coa and not self.__is_present_proto__(addr, self.coa_port): protocol_coa = DatagramProtocolServer( addr, self.coa_port, self.logger, self, ServerType.Coa, self.hosts, self.__request_handler__ ) bind_addr = (addr, self.coa_port) coa_connect = self.loop.create_datagram_endpoint( protocol_coa, reuse_port=True, local_addr=bind_addr ) self.coa_protocols.append(protocol_coa) task_list.append(coa_connect) await asyncio.ensure_future( asyncio.gather( *task_list, return_exceptions=False, ), loop=self.loop ) # noinspection SpellCheckingInspection async def deinitialize_transports(self, deinit_coa=True, deinit_auth=True, deinit_acct=True): if deinit_coa: for proto in self.coa_protocols: await proto.close_transport() del proto self.coa_protocols = [] if deinit_auth: for proto in self.auth_protocols: await proto.close_transport() del proto self.auth_protocols = [] if deinit_acct: for proto in self.acct_protocols: await proto.close_transport() del proto self.acct_protocols = [] @abstractmethod def handle_auth_packet(self, protocol, pkt, addr): pass @abstractmethod def handle_acct_packet(self, protocol, pkt, addr): pass @abstractmethod def handle_coa_packet(self, protocol, pkt, addr): pass @abstractmethod def handle_disconnect_packet(self, protocol, pkt, addr): pass pyrad-2.4/pyrad/tools.py000066400000000000000000000156251465226464100153520ustar00rootroot00000000000000# tools.py # # Utility functions from netaddr import IPAddress from netaddr import IPNetwork import struct import six import binascii def EncodeString(str): if len(str) > 253: raise ValueError('Can only encode strings of <= 253 characters') if isinstance(str, six.text_type): return str.encode('utf-8') else: return str def EncodeOctets(str): if len(str) > 253: raise ValueError('Can only encode strings of <= 253 characters') if str.startswith(b'0x'): hexstring = str.split(b'0x')[1] return binascii.unhexlify(hexstring) else: return str def EncodeAddress(addr): if not isinstance(addr, six.string_types): raise TypeError('Address has to be a string') return IPAddress(addr).packed def EncodeIPv6Prefix(addr): if not isinstance(addr, six.string_types): raise TypeError('IPv6 Prefix has to be a string') ip = IPNetwork(addr) return struct.pack('2B', *[0, ip.prefixlen]) + ip.ip.packed def EncodeIPv6Address(addr): if not isinstance(addr, six.string_types): raise TypeError('IPv6 Address has to be a string') return IPAddress(addr).packed def EncodeAscendBinary(str): """ Format: List of type=value pairs sperated by spaces. Example: 'family=ipv4 action=discard direction=in dst=10.10.255.254/32' Type: family ipv4(default) or ipv6 action discard(default) or accept direction in(default) or out src source prefix (default ignore) dst destination prefix (default ignore) proto protocol number / next-header number (default ignore) sport source port (default ignore) dport destination port (default ignore) sportq source port qualifier (default 0) dportq destination port qualifier (default 0) Source/Destination Port Qualifier: 0 no compare 1 less than 2 equal to 3 greater than 4 not equal to """ terms = { 'family': b'\x01', 'action': b'\x00', 'direction': b'\x01', 'src': b'\x00\x00\x00\x00', 'dst': b'\x00\x00\x00\x00', 'srcl': b'\x00', 'dstl': b'\x00', 'proto': b'\x00', 'sport': b'\x00\x00', 'dport': b'\x00\x00', 'sportq': b'\x00', 'dportq': b'\x00' } for t in str.split(' '): key, value = t.split('=') if key == 'family' and value == 'ipv6': terms[key] = b'\x03' if terms['src'] == b'\x00\x00\x00\x00': terms['src'] = 16 * b'\x00' if terms['dst'] == b'\x00\x00\x00\x00': terms['dst'] = 16 * b'\x00' elif key == 'action' and value == 'accept': terms[key] = b'\x01' elif key == 'direction' and value == 'out': terms[key] = b'\x00' elif key == 'src' or key == 'dst': ip = IPNetwork(value) terms[key] = ip.ip.packed terms[key+'l'] = struct.pack('B', ip.prefixlen) elif key == 'sport' or key == 'dport': terms[key] = struct.pack('!H', int(value)) elif key == 'sportq' or key == 'dportq' or key == 'proto': terms[key] = struct.pack('B', int(value)) trailer = 8 * b'\x00' result = b''.join((terms['family'], terms['action'], terms['direction'], b'\x00', terms['src'], terms['dst'], terms['srcl'], terms['dstl'], terms['proto'], b'\x00', terms['sport'], terms['dport'], terms['sportq'], terms['dportq'], b'\x00\x00', trailer)) return result def EncodeInteger(num, format='!I'): try: num = int(num) except: raise TypeError('Can not encode non-integer as integer') return struct.pack(format, num) def EncodeInteger64(num, format='!Q'): try: num = int(num) except: raise TypeError('Can not encode non-integer as integer64') return struct.pack(format, num) def EncodeDate(num): if not isinstance(num, int): raise TypeError('Can not encode non-integer as date') return struct.pack('!I', num) def DecodeString(str): try: return str.decode('utf-8') except: return str def DecodeOctets(str): return str def DecodeAddress(addr): return '.'.join(map(str, struct.unpack('BBBB', addr))) def DecodeIPv6Prefix(addr): addr = addr + b'\x00' * (18-len(addr)) _, length, prefix = ':'.join(map('{0:x}'.format, struct.unpack('!BB'+'H'*8, addr))).split(":", 2) return str(IPNetwork("%s/%s" % (prefix, int(length, 16)))) def DecodeIPv6Address(addr): addr = addr + b'\x00' * (16-len(addr)) prefix = ':'.join(map('{0:x}'.format, struct.unpack('!'+'H'*8, addr))) return str(IPAddress(prefix)) def DecodeAscendBinary(str): return str def DecodeInteger(num, format='!I'): return (struct.unpack(format, num))[0] def DecodeInteger64(num, format='!Q'): return (struct.unpack(format, num))[0] def DecodeDate(num): return (struct.unpack('!I', num))[0] def EncodeAttr(datatype, value): if datatype == 'string': return EncodeString(value) elif datatype == 'octets': return EncodeOctets(value) elif datatype == 'integer': return EncodeInteger(value) elif datatype == 'ipaddr': return EncodeAddress(value) elif datatype == 'ipv6prefix': return EncodeIPv6Prefix(value) elif datatype == 'ipv6addr': return EncodeIPv6Address(value) elif datatype == 'abinary': return EncodeAscendBinary(value) elif datatype == 'signed': return EncodeInteger(value, '!i') elif datatype == 'short': return EncodeInteger(value, '!H') elif datatype == 'byte': return EncodeInteger(value, '!B') elif datatype == 'date': return EncodeDate(value) elif datatype == 'integer64': return EncodeInteger64(value) else: raise ValueError('Unknown attribute type %s' % datatype) def DecodeAttr(datatype, value): if datatype == 'string': return DecodeString(value) elif datatype == 'octets': return DecodeOctets(value) elif datatype == 'integer': return DecodeInteger(value) elif datatype == 'ipaddr': return DecodeAddress(value) elif datatype == 'ipv6prefix': return DecodeIPv6Prefix(value) elif datatype == 'ipv6addr': return DecodeIPv6Address(value) elif datatype == 'abinary': return DecodeAscendBinary(value) elif datatype == 'signed': return DecodeInteger(value, '!i') elif datatype == 'short': return DecodeInteger(value, '!H') elif datatype == 'byte': return DecodeInteger(value, '!B') elif datatype == 'date': return DecodeDate(value) elif datatype == 'integer64': return DecodeInteger64(value) else: raise ValueError('Unknown attribute type %s' % datatype) pyrad-2.4/setup.cfg000066400000000000000000000001261465226464100143300ustar00rootroot00000000000000[flake8] max-line-length = 100 doctests = True [egg_info] tag_build = tag_date = 0 pyrad-2.4/setup.py000066400000000000000000000022561465226464100142270ustar00rootroot00000000000000#!/usr/bin/env python from setuptools import setup, find_packages import pyrad setup(name='pyrad', version=pyrad.__version__, author='Istvan Ruzman, Christian Giese', author_email='istvan@ruzman.eu, developer@gicnet.de', url='https://github.com/pyradius/pyrad', license='BSD', description='RADIUS tools', long_description=open('README.rst').read(), classifiers=[ 'Development Status :: 6 - Mature', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: System :: Systems Administration :: Authentication/Directory', ], packages=find_packages(exclude=['tests']), keywords=['radius', 'authentication'], zip_safe=True, include_package_data=True, install_requires=['six', 'netaddr'], tests_require='nose>=0.10.0b1', test_suite='nose.collector', )