cleanup and a lot more tests
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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 |
@@ -1,10 +0,0 @@
|
||||
:mod:`pyrad.client` -- basic client
|
||||
===================================
|
||||
|
||||
.. automodule:: pyrad.client
|
||||
|
||||
.. autoclass:: Timeout
|
||||
:members:
|
||||
|
||||
.. autoclass:: Client
|
||||
:members:
|
||||
@@ -1,10 +0,0 @@
|
||||
:mod:`pyrad.dictionary` -- RADIUS dictionary
|
||||
============================================
|
||||
|
||||
.. automodule:: pyrad.dictionary
|
||||
|
||||
.. autoclass:: ParseError
|
||||
:members:
|
||||
|
||||
.. autoclass:: Dictionary
|
||||
:members:
|
||||
@@ -1,7 +0,0 @@
|
||||
:mod:`pyrad.host` -- RADIUS host definition
|
||||
===========================================
|
||||
|
||||
.. automodule:: pyrad.host
|
||||
|
||||
.. autoclass:: Host
|
||||
:members:
|
||||
@@ -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
|
||||
================== ======
|
||||
@@ -1,7 +0,0 @@
|
||||
:mod:`pyrad.proxy` -- basic proxy
|
||||
=================================
|
||||
|
||||
.. automodule:: pyrad.proxy
|
||||
|
||||
.. autoclass:: Proxy
|
||||
:members:
|
||||
@@ -1,13 +0,0 @@
|
||||
:mod:`pyrad.server` -- basic server
|
||||
===================================
|
||||
|
||||
.. automodule:: pyrad.server
|
||||
|
||||
.. autoclass:: RemoteHost
|
||||
:members:
|
||||
|
||||
.. autoclass:: ServerPacketError
|
||||
:members:
|
||||
|
||||
.. autoclass:: Server
|
||||
:members:
|
||||
@@ -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}
|
||||
@@ -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`
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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]))
|
||||
@@ -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])
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -36,11 +36,11 @@ This package contains four modules:
|
||||
- tools: utility functions
|
||||
"""
|
||||
|
||||
__docformat__ = 'epytext en'
|
||||
__docformat__ = "epytext en"
|
||||
|
||||
__author__ = 'Istvan Ruzman <istvan@ruzman.eu>'
|
||||
__url__ = 'http://pyrad.readthedocs.io/en/latest/?badge=latest'
|
||||
__copyright__ = 'Copyright 2020 Istvan Ruzman'
|
||||
__version__ = '0.1.0'
|
||||
__author__ = "Istvan Ruzman <istvan@ruzman.eu>"
|
||||
__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"]
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user