pyrad 2.3__py3-none-any.whl → 2.5.0__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.
Files changed (53) hide show
  1. docs/Makefile +20 -0
  2. docs/make.bat +36 -0
  3. docs/source/_static/logo.png +0 -0
  4. docs/source/api/client.rst +10 -0
  5. docs/source/api/dictionary.rst +10 -0
  6. docs/source/api/host.rst +7 -0
  7. docs/source/api/packet.rst +48 -0
  8. docs/source/api/proxy.rst +7 -0
  9. docs/source/api/server.rst +13 -0
  10. docs/source/conf.py +158 -0
  11. docs/source/index.rst +75 -0
  12. example/acct.py +41 -0
  13. example/auth.py +37 -0
  14. example/auth_async.py +164 -0
  15. example/client-coa.py +61 -0
  16. example/coa.py +40 -0
  17. example/dictionary +405 -0
  18. example/dictionary.freeradius +91 -0
  19. example/pyrad.log +0 -0
  20. example/server.py +68 -0
  21. example/server_async.py +117 -0
  22. example/status.py +26 -0
  23. pyrad/__init__.py +3 -3
  24. pyrad/client.py +54 -9
  25. pyrad/client_async.py +22 -14
  26. pyrad/dictfile.py +2 -5
  27. pyrad/dictionary.py +12 -1
  28. pyrad/host.py +1 -1
  29. pyrad/packet.py +208 -133
  30. pyrad/proxy.py +2 -2
  31. pyrad/server.py +3 -7
  32. pyrad/server_async.py +4 -5
  33. pyrad/tests/__init__.py +2 -2
  34. pyrad/tests/mock.py +5 -1
  35. pyrad/tests/{testBidict.py → test_bidict.py} +2 -2
  36. pyrad/tests/{testClient.py → test_client.py} +28 -30
  37. pyrad/tests/{testDictionary.py → test_dictionary.py} +38 -21
  38. pyrad/tests/{testHost.py → test_host.py} +10 -10
  39. pyrad/tests/test_packet.py +679 -0
  40. pyrad/tests/{testProxy.py → test_proxy.py} +11 -11
  41. pyrad/tests/{testServer.py → test_server.py} +35 -33
  42. pyrad/tests/test_tools.py +126 -0
  43. pyrad/tools.py +254 -158
  44. {pyrad-2.3.dist-info → pyrad-2.5.0.dist-info}/METADATA +44 -20
  45. pyrad-2.5.0.dist-info/RECORD +51 -0
  46. {pyrad-2.3.dist-info → pyrad-2.5.0.dist-info}/WHEEL +1 -1
  47. {pyrad-2.3.dist-info → pyrad-2.5.0.dist-info/licenses}/LICENSE.txt +2 -1
  48. pyrad-2.5.0.dist-info/top_level.txt +3 -0
  49. pyrad/tests/testPacket.py +0 -530
  50. pyrad/tests/testTools.py +0 -122
  51. pyrad-2.3.dist-info/RECORD +0 -29
  52. pyrad-2.3.dist-info/top_level.txt +0 -1
  53. {pyrad-2.3.dist-info → pyrad-2.5.0.dist-info}/zip-safe +0 -0
@@ -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 <developer@gicnet.de>'
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-2020 Wichert Akkerman and Christian Giese. All rights reserved.'
44
- __version__ = '2.3'
43
+ __copyright__ = 'Copyright 2002-2026 Wichert Akkerman, Christian Giese, Istvan Ruzman and Stefan Lieberth. All rights reserved.'
44
+ __version__ = '2.5.0'
45
45
 
46
46
  __all__ = ['client', 'dictionary', 'packet', 'server', 'tools', 'dictfile']
pyrad/client.py CHANGED
@@ -4,13 +4,19 @@
4
4
 
5
5
  __docformat__ = "epytext en"
6
6
 
7
+ import hashlib
7
8
  import select
8
9
  import socket
9
10
  import time
11
+ import struct
10
12
  import six
11
13
  from pyrad import host
12
14
  from pyrad import packet
13
15
 
16
+ EAP_CODE_REQUEST = 1
17
+ EAP_CODE_RESPONSE = 2
18
+ EAP_TYPE_IDENTITY = 1
19
+
14
20
 
15
21
  class Timeout(Exception):
16
22
  """Simple exception class which is raised when a timeout occurs
@@ -26,11 +32,10 @@ class Client(host.Host):
26
32
  :ivar retries: number of times to retry sending a RADIUS request
27
33
  :type retries: integer
28
34
  :ivar timeout: number of seconds to wait for an answer
29
- :type timeout: integer
35
+ :type timeout: float
30
36
  """
31
37
  def __init__(self, server, authport=1812, acctport=1813,
32
- coaport=3799, secret=six.b(''), dict=None):
33
-
38
+ coaport=3799, secret=six.b(''), dict=None, retries=3, timeout=5, enforce_ma=False):
34
39
  """Constructor.
35
40
 
36
41
  :param server: hostname or IP address of RADIUS server
@@ -45,14 +50,17 @@ class Client(host.Host):
45
50
  :type secret: string
46
51
  :param dict: RADIUS dictionary
47
52
  :type dict: pyrad.dictionary.Dictionary
53
+ :param enforce_ma: Enforce usage and check of Message-Authenticator
54
+ :type enforce_ma: boolean
48
55
  """
49
56
  host.Host.__init__(self, authport, acctport, coaport, dict)
50
57
 
51
58
  self.server = server
52
59
  self.secret = secret
53
60
  self._socket = None
54
- self.retries = 3
55
- self.timeout = 5
61
+ self.retries = retries
62
+ self.timeout = timeout
63
+ self.enforce_ma = enforce_ma
56
64
  self._poll = select.poll()
57
65
 
58
66
  def bind(self, addr):
@@ -69,12 +77,12 @@ class Client(host.Host):
69
77
 
70
78
  def _SocketOpen(self):
71
79
  try:
72
- family = socket.getaddrinfo(self.server, 'www')[0][0]
73
- except:
80
+ family = socket.getaddrinfo(self.server, 80)[0][0]
81
+ except Exception:
74
82
  family = socket.AF_INET
75
83
  if not self._socket:
76
84
  self._socket = socket.socket(family,
77
- socket.SOCK_DGRAM)
85
+ socket.SOCK_DGRAM)
78
86
  self._socket.setsockopt(socket.SOL_SOCKET,
79
87
  socket.SO_REUSEADDR, 1)
80
88
  self._poll.register(self._socket, select.POLLIN)
@@ -95,6 +103,9 @@ class Client(host.Host):
95
103
  :return: a new empty packet instance
96
104
  :rtype: pyrad.packet.AuthPacket
97
105
  """
106
+ if self.enforce_ma:
107
+ return host.Host.CreateAuthPacket(self, secret=self.secret,
108
+ message_authenticator=True, **args)
98
109
  return host.Host.CreateAuthPacket(self, secret=self.secret, **args)
99
110
 
100
111
  def CreateAcctPacket(self, **args):
@@ -159,6 +170,8 @@ class Client(host.Host):
159
170
  try:
160
171
  reply = pkt.CreateReply(packet=rawreply)
161
172
  if pkt.VerifyReply(reply, rawreply):
173
+ if hasattr(pkt, 'authenticator'):
174
+ reply.request_authenticator = pkt.authenticator
162
175
  return reply
163
176
  except packet.PacketError:
164
177
  pass
@@ -177,7 +190,39 @@ class Client(host.Host):
177
190
  :raise Timeout: RADIUS server does not reply
178
191
  """
179
192
  if isinstance(pkt, packet.AuthPacket):
180
- return self._SendPacket(pkt, self.authport)
193
+ if pkt.auth_type == 'eap-md5':
194
+ # Creating EAP-Identity
195
+ password = pkt[2][0] if 2 in pkt else pkt[1][0]
196
+ pkt[79] = [struct.pack('!BBHB%ds' % len(password),
197
+ EAP_CODE_RESPONSE,
198
+ packet.CurrentID,
199
+ len(password) + 5,
200
+ EAP_TYPE_IDENTITY,
201
+ password)]
202
+ reply = self._SendPacket(pkt, self.authport)
203
+ if (
204
+ reply
205
+ and reply.code == packet.AccessChallenge
206
+ and pkt.auth_type == 'eap-md5'
207
+ ):
208
+ # Got an Access-Challenge
209
+ eap_code, eap_id, eap_size, eap_type, eap_md5 = struct.unpack(
210
+ '!BBHB%ds' % (len(reply[79][0]) - 5), reply[79][0]
211
+ )
212
+ # Sending back an EAP-Type-MD5-Challenge
213
+ # Thank god for http://www.secdev.org/python/eapy.py
214
+ client_pw = pkt[2][0] if 2 in pkt else pkt[1][0]
215
+ md5_challenge = hashlib.md5(
216
+ struct.pack('!B', eap_id) + client_pw + eap_md5[1:]
217
+ ).digest()
218
+ pkt[79] = [
219
+ struct.pack('!BBHBB', 2, eap_id, len(md5_challenge) + 6,
220
+ 4, len(md5_challenge)) + md5_challenge
221
+ ]
222
+ # Copy over Challenge-State
223
+ pkt[24] = reply[24]
224
+ reply = self._SendPacket(pkt, self.authport)
225
+ return reply
181
226
  elif isinstance(pkt, packet.CoAPacket):
182
227
  return self._SendPacket(pkt, self.coaport)
183
228
  else:
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
- '[%s:%d] Transport created with binding in %s:%d',
97
- self.server, self.port,
98
- socket.getsockname()[0],
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,21 +118,21 @@ 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. %s', self.server, self.port, reply.id)
133
+ self.logger.warn('[%s:%d] Ignore invalid reply for id %d: %s', self.server, self.port, reply.id, data)
137
134
  else:
138
- self.logger.warn('[%s:%d] Ignore invalid reply: %d', self.server, self.port, data)
135
+ self.logger.warn('[%s:%d] Ignore invalid reply: %s', self.server, self.port, data)
139
136
 
140
137
  except Exception as exc:
141
138
  self.logger.error('[%s:%d] Error on decode packet: %s', self.server, self.port, exc)
@@ -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=six.b(''), dict=None,
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.protocol_acct:
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(),
@@ -398,9 +401,14 @@ class ClientAsync:
398
401
  if not self.protocol_acct:
399
402
  raise Exception('Transport not initialized')
400
403
 
404
+ self.protocol_acct.send_packet(pkt, ans)
405
+
401
406
  elif isinstance(pkt, CoAPacket):
402
407
  if not self.protocol_coa:
403
408
  raise Exception('Transport not initialized')
409
+
410
+ self.protocol_coa.send_packet(pkt, ans)
411
+
404
412
  else:
405
413
  raise Exception('Unsupported packet')
406
414
 
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, six.string_types):
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 == None:
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
 
@@ -220,6 +219,18 @@ class Dictionary(object):
220
219
  (attribute, code, datatype) = tokens[1:4]
221
220
 
222
221
  codes = code.split('.')
222
+
223
+ # Codes can be sent as hex, or octal or decimal string representations.
224
+ tmp = []
225
+ for c in codes:
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))
232
+ codes = tmp
233
+
223
234
  is_sub_attribute = (len(codes) > 1)
224
235
  if len(codes) == 2:
225
236
  code = int(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 accouting RADIUS packet
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.