import from HEAD:
[freeradius.git] / src / modules / rlm_python / prepaid.py
1 #
2 # Example Python module for prepaid usage using MySQL
3
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12 # You should have received a copy of the GNU General Public License
13 # along with this program; if not, write to the Free Software
14 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
15 #
16 # Copyright 2002 Miguel A.L. Paraz <mparaz@mparaz.com>
17 # Copyright 2002 Imperium Technology, Inc.
18
19 import radiusd
20 import MySQLdb
21
22 # Configuration
23 configDb = 'python'                     # Database name
24 configHost = 'localhost'                # Database host
25 configUser = 'python'                   # Database user and password
26 configPasswd = 'python'         
27
28 # xxx Database 
29
30 # Globals
31 dbHandle = None
32
33 def log(level, s):
34   """Log function."""
35   radiusd.radlog(level, 'prepaid.py: ' + s)
36
37 def instantiate(p):
38   """Module Instantiation.  0 for success, -1 for failure.
39   p is a dummy variable here."""
40   global dbHandle
41
42   p = p
43
44   try:
45     dbHandle = MySQLdb.connect(db=configDb, host=configHost,
46                                user=configUser, passwd=configPasswd)
47
48   except MySQLdb.OperationalError, e:
49     # Report the error and return -1 for failure.
50     # xxx A more advanced module would retry the database.
51     log(radiusd.L_ERR, str(e))
52     return -1
53
54   log(radiusd.L_INFO, 'db connection: ' + str(dbHandle))
55
56   return 0
57
58
59 def authorize(authData):
60   """Authorization and authentication are done in one step."""
61
62   # Extract the data we need.
63   userName = None
64   userPasswd = None
65   
66   for t in authData:
67     if t[0] == 'User-Name':
68       userName = t[1]
69     elif t[0] == 'Password':
70       userPasswd = t[1]
71
72   # Build and log the SQL statement
73   # radiusd puts double quotes (") around the string representation of
74   # the RADIUS packet.
75   sql = 'select passwd, maxseconds from users where username = ' +  userName
76   
77   log(radiusd.L_DBG, sql)
78
79   # Get a cursor
80   # xxx Or should this be one cursor all throughout?
81   try:
82     dbCursor = dbHandle.cursor()
83   except MySQLdb.OperationalError, e:
84     log(radiusd.L_ERR, str(e))
85     return radiusd.RLM_MODULE_FAIL
86
87   # Execute the SQL statement
88   try:
89     dbCursor.execute(sql)
90   except MySQLdb.OperationalError, e:
91     log(radiusd.L_ERR, str(e))
92     dbCursor.close()
93     return radiusd.RLM_MODULE_FAIL
94   
95   # Get the result. (passwd, maxseconds)
96   result = dbCursor.fetchone()
97   if not result:
98     # User not found
99     log(radiusd.L_INFO, 'user not found: ' + userName)
100     dbCursor.close()
101     return radiusd.RLM_MODULE_NOTFOUND
102
103
104  
105   # Compare passwords
106   # Ignore the quotes around userPasswd.
107   if result[0] != userPasswd[1:-1]:
108     log(radiusd.L_DBG, 'user password mismatch: ' + userName)
109     return radiusd.RLM_MODULE_REJECT
110
111   maxSeconds = result[1]
112
113   # Compute their session limit
114   
115   # Build and log the SQL statement
116   sql = 'select sum(seconds) from sessions where username = ' + userName
117
118   log(radiusd.L_DBG, sql)
119   
120   # Execute the SQL statement
121   try:
122     dbCursor.execute(sql)
123   except MySQLdb.OperationalError, e:
124     log(radiusd.L_ERR, str(e))
125     dbCursor.close()
126     return radiusd.RLM_MODULE_FAIL
127   
128   # Get the result. (sum,)
129   result = dbCursor.fetchone()
130   if (not result) or (not result[0]):
131     # No usage yet
132     secondsUsed = 0
133   else:
134     secondsUsed = result[0]
135
136   # Done with cursor
137   dbCursor.close()
138
139   # Note that MySQL returns the result of SUM() as a float.
140   sessionTimeout = maxSeconds - int(secondsUsed)
141   
142   if sessionTimeout <= 0:
143     # No more time, reject outright
144     log(radiusd.L_INFO, 'user out of time: ' + userName)    
145     return radiusd.RLM_MODULE_REJECT
146
147   # Log the success
148   log(radiusd.L_DBG, 'user accepted: %s, %d seconds' %
149       (userName, sessionTimeout))
150
151   # We are adding to the RADIUS packet
152   # Note that the session timeout integer must be converted to string.
153   # We need to set an Auth-Type.
154
155   return (radiusd.RLM_MODULE_UPDATED,
156           (('Session-Timeout', str(sessionTimeout)),),
157           (('Auth-Type', 'python'),))
158
159     
160
161 def authenticate(p):
162   p = p
163   return radiusd.RLM_MODULE_OK
164
165
166 def preacct(p):
167   p = p
168   return radiusd.RLM_MODULE_OK
169
170
171 def accounting(acctData):
172   """Accounting."""
173   # Extract the data we need.
174
175   userName = None
176   acctSessionTime = None
177   acctStatusType = None
178
179   # xxx A dict would make this nice.
180   for t in acctData:
181     if t[0] == 'User-Name':
182       userName = t[1]
183     elif t[0] == 'Acct-Session-Time':
184       acctSessionTime = t[1]
185     elif t[0] == 'Acct-Status-Type':
186       acctStatusType = t[1]
187
188
189   # We will not deal with Start for now.
190   # We may later, for simultaneous checks and the like.
191   if acctStatusType == 'Start':
192     return radiusd.RLM_MODULE_OK
193   
194   # Build and log the SQL statement
195   # radiusd puts double quotes (") around the string representation of
196   # the RADIUS packet.
197   #
198   # xxx This is simplistic as it does not record the time, etc.
199   # 
200   sql = 'insert into sessions (username, seconds) values (%s, %d)' % \
201         (userName, int(acctSessionTime))
202   
203   log(radiusd.L_DBG, sql)
204
205   # Get a cursor
206   # xxx Or should this be one cursor all throughout?
207   try:
208     dbCursor = dbHandle.cursor()
209   except MySQLdb.OperationalError, e:
210     log(radiusd.L_ERR, str(e))
211     return radiusd.RLM_MODULE_FAIL
212
213   # Execute the SQL statement
214   try:
215     dbCursor.execute(sql)
216   except MySQLdb.OperationalError, e:
217     log(radiusd.L_ERR, str(e))
218     dbCursor.close()
219     return radiusd.RLM_MODULE_FAIL
220
221   
222   return radiusd.RLM_MODULE_OK
223
224
225 def detach():
226   """Detach and clean up."""
227   # Shut down the database connection.
228   global dbHandle
229   log(radiusd.L_DBG, 'closing database handle: ' + str(dbHandle))
230   dbHandle.close()
231
232   return radiusd.RLM_MODULE_OK
233
234
235
236 # Test the modules 
237 if __name__ == '__main__':
238   instantiate(None)
239   print authorize((('User-Name', '"map"'), ('User-Password', '"abc"')))