pyrad2 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ Copyright 2020 Istvan Ruzman. All rights reserved.
2
+ Copyright 2017-2023 Christian Giese. All rights reserved.
3
+ Copyright 2007-2008 Simplon. All rights reserved.
4
+ Copyright 2002-2008 Wichert Akkerman. All rights reserved.
5
+
6
+ All rights reserved.
7
+
8
+ Redistribution and use in source and binary forms, with or without
9
+ modification, are permitted provided that the following conditions
10
+ are met:
11
+ 1. Redistributions of source code must retain the above copyright
12
+ notice, this list of conditions and the following disclaimer.
13
+ 2. Redistributions in binary form must reproduce the above copyright
14
+ notice, this list of conditions and the following disclaimer in the
15
+ documentation and/or other materials provided with the distribution.
16
+ 3. Neither the name of the University nor the names of its contributors
17
+ may be used to endorse or promote products derived from this software
18
+ without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
21
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
26
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
27
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
28
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
29
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
30
+ SUCH DAMAGE.
31
+
pyrad2-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyrad2
3
+ Version: 1.0.0
4
+ Summary: RADIUS Server
5
+ Home-page: https://github.com/nicholasamorim/pyrad2
6
+ Author: Nicholas Amorim, Istvan Ruzman, Christian Giese
7
+ Author-email: nicholas@santos.ee, istvan@ruzman.eu, developer@gicnet.de
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE.txt
11
+ Requires-Dist: loguru>=0.7.3
12
+ Dynamic: author
13
+ Dynamic: author-email
14
+ Dynamic: home-page
15
+ Dynamic: license-file
16
+
17
+ [![Tests](https://github.com/nicholasamorim/pyrad2/actions/workflows/python-test.yml/badge.svg)](https://github.com/miraclesupernova/stickystack/actions/workflows/django.yml)
18
+ [![python](https://img.shields.io/badge/Python-3.12+-3776AB.svg?style=flat&logo=python&logoColor=white)](https://www.python.org)
19
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
20
+ [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)]([https://github.com/psf/black](https://github.com/astral-sh/uv))
21
+ [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
22
+
23
+ pyrad2 is an implementation of a RADIUS client/server as described in RFC2865. It takes care of all the details like building RADIUS packets,
24
+ sending them and decoding responses.
25
+
26
+ # Introduction
27
+
28
+ This is a fork of [pyrad](https://github.com/pyradius/pyrad) aiming to make it compatible with Python 3.12+ and introduce bug fixes and features. Most of the codebase now has type checking and test coverage has been increased. Legacy compatibility code with (very) older versions of Python have been removed and we only support Python 3.12+.
29
+
30
+ Note that this is _not_ a stand-alone Radius implementation like [FreeRadius](https://www.freeradius.org). You are supposed to inherit the server classes and code your own behind-the-scenes implementation. This package allows you to code your business logic on top of it.
31
+
32
+ # Requirements & Installation
33
+
34
+ pyrad2 requires Python 3.12 and uses [uv](https://github.com/astral-sh/uv). On a Mac, you can simply run `brew install uv`.
35
+
36
+ # Examples
37
+
38
+ There are a few examples in the `examples` folder.
39
+
40
+ The easiest way to start a server is by running `make test_server_async`. This will run the example server in `examples/server_async.py`.
41
+
42
+ If you want to see a request in action, leave the server running, open another terminal and type `make test_auth`.
43
+
44
+ # Tests
45
+
46
+ Run `make test`.
47
+
48
+ # Author, Copyright, Availability
49
+
50
+ pyrad2 is currently maintaned by Nicholas Amorim \<<nicholas@santos.ee\>.
51
+
52
+ pyrad was written by Wichert Akkerman \<<wichert@wiggy.net>\> and is
53
+ maintained by Christian Giese (GIC-de) and Istvan Ruzman (Istvan91).
54
+
55
+ This project is licensed under a BSD license.
56
+
57
+ Copyright and license information can be found in the LICENSE.txt file.
58
+
59
+ The current version and documentation can be found on pypi:
60
+ <https://pypi.org/project/pyrad2/>
61
+
62
+ Bugs and wishes can be submitted in the pyrad issue tracker on github:
63
+ <https://github.com/nicholasamorim/pyrad2/issues>
pyrad2-1.0.0/README.md ADDED
@@ -0,0 +1,47 @@
1
+ [![Tests](https://github.com/nicholasamorim/pyrad2/actions/workflows/python-test.yml/badge.svg)](https://github.com/miraclesupernova/stickystack/actions/workflows/django.yml)
2
+ [![python](https://img.shields.io/badge/Python-3.12+-3776AB.svg?style=flat&logo=python&logoColor=white)](https://www.python.org)
3
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
4
+ [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)]([https://github.com/psf/black](https://github.com/astral-sh/uv))
5
+ [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
6
+
7
+ pyrad2 is an implementation of a RADIUS client/server as described in RFC2865. It takes care of all the details like building RADIUS packets,
8
+ sending them and decoding responses.
9
+
10
+ # Introduction
11
+
12
+ This is a fork of [pyrad](https://github.com/pyradius/pyrad) aiming to make it compatible with Python 3.12+ and introduce bug fixes and features. Most of the codebase now has type checking and test coverage has been increased. Legacy compatibility code with (very) older versions of Python have been removed and we only support Python 3.12+.
13
+
14
+ Note that this is _not_ a stand-alone Radius implementation like [FreeRadius](https://www.freeradius.org). You are supposed to inherit the server classes and code your own behind-the-scenes implementation. This package allows you to code your business logic on top of it.
15
+
16
+ # Requirements & Installation
17
+
18
+ pyrad2 requires Python 3.12 and uses [uv](https://github.com/astral-sh/uv). On a Mac, you can simply run `brew install uv`.
19
+
20
+ # Examples
21
+
22
+ There are a few examples in the `examples` folder.
23
+
24
+ The easiest way to start a server is by running `make test_server_async`. This will run the example server in `examples/server_async.py`.
25
+
26
+ If you want to see a request in action, leave the server running, open another terminal and type `make test_auth`.
27
+
28
+ # Tests
29
+
30
+ Run `make test`.
31
+
32
+ # Author, Copyright, Availability
33
+
34
+ pyrad2 is currently maintaned by Nicholas Amorim \<<nicholas@santos.ee\>.
35
+
36
+ pyrad was written by Wichert Akkerman \<<wichert@wiggy.net>\> and is
37
+ maintained by Christian Giese (GIC-de) and Istvan Ruzman (Istvan91).
38
+
39
+ This project is licensed under a BSD license.
40
+
41
+ Copyright and license information can be found in the LICENSE.txt file.
42
+
43
+ The current version and documentation can be found on pypi:
44
+ <https://pypi.org/project/pyrad2/>
45
+
46
+ Bugs and wishes can be submitted in the pyrad issue tracker on github:
47
+ <https://github.com/nicholasamorim/pyrad2/issues>
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "pyrad2"
3
+ version = "1.0.0"
4
+ description = "RADIUS Server"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "loguru>=0.7.3",
9
+ ]
10
+
11
+ [dependency-groups]
12
+ dev = [
13
+ "mypy>=1.16.1",
14
+ "pytest>=8.4.1",
15
+ "pytest-cov>=6.2.1",
16
+ "pytest-sugar>=1.0.0",
17
+ "pyupgrade>=3.20.0",
18
+ "ruff>=0.12.2",
19
+ "types-setuptools>=80.9.0.20250529",
20
+ ]
21
+
22
+ # [build-system]
23
+ # requires = ["setuptools>=61.0", "wheel"]
24
+ # build-backend = "setuptools.build_meta"
@@ -0,0 +1,45 @@
1
+ """Python RADIUS client code.
2
+
3
+ pyrad is an implementation of a RADIUS client as described in RFC2865.
4
+ It takes care of all the details like building RADIUS packets, sending
5
+ them and decoding responses.
6
+
7
+ Here is an example of doing a authentication request::
8
+
9
+ import pyrad.packet
10
+ from pyrad.client import Client
11
+ from pyrad.dictionary import Dictionary
12
+
13
+ srv = Client(server="radius.my.domain", secret="s3cr3t",
14
+ dict = Dictionary("dicts/dictionary", "dictionary.acc"))
15
+
16
+ req = srv.CreatePacket(code=pyrad.packet.AccessRequest,
17
+ User_Name = "wichert", NAS_Identifier="localhost")
18
+ req["User-Password"] = req.PwCrypt("password")
19
+
20
+ reply = srv.SendPacket(req)
21
+ if reply.code = =pyrad.packet.AccessAccept:
22
+ print "access accepted"
23
+ else:
24
+ print "access denied"
25
+
26
+ print "Attributes returned by server:"
27
+ for i in reply.keys():
28
+ print "%s: %s" % (i, reply[i])
29
+
30
+
31
+ This package contains four modules:
32
+
33
+ - client: RADIUS client code
34
+ - dictionary: RADIUS attribute dictionary
35
+ - packet: a RADIUS packet as send to/from servers
36
+ - tools: utility functions
37
+ """
38
+
39
+ __docformat__ = "epytext en"
40
+
41
+ __author__ = "Nicholas Amorim <nicholas@bloomshield.ee>"
42
+ __url__ = "http://pyrad2.readthedocs.io/en/latest/?badge=latest"
43
+ __version__ = "1.0"
44
+
45
+ __all__ = ["client", "dictionary", "packet", "server", "tools", "dictfile"]
@@ -0,0 +1,53 @@
1
+ from typing import Any, Dict, Hashable
2
+
3
+
4
+ class BiDict:
5
+ """
6
+ BiDict (Bidirectional Dictionary) provides a one-to-one mapping
7
+ between keys and values.
8
+
9
+ Supports both forward and reverse lookup.
10
+ """
11
+
12
+ def __init__(self) -> None:
13
+ """Initialize empty forward and reverse dictionaries."""
14
+ self.forward: Dict[Hashable, Any] = {}
15
+ self.backward: Dict[Hashable, Any] = {}
16
+
17
+ def Add(self, one: Hashable, two: Hashable) -> None:
18
+ """Add a bidirectional mapping between 'one' and 'two'."""
19
+ self.forward[one] = two
20
+ self.backward[two] = one
21
+
22
+ def __len__(self) -> int:
23
+ """Return the number of entries in the dictionary."""
24
+ return len(self.forward)
25
+
26
+ def __getitem__(self, key: Hashable) -> Any:
27
+ """Retrieve the value associated with the given key."""
28
+ return self.GetForward(key)
29
+
30
+ def __delitem__(self, key: Hashable) -> None:
31
+ """Remove key and its associated value from the dictionary."""
32
+ if key in self.forward:
33
+ del self.backward[self.forward[key]]
34
+ del self.forward[key]
35
+ else:
36
+ del self.forward[self.backward[key]]
37
+ del self.backward[key]
38
+
39
+ def GetForward(self, key: Hashable) -> Any:
40
+ """Return the value associated with 'key' from the forward mapping."""
41
+ return self.forward[key]
42
+
43
+ def HasForward(self, key: Hashable) -> bool:
44
+ """Check if 'key' exists in the forward mapping."""
45
+ return key in self.forward
46
+
47
+ def GetBackward(self, key: Hashable) -> Any:
48
+ """Return the key associated with 'value' from the reverse mapping."""
49
+ return self.backward[key]
50
+
51
+ def HasBackward(self, key: Hashable) -> bool:
52
+ """Check if 'value' exists in the reverse mapping."""
53
+ return key in self.backward
@@ -0,0 +1,242 @@
1
+ __docformat__ = "epytext en"
2
+
3
+ import hashlib
4
+ import select
5
+ import socket
6
+ import struct
7
+ import time
8
+ from typing import Optional
9
+
10
+ from pyrad2 import host, packet
11
+ from pyrad2.dictionary import Dictionary
12
+
13
+ EAP_CODE_REQUEST = 1
14
+ EAP_CODE_RESPONSE = 2
15
+ EAP_TYPE_IDENTITY = 1
16
+
17
+
18
+ class Timeout(Exception):
19
+ """Simple exception class which is raised when a timeout occurs
20
+ while waiting for a RADIUS server to respond."""
21
+
22
+
23
+ class Client(host.Host):
24
+ """Basic RADIUS client.
25
+ This class implements a basic RADIUS client. It can send requests
26
+ to a RADIUS server, taking care of timeouts and retries, and
27
+ validate its replies.
28
+
29
+ :ivar retries: number of times to retry sending a RADIUS request
30
+ :type retries: integer
31
+ :ivar timeout: number of seconds to wait for an answer
32
+ :type timeout: float
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ server: str,
38
+ authport: int = 1812,
39
+ acctport: int = 1813,
40
+ coaport: int = 3799,
41
+ secret: bytes = b"",
42
+ dict: Optional[Dictionary] = None,
43
+ retries: int = 3,
44
+ timeout: int = 5,
45
+ ):
46
+ """Constructor.
47
+
48
+ :param server: hostname or IP address of RADIUS server
49
+ :type server: string
50
+ :param authport: port to use for authentication packets
51
+ :type authport: integer
52
+ :param acctport: port to use for accounting packets
53
+ :type acctport: integer
54
+ :param coaport: port to use for CoA packets
55
+ :type coaport: integer
56
+ :param secret: RADIUS secret
57
+ :type secret: string
58
+ :param dict: RADIUS dictionary
59
+ :type dict: pyrad.dictionary.Dictionary
60
+ """
61
+ super().__init__(authport, acctport, coaport, dict)
62
+
63
+ self.server = server
64
+ self.secret = secret
65
+ self.retries = retries
66
+ self.timeout = timeout
67
+ self._poll = select.poll()
68
+ self._socket: Optional[socket.socket] = None
69
+
70
+ def bind(self, addr: str) -> None:
71
+ """Bind socket to an address.
72
+ Binding the socket used for communicating to an address can be
73
+ usefull when working on a machine with multiple addresses.
74
+
75
+ :param addr: network address (hostname or IP) and port to bind to
76
+ :type addr: host,port tuple
77
+ """
78
+ self._CloseSocket()
79
+ self._SocketOpen()
80
+ if self._socket:
81
+ self._socket.bind(addr)
82
+ else:
83
+ raise RuntimeError("No socket present")
84
+
85
+ def _SocketOpen(self) -> None:
86
+ try:
87
+ family = socket.getaddrinfo(self.server, 80)[0][0]
88
+ except Exception:
89
+ family = socket.AF_INET
90
+ if not self._socket:
91
+ self._socket = socket.socket(family, socket.SOCK_DGRAM)
92
+ self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
93
+ self._poll.register(self._socket, select.POLLIN)
94
+
95
+ def _CloseSocket(self) -> None:
96
+ if self._socket:
97
+ self._poll.unregister(self._socket)
98
+ self._socket.close()
99
+ self._socket = None
100
+
101
+ def CreateAuthPacket(self, **args) -> packet.Packet:
102
+ """Create a new RADIUS packet.
103
+ This utility function creates a new RADIUS packet which can
104
+ be used to communicate with the RADIUS server this client
105
+ talks to. This is initializing the new packet with the
106
+ dictionary and secret used for the client.
107
+
108
+ :return: a new empty packet instance
109
+ :rtype: pyrad.packet.AuthPacket
110
+ """
111
+ return super().CreateAuthPacket(secret=self.secret, **args)
112
+
113
+ def CreateAcctPacket(self, **args) -> packet.Packet:
114
+ """Create a new RADIUS packet.
115
+ This utility function creates a new RADIUS packet which can
116
+ be used to communicate with the RADIUS server this client
117
+ talks to. This is initializing the new packet with the
118
+ dictionary and secret used for the client.
119
+
120
+ :return: a new empty packet instance
121
+ :rtype: pyrad.packet.Packet
122
+ """
123
+ return super().CreateAcctPacket(secret=self.secret, **args)
124
+
125
+ def CreateCoAPacket(self, **args) -> packet.Packet:
126
+ """Create a new RADIUS packet.
127
+ This utility function creates a new RADIUS packet which can
128
+ be used to communicate with the RADIUS server this client
129
+ talks to. This is initializing the new packet with the
130
+ dictionary and secret used for the client.
131
+
132
+ :return: a new empty packet instance
133
+ :rtype: pyrad.packet.Packet
134
+ """
135
+ return super().CreateCoAPacket(secret=self.secret, **args)
136
+
137
+ def _SendPacket(self, pkt: packet.PacketImplementation, port: int):
138
+ """Send a packet to a RADIUS server.
139
+
140
+ :param pkt: the packet to send
141
+ :type pkt: pyrad.packet.Packet
142
+ :param port: UDP port to send packet to
143
+ :type port: integer
144
+ :return: the reply packet received
145
+ :rtype: pyrad.packet.Packet
146
+ :raise Timeout: RADIUS server does not reply
147
+ """
148
+ self._SocketOpen()
149
+
150
+ for attempt in range(self.retries):
151
+ if attempt and pkt.code == packet.AccountingRequest:
152
+ if "Acct-Delay-Time" in pkt:
153
+ pkt["Acct-Delay-Time"] = pkt["Acct-Delay-Time"][0] + self.timeout
154
+ else:
155
+ pkt["Acct-Delay-Time"] = self.timeout
156
+
157
+ now = time.time()
158
+ waitto = now + self.timeout
159
+
160
+ if not self._socket:
161
+ raise RuntimeError("No socket present")
162
+
163
+ self._socket.sendto(pkt.RequestPacket(), (self.server, port))
164
+
165
+ while now < waitto:
166
+ ready = self._poll.poll((waitto - now) * 1000)
167
+
168
+ if ready:
169
+ rawreply = self._socket.recv(4096)
170
+ else:
171
+ now = time.time()
172
+ continue
173
+
174
+ try:
175
+ reply = pkt.CreateReply(packet=rawreply)
176
+ if pkt.VerifyReply(reply, rawreply):
177
+ return reply
178
+ except packet.PacketError:
179
+ pass
180
+
181
+ now = time.time()
182
+
183
+ raise Timeout
184
+
185
+ def SendPacket(self, pkt: packet.PacketImplementation): # type: ignore
186
+ """Send a packet to a RADIUS server.
187
+
188
+ :param pkt: the packet to send
189
+ :type pkt: pyrad.packet.Packet
190
+ :return: the reply packet received
191
+ :rtype: pyrad.packet.Packet
192
+ :raise Timeout: RADIUS server does not reply
193
+ """
194
+ if isinstance(pkt, packet.AuthPacket):
195
+ if pkt.auth_type == "eap-md5":
196
+ # Creating EAP-Identity
197
+ password = pkt[2][0] if 2 in pkt else pkt[1][0]
198
+ pkt[79] = [
199
+ struct.pack(
200
+ "!BBHB%ds" % len(password),
201
+ EAP_CODE_RESPONSE,
202
+ packet.CurrentID,
203
+ len(password) + 5,
204
+ EAP_TYPE_IDENTITY,
205
+ password,
206
+ )
207
+ ]
208
+ reply = self._SendPacket(pkt, self.authport)
209
+ if (
210
+ reply
211
+ and reply.code == packet.AccessChallenge
212
+ and pkt.auth_type == "eap-md5"
213
+ ):
214
+ # Got an Access-Challenge
215
+ eap_code, eap_id, eap_size, eap_type, eap_md5 = struct.unpack(
216
+ "!BBHB%ds" % (len(reply[79][0]) - 5), reply[79][0]
217
+ )
218
+ # Sending back an EAP-Type-MD5-Challenge
219
+ # Thank god for http://www.secdev.org/python/eapy.py
220
+ client_pw = pkt[2][0] if 2 in pkt else pkt[1][0]
221
+ md5_challenge = hashlib.md5(
222
+ struct.pack("!B", eap_id) + client_pw + eap_md5[1:]
223
+ ).digest()
224
+ pkt[79] = [
225
+ struct.pack(
226
+ "!BBHBB",
227
+ 2,
228
+ eap_id,
229
+ len(md5_challenge) + 6,
230
+ 4,
231
+ len(md5_challenge),
232
+ )
233
+ + md5_challenge
234
+ ]
235
+ # Copy over Challenge-State
236
+ pkt[24] = reply[24]
237
+ reply = self._SendPacket(pkt, self.authport)
238
+ return reply
239
+ elif isinstance(pkt, packet.CoAPacket):
240
+ return self._SendPacket(pkt, self.coaport)
241
+ else:
242
+ return self._SendPacket(pkt, self.acctport)