diff --git a/.travis.yml b/.travis.yml index f14407c..8dede9f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,10 @@ language: python python: - - "3.6" - "3.7" - "3.8" # command to install dependencies -install: "pip install nose coverage python-coveralls" +install: "pip install pytest pytest-black pytest-cov pytest-mypy pytest-pylint pytest-flake8 coveralls" # command to run tests -script: nosetests -v --with-coverage --cover-package=pyrad +script: pytest --black --pylint --pylint-jobs=4 --mypy --cov=pyrad3 --flake8 after_success: - coveralls diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 1446ca6..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXPROJ = pyrad -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 9fa49d9..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,36 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build -set SPHINXPROJ=pyrad - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd diff --git a/docs/source/_static/logo.png b/docs/source/_static/logo.png deleted file mode 100644 index a4e16d5..0000000 Binary files a/docs/source/_static/logo.png and /dev/null differ diff --git a/docs/source/api/client.rst b/docs/source/api/client.rst deleted file mode 100644 index ae90c0d..0000000 --- a/docs/source/api/client.rst +++ /dev/null @@ -1,10 +0,0 @@ -:mod:`pyrad.client` -- basic client -=================================== - -.. automodule:: pyrad.client - - .. autoclass:: Timeout - :members: - - .. autoclass:: Client - :members: diff --git a/docs/source/api/dictionary.rst b/docs/source/api/dictionary.rst deleted file mode 100644 index da370e8..0000000 --- a/docs/source/api/dictionary.rst +++ /dev/null @@ -1,10 +0,0 @@ -:mod:`pyrad.dictionary` -- RADIUS dictionary -============================================ - -.. automodule:: pyrad.dictionary - - .. autoclass:: ParseError - :members: - - .. autoclass:: Dictionary - :members: diff --git a/docs/source/api/host.rst b/docs/source/api/host.rst deleted file mode 100644 index 29e1760..0000000 --- a/docs/source/api/host.rst +++ /dev/null @@ -1,7 +0,0 @@ -:mod:`pyrad.host` -- RADIUS host definition -=========================================== - -.. automodule:: pyrad.host - - .. autoclass:: Host - :members: diff --git a/docs/source/api/packet.rst b/docs/source/api/packet.rst deleted file mode 100644 index 05c4a21..0000000 --- a/docs/source/api/packet.rst +++ /dev/null @@ -1,48 +0,0 @@ -:mod:`pyrad.packet` -- packet encoding and decoding -=================================================== - -.. automodule:: pyrad.packet - - .. autoclass:: Packet - :members: - - .. autoclass:: AuthPacket - :members: - - .. autoclass:: AcctPacket - :members: - - .. autoclass:: CoAPacket - :members: - - .. autoclass:: PacketError - :members: - - -Constants ---------- - -The :mod:`pyrad.packet` module defines several common constants -that are useful when dealing with RADIUS packets. - -The following packet codes are defined: - -================== ====== -Constant name Value -================== ====== -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 -================== ====== diff --git a/docs/source/api/proxy.rst b/docs/source/api/proxy.rst deleted file mode 100644 index 34017a4..0000000 --- a/docs/source/api/proxy.rst +++ /dev/null @@ -1,7 +0,0 @@ -:mod:`pyrad.proxy` -- basic proxy -================================= - -.. automodule:: pyrad.proxy - - .. autoclass:: Proxy - :members: diff --git a/docs/source/api/server.rst b/docs/source/api/server.rst deleted file mode 100644 index 2cfbd38..0000000 --- a/docs/source/api/server.rst +++ /dev/null @@ -1,13 +0,0 @@ -:mod:`pyrad.server` -- basic server -=================================== - -.. automodule:: pyrad.server - - .. autoclass:: RemoteHost - :members: - - .. autoclass:: ServerPacketError - :members: - - .. autoclass:: Server - :members: diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index 84f285e..0000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,160 +0,0 @@ -# -*- coding: utf-8 -*- -# -# pyrad documentation build configuration file, created by -# sphinx-quickstart on Thu Feb 2 15:16:16 2017. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. - -import os -import sys -sys.path.insert(0, os.path.abspath('../../')) - - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.viewcode'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'pyrad' -copyright = u'Copyright 2002-2020 Wichert Akkerman and Christian Giese. All rights reserved.' -author = u'Christian Giese ' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = u'2.3' -# The full version, including alpha/beta/rc tags. -release = u'2.3' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = [] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -# html_theme = 'alabaster' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -html_logo = '_static/logo.png' - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = 'pyraddoc' - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'pyrad.tex', u'pyrad Documentation', - u'Wichert Akkerman', 'manual'), -] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'pyrad', u'pyrad Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'pyrad', u'pyrad Documentation', - author, 'pyrad', 'One line description of project.', - 'Miscellaneous'), -] - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index 45ee377..0000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,75 +0,0 @@ - -********************************* -:mod:`pyrad` -- RADIUS for Python -********************************* - -:Author: Wichert Akkerman -:Version: |version| - -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 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.6 or later, or Python 3.2 or later - -Installing is simple; pyrad uses the standard distutils system for installing -Python modules:: - - python setup.py install - - -API Documentation -================= - -Per-module :mod:`pyrad` API documentation. - -.. toctree:: - :maxdepth: 2 - - api/client - api/dictionary - api/host - api/packet - api/proxy - api/server - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/example/acct.py b/example/acct.py deleted file mode 100755 index d68601f..0000000 --- a/example/acct.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -import random -import socket -import sys - -from os import path - -import pyrad.packet - -from pyrad.client import Client -from pyrad.dictionary import Dictionary - - -def send_accounting_packet(srv, req): - try: - srv.SendPacket(req) - except pyrad.client.Timeout: - print("RADIUS server does not reply") - sys.exit(1) - except socket.error as error: - print("Network error: " + error[1]) - sys.exit(1) - - -def main(path_to_dictionary): - srv = Client( - server="127.0.0.1", - secret=b"Kah3choteereethiejeimaeziecumi", - dict=Dictionary(path_to_dictionary), - ) - - req = srv.CreateAcctPacket( - **{ - "User-Name": "wichert", - "NAS-IP-Address": "192.168.1.10", - "NAS-Port": 0, - "NAS-Identifier": "trillian", - "Called-Station-Id": "00-04-5F-00-0F-D1", - "Calling-Station-Id": "00-01-24-80-B3-9C", - "Framed-IP-Address": "10.0.0.100", - } - ) - - print("Sending accounting start packet") - req["Acct-Status-Type"] = "Start" - send_accounting_packet(srv, req) - - print("Sending accounting stop packet") - req["Acct-Status-Type"] = "Stop" - req["Acct-Input-Octets"] = random.randrange(2 ** 10, 2 ** 30) - req["Acct-Output-Octets"] = random.randrange(2 ** 10, 2 ** 30) - req["Acct-Session-Time"] = random.randrange(120, 3600) - req["Acct-Terminate-Cause"] = random.choice( - ["User-Request", "Idle-Timeout"] - ) - send_accounting_packet(srv, req) - - -if __name__ == "__main__": - dictionary = path.join(path.dirname(path.abspath(__file__)), "dictionary") - main(dictionary) diff --git a/example/auth.py b/example/auth.py deleted file mode 100755 index 92a3939..0000000 --- a/example/auth.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -import socket -import sys - -from os import path - -import pyrad.packet - -from pyrad.client import Client -from pyrad.dictionary import Dictionary - - -def main(path_to_dictionary): - srv = Client( - server="127.0.0.1", - secret=b"Kah3choteereethiejeimaeziecumi", - dict=Dictionary(path_to_dictionary), - ) - - req = srv.CreateAuthPacket( - code=pyrad.packet.AccessRequest, - **{ - "User-Name": "wichert", - "NAS-IP-Address": "192.168.1.10", - "NAS-Port": 0, - "Service-Type": "Login-User", - "NAS-Identifier": "trillian", - "Called-Station-Id": "00-04-5F-00-0F-D1", - "Calling-Station-Id": "00-01-24-80-B3-9C", - "Framed-IP-Address": "10.0.0.100", - }, - ) - - try: - print("Sending authentication request") - reply = srv.SendPacket(req) - except pyrad.client.Timeout: - print("RADIUS server does not reply") - sys.exit(1) - except socket.error as error: - print("Network error: " + error[1]) - sys.exit(1) - - if reply.code == pyrad.packet.AccessAccept: - print("Access accepted") - else: - print("Access denied") - - print("Attributes returned by server:") - for key, value in reply.items(): - print(f"{key} {value}") - - -if __name__ == "__main__": - dictionary = path.join(path.dirname(path.abspath(__file__)), "dictionary") - main(dictionary) diff --git a/example/auth_async.py b/example/auth_async.py deleted file mode 100755 index 7141fed..0000000 --- a/example/auth_async.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 - -import asyncio -import logging -import traceback - -from os import path - -from pyrad.client_async import ClientAsync -from pyrad.dictionary import Dictionary -from pyrad.packet import AccessAccept - -logging.basicConfig( - level="DEBUG", format="%(asctime)s [%(levelname)-8s] %(message)s" -) - - -def create_request(client, user): - return client.CreateAuthPacket( - **{ - "User-Name": user, - "NAS-IP-Address": "192.168.1.10", - "NAS-Port": 0, - "Service-Type": "Login-User", - "NAS-Identifier": "trillian", - "Called-Station-Id": "00-04-5F-00-0F-D1", - "Calling-Station-Id": "00-01-24-80-B3-9C", - "Framed-IP-Address": "10.0.0.100", - } - ) - - -def print_reply(reply): - if reply.code == AccessAccept: - print("Access accepted") - else: - print("Access denied") - - print("Attributes returned by server:") - for key, value in reply.items(): - print(f"{key}: {value}") - - -def initialize_transport(loop, client): - loop.run_until_complete( - asyncio.ensure_future( - client.initialize_transports( - enable_auth=True, - local_addr="127.0.0.1", - local_auth_port=8000, - enable_acct=True, - enable_coa=True, - ) - ) - ) - - -def main(path_to_dictionary): - client = ClientAsync( - server="localhost", - secret=b"Kah3choteereethiejeimaeziecumi", - timeout=4, - dict=Dictionary(path_to_dictionary), - ) - - loop = asyncio.get_event_loop() - - try: - # Initialize transports - initialize_transport(loop, client) - - requests = [] - for i in range(255): - req = create_request(client, f"user{i}") - future = client.SendPacket(req) - requests.append(future) - - # Send auth requests asynchronously to the server - loop.run_until_complete( - asyncio.ensure_future( - asyncio.gather(*requests, return_exceptions=True) - ) - ) - - for future in requests: - if future.exception(): - print("EXCEPTION ", future.exception()) - else: - reply = future.result() - print_reply(reply) - - # Close transports - loop.run_until_complete( - asyncio.ensure_future(client.deinitialize_transports()) - ) - print("END") - except Exception as exc: - print("Error: ", exc) - traceback.print_exc() - - # Close transports - loop.run_until_complete( - asyncio.ensure_future(client.deinitialize_transports()) - ) - - loop.close() - - -if __name__ == "__main__": - dictionary = path.join(path.dirname(path.abspath(__file__)), "dictionary") - main(dictionary) diff --git a/example/client-coa.py b/example/client-coa.py deleted file mode 100755 index a41a035..0000000 --- a/example/client-coa.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright 6WIND, 2017 -# - -import sys - -from os import path - -import pyrad.packet - -from pyrad.dictionary import Dictionary -from pyrad.server import Server, RemoteHost - - -def print_attributes(packet): - print("Attributes") - for key, value in packet.items(): - print(f"{key}: {value}") - - -class FakeCoA(Server): - def HandleCoaPacket(self, packet): - """Accounting packet handler. - Function that is called when a valid - accounting packet has been received. - - :param packet: packet to process - :type packet: Packet class instance - """ - print("Received a coa request %d" % packet.code) - print_attributes(packet) - - reply = self.CreateReplyPacket(packet) - # try ACK or NACK - # reply.code = packet.CoANAK - reply.code = packet.CoAACK - self.SendReplyPacket(packet.fd, reply) - - def HandleDisconnectPacket(self, packet): - print("Received a disconnect request %d" % packet.code) - print_attributes(packet) - - reply = self.CreateReplyPacket(packet) - # try ACK or NACK - # reply.code = packet.DisconnectNAK - reply.code = pyrad.packet.DisconnectACK - self.SendReplyPacket(packet.fd, reply) - - -def main(path_to_dictionary, coa_port): - # create server/coa only and read dictionary - # bind and listen only on 127.0.0.1:argv[1] - coa = FakeCoA( - addresses=["127.0.0.1"], - dict=Dictionary(path_to_dictionary), - coaport=coa_port, - auth_enabled=False, - acct_enabled=False, - coa_enabled=True, - ) - - # add peers (address, secret, name) - coa.hosts["127.0.0.1"] = RemoteHost( - "127.0.0.1", b"Kah3choteereethiejeimaeziecumi", "localhost" - ) - - # start - coa.Run() - - -if __name__ == "__main__": - if len(sys.argv) != 2: - print("usage: client-coa.py {portnumber}") - sys.exit(1) - - dictionary = path.join(path.dirname(path.abspath(__file__)), "dictionary") - main(dictionary, int(sys.argv[1])) diff --git a/example/coa.py b/example/coa.py deleted file mode 100755 index f0ef313..0000000 --- a/example/coa.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python3 -import sys - -from os import path - -import pyrad.packet - -from pyrad.client import Client -from pyrad.dictionary import Dictionary - - -def main(path_to_dictionary, coa_type, nas_identifier): - # create coa client - client = Client( - server="127.0.0.1", - secret=b"Kah3choteereethiejeimaeziecumi", - dict=Dictionary(path_to_dictionary), - ) - - # set coa timeout - client.timeout = 30 - - # create coa request packet - attributes = { - "Acct-Session-Id": "1337", - "NAS-Identifier": nas_identifier, - } - - if coa_type == "coa": - # create coa request - request = client.CreateCoAPacket(**attributes) - elif coa_type == "dis": - # create disconnect request - request = client.CreateCoAPacket( - code=pyrad.packet.DisconnectRequest, **attributes - ) - else: - sys.exit(1) - - # send request - result = client.SendPacket(request) - print(result) - print(result.code) - - -if __name__ == "__main__": - if len(sys.argv) != 3: - print("usage: coa.py {coa|dis} daemon-1234") - sys.exit(1) - - dictionary = path.join(path.dirname(path.abspath(__file__)), "dictionary") - - main(dictionary, sys.argv[1], sys.argv[2]) diff --git a/example/dictionary b/example/dictionary deleted file mode 100644 index a0014c5..0000000 --- a/example/dictionary +++ /dev/null @@ -1,405 +0,0 @@ -# -# Version $Id: dictionary,v 1.1.1.1 2002/10/11 12:25:39 wichert Exp $ -# -# This file contains dictionary translations for parsing -# requests and generating responses. All transactions are -# composed of Attribute/Value Pairs. The value of each attribute -# is specified as one of 4 data types. Valid data types are: -# -# string - 0-253 octets -# ipaddr - 4 octets in network byte order -# integer - 32 bit value in big endian order (high byte first) -# date - 32 bit value in big endian order - seconds since -# 00:00:00 GMT, Jan. 1, 1970 -# -# FreeRADIUS includes extended data types which are not defined -# in RFC 2865 or RFC 2866. These data types are: -# -# abinary - Ascend's binary filter format. -# octets - raw octets, printed and input as hex strings. -# e.g.: 0x123456789abcdef -# -# -# Enumerated values are stored in the user file with dictionary -# VALUE translations for easy administration. -# -# Example: -# -# ATTRIBUTE VALUE -# --------------- ----- -# Framed-Protocol = PPP -# 7 = 1 (integer encoding) -# - -# -# Include compatibility dictionary for older users file. Move this -# directive to the end of the file if you want to see the old names -# in the logfiles too. -# -#$INCLUDE dictionary.compat # compability issues -#$INCLUDE dictionary.acc -#$INCLUDE dictionary.ascend -#$INCLUDE dictionary.bay -#$INCLUDE dictionary.cisco -#$INCLUDE dictionary.livingston -#$INCLUDE dictionary.microsoft -#$INCLUDE dictionary.quintum -#$INCLUDE dictionary.redback -#$INCLUDE dictionary.shasta -#$INCLUDE dictionary.shiva -#$INCLUDE dictionary.tunnel -#$INCLUDE dictionary.usr -#$INCLUDE dictionary.versanet -#$INCLUDE dictionary.erx -$INCLUDE dictionary.freeradius -#$INCLUDE dictionary.alcatel - -# -# Following are the proper new names. Use these. -# -ATTRIBUTE User-Name 1 string -ATTRIBUTE User-Password 2 string -ATTRIBUTE CHAP-Password 3 octets -ATTRIBUTE NAS-IP-Address 4 ipaddr -ATTRIBUTE NAS-Port 5 integer -ATTRIBUTE Service-Type 6 integer -ATTRIBUTE Framed-Protocol 7 integer -ATTRIBUTE Framed-IP-Address 8 ipaddr -ATTRIBUTE Framed-IP-Netmask 9 ipaddr -ATTRIBUTE Framed-Routing 10 integer -ATTRIBUTE Filter-Id 11 string -ATTRIBUTE Framed-MTU 12 integer -ATTRIBUTE Framed-Compression 13 integer -ATTRIBUTE Login-IP-Host 14 ipaddr -ATTRIBUTE Login-Service 15 integer -ATTRIBUTE Login-TCP-Port 16 integer -ATTRIBUTE Reply-Message 18 string -ATTRIBUTE Callback-Number 19 string -ATTRIBUTE Callback-Id 20 string -ATTRIBUTE Framed-Route 22 string -ATTRIBUTE Framed-IPX-Network 23 ipaddr -ATTRIBUTE State 24 octets -ATTRIBUTE Class 25 octets -ATTRIBUTE Vendor-Specific 26 octets -ATTRIBUTE Session-Timeout 27 integer -ATTRIBUTE Idle-Timeout 28 integer -ATTRIBUTE Termination-Action 29 integer -ATTRIBUTE Called-Station-Id 30 string -ATTRIBUTE Calling-Station-Id 31 string -ATTRIBUTE NAS-Identifier 32 string -ATTRIBUTE Proxy-State 33 octets -ATTRIBUTE Login-LAT-Service 34 string -ATTRIBUTE Login-LAT-Node 35 string -ATTRIBUTE Login-LAT-Group 36 octets -ATTRIBUTE Framed-AppleTalk-Link 37 integer -ATTRIBUTE Framed-AppleTalk-Network 38 integer -ATTRIBUTE Framed-AppleTalk-Zone 39 string - -ATTRIBUTE Acct-Status-Type 40 integer -ATTRIBUTE Acct-Delay-Time 41 integer -ATTRIBUTE Acct-Input-Octets 42 integer -ATTRIBUTE Acct-Output-Octets 43 integer -ATTRIBUTE Acct-Session-Id 44 string -ATTRIBUTE Acct-Authentic 45 integer -ATTRIBUTE Acct-Session-Time 46 integer -ATTRIBUTE Acct-Input-Packets 47 integer -ATTRIBUTE Acct-Output-Packets 48 integer -ATTRIBUTE Acct-Terminate-Cause 49 integer -ATTRIBUTE Acct-Multi-Session-Id 50 string -ATTRIBUTE Acct-Link-Count 51 integer -ATTRIBUTE Acct-Input-Gigawords 52 integer -ATTRIBUTE Acct-Output-Gigawords 53 integer -ATTRIBUTE Event-Timestamp 55 date - -ATTRIBUTE CHAP-Challenge 60 string -ATTRIBUTE NAS-Port-Type 61 integer -ATTRIBUTE Port-Limit 62 integer -ATTRIBUTE Login-LAT-Port 63 integer - -ATTRIBUTE Acct-Tunnel-Connection 68 string - -ATTRIBUTE ARAP-Password 70 string -ATTRIBUTE ARAP-Features 71 string -ATTRIBUTE ARAP-Zone-Access 72 integer -ATTRIBUTE ARAP-Security 73 integer -ATTRIBUTE ARAP-Security-Data 74 string -ATTRIBUTE Password-Retry 75 integer -ATTRIBUTE Prompt 76 integer -ATTRIBUTE Connect-Info 77 string -ATTRIBUTE Configuration-Token 78 string -ATTRIBUTE EAP-Message 79 string -ATTRIBUTE Message-Authenticator 80 octets -ATTRIBUTE ARAP-Challenge-Response 84 string # 10 octets -ATTRIBUTE Acct-Interim-Interval 85 integer -ATTRIBUTE NAS-Port-Id 87 string -ATTRIBUTE Framed-Pool 88 string -ATTRIBUTE NAS-IPv6-Address 95 octets # really IPv6 -ATTRIBUTE Framed-Interface-Id 96 octets # 8 octets -ATTRIBUTE Framed-IPv6-Prefix 97 ipv6prefix # stupid format -ATTRIBUTE Login-IPv6-Host 98 octets # really IPv6 -ATTRIBUTE Framed-IPv6-Route 99 string -ATTRIBUTE Framed-IPv6-Pool 100 string -ATTRIBUTE Delegated-IPv6-Prefix 123 ipv6prefix - - -ATTRIBUTE Digest-Response 206 string -ATTRIBUTE Digest-Attributes 207 octets # stupid format - -# -# Experimental Non Protocol Attributes used by Cistron-Radiusd -# - -# These attributes CAN go in the reply item list. -ATTRIBUTE Fall-Through 500 integer -ATTRIBUTE Exec-Program 502 string -ATTRIBUTE Exec-Program-Wait 503 string - -# These attributes CANNOT go in the reply item list. -ATTRIBUTE User-Category 1029 string -ATTRIBUTE Group-Name 1030 string -ATTRIBUTE Huntgroup-Name 1031 string -ATTRIBUTE Simultaneous-Use 1034 integer -ATTRIBUTE Strip-User-Name 1035 integer -ATTRIBUTE Hint 1040 string -ATTRIBUTE Pam-Auth 1041 string -ATTRIBUTE Login-Time 1042 string -ATTRIBUTE Stripped-User-Name 1043 string -ATTRIBUTE Current-Time 1044 string -ATTRIBUTE Realm 1045 string -ATTRIBUTE No-Such-Attribute 1046 string -ATTRIBUTE Packet-Type 1047 integer -ATTRIBUTE Proxy-To-Realm 1048 string -ATTRIBUTE Replicate-To-Realm 1049 string -ATTRIBUTE Acct-Session-Start-Time 1050 date -ATTRIBUTE Acct-Unique-Session-Id 1051 string -ATTRIBUTE Client-IP-Address 1052 ipaddr -ATTRIBUTE Ldap-UserDn 1053 string -ATTRIBUTE NS-MTA-MD5-Password 1054 string -ATTRIBUTE SQL-User-Name 1055 string -ATTRIBUTE LM-Password 1057 octets -ATTRIBUTE NT-Password 1058 octets -ATTRIBUTE SMB-Account-CTRL 1059 integer -ATTRIBUTE SMB-Account-CTRL-TEXT 1061 string -ATTRIBUTE User-Profile 1062 string -ATTRIBUTE Digest-Realm 1063 string -ATTRIBUTE Digest-Nonce 1064 string -ATTRIBUTE Digest-Method 1065 string -ATTRIBUTE Digest-URI 1066 string -ATTRIBUTE Digest-QOP 1067 string -ATTRIBUTE Digest-Algorithm 1068 string -ATTRIBUTE Digest-Body-Digest 1069 string -ATTRIBUTE Digest-CNonce 1070 string -ATTRIBUTE Digest-Nonce-Count 1071 string -ATTRIBUTE Digest-User-Name 1072 string -ATTRIBUTE Pool-Name 1073 string -ATTRIBUTE Ldap-Group 1074 string -ATTRIBUTE Module-Success-Message 1075 string -ATTRIBUTE Module-Failure-Message 1076 string -# X99-Fast 1077 integer - -# -# Non-Protocol Attributes -# These attributes are used internally by the server -# -ATTRIBUTE Auth-Type 1000 integer -ATTRIBUTE Menu 1001 string -ATTRIBUTE Termination-Menu 1002 string -ATTRIBUTE Prefix 1003 string -ATTRIBUTE Suffix 1004 string -ATTRIBUTE Group 1005 string -ATTRIBUTE Crypt-Password 1006 string -ATTRIBUTE Connect-Rate 1007 integer -ATTRIBUTE Add-Prefix 1008 string -ATTRIBUTE Add-Suffix 1009 string -ATTRIBUTE Expiration 1010 date -ATTRIBUTE Autz-Type 1011 integer - -# -# Integer Translations -# - -# User Types - -VALUE Service-Type Login-User 1 -VALUE Service-Type Framed-User 2 -VALUE Service-Type Callback-Login-User 3 -VALUE Service-Type Callback-Framed-User 4 -VALUE Service-Type Outbound-User 5 -VALUE Service-Type Administrative-User 6 -VALUE Service-Type NAS-Prompt-User 7 -VALUE Service-Type Authenticate-Only 8 -VALUE Service-Type Callback-NAS-Prompt 9 -VALUE Service-Type Call-Check 10 -VALUE Service-Type Callback-Administrative 11 - -# Framed Protocols - -VALUE Framed-Protocol PPP 1 -VALUE Framed-Protocol SLIP 2 -VALUE Framed-Protocol ARAP 3 -VALUE Framed-Protocol Gandalf-SLML 4 -VALUE Framed-Protocol Xylogics-IPX-SLIP 5 -VALUE Framed-Protocol X.75-Synchronous 6 - -# Framed Routing Values - -VALUE Framed-Routing None 0 -VALUE Framed-Routing Broadcast 1 -VALUE Framed-Routing Listen 2 -VALUE Framed-Routing Broadcast-Listen 3 - -# Framed Compression Types - -VALUE Framed-Compression None 0 -VALUE Framed-Compression Van-Jacobson-TCP-IP 1 -VALUE Framed-Compression IPX-Header-Compression 2 -VALUE Framed-Compression Stac-LZS 3 - -# Login Services - -VALUE Login-Service Telnet 0 -VALUE Login-Service Rlogin 1 -VALUE Login-Service TCP-Clear 2 -VALUE Login-Service PortMaster 3 -VALUE Login-Service LAT 4 -VALUE Login-Service X25-PAD 5 -VALUE Login-Service X25-T3POS 6 -VALUE Login-Service TCP-Clear-Quiet 8 - -# Login-TCP-Port (see /etc/services for more examples) - -VALUE Login-TCP-Port Telnet 23 -VALUE Login-TCP-Port Rlogin 513 -VALUE Login-TCP-Port Rsh 514 - -# Status Types - -VALUE Acct-Status-Type Start 1 -VALUE Acct-Status-Type Stop 2 -VALUE Acct-Status-Type Interim-Update 3 -VALUE Acct-Status-Type Alive 3 -VALUE Acct-Status-Type Accounting-On 7 -VALUE Acct-Status-Type Accounting-Off 8 -# RFC 2867 Additional Status-Type Values -VALUE Acct-Status-Type Tunnel-Start 9 -VALUE Acct-Status-Type Tunnel-Stop 10 -VALUE Acct-Status-Type Tunnel-Reject 11 -VALUE Acct-Status-Type Tunnel-Link-Start 12 -VALUE Acct-Status-Type Tunnel-Link-Stop 13 -VALUE Acct-Status-Type Tunnel-Link-Reject 14 - -# Authentication Types - -VALUE Acct-Authentic RADIUS 1 -VALUE Acct-Authentic Local 2 - -# Termination Options - -VALUE Termination-Action Default 0 -VALUE Termination-Action RADIUS-Request 1 - -# NAS Port Types - -VALUE NAS-Port-Type Async 0 -VALUE NAS-Port-Type Sync 1 -VALUE NAS-Port-Type ISDN 2 -VALUE NAS-Port-Type ISDN-V120 3 -VALUE NAS-Port-Type ISDN-V110 4 -VALUE NAS-Port-Type Virtual 5 -VALUE NAS-Port-Type PIAFS 6 -VALUE NAS-Port-Type HDLC-Clear-Channel 7 -VALUE NAS-Port-Type X.25 8 -VALUE NAS-Port-Type X.75 9 -VALUE NAS-Port-Type G.3-Fax 10 -VALUE NAS-Port-Type SDSL 11 -VALUE NAS-Port-Type ADSL-CAP 12 -VALUE NAS-Port-Type ADSL-DMT 13 -VALUE NAS-Port-Type IDSL 14 -VALUE NAS-Port-Type Ethernet 15 -VALUE NAS-Port-Type xDSL 16 -VALUE NAS-Port-Type Cable 17 -VALUE NAS-Port-Type Wireless-Other 18 -VALUE NAS-Port-Type Wireless-802.11 19 - -# Acct Terminate Causes, available in 3.3.2 and later - -VALUE Acct-Terminate-Cause User-Request 1 -VALUE Acct-Terminate-Cause Lost-Carrier 2 -VALUE Acct-Terminate-Cause Lost-Service 3 -VALUE Acct-Terminate-Cause Idle-Timeout 4 -VALUE Acct-Terminate-Cause Session-Timeout 5 -VALUE Acct-Terminate-Cause Admin-Reset 6 -VALUE Acct-Terminate-Cause Admin-Reboot 7 -VALUE Acct-Terminate-Cause Port-Error 8 -VALUE Acct-Terminate-Cause NAS-Error 9 -VALUE Acct-Terminate-Cause NAS-Request 10 -VALUE Acct-Terminate-Cause NAS-Reboot 11 -VALUE Acct-Terminate-Cause Port-Unneeded 12 -VALUE Acct-Terminate-Cause Port-Preempted 13 -VALUE Acct-Terminate-Cause Port-Suspended 14 -VALUE Acct-Terminate-Cause Service-Unavailable 15 -VALUE Acct-Terminate-Cause Callback 16 -VALUE Acct-Terminate-Cause User-Error 17 -VALUE Acct-Terminate-Cause Host-Request 18 - -#VALUE Tunnel-Type L2TP 3 -#VALUE Tunnel-Medium-Type IP 1 - -VALUE Prompt No-Echo 0 -VALUE Prompt Echo 1 - -# -# Non-Protocol Integer Translations -# - -VALUE Auth-Type Local 0 -VALUE Auth-Type System 1 -VALUE Auth-Type SecurID 2 -VALUE Auth-Type Crypt-Local 3 -VALUE Auth-Type Reject 4 -VALUE Auth-Type ActivCard 5 -VALUE Auth-Type EAP 6 -VALUE Auth-Type ARAP 7 - -# -# Cistron extensions -# -VALUE Auth-Type Ldap 252 -VALUE Auth-Type Pam 253 -VALUE Auth-Type Accept 254 - -VALUE Auth-Type PAP 1024 -VALUE Auth-Type CHAP 1025 -VALUE Auth-Type LDAP 1026 -VALUE Auth-Type PAM 1027 -VALUE Auth-Type MS-CHAP 1028 -VALUE Auth-Type Kerberos 1029 -VALUE Auth-Type CRAM 1030 -VALUE Auth-Type NS-MTA-MD5 1031 -VALUE Auth-Type CRAM 1032 -VALUE Auth-Type SMB 1033 - -# -# Authorization type, too. -# -VALUE Autz-Type Local 0 - -# -# Experimental Non-Protocol Integer Translations for Cistron-Radiusd -# -VALUE Fall-Through No 0 -VALUE Fall-Through Yes 1 - -VALUE Packet-Type Access-Request 1 -VALUE Packet-Type Access-Accept 2 -VALUE Packet-Type Access-Reject 3 -VALUE Packet-Type Accounting-Request 4 -VALUE Packet-Type Accounting-Response 5 -VALUE Packet-Type Accounting-Status 6 -VALUE Packet-Type Password-Request 7 -VALUE Packet-Type Password-Accept 8 -VALUE Packet-Type Password-Reject 9 -VALUE Packet-Type Accounting-Message 10 -VALUE Packet-Type Access-Challenge 11 -VALUE Packet-Type Status-Server 12 -VALUE Packet-Type Status-Client 13 diff --git a/example/dictionary.freeradius b/example/dictionary.freeradius deleted file mode 100644 index 544d0d7..0000000 --- a/example/dictionary.freeradius +++ /dev/null @@ -1,37 +0,0 @@ -# -*- text -*- -# Copyright (C) 2015 The FreeRADIUS Server project and contributors -# -# The FreeRADIUS Vendor-Specific dictionary. -# -# Version: $Id: ea468da88509aeff96b6f0d38ebc97411b9775b3 $ -# -# For a complete list of Private Enterprise Codes, see: -# -# http://www.isi.edu/in-notes/iana/assignments/enterprise-numbers -# - -VENDOR FreeRADIUS 11344 - -BEGIN-VENDOR FreeRADIUS - -# -# This attribute is really a bitmask. -# -ATTRIBUTE FreeRADIUS-Statistics-Type 127 integer - -VALUE FreeRADIUS-Statistics-Type None 0 -VALUE FreeRADIUS-Statistics-Type Authentication 1 -VALUE FreeRADIUS-Statistics-Type Accounting 2 -VALUE FreeRADIUS-Statistics-Type Proxy-Authentication 4 -VALUE FreeRADIUS-Statistics-Type Proxy-Accounting 8 -VALUE FreeRADIUS-Statistics-Type Internal 0x10 -VALUE FreeRADIUS-Statistics-Type Client 0x20 -VALUE FreeRADIUS-Statistics-Type Server 0x40 -VALUE FreeRADIUS-Statistics-Type Home-Server 0x80 - -VALUE FreeRADIUS-Statistics-Type Auth-Acct 0x03 -VALUE FreeRADIUS-Statistics-Type Proxy-Auth-Acct 0x0c - -VALUE FreeRADIUS-Statistics-Type All 0x1f - -END-VENDOR FreeRADIUS \ No newline at end of file diff --git a/example/server.py b/example/server.py deleted file mode 100755 index 3de61f9..0000000 --- a/example/server.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -import logging - -from os import path - -import pyrad.packet - -from pyrad import server -from pyrad.dictionary import Dictionary - -logging.basicConfig( - filename="pyrad.log", - level="DEBUG", - format="%(asctime)s [%(levelname)-8s] %(message)s", -) - - -def print_attributes(packet): - print("Attributes") - for key, value in packet.items(): - print(f"{key}: {value}") - - -class FakeServer(server.Server): - def HandleAuthPacket(self, packet): - print("Received an authentication request") - print_attributes(packet) - - reply = self.CreateReplyPacket( - packet, - **{ - "Service-Type": "Framed-User", - "Framed-IP-Address": "192.168.0.1", - "Framed-IPv6-Prefix": "fc66::/64", - }, - ) - - reply.code = pyrad.packet.AccessAccept - self.SendReplyPacket(packet.fd, reply) - - def HandleAcctPacket(self, packet): - print("Received an accounting request") - print_attributes(packet) - - reply = self.CreateReplyPacket(packet) - self.SendReplyPacket(packet.fd, reply) - - def HandleCoaPacket(self, packet): - print("Received an coa request") - print_attributes(packet) - - reply = self.CreateReplyPacket(packet) - self.SendReplyPacket(packet.fd, reply) - - def HandleDisconnectPacket(self, packet): - print("Received an disconnect request") - print_attributes(packet) - - reply = self.CreateReplyPacket(packet) - # COA NAK - reply.code = 45 - self.SendReplyPacket(packet.fd, reply) - - -def main(path_to_dictionary): - # create server and read dictionary - srv = FakeServer(dict=Dictionary(path_to_dictionary), coa_enabled=True) - - # add clients (address, secret, name) - srv.hosts["127.0.0.1"] = server.RemoteHost( - "127.0.0.1", b"Kah3choteereethiejeimaeziecumi", "localhost" - ) - srv.BindToAddress("0.0.0.0") - - # start server - srv.Run() - - -if __name__ == "__main__": - dictionary = path.join(path.dirname(path.abspath(__file__)), "dictionary") - main(dictionary) diff --git a/example/server_async.py b/example/server_async.py deleted file mode 100755 index 2f918a3..0000000 --- a/example/server_async.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env python3 -import asyncio -import logging -import traceback - -from os import path - -from pyrad.dictionary import Dictionary -from pyrad.packet import AccessAccept -from pyrad.server_async import ServerAsync -from pyrad.server import RemoteHost - -try: - import uvloop - - asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -except: - pass - -logging.basicConfig( - level="DEBUG", format="%(asctime)s [%(levelname)-8s] %(message)s" -) - - -def print_attributes(packet): - print("Attributes returned by server:") - for key, value in packet.items(): - print(f"{key}: {value}") - - -class FakeServer(ServerAsync): - def __init__(self, loop, dictionary): - - ServerAsync.__init__( - self, - loop=loop, - dictionary=dictionary, - enable_pkt_verify=True, - debug=True, - ) - - def handle_auth_packet(self, protocol, packet, addr): - print("Received an authentication request with id ", packet.id) - print("Authenticator ", packet.authenticator.hex()) - print("Secret ", packet.secret) - print_attributes(packet) - - reply = self.CreateReplyPacket( - packet, - **{ - "Service-Type": "Framed-User", - "Framed-IP-Address": "192.168.0.1", - "Framed-IPv6-Prefix": "fc66::/64", - }, - ) - - reply.code = AccessAccept - protocol.send_response(reply, addr) - - def handle_acct_packet(self, protocol, packet, addr): - print("Received an accounting request") - print_attributes(packet) - - reply = self.CreateReplyPacket(packet) - protocol.send_response(reply, addr) - - def handle_coa_packet(self, protocol, packet, addr): - print("Received an coa request") - print_attributes(packet) - - reply = self.CreateReplyPacket(packet) - protocol.send_response(reply, addr) - - def handle_disconnect_packet(self, protocol, packet, addr): - print("Received an disconnect request") - print_attributes(packet) - - reply = self.CreateReplyPacket(packet) - # COA NAK - reply.code = 45 - protocol.send_response(reply, addr) - - -def main(path_to_dictionary): - # create server and read dictionary - loop = asyncio.get_event_loop() - server = FakeServer(loop=loop, dictionary=Dictionary(path_to_dictionary)) - - # add clients (address, secret, name) - server.hosts["127.0.0.1"] = RemoteHost( - "127.0.0.1", b"Kah3choteereethiejeimaeziecumi", "localhost" - ) - - try: - # Initialize transports - loop.run_until_complete( - asyncio.ensure_future( - server.initialize_transports( - enable_auth=True, enable_acct=True, enable_coa=True - ) - ) - ) - - try: - # start server - loop.run_forever() - except KeyboardInterrupt: - pass - - # Close transports - loop.run_until_complete( - asyncio.ensure_future(server.deinitialize_transports()) - ) - - except Exception as exc: - print("Error: ", exc) - traceback.print_exc() - - # Close transports - loop.run_until_complete( - asyncio.ensure_future(server.deinitialize_transports()) - ) - - loop.close() - - -if __name__ == "__main__": - dictionary = path.join(path.dirname(path.abspath(__file__)), "dictionary") - main(dictionary) diff --git a/example/status.py b/example/status.py deleted file mode 100755 index 6322a02..0000000 --- a/example/status.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 -import socket -import sys - -from os import path - -import pyrad.packet - -from pyrad.client import Client -from pyrad.dictionary import Dictionary - - -def main(path_to_dictionary): - srv = Client( - server="localhost", - authport=18121, - secret=b"test", - dict=Dictionary(path_to_dictionary), - ) - - req = srv.CreateAuthPacket( - code=pyrad.packet.StatusServer, FreeRADIUS_Statistics_Type="All", - ) - req.add_message_authenticator() - - try: - print("Sending FreeRADIUS status request") - reply = srv.SendPacket(req) - except pyrad.client.Timeout: - print("RADIUS server does not reply") - sys.exit(1) - except socket.error as error: - print("Network error: " + error[1]) - sys.exit(1) - - print("Attributes returned by server:") - for key, value in reply.items(): - print(f"{key}: {value}") - - -if __name__ == "__main__": - dictionary = path.join(path.dirname(path.abspath(__file__)), "dictionary") - main(dictionary) diff --git a/shell.nix b/shell.nix index 3379b60..ad4c0f7 100644 --- a/shell.nix +++ b/shell.nix @@ -6,9 +6,11 @@ pkgs.mkShell { git nixfmt pkgs.python3 + pkgs.python3Packages.coveralls pkgs.python3Packages.black pkgs.python3Packages.pytest pkgs.python3Packages.pytest-black + pkgs.python3Packages.pytestcov pkgs.python3Packages.pytest-flake8 pkgs.python3Packages.pytest-mypy pkgs.python3Packages.pytest-pylint diff --git a/src/pyrad3/__init__.py b/src/pyrad3/__init__.py index f771b8b..3f8b558 100644 --- a/src/pyrad3/__init__.py +++ b/src/pyrad3/__init__.py @@ -36,11 +36,11 @@ This package contains four modules: - tools: utility functions """ -__docformat__ = 'epytext en' +__docformat__ = "epytext en" -__author__ = 'Istvan Ruzman ' -__url__ = 'http://pyrad.readthedocs.io/en/latest/?badge=latest' -__copyright__ = 'Copyright 2020 Istvan Ruzman' -__version__ = '0.1.0' +__author__ = "Istvan Ruzman " +__url__ = "http://pyrad.readthedocs.io/en/latest/?badge=latest" +__copyright__ = "Copyright 2020 Istvan Ruzman" +__version__ = "0.1.0" -__all__ = ['client', 'dictionary', 'packet', 'server', 'tools', 'utils'] +__all__ = ["client", "dictionary", "packet", "server", "tools", "utils"] diff --git a/src/pyrad3/bidict.py b/src/pyrad3/bidict.py deleted file mode 100644 index 15aaa21..0000000 --- a/src/pyrad3/bidict.py +++ /dev/null @@ -1,39 +0,0 @@ -# bidict.py -# -# Bidirectional map - - -class BiDict: - 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.get_forward(key) - - def __delitem__(self, key): - try: - del self.backward[self.forward[key]] - del self.forward[key] - except KeyError: - del self.forward[self.backward[key]] - del self.backward[key] - - def get_forward(self, key): - return self.forward[key] - - def has_forward(self, key): - return key in self.forward - - def get_backward(self, key): - return self.backward[key] - - def has_backward(self, key): - return key in self.backward diff --git a/src/pyrad3/client_async.py b/src/pyrad3/client_async.py deleted file mode 100644 index 783d691..0000000 --- a/src/pyrad3/client_async.py +++ /dev/null @@ -1,442 +0,0 @@ -# client_async.py -# -# Copyright 2018-2020 Geaaru gmail.com> - -__docformat__ = "epytext en" - -from datetime import datetime -import asyncio -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(f"Packet with id {packet.id} already present") - - # 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: - req = self.pending_requests[data[0]] - reply = req.VerifyPacket(data) - req["future"].set_result(reply) - # Remove request for map - del self.pending_requests[reply.id] - except KeyError: - self.logger.warn( - "[%s:%d] Ignore invalid reply: %s", self.server, self.port, data - ) - except PacketError as exc: - self.logger.error( - "[%s:%d] Error on decode or verify 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 ( - f"DatagramProtocolClient(server?={self.server}, port={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=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 diff --git a/src/pyrad3/dictionary.py b/src/pyrad3/dictionary.py index cd1b2bb..71ec478 100644 --- a/src/pyrad3/dictionary.py +++ b/src/pyrad3/dictionary.py @@ -10,7 +10,6 @@ from enum import IntEnum, Enum, auto from dataclasses import dataclass from os.path import dirname, isabs, join, normpath from typing import ( - cast, Dict, Generator, IO, @@ -167,18 +166,6 @@ def _parse_number(num: str) -> int: return int(num) -def _parse_attribute_code(attr_code: str) -> List[int]: - """Parse attribute codes from (Free)RADIUS dictionaries - - Codes can be either decimal, octal, or hexadecimal. - TLV typed can - """ - codes = [] - for code in attr_code.split("."): - codes.append(_parse_number(code)) - return codes - - class Dictionary: """(Free)RADIUS Dictionary. @@ -358,7 +345,7 @@ class Dictionary: for flag in flags: flag_len = len(flag) - if flag == 1: + if flag_len == 1: value = None elif flag_len == 2: value = flag[1] @@ -372,6 +359,8 @@ class Dictionary: has_tag = True elif key == "encrypt": try: + if value == "0": + raise ValueError encrypt = Encrypt(int(value)) # type: ignore except (ValueError, TypeError): raise ParseError( @@ -386,6 +375,32 @@ class Dictionary: return has_tag, encrypt + def _parse_attribute_code(self, attr_code: str, line_num: int) -> List[int]: + filename = self.filestack[-1] + tlength = self.cur_vendor.tlength + codes = [] + for code in attr_code.split("."): + try: + code_num = _parse_number(code) + except ValueError: + raise ParseError( + filename, f'invalid attribute code {attr_code}""', line_num + ) + if 2 ** (8 * tlength) <= code_num: + raise ParseError( + filename, + f"attribute code is too big, must be smaller than 2**{tlength}", + line_num, + ) + if code_num < 0: + raise ParseError( + filename, + "negative attribute codes are not allowed", + line_num, + ) + codes.append(code_num) + return codes + def _parse_attribute(self, tokens: Sequence[str], line_num: int): """Parse an ATTRIBUTE line of (Free)RADIUS dictionaries.""" filename = self.filestack[-1] @@ -409,27 +424,7 @@ class Dictionary: line_num, ) - try: - codes = _parse_attribute_code(attr_code) - except ValueError: - raise ParseError( - filename, f'invalid attribute code {attr_code}""', line_num - ) - - for code in codes: - tlength = self.cur_vendor.tlength - if 2 ** (8 * tlength) <= code: - raise ParseError( - filename, - f"attribute code is too big, must be smaller than 2**{tlength}", - line_num, - ) - if code < 0: - raise ParseError( - filename, - "negative attribute codes are not allowed", - line_num, - ) + codes = self._parse_attribute_code(attr_code, line_num) # TODO: Do we some explicit handling of tlvs? # if len(codes) > 1: @@ -485,7 +480,14 @@ class Dictionary: filename, f"Invalid number {vvalue} for VALUE {key}", line_num ) - attribute = self.attrindex[attr_name] + try: + attribute = self.attrindex[attr_name] + except KeyError: + raise ParseError( + filename, + f"ATTRIBUTE {attr_name} has not been defined yet", + line_num, + ) try: datatype = str(attribute.datatype).split(".")[1] lmin, lmax = INTEGER_TYPES[datatype] @@ -504,3 +506,6 @@ class Dictionary: ) attribute.values[value] = key attribute.values[key] = value + + def __getitem__(self, key: Union[str, int, Tuple[int, ...]]) -> Attribute: + return self.attrindex[key] diff --git a/src/pyrad3/host.py b/src/pyrad3/host.py index e7d7c6e..c9f2081 100644 --- a/src/pyrad3/host.py +++ b/src/pyrad3/host.py @@ -7,8 +7,9 @@ from pyrad3.dictionary import Dictionary from pyrad3 import packet -class Host: # pylint: disable=too-many-arguments +class Host: # pylint: disable=too-many-arguments """Interface Class for RADIUS Clients and Servers""" + def __init__( self, secret: bytes, diff --git a/src/pyrad3/packet.py b/src/pyrad3/packet.py index 24329d1..98d57a6 100644 --- a/src/pyrad3/packet.py +++ b/src/pyrad3/packet.py @@ -169,7 +169,8 @@ class Packet(OrderedDict): self["Message-Authenticator"] = 16 * b"\00" attr = self.ordered_attributes[-1] generated = self._generate_message_authenticator(attr) - self[attr.pos + 2 :] = generated + index = attr.pos + 2 + self[index:] = generated def refresh_message_authenticator(self): self.add_message_authenticator() @@ -283,7 +284,7 @@ class AcctPacket(Packet): def increase_acct_delay_time(self, delay_time: float): try: - self['Acct-Delay-Time'] += int(delay_time) + self["Acct-Delay-Time"] += int(delay_time) except KeyError: pass diff --git a/src/pyrad3/proxy.py b/src/pyrad3/proxy.py deleted file mode 100644 index 20afb26..0000000 --- a/src/pyrad3/proxy.py +++ /dev/null @@ -1,73 +0,0 @@ -# proxy.py -# -# Copyright 2005,2007 Wichert Akkerman -# -# A RADIUS proxy as defined in RFC 2138 - -import select -import socket - -from pyrad.server import Server, ServerPacketError -from pyrad import packet - - -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 _prepare_sockets(self): - Server._prepare_sockets(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 _handle_proxy_packet(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 _process_input(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:`_handle_proxy_packet` 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._grab_packet( - lambda data, s=self: s.CreatePacket(packet=data), fd - ) - self._handle_proxy_packet(pkt) - else: - Server._process_input(self, fd) diff --git a/src/pyrad3/server.py b/src/pyrad3/server.py deleted file mode 100644 index f20b06f..0000000 --- a/src/pyrad3/server.py +++ /dev/null @@ -1,367 +0,0 @@ -# server.py -# -# Copyright 2003-2004,2007,2016 Wichert Akkerman - -import logging -import select -import socket -from pyrad import host -from pyrad import packet - - -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 = 4096 - - 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 _get_addr_info(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._get_addr_info(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 _add_secret(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 _handle_auth_packet(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._add_secret(pkt) - if pkt.code != packet.AccessRequest: - raise ServerPacketError( - "Received non-authentication packet on authentication port" - ) - self.HandleAuthPacket(pkt) - - def _handle_acct_packet(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._add_secret(pkt) - if pkt.code not in [ - packet.AccountingRequest, - packet.AccountingResponse, - ]: - raise ServerPacketError( - "Received non-accounting packet on accounting port" - ) - self.HandleAcctPacket(pkt) - - def _handle_coa_packet(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._add_secret(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 _grab_packet(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 _prepare_sockets(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 _process_input(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._grab_packet( - lambda data, s=self: s.CreateAuthPacket(packet=data), fd - ) - self._handle_auth_packet(pkt) - elif self.acct_enabled and fd.fileno() in self._realacctfds: - pkt = self._grab_packet( - lambda data, s=self: s.CreateAcctPacket(packet=data), fd - ) - self._handle_acct_packet(pkt) - elif self.coa_enabled: - pkt = self._grab_packet( - lambda data, s=self: s.CreateCoAPacket(packet=data), fd - ) - self._handle_coa_packet(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._prepare_sockets() - - while True: - for (fd, event) in self._poll.poll(): - if event == select.POLLIN: - try: - fdo = self._fdmap[fd] - self._process_input(fdo) - except ServerPacketError as err: - LOGGER.info("Dropping packet: %s", err) - except packet.PacketError as err: - LOGGER.info("Received a broken packet: %s", err) - else: - LOGGER.error("Unexpected event in server main loop") diff --git a/src/pyrad3/server_async.py b/src/pyrad3/server_async.py deleted file mode 100644 index 2476bde..0000000 --- a/src/pyrad3/server_async.py +++ /dev/null @@ -1,429 +0,0 @@ -# server_async.py -# -# Copyright 2018-2019 Geaaru - -import asyncio -import logging - -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(f"Invalid response packet {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 f"DatagramProtocolServer(ip={self.ip}, port={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 diff --git a/src/pyrad3/tools.py b/src/pyrad3/tools.py index 71df498..55163eb 100644 --- a/src/pyrad3/tools.py +++ b/src/pyrad3/tools.py @@ -4,7 +4,14 @@ """Collections of functions to en- and decode RADIUS Attributes""" from typing import Union -from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_network, ip_address +from ipaddress import ( + IPv4Address, + IPv4Network, + IPv6Address, + IPv6Network, + ip_network, + ip_address, +) import struct @@ -33,13 +40,17 @@ def encode_address(addr: Union[str, IPv4Address]) -> bytes: def encode_network(network: Union[str, IPv4Network]) -> bytes: """Encode a RADIUS value of type ipv4prefix""" address = IPv4Network(network) - return struct.pack("2B", 0, address.prefixlen) + address.network_address.packed + return ( + struct.pack("2B", 0, address.prefixlen) + address.network_address.packed + ) def encode_ipv6_prefix(network: Union[str, IPv6Network]) -> bytes: """Encode a RADIUS value of type ipv6prefix""" address = IPv6Network(network) - return struct.pack("2B", 0, address.prefixlen) + address.network_address.packed.rstrip(b'\0') + return struct.pack( + "2B", 0, address.prefixlen + ) + address.network_address.packed.rstrip(b"\0") def encode_ipv6_address(addr: Union[str, IPv6Address]) -> bytes: @@ -212,7 +223,7 @@ ENCODE_MAP = { "signed": lambda value: encode_integer(value, "!i"), "short": lambda value: encode_integer(value, "!H"), "byte": lambda value: encode_integer(value, "!B"), - "integer64": lambda value: encode_integer(value, '!Q'), + "integer64": lambda value: encode_integer(value, "!Q"), "date": encode_date, } diff --git a/src/pyrad3/utils.py b/src/pyrad3/utils.py index 876c14f..b7a12cc 100644 --- a/src/pyrad3/utils.py +++ b/src/pyrad3/utils.py @@ -127,11 +127,16 @@ def parse_key(rad_dict: Dictionary, key_id: int) -> Union[str, int]: return key_id -def parse_value( - rad_dict: Dictionary, key: Union[str, int], offset: int, raw_value: bytes -) -> List[Attribute]: +def parse_value(*_): """Parse the Value in the given Key/Dictionary Context""" - return [] + raise NotImplementedError + + +# def parse_value( +# rad_dict: Dictionary, key: Union[str, int], offset: int, raw_value: bytes +# ) -> List[Attribute]: +# """Parse the Value in the given Key/Dictionary Context""" +# return [] def calculate_authenticator( diff --git a/tests/test_dictionary.py b/tests/test_dictionary.py index ebcf2dd..6271329 100644 --- a/tests/test_dictionary.py +++ b/tests/test_dictionary.py @@ -36,6 +36,12 @@ def test_lines_missing_tokens(line): Dictionary("", dictionary) +def test_invalid_token(): + dictionary = StringIO("invalid_token") + with pytest.raises(ParseError): + Dictionary("", dictionary) + + @pytest.mark.parametrize( "vendor", [ @@ -53,6 +59,33 @@ def test_valid_vendor_definitions(vendor): Dictionary("", dictionary) +def test_closing_wrong_vendor(): + dictionary = StringIO( + "VENDOR TEST-VENDOR 1234\n" + "BEGIN-VENDOR TEST-VENDOR\n" + "END-VENDOR WRONG-VENDOR" + ) + with pytest.raises(ParseError): + Dictionary("", dictionary) + + +def test_nested_vendor(): + dictionary = StringIO( + "VENDOR TEST-VENDOR1 1234\n" + "VENDOR TEST-VENDOR2 1235\n" + "BEGIN-VENDOR TEST-VENDOR1\n" + "BEGIN-VENDOR TEST-VENDOR2" + ) + with pytest.raises(ParseError): + Dictionary("", dictionary) + + +def test_begin_vendor_without_definition(): + dictionary = StringIO("BEGIN-VENDOR TEST-VENDOR") + with pytest.raises(ParseError): + Dictionary("", dictionary) + + @pytest.mark.parametrize( "vendor", [ @@ -86,7 +119,8 @@ def test_valid_attribute_numbers(number): @pytest.mark.parametrize( - "invalid_number", ["ABCD", "-1", "inf", "INF", "-INF", "2e4", "2.5e3"], + "invalid_number", + ["1000", "ABCD", "-1", "inf", "INF", "-INF", "2e4", "2.5e3"], ) def test_invalid_attribute_numbers(invalid_number): dictionary = StringIO(f"ATTRIBUTE NAME {invalid_number} integer64") @@ -115,6 +149,12 @@ def test_attribute_number_limits(type_length): Dictionary("", dictionary) +def test_invalid_attr_type(): + dictionary = StringIO("ATTRIBUTE NAME 2 invalid") + with pytest.raises(ParseError): + Dictionary("", dictionary) + + @pytest.mark.parametrize("value", ["1", "0x1", "0o1"]) def test_value_definition(value): dictionary = StringIO( @@ -249,7 +289,7 @@ def test_valid_datatypes_in_vendor_space(datatype): @pytest.mark.parametrize( - "datatype", ["concat", "extended", "long-extended", "evs",] + "datatype", ["concat", "extended", "long-extended", "evs"] ) def test_invalid_datatypes_in_vendor_space(datatype): dictionary = StringIO( @@ -266,10 +306,74 @@ def test_invalid_datatypes_in_vendor_space(datatype): "invalid_number", ["ABCD", "-1", "inf", "INF", "-INF", "0.1", "2e4", "2.5e3"], ) -def test_invalid_attribute_numbers(invalid_number): +def test_invalid_value_numbers(invalid_number): dictionary = StringIO( f"ATTRIBUTE TEST-ATTRIBUTE 1 integer\n" f"VALUE TEST-ATTRIBUTE TEST-VALUE {invalid_number}" ) with pytest.raises(ParseError): Dictionary("", dictionary) + + +def test_value_for_non_existing_attribute(): + dictionary = StringIO("VALUE ATTRNAME VALUENAME 1234") + with pytest.raises(ParseError): + Dictionary("", dictionary) + + +@pytest.mark.parametrize( + "datatype", + [ + "string", + "octets", + "abinary", + "ipaddr", + "ipv4prefix", + "ipv6addr", + "ipv6prefix", + "combo-ip", + "ifid", + "ether", + "tlv", + ], +) +def test_value_for_wrong_datatype(datatype): + dictionary = StringIO( + f"ATTRIBUTE NAME 123 {datatype}\n" "VALUE NAME VNAME 256" + ) + with pytest.raises(ParseError): + Dictionary("", dictionary) + + +def test_unimplemented_tlvs(): + dictionary = StringIO("BEGIN-TLV") + with pytest.raises(NotImplementedError): + Dictionary("", dictionary) + dictionary = StringIO("END-TLV") + with pytest.raises(NotImplementedError): + Dictionary("", dictionary) + + +@pytest.mark.parametrize("flag", [1, 2, 3]) +def test_valid_attribute_encrpytion_flags(flag): + dictionary = StringIO(f"ATTRIBUTE NAME 123 octets encrypt={flag}") + Dictionary("", dictionary) + + +@pytest.mark.parametrize("flag", ["0.1", "0", "4", "0x1", "0o2", "user", ""]) +def test_invalid_attribute_encrpytion_flags(flag): + dictionary = StringIO(f"ATTRIBUTE NAME 123 octets encrypt={flag}") + with pytest.raises(ParseError): + Dictionary("", dictionary) + + +def test_has_tag_flag(): + dictionary = StringIO("ATTRIBUTE NAME 123 octets has_tag") + Dictionary("", dictionary) + + +@pytest.mark.parametrize("invalid_flag", ["blablub", "encrypt=2=2", "concat"]) +def test_invalid_attribute_flags(invalid_flag): + dictionary = StringIO(f"ATTRIBUTE NAME 123 octets {invalid_flag}") + with pytest.raises(ParseError): + Dictionary("", dictionary)