1### 2# Copyright (c) 2002-2009, Jeremiah Fincher 3# Copyright (c) 2011, Valentin Lorentz 4# Copyright (c) 2009,2013, James McCoy 5# All rights reserved. 6# 7# Redistribution and use in source and binary forms, with or without 8# modification, are permitted provided that the following conditions are met: 9# 10# * Redistributions of source code must retain the above copyright notice, 11# this list of conditions, and the following disclaimer. 12# * Redistributions in binary form must reproduce the above copyright notice, 13# this list of conditions, and the following disclaimer in the 14# documentation and/or other materials provided with the distribution. 15# * Neither the name of the author of this software nor the name of 16# contributors to this software may be used to endorse or promote products 17# derived from this software without specific prior written consent. 18# 19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 23# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29# POSSIBILITY OF SUCH DAMAGE. 30### 31 32import os 33import time 34import operator 35 36from . import conf, ircutils, log, registry, unpreserve, utils, world 37from .utils import minisix 38 39def isCapability(capability): 40 return len(capability.split(None, 1)) == 1 41 42def fromChannelCapability(capability): 43 """Returns a (channel, capability) tuple from a channel capability.""" 44 assert isChannelCapability(capability), 'got %s' % capability 45 return capability.split(',', 1) 46 47def isChannelCapability(capability): 48 """Returns True if capability is a channel capability; False otherwise.""" 49 if ',' in capability: 50 (channel, capability) = capability.split(',', 1) 51 return ircutils.isChannel(channel) and isCapability(capability) 52 else: 53 return False 54 55def makeChannelCapability(channel, capability): 56 """Makes a channel capability given a channel and a capability.""" 57 assert isCapability(capability), 'got %s' % capability 58 assert ircutils.isChannel(channel), 'got %s' % channel 59 return '%s,%s' % (channel, capability) 60 61def isAntiCapability(capability): 62 """Returns True if capability is an anticapability; False otherwise.""" 63 if isChannelCapability(capability): 64 (_, capability) = fromChannelCapability(capability) 65 return isCapability(capability) and capability[0] == '-' 66 67def makeAntiCapability(capability): 68 """Returns the anticapability of a given capability.""" 69 assert isCapability(capability), 'got %s' % capability 70 assert not isAntiCapability(capability), \ 71 'makeAntiCapability does not work on anticapabilities. ' \ 72 'You probably want invertCapability; got %s.' % capability 73 if isChannelCapability(capability): 74 (channel, capability) = fromChannelCapability(capability) 75 return makeChannelCapability(channel, '-' + capability) 76 else: 77 return '-' + capability 78 79def unAntiCapability(capability): 80 """Takes an anticapability and returns the non-anti form.""" 81 assert isCapability(capability), 'got %s' % capability 82 if not isAntiCapability(capability): 83 raise ValueError('%s is not an anti capability' % capability) 84 if isChannelCapability(capability): 85 (channel, capability) = fromChannelCapability(capability) 86 return ','.join((channel, capability[1:])) 87 else: 88 return capability[1:] 89 90def invertCapability(capability): 91 """Make a capability into an anticapability and vice versa.""" 92 assert isCapability(capability), 'got %s' % capability 93 if isAntiCapability(capability): 94 return unAntiCapability(capability) 95 else: 96 return makeAntiCapability(capability) 97 98def canonicalCapability(capability): 99 if callable(capability): 100 capability = capability() 101 assert isCapability(capability), 'got %s' % capability 102 return capability.lower() 103 104_unwildcard_remover = utils.str.MultipleRemover('!@*?') 105def unWildcardHostmask(hostmask): 106 return _unwildcard_remover(hostmask) 107 108_invert = invertCapability 109class CapabilitySet(set): 110 """A subclass of set handling basic capability stuff.""" 111 __slots__ = ('__parent',) 112 def __init__(self, capabilities=()): 113 self.__parent = super(CapabilitySet, self) 114 self.__parent.__init__() 115 for capability in capabilities: 116 self.add(capability) 117 118 def add(self, capability): 119 """Adds a capability to the set.""" 120 capability = ircutils.toLower(capability) 121 inverted = _invert(capability) 122 if self.__parent.__contains__(inverted): 123 self.__parent.remove(inverted) 124 self.__parent.add(capability) 125 126 def remove(self, capability): 127 """Removes a capability from the set.""" 128 capability = ircutils.toLower(capability) 129 self.__parent.remove(capability) 130 131 def __contains__(self, capability): 132 capability = ircutils.toLower(capability) 133 if self.__parent.__contains__(capability): 134 return True 135 if self.__parent.__contains__(_invert(capability)): 136 return True 137 else: 138 return False 139 140 def check(self, capability, ignoreOwner=False): 141 """Returns the appropriate boolean for whether a given capability is 142 'allowed' given its (or its anticapability's) presence in the set. 143 """ 144 capability = ircutils.toLower(capability) 145 if self.__parent.__contains__(capability): 146 return True 147 elif self.__parent.__contains__(_invert(capability)): 148 return False 149 else: 150 raise KeyError 151 152 def __repr__(self): 153 return '%s([%s])' % (self.__class__.__name__, 154 ', '.join(map(repr, self))) 155 156antiOwner = makeAntiCapability('owner') 157class UserCapabilitySet(CapabilitySet): 158 """A subclass of CapabilitySet to handle the owner capability correctly.""" 159 __slots__ = ('__parent',) 160 def __init__(self, *args, **kwargs): 161 self.__parent = super(UserCapabilitySet, self) 162 self.__parent.__init__(*args, **kwargs) 163 164 def __contains__(self, capability, ignoreOwner=False): 165 capability = ircutils.toLower(capability) 166 if not ignoreOwner and capability == 'owner' or capability == antiOwner: 167 return True 168 elif not ignoreOwner and self.__parent.__contains__('owner'): 169 return True 170 else: 171 return self.__parent.__contains__(capability) 172 173 def check(self, capability, ignoreOwner=False): 174 """Returns the appropriate boolean for whether a given capability is 175 'allowed' given its (or its anticapability's) presence in the set. 176 Differs from CapabilitySet in that it handles the 'owner' capability 177 appropriately. 178 """ 179 capability = ircutils.toLower(capability) 180 if capability == 'owner' or capability == antiOwner: 181 if self.__parent.__contains__('owner'): 182 return not isAntiCapability(capability) 183 else: 184 return isAntiCapability(capability) 185 elif not ignoreOwner and self.__parent.__contains__('owner'): 186 if isAntiCapability(capability): 187 return False 188 else: 189 return True 190 else: 191 return self.__parent.check(capability) 192 193 def add(self, capability): 194 """Adds a capability to the set. Just make sure it's not -owner.""" 195 capability = ircutils.toLower(capability) 196 assert capability != '-owner', '"-owner" disallowed.' 197 self.__parent.add(capability) 198 199class IrcUser(object): 200 """This class holds the capabilities and authentications for a user.""" 201 __slots__ = ('id', 'auth', 'name', 'ignore', 'secure', 'hashed', 202 'password', 'capabilities', 'hostmasks', 'nicks', 'gpgkeys') 203 def __init__(self, ignore=False, password='', name='', 204 capabilities=(), hostmasks=None, nicks=None, 205 secure=False, hashed=False): 206 self.id = None 207 self.auth = [] # The (time, hostmask) list of auth crap. 208 self.name = name # The name of the user. 209 self.ignore = ignore # A boolean deciding if the person is ignored. 210 self.secure = secure # A boolean describing if hostmasks *must* match. 211 self.hashed = hashed # True if the password is hashed on disk. 212 self.password = password # password (plaintext? hashed?) 213 self.capabilities = UserCapabilitySet() 214 for capability in capabilities: 215 self.capabilities.add(capability) 216 if hostmasks is None: 217 self.hostmasks = ircutils.IrcSet() # hostmasks used for recognition 218 else: 219 self.hostmasks = hostmasks 220 if nicks is None: 221 self.nicks = {} # {'network1': ['foo', 'bar'], 'network': ['baz']} 222 else: 223 self.nicks = nicks 224 self.gpgkeys = [] # GPG key ids 225 226 def __repr__(self): 227 return format('%s(id=%s, ignore=%s, password="", name=%q, hashed=%r, ' 228 'capabilities=%r, hostmasks=[], secure=%r)\n', 229 self.__class__.__name__, self.id, self.ignore, 230 self.name, self.hashed, self.capabilities, self.secure) 231 232 def __hash__(self): 233 return hash(self.id) 234 235 def addCapability(self, capability): 236 """Gives the user the given capability.""" 237 self.capabilities.add(capability) 238 239 def removeCapability(self, capability): 240 """Takes from the user the given capability.""" 241 self.capabilities.remove(capability) 242 243 def _checkCapability(self, capability, ignoreOwner=False): 244 """Checks the user for a given capability.""" 245 if self.ignore: 246 if isAntiCapability(capability): 247 return True 248 else: 249 return False 250 else: 251 return self.capabilities.check(capability, ignoreOwner=ignoreOwner) 252 253 def setPassword(self, password, hashed=False): 254 """Sets the user's password.""" 255 if hashed or self.hashed: 256 self.hashed = True 257 self.password = utils.saltHash(password) 258 else: 259 self.password = password 260 261 def checkPassword(self, password): 262 """Checks the user's password.""" 263 if password is None: 264 return False 265 if self.hashed: 266 (salt, _) = self.password.split('|') 267 return (self.password == utils.saltHash(password, salt=salt)) 268 else: 269 return (self.password == password) 270 271 def checkHostmask(self, hostmask, useAuth=True): 272 """Checks a given hostmask against the user's hostmasks or current 273 authentication. If useAuth is False, only checks against the user's 274 hostmasks. 275 """ 276 if useAuth: 277 timeout = conf.supybot.databases.users.timeoutIdentification() 278 removals = [] 279 try: 280 for (when, authmask) in self.auth: 281 if timeout and when+timeout < time.time(): 282 removals.append((when, authmask)) 283 elif hostmask == authmask: 284 return True 285 finally: 286 while removals: 287 self.auth.remove(removals.pop()) 288 for pat in self.hostmasks: 289 if ircutils.hostmaskPatternEqual(pat, hostmask): 290 return pat 291 return False 292 293 def addHostmask(self, hostmask): 294 """Adds a hostmask to the user's hostmasks.""" 295 assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask 296 if len(unWildcardHostmask(hostmask)) < 3: 297 raise ValueError('Hostmask must contain at least 3 non-wildcard characters.') 298 self.hostmasks.add(hostmask) 299 300 def removeHostmask(self, hostmask): 301 """Removes a hostmask from the user's hostmasks.""" 302 self.hostmasks.remove(hostmask) 303 304 def checkNick(self, network, nick): 305 """Checks a given nick against the user's nicks.""" 306 return nick in self.nicks[network] 307 308 def addNick(self, network, nick): 309 """Adds a nick to the user's registered nicks on the network.""" 310 global users 311 assert isinstance(network, minisix.string_types) 312 assert ircutils.isNick(nick), 'got %s' % nick 313 if users.getUserFromNick(network, nick) is not None: 314 raise KeyError 315 if network not in self.nicks: 316 self.nicks[network] = [] 317 if nick not in self.nicks[network]: 318 self.nicks[network].append(nick) 319 320 def removeNick(self, network, nick): 321 """Removes a nick from the user's registered nicks on the network.""" 322 assert isinstance(network, minisix.string_types) 323 if nick not in self.nicks[network]: 324 raise KeyError 325 self.nicks[network].remove(nick) 326 327 def addAuth(self, hostmask): 328 """Sets a user's authenticated hostmask. This times out according to 329 conf.supybot.timeoutIdentification. If hostmask exactly matches an 330 existing, known hostmask, the previous entry is removed.""" 331 if self.checkHostmask(hostmask, useAuth=False) or not self.secure: 332 self.auth.append((time.time(), hostmask)) 333 knownHostmasks = set() 334 def uniqueHostmask(auth): 335 (_, mask) = auth 336 if mask not in knownHostmasks: 337 knownHostmasks.add(mask) 338 return True 339 return False 340 uniqued = list(filter(uniqueHostmask, reversed(self.auth))) 341 self.auth = list(reversed(uniqued)) 342 else: 343 raise ValueError('secure flag set, unmatched hostmask') 344 345 def clearAuth(self): 346 """Unsets a user's authenticated hostmask.""" 347 for (when, hostmask) in self.auth: 348 users.invalidateCache(hostmask=hostmask) 349 self.auth = [] 350 351 def preserve(self, fd, indent=''): 352 def write(s): 353 fd.write(indent) 354 fd.write(s) 355 fd.write(os.linesep) 356 write('name %s' % self.name) 357 write('ignore %s' % self.ignore) 358 write('secure %s' % self.secure) 359 if self.password: 360 write('hashed %s' % self.hashed) 361 write('password %s' % self.password) 362 for capability in self.capabilities: 363 write('capability %s' % capability) 364 for hostmask in self.hostmasks: 365 write('hostmask %s' % hostmask) 366 for network, nicks in self.nicks.items(): 367 write('nicks %s %s' % (network, ' '.join(nicks))) 368 for key in self.gpgkeys: 369 write('gpgkey %s' % key) 370 fd.write(os.linesep) 371 372 373class IrcChannel(object): 374 """This class holds the capabilities, bans, and ignores of a channel.""" 375 __slots__ = ('defaultAllow', 'expiredBans', 'bans', 'ignores', 'silences', 376 'exceptions', 'capabilities', 'lobotomized') 377 defaultOff = ('op', 'halfop', 'voice', 'protected') 378 def __init__(self, bans=None, silences=None, exceptions=None, ignores=None, 379 capabilities=None, lobotomized=False, defaultAllow=True): 380 self.defaultAllow = defaultAllow 381 self.expiredBans = [] 382 self.bans = bans or {} 383 self.ignores = ignores or {} 384 self.silences = silences or [] 385 self.exceptions = exceptions or [] 386 self.capabilities = capabilities or CapabilitySet() 387 for capability in self.defaultOff: 388 if capability not in self.capabilities: 389 self.capabilities.add(makeAntiCapability(capability)) 390 self.lobotomized = lobotomized 391 392 def __repr__(self): 393 return '%s(bans=%r, ignores=%r, capabilities=%r, ' \ 394 'lobotomized=%r, defaultAllow=%s, ' \ 395 'silences=%r, exceptions=%r)\n' % \ 396 (self.__class__.__name__, self.bans, self.ignores, 397 self.capabilities, self.lobotomized, 398 self.defaultAllow, self.silences, self.exceptions) 399 400 def addBan(self, hostmask, expiration=0): 401 """Adds a ban to the channel banlist.""" 402 assert not conf.supybot.protocols.irc.strictRfc() or \ 403 ircutils.isUserHostmask(hostmask), 'got %s' % hostmask 404 self.bans[hostmask] = int(expiration) 405 406 def removeBan(self, hostmask): 407 """Removes a ban from the channel banlist.""" 408 assert not conf.supybot.protocols.irc.strictRfc() or \ 409 ircutils.isUserHostmask(hostmask), 'got %s' % hostmask 410 return self.bans.pop(hostmask) 411 412 def checkBan(self, hostmask): 413 """Checks whether a given hostmask is banned by the channel banlist.""" 414 assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask 415 now = time.time() 416 for (pattern, expiration) in list(self.bans.items()): 417 if now < expiration or not expiration: 418 if ircutils.hostmaskPatternEqual(pattern, hostmask): 419 return True 420 else: 421 self.expiredBans.append((pattern, expiration)) 422 del self.bans[pattern] 423 return False 424 425 def addIgnore(self, hostmask, expiration=0): 426 """Adds an ignore to the channel ignore list.""" 427 assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask 428 self.ignores[hostmask] = int(expiration) 429 430 def removeIgnore(self, hostmask): 431 """Removes an ignore from the channel ignore list.""" 432 assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask 433 return self.ignores.pop(hostmask) 434 435 def addCapability(self, capability): 436 """Adds a capability to the channel's default capabilities.""" 437 assert isCapability(capability), 'got %s' % capability 438 self.capabilities.add(capability) 439 440 def removeCapability(self, capability): 441 """Removes a capability from the channel's default capabilities.""" 442 assert isCapability(capability), 'got %s' % capability 443 self.capabilities.remove(capability) 444 445 def setDefaultCapability(self, b): 446 """Sets the default capability in the channel.""" 447 self.defaultAllow = b 448 449 def _checkCapability(self, capability, ignoreOwner=False): 450 """Checks whether a certain capability is allowed by the channel.""" 451 assert isCapability(capability), 'got %s' % capability 452 if capability in self.capabilities: 453 return self.capabilities.check(capability, ignoreOwner=ignoreOwner) 454 else: 455 if isAntiCapability(capability): 456 return not self.defaultAllow 457 else: 458 return self.defaultAllow 459 460 def checkIgnored(self, hostmask): 461 """Checks whether a given hostmask is to be ignored by the channel.""" 462 if self.lobotomized: 463 return True 464 if world.testing: 465 return False 466 assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask 467 if self.checkBan(hostmask): 468 return True 469 now = time.time() 470 for (pattern, expiration) in list(self.ignores.items()): 471 if now < expiration or not expiration: 472 if ircutils.hostmaskPatternEqual(pattern, hostmask): 473 return True 474 else: 475 del self.ignores[pattern] 476 # Later we may wish to keep expiredIgnores, but not now. 477 return False 478 479 def preserve(self, fd, indent=''): 480 def write(s): 481 fd.write(indent) 482 fd.write(s) 483 fd.write(os.linesep) 484 write('lobotomized %s' % self.lobotomized) 485 write('defaultAllow %s' % self.defaultAllow) 486 for capability in self.capabilities: 487 write('capability ' + capability) 488 bans = list(self.bans.items()) 489 utils.sortBy(operator.itemgetter(1), bans) 490 for (ban, expiration) in bans: 491 write('ban %s %d' % (ban, expiration)) 492 ignores = list(self.ignores.items()) 493 utils.sortBy(operator.itemgetter(1), ignores) 494 for (ignore, expiration) in ignores: 495 write('ignore %s %d' % (ignore, expiration)) 496 fd.write(os.linesep) 497 498 499class Creator(object): 500 __slots__ = () 501 def badCommand(self, command, rest, lineno): 502 raise ValueError('Invalid command on line %s: %s' % (lineno, command)) 503 504class IrcUserCreator(Creator): 505 __slots__ = ('users') 506 u = None 507 def __init__(self, users): 508 if self.u is None: 509 IrcUserCreator.u = IrcUser() 510 self.users = users 511 512 def user(self, rest, lineno): 513 if self.u.id is not None: 514 raise ValueError('Unexpected user command on line %s.' % lineno) 515 self.u.id = int(rest) 516 517 def _checkId(self): 518 if self.u.id is None: 519 raise ValueError('Unexpected user description without user.') 520 521 def name(self, rest, lineno): 522 self._checkId() 523 self.u.name = rest 524 525 def ignore(self, rest, lineno): 526 self._checkId() 527 self.u.ignore = bool(utils.gen.safeEval(rest)) 528 529 def secure(self, rest, lineno): 530 self._checkId() 531 self.u.secure = bool(utils.gen.safeEval(rest)) 532 533 def hashed(self, rest, lineno): 534 self._checkId() 535 self.u.hashed = bool(utils.gen.safeEval(rest)) 536 537 def password(self, rest, lineno): 538 self._checkId() 539 self.u.password = rest 540 541 def hostmask(self, rest, lineno): 542 self._checkId() 543 self.u.hostmasks.add(rest) 544 545 def nicks(self, rest, lineno): 546 self._checkId() 547 network, nicks = rest.split(' ', 1) 548 self.u.nicks[network] = nicks.split(' ') 549 550 def capability(self, rest, lineno): 551 self._checkId() 552 self.u.capabilities.add(rest) 553 554 def gpgkey(self, rest, lineno): 555 self._checkId() 556 self.u.gpgkeys.append(rest) 557 558 def finish(self): 559 if self.u.name: 560 try: 561 self.users.setUser(self.u) 562 except DuplicateHostmask: 563 log.error('Hostmasks for %s collided with another user\'s. ' 564 'Resetting hostmasks for %s.', 565 self.u.name, self.u.name) 566 # Some might argue that this is arbitrary, and perhaps it is. 567 # But we've got to do *something*, so we'll show some deference 568 # to our lower-numbered users. 569 self.u.hostmasks.clear() 570 self.users.setUser(self.u) 571 IrcUserCreator.u = None 572 573class IrcChannelCreator(Creator): 574 __slots__ = ('c', 'channels', 'hadChannel') 575 name = None 576 def __init__(self, channels): 577 self.c = IrcChannel() 578 self.channels = channels 579 self.hadChannel = bool(self.name) 580 581 def channel(self, rest, lineno): 582 if self.name is not None: 583 raise ValueError('Unexpected channel command on line %s' % lineno) 584 IrcChannelCreator.name = rest 585 586 def _checkId(self): 587 if self.name is None: 588 raise ValueError('Unexpected channel description without channel.') 589 590 def lobotomized(self, rest, lineno): 591 self._checkId() 592 self.c.lobotomized = bool(utils.gen.safeEval(rest)) 593 594 def defaultallow(self, rest, lineno): 595 self._checkId() 596 self.c.defaultAllow = bool(utils.gen.safeEval(rest)) 597 598 def capability(self, rest, lineno): 599 self._checkId() 600 self.c.capabilities.add(rest) 601 602 def ban(self, rest, lineno): 603 self._checkId() 604 (pattern, expiration) = rest.split() 605 self.c.bans[pattern] = int(float(expiration)) 606 607 def ignore(self, rest, lineno): 608 self._checkId() 609 (pattern, expiration) = rest.split() 610 self.c.ignores[pattern] = int(float(expiration)) 611 612 def finish(self): 613 if self.hadChannel: 614 self.channels.setChannel(self.name, self.c) 615 IrcChannelCreator.name = None 616 617 618class DuplicateHostmask(ValueError): 619 pass 620 621class UsersDictionary(utils.IterableMap): 622 """A simple serialized-to-file User Database.""" 623 __slots__ = ('noFlush', 'filename', 'users', '_nameCache', 624 '_hostmaskCache') 625 def __init__(self): 626 self.noFlush = False 627 self.filename = None 628 self.users = {} 629 self.nextId = 0 630 self._nameCache = utils.structures.CacheDict(1000) 631 self._hostmaskCache = utils.structures.CacheDict(1000) 632 633 # This is separate because the Creator has to access our instance. 634 def open(self, filename): 635 self.filename = filename 636 reader = unpreserve.Reader(IrcUserCreator, self) 637 try: 638 self.noFlush = True 639 try: 640 reader.readFile(filename) 641 self.noFlush = False 642 self.flush() 643 except EnvironmentError as e: 644 log.error('Invalid user dictionary file, resetting to empty.') 645 log.error('Exact error: %s', utils.exnToString(e)) 646 except Exception as e: 647 log.exception('Exact error:') 648 finally: 649 self.noFlush = False 650 651 def reload(self): 652 """Reloads the database from its file.""" 653 self.nextId = 0 654 self.users.clear() 655 self._nameCache.clear() 656 self._hostmaskCache.clear() 657 if self.filename is not None: 658 try: 659 self.open(self.filename) 660 except EnvironmentError as e: 661 log.warning('UsersDictionary.reload failed: %s', e) 662 else: 663 log.error('UsersDictionary.reload called with no filename.') 664 665 def flush(self): 666 """Flushes the database to its file.""" 667 if not self.noFlush: 668 if self.filename is not None: 669 L = list(self.users.items()) 670 L.sort() 671 fd = utils.file.AtomicFile(self.filename) 672 for (id, u) in L: 673 fd.write('user %s' % id) 674 fd.write(os.linesep) 675 u.preserve(fd, indent=' ') 676 fd.close() 677 else: 678 log.error('UsersDictionary.flush called with no filename.') 679 else: 680 log.debug('Not flushing UsersDictionary because of noFlush.') 681 682 def close(self): 683 self.flush() 684 if self.flush in world.flushers: 685 world.flushers.remove(self.flush) 686 self.users.clear() 687 688 def items(self): 689 return self.users.items() 690 691 def getUserId(self, s): 692 """Returns the user ID of a given name or hostmask.""" 693 if ircutils.isUserHostmask(s): 694 try: 695 return self._hostmaskCache[s] 696 except KeyError: 697 ids = {} 698 for (id, user) in self.users.items(): 699 x = user.checkHostmask(s) 700 if x: 701 ids[id] = x 702 if len(ids) == 1: 703 id = list(ids.keys())[0] 704 self._hostmaskCache[s] = id 705 try: 706 self._hostmaskCache[id].add(s) 707 except KeyError: 708 self._hostmaskCache[id] = set([s]) 709 return id 710 elif len(ids) == 0: 711 raise KeyError(s) 712 else: 713 log.error('Multiple matches found in user database. ' 714 'Removing the offending hostmasks.') 715 for (id, hostmask) in ids.items(): 716 log.error('Removing %q from user %s.', hostmask, id) 717 self.users[id].removeHostmask(hostmask) 718 raise DuplicateHostmask('Ids %r matched.' % ids) 719 else: # Not a hostmask, must be a name. 720 s = s.lower() 721 try: 722 return self._nameCache[s] 723 except KeyError: 724 for (id, user) in self.users.items(): 725 if s == user.name.lower(): 726 self._nameCache[s] = id 727 self._nameCache[id] = s 728 return id 729 else: 730 raise KeyError(s) 731 732 def getUser(self, id): 733 """Returns a user given its id, name, or hostmask.""" 734 if not isinstance(id, int): 735 # Must be a string. Get the UserId first. 736 id = self.getUserId(id) 737 u = self.users[id] 738 while isinstance(u, int): 739 id = u 740 u = self.users[id] 741 u.id = id 742 return u 743 744 def getUserFromNick(self, network, nick): 745 """Return a user given its nick.""" 746 for user in self.users.values(): 747 try: 748 if nick in user.nicks[network]: 749 return user 750 except KeyError: 751 pass 752 return None 753 754 def hasUser(self, id): 755 """Returns the database has a user given its id, name, or hostmask.""" 756 try: 757 self.getUser(id) 758 return True 759 except KeyError: 760 return False 761 762 def numUsers(self): 763 return len(self.users) 764 765 def invalidateCache(self, id=None, hostmask=None, name=None): 766 if hostmask is not None: 767 if hostmask in self._hostmaskCache: 768 id = self._hostmaskCache.pop(hostmask) 769 self._hostmaskCache[id].remove(hostmask) 770 if not self._hostmaskCache[id]: 771 del self._hostmaskCache[id] 772 if name is not None: 773 del self._nameCache[self._nameCache[id]] 774 del self._nameCache[id] 775 if id is not None: 776 if id in self._nameCache: 777 del self._nameCache[self._nameCache[id]] 778 del self._nameCache[id] 779 if id in self._hostmaskCache: 780 for hostmask in self._hostmaskCache[id]: 781 del self._hostmaskCache[hostmask] 782 del self._hostmaskCache[id] 783 784 def setUser(self, user, flush=True): 785 """Sets a user (given its id) to the IrcUser given it.""" 786 self.nextId = max(self.nextId, user.id) 787 try: 788 if self.getUserId(user.name) != user.id: 789 raise DuplicateHostmask(user.name, user.name) 790 except KeyError: 791 pass 792 for hostmask in user.hostmasks: 793 for (i, u) in self.items(): 794 if i == user.id: 795 continue 796 elif u.checkHostmask(hostmask): 797 # We used to remove the hostmask here, but it's not 798 # appropriate for us both to remove the hostmask and to 799 # raise an exception. So instead, we'll raise an 800 # exception, but be nice and give the offending hostmask 801 # back at the same time. 802 raise DuplicateHostmask(u.name, hostmask) 803 for otherHostmask in u.hostmasks: 804 if ircutils.hostmaskPatternEqual(hostmask, otherHostmask): 805 raise DuplicateHostmask(u.name, hostmask) 806 self.invalidateCache(user.id) 807 self.users[user.id] = user 808 if flush: 809 self.flush() 810 811 def delUser(self, id): 812 """Removes a user from the database.""" 813 del self.users[id] 814 if id in self._nameCache: 815 del self._nameCache[self._nameCache[id]] 816 del self._nameCache[id] 817 if id in self._hostmaskCache: 818 for hostmask in list(self._hostmaskCache[id]): 819 del self._hostmaskCache[hostmask] 820 del self._hostmaskCache[id] 821 self.flush() 822 823 def newUser(self): 824 """Allocates a new user in the database and returns it and its id.""" 825 user = IrcUser(hashed=True) 826 self.nextId += 1 827 id = self.nextId 828 self.users[id] = user 829 self.flush() 830 user.id = id 831 return user 832 833 834class ChannelsDictionary(utils.IterableMap): 835 __slots__ = ('noFlush', 'filename', 'channels') 836 def __init__(self): 837 self.noFlush = False 838 self.filename = None 839 self.channels = ircutils.IrcDict() 840 841 def open(self, filename): 842 self.noFlush = True 843 try: 844 self.filename = filename 845 reader = unpreserve.Reader(IrcChannelCreator, self) 846 try: 847 reader.readFile(filename) 848 self.noFlush = False 849 self.flush() 850 except EnvironmentError as e: 851 log.error('Invalid channel database, resetting to empty.') 852 log.error('Exact error: %s', utils.exnToString(e)) 853 except Exception as e: 854 log.error('Invalid channel database, resetting to empty.') 855 log.exception('Exact error:') 856 finally: 857 self.noFlush = False 858 859 def flush(self): 860 """Flushes the channel database to its file.""" 861 if not self.noFlush: 862 if self.filename is not None: 863 fd = utils.file.AtomicFile(self.filename) 864 for (channel, c) in self.channels.items(): 865 fd.write('channel %s' % channel) 866 fd.write(os.linesep) 867 c.preserve(fd, indent=' ') 868 fd.close() 869 else: 870 log.warning('ChannelsDictionary.flush without self.filename.') 871 else: 872 log.debug('Not flushing ChannelsDictionary because of noFlush.') 873 874 def close(self): 875 self.flush() 876 if self.flush in world.flushers: 877 world.flushers.remove(self.flush) 878 self.channels.clear() 879 880 def reload(self): 881 """Reloads the channel database from its file.""" 882 if self.filename is not None: 883 self.channels.clear() 884 try: 885 self.open(self.filename) 886 except EnvironmentError as e: 887 log.warning('ChannelsDictionary.reload failed: %s', e) 888 else: 889 log.warning('ChannelsDictionary.reload without self.filename.') 890 891 def getChannel(self, channel): 892 """Returns an IrcChannel object for the given channel.""" 893 channel = channel.lower() 894 if channel in self.channels: 895 return self.channels[channel] 896 else: 897 c = IrcChannel() 898 self.channels[channel] = c 899 return c 900 901 def setChannel(self, channel, ircChannel): 902 """Sets a given channel to the IrcChannel object given.""" 903 channel = channel.lower() 904 self.channels[channel] = ircChannel 905 self.flush() 906 907 def items(self): 908 return self.channels.items() 909 910 911class IgnoresDB(object): 912 __slots__ = ('filename', 'hostmasks') 913 def __init__(self): 914 self.filename = None 915 self.hostmasks = {} 916 917 def open(self, filename): 918 self.filename = filename 919 fd = open(self.filename) 920 for line in utils.file.nonCommentNonEmptyLines(fd): 921 try: 922 line = line.rstrip('\r\n') 923 L = line.split() 924 hostmask = L.pop(0) 925 if L: 926 expiration = int(float(L.pop(0))) 927 else: 928 expiration = 0 929 self.add(hostmask, expiration) 930 except Exception: 931 log.error('Invalid line in ignores database: %q', line) 932 fd.close() 933 934 def flush(self): 935 if self.filename is not None: 936 fd = utils.file.AtomicFile(self.filename) 937 now = time.time() 938 for (hostmask, expiration) in self.hostmasks.items(): 939 if now < expiration or not expiration: 940 fd.write('%s %s' % (hostmask, expiration)) 941 fd.write(os.linesep) 942 fd.close() 943 else: 944 log.warning('IgnoresDB.flush called without self.filename.') 945 946 def close(self): 947 if self.flush in world.flushers: 948 world.flushers.remove(self.flush) 949 self.flush() 950 self.hostmasks.clear() 951 952 def reload(self): 953 if self.filename is not None: 954 oldhostmasks = self.hostmasks.copy() 955 self.hostmasks.clear() 956 try: 957 self.open(self.filename) 958 except EnvironmentError as e: 959 log.warning('IgnoresDB.reload failed: %s', e) 960 # Let's be somewhat transactional. 961 self.hostmasks.update(oldhostmasks) 962 else: 963 log.warning('IgnoresDB.reload called without self.filename.') 964 965 def checkIgnored(self, prefix): 966 now = time.time() 967 for (hostmask, expiration) in list(self.hostmasks.items()): 968 if expiration and now > expiration: 969 del self.hostmasks[hostmask] 970 else: 971 if ircutils.hostmaskPatternEqual(hostmask, prefix): 972 return True 973 return False 974 975 def add(self, hostmask, expiration=0): 976 assert ircutils.isUserHostmask(hostmask), 'got %s' % hostmask 977 self.hostmasks[hostmask] = expiration 978 979 def remove(self, hostmask): 980 del self.hostmasks[hostmask] 981 982 983confDir = conf.supybot.directories.conf() 984try: 985 userFile = os.path.join(confDir, conf.supybot.databases.users.filename()) 986 users = UsersDictionary() 987 users.open(userFile) 988except EnvironmentError as e: 989 log.warning('Couldn\'t open user database: %s', e) 990 991try: 992 channelFile = os.path.join(confDir, 993 conf.supybot.databases.channels.filename()) 994 channels = ChannelsDictionary() 995 channels.open(channelFile) 996except EnvironmentError as e: 997 log.warning('Couldn\'t open channel database: %s', e) 998 999try: 1000 ignoreFile = os.path.join(confDir, 1001 conf.supybot.databases.ignores.filename()) 1002 ignores = IgnoresDB() 1003 ignores.open(ignoreFile) 1004except EnvironmentError as e: 1005 log.warning('Couldn\'t open ignore database: %s', e) 1006 1007 1008world.flushers.append(users.flush) 1009world.flushers.append(ignores.flush) 1010world.flushers.append(channels.flush) 1011 1012 1013### 1014# Useful functions for checking credentials. 1015### 1016def checkIgnored(hostmask, recipient='', users=users, channels=channels): 1017 """checkIgnored(hostmask, recipient='') -> True/False 1018 1019 Checks if the user is ignored by the recipient of the message. 1020 """ 1021 try: 1022 id = users.getUserId(hostmask) 1023 user = users.getUser(id) 1024 if user._checkCapability('owner'): 1025 # Owners shouldn't ever be ignored. 1026 return False 1027 elif user.ignore: 1028 log.debug('Ignoring %s due to their IrcUser ignore flag.', hostmask) 1029 return True 1030 except KeyError: 1031 # If there's no user... 1032 if conf.supybot.defaultIgnore(): 1033 log.debug('Ignoring %s due to conf.supybot.defaultIgnore', 1034 hostmask) 1035 return True 1036 if ignores.checkIgnored(hostmask): 1037 log.debug('Ignoring %s due to ignore database.', hostmask) 1038 return True 1039 if ircutils.isChannel(recipient): 1040 channel = channels.getChannel(recipient) 1041 if channel.checkIgnored(hostmask): 1042 log.debug('Ignoring %s due to the channel ignores.', hostmask) 1043 return True 1044 return False 1045 1046def _x(capability, ret): 1047 if isAntiCapability(capability): 1048 return not ret 1049 else: 1050 return ret 1051 1052def _checkCapabilityForUnknownUser(capability, users=users, channels=channels, 1053 ignoreDefaultAllow=False): 1054 if isChannelCapability(capability): 1055 (channel, capability) = fromChannelCapability(capability) 1056 try: 1057 c = channels.getChannel(channel) 1058 if capability in c.capabilities: 1059 return c._checkCapability(capability) 1060 else: 1061 return _x(capability, (not ignoreDefaultAllow) and c.defaultAllow) 1062 except KeyError: 1063 pass 1064 defaultCapabilities = conf.supybot.capabilities() 1065 if capability in defaultCapabilities: 1066 return defaultCapabilities.check(capability) 1067 elif ignoreDefaultAllow: 1068 return _x(capability, False) 1069 else: 1070 return _x(capability, conf.supybot.capabilities.default()) 1071 1072def checkCapability(hostmask, capability, users=users, channels=channels, 1073 ignoreOwner=False, ignoreChannelOp=False, 1074 ignoreDefaultAllow=False): 1075 """Checks that the user specified by name/hostmask has the capability given. 1076 1077 ``users`` and ``channels`` default to ``ircdb.users`` and 1078 ``ircdb.channels``. 1079 1080 ``ignoreOwner``, ``ignoreChannelOp``, and ``ignoreDefaultAllow`` are 1081 used to override default behavior of the capability system in special 1082 cases (actually, in the AutoMode plugin): 1083 1084 * ``ignoreOwner`` disables the behavior "owners have all capabilites" 1085 * ``ignoreChannelOp`` disables the behavior "channel ops have all 1086 channel capabilities" 1087 * ``ignoreDefaultAllow`` disables the behavior "if a user does not have 1088 a capability or the associated anticapability, then they have the 1089 capability" 1090 """ 1091 if world.testing and (not isinstance(hostmask, str) or 1092 '@' not in hostmask or 1093 '__no_testcap__' not in hostmask.split('@')[1]): 1094 return _x(capability, True) 1095 try: 1096 u = users.getUser(hostmask) 1097 if u.secure and not u.checkHostmask(hostmask, useAuth=False): 1098 raise KeyError 1099 except KeyError: 1100 # Raised when no hostmasks match. 1101 return _checkCapabilityForUnknownUser(capability, users=users, 1102 channels=channels, ignoreDefaultAllow=ignoreDefaultAllow) 1103 except ValueError as e: 1104 # Raised when multiple hostmasks match. 1105 log.warning('%s: %s', hostmask, e) 1106 return _checkCapabilityForUnknownUser(capability, users=users, 1107 channels=channels, ignoreDefaultAllow=ignoreDefaultAllow) 1108 if capability in u.capabilities: 1109 try: 1110 return u._checkCapability(capability, ignoreOwner) 1111 except KeyError: 1112 pass 1113 if isChannelCapability(capability): 1114 (channel, capability) = fromChannelCapability(capability) 1115 if not ignoreChannelOp: 1116 try: 1117 chanop = makeChannelCapability(channel, 'op') 1118 if u._checkCapability(chanop): 1119 return _x(capability, True) 1120 except KeyError: 1121 pass 1122 c = channels.getChannel(channel) 1123 if capability in c.capabilities: 1124 return c._checkCapability(capability) 1125 elif not ignoreDefaultAllow: 1126 return _x(capability, c.defaultAllow) 1127 else: 1128 return False 1129 defaultCapabilities = conf.supybot.capabilities() 1130 defaultCapabilitiesRegistered = conf.supybot.capabilities.registeredUsers() 1131 if capability in defaultCapabilities: 1132 return defaultCapabilities.check(capability) 1133 elif capability in defaultCapabilitiesRegistered: 1134 return defaultCapabilitiesRegistered.check(capability) 1135 elif ignoreDefaultAllow: 1136 return _x(capability, False) 1137 else: 1138 return _x(capability, conf.supybot.capabilities.default()) 1139 1140 1141def checkCapabilities(hostmask, capabilities, requireAll=False): 1142 """Checks that a user has capabilities in a list. 1143 1144 requireAll is True if *all* capabilities in the list must be had, False if 1145 *any* of the capabilities in the list must be had. 1146 """ 1147 for capability in capabilities: 1148 if requireAll: 1149 if not checkCapability(hostmask, capability): 1150 return False 1151 else: 1152 if checkCapability(hostmask, capability): 1153 return True 1154 return requireAll 1155 1156### 1157# supybot.capabilities 1158### 1159 1160class SpaceSeparatedListOfCapabilities(registry.SpaceSeparatedListOfStrings): 1161 __slots__ = () 1162 List = CapabilitySet 1163 1164class DefaultCapabilities(SpaceSeparatedListOfCapabilities): 1165 __slots__ = () 1166 # We use a keyword argument trick here to prevent eval'ing of code that 1167 # changes allowDefaultOwner from affecting this. It's not perfect, but 1168 # it's still an improvement, raising the bar for potential crackers. 1169 def setValue(self, v, allowDefaultOwner=conf.allowDefaultOwner): 1170 registry.SpaceSeparatedListOfStrings.setValue(self, v) 1171 if '-owner' not in self.value and not allowDefaultOwner: 1172 print('*** You must run supybot with the --allow-default-owner') 1173 print('*** option in order to allow a default capability of owner.') 1174 print('*** Don\'t do that, it\'s dumb.') 1175 self.value.add('-owner') 1176 1177conf.registerGlobalValue(conf.supybot, 'capabilities', 1178 DefaultCapabilities([ 1179 '-owner', '-admin', '-trusted', 1180 '-aka.add', '-aka.set', '-aka.remove', 1181 '-alias.add', '-alias.remove', 1182 '-scheduler.add', '-scheduler.remove', 1183 ], 1184 """These are the 1185 capabilities that are given to everyone by default. If they are normal 1186 capabilities, then the user will have to have the appropriate 1187 anti-capability if you want to override these capabilities; if they are 1188 anti-capabilities, then the user will have to have the actual capability 1189 to override these capabilities. See docs/CAPABILITIES if you don't 1190 understand why these default to what they do.""")) 1191 1192conf.registerGlobalValue(conf.supybot.capabilities, 'registeredUsers', 1193 SpaceSeparatedListOfCapabilities([], """These are the 1194 capabilities that are given to every authenticated user by default. 1195 You probably want to use supybot.capabilities instead, to give these 1196 capabilities both to registered and non-registered users.""")) 1197conf.registerGlobalValue(conf.supybot.capabilities, 'default', 1198 registry.Boolean(True, """Determines whether the bot by default will allow 1199 users to have a capability. If this is disabled, a user must explicitly 1200 have the capability for whatever command they wish to run.""")) 1201conf.registerGlobalValue(conf.supybot.capabilities, 'private', 1202 registry.SpaceSeparatedListOfStrings([], """Determines what capabilities 1203 the bot will never tell to a non-admin whether or not a user has them.""")) 1204 1205 1206# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: 1207