cleanup and a lot more tests

This commit is contained in:
Istvan Ruzman
2020-08-07 20:12:10 +02:00
parent e8dce3e2cf
commit 360927e559
35 changed files with 187 additions and 2849 deletions

View File

@@ -1,11 +1,10 @@
language: python language: python
python: python:
- "3.6"
- "3.7" - "3.7"
- "3.8" - "3.8"
# command to install dependencies # 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 # 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: after_success:
- coveralls - coveralls

View File

@@ -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)

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,10 +0,0 @@
:mod:`pyrad.client` -- basic client
===================================
.. automodule:: pyrad.client
.. autoclass:: Timeout
:members:
.. autoclass:: Client
:members:

View File

@@ -1,10 +0,0 @@
:mod:`pyrad.dictionary` -- RADIUS dictionary
============================================
.. automodule:: pyrad.dictionary
.. autoclass:: ParseError
:members:
.. autoclass:: Dictionary
:members:

View File

@@ -1,7 +0,0 @@
:mod:`pyrad.host` -- RADIUS host definition
===========================================
.. automodule:: pyrad.host
.. autoclass:: Host
:members:

View File

@@ -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
================== ======

View File

@@ -1,7 +0,0 @@
:mod:`pyrad.proxy` -- basic proxy
=================================
.. automodule:: pyrad.proxy
.. autoclass:: Proxy
:members:

View File

@@ -1,13 +0,0 @@
:mod:`pyrad.server` -- basic server
===================================
.. automodule:: pyrad.server
.. autoclass:: RemoteHost
:members:
.. autoclass:: ServerPacketError
:members:
.. autoclass:: Server
:members:

View File

@@ -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 <developer@gicnet.de>'
# 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}

View File

@@ -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`

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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]))

View File

@@ -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])

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -6,9 +6,11 @@ pkgs.mkShell {
git git
nixfmt nixfmt
pkgs.python3 pkgs.python3
pkgs.python3Packages.coveralls
pkgs.python3Packages.black pkgs.python3Packages.black
pkgs.python3Packages.pytest pkgs.python3Packages.pytest
pkgs.python3Packages.pytest-black pkgs.python3Packages.pytest-black
pkgs.python3Packages.pytestcov
pkgs.python3Packages.pytest-flake8 pkgs.python3Packages.pytest-flake8
pkgs.python3Packages.pytest-mypy pkgs.python3Packages.pytest-mypy
pkgs.python3Packages.pytest-pylint pkgs.python3Packages.pytest-pylint

View File

@@ -36,11 +36,11 @@ This package contains four modules:
- tools: utility functions - tools: utility functions
""" """
__docformat__ = 'epytext en' __docformat__ = "epytext en"
__author__ = 'Istvan Ruzman <istvan@ruzman.eu>' __author__ = "Istvan Ruzman <istvan@ruzman.eu>"
__url__ = 'http://pyrad.readthedocs.io/en/latest/?badge=latest' __url__ = "http://pyrad.readthedocs.io/en/latest/?badge=latest"
__copyright__ = 'Copyright 2020 Istvan Ruzman' __copyright__ = "Copyright 2020 Istvan Ruzman"
__version__ = '0.1.0' __version__ = "0.1.0"
__all__ = ['client', 'dictionary', 'packet', 'server', 'tools', 'utils'] __all__ = ["client", "dictionary", "packet", "server", "tools", "utils"]

View File

@@ -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

View File

@@ -1,442 +0,0 @@
# client_async.py
#
# Copyright 2018-2020 Geaaru <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

View File

@@ -10,7 +10,6 @@ from enum import IntEnum, Enum, auto
from dataclasses import dataclass from dataclasses import dataclass
from os.path import dirname, isabs, join, normpath from os.path import dirname, isabs, join, normpath
from typing import ( from typing import (
cast,
Dict, Dict,
Generator, Generator,
IO, IO,
@@ -167,18 +166,6 @@ def _parse_number(num: str) -> int:
return int(num) 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: class Dictionary:
"""(Free)RADIUS Dictionary. """(Free)RADIUS Dictionary.
@@ -358,7 +345,7 @@ class Dictionary:
for flag in flags: for flag in flags:
flag_len = len(flag) flag_len = len(flag)
if flag == 1: if flag_len == 1:
value = None value = None
elif flag_len == 2: elif flag_len == 2:
value = flag[1] value = flag[1]
@@ -372,6 +359,8 @@ class Dictionary:
has_tag = True has_tag = True
elif key == "encrypt": elif key == "encrypt":
try: try:
if value == "0":
raise ValueError
encrypt = Encrypt(int(value)) # type: ignore encrypt = Encrypt(int(value)) # type: ignore
except (ValueError, TypeError): except (ValueError, TypeError):
raise ParseError( raise ParseError(
@@ -386,6 +375,32 @@ class Dictionary:
return has_tag, encrypt 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): def _parse_attribute(self, tokens: Sequence[str], line_num: int):
"""Parse an ATTRIBUTE line of (Free)RADIUS dictionaries.""" """Parse an ATTRIBUTE line of (Free)RADIUS dictionaries."""
filename = self.filestack[-1] filename = self.filestack[-1]
@@ -409,27 +424,7 @@ class Dictionary:
line_num, line_num,
) )
try: codes = self._parse_attribute_code(attr_code, line_num)
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,
)
# TODO: Do we some explicit handling of tlvs? # TODO: Do we some explicit handling of tlvs?
# if len(codes) > 1: # if len(codes) > 1:
@@ -485,7 +480,14 @@ class Dictionary:
filename, f"Invalid number {vvalue} for VALUE {key}", line_num 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: try:
datatype = str(attribute.datatype).split(".")[1] datatype = str(attribute.datatype).split(".")[1]
lmin, lmax = INTEGER_TYPES[datatype] lmin, lmax = INTEGER_TYPES[datatype]
@@ -504,3 +506,6 @@ class Dictionary:
) )
attribute.values[value] = key attribute.values[value] = key
attribute.values[key] = value attribute.values[key] = value
def __getitem__(self, key: Union[str, int, Tuple[int, ...]]) -> Attribute:
return self.attrindex[key]

View File

@@ -7,8 +7,9 @@ from pyrad3.dictionary import Dictionary
from pyrad3 import packet 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""" """Interface Class for RADIUS Clients and Servers"""
def __init__( def __init__(
self, self,
secret: bytes, secret: bytes,

View File

@@ -169,7 +169,8 @@ class Packet(OrderedDict):
self["Message-Authenticator"] = 16 * b"\00" self["Message-Authenticator"] = 16 * b"\00"
attr = self.ordered_attributes[-1] attr = self.ordered_attributes[-1]
generated = self._generate_message_authenticator(attr) generated = self._generate_message_authenticator(attr)
self[attr.pos + 2 :] = generated index = attr.pos + 2
self[index:] = generated
def refresh_message_authenticator(self): def refresh_message_authenticator(self):
self.add_message_authenticator() self.add_message_authenticator()
@@ -283,7 +284,7 @@ class AcctPacket(Packet):
def increase_acct_delay_time(self, delay_time: float): def increase_acct_delay_time(self, delay_time: float):
try: try:
self['Acct-Delay-Time'] += int(delay_time) self["Acct-Delay-Time"] += int(delay_time)
except KeyError: except KeyError:
pass pass

View File

@@ -1,73 +0,0 @@
# proxy.py
#
# Copyright 2005,2007 Wichert Akkerman <wichert@wiggy.net>
#
# 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)

View File

@@ -1,367 +0,0 @@
# server.py
#
# Copyright 2003-2004,2007,2016 Wichert Akkerman <wichert@wiggy.net>
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")

View File

@@ -1,429 +0,0 @@
# server_async.py
#
# Copyright 2018-2019 Geaaru <geaaru@gmail.com>
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

View File

@@ -4,7 +4,14 @@
"""Collections of functions to en- and decode RADIUS Attributes""" """Collections of functions to en- and decode RADIUS Attributes"""
from typing import Union 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 import struct
@@ -33,13 +40,17 @@ def encode_address(addr: Union[str, IPv4Address]) -> bytes:
def encode_network(network: Union[str, IPv4Network]) -> bytes: def encode_network(network: Union[str, IPv4Network]) -> bytes:
"""Encode a RADIUS value of type ipv4prefix""" """Encode a RADIUS value of type ipv4prefix"""
address = IPv4Network(network) 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: def encode_ipv6_prefix(network: Union[str, IPv6Network]) -> bytes:
"""Encode a RADIUS value of type ipv6prefix""" """Encode a RADIUS value of type ipv6prefix"""
address = IPv6Network(network) 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: def encode_ipv6_address(addr: Union[str, IPv6Address]) -> bytes:
@@ -212,7 +223,7 @@ ENCODE_MAP = {
"signed": lambda value: encode_integer(value, "!i"), "signed": lambda value: encode_integer(value, "!i"),
"short": lambda value: encode_integer(value, "!H"), "short": lambda value: encode_integer(value, "!H"),
"byte": lambda value: encode_integer(value, "!B"), "byte": lambda value: encode_integer(value, "!B"),
"integer64": lambda value: encode_integer(value, '!Q'), "integer64": lambda value: encode_integer(value, "!Q"),
"date": encode_date, "date": encode_date,
} }

View File

@@ -127,11 +127,16 @@ def parse_key(rad_dict: Dictionary, key_id: int) -> Union[str, int]:
return key_id return key_id
def parse_value( 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""" """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( def calculate_authenticator(

View File

@@ -36,6 +36,12 @@ def test_lines_missing_tokens(line):
Dictionary("", dictionary) Dictionary("", dictionary)
def test_invalid_token():
dictionary = StringIO("invalid_token")
with pytest.raises(ParseError):
Dictionary("", dictionary)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"vendor", "vendor",
[ [
@@ -53,6 +59,33 @@ def test_valid_vendor_definitions(vendor):
Dictionary("", dictionary) 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( @pytest.mark.parametrize(
"vendor", "vendor",
[ [
@@ -86,7 +119,8 @@ def test_valid_attribute_numbers(number):
@pytest.mark.parametrize( @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): def test_invalid_attribute_numbers(invalid_number):
dictionary = StringIO(f"ATTRIBUTE NAME {invalid_number} integer64") dictionary = StringIO(f"ATTRIBUTE NAME {invalid_number} integer64")
@@ -115,6 +149,12 @@ def test_attribute_number_limits(type_length):
Dictionary("", dictionary) 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"]) @pytest.mark.parametrize("value", ["1", "0x1", "0o1"])
def test_value_definition(value): def test_value_definition(value):
dictionary = StringIO( dictionary = StringIO(
@@ -249,7 +289,7 @@ def test_valid_datatypes_in_vendor_space(datatype):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"datatype", ["concat", "extended", "long-extended", "evs",] "datatype", ["concat", "extended", "long-extended", "evs"]
) )
def test_invalid_datatypes_in_vendor_space(datatype): def test_invalid_datatypes_in_vendor_space(datatype):
dictionary = StringIO( dictionary = StringIO(
@@ -266,10 +306,74 @@ def test_invalid_datatypes_in_vendor_space(datatype):
"invalid_number", "invalid_number",
["ABCD", "-1", "inf", "INF", "-INF", "0.1", "2e4", "2.5e3"], ["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( dictionary = StringIO(
f"ATTRIBUTE TEST-ATTRIBUTE 1 integer\n" f"ATTRIBUTE TEST-ATTRIBUTE 1 integer\n"
f"VALUE TEST-ATTRIBUTE TEST-VALUE {invalid_number}" f"VALUE TEST-ATTRIBUTE TEST-VALUE {invalid_number}"
) )
with pytest.raises(ParseError): with pytest.raises(ParseError):
Dictionary("", dictionary) 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)