2 from datetime
import datetime
3 from fnmatch
import fnmatch
4 from operator
import itemgetter
5 from socket
import getaddrinfo
, gaierror
10 import pyva_net_rizon_acid_core_User
as User
13 def __init__(self
, row
):
14 self
.nick
, self
.reason
, self
.banned_by
, self
.date
= row
16 class Blacklist(object):
17 def __init__(self
, row
):
18 self
.vhost
, self
.added_by
, self
.reason
, self
.date
= row
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
24 class RejectedRequest(Request
):
25 def __init__(self
, row
):
26 Request
.__init
__(self
, row
[:-1])
29 class Suspicious(object):
30 def __init__(self
, row
):
31 self
.vhost
, self
.added_by
, self
.reason
, self
.date
= row
33 URL
= 'http://s.rizon.net/vhost'
34 RULES
= 'For basic vhost rules and restrictions, see: %s' % URL
36 class RequestManager(object):
37 def __init__(self
, module
):
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 ,
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
)
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()]
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()]
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 ,
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()]
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 ,
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()]
94 def add(self
, vhost
, nick
, main
=None):
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
)
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' % {
112 'int' : ' %s' % internal
if internal
else ''})
114 t
= (nick
, main
, vhost
, 'py-moo', 0, now
, rating
)
115 r
= RejectedRequest(t
)
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
))
121 self
.rejections
.append(r
)
124 if nick
.lower() in self
.list:
125 if self
.list[nick
.lower()]['vhost'] == vhost
:
126 return # don't repeat if no change
128 id = self
.list[nick
.lower()]['id'] # replace the old request with the new vhost
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' % {
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 ''}))
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
)
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
))
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
))
163 self
.module
.msg(self
.module
.chan
, '@b%s@b not found in blacklisted vHost list' % vhost
)
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
)
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
))
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
))
185 self
.module
.msg(self
.module
.chan
, '@b%s@b not found in suspicious vHost list' % vhost
)
187 def __get_nick(self
, list, s
):
191 for k
, v
in list.iteritems():
192 if k
== str(s
).lower() or str(v
['id']) == str(s
):
197 def __get_last_request(self
, nick
):
199 for r
in reversed(self
.requests
):
200 if r
.main
.lower() == nick
or r
.nickname
.lower() == nick
:
210 sorted_list
= sorted(map(itemgetter('id'), self
.list.values()))
211 for req
in sorted_list
:
217 return len(sorted_list
) + 1
219 def approve(self
, s
, resolved_by
, silent
=False):
220 nick
= self
.__get
_nick
(self
.list, s
)
222 self
.module
.msg(self
.module
.chan
, 'No pending vHost request for nick/id @b%s@b' % s
)
225 req
= self
.list[nick
]
227 t
= (req
['nick'], req
['main'], req
['vhost'], resolved_by
, (now
- req
['date']).seconds
, now
)
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
234 self
.requests
.append(r
)
238 return # was already manually approved
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.')
244 def ban(self
, nick
, banned_by
, reason
):
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
))
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
)
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' % {
267 'sep' : '@b@c%s::@o' % sepc
,
271 'appr' : r
.resolved_by
,
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 ''})
278 self
.module
.msg(self
.module
.chan
, 'No previous requests by nick @b%s@b' % nick
)
280 def reject(self
, s
, resolved_by
, reason
=None, internal
=None):
281 nick
= self
.__get
_nick
(self
.list, s
)
283 self
.module
.msg(self
.module
.chan
, 'No pending vHost request for nick/id @b%s@b' % s
)
286 req
= self
.list[nick
]
287 self
.module
.msg('HostServ', 'REJECT %s %s' % (req
['nick'], reason
if reason
else RULES
))
289 self
.module
.msg(self
.module
.chan
, 'Rejected vhost for @b%(nick)s@b.%(int)s' % {
291 'int' : ' %s' % internal
if internal
else ''})
294 t
= (req
['nick'], req
['main'], req
['vhost'], resolved_by
, (now
- req
['date']).seconds
, now
, reason
if reason
else RULES
)
295 r
= RejectedRequest(t
)
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
))
301 self
.rejections
.append(r
)
303 def delete(self
, nick
): # remove this request from pending requests, used when manually approving/rejecting with hostserv
304 del self
.list[nick
.lower()]
306 def search(self
, vhost
):
307 list = [req
for req
in self
.requests
if req
.vhost
.lower() == vhost
.lower()]
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' % {
317 'appr': r
.resolved_by
})
319 self
.module
.msg(self
.module
.chan
, 'vHost @b%s@b was never requested' % vhost
)
321 def unban(self
, nick
):
322 is_banned
, reason
, by
, date
= self
.__is
_banned
(nick
)
324 self
.module
.msg(self
.module
.chan
, 'No bans found for nick @b%s@b' % nick
)
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
)
331 def __verify(self
, nick
, vhost
, main
=None):
332 is_banned
, reason
, by
, date
= self
.__is
_banned
(nick
)
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
)))
338 if main
and main
!= nick
:
339 is_banned
, reason
, by
, date
= self
.__is
_banned
(main
)
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
)))
345 is_blacklisted
, reason
, int = self
.__is
_blacklisted
(vhost
)
347 return (False, reason
, int)
349 for req
in self
.list.values():
350 if main
== req
['main'] and nick
!= req
['nick']:
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'])
355 if vhost
.replace(".", "").isdigit() and not self
.__is
_numeric
_ip
(vhost
):
356 return (True, '@c9Acceptable@c', None)
358 is_resolvable
, resolved
, host
= self
.__is
_resolvable
(vhost
)
360 (txt_record_exists
, resolving_error
) = self
.check_nick_in_TXT_records(vhost
, nick
)
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(
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
))
369 if not txt_record_exists
:
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
))
374 return (True, '@c9Acceptable@c', 'TXT record')
376 return (True, '@c9Acceptable@c', None)
378 def __is_numeric_ip(self
, host
):
380 parts
= host
.split(".")
381 return len(parts
) == len([x
for x
in parts
if 0 <= int(x
) <= 255]) == 4
385 def __is_resolvable(self
, host
):
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
)
392 return (False, None, None)
393 except UnicodeError, e
:
394 return (False, None, None)
396 def __is_banned(self
, nick
):
398 if nick
in self
.banlist
:
399 b
= self
.banlist
[nick
]
400 return (True, b
.reason
, b
.banned_by
, b
.date
)
402 return (False, None, None, None)
404 def __is_blacklisted(self
, host
):
406 for b
in self
.blacklist
:
407 if (fnmatch(h
, b
.vhost
.lower())):
408 return (True, RULES
, 'Host matches %s (%s)' % (b
.vhost
, host
))
410 return (False, None, None)
412 def __is_suspicious(self
, host
):
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 '')
420 def check_nick_in_TXT_records(self
, host
, nick
):
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() == '*':
429 nicks
= [n
.lower().strip() for n
in parts
[1].split(',')]
430 if nick
.lower() in nicks
:
433 except dns
.exception
.Timeout
, e
:
434 return (False, "timeout")
436 return (False, str(e
))