]> jfr.im git - irc/quakenet/iauthd.git/blob - quakenet-iauthd
04f14a8282e67264144d0b8712d17472226ff3ce
[irc/quakenet/iauthd.git] / quakenet-iauthd
1 #!/usr/bin/env python
2 # Copyright (C) 2013 Gunnar Beutner
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17
18 TRUST_HOST = '127.0.0.1'
19 TRUST_PORT = 5776
20 TRUST_NAME = 'gnb.netsplit.net'
21 TRUST_PASS = 'test123'
22 BIND_HOST = '0.0.0.0'
23
24 # Do not modify beyond this point.
25
26 VERSION = 333333333333333333333333333333333
27
28 import os
29 import sys
30 import hmac
31 import fcntl
32 import asyncore
33 import socket
34 import base64
35 import resource
36 from time import time
37
38 class LineDispatcher(asyncore.dispatcher):
39 def __init__(self, fd=None, map=None):
40 asyncore.dispatcher.__init__(self, None, map)
41
42 self.in_buffer = ''
43 self.out_buffer = ''
44
45 self.send('VERSION %s\n' % (VERSION))
46
47 if fd == None:
48 return
49
50 self.connected = True
51 try:
52 fd = fd.fileno()
53 except AttributeError:
54 pass
55 self.set_file(fd)
56 # set it to non-blocking mode
57 flags = fcntl.fcntl(fd, fcntl.F_GETFL, 0)
58 flags = flags | os.O_NONBLOCK
59 fcntl.fcntl(fd, fcntl.F_SETFL, flags)
60
61 def set_file(self, fd):
62 self.socket = asyncore.file_wrapper(fd)
63 self._fileno = self.socket.fileno()
64 self.add_channel()
65
66 def handle_read(self):
67 data = self.recv(8192)
68
69 if not data:
70 return
71
72 self.in_buffer += data
73
74 while True:
75 pos = self.in_buffer.find('\n')
76
77 if pos == -1:
78 break
79
80 line = self.in_buffer[0:pos]
81 self.in_buffer = self.in_buffer[pos + 1:]
82
83 # remove new-line characters
84 line = line.rstrip('\r\n')
85
86 self.handle_line(line)
87
88 def writable(self):
89 return self.out_buffer != ''
90
91 def send(self, data):
92 self.out_buffer += data
93
94 def handle_write(self):
95 sent = 0
96 sent = asyncore.dispatcher.send(self, self.out_buffer)
97 self.out_buffer = self.out_buffer[sent:]
98
99 def log_info(self, message, type='info'):
100 if __debug__ or type != 'info':
101 sys.stderr.write('%s: %s\n' % (type, message))
102
103 class IAuthHandler(LineDispatcher):
104 def __init__(self, fin):
105 LineDispatcher.__init__(self, fin)
106
107 self.clients = {}
108 self.next_unique_id = 0
109
110 self.send("V :quakenet-iauthd\n")
111 self.send("O AU\n")
112
113 def handle_line(self, line):
114 global trust_handler
115
116 # tokenize the line according to RFC 2812
117 if line.find(' :') != -1:
118 line, trailing = line.split(' :', 1)
119 tokens = line.split()
120 tokens.append(trailing)
121 else:
122 tokens = line.split()
123
124 if len(tokens) < 3: # too few tokens
125 return
126
127 id = tokens[0]
128 command = tokens[1]
129 params = tokens[2:]
130
131 if id != '-1' and not id in self.clients:
132 self.clients[id] = {}
133
134 if command == 'C': # new client
135 if len(params) < 2: # too few parameters
136 return
137
138 self.clients[id]['remoteip'] = params[0]
139 self.clients[id]['remoteport'] = params[1]
140
141 self.clients[id]['unique_id'] = str(self.next_unique_id)
142 self.next_unique_id += 1
143 elif command == 'H': # hurry state (ircd has finished DNS/ident check)
144 if trust_handler and trust_handler.connected and trust_handler.authed:
145 trust_handler.send('CHECK %s-%s %s %s\n' %
146 (id, self.clients[id]['unique_id'], self.clients[id]['username'], self.clients[id]['remoteip']))
147 else:
148 self.send_verdict('%s-%s' % (id, self.clients[id]['unique_id']), 'PASS')
149 elif command == 'u' or command == 'U': # trusted/untrusted username
150 username = params[0]
151
152 # untrusted username (i.e. non-working identd)
153 if command == 'U':
154 username = '~' + username
155
156 if not 'username' in self.clients[id] or username[0] != '~':
157 self.clients[id]['username'] = username
158 elif command == 'D': # client disconnected
159 if id in self.clients:
160 del self.clients[id]
161
162 def send_verdict(self, combined_id, verdict, message=None):
163 global stats_passed, stats_killed
164
165 tokens = combined_id.split('-', 1)
166
167 if len(tokens) < 2:
168 return
169
170 id, unique_id = tokens
171
172 if not id in self.clients or self.clients[id]['unique_id'] != unique_id:
173 return
174
175 if verdict == 'PASS':
176 # Every 10000th accepted connection gets a free cow.
177 if stats_passed % 10000 == 0:
178 cow = [ '(__)', ' oo\\\\\\~', ' !!!!' ]
179
180 for line in cow:
181 self.send('C %s %s %s :%s\n' %
182 (id, self.clients[id]['remoteip'], self.clients[id]['remoteport'], line))
183
184 if message:
185 self.send('C %s %s %s :%s\n' %
186 (id, self.clients[id]['remoteip'], self.clients[id]['remoteport'], message))
187
188 self.send('D %s %s %s\n' %
189 (id, self.clients[id]['remoteip'], self.clients[id]['remoteport']))
190
191 stats_passed += 1
192 else:
193 if not message:
194 message = 'Connections from your host cannot be accepted at this time.'
195
196 self.send('k %s %s %s :%s\n' %
197 (id, self.clients[id]['remoteip'], self.clients[id]['remoteport'], message))
198
199 stats_killed += 1
200
201 del self.clients[id]
202
203 def send_snotice(self, message):
204 self.send('> :iauthd: %s\n' % (message))
205
206 def send_throttle(self, op, addr):
207 self.send('T %s %s\n' % (op, addr))
208
209 def clear_stats(self):
210 self.send('s\n');
211
212 def add_stats(self, message):
213 self.send('S quakenet-iauthd :%s\n' % message)
214
215 class TrustHandler(LineDispatcher):
216 def __init__(self, host, port):
217 LineDispatcher.__init__(self)
218
219 self.authed = False
220 self.connected_since = False
221
222 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
223 if BIND_HOST != '0.0.0.0': # maybe unnecessary?
224 self.bind( (BIND_HOST, 0) )
225 self.connect( (TRUST_HOST, TRUST_PORT) )
226
227 def handle_connect_event(self):
228 try:
229 LineDispatcher.handle_connect_event(self)
230
231 self.connected_since = time()
232 except:
233 iauth_handler.send_snotice('Could not connect to trusts backend: ' + str(sys.exc_info()[1]))
234
235 def handle_line(self, line):
236 global iauth_handler
237
238 tokens = line.split(' ', 1)
239
240 if tokens[0] == 'KILL' or tokens[0] == 'PASS':
241 if len(tokens) < 2:
242 return
243
244 arguments = tokens[1].split(' ', 1)
245
246 verdict = tokens[0]
247 id = arguments[0]
248 message = None
249
250 if len(arguments) > 1:
251 message = arguments[1]
252
253 iauth_handler.send_verdict(id, verdict, message)
254 elif tokens[0] == 'THROTTLE' or tokens[0] == 'UNTHROTTLE':
255 if len(tokens) < 2:
256 return
257
258 if tokens[0] == 'THROTTLE':
259 op = '+'
260 else:
261 op = '-'
262
263 addr = tokens[1]
264
265 iauth_handler.send_throttle(op, addr)
266 elif tokens[0] == 'AUTH':
267 if len(tokens) < 2:
268 return
269
270 iauth_handler.send_snotice('Received authentication request from trusts backend.')
271
272 nonce = tokens[1]
273
274 self.send('AUTH %s %s\n' %
275 (TRUST_NAME, hmac.HMAC(TRUST_PASS, nonce).hexdigest()))
276 elif tokens[0] == 'AUTHOK':
277 iauth_handler.send_snotice('Successfully authenticated with trusts backend.')
278 self.authed = True
279 elif tokens[0] == 'QUIT':
280 if len(tokens) < 2:
281 message = 'No reason specified.'
282 else:
283 message = tokens[1]
284
285 iauth_handler.send_snotice('Trusts backend closed connection: ' + message)
286 self.close()
287
288 def handle_connect(self):
289 global iauth_handler
290 iauth_handler.send_snotice('Reconnected to trusts backend.')
291
292 def handle_close(self):
293 global iauth_handler
294 iauth_handler.send_snotice('Connection to trusts backend failed.')
295
296 self.close()
297
298 iauth_handler = IAuthHandler(sys.stdin)
299 trust_handler = None
300
301 last_restart = time()
302 last_reconnect = 0
303 last_stats = 0
304 stats_passed = 0
305 stats_killed = 0
306
307 while True:
308 # Try to (re-)connect to trusts backend if necessary
309 if (not trust_handler or not trust_handler.connected) and last_reconnect + 10 < time():
310 if trust_handler:
311 trust_handler.close()
312
313 iauth_handler.send_snotice('Attempting to reconnect to trusts backend.')
314 trust_handler = TrustHandler(TRUST_HOST, TRUST_PORT)
315 last_reconnect = time()
316
317 if last_stats + 15 < time():
318 # Update statistics
319 iauth_handler.clear_stats()
320
321 iauth_handler.add_stats('Version: %s' % (VERSION))
322 iauth_handler.add_stats('Started: %s seconds ago' % (int(time() - last_restart)))
323
324 if trust_handler and trust_handler.connected and trust_handler.authed:
325 iauth_handler.add_stats('Connected to trusts backend for %s seconds.' % (int(time() - trust_handler.connected_since)))
326 else:
327 iauth_handler.add_stats('Not connected to trusts backend.')
328
329 iauth_handler.add_stats('Accepted connections: %d' % (stats_passed))
330 iauth_handler.add_stats('Rejected connections: %d' % (stats_killed))
331 iauth_handler.add_stats('Pending connections: %d' % (len(iauth_handler.clients)))
332
333 ru = resource.getrusage(resource.RUSAGE_SELF)
334
335 iauth_handler.add_stats('--')
336 iauth_handler.add_stats('Memory usage: %s kB' % (ru[2] / 1024))
337 iauth_handler.add_stats('CPU usage: %.2f%%' % ((ru[0] + ru[1]) / (time() - last_restart)))
338
339 last_stats = time()
340
341 asyncore.loop(timeout=10, count=1)