pyrad 2.4__py3-none-any.whl → 2.5.1__py3-none-any.whl
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.
- docs/Makefile +20 -0
- docs/make.bat +36 -0
- docs/source/_static/logo.png +0 -0
- docs/source/api/client.rst +10 -0
- docs/source/api/dictionary.rst +10 -0
- docs/source/api/host.rst +7 -0
- docs/source/api/packet.rst +48 -0
- docs/source/api/proxy.rst +7 -0
- docs/source/api/server.rst +13 -0
- docs/source/conf.py +158 -0
- docs/source/index.rst +75 -0
- example/acct.py +41 -0
- example/auth.py +37 -0
- example/auth_async.py +164 -0
- example/client-coa.py +61 -0
- example/coa.py +40 -0
- example/dictionary +405 -0
- example/dictionary.freeradius +91 -0
- example/pyrad.log +0 -0
- example/server.py +68 -0
- example/server_async.py +117 -0
- example/status.py +26 -0
- pyrad/__init__.py +3 -3
- pyrad/client.py +14 -6
- pyrad/client_async.py +16 -13
- pyrad/dictfile.py +2 -5
- pyrad/dictionary.py +6 -7
- pyrad/host.py +1 -1
- pyrad/packet.py +145 -114
- pyrad/proxy.py +2 -2
- pyrad/server.py +3 -7
- pyrad/server_async.py +3 -4
- pyrad/tests/__init__.py +5 -0
- pyrad/tests/mock.py +145 -0
- pyrad/tests/test_bidict.py +56 -0
- pyrad/tests/test_client.py +183 -0
- pyrad/tests/test_dictionary.py +357 -0
- pyrad/tests/test_host.py +87 -0
- pyrad/tests/test_packet.py +679 -0
- pyrad/tests/test_proxy.py +96 -0
- pyrad/tests/test_server.py +323 -0
- pyrad/tests/test_tools.py +126 -0
- pyrad/tools.py +254 -158
- {pyrad-2.4.dist-info → pyrad-2.5.1.dist-info}/METADATA +45 -22
- pyrad-2.5.1.dist-info/RECORD +51 -0
- {pyrad-2.4.dist-info → pyrad-2.5.1.dist-info}/WHEEL +1 -1
- {pyrad-2.4.dist-info → pyrad-2.5.1.dist-info/licenses}/LICENSE.txt +1 -1
- pyrad-2.5.1.dist-info/top_level.txt +3 -0
- pyrad-2.4.dist-info/RECORD +0 -19
- pyrad-2.4.dist-info/top_level.txt +0 -1
- {pyrad-2.4.dist-info → pyrad-2.5.1.dist-info}/zip-safe +0 -0
example/server_async.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import traceback
|
|
7
|
+
from pyrad.dictionary import Dictionary
|
|
8
|
+
from pyrad.server_async import ServerAsync
|
|
9
|
+
from pyrad.packet import AccessAccept
|
|
10
|
+
from pyrad.server import RemoteHost
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import uvloop
|
|
14
|
+
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
|
15
|
+
except:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
logging.basicConfig(level="DEBUG",
|
|
19
|
+
format="%(asctime)s [%(levelname)-8s] %(message)s")
|
|
20
|
+
|
|
21
|
+
class FakeServer(ServerAsync):
|
|
22
|
+
|
|
23
|
+
def __init__(self, loop, dictionary):
|
|
24
|
+
|
|
25
|
+
ServerAsync.__init__(self, loop=loop, dictionary=dictionary,
|
|
26
|
+
enable_pkt_verify=True, debug=True)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def handle_auth_packet(self, protocol, pkt, addr):
|
|
30
|
+
|
|
31
|
+
print("Received an authentication request with id ", pkt.id)
|
|
32
|
+
print('Authenticator ', pkt.authenticator.hex())
|
|
33
|
+
print('Secret ', pkt.secret)
|
|
34
|
+
print("Attributes: ")
|
|
35
|
+
for attr in pkt.keys():
|
|
36
|
+
print("%s: %s" % (attr, pkt[attr]))
|
|
37
|
+
|
|
38
|
+
reply = self.CreateReplyPacket(pkt, **{
|
|
39
|
+
"Service-Type": "Framed-User",
|
|
40
|
+
"Framed-IP-Address": '192.168.0.1',
|
|
41
|
+
"Framed-IPv6-Prefix": "fc66::1/64"
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
reply.code = AccessAccept
|
|
45
|
+
protocol.send_response(reply, addr)
|
|
46
|
+
|
|
47
|
+
def handle_acct_packet(self, protocol, pkt, addr):
|
|
48
|
+
|
|
49
|
+
print("Received an accounting request")
|
|
50
|
+
print("Attributes: ")
|
|
51
|
+
for attr in pkt.keys():
|
|
52
|
+
print("%s: %s" % (attr, pkt[attr]))
|
|
53
|
+
|
|
54
|
+
reply = self.CreateReplyPacket(pkt)
|
|
55
|
+
protocol.send_response(reply, addr)
|
|
56
|
+
|
|
57
|
+
def handle_coa_packet(self, protocol, pkt, addr):
|
|
58
|
+
|
|
59
|
+
print("Received an coa request")
|
|
60
|
+
print("Attributes: ")
|
|
61
|
+
for attr in pkt.keys():
|
|
62
|
+
print("%s: %s" % (attr, pkt[attr]))
|
|
63
|
+
|
|
64
|
+
reply = self.CreateReplyPacket(pkt)
|
|
65
|
+
protocol.send_response(reply, addr)
|
|
66
|
+
|
|
67
|
+
def handle_disconnect_packet(self, protocol, pkt, addr):
|
|
68
|
+
|
|
69
|
+
print("Received an disconnect request")
|
|
70
|
+
print("Attributes: ")
|
|
71
|
+
for attr in pkt.keys():
|
|
72
|
+
print("%s: %s" % (attr, pkt[attr]))
|
|
73
|
+
|
|
74
|
+
reply = self.CreateReplyPacket(pkt)
|
|
75
|
+
# COA NAK
|
|
76
|
+
reply.code = 45
|
|
77
|
+
protocol.send_response(reply, addr)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == '__main__':
|
|
81
|
+
|
|
82
|
+
# create server and read dictionary
|
|
83
|
+
loop = asyncio.get_event_loop()
|
|
84
|
+
server = FakeServer(loop=loop, dictionary=Dictionary('dictionary'))
|
|
85
|
+
|
|
86
|
+
# add clients (address, secret, name)
|
|
87
|
+
server.hosts["127.0.0.1"] = RemoteHost("127.0.0.1",
|
|
88
|
+
b"Kah3choteereethiejeimaeziecumi",
|
|
89
|
+
"localhost")
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
|
|
93
|
+
# Initialize transports
|
|
94
|
+
loop.run_until_complete(
|
|
95
|
+
asyncio.ensure_future(
|
|
96
|
+
server.initialize_transports(enable_auth=True,
|
|
97
|
+
enable_acct=True,
|
|
98
|
+
enable_coa=True)))
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# start server
|
|
102
|
+
loop.run_forever()
|
|
103
|
+
except KeyboardInterrupt as k:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
# Close transports
|
|
107
|
+
loop.run_until_complete(asyncio.ensure_future(
|
|
108
|
+
server.deinitialize_transports()))
|
|
109
|
+
|
|
110
|
+
except Exception as exc:
|
|
111
|
+
print('Error: ', exc)
|
|
112
|
+
print('\n'.join(traceback.format_exc().splitlines()))
|
|
113
|
+
# Close transports
|
|
114
|
+
loop.run_until_complete(asyncio.ensure_future(
|
|
115
|
+
server.deinitialize_transports()))
|
|
116
|
+
|
|
117
|
+
loop.close()
|
example/status.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
from pyrad.client import Client
|
|
3
|
+
from pyrad.dictionary import Dictionary
|
|
4
|
+
import socket
|
|
5
|
+
import sys
|
|
6
|
+
import pyrad.packet
|
|
7
|
+
|
|
8
|
+
srv = Client(server="localhost", authport=18121, secret=b"test", dict=Dictionary("dictionary"))
|
|
9
|
+
|
|
10
|
+
req = srv.CreateAuthPacket(code=pyrad.packet.StatusServer)
|
|
11
|
+
req["FreeRADIUS-Statistics-Type"] = "All"
|
|
12
|
+
req.add_message_authenticator()
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
print("Sending FreeRADIUS status request")
|
|
16
|
+
reply = srv.SendPacket(req)
|
|
17
|
+
except pyrad.client.Timeout:
|
|
18
|
+
print("RADIUS server does not reply")
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
except socket.error as error:
|
|
21
|
+
print("Network error: " + error[1])
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
print("Attributes returned by server:")
|
|
25
|
+
for i in reply.keys():
|
|
26
|
+
print("%s: %s" % (i, reply[i]))
|
pyrad/__init__.py
CHANGED
|
@@ -38,9 +38,9 @@ This package contains four modules:
|
|
|
38
38
|
|
|
39
39
|
__docformat__ = 'epytext en'
|
|
40
40
|
|
|
41
|
-
__author__ = 'Christian Giese <
|
|
41
|
+
__author__ = 'Christian Giese <gic@gicnet.de>, Istvan Ruzman <istvan@ruzman.eu> and Stefan Lieberth <stefan@lieberth.net>'
|
|
42
42
|
__url__ = 'http://pyrad.readthedocs.io/en/latest/?badge=latest'
|
|
43
|
-
__copyright__ = 'Copyright 2002-
|
|
44
|
-
__version__ = '2.
|
|
43
|
+
__copyright__ = 'Copyright 2002-2026 Wichert Akkerman, Christian Giese, Istvan Ruzman and Stefan Lieberth. All rights reserved.'
|
|
44
|
+
__version__ = '2.5.1'
|
|
45
45
|
|
|
46
46
|
__all__ = ['client', 'dictionary', 'packet', 'server', 'tools', 'dictfile']
|
pyrad/client.py
CHANGED
|
@@ -8,8 +8,8 @@ import hashlib
|
|
|
8
8
|
import select
|
|
9
9
|
import socket
|
|
10
10
|
import time
|
|
11
|
-
import six
|
|
12
11
|
import struct
|
|
12
|
+
import six
|
|
13
13
|
from pyrad import host
|
|
14
14
|
from pyrad import packet
|
|
15
15
|
|
|
@@ -17,6 +17,7 @@ EAP_CODE_REQUEST = 1
|
|
|
17
17
|
EAP_CODE_RESPONSE = 2
|
|
18
18
|
EAP_TYPE_IDENTITY = 1
|
|
19
19
|
|
|
20
|
+
|
|
20
21
|
class Timeout(Exception):
|
|
21
22
|
"""Simple exception class which is raised when a timeout occurs
|
|
22
23
|
while waiting for a RADIUS server to respond."""
|
|
@@ -34,8 +35,7 @@ class Client(host.Host):
|
|
|
34
35
|
:type timeout: float
|
|
35
36
|
"""
|
|
36
37
|
def __init__(self, server, authport=1812, acctport=1813,
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
coaport=3799, secret=six.b(''), dict=None, retries=3, timeout=5, enforce_ma=False):
|
|
39
39
|
"""Constructor.
|
|
40
40
|
|
|
41
41
|
:param server: hostname or IP address of RADIUS server
|
|
@@ -50,6 +50,8 @@ class Client(host.Host):
|
|
|
50
50
|
:type secret: string
|
|
51
51
|
:param dict: RADIUS dictionary
|
|
52
52
|
:type dict: pyrad.dictionary.Dictionary
|
|
53
|
+
:param enforce_ma: Enforce usage and check of Message-Authenticator
|
|
54
|
+
:type enforce_ma: boolean
|
|
53
55
|
"""
|
|
54
56
|
host.Host.__init__(self, authport, acctport, coaport, dict)
|
|
55
57
|
|
|
@@ -58,6 +60,7 @@ class Client(host.Host):
|
|
|
58
60
|
self._socket = None
|
|
59
61
|
self.retries = retries
|
|
60
62
|
self.timeout = timeout
|
|
63
|
+
self.enforce_ma = enforce_ma
|
|
61
64
|
self._poll = select.poll()
|
|
62
65
|
|
|
63
66
|
def bind(self, addr):
|
|
@@ -74,12 +77,12 @@ class Client(host.Host):
|
|
|
74
77
|
|
|
75
78
|
def _SocketOpen(self):
|
|
76
79
|
try:
|
|
77
|
-
family = socket.getaddrinfo(self.server,
|
|
78
|
-
except:
|
|
80
|
+
family = socket.getaddrinfo(self.server, 80)[0][0]
|
|
81
|
+
except Exception:
|
|
79
82
|
family = socket.AF_INET
|
|
80
83
|
if not self._socket:
|
|
81
84
|
self._socket = socket.socket(family,
|
|
82
|
-
|
|
85
|
+
socket.SOCK_DGRAM)
|
|
83
86
|
self._socket.setsockopt(socket.SOL_SOCKET,
|
|
84
87
|
socket.SO_REUSEADDR, 1)
|
|
85
88
|
self._poll.register(self._socket, select.POLLIN)
|
|
@@ -100,6 +103,9 @@ class Client(host.Host):
|
|
|
100
103
|
:return: a new empty packet instance
|
|
101
104
|
:rtype: pyrad.packet.AuthPacket
|
|
102
105
|
"""
|
|
106
|
+
if self.enforce_ma:
|
|
107
|
+
return host.Host.CreateAuthPacket(self, secret=self.secret,
|
|
108
|
+
message_authenticator=True, **args)
|
|
103
109
|
return host.Host.CreateAuthPacket(self, secret=self.secret, **args)
|
|
104
110
|
|
|
105
111
|
def CreateAcctPacket(self, **args):
|
|
@@ -164,6 +170,8 @@ class Client(host.Host):
|
|
|
164
170
|
try:
|
|
165
171
|
reply = pkt.CreateReply(packet=rawreply)
|
|
166
172
|
if pkt.VerifyReply(reply, rawreply):
|
|
173
|
+
if hasattr(pkt, 'authenticator'):
|
|
174
|
+
reply.request_authenticator = pkt.authenticator
|
|
167
175
|
return reply
|
|
168
176
|
except packet.PacketError:
|
|
169
177
|
pass
|
pyrad/client_async.py
CHANGED
|
@@ -6,7 +6,6 @@ __docformat__ = "epytext en"
|
|
|
6
6
|
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
import asyncio
|
|
9
|
-
import six
|
|
10
9
|
import logging
|
|
11
10
|
import random
|
|
12
11
|
|
|
@@ -92,12 +91,10 @@ class DatagramProtocolClient(asyncio.Protocol):
|
|
|
92
91
|
def connection_made(self, transport):
|
|
93
92
|
self.transport = transport
|
|
94
93
|
socket = transport.get_extra_info('socket')
|
|
95
|
-
self.logger.info(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
socket.getsockname()[1]
|
|
100
|
-
)
|
|
94
|
+
self.logger.info('[%s:%d] Transport created with binding in %s:%d',
|
|
95
|
+
self.server, self.port,
|
|
96
|
+
socket.getsockname()[0],
|
|
97
|
+
socket.getsockname()[1])
|
|
101
98
|
|
|
102
99
|
pre_loop = asyncio.get_event_loop()
|
|
103
100
|
asyncio.set_event_loop(loop=self.client.loop)
|
|
@@ -121,19 +118,19 @@ class DatagramProtocolClient(asyncio.Protocol):
|
|
|
121
118
|
try:
|
|
122
119
|
reply = Packet(packet=data, dict=self.client.dict)
|
|
123
120
|
|
|
124
|
-
if reply and reply.id in self.pending_requests:
|
|
121
|
+
if reply.code and reply.id in self.pending_requests:
|
|
125
122
|
req = self.pending_requests[reply.id]
|
|
126
123
|
packet = req['packet']
|
|
127
124
|
|
|
128
125
|
reply.dict = packet.dict
|
|
129
126
|
reply.secret = packet.secret
|
|
130
127
|
|
|
131
|
-
if packet.VerifyReply(reply, data):
|
|
128
|
+
if packet.VerifyReply(reply, data, enforce_ma=self.client.enforce_ma):
|
|
132
129
|
req['future'].set_result(reply)
|
|
133
130
|
# Remove request for map
|
|
134
131
|
del self.pending_requests[reply.id]
|
|
135
132
|
else:
|
|
136
|
-
self.logger.warn('[%s:%d] Ignore invalid reply for id %d
|
|
133
|
+
self.logger.warn('[%s:%d] Ignore invalid reply for id %d: %s', self.server, self.port, reply.id, data)
|
|
137
134
|
else:
|
|
138
135
|
self.logger.warn('[%s:%d] Ignore invalid reply: %s', self.server, self.port, data)
|
|
139
136
|
|
|
@@ -175,9 +172,9 @@ class ClientAsync:
|
|
|
175
172
|
"""
|
|
176
173
|
# noinspection PyShadowingBuiltins
|
|
177
174
|
def __init__(self, server, auth_port=1812, acct_port=1813,
|
|
178
|
-
coa_port=3799, secret=
|
|
175
|
+
coa_port=3799, secret=b'', dict=None,
|
|
179
176
|
loop=None, retries=3, timeout=30,
|
|
180
|
-
logger_name='pyrad'):
|
|
177
|
+
logger_name='pyrad', enforce_ma=False):
|
|
181
178
|
|
|
182
179
|
"""Constructor.
|
|
183
180
|
|
|
@@ -216,6 +213,7 @@ class ClientAsync:
|
|
|
216
213
|
|
|
217
214
|
self.protocol_coa = None
|
|
218
215
|
self.coa_port = coa_port
|
|
216
|
+
self.enforce_ma = enforce_ma
|
|
219
217
|
|
|
220
218
|
async def initialize_transports(self, enable_acct=False,
|
|
221
219
|
enable_auth=False, enable_coa=False,
|
|
@@ -325,6 +323,11 @@ class ClientAsync:
|
|
|
325
323
|
"""
|
|
326
324
|
if not self.protocol_auth:
|
|
327
325
|
raise Exception('Transport not initialized')
|
|
326
|
+
if self.enforce_ma:
|
|
327
|
+
return AuthPacket(dict=self.dict,
|
|
328
|
+
id=self.protocol_auth.create_id(),
|
|
329
|
+
secret=self.secret,
|
|
330
|
+
message_authenticator=True, **args)
|
|
328
331
|
|
|
329
332
|
return AuthPacket(dict=self.dict,
|
|
330
333
|
id=self.protocol_auth.create_id(),
|
|
@@ -360,7 +363,7 @@ class ClientAsync:
|
|
|
360
363
|
:rtype: pyrad.packet.Packet
|
|
361
364
|
"""
|
|
362
365
|
|
|
363
|
-
if not self.
|
|
366
|
+
if not self.protocol_coa:
|
|
364
367
|
raise Exception('Transport not initialized')
|
|
365
368
|
|
|
366
369
|
return CoAPacket(id=self.protocol_coa.create_id(),
|
pyrad/dictfile.py
CHANGED
|
@@ -9,7 +9,6 @@ RADIUS $INCLUDE directives behind the scene.
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import os
|
|
12
|
-
import six
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
class _Node(object):
|
|
@@ -54,10 +53,8 @@ class DictFile(object):
|
|
|
54
53
|
self.__ReadNode(fil)
|
|
55
54
|
|
|
56
55
|
def __ReadNode(self, fil):
|
|
57
|
-
node = None
|
|
58
56
|
parentdir = self.__CurDir()
|
|
59
|
-
if isinstance(fil,
|
|
60
|
-
fname = None
|
|
57
|
+
if isinstance(fil, str):
|
|
61
58
|
if os.path.isabs(fil):
|
|
62
59
|
fname = fil
|
|
63
60
|
else:
|
|
@@ -105,7 +102,7 @@ class DictFile(object):
|
|
|
105
102
|
def __next__(self):
|
|
106
103
|
while self.stack:
|
|
107
104
|
line = self.stack[-1].Next()
|
|
108
|
-
if line
|
|
105
|
+
if line is None:
|
|
109
106
|
self.stack.pop()
|
|
110
107
|
else:
|
|
111
108
|
inc = self.__GetInclude(line)
|
pyrad/dictionary.py
CHANGED
|
@@ -75,7 +75,6 @@ from pyrad import bidict
|
|
|
75
75
|
from pyrad import tools
|
|
76
76
|
from pyrad import dictfile
|
|
77
77
|
from copy import copy
|
|
78
|
-
import logging
|
|
79
78
|
|
|
80
79
|
__docformat__ = 'epytext en'
|
|
81
80
|
|
|
@@ -224,12 +223,12 @@ class Dictionary(object):
|
|
|
224
223
|
# Codes can be sent as hex, or octal or decimal string representations.
|
|
225
224
|
tmp = []
|
|
226
225
|
for c in codes:
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
226
|
+
if c.startswith('0x'):
|
|
227
|
+
tmp.append(int(c, 16))
|
|
228
|
+
elif c.startswith('0o'):
|
|
229
|
+
tmp.append(int(c, 8))
|
|
230
|
+
else:
|
|
231
|
+
tmp.append(int(c, 10))
|
|
233
232
|
codes = tmp
|
|
234
233
|
|
|
235
234
|
is_sub_attribute = (len(codes) > 1)
|
pyrad/host.py
CHANGED
|
@@ -57,7 +57,7 @@ class Host(object):
|
|
|
57
57
|
|
|
58
58
|
def CreateAcctPacket(self, **args):
|
|
59
59
|
"""Create a new accounting RADIUS packet.
|
|
60
|
-
This utility function creates a new
|
|
60
|
+
This utility function creates a new accounting RADIUS packet
|
|
61
61
|
which can be used to communicate with the RADIUS server this
|
|
62
62
|
client talks to. This is initializing the new packet with the
|
|
63
63
|
dictionary and secret used for the client.
|