]> jfr.im git - irc/rizon/acid.git/blob - pyva/pyva/src/main/python/moo/requests.py
Merge branch 'py-moo/fix-timeout-issue' into 'master'
[irc/rizon/acid.git] / pyva / pyva / src / main / python / moo / requests.py
1 import utils
2 from datetime import datetime
3 from fnmatch import fnmatch
4 from operator import itemgetter
5 from socket import getaddrinfo, gaierror
6 import moo_utils
7 import dns.resolver
8 import dns.exception
9
10 import pyva_net_rizon_acid_core_User as User
11
12 class Ban(object):
13 def __init__(self, row):
14 self.nick, self.reason, self.banned_by, self.date = row
15
16 class Blacklist(object):
17 def __init__(self, row):
18 self.vhost, self.added_by, self.reason, self.date = row
19
20 class Request(object):
21 def __init__(self, row):
22 self.nickname, self.main, self.vhost, self.resolved_by, self.waited_for, self.date = row
23
24 class RejectedRequest(Request):
25 def __init__(self, row):
26 Request.__init__(self, row[:-1])
27 self.reason = row[-1]
28
29 class Suspicious(object):
30 def __init__(self, row):
31 self.vhost, self.added_by, self.reason, self.date = row
32
33 URL = 'http://s.rizon.net/vhost'
34 RULES = 'For basic vhost rules and restrictions, see: %s' % URL
35
36 class RequestManager(object):
37 def __init__(self, module):
38 self.module = module
39 self.dbp = module.dbp
40 self.list = {}
41 self.banlist = {}
42
43 self.dbp.execute("""CREATE TABLE IF NOT EXISTS `moo_bans` (
44 `id` MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
45 `nickname` VARCHAR( 31 ) NOT NULL UNIQUE KEY ,
46 `reason` TEXT NULL ,
47 `banned_by` VARCHAR( 31 ) NOT NULL ,
48 `date` TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)""")
49 self.dbp.execute('SELECT nickname, reason, banned_by, date FROM moo_bans')
50 for row in self.dbp.fetchall():
51 self.banlist[row[0].lower()] = Ban(row)
52
53 self.dbp.execute("""CREATE TABLE IF NOT EXISTS `moo_requests` (
54 `id` MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
55 `nickname` VARCHAR( 31 ) NOT NULL ,
56 `main` VARCHAR ( 31 ) NOT NULL ,
57 `vhost` VARCHAR( 64 ) NOT NULL ,
58 `resolved_by` VARCHAR( 31 ) NULL DEFAULT NULL ,
59 `waited_for` MEDIUMINT UNSIGNED NOT NULL ,
60 `date` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)""")
61 self.dbp.execute('SELECT nickname, main, vhost, resolved_by, waited_for, date FROM moo_requests ORDER BY date ASC')
62 self.requests = [Request(req) for req in self.dbp.fetchall()]
63
64 self.dbp.execute("""CREATE TABLE IF NOT EXISTS `moo_rejections` (
65 `id` MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
66 `nickname` VARCHAR( 31 ) NOT NULL ,
67 `main` VARCHAR ( 31 ) NOT NULL ,
68 `vhost` VARCHAR( 64 ) NOT NULL ,
69 `resolved_by` VARCHAR( 31 ) NULL DEFAULT NULL ,
70 `reason` VARCHAR ( 512 ) NOT NULL ,
71 `waited_for` MEDIUMINT UNSIGNED NOT NULL ,
72 `date` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)""")
73 self.dbp.execute('SELECT nickname, main, vhost, resolved_by, waited_for, date, reason FROM moo_rejections ORDER BY date ASC')
74 self.rejections = [RejectedRequest(req) for req in self.dbp.fetchall()]
75
76 self.dbp.execute("""CREATE TABLE IF NOT EXISTS `moo_blacklist` (
77 `id` MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
78 `vhost` VARCHAR( 64 ) NOT NULL UNIQUE KEY ,
79 `added_by` VARCHAR( 31 ) NOT NULL ,
80 `reason` TEXT NULL ,
81 `date` TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)""")
82 self.dbp.execute('SELECT vhost, added_by, reason, date FROM moo_blacklist')
83 self.blacklist = [Blacklist(row) for row in self.dbp.fetchall()]
84
85 self.dbp.execute("""CREATE TABLE IF NOT EXISTS `moo_suspicious` (
86 `id` MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
87 `vhost` VARCHAR( 64 ) NOT NULL UNIQUE KEY ,
88 `added_by` VARCHAR( 31 ) NOT NULL ,
89 `reason` TEXT NULL ,
90 `date` TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)""")
91 self.dbp.execute('SELECT vhost, added_by, reason, date FROM moo_suspicious')
92 self.suspicious = [Suspicious(row) for row in self.dbp.fetchall()]
93
94 def add(self, vhost, nick, main=None):
95 if main:
96 last_req = self.__get_last_request(main)
97 acceptable, rating, internal = self.__verify(nick, vhost, main)
98 else: # only for requests missed during downtime
99 userinfo = User.findUser(nick)
100 if userinfo and userinfo['su']:
101 main = userinfo['su']
102 last_req = self.__get_last_request(main)
103 acceptable, rating, internal = self.__verify(nick, vhost, main)
104 else: # user no longer online or SU is empty, can't check main nick / last request
105 last_req = self.__get_last_request(nick)
106 acceptable, rating, internal = self.__verify(nick, vhost)
107
108 if not acceptable:
109 self.module.msg('HostServ', 'REJECT %s %s' % (nick, rating if rating else ''))
110 self.module.msg(self.module.chan, 'Rejected vhost for @b%(nick)s@b.%(int)s' % {
111 'nick': nick,
112 'int' : ' %s' % internal if internal else ''})
113 now = datetime.now()
114 t = (nick, main, vhost, 'py-moo', 0, now, rating)
115 r = RejectedRequest(t)
116 if r.main:
117 self.dbp.execute('INSERT INTO moo_rejections (nickname, main, vhost, resolved_by, waited_for, reason) VALUES (%s, %s, %s, %s, %s, %s)',
118 (r.nickname, r.main, r.vhost, r.resolved_by, r.waited_for, r.reason))
119 else:
120 r.main = r.nickname
121 self.rejections.append(r)
122 return
123
124 if nick.lower() in self.list:
125 if self.list[nick.lower()]['vhost'] == vhost:
126 return # don't repeat if no change
127 else:
128 id = self.list[nick.lower()]['id'] # replace the old request with the new vhost
129 else:
130 id = self.__get_id()
131
132 suspicious = self.__is_suspicious(vhost)
133 date = datetime.now()
134 self.list[nick.lower()] = {'id': id, 'vhost': vhost, 'rating': rating, 'date': date, 'main': main, 'nick': nick, 'last': last_req, 'suspicious': suspicious}
135 self.module.msg(self.module.chan, moo_utils.format_last_req(last_req, '[new] %(id)d.@o %(nick)s @sep %(vhost)s%(susp)s @sep%(lastreq)s %(extra)s' % {
136 'id' : id,
137 'nick' : nick,
138 'vhost' : vhost if not suspicious else '@c10@b[!]@o ' + vhost,
139 'susp' : ' @sep @bSuspicious@b %s' % suspicious if suspicious else '',
140 'lastreq': ' @bLast request@b %s ago as %s @sep' % (utils.get_timespan(last_req.date), last_req.nickname) if last_req else '',
141 'extra' : 'vHost resolved but its ownership was verified by the existence of a DNS TXT record' if internal == 'TXT record' else ''}))
142
143 def add_blacklist(self, vhost, by, reason=None):
144 b = (vhost, by, reason, datetime.now())
145 self.dbp.execute('INSERT INTO moo_blacklist (vhost, added_by, reason) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE added_by=%s, reason=%s',
146 b[:-1] + (by, reason))
147 for bla in self.blacklist:
148 if bla.vhost.lower() == vhost.lower():
149 self.blacklist.remove(bla)
150 break
151
152 self.blacklist.append(Blacklist(b))
153 self.module.msg(self.module.chan, '@b%s@b added @b%s@b to blacklisted vHost list' % (by, vhost))
154
155 def del_blacklist(self, vhost, by):
156 for bla in self.blacklist:
157 if bla.vhost.lower() == vhost.lower():
158 self.blacklist.remove(bla)
159 self.dbp.execute('DELETE FROM moo_blacklist WHERE vhost = %s', bla.vhost)
160 self.module.msg(self.module.chan, '@b%s@b removed @b%s@b from blacklisted vHost list' % (by, vhost))
161 return
162
163 self.module.msg(self.module.chan, '@b%s@b not found in blacklisted vHost list' % vhost)
164
165 def add_suspicious(self, vhost, by, reason=None):
166 s = (vhost, by, reason, datetime.now())
167 self.dbp.execute('INSERT INTO moo_suspicious (vhost, added_by, reason) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE added_by=%s, reason=%s',
168 s[:-1] + (by, reason))
169 for sus in self.suspicious:
170 if sus.vhost.lower() == vhost.lower():
171 self.suspicious.remove(sus)
172 break
173
174 self.suspicious.append(Suspicious(s))
175 self.module.msg(self.module.chan, '@b%s@b added @b%s@b to suspicious vHost list' % (by, vhost))
176
177 def del_suspicious(self, vhost, by):
178 for sus in self.suspicious:
179 if sus.vhost.lower() == vhost.lower():
180 self.suspicious.remove(sus)
181 self.dbp.execute('DELETE FROM moo_suspicious WHERE vhost = %s', sus.vhost)
182 self.module.msg(self.module.chan, '@b%s@b removed @b%s@b from suspicious vHost list' % (by, vhost))
183 return
184
185 self.module.msg(self.module.chan, '@b%s@b not found in suspicious vHost list' % vhost)
186
187 def __get_nick(self, list, s):
188 if s in list:
189 return s
190 else:
191 for k, v in list.iteritems():
192 if k == str(s).lower() or str(v['id']) == str(s):
193 return k
194
195 return None
196
197 def __get_last_request(self, nick):
198 nick = nick.lower()
199 for r in reversed(self.requests):
200 if r.main.lower() == nick or r.nickname.lower() == nick:
201 return r
202
203 return None
204
205 def __get_id(self):
206 id = 1
207 if not self.list:
208 return 1
209 while(True):
210 sorted_list = sorted(map(itemgetter('id'), self.list.values()))
211 for req in sorted_list:
212 if req != id:
213 return id
214
215 id = id + 1
216
217 return len(sorted_list) + 1
218
219 def approve(self, s, resolved_by, silent=False):
220 nick = self.__get_nick(self.list, s)
221 if not nick:
222 self.module.msg(self.module.chan, 'No pending vHost request for nick/id @b%s@b' % s)
223 return
224
225 req = self.list[nick]
226 now = datetime.now()
227 t = (req['nick'], req['main'], req['vhost'], resolved_by, (now - req['date']).seconds, now)
228 r = Request(t)
229 if r.main:
230 self.dbp.execute('INSERT INTO moo_requests (nickname, main, vhost, resolved_by, waited_for) VALUES (%s, %s, %s, %s, %s)', t[:-1])
231 else: #if the request was made while moo was offline and it couldn't check main nick, it will consider the requesting nick as main nick for that group
232 r.main = r.nickname
233
234 self.requests.append(r)
235 del self.list[nick]
236
237 if silent:
238 return # was already manually approved
239
240 self.module.msg('HostServ', 'ACTIVATE %s' % req['nick'])
241 self.module.msg(self.module.chan, 'Activated vhost for @b%s@b' % req['nick'])
242 # self.module.notice(nick, 'Your requested vHost has been approved. Type "/msg HostServ ON" to activate it.')
243
244 def ban(self, nick, banned_by, reason):
245 nick = nick.lower()
246 b = Ban((nick, reason, banned_by, datetime.now()))
247 self.banlist[nick.lower()] = b
248 self.dbp.execute('INSERT INTO moo_bans (nickname, reason, banned_by) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE reason=%s, banned_by=%s',
249 (nick, reason, banned_by, reason, banned_by))
250 self.module.msg('HostServ', 'DELALL %s' % nick)
251 self.module.msg(self.module.chan, '@b%s@b banned nick @b%s@b with reason @b%s@b. Deleted vHosts for all nicks in group @b%s@b.' % (
252 banned_by, nick, reason if reason else 'No reason', nick))
253
254 def list_vhosts(self, nick):
255 approved = [req for req in self.requests if req.main.lower() == nick.lower() or req.nickname.lower() == nick.lower()]
256 rejections = [req for req in self.rejections if req.main.lower() == nick.lower() or req.nickname.lower() == nick.lower()]
257 merge = sorted(approved + rejections, key=lambda x: x.date)
258 if merge:
259 self.module.msg(self.module.chan, '@b[list]@b %d entries found for nick @b%s@b:' % (len(merge), nick))
260 for n, r in enumerate(merge):
261 is_rejection = r in rejections
262 sepc = 4 if is_rejection else 3
263 ago = utils.get_timespan(r.date)
264 wait = int(r.waited_for / 60)
265 self.module.msg(self.module.chan, '@b%(num)d.@b @bNick@b %(nick)s %(sep)s %(ago)s ago %(sep)s @bvHost@b %(vhost)s %(sep)s @bResolved by@b %(appr)s in %(wait)s minute%(s)s %(sep)s%(reason)s' % {
266 'num' : n + 1,
267 'sep' : '@b@c%s::@o' % sepc,
268 'ago' : ago,
269 'nick' : r.nickname,
270 'vhost' : r.vhost,
271 'appr' : r.resolved_by,
272 'wait' : wait,
273 'reason': ' @bReason@b %(r)s %(sep)s' % {
274 'sep': '@b@c%s::@o' % sepc,
275 'r': r.reason} if is_rejection else '',
276 's' : 's' if wait != 1 else ''})
277 else:
278 self.module.msg(self.module.chan, 'No previous requests by nick @b%s@b' % nick)
279
280 def reject(self, s, resolved_by, reason=None, internal=None):
281 nick = self.__get_nick(self.list, s)
282 if not nick:
283 self.module.msg(self.module.chan, 'No pending vHost request for nick/id @b%s@b' % s)
284 return
285
286 req = self.list[nick]
287 self.module.msg('HostServ', 'REJECT %s %s' % (req['nick'], reason if reason else RULES))
288 del self.list[nick]
289 self.module.msg(self.module.chan, 'Rejected vhost for @b%(nick)s@b.%(int)s' % {
290 'nick': req['nick'],
291 'int' : ' %s' % internal if internal else ''})
292
293 now = datetime.now()
294 t = (req['nick'], req['main'], req['vhost'], resolved_by, (now - req['date']).seconds, now, reason if reason else RULES)
295 r = RejectedRequest(t)
296 if r.main:
297 self.dbp.execute('INSERT INTO moo_rejections (nickname, main, vhost, resolved_by, waited_for, reason) VALUES (%s, %s, %s, %s, %s, %s)',
298 (r.nickname, r.main, r.vhost, r.resolved_by, r.waited_for, r.reason))
299 else:
300 r.main = r.nickname
301 self.rejections.append(r)
302
303 def delete(self, nick): # remove this request from pending requests, used when manually approving/rejecting with hostserv
304 del self.list[nick.lower()]
305
306 def search(self, vhost):
307 list = [req for req in self.requests if req.vhost.lower() == vhost.lower()]
308 if list:
309 self.module.msg(self.module.chan, '@b[search]@b %d entries found for vhost @b%s@b' % (len(list), vhost))
310 for n, r in enumerate(list):
311 ago = utils.get_timespan(r.date)
312 self.module.msg(self.module.chan, '@b%(num)d.@b %(ago)s ago @sep @bNick@b %(nick)s @sep @bvHost@b %(vhost)s @sep @bApproved by@b %(appr)s @sep' % {
313 'num': n + 1,
314 'ago': ago,
315 'nick': r.nickname,
316 'vhost': r.vhost,
317 'appr': r.resolved_by})
318 else:
319 self.module.msg(self.module.chan, 'vHost @b%s@b was never requested' % vhost)
320
321 def unban(self, nick):
322 is_banned, reason, by, date = self.__is_banned(nick)
323 if not is_banned:
324 self.module.msg(self.module.chan, 'No bans found for nick @b%s@b' % nick)
325 return
326
327 self.dbp.execute('DELETE FROM moo_bans WHERE nickname = %s', (nick,))
328 del self.banlist[nick.lower()]
329 self.module.msg(self.module.chan, 'Unbanned nick @b%s@b' % nick)
330
331 def __verify(self, nick, vhost, main=None):
332 is_banned, reason, by, date = self.__is_banned(nick)
333 if is_banned:
334 return (False,
335 'You have been banned from requesting vHosts. For more information, join #services',
336 'vHost: %s. Ban reason: %s - %s ago' % (vhost, reason if reason else 'No reason', utils.get_timespan(date)))
337
338 if main and main != nick:
339 is_banned, reason, by, date = self.__is_banned(main)
340 if is_banned:
341 return (False,
342 'You have been banned from requesting vHosts%s.' % (('. Reason: %s' % reason) if reason else ''),
343 'vHost: %s. Ban reason: %s - %s ago' % (vhost, reason if reason else 'No reason', utils.get_timespan(date)))
344
345 is_blacklisted, reason, int = self.__is_blacklisted(vhost)
346 if is_blacklisted:
347 return (False, reason, int)
348
349 for req in self.list.values():
350 if main == req['main'] and nick != req['nick']:
351 return (False,
352 'You already have a pending vHost request for a grouped nickname. Please wait for it to be reviewed before requesting a new one.',
353 'Has a pending request with grouped nick %s' % req['nick'])
354
355 if vhost.replace(".", "").isdigit() and not self.__is_numeric_ip(vhost):
356 return (True, '@c9Acceptable@c', None)
357
358 is_resolvable, resolved, host = self.__is_resolvable(vhost)
359 if is_resolvable:
360 (txt_record_exists, resolving_error) = self.check_nick_in_TXT_records(vhost, nick)
361
362 if resolving_error == "timeout":
363 return (False, "Rejected vHost", "DNS Timeout when trying to check TXT record for vhost @b{}@b requested by @b{}@b.".format(
364 vhost, nick))
365 elif resolving_error:
366 return (False, "Unkown error", "The following error occured when checking TXT record for vhost @b{}@b requested by @b{}@b @sep {}.".format(
367 vhost, nick, resolving_error))
368
369 if not txt_record_exists:
370 return (False,
371 'Your vHost resolves. You can add a particular DNS TXT record to confirm the ownership of the domain and request again. %s' % RULES,
372 'vHost resolves (%s => %s)' % (host, resolved))
373 else:
374 return (True, '@c9Acceptable@c', 'TXT record')
375
376 return (True, '@c9Acceptable@c', None)
377
378 def __is_numeric_ip(self, host):
379 try:
380 parts = host.split(".")
381 return len(parts) == len([x for x in parts if 0 <= int(x) <= 255]) == 4
382 except:
383 return False
384
385 def __is_resolvable(self, host):
386 try:
387 res = getaddrinfo(host, 80)
388 if res[0][4][0] == '127.0.53.53': # https://www.icann.org/news/announcement-2-2014-08-01-en
389 return (False, None, None)
390 return (True, res[0][4][0], host)
391 except gaierror, e:
392 return (False, None, None)
393 except UnicodeError, e:
394 return (False, None, None)
395
396 def __is_banned(self, nick):
397 nick = nick.lower()
398 if nick in self.banlist:
399 b = self.banlist[nick]
400 return (True, b.reason, b.banned_by, b.date)
401
402 return (False, None, None, None)
403
404 def __is_blacklisted(self, host):
405 h = host.lower()
406 for b in self.blacklist:
407 if (fnmatch(h, b.vhost.lower())):
408 return (True, RULES, 'Host matches %s (%s)' % (b.vhost, host))
409
410 return (False, None, None)
411
412 def __is_suspicious(self, host):
413 h = host.lower()
414 for b in self.suspicious:
415 if (fnmatch(h, b.vhost.lower())):
416 return 'matches @c10%s@c%s' % (b.vhost, ' (%s)' % b.reason if b.reason else '')
417
418 return False
419
420 def check_nick_in_TXT_records(self, host, nick):
421 try:
422 answers = dns.resolver.query(host, 'TXT')
423 for answer in answers:
424 for ans in answer.strings:
425 parts = ans.split('=')
426 if len(parts) > 1 and parts[0].lower().strip() == 'rizon_vhost':
427 if parts[1].strip() == '*':
428 return (True, "")
429 nicks = [n.lower().strip() for n in parts[1].split(',')]
430 if nick.lower() in nicks:
431 return (True, "")
432 return (False, "")
433 except dns.exception.Timeout, e:
434 return (False, "timeout")
435 except Exception, e:
436 return (False, str(e))