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