class IRCUser(IRCBase): def __init__(self, ircd, ip, uuid=None, host=None): self.ircd = ircd self.uuid = ircd.createUUID() if uuid is None else uuid registrationTimeout = self.ircd.config.get("user_registration_timeout", 10) self.nick = None self.ident = None if ip[0] == ":": # Normalize IPv6 address for IRC ip = "0{}".format(ip) if host is None: self.realHost = ip else: self.realHost = host self.ip = ip self._hostStack = [] self._hostsByType = {} self.gecos = None self._metadata = CaseInsensitiveDictionary() self.cache = {} self.channels = [] self.modes = {} self.connectedSince = now() self.nickSince = now() self.idleSince = now() self._registerHolds = set(("connection", "dns", "NICK", "USER")) self.disconnectedDeferred = Deferred() self._messageBatches = {} self._errorBatchName = None self._errorBatch = [] self.ircd.users[self.uuid] = self self.localOnly = False self.secureConnection = False self._pinger = LoopingCall(self._ping) self._registrationTimeoutTimer = reactor.callLater( registrationTimeout, self._timeoutRegistration) self._connectHandlerTimer = None self._startDNSResolving(registrationTimeout) def _startDNSResolving(self, timeout): ip = self.ip if ipIsV4(ip): addr = "{}.in-addr.arpa".format(".".join(reversed(ip.split(".")))) else: addr = reversed(expandIPv6Address(ip).replace(":", "")) addr = "{}.ip6.arpa".format(".".join(addr)) resolveDeferred = dnsClient.lookupPointer(addr, ((timeout / 2), )) resolveDeferred.addCallbacks(callback=self._verifyDNSResolution, callbackArgs=(timeout, ), errback=self._cancelDNSResolution) def _verifyDNSResolution(self, result, timeout): name = result[0][0].payload.name.name if len(name) > self.ircd.config.get("hostname_length", 64): self._cancelDNSResolution() return if not isValidHost(name): self._cancelDNSResolution() return resolveDeferred = dnsClient.getHostByName(name, ((timeout / 2), )) resolveDeferred.addCallbacks(callback=self._completeDNSResolution, errback=self._cancelDNSResolution, callbackArgs=(name, )) def _completeDNSResolution(self, result, name): if result == self.ip: self.realHost = name self.register("dns") def _cancelDNSResolution(self, error=None): self.register("dns") def connectionMade(self): # We need to callLater the connect action call because the connection isn't fully set up yet, # nor is it fully set up even with a delay of zero, which causes the message buffer not to be sent # when the connection is closed. # The "connection" register hold is used basically solely for the purposes of this to prevent potential # race conditions with registration. self._connectHandlerTimer = reactor.callLater(0.1, self._callConnectAction) if ISSLTransport.providedBy(self.transport): self.secureConnection = True def _callConnectAction(self): self._connectHandlerTimer = None if self.ircd.runActionUntilFalse("userconnect", self, users=[self]): self.transport.loseConnection() else: self.register("connection") def dataReceived(self, data): self.ircd.runActionStandard("userrecvdata", self, data, users=[self]) try: IRCBase.dataReceived(self, data) except Exception: self.ircd.log.failure( "An error occurred while processing incoming data.") if self.uuid in self.ircd.users: self.disconnect("Error occurred") def sendLine(self, line): self.ircd.runActionStandard("usersenddata", self, line, users=[self]) IRCBase.sendLine(self, line) def sendMessage(self, command, *args, **kw): """ Sends the given message to this user. Accepts the following keyword arguments: - prefix: The message prefix or None to suppress the default prefix If not given, defaults to the server name. - to: The destination of the message or None if the message has no destination. The implicit destination is this user if this argument isn't specified. - tags: Dict of message tags to send. - alwaysPrefixLastParam: For compatibility with some broken clients, you might want some messages to always have the last parameter prefixed with a colon. To do that, pass this as True. """ if "prefix" not in kw: kw["prefix"] = self.ircd.name if kw["prefix"] is None: del kw["prefix"] to = self.nick if self.nick else "*" if "to" in kw: to = kw["to"] del kw["to"] if to: args = [to] + list(args) self.ircd.runActionStandard("modifyoutgoingmessage", self, command, args, kw) IRCBase.sendMessage(self, command, *args, **kw) def handleCommand(self, command, params, prefix, tags): if self.uuid not in self.ircd.users: return # we have been disconnected - ignore all further commands if command in self.ircd.userCommands: handlers = self.ircd.userCommands[command] if not handlers: return data = None spewRegWarning = True affectedUsers = [] affectedChannels = [] for handler in handlers: if handler[0].forRegistered is not None: if (handler[0].forRegistered is True and not self.isRegistered()) or ( handler[0].forRegistered is False and self.isRegistered()): continue spewRegWarning = False data = handler[0].parseParams(self, params, prefix, tags) if data is not None: affectedUsers = handler[0].affectedUsers(self, data) affectedChannels = handler[0].affectedChannels(self, data) if self not in affectedUsers: affectedUsers.append(self) break if data is None: if spewRegWarning: if self.isRegistered(): self.sendMessage(irc.ERR_ALREADYREGISTERED, "You may not reregister") else: self.sendMessage(irc.ERR_NOTREGISTERED, command, "You have not registered") elif self._hasBatchedErrors(): self._dispatchErrorBatch() return self._clearErrorBatch() if self.ircd.runComboActionUntilValue( (("commandpermission-{}".format(command), self, data), ("commandpermission", self, command, data)), users=affectedUsers, channels=affectedChannels) is False: return self.ircd.runComboActionStandard( (("commandmodify-{}".format(command), self, data), ("commandmodify", self, command, data)), users=affectedUsers, channels=affectedChannels ) # This allows us to do processing without the "stop on empty" feature of runActionProcessing for handler in handlers: if handler[0].execute(self, data): if handler[0].resetsIdleTime: self.idleSince = now() break # If the command executor returns True, it was handled else: return # Don't process commandextra if it wasn't handled self.ircd.runComboActionStandard( (("commandextra-{}".format(command), self, data), ("commandextra", self, command, data)), users=affectedUsers, channels=affectedChannels) else: if not self.ircd.runActionFlagTrue("commandunknown", self, command, params, {}): self.sendMessage(irc.ERR_UNKNOWNCOMMAND, command, "Unknown command") def createMessageBatch(self, batchName, batchType, batchParameters=None): """ Start a new message batch with the given batch name, type, and list of parameters. If a batch with the given name already exists, that batch will be overwritten. """ self._messageBatches[batchName] = { "type": batchType, "parameters": batchParameters, "messages": [] } def sendMessageInBatch(self, batchName, command, *args, **kw): """ Adds a message to the batch with the given name. """ if batchName not in self._messageBatches: return self._messageBatches[batchName]["messages"].append((command, args, kw)) def sendBatch(self, batchName): """ Sends the messages in the given batch to the user. """ if batchName not in self._messageBatches: return batchType = self._messageBatches[batchName]["type"] batchParameters = self._messageBatches[batchName]["parameters"] self.ircd.runActionStandard("startbatchsend", self, batchName, batchType, batchParameters) for messageData in self._messageBatches[batchName]["messages"]: self.sendMessage(messageData[0], *messageData[1], **messageData[2]) self.ircd.runActionStandard("endbatchsend", self, batchName, batchType, batchParameters) def startErrorBatch(self, batchName): """ Used to start an error batch when sending multiple error messages to a user from a command's parseParams or from the commandpermission action. """ if not self._errorBatchName or not self._errorBatch: # Only the first batch should apply self._errorBatchName = batchName def sendBatchedError(self, batchName, command, *args, **kw): """ Adds an error to the current error batch if the specified error batch is the current error batch. """ if batchName and self._errorBatchName == batchName: self._errorBatch.append((command, args, kw)) def sendSingleError(self, batchName, command, *args, **kw): """ Creates a batch containing a single error and adds the specified error to it. """ if not self._errorBatchName: self._errorBatchName = batchName self._errorBatch.append((command, args, kw)) def _hasBatchedErrors(self): if self._errorBatch: return True return False def _clearErrorBatch(self): self._errorBatchName = None self._errorBatch = [] def _dispatchErrorBatch(self): for error in self._errorBatch: self.sendMessage(error[0], *error[1], **error[2]) self._clearErrorBatch() def filterConditionalTags(self, conditionalTags): applyTags = {} for tag, data in conditionalTags.iteritems(): value, check = data if check(self): applyTags[tag] = value return applyTags def connectionLost(self, reason): if self.uuid in self.ircd.users: self.disconnect("Connection reset") self.disconnectedDeferred.callback(None) def disconnect(self, reason): """ Disconnects the user from the server. """ self.ircd.log.debug( "Disconnecting user {user.uuid} ({user.hostmask()}): {reason}", user=self, reason=reason) # Sometimes, actions deferred from initial connection may cause registration to occur after disconnection if # disconnection happens before registration completes. If the user is unregistered on disconnection, this prevents # the user from completing registration. self.addRegisterHold("QUIT") if self._pinger: if self._pinger.running: self._pinger.stop() self._pinger = None if self._registrationTimeoutTimer: if self._registrationTimeoutTimer.active(): self._registrationTimeoutTimer.cancel() self._registrationTimeoutTimer = None if self._connectHandlerTimer and self._connectHandlerTimer.active(): self._connectHandlerTimer.cancel() self._connectHandlerTimer = None self.ircd.recentlyQuitUsers[self.uuid] = now() del self.ircd.users[self.uuid] if self.isRegistered(): del self.ircd.userNicks[self.nick] userSendList = [self] while self.channels: channel = self.channels[0] userSendList.extend(channel.users.keys()) self._leaveChannel(channel) userSendList = [ u for u in set(userSendList) if u.uuid[:3] == self.ircd.serverID ] userSendList.remove(self) self.ircd.runActionProcessing("quitmessage", userSendList, self, reason, users=[self] + userSendList) self.ircd.runActionStandard("quit", self, reason, users=self) self.transport.loseConnection() def _timeoutRegistration(self): if self.isRegistered(): self._pinger.start(self.ircd.config.get("user_ping_frequency", 60), False) return self.disconnect("Registration timeout") def _ping(self): self.ircd.runActionStandard("pinguser", self) def isRegistered(self): """ Returns True if this user session is fully registered. """ return not self._registerHolds def register(self, holdName): """ Removes the specified hold on a user's registration. If this is the last hold on a user, completes registration on the user. """ if holdName not in self._registerHolds: return self._registerHolds.remove(holdName) if not self._registerHolds: if not self.nick or self.nick in self.ircd.userNicks: self._registerHolds.add("NICK") if not self.ident or not self.gecos: self._registerHolds.add("USER") if self._registerHolds: return self._registerHolds.add( "registercheck" ) # The user shouldn't be considered registered until we complete these final checks if self.ircd.runActionUntilFalse("register", self, users=[self]): self.transport.loseConnection() return self._registerHolds.remove("registercheck") self.ircd.userNicks[self.nick] = self.uuid self.ircd.log.debug( "Registering user {user.uuid} ({user.hostmask()})", user=self) versionWithName = "txircd-{}".format(version) self.sendMessage( irc.RPL_WELCOME, "Welcome to the {} Internet Relay Chat Network {}".format( self.ircd.config["network_name"], self.hostmask())) self.sendMessage( irc.RPL_YOURHOST, "Your host is {}, running version {}".format( self.ircd.name, versionWithName)) self.sendMessage( irc.RPL_CREATED, "This server was created {}".format( self.ircd.startupTime.replace(microsecond=0))) chanModes = "".join( ["".join(modes.keys()) for modes in self.ircd.channelModes]) chanModes += "".join(self.ircd.channelStatuses.keys()) self.sendMessage( irc.RPL_MYINFO, self.ircd.name, versionWithName, "".join( ["".join(modes.keys()) for modes in self.ircd.userModes]), chanModes) self.sendISupport() self.ircd.runActionStandard("welcome", self, users=[self]) def addRegisterHold(self, holdName): """ Adds a register hold to this user if the user is not yet registered. """ if not self._registerHolds: return self._registerHolds.add(holdName) def sendISupport(self): """ Sends ISUPPORT to this user.""" isupportList = self.ircd.generateISupportList() isupportMsgList = splitMessage(" ".join(isupportList), 350) for line in isupportMsgList: lineArgs = line.split(" ") lineArgs.append("are supported by this server") self.sendMessage(irc.RPL_ISUPPORT, *lineArgs) def hostmask(self): """ Returns the user's hostmask. """ return "{}!{}@{}".format(self.nick, self.ident, self.host()) def hostmaskWithRealHost(self): """ Returns the user's hostmask using the user's real host rather than any vhost that may have been applied. """ return "{}!{}@{}".format(self.nick, self.ident, self.realHost) def hostmaskWithIP(self): """ Returns the user's hostmask using the user's IP address instead of the host. """ return "{}!{}@{}".format(self.nick, self.ident, self.ip) def changeNick(self, newNick, fromServer=None): """ Changes this user's nickname. If initiated by a remote server, that server should be specified in the fromServer parameter. """ if newNick == self.nick: return if newNick in self.ircd.userNicks and self.ircd.userNicks[ newNick] != self.uuid: return oldNick = self.nick if oldNick and oldNick in self.ircd.userNicks: del self.ircd.userNicks[self.nick] self.nick = newNick self.nickSince = now() if self.isRegistered(): self.ircd.userNicks[self.nick] = self.uuid userSendList = [self] for channel in self.channels: userSendList.extend(channel.users.keys()) userSendList = [ u for u in set(userSendList) if u.uuid[:3] == self.ircd.serverID ] self.ircd.runActionProcessing("changenickmessage", userSendList, self, oldNick, users=userSendList) self.ircd.runActionStandard("changenick", self, oldNick, fromServer, users=[self]) def changeIdent(self, newIdent, fromServer=None): """ Changes this user's ident. If initiated by a remote server, that server should be specified in the fromServer parameter. """ if newIdent == self.ident: return if len(newIdent) > self.ircd.config.get("ident_length", 12): return oldIdent = self.ident self.ident = newIdent if self.isRegistered(): self.ircd.runActionStandard("changeident", self, oldIdent, fromServer, users=[self]) def host(self): if not self._hostStack: return self.realHost return self._hostsByType[self._hostStack[-1]] def changeHost(self, hostType, newHost, fromServer=None): """ Changes a user's host. If initiated by a remote server, that server should be specified in the fromServer parameter. """ if hostType == "*": return if len(newHost) > self.ircd.config.get("hostname_length", 64): return if hostType in self._hostsByType and self._hostsByType[ hostType] == newHost: return oldHost = self.host() self._hostsByType[hostType] = newHost if hostType in self._hostStack: self._hostStack.remove(hostType) self._hostStack.append(hostType) if self.isRegistered(): self.ircd.runComboActionStandard( (("changehost", self, hostType, oldHost, fromServer), ("updatehost", self, hostType, oldHost, newHost, fromServer)), users=[self]) def updateHost(self, hostType, newHost, fromServer=None): """ Updates the host of a given host type for the user. If initiated by a remote server, that server should be specified in the fromServer parameter. """ if hostType not in self._hostStack: self.changeHost(hostType, newHost, fromServer) return if hostType == "*": return if len(newHost) > self.ircd.config.get("hostname_length", 64): return if hostType in self._hostsByType and self._hostsByType[ hostType] == newHost: return oldHost = self.host() oldHostOfType = None if hostType in self._hostsByType: oldHostOfType = self._hostsByType[hostType] self._hostsByType[hostType] = newHost changedUserHost = (oldHost != self.host()) changedHostOfType = (oldHostOfType != newHost) if self.isRegistered(): if changedUserHost and changedHostOfType: self.ircd.runComboActionStandard( (("changehost", self, hostType, oldHost, fromServer), ("updatehost", self, hostType, oldHost, newHost, fromServer)), users=[self]) elif changedHostOfType: self.ircd.runActionStandard("updatehost", self, hostType, oldHost, newHost, fromServer, users=[self]) def resetHost(self, hostType, fromServer=None): """ Resets the user's host to the real host. """ if hostType not in self._hostsByType: return oldHost = self.host() if hostType in self._hostStack: self._hostStack.remove(hostType) del self._hostsByType[hostType] currentHost = self.host() if currentHost != oldHost: self.ircd.runComboActionStandard( (("changehost", self, hostType, oldHost, fromServer), ("updatehost", self, hostType, oldHost, None, fromServer)), users=[self]) else: self.ircd.runActionStandard("updatehost", self, hostType, oldHost, None, fromServer, users=[self]) def currentHostType(self): if self._hostStack: return self._hostStack[-1] return "*" def changeGecos(self, newGecos, fromServer=None): """ Changes a user's real name. If initiated by a remote server, that server should be specified in the fromServer parameter. """ if len(newGecos) > self.ircd.config.get("gecos_length", 128): return if newGecos == self.gecos: return oldGecos = self.gecos self.gecos = newGecos if self.isRegistered(): self.ircd.runActionStandard("changegecos", self, oldGecos, fromServer, users=[self]) def metadataKeyExists(self, key): """ Checks whether the specified key exists in the user's metadata. """ return key in self._metadata def metadataKeyCase(self, key): """ Returns the specified key in the user's metadata in its original case. Returns None if the given key is not in the user's metadata. """ if key not in self._metadata: return None return self._metadata[key][0] def metadataValue(self, key): """ Returns the value of the given key in the user's metadata or None if the given key is not in the user's metadata. """ if key not in self._metadata: return None return self._metadata[key][1] def metadataVisibility(self, key): """ Returns the visibility value of the given key in the user's metadata or None if the given key is not in the user's metadata. """ if key not in self._metadata: return None return self._metadata[key][2] def metadataSetByUser(self, key): """ Returns whether the given key in the user's metadata was set by a user or None if the given key is not in the user's metadata. """ if key not in self._metadata: return None return self._metadata[key][3] def metadataList(self): """ Returns the list of metadata keys/values for the user as a list of tuples in the format [ (key, value, visibility, setByUser) ] """ return self._metadata.values() def setMetadata(self, key, value, visibility, setByUser, fromServer=None): """ Sets metadata for the user. If initiated by a remote server, that server should be specified in the fromServer parameter. If the value is None, deletes the metadata at the provided key. """ if not isValidMetadataKey(key): return False oldData = None if key in self._metadata: oldData = self._metadata[key] if setByUser and oldData and not oldData[3]: return False if setByUser and self.ircd.runActionUntilValue( "usercansetmetadata", key, users=[self]) is False: return False if value is None: if key in self._metadata: del self._metadata[key] elif not visibility: return False else: self._metadata[key] = (key, value, visibility, setByUser) oldValue = oldData[1] if oldData else None self.ircd.runActionStandard("usermetadataupdate", self, key, oldValue, value, visibility, setByUser, fromServer, users=[self]) return True def canSeeMetadataVisibility(self, visibility): if visibility == "*": return True return self.ircd.runActionUntilValue("usercanseemetadata", self, visibility) is not False def joinChannel(self, channel, override=False): """ Joins the user to a channel. Specify the override parameter only if all permission checks should be bypassed. """ if channel in self.channels: return if not override: if self.ircd.runActionUntilValue("joinpermission", channel, self, users=[self], channels=[channel]) is False: return channel.users[self] = {"status": ""} self.channels.append(channel) newChannel = False if channel.name not in self.ircd.channels: newChannel = True self.ircd.channels[channel.name] = channel self.ircd.recentlyDestroyedChannels[channel.name] = False # We need to send the JOIN message before doing other processing, as chancreate will do things like # mode defaulting, which will send messages about the channel before the JOIN message, which is bad. messageUsers = [ u for u in channel.users.iterkeys() if u.uuid[:3] == self.ircd.serverID ] self.ircd.runActionProcessing("joinmessage", messageUsers, channel, self, users=messageUsers, channels=[channel]) if newChannel: self.ircd.runActionStandard("channelcreate", channel, self, channels=[channel]) self.ircd.runActionStandard("join", channel, self, users=[self], channels=[channel]) def leaveChannel(self, channel, partType="PART", typeData={}, fromServer=None): """ Removes the user from a channel. The partType and typeData are used for the leavemessage action to send the parting message. If the channel leaving is initiated by a remote server, that server should be specified in the fromServer parameter. """ if channel not in self.channels: return messageUsers = [ u for u in channel.users.iterkeys() if u.uuid[:3] == self.ircd.serverID ] self.ircd.runActionProcessing("leavemessage", messageUsers, channel, self, partType, typeData, fromServer, users=[self], channels=[channel]) self._leaveChannel(channel) def _leaveChannel(self, channel): self.ircd.runActionStandard("leave", channel, self, users=[self], channels=[channel]) self.channels.remove(channel) del channel.users[self] def setModes(self, modes, defaultSource): """ Sets modes on the user. Accepts modes as a list of tuples in the format: [ (adding, mode, param, setBy, setTime) ] - adding: True if we're setting the mode; False if unsetting - mode: The mode letter - param: The mode's parameter; None if no parameter is needed for that mode - setBy: Optional, only used for list modes; a human-readable string (typically server name or nick!user@host) for who/what set this mode) - setTime: Optional, only used for list modes; a datetime object containing when the mode was set The defaultSource is a valid user ID or server ID of someone who set the modes. It is used as the source for announcements about the mode change and as the default setter for any list modes who do not have the setBy parameter specified. The default time for list modes with no setTime specified is now(). """ modeChanges = [] defaultSourceName = self._sourceName(defaultSource) if defaultSourceName is None: raise ValueError("Source must be a valid user or server ID.") nowTime = now() for modeData in modes: mode = modeData[1] if mode not in self.ircd.userModeTypes: continue setBy = defaultSourceName setTime = nowTime modeType = self.ircd.userModeTypes[mode] adding = modeData[0] if modeType in (ModeType.List, ModeType.ParamOnUnset, ModeType.Param): param = modeData[2] else: param = None if modeType == ModeType.List: dataCount = len(modeData) if dataCount >= 4: setBy = modeData[3] if dataCount >= 5: setTime = modeData[4] if adding: paramList = self.ircd.userModes[modeType][mode].checkSet( self, param) else: paramList = self.ircd.userModes[modeType][mode].checkUnset( self, param) if paramList is None: continue for parameter in paramList: if self._applyMode(adding, modeType, mode, parameter, setBy, setTime): modeChanges.append( (adding, mode, parameter, setBy, setTime)) self._notifyModeChanges(modeChanges, defaultSource, defaultSourceName) return modeChanges def setModesByUser(self, user, modes, params, override=False): """ Parses a mode string specified by a user and sets those modes on the user. The user parameter should be the user who set the modes (usually, but not always, this user). The modes parameter is the actual modes string; parameters specified by the user should be as a list of strings in params. The override parameter should be used only when all permission checks should be overridden. """ adding = True changes = [] setBy = self._sourceName(user.uuid) setTime = now() for mode in modes: if len(changes) >= self.ircd.config.get("modes_per_line", 20): break if mode == "+": adding = True continue if mode == "-": adding = False continue if mode not in self.ircd.userModeTypes: user.sendMessage(irc.ERR_UNKNOWNMODE, mode, "is unknown mode char to me") continue modeType = self.ircd.userModeTypes[mode] param = None if modeType in (ModeType.List, ModeType.ParamOnUnset) or ( adding and modeType == ModeType.Param): try: param = params.pop(0) except IndexError: if modeType == ModeType.List: self.ircd.userModes[modeType][mode].showListParams( user, self) continue if adding: paramList = self.ircd.userModes[modeType][mode].checkSet( self, param) else: paramList = self.ircd.userModes[modeType][mode].checkUnset( self, param) if paramList is None: continue for parameter in paramList: if len(changes) >= self.ircd.config.get("modes_per_line", 20): break if not override and self.ircd.runActionUntilValue( "modepermission-user-{}".format(mode), self, user, adding, parameter, users=[self, user]) is False: continue if adding: if modeType == ModeType.List: if mode in self.modes and len( self.modes[mode]) > self.ircd.config.get( "user_listmode_limit", 128): user.sendMessage( irc.ERR_BANLISTFULL, self.name, parameter, "Channel +{} list is full".format(mode)) continue if self._applyMode(adding, modeType, mode, parameter, setBy, setTime): changes.append((adding, mode, parameter, setBy, setTime)) self._notifyModeChanges(changes, user.uuid, setBy) return changes def _applyMode(self, adding, modeType, mode, parameter, setBy, setTime): if parameter: if len(parameter) > 255: return False if " " in parameter: return False if adding: if modeType == ModeType.List: if mode in self.modes: if len(self.modes[mode]) > self.ircd.config.get( "user_listmode_limit", 128): return False for paramData in self.modes[mode]: if parameter == paramData[0]: return False else: self.modes[mode] = [] self.modes[mode].append((parameter, setBy, setTime)) return True if mode in self.modes and self.modes[mode] == parameter: return False self.modes[mode] = parameter return True if modeType == ModeType.List: if mode not in self.modes: return False for index, paramData in enumerate(self.modes[mode]): if paramData[0] == parameter: del self.modes[mode][index] break else: return False if not self.modes[mode]: del self.modes[mode] return True if mode not in self.modes: return False if modeType == ModeType.ParamOnUnset and parameter != self.modes[mode]: return False del self.modes[mode] return True def _notifyModeChanges(self, modeChanges, source, sourceName): if not modeChanges: return for change in modeChanges: self.ircd.runActionStandard("modechange-user-{}".format(change[1]), self, change[3], change[0], change[2], users=[self]) users = [] if source in self.ircd.users and source[:3] == self.ircd.serverID: users.append(self.ircd.users[source]) if self.uuid[:3] == self.ircd.serverID: users.append(self) if users: self.ircd.runActionProcessing("modemessage-user", users, self, source, sourceName, modeChanges, users=users) self.ircd.runActionStandard("modechanges-user", self, source, sourceName, modeChanges, users=[self]) def _sourceName(self, source): if source in self.ircd.users: return self.ircd.users[source].hostmask() if source == self.ircd.serverID: return self.ircd.name if source in self.ircd.servers: return self.ircd.servers[source].name return None def modeString(self, toUser): """ Get a user-reportable mode string for the modes set on the user. """ modeStr = ["+"] params = [] for mode in self.modes: modeType = self.ircd.userModeTypes[mode] if modeType not in (ModeType.ParamOnUnset, ModeType.Param, ModeType.NoParam): continue if modeType != ModeType.NoParam: param = None if toUser: param = self.ircd.userModes[modeType][mode].showParam( toUser, self) if not param: param = self.modes[mode] else: param = None modeStr.append(mode) if param: params.append(param) if params: return "{} {}".format("".join(modeStr), " ".join(params)) return "".join(modeStr)
class IRCD(Factory): protocol = IRCProtocol def __init__(self, config, options = None, sslCert = None): reactor.addSystemEventTrigger("before", "shutdown", self.cleanup) self.dead = False self.config = config self.version = "txircd-{}".format(__version__) self.created = now() self.servers = CaseInsensitiveDictionary() self.users = CaseInsensitiveDictionary() self.userid = {} self.channels = CaseInsensitiveDictionary() self.peerConnections = {} self.ssl_cert = sslCert self.client_ports = {} self.server_ports = {} self.modules = {} self.module_abilities = {} self.actions = { "connect": [], "register": [], "welcome": [], "join": [], "joinmessage": [], "nick": [], "quit": [], "topic": [], "mode": [], "nameslistentry": [], "chancreate": [], "chandestroy": [], "commandextra": [], "commandunknown": [], "commandpermission": [], "metadataupdate": [], "recvdata": [], "senddata": [], "netmerge": [], "netsplit": [] } self.commands = {} self.channel_modes = [{}, {}, {}, {}] self.channel_mode_type = {} self.user_modes = [{}, {}, {}, {}] self.user_mode_type = {} self.prefixes = {} self.prefix_symbols = {} self.prefix_order = [] self.server_commands = {} self.module_data_cache = {} self.server_factory = None self.common_modules = set() log.msg("Loading module data...") try: with open("data.yaml", "r") as dataFile: self.serialized_data = yaml.safe_load(dataFile) if self.serialized_data is None: self.serialized_data = {} except IOError: self.serialized_data = {} self.isupport = {} self.usercount = { "localmax": 0, "globalmax": 0 } log.msg("Loading configuration...") self.servconfig = {} if not options: options = {} self.load_options(options) self.name = self.servconfig["server_name"] log.msg("Loading modules...") self.all_module_load() self.save_serialized_deferred = None self.autoconnect_servers = LoopingCall(self.server_autoconnect) self.autoconnect_servers.start(60, now=False) # The server factory isn't added to here yet # Fill in the default ISUPPORT dictionary once config and modules are loaded, since some values depend on those self.isupport["CASEMAPPING"] = "rfc1459" self.isupport["CHANMODES"] = ",".join(["".join(modedict.keys()) for modedict in self.channel_modes]) self.isupport["CHANNELLEN"] = "64" self.isupport["CHANTYPES"] = "#" self.isupport["MODES"] = 20 self.isupport["NETWORK"] = self.servconfig["server_network_name"] self.isupport["NICKLEN"] = "32" self.isupport["PREFIX"] = "({}){}".format("".join(self.prefix_order), "".join([self.prefixes[mode][0] for mode in self.prefix_order])) self.isupport["STATUSMSG"] = "".join([self.prefixes[mode][0] for mode in self.prefix_order]) self.isupport["TOPICLEN"] = "316" self.isupport["USERMODES"] = ",".join(["".join(modedict.keys()) for modedict in self.user_modes]) def all_module_load(self): # load RFC-required modules rfc_spec = [ # commands "cmd_user", "cmd_nick", "cmd_pass", # registration "cmd_ping", "cmd_pong", # connection keep-alive "cmd_join", "cmd_part", "cmd_kick", "cmd_topic", "cmd_mode", "cmd_invite", # channels "cmd_quit", # connection end "cmd_privmsg_notice", # messages "cmd_oper", "umode_o", "cmd_rehash", "cmd_wallops", # oper "cmd_admin", "cmd_info", "cmd_lusers", "cmd_motd", "cmd_stats", "cmd_time", "cmd_version", # server info "cmd_away", "cmd_ison", "cmd_userhost", "cmd_who", "cmd_whois", "cmd_whowas", # user info "cmd_names", "cmd_list", # channel info "cmd_kill", "cmd_eline", "cmd_gline", "cmd_kline", "cmd_qline", "cmd_zline", # user management "cmd_links", "cmd_connect", "cmd_squit", # linked servers # channel modes "cmode_b", "cmode_i", "cmode_k", "cmode_l", "cmode_m", "cmode_n", "cmode_o", "cmode_p", "cmode_s", "cmode_t", "cmode_v", # user modes "umode_i", "umode_s" ] ircv3_spec = [ # http://ircv3.atheme.org/ "ircv3_cap", # capability mechanism which essentially serves as the base for everything else "ircv3_multi-prefix", "ircv3_sasl", # other IRC 3.1 base extensions "ircv3_account-notify", "ircv3_away-notify", "ircv3_extended-join", "ircv3_tls", # IRC 3.1 optional extensions "ircv3_monitor", "ircv3_metadata" # IRC 3.2 base extensions ] for module in rfc_spec: check = self.load_module(module) if not check: log.msg("An RFC-required capability could not be loaded!") raise RuntimeError("A module required for RFC compatibility could not be loaded.") return if self.servconfig["app_irc_spec"] == "ircv3": for module in ircv3_spec: check = self.load_module(module) if not check: log.msg("IRCv3 compatibility was specified, but a required IRCv3 module could not be loaded!") raise RuntimeError("A module required for IRCv3 compatibility could not be loaded.") return for module in self.servconfig["server_modules"]: self.load_module(module) def rehash(self): log.msg("Rehashing config file and reloading modules") try: with open(self.config) as f: self.load_options(yaml.safe_load(f)) self.all_module_load() self.save_module_data() self.rebind_ports() except: return False return True def load_options(self, options): for var, value in options.iteritems(): self.servconfig[var] = value for var, value in default_options.iteritems(): if var not in self.servconfig: self.servconfig[var] = value def cleanup(self): # Track the disconnections so we know they get done deferreds = [] log.msg("Disconnecting servers...") for server in self.servers.values(): if server.nearHop == self.name: server.transport.loseConnection() deferreds.append(server.disconnected) # Cleanly disconnect all clients log.msg("Disconnecting clients...") for u in self.users.values(): u.sendMessage("ERROR", ":Closing Link: {} [Server shutting down]".format(u.hostname), to=None, prefix=None) u.socket.transport.loseConnection() deferreds.append(u.disconnected) log.msg("Unloading modules...") for name, spawner in self.modules.iteritems(): try: spawner.cleanup() except AttributeError: pass # If the module has no extra cleanup to do, that's fine try: data_to_save, free_data = self.modules[name].data_serialize() if data_to_save: self.serialized_data[name] = data_to_save except AttributeError: pass # If the module has no data to save, that's also fine. log.msg("Saving serialized data...") if not self.save_module_data(): self.save_serialized_deferred.addCallback(self.save_serialized) deferreds.append(self.save_serialized_deferred) # Return deferreds log.msg("Waiting on deferreds...") self.dead = True return DeferredList(deferreds) def connect_server(self, servername): def sendServerHandshake(protocol, password): protocol.callRemote(IntroduceServer, name=self.name, password=password, description=self.servconfig["server_description"], version=protocol_version, commonmodules=self.common_modules) protocol.sentDataBurst = False if servername in self.servers: raise RuntimeError ("Server {} is already connected".format(servername)) if servername not in self.servconfig["serverlinks"]: raise RuntimeError ("Server {} is not configured".format(servername)) servinfo = self.servconfig["serverlinks"][servername] if "ip" not in servinfo: raise RuntimeError ("Server {} is not properly configured: IP address must be specified".format(servername)) if "connect" not in servinfo: raise RuntimeError ("Server {} is not properly configured: Connection description not provided".format(servername)) if "incoming_password" not in servinfo or "outgoing_password" not in servinfo: raise RuntimeError ("Server {} is not properly configured: Passwords not specified".format(servername)) try: endpoint = clientFromString(reactor, resolveEndpointDescription(servinfo["connect"])) except ValueError as e: raise RuntimeError ("Server {} is not properly configured: Connection description is not valid ({})".format(servername, e)) connectDeferred = endpoint.connect(self.server_factory) connectDeferred.addCallback(sendServerHandshake, servinfo["outgoing_password"]) reactor.callLater(30, connectDeferred.cancel) # Time out the connection after 30 seconds def server_autoconnect(self): for server in self.servconfig["serverlink_autoconnect"]: if server not in self.servers and server in self.servconfig["serverlinks"]: log.msg("Initiating autoconnect to server {}".format(server)) try: self.connect_server(server) except RuntimeError as ex: log.msg("Connection to server failed: {}".format(ex)) def load_module(self, name): saved_data = {} if name in self.modules: saved_data = self.unload_module_data(name) try: mod_find = imp.find_module("txircd/modules/{}".format(name)) except ImportError as e: log.msg("Module not found: {} {}".format(name, e)) return False try: mod_load = imp.load_module(name, mod_find[0], mod_find[1], mod_find[2]) except ImportError as e: log.msg("Could not load module: {} ({})".format(name, e)) mod_find[0].close() return False mod_find[0].close() try: mod_spawner = mod_load.Spawner(self) except Exception as e: log.msg("Module is not a valid txircd module: {} ({})".format(name, e)) return False try: mod_contains = mod_spawner.spawn() except Exception as e: log.msg("Module is not a valid txircd module: {} ({})".format(name, e)) return False self.modules[name] = mod_spawner self.module_abilities[name] = mod_contains if "commands" in mod_contains: for command, implementation in mod_contains["commands"].iteritems(): if command in self.commands: log.msg("Module {} tries to reimplement command {}".format(name, command)) continue self.commands[command] = implementation.hook(self) if "modes" in mod_contains: for mode, implementation in mod_contains["modes"].iteritems(): if len(mode) < 2: continue if mode[1] == "l": modetype = 0 elif mode[1] == "u": modetype = 1 elif mode[1] == "p": modetype = 2 elif mode[1] == "n": modetype = 3 elif mode[1] == "s": modetype = -1 else: log.msg("Module {} registers a mode of an invalid type".format(name)) continue if mode[0] == "c": if mode[2] in self.channel_mode_type: log.msg("Module {} tries to reimplement channel mode {}".format(name, mode)) continue if modetype >= 0: self.channel_modes[modetype][mode[2]] = implementation.hook(self) else: if len(mode) < 5: log.msg("Module {} tries to register a prefix without a symbol or level".format(name)) continue try: level = int(mode[4:]) except: log.msg("Module {} tries to register a prefix without a numeric level".format(name)) continue closestLevel = 0 closestModeChar = None orderFail = False for levelMode, levelData in self.prefixes.iteritems(): if level == levelData[1]: log.msg("Module {} tries to register a prefix with the same rank level as an existing prefix") orderFail = True break if levelData[1] < level and levelData[1] > closestLevel: closestLevel = levelData[1] closestModeChar = levelMode if orderFail: continue if closestModeChar: self.prefix_order.insert(self.prefix_order.index(closestModeChar), mode[2]) else: self.prefix_order.append(mode[2]) self.prefixes[mode[2]] = [mode[3], level, implementation.hook(self)] self.prefix_symbols[mode[3]] = mode[2] self.channel_mode_type[mode[2]] = modetype self.isupport["PREFIX"] = "({}){}".format("".join(self.prefix_order), "".join([self.prefixes[mode][0] for mode in self.prefix_order])) self.isupport["STATUSMSG"] = "".join([self.prefixes[mode][0] for mode in self.prefix_order]) elif mode[0] == "u": if modetype == -1: log.msg("Module {} registers a mode of an invalid type".format(name)) continue if mode[2] in self.user_mode_type: log.msg("Module {} tries to reimplement user mode {}".format(name, mode)) continue self.user_modes[modetype][mode[2]] = implementation.hook(self) self.user_mode_type[mode[2]] = modetype if "actions" in mod_contains: for actiontype, actionfunc in mod_contains["actions"].iteritems(): if actiontype not in self.actions: self.actions[actiontype] = [] self.actions[actiontype].append(actionfunc) if "server" in mod_contains: for commandtype, commandfunc in mod_contains["server"].iteritems(): if commandtype not in self.server_commands: self.server_commands[commandtype] = [] self.server_commands[commandtype].append(commandfunc) if "common" in mod_contains and mod_contains["common"]: self.common_modules.add(name) if not saved_data and name in self.serialized_data: saved_data = self.serialized_data[name] # present serialized data on first load of session if saved_data: try: mod_spawner.data_unserialize(saved_data) except AttributeError: pass return True def unload_module_data(self, name): data_to_save = {} all_data = {} try: data_to_save, all_data = self.modules[name].data_serialize() if data_to_save: self.serialized_data[name] = data_to_save elif name in self.serialized_data: del self.serialized_data[name] # Copy data_to_save (if anything) over to all_data (intentionally overwriting any non-permanent items already in all_data) # So that we have one dictionary to pass back to data_unserialize if this is being immediately reloaded for item, value in data_to_save.iteritems(): all_data[item] = value except AttributeError: pass try: self.modules[name].cleanup() except AttributeError: pass abilities = self.module_abilities[name] del self.module_abilities[name] del self.modules[name] if "commands" in abilities: for command, implementation in abilities["commands"].iteritems(): if self.commands[command] == implementation: del self.commands[command] if "modes" in abilities: for mode, implementation in abilities["modes"].iteritems(): if mode[1] == "l": modetype = 0 elif mode[1] == "u": modetype = 1 elif mode[1] == "p": modetype = 2 elif mode[1] == "n": modetype = 3 elif mode[1] == "s": modetype = -1 if mode[0] == "c": if modetype == -1: if mode[2] in self.prefixes: del self.prefix_symbols[self.prefixes[mode[2]][0]] del self.prefixes[mode[2]] self.prefix_order.remove(mode[2]) else: if mode[2] in self.channel_modes[modetype]: del self.channel_modes[modetype][mode[2]] if mode[2] in self.channel_mode_type: del self.channel_mode_type[mode[2]] else: if mode[2] in self.user_modes[modetype]: del self.user_modes[modetype][mode[2]] if mode[2] in self.user_mode_type: del self.user_mode_type[mode[2]] if "actions" in abilities: for type, function in abilities["actions"].iteritems(): if type in self.actions and function in self.actions[type]: self.actions[type].remove(function) if "server" in abilities: for command, function in abilities["server"].iteritems(): if command in self.server_commands and function in self.server_commands[command]: self.server_commands[command].remove(function) return all_data def save_module_data(self): if self.save_serialized_deferred is None or self.save_serialized_deferred.called: self.save_serialized_deferred = deferToThread(self.save_serialized) return True # Otherwise, there's a save currently happening. This likely means that # 1. We don't need to save now; not THAT much has changed # 2. Saving now has the potential to cause problems. # We could add self.save_serialized as a callback to the Deferred, but there's # not a good way to check whether that's done yet without complicating things (and, # as mentioned, there's not a need for it). # The return value allows us to work around it currently saving already in the # cleanup step (when we absolutely must save regardless), as adding a callback # in IRCD.cleanup won't hurt anything. return False def save_serialized(self, _ = None): with open("data.yaml", "w") as dataFile: yaml.safe_dump(self.serialized_data, dataFile, default_flow_style=False) def saveClientPort(self, desc, port): if desc in self.client_ports: return self.client_ports[desc] = port def saveServerPort(self, desc, port): if desc in self.server_ports: return self.server_ports[desc] = port def rebind_ports(self): def addClientPortToIRCd(port, ircd, desc): ircd.saveClientPort(desc, port) def addServerPortToIRCd(port, ircd, desc): ircd.saveServerPort(desc, port) def logPortNotBound(error): log.msg("An error occurred: {}".format(error)) # Client ports old_ports, new_ports = set(self.client_ports.keys()), set(self.servconfig["server_client_ports"]) ports_to_unbind, ports_to_bind = old_ports - new_ports, new_ports - old_ports for port in ports_to_unbind: self.client_ports[port].stopListening() del self.client_ports[port] for port in ports_to_bind: try: endpoint = serverFromString(reactor, resolveEndpointDescription(port)) except ValueError as e: log.msg("Could not bind {}: not a valid description ({})".format(port, e)) continue listenDeferred = endpoint.listen(self) listenDeferred.addCallback(addClientPortToIRCd, self, port) listenDeferred.addErrback(logPortNotBound) # Server ports old_ports, new_ports = set(self.server_ports.keys()), set(self.servconfig["server_link_ports"]) ports_to_unbind, ports_to_bind = old_ports - new_ports, new_ports - old_ports for port in ports_to_unbind: self.server_ports[port].stopListening() del self.server_ports[port] for port in ports_to_bind: try: endpoint = serverFromString(reactor, resolveEndpointDescription(port)) except ValueError as e: log.msg("Could not bind {}: not a valid description ({})".format(port, e)) continue listenDeferred = endpoint.listen(self.server_factory) listenDeferred.addCallback(addServerPortToIRCd, self, port) listenDeferred.addErrback(logPortNotBound) def buildProtocol(self, addr): if self.dead: return None ip = addr.host connections = self.peerConnections.get(ip, 0) maxConnections = self.servconfig["client_peer_exempt"][ip] if ip in self.servconfig["client_peer_exempt"] else self.servconfig["client_peer_connections"] if maxConnections and connections >= maxConnections: log.msg("A client at IP address {} has exceeded the session limit".format(ip)) return None self.peerConnections[ip] = connections + 1 newProtocol = IRCProtocol(ip) newProtocol.factory = self return newProtocol def unregisterProtocol(self, p): self.peerConnections[p.ip] -= 1
class IRCD(Factory): protocol = IRCProtocol def __init__(self, config, options = None, sslCert = None): reactor.addSystemEventTrigger("before", "shutdown", self.cleanup) self.dead = False self.config = config self.version = "txircd-{}".format(__version__) self.created = now() self.servers = CaseInsensitiveDictionary() self.users = CaseInsensitiveDictionary() self.userid = {} self.channels = CaseInsensitiveDictionary() self.peerConnections = {} self.ssl_cert = sslCert self.modules = {} self.actions = { "connect": [], "register": [], "welcome": [], "join": [], "joinmessage": [], "nick": [], "quit": [], "topic": [], "nameslistentry": [], "chancreate": [], "chandestroy": [], "commandextra": [], "commandunknown": [], "commandpermission": [], "metadataupdate": [], "recvdata": [], "senddata": [], "netmerge": [], "netsplit": [] } self.commands = {} self.channel_modes = [{}, {}, {}, {}] self.channel_mode_type = {} self.user_modes = [{}, {}, {}, {}] self.user_mode_type = {} self.prefixes = {} self.prefix_symbols = {} self.prefix_order = [] self.server_commands = {} self.module_data_cache = {} self.server_factory = None self.common_modules = set() log.msg("Loading module data...") try: with open("data.yaml", "r") as dataFile: self.serialized_data = yaml.safe_load(dataFile) if self.serialized_data is None: self.serialized_data = {} except IOError: self.serialized_data = {} self.serialize_timer = LoopingCall(self.save_serialized) self.isupport = {} self.usercount = { "localmax": 0, "globalmax": 0 } log.msg("Loading configuration...") self.servconfig = {} if not options: options = {} self.load_options(options) self.name = self.servconfig["server_name"] log.msg("Loading modules...") self.all_module_load() self.autoconnect_servers = LoopingCall(self.server_autoconnect) self.autoconnect_servers.start(60, now=False) # The server factory isn't added to here yet # Fill in the default ISUPPORT dictionary once config and modules are loaded, since some values depend on those self.isupport["CASEMAPPING"] = "rfc1459" self.isupport["CHANMODES"] = ",".join(["".join(modedict.keys()) for modedict in self.channel_modes]) self.isupport["CHANNELLEN"] = "64" self.isupport["CHANTYPES"] = "#" self.isupport["MODES"] = 20 self.isupport["NETWORK"] = self.servconfig["server_network_name"] self.isupport["NICKLEN"] = "32" self.isupport["PREFIX"] = "({}){}".format("".join(self.prefix_order), "".join([self.prefixes[mode][0] for mode in self.prefix_order])) self.isupport["STATUSMSG"] = "".join([self.prefixes[mode][0] for mode in self.prefix_order]) self.isupport["TOPICLEN"] = "316" self.serialize_timer.start(300, now=False) # run every 5 minutes def all_module_load(self): # load RFC-required modules rfc_spec = [ # commands "cmd_user", "cmd_nick", "cmd_pass", # registration "cmd_ping", "cmd_pong", # connection keep-alive "cmd_join", "cmd_part", "cmd_kick", "cmd_topic", "cmd_mode", "cmd_invite", # channels "cmd_quit", # connection end "cmd_privmsg_notice", # messages "cmd_oper", "umode_o", "cmd_rehash", "cmd_wallops", # oper "cmd_admin", "cmd_info", "cmd_lusers", "cmd_motd", "cmd_stats", "cmd_time", "cmd_version", # server info "cmd_away", "cmd_ison", "cmd_userhost", "cmd_who", "cmd_whois", "cmd_whowas", # user info "cmd_names", "cmd_list", # channel info "cmd_kill", "cmd_eline", "cmd_gline", "cmd_kline", "cmd_qline", "cmd_zline", # user management "cmd_links", "cmd_connect", "cmd_squit", # linked servers # channel modes "cmode_b", "cmode_i", "cmode_k", "cmode_l", "cmode_m", "cmode_n", "cmode_o", "cmode_p", "cmode_s", "cmode_t", "cmode_v", # user modes "umode_i", "umode_s" ] ircv3_spec = [ # http://ircv3.atheme.org/ "ircv3_cap", # capability mechanism which essentially serves as the base for everything else "ircv3_multi-prefix", "ircv3_sasl", # other IRC 3.1 base extensions "ircv3_account-notify", "ircv3_away-notify", "ircv3_extended-join", "ircv3_tls", # IRC 3.1 optional extensions "ircv3_monitor", "ircv3_metadata" # IRC 3.2 base extensions ] for module in rfc_spec: check = self.load_module(module) if not check: log.msg("An RFC-required capability could not be loaded!") raise RuntimeError("A module required for RFC compatibility could not be loaded.") return if self.servconfig["app_irc_spec"] == "ircv3": for module in ircv3_spec: check = self.load_module(module) if not check: log.msg("IRCv3 compatibility was specified, but a required IRCv3 module could not be loaded!") raise RuntimeError("A module required for IRCv3 compatibility could not be loaded.") return for module in self.servconfig["server_modules"]: self.load_module(module) def rehash(self): log.msg("Rehashing config file and reloading modules") try: with open(self.config) as f: self.load_options(yaml.safe_load(f)) self.all_module_load() except: return False return True def load_options(self, options): for var, value in options.iteritems(): self.servconfig[var] = value for var, value in default_options.iteritems(): if var not in self.servconfig: self.servconfig[var] = value def cleanup(self): # Track the disconnections so we know they get done deferreds = [] log.msg("Disconnecting servers...") for server in self.servers.values(): if server.nearHop == self.name: server.transport.loseConnection() deferreds.append(server.disconnected) # Cleanly disconnect all clients log.msg("Disconnecting clients...") for u in self.users.values(): u.sendMessage("ERROR", ":Closing Link: {} [Server shutting down]".format(u.hostname), to=None, prefix=None) u.socket.transport.loseConnection() deferreds.append(u.disconnected) log.msg("Unloading modules...") for name, spawner in self.modules.iteritems(): spawner.cleanup() try: data_to_save, free_data = self.modules[name].data_serialize() if data_to_save: self.serialized_data[name] = data_to_save except AttributeError: pass log.msg("Saving serialized data...") self.save_serialized() # Return deferreds log.msg("Waiting on deferreds...") self.dead = True return DeferredList(deferreds) def server_autoconnect(self): def sendServerHandshake(protocol, password): protocol.callRemote(IntroduceServer, name=self.name, password=password, description=self.servconfig["server_description"], version=protocol_version, commonmodules=self.common_modules) protocol.sentDataBurst = False for server in self.servconfig["serverlink_autoconnect"]: if server not in self.servers and server in self.servconfig["serverlinks"]: log.msg("Initiating autoconnect to server {}".format(server)) servinfo = self.servconfig["serverlinks"][server] if "ip" not in servinfo or "port" not in servinfo: continue if "bindaddress" in servinfo and "bindport" in servinfo: bind = (servinfo["bindaddress"], servinfo["bindport"]) else: bind = None creator = ClientCreator(reactor, ServerProtocol, self) if "ssl" in servinfo and servinfo["ssl"]: d = creator.connectSSL(servinfo["ip"], servinfo["port"], self.ssl_cert, bindAddress=bind) else: d = creator.connectTCP(servinfo["ip"], servinfo["port"], bindAddress=bind) d.addCallback(sendServerHandshake, servinfo["outgoing_password"]) def load_module(self, name): saved_data = {} if name in self.modules: try: data_to_save, free_data = self.modules[name].data_serialize() if data_to_save: self.serialized_data[name] = data_to_save elif name in self.serialized_data: del self.serialized_data[name] for key, value in free_data.iteritems(): saved_data[key] = value for key, value in data_to_save.iteritems(): saved_data[key] = value except AttributeError: pass try: self.modules[name].cleanup() except: log.msg("Cleanup failed for module {}: some pieces may still be remaining!".format(name)) del self.modules[name] try: mod_find = imp.find_module("txircd/modules/{}".format(name)) except ImportError as e: log.msg("Module not found: {} {}".format(name, e)) return False try: mod_load = imp.load_module(name, mod_find[0], mod_find[1], mod_find[2]) except ImportError as e: log.msg("Could not load module: {} ({})".format(name, e)) mod_find[0].close() return False mod_find[0].close() try: mod_spawner = mod_load.Spawner(self) except Exception as e: log.msg("Module is not a valid txircd module: {} ({})".format(name, e)) return False try: mod_contains = mod_spawner.spawn() except Exception as e: log.msg("Module is not a valid txircd module: {} ({})".format(name, e)) return False self.modules[name] = mod_spawner if "commands" in mod_contains: for command, implementation in mod_contains["commands"].iteritems(): if command in self.commands: log.msg("Module {} tries to reimplement command {}".format(name, command)) continue self.commands[command] = implementation.hook(self) if "modes" in mod_contains: for mode, implementation in mod_contains["modes"].iteritems(): if len(mode) < 2: continue if mode[1] == "l": modetype = 0 elif mode[1] == "u": modetype = 1 elif mode[1] == "p": modetype = 2 elif mode[1] == "n": modetype = 3 elif mode[1] == "s": modetype = -1 else: log.msg("Module {} registers a mode of an invalid type".format(name)) continue if mode[0] == "c": if mode[2] in self.channel_mode_type: log.msg("Module {} tries to reimplement channel mode {}".format(name, mode)) continue if modetype >= 0: self.channel_modes[modetype][mode[2]] = implementation.hook(self) else: if len(mode) < 5: log.msg("Module {} tries to register a prefix without a symbol or level".format(name)) continue try: level = int(mode[4:]) except: log.msg("Module {} tries to register a prefix without a numeric level".format(name)) continue closestLevel = 0 closestModeChar = None orderFail = False for levelMode, levelData in self.prefixes.iteritems(): if level == levelData[1]: log.msg("Module {} tries to register a prefix with the same rank level as an existing prefix") orderFail = True break if levelData[1] < level and levelData[1] > closestLevel: closestLevel = levelData[1] closestModeChar = levelMode if orderFail: continue if closestModeChar: self.prefix_order.insert(self.prefix_order.index(closestModeChar), mode[2]) else: self.prefix_order.append(mode[2]) self.prefixes[mode[2]] = [mode[3], level, implementation.hook(self)] self.prefix_symbols[mode[3]] = mode[2] self.channel_mode_type[mode[2]] = modetype self.isupport["PREFIX"] = "({}){}".format("".join(self.prefix_order), "".join([self.prefixes[mode][0] for mode in self.prefix_order])) self.isupport["STATUSMSG"] = "".join([self.prefixes[mode][0] for mode in self.prefix_order]) elif mode[0] == "u": if modetype == -1: log.msg("Module {} registers a mode of an invalid type".format(name)) continue if mode[2] in self.user_mode_type: log.msg("Module {} tries to reimplement user mode {}".format(name, mode)) continue self.user_modes[modetype][mode[2]] = implementation.hook(self) self.user_mode_type[mode[2]] = modetype if "actions" in mod_contains: for actiontype, actionfuncs in mod_contains["actions"].iteritems(): if actiontype in self.actions: for func in actionfuncs: self.actions[actiontype].append(func) else: self.actions[actiontype] = actionfuncs if "server" in mod_contains: for commandtype, commandfunc in mod_contains["server"].iteritems(): if commandtype not in self.server_commands: self.server_commands[commandtype] = [] self.server_commands[commandtype].append(commandfunc) if "common" in mod_contains and mod_contains["common"]: self.common_modules.add(name) if not saved_data and name in self.serialized_data: saved_data = self.serialized_data[name] # present serialized data on first load of session if saved_data: try: mod_spawner.data_unserialize(saved_data) except AttributeError: pass return True def removeMode(self, modedesc): # This function is heavily if'd in case we get passed invalid data. if modedesc[1] == "l": modetype = 0 elif modedesc[1] == "u": modetype = 1 elif modedesc[1] == "p": modetype = 2 elif modedesc[1] == "n": modetype = 3 elif modedesc[1] == "s": modetype = -1 else: return if modedesc[0] == "c": if modetype != -1 and modedesc[2] in self.channel_modes[modetype]: del self.channel_modes[modetype][modedesc[2]] if modedesc[2] in self.channel_mode_type: del self.channel_mode_type[modedesc[2]] if modetype == -1 and modedesc[2] in self.prefixes: del self.prefix_symbols[self.prefixes[modedesc[2]][0]] if modedesc[2] in self.prefixes: del self.prefixes[modedesc[2]] if modedesc[2] in self.prefix_order: self.prefix_order.remove(modedesc[2]) else: if modedesc[2] in self.user_modes[modetype]: del self.user_modes[modetype][modedesc[2]] if modedesc[2] in self.user_mode_type: del self.user_mode_type[modedesc[2]] def save_serialized(self): with open("data.yaml", "w") as dataFile: yaml.dump(self.serialized_data, dataFile, default_flow_style=False) def buildProtocol(self, addr): if self.dead: return None ip = addr.host connections = self.peerConnections.get(ip, 0) maxConnections = self.servconfig["client_peer_exempt"][ip] if ip in self.servconfig["client_peer_exempt"] else self.servconfig["client_peer_connections"] if maxConnections and connections >= maxConnections: log.msg("A client at IP address {} has exceeded the session limit".format(ip)) return None self.peerConnections[ip] = connections + 1 return Factory.buildProtocol(self, addr) def unregisterProtocol(self, p): ip = p.transport.getPeer().host self.peerConnections[ip] -= 1
class IRCChannel(object): def __init__(self, ircd, name): if not isValidChannelName(name): raise InvalidChannelNameError self.ircd = ircd self.name = name[:self.ircd.config.get("channel_name_length", 64)] self.users = WeakKeyDictionary() self.modes = {} self.existedSince = now() self.topic = "" self.topicSetter = "" self.topicTime = now() self._metadata = CaseInsensitiveDictionary() self.cache = {} def sendUserMessage(self, command, *params, **kw): """ Sends a message to all local users in a channel. Accepts a command and some parameters for that command to send. Accepts any keyword arguments accepted by IRCUser.sendMessage. Also accepts the following keyword arguments: - skip: list of users in the channel to skip when sending the message """ if "to" not in kw: kw["to"] = self.name if kw["to"] is None: del kw["to"] userList = [u for u in self.users.iterkeys() if u.uuid[:3] == self.ircd.serverID] if "skip" in kw: for u in kw["skip"]: if u in userList: userList.remove(u) kw["users"] = userList kw["channels"] = [self] baseTags = {} if "tags" in kw: baseTags = kw["tags"] del kw["tags"] conditionalTags = {} if "conditionalTags" in kw: conditionalTags = kw["conditionalTags"] del kw["conditionalTags"] for user in userList: if conditionalTags: tags = baseTags.copy() addTags = user.filterConditionalTags(conditionalTags) tags.update(addTags) else: tags = baseTags kw["tags"] = tags user.sendMessage(command, *params, **kw) def sendServerMessage(self, command, *params, **kw): """ Sends a message to all remote servers to which any user in this channel is connected. Accepts a command and some parameters for that command to send. Also accepts the following keyword arguments: - skipall: list of servers to skip from the network - skiplocal: list of locally-connected servers to which to skip sending after we've determined the closest hop of all the servers to which we're sending """ servers = set() for user in self.users.iterkeys(): if user.uuid[:3] != self.ircd.serverID: servers.add(self.ircd.servers[user.uuid[:3]]) if "skipall" in kw: for s in kw["skipall"]: servers.discard(s) localServers = set() for server in servers: nearHop = server while nearHop.nextClosest != self.ircd.serverID: nearHop = self.ircd.servers[nearHop.nextClosest] localServers.add(nearHop) if "skiplocal" in kw: for s in kw["skiplocal"]: localServers.discard(s) for server in localServers: server.sendMessage(command, *params, **kw) def setTopic(self, topic, setter): """ Sets the channel topic. """ if setter in self.ircd.users: source = self.ircd.users[setter].hostmask() elif setter == self.ircd.serverID: source = self.ircd.name elif setter in self.ircd.servers: source = self.ircd.servers[setter].name else: return False if topic == self.topic: return True oldTopic = self.topic self.topic = topic self.topicSetter = source self.topicTime = now() self.ircd.runActionStandard("topic", self, setter, oldTopic, channels=[self]) return True def metadataKeyExists(self, key): """ Checks whether a specific key exists in the channel's metadata. """ return key in self._metadata def metadataKeyCase(self, key): """ Gets the key from the channel's metadata in its original case. Returns None if the key is not present. """ if key not in self._metadata: return None return self._metadata[key][0] def metadataValue(self, key): """ Gets the value for the given key in the channel's metadata. Returns None if the key is not present. """ if key not in self._metadata: return None return self._metadata[key][1] def metadataVisibility(self, key): """ Gets the visibility value for the given key in the channel's metadata. Returns None if the key is not present. """ if key not in self._metadata: return None return self._metadata[key][2] def metadataSetByUser(self, key): """ Gets whether the given metadata key/value was set by a user. Returns None if the key is not present. """ if key not in self._metadata: return None return self._metadata[key][3] def metadataList(self): """ Returns the list of metadata keys/values for the channel as a list of tuples in the format [ (key, value, visibility, setByUser) ] """ return self._metadata.values() def setMetadata(self, key, value, visibility, setByUser, fromServer = None): """ Sets metadata for the channel. Returns True if the set is successful or False if it is not. If the metadata set is caused by a message from a remote server, pass the server object as the fromServer parameter. If value is None, deletes the key provided. """ if not isValidMetadataKey(key): return False oldData = None if key in self._metadata: oldData = self._metadata[key] if setByUser and oldData and not oldData[3]: return False if setByUser and self.ircd.runActionUntilValue("usercansetmetadata", key, channels=[self]) is False: return False if value is None: del self._metadata[key] elif not visibility: return False else: self._metadata[key] = (key, value, visibility, setByUser) oldValue = oldData[1] if oldData else None self.ircd.runActionStandard("channelmetadataupdate", self, key, oldValue, value, visibility, setByUser, fromServer, channels=[self]) return True def setModes(self, modes, defaultSource): """ Sets modes on the channel. Accepts modes as a list of tuples in the format: [ (adding, mode, param, setBy, setTime) ] - adding: True if we're setting the mode; False if unsetting - mode: The mode letter - param: The mode's parameter; None if no parameter is needed for that mode - setBy: Optional, only used for list modes; a human-readable string (typically server name or nick!user@host) for who/what set this mode) - setTime: Optional, only used for list modes; a datetime object containing when the mode was set The defaultSource is a valid user ID or server ID of someone who set the modes. It is used as the source for announcements about the mode change and as the default setter for any list modes who do not have the setBy parameter specified. The default time for list modes with no setTime specified is now(). """ modeChanges = [] defaultSourceName = self._sourceName(defaultSource) if defaultSourceName is None: raise ValueError ("Source must be a valid user or server ID.") nowTime = now() for modeData in modes: mode = modeData[1] if mode not in self.ircd.channelModeTypes: continue setBy = defaultSourceName setTime = nowTime modeType = self.ircd.channelModeTypes[mode] adding = modeData[0] if modeType in (ModeType.List, ModeType.ParamOnUnset, ModeType.Param, ModeType.Status): param = modeData[2] else: param = None if modeType == ModeType.List: dataCount = len(modeData) if dataCount >= 4: setBy = modeData[3] if dataCount >= 5: setTime = modeData[4] if modeType == ModeType.Status: if adding: paramList = self.ircd.channelStatuses[mode][2].checkSet(self, param) else: paramList = self.ircd.channelStatuses[mode][2].checkUnset(self, param) else: if adding: paramList = self.ircd.channelModes[modeType][mode].checkSet(self, param) else: paramList = self.ircd.channelModes[modeType][mode].checkUnset(self, param) if paramList is None: continue for parameter in paramList: if self._applyMode(adding, modeType, mode, parameter, setBy, setTime): modeChanges.append((adding, mode, parameter, setBy, setTime)) self._notifyModeChanges(modeChanges, defaultSource, defaultSourceName) return modeChanges def setModesByUser(self, user, modes, params, override = False): """ Parses a mode string specified by a user and sets those modes on the channel. The user parameter should be the user who set the modes. The modes parameter is the actual modes string; parameters specified by the user should be as a list of strings in params. The override parameter should be used only when all permission checks should be overridden. """ adding = True changes = [] setBy = self._sourceName(user.uuid) setTime = now() for mode in modes: if len(changes) >= self.ircd.config.get("modes_per_line", 20): break if mode == "+": adding = True continue if mode == "-": adding = False continue if mode not in self.ircd.channelModeTypes: user.sendMessage(irc.ERR_UNKNOWNMODE, mode, "is unknown mode char to me") continue modeType = self.ircd.channelModeTypes[mode] param = None if modeType in (ModeType.List, ModeType.ParamOnUnset, ModeType.Status) or (adding and modeType == ModeType.Param): try: param = params.pop(0) except IndexError: if modeType == ModeType.List: self.ircd.channelModes[modeType][mode].showListParams(user, self) continue if modeType == ModeType.Status: if adding: paramList = self.ircd.channelStatuses[mode][2].checkSet(self, param) else: paramList = self.ircd.channelStatuses[mode][2].checkUnset(self, param) else: if adding: paramList = self.ircd.channelModes[modeType][mode].checkSet(self, param) else: paramList = self.ircd.channelModes[modeType][mode].checkUnset(self, param) if paramList is None: continue for parameter in paramList: if len(changes) >= self.ircd.config.get("modes_per_line", 20): break if not override and self.ircd.runActionUntilValue("modepermission-channel-{}".format(mode), self, user, adding, parameter, users=[user], channels=[self]) is False: continue if adding: if modeType == ModeType.Status: try: targetUser = self.ircd.users[self.ircd.userNicks[parameter]] except KeyError: continue if targetUser not in self.users: continue if mode in self.users[targetUser]["status"]: continue statusLevel = self.ircd.channelStatuses[mode][1] if not override and self.userRank(user) < statusLevel and not self.ircd.runActionUntilValue("channelstatusoverride", self, user, mode, parameter, users=[user], channels=[self]): user.sendMessage(irc.ERR_CHANOPRIVSNEEDED, self.name, "You do not have permission to set channel mode +{}".format(mode)) continue parameter = targetUser.uuid elif modeType == ModeType.List: if mode in self.modes and len(self.modes[mode]) > self.ircd.config.get("channel_listmode_limit", 128): user.sendMessage(irc.ERR_BANLISTFULL, self.name, parameter, "Channel +{} list is full".format(mode)) continue else: if modeType == ModeType.Status: try: targetUser = self.ircd.users[self.ircd.userNicks[parameter]] except KeyError: continue if mode not in self.users[targetUser]["status"]: continue statusLevel = self.ircd.channelStatuses[mode][1] if not override and self.userRank(user) < statusLevel and not self.ircd.runActionUntilValue("channelstatusoverride", self, user, mode, parameter, users=[user], channels=[self]): user.sendMessage(irc.ERR_CHANOPRIVSNEEDED, self.name, "You do not have permission to set channel mode -{}".format(mode)) continue parameter = targetUser.uuid if self._applyMode(adding, modeType, mode, parameter, setBy, setTime): changes.append((adding, mode, parameter, setBy, setTime)) self._notifyModeChanges(changes, user.uuid, setBy) return changes def _applyMode(self, adding, modeType, mode, parameter, setBy, setTime): if parameter: if len(parameter) > 255: return False if " " in parameter: return False if adding: if modeType == ModeType.Status: try: targetUser = self.ircd.users[parameter] except KeyError: return False if targetUser not in self.users: return False if mode in self.users[targetUser]: return False statusLevel = self.ircd.channelStatuses[mode][1] targetStatus = self.users[targetUser]["status"] if mode in targetStatus: return False for index, rank in enumerate(targetStatus): if self.ircd.channelStatuses[rank][1] < statusLevel: statusList = list(targetStatus) statusList.insert(index, mode) self.users[targetUser]["status"] = "".join(statusList) return True self.users[targetUser]["status"] += mode return True if modeType == ModeType.List: if mode in self.modes: if len(self.modes[mode]) > self.ircd.config.get("channel_listmode_limit", 128): return False for paramData in self.modes[mode]: if parameter == paramData[0]: return False else: self.modes[mode] = [] self.modes[mode].append((parameter, setBy, setTime)) return True if mode in self.modes and self.modes[mode] == parameter: return False self.modes[mode] = parameter return True if modeType == ModeType.Status: try: targetUser = self.ircd.users[parameter] except KeyError: return False if targetUser not in self.users: return False if mode not in self.users[targetUser]["status"]: return False self.users[targetUser]["status"] = self.users[targetUser]["status"].replace(mode, "") return True if modeType == ModeType.List: if mode not in self.modes: return False for index, paramData in enumerate(self.modes[mode]): if paramData[0] == parameter: del self.modes[mode][index] break else: return False if not self.modes[mode]: del self.modes[mode] return True if mode not in self.modes: return False if modeType == ModeType.ParamOnUnset and parameter != self.modes[mode]: return False del self.modes[mode] return True def _notifyModeChanges(self, modeChanges, source, sourceName): if not modeChanges: return channelUsers = [] for user in self.users.iterkeys(): if user.uuid[:3] == self.ircd.serverID: channelUsers.append(user) for change in modeChanges: self.ircd.runActionStandard("modechange-channel-{}".format(change[1]), self, change[3], change[0], change[2], channels=[self]) self.ircd.runActionProcessing("modemessage-channel", channelUsers, self, source, sourceName, modeChanges, users=channelUsers, channels=[self]) self.ircd.runActionStandard("modechanges-channel", self, source, sourceName, modeChanges, channels=[self]) def _sourceName(self, source): if source in self.ircd.users: return self.ircd.users[source].hostmask() if source == self.ircd.serverID: return self.ircd.name if source in self.ircd.servers: return self.ircd.servers[source].name return None def modeString(self, toUser): """ Get a user-reportable mode string for the modes set on the channel. """ modeStr = ["+"] params = [] for mode in self.modes: modeType = self.ircd.channelModeTypes[mode] if modeType not in (ModeType.ParamOnUnset, ModeType.Param, ModeType.NoParam): continue if modeType != ModeType.NoParam: param = self.ircd.channelModes[modeType][mode].showParam(toUser, self) if not param: param = self.modes[mode] else: param = None modeStr.append(mode) if param: params.append(param) if params: return "{} {}".format("".join(modeStr), " ".join(params)) return "".join(modeStr) def userRank(self, user): """ Gets the user's numeric rank in the channel. """ if user not in self.users: return -1 status = self.users[user]["status"] if not status: return 0 return self.ircd.channelStatuses[status[0]][1]
class IRCD(Factory): protocol = IRCProtocol channel_prefixes = "#" types = {"user": DBUser, "server": IRCServer, "service": IRCService} prefix_order = "qaohv" # Hardcoded into modes :( prefix_symbols = {"q": "~", "a": "&", "o": "@", "h": "%", "v": "+"} def __init__(self, config, options=None): reactor.addSystemEventTrigger("before", "shutdown", self.cleanup) self.dead = False self.config = config self.version = "txircd.{}".format(__version__) self.created = now() self.token = uuid.uuid1() self.servers = CaseInsensitiveDictionary() self.users = CaseInsensitiveDictionary() self.whowas = CaseInsensitiveDictionary() self.channels = DefaultCaseInsensitiveDictionary(self.ChannelFactory) self.peerConnections = {} self.db = None # self.stats = None # self.stats_timer = LoopingCall(self.flush_stats) # self.stats_data = { # "bytes_in": 0, # "bytes_out": 0, # "lines_in": 0, # "lines_out": 0, # "total_bytes_in": 0, # "total_bytes_out": 0, # "total_lines_in": 0, # "total_lines_out": 0, # "connections": 0, # "total_connections": 0 # } self.xlines = { "G": CaseInsensitiveDictionary(), "K": CaseInsensitiveDictionary(), "Z": CaseInsensitiveDictionary(), "E": CaseInsensitiveDictionary(), "Q": CaseInsensitiveDictionary(), "SHUN": CaseInsensitiveDictionary(), } self.xline_match = { "G": ["{ident}@{host}", "{ident}@{ip}"], "K": ["{ident}@{host}", "{ident}@{ip}"], "Z": ["{ip}"], "E": ["{ident}@{host}", "{ident}@{ip}"], "Q": ["{nick}"], "SHUN": ["{ident}@{host}", "{ident}@{ip}"], } if not options: options = {} self.load_options(options) # if self.app_ip_log: # try: # with open(self.app_ip_log) as f: # self.unique_ips = set(json.loads(f.read())) # self.stats_data["total_connections"] = len(self.unique_ips) # except: # self.unique_ips = set() # else: # self.unique_ips = set() # logfile = "{}/{}".format(self.app_log_dir,"stats") # if not os.path.exists(logfile): # os.makedirs(logfile) # self.stats_log = DailyLogFile("log",logfile) # self.stats_timer.start(1) def rehash(self): try: with open(self.config) as f: self.load_options(yaml.safe_load(f)) except: return False return True def load_options(self, options): # Populate attributes with options for var in default_options.iterkeys(): setattr(self, var, options[var] if var in options else default_options[var]) # Unserialize xlines for key in self.xlines.iterkeys(): self.xlines[key] = CaseInsensitiveDictionary() xlines = getattr(self, "server_xlines_{}".format(key.lower()), None) if not xlines: continue for user, data in xlines.iteritems(): self.xlines[key][user] = { "created": datetime.datetime.strptime(data["created"], "%Y-%m-%d %H:%M:%S"), "duration": parse_duration(data["duration"]), "setter": data["setter"], "reason": data["reason"], } # Create database connection if self.db: self.db.close() if self.db_library: self.db = adbapi.ConnectionPool( self.db_library, host=self.db_host, port=self.db_port, db=self.db_database, user=self.db_username, passwd=self.db_password, cp_reconnect=True, ) # Turn on stats factory if needed, or shut it down if needed # if self.stats_enabled and not self.stats: # self.stats = StatFactory() # if self.stats_port_tcp: # try: # reactor.listenTCP(int(self.stats_port_tcp), self.stats) # except: # pass # Wasn't a number # if self.stats_port_web: # try: # reactor.listenTCP(int(self.stats_port_web), SockJSFactory(self.stats)) # except: # pass # Wasn't a number # elif not self.stats_enabled and self.stats: # self.stats.shutdown() # self.stats = None # Load geoip data # self.geo_db = pygeoip.GeoIP(self.app_geoip_database, pygeoip.MEMORY_CACHE) if self.app_geoip_database else None def save_options(self): # Serialize xlines for key, lines in self.xlines.iteritems(): xlines = {} for user, data in lines.iteritems(): xlines[user] = { "created": str(data["created"]), "duration": build_duration(data["duration"]), "setter": data["setter"], "reason": data["reason"], } setattr(self, "server_xlines_{}".format(key.lower()), xlines) # Load old options options = {} try: with open(self.config) as f: options = yaml.safe_load(f) except: return False # Overwrite with the new stuff for var in default_options.iterkeys(): options[var] = getattr(self, var, None) # Save em try: with open(self.config, "w") as f: yaml.dump(options, f, default_flow_style=False) except: return False return True def cleanup(self): # Track the disconnections so we know they get done deferreds = [] # Cleanly disconnect all clients log.msg("Disconnecting clients...") for u in self.users.values(): u.irc_QUIT(None, ["Server shutting down"]) deferreds.append(u.disconnected) # Without any clients, all channels should be gone # But make sure the logs are closed, just in case log.msg("Closing logs...") for c in self.channels.itervalues(): c.log.close() # self.stats_log.close() # Finally, save the config. Just in case. log.msg("Saving options...") self.save_options() # Return deferreds log.msg("Waiting on deferreds...") self.dead = True return DeferredList(deferreds) def buildProtocol(self, addr): if self.dead: return None ip = addr.host # self.unique_ips.add(ip) # self.stats_data["total_connections"] = len(self.unique_ips) # if self.app_ip_log: # with open(self.app_ip_log,"w") as f: # f.write(json.dumps(list(self.unique_ips), separators=(',',':'))) conn = self.peerConnections.get(ip, 0) max = self.client_peer_exempt[ip] if ip in self.client_peer_exempt else self.client_peer_connections if max and conn >= max: return None # self.stats_data["connections"] += 1 self.peerConnections[ip] = conn + 1 return Factory.buildProtocol(self, addr) def unregisterProtocol(self, p): # self.stats_data["connections"] -= 1 peerHost = p.transport.getPeer().host self.peerConnections[peerHost] -= 1 if self.peerConnections[peerHost] == 0: del self.peerConnections[peerHost] def ChannelFactory(self, name): logfile = "{}/{}".format(self.app_log_dir, irc_lower(name)) if not os.path.exists(logfile): os.makedirs(logfile) c = Channel( name, now(), {"message": None, "author": "", "created": now()}, CaseInsensitiveDictionary(), ChannelModes(self, None), DailyLogFile("log", logfile), ) c.mode.parent = c c.mode.combine("nt", [], name) return c def flush_stats(self): return users = {} countries = {} uptime = now() - self.created for u in self.users.itervalues(): users[u.nickname] = [u.latitude, u.longitude] if u.country not in countries: countries[u.country] = 0 countries[u.country] += 1 line = json.dumps( { "io": self.stats_data, "users": users, "countries": countries, "uptime": "{}".format(uptime if uptime.days > 0 else "0 days, {}".format(uptime)), }, separators=(",", ":"), ) self.stats_data["bytes_in"] = 0 self.stats_data["bytes_out"] = 0 self.stats_data["lines_in"] = 0 self.stats_data["lines_out"] = 0 # if not self.stats_log.closed: # self.stats_log.write(line+"\n") if self.stats: self.stats.broadcast(line + "\r\n")
class IRCChannel(object): def __init__(self, ircd, name): if not isValidChannelName(name): raise InvalidChannelNameError self.ircd = ircd self.name = name[:self.ircd.config.get("channel_name_length", 64)] self.users = WeakKeyDictionary() self.modes = {} self.existedSince = now() self.topic = "" self.topicSetter = "" self.topicTime = now() self._metadata = CaseInsensitiveDictionary() self.cache = {} def sendUserMessage(self, command, *params, **kw): """ Sends a message to all local users in a channel. Accepts a command and some parameters for that command to send. Accepts any keyword arguments accepted by IRCUser.sendMessage. Also accepts the following keyword arguments: - skip: list of users in the channel to skip when sending the message """ if "to" not in kw: kw["to"] = self.name if kw["to"] is None: del kw["to"] userList = [ u for u in self.users.iterkeys() if u.uuid[:3] == self.ircd.serverID ] if "skip" in kw: for u in kw["skip"]: if u in userList: userList.remove(u) kw["users"] = userList kw["channels"] = [self] baseTags = {} if "tags" in kw: baseTags = kw["tags"] del kw["tags"] conditionalTags = {} if "conditionalTags" in kw: conditionalTags = kw["conditionalTags"] del kw["conditionalTags"] for user in userList: if conditionalTags: tags = baseTags.copy() addTags = user.filterConditionalTags(conditionalTags) tags.update(addTags) else: tags = baseTags kw["tags"] = tags user.sendMessage(command, *params, **kw) def sendServerMessage(self, command, *params, **kw): """ Sends a message to all remote servers to which any user in this channel is connected. Accepts a command and some parameters for that command to send. Also accepts the following keyword arguments: - skipall: list of servers to skip from the network - skiplocal: list of locally-connected servers to which to skip sending after we've determined the closest hop of all the servers to which we're sending """ servers = set() for user in self.users.iterkeys(): if user.uuid[:3] != self.ircd.serverID: servers.add(self.ircd.servers[user.uuid[:3]]) if "skipall" in kw: for s in kw["skipall"]: servers.discard(s) localServers = set() for server in servers: nearHop = server while nearHop.nextClosest != self.ircd.serverID: nearHop = self.ircd.servers[nearHop.nextClosest] localServers.add(nearHop) if "skiplocal" in kw: for s in kw["skiplocal"]: localServers.discard(s) for server in localServers: server.sendMessage(command, *params, **kw) def setTopic(self, topic, setter): """ Sets the channel topic. """ if setter in self.ircd.users: source = self.ircd.users[setter].hostmask() elif setter == self.ircd.serverID: source = self.ircd.name elif setter in self.ircd.servers: source = self.ircd.servers[setter].name else: return False if topic == self.topic: return True oldTopic = self.topic self.topic = topic self.topicSetter = source self.topicTime = now() self.ircd.runActionStandard("topic", self, setter, oldTopic, channels=[self]) return True def metadataKeyExists(self, key): """ Checks whether a specific key exists in the channel's metadata. """ return key in self._metadata def metadataKeyCase(self, key): """ Gets the key from the channel's metadata in its original case. Returns None if the key is not present. """ if key not in self._metadata: return None return self._metadata[key][0] def metadataValue(self, key): """ Gets the value for the given key in the channel's metadata. Returns None if the key is not present. """ if key not in self._metadata: return None return self._metadata[key][1] def metadataVisibility(self, key): """ Gets the visibility value for the given key in the channel's metadata. Returns None if the key is not present. """ if key not in self._metadata: return None return self._metadata[key][2] def metadataSetByUser(self, key): """ Gets whether the given metadata key/value was set by a user. Returns None if the key is not present. """ if key not in self._metadata: return None return self._metadata[key][3] def metadataList(self): """ Returns the list of metadata keys/values for the channel as a list of tuples in the format [ (key, value, visibility, setByUser) ] """ return self._metadata.values() def setMetadata(self, key, value, visibility, setByUser, fromServer=None): """ Sets metadata for the channel. Returns True if the set is successful or False if it is not. If the metadata set is caused by a message from a remote server, pass the server object as the fromServer parameter. If value is None, deletes the key provided. """ if not isValidMetadataKey(key): return False oldData = None if key in self._metadata: oldData = self._metadata[key] if setByUser and oldData and not oldData[3]: return False if setByUser and self.ircd.runActionUntilValue( "usercansetmetadata", key, channels=[self]) is False: return False if value is None: del self._metadata[key] elif not visibility: return False else: self._metadata[key] = (key, value, visibility, setByUser) oldValue = oldData[1] if oldData else None self.ircd.runActionStandard("channelmetadataupdate", self, key, oldValue, value, visibility, setByUser, fromServer, channels=[self]) return True def setModes(self, modes, defaultSource): """ Sets modes on the channel. Accepts modes as a list of tuples in the format: [ (adding, mode, param, setBy, setTime) ] - adding: True if we're setting the mode; False if unsetting - mode: The mode letter - param: The mode's parameter; None if no parameter is needed for that mode - setBy: Optional, only used for list modes; a human-readable string (typically server name or nick!user@host) for who/what set this mode) - setTime: Optional, only used for list modes; a datetime object containing when the mode was set The defaultSource is a valid user ID or server ID of someone who set the modes. It is used as the source for announcements about the mode change and as the default setter for any list modes who do not have the setBy parameter specified. The default time for list modes with no setTime specified is now(). """ modeChanges = [] defaultSourceName = self._sourceName(defaultSource) if defaultSourceName is None: raise ValueError("Source must be a valid user or server ID.") nowTime = now() for modeData in modes: mode = modeData[1] if mode not in self.ircd.channelModeTypes: continue setBy = defaultSourceName setTime = nowTime modeType = self.ircd.channelModeTypes[mode] adding = modeData[0] if modeType in (ModeType.List, ModeType.ParamOnUnset, ModeType.Param, ModeType.Status): param = modeData[2] else: param = None if modeType == ModeType.List: dataCount = len(modeData) if dataCount >= 4: setBy = modeData[3] if dataCount >= 5: setTime = modeData[4] if modeType == ModeType.Status: if adding: paramList = self.ircd.channelStatuses[mode][2].checkSet( self, param) else: paramList = self.ircd.channelStatuses[mode][2].checkUnset( self, param) else: if adding: paramList = self.ircd.channelModes[modeType][ mode].checkSet(self, param) else: paramList = self.ircd.channelModes[modeType][ mode].checkUnset(self, param) if paramList is None: continue for parameter in paramList: if self._applyMode(adding, modeType, mode, parameter, setBy, setTime): modeChanges.append( (adding, mode, parameter, setBy, setTime)) self._notifyModeChanges(modeChanges, defaultSource, defaultSourceName) return modeChanges def setModesByUser(self, user, modes, params, override=False): """ Parses a mode string specified by a user and sets those modes on the channel. The user parameter should be the user who set the modes. The modes parameter is the actual modes string; parameters specified by the user should be as a list of strings in params. The override parameter should be used only when all permission checks should be overridden. """ adding = True changes = [] setBy = self._sourceName(user.uuid) setTime = now() for mode in modes: if len(changes) >= self.ircd.config.get("modes_per_line", 20): break if mode == "+": adding = True continue if mode == "-": adding = False continue if mode not in self.ircd.channelModeTypes: user.sendMessage(irc.ERR_UNKNOWNMODE, mode, "is unknown mode char to me") continue modeType = self.ircd.channelModeTypes[mode] param = None if modeType in (ModeType.List, ModeType.ParamOnUnset, ModeType.Status) or (adding and modeType == ModeType.Param): try: param = params.pop(0) except IndexError: if modeType == ModeType.List: self.ircd.channelModes[modeType][mode].showListParams( user, self) continue if modeType == ModeType.Status: if adding: paramList = self.ircd.channelStatuses[mode][2].checkSet( self, param) else: paramList = self.ircd.channelStatuses[mode][2].checkUnset( self, param) else: if adding: paramList = self.ircd.channelModes[modeType][ mode].checkSet(self, param) else: paramList = self.ircd.channelModes[modeType][ mode].checkUnset(self, param) if paramList is None: continue for parameter in paramList: if len(changes) >= self.ircd.config.get("modes_per_line", 20): break if not override and self.ircd.runActionUntilValue( "modepermission-channel-{}".format(mode), self, user, adding, parameter, users=[user], channels=[self]) is False: continue if adding: if modeType == ModeType.Status: try: targetUser = self.ircd.users[ self.ircd.userNicks[parameter]] except KeyError: continue if targetUser not in self.users: continue if mode in self.users[targetUser]["status"]: continue statusLevel = self.ircd.channelStatuses[mode][1] if not override and self.userRank( user ) < statusLevel and not self.ircd.runActionUntilValue( "channelstatusoverride", self, user, mode, parameter, users=[user], channels=[self]): user.sendMessage( irc.ERR_CHANOPRIVSNEEDED, self.name, "You do not have permission to set channel mode +{}" .format(mode)) continue parameter = targetUser.uuid elif modeType == ModeType.List: if mode in self.modes and len( self.modes[mode]) > self.ircd.config.get( "channel_listmode_limit", 128): user.sendMessage( irc.ERR_BANLISTFULL, self.name, parameter, "Channel +{} list is full".format(mode)) continue else: if modeType == ModeType.Status: try: targetUser = self.ircd.users[ self.ircd.userNicks[parameter]] except KeyError: continue if mode not in self.users[targetUser]["status"]: continue statusLevel = self.ircd.channelStatuses[mode][1] if not override and self.userRank( user ) < statusLevel and not self.ircd.runActionUntilValue( "channelstatusoverride", self, user, mode, parameter, users=[user], channels=[self]): user.sendMessage( irc.ERR_CHANOPRIVSNEEDED, self.name, "You do not have permission to set channel mode -{}" .format(mode)) continue parameter = targetUser.uuid if self._applyMode(adding, modeType, mode, parameter, setBy, setTime): changes.append((adding, mode, parameter, setBy, setTime)) self._notifyModeChanges(changes, user.uuid, setBy) return changes def _applyMode(self, adding, modeType, mode, parameter, setBy, setTime): if parameter: if len(parameter) > 255: return False if " " in parameter: return False if adding: if modeType == ModeType.Status: try: targetUser = self.ircd.users[parameter] except KeyError: return False if targetUser not in self.users: return False if mode in self.users[targetUser]: return False statusLevel = self.ircd.channelStatuses[mode][1] targetStatus = self.users[targetUser]["status"] if mode in targetStatus: return False for index, rank in enumerate(targetStatus): if self.ircd.channelStatuses[rank][1] < statusLevel: statusList = list(targetStatus) statusList.insert(index, mode) self.users[targetUser]["status"] = "".join(statusList) return True self.users[targetUser]["status"] += mode return True if modeType == ModeType.List: if mode in self.modes: if len(self.modes[mode]) > self.ircd.config.get( "channel_listmode_limit", 128): return False for paramData in self.modes[mode]: if parameter == paramData[0]: return False else: self.modes[mode] = [] self.modes[mode].append((parameter, setBy, setTime)) return True if mode in self.modes and self.modes[mode] == parameter: return False self.modes[mode] = parameter return True if modeType == ModeType.Status: try: targetUser = self.ircd.users[parameter] except KeyError: return False if targetUser not in self.users: return False if mode not in self.users[targetUser]["status"]: return False self.users[targetUser]["status"] = self.users[targetUser][ "status"].replace(mode, "") return True if modeType == ModeType.List: if mode not in self.modes: return False for index, paramData in enumerate(self.modes[mode]): if paramData[0] == parameter: del self.modes[mode][index] break else: return False if not self.modes[mode]: del self.modes[mode] return True if mode not in self.modes: return False if modeType == ModeType.ParamOnUnset and parameter != self.modes[mode]: return False del self.modes[mode] return True def _notifyModeChanges(self, modeChanges, source, sourceName): if not modeChanges: return channelUsers = [] for user in self.users.iterkeys(): if user.uuid[:3] == self.ircd.serverID: channelUsers.append(user) for change in modeChanges: self.ircd.runActionStandard("modechange-channel-{}".format( change[1]), self, change[3], change[0], change[2], channels=[self]) self.ircd.runActionProcessing("modemessage-channel", channelUsers, self, source, sourceName, modeChanges, users=channelUsers, channels=[self]) self.ircd.runActionStandard("modechanges-channel", self, source, sourceName, modeChanges, channels=[self]) def _sourceName(self, source): if source in self.ircd.users: return self.ircd.users[source].hostmask() if source == self.ircd.serverID: return self.ircd.name if source in self.ircd.servers: return self.ircd.servers[source].name return None def modeString(self, toUser): """ Get a user-reportable mode string for the modes set on the channel. """ modeStr = ["+"] params = [] for mode in self.modes: modeType = self.ircd.channelModeTypes[mode] if modeType not in (ModeType.ParamOnUnset, ModeType.Param, ModeType.NoParam): continue if modeType != ModeType.NoParam: param = self.ircd.channelModes[modeType][mode].showParam( toUser, self) if not param: param = self.modes[mode] else: param = None modeStr.append(mode) if param: params.append(param) if params: return "{} {}".format("".join(modeStr), " ".join(params)) return "".join(modeStr) def userRank(self, user): """ Gets the user's numeric rank in the channel. """ if user not in self.users: return -1 status = self.users[user]["status"] if not status: return 0 return self.ircd.channelStatuses[status[0]][1]
class IRCUser(IRCBase): def __init__(self, ircd, ip, uuid = None, host = None): self.ircd = ircd self.uuid = ircd.createUUID() if uuid is None else uuid self.nick = None self.ident = None if ip[0] == ":": # Normalize IPv6 address for IRC ip = "0{}".format(ip) if host is None: try: resolvedHost = gethostbyaddr(ip)[0] # First half of host resolution done, run second half to prevent rDNS spoofing. # Refuse hosts that are too long as well. if ip == gethostbyname(resolvedHost) and len(resolvedHost) <= self.ircd.config.get("hostname_length", 64) and isValidHost(resolvedHost): host = resolvedHost else: host = ip except (herror, gaierror): host = ip self.realHost = host self.ip = ip self._hostStack = [] self._hostsByType = {} self.gecos = None self._metadata = CaseInsensitiveDictionary() self.cache = {} self.channels = [] self.modes = {} self.connectedSince = now() self.nickSince = now() self.idleSince = now() self._registerHolds = set(("connection", "NICK", "USER")) self.disconnectedDeferred = Deferred() self._messageBatches = {} self._errorBatchName = None self._errorBatch = [] self.ircd.users[self.uuid] = self self.localOnly = False self.secureConnection = False self._pinger = LoopingCall(self._ping) self._registrationTimeoutTimer = reactor.callLater(self.ircd.config.get("user_registration_timeout", 10), self._timeoutRegistration) def connectionMade(self): # We need to callLater the connect action call because the connection isn't fully set up yet, # nor is it fully set up even with a delay of zero, which causes the message buffer not to be sent # when the connection is closed. # The "connection" register hold is used basically solely for the purposes of this to prevent potential # race conditions with registration. reactor.callLater(0.1, self._callConnectAction) if ISSLTransport.providedBy(self.transport): self.secureConnection = True def _callConnectAction(self): if self.ircd.runActionUntilFalse("userconnect", self, users=[self]): self.transport.loseConnection() else: self.register("connection") def dataReceived(self, data): self.ircd.runActionStandard("userrecvdata", self, data, users=[self]) try: IRCBase.dataReceived(self, data) except Exception: self.ircd.log.failure("An error occurred while processing incoming data.") if self.uuid in self.ircd.users: self.disconnect("Error occurred") def sendLine(self, line): self.ircd.runActionStandard("usersenddata", self, line, users=[self]) IRCBase.sendLine(self, line) def sendMessage(self, command, *args, **kw): """ Sends the given message to this user. Accepts the following keyword arguments: - prefix: The message prefix or None to suppress the default prefix If not given, defaults to the server name. - to: The destination of the message or None if the message has no destination. The implicit destination is this user if this argument isn't specified. - tags: Dict of message tags to send. - alwaysPrefixLastParam: For compatibility with some broken clients, you might want some messages to always have the last parameter prefixed with a colon. To do that, pass this as True. """ if "prefix" not in kw: kw["prefix"] = self.ircd.name if kw["prefix"] is None: del kw["prefix"] to = self.nick if self.nick else "*" if "to" in kw: to = kw["to"] del kw["to"] if to: args = [to] + list(args) self.ircd.runActionStandard("modifyoutgoingmessage", self, command, args, kw) IRCBase.sendMessage(self, command, *args, **kw) def handleCommand(self, command, params, prefix, tags): if self.uuid not in self.ircd.users: return # we have been disconnected - ignore all further commands if command in self.ircd.userCommands: handlers = self.ircd.userCommands[command] if not handlers: return data = None spewRegWarning = True affectedUsers = [] affectedChannels = [] for handler in handlers: if handler[0].forRegistered is not None: if (handler[0].forRegistered is True and not self.isRegistered()) or (handler[0].forRegistered is False and self.isRegistered()): continue spewRegWarning = False data = handler[0].parseParams(self, params, prefix, tags) if data is not None: affectedUsers = handler[0].affectedUsers(self, data) affectedChannels = handler[0].affectedChannels(self, data) if self not in affectedUsers: affectedUsers.append(self) break if data is None: if spewRegWarning: if self.isRegistered(): self.sendMessage(irc.ERR_ALREADYREGISTERED, "You may not reregister") else: self.sendMessage(irc.ERR_NOTREGISTERED, command, "You have not registered") elif self._hasBatchedErrors(): self._dispatchErrorBatch() return self._clearErrorBatch() if self.ircd.runComboActionUntilValue((("commandpermission-{}".format(command), self, data), ("commandpermission", self, command, data)), users=affectedUsers, channels=affectedChannels) is False: return self.ircd.runComboActionStandard((("commandmodify-{}".format(command), self, data), ("commandmodify", self, command, data)), users=affectedUsers, channels=affectedChannels) # This allows us to do processing without the "stop on empty" feature of runActionProcessing for handler in handlers: if handler[0].execute(self, data): if handler[0].resetsIdleTime: self.idleSince = now() break # If the command executor returns True, it was handled else: return # Don't process commandextra if it wasn't handled self.ircd.runComboActionStandard((("commandextra-{}".format(command), self, data), ("commandextra", self, command, data)), users=affectedUsers, channels=affectedChannels) else: if not self.ircd.runActionFlagTrue("commandunknown", self, command, params, {}): self.sendMessage(irc.ERR_UNKNOWNCOMMAND, command, "Unknown command") def createMessageBatch(self, batchName, batchType, batchParameters = None): """ Start a new message batch with the given batch name, type, and list of parameters. If a batch with the given name already exists, that batch will be overwritten. """ self._messageBatches[batchName] = { "type": batchType, "parameters": batchParameters, "messages": [] } def sendMessageInBatch(self, batchName, command, *args, **kw): """ Adds a message to the batch with the given name. """ if batchName not in self._messageBatches: return self._messageBatches[batchName]["messages"].append((command, args, kw)) def sendBatch(self, batchName): """ Sends the messages in the given batch to the user. """ if batchName not in self._messageBatches: return batchType = self._messageBatches[batchName]["type"] batchParameters = self._messageBatches[batchName]["parameters"] self.ircd.runActionStandard("startbatchsend", self, batchName, batchType, batchParameters) for messageData in self._messageBatches[batchName]["messages"]: self.sendMessage(messageData[0], *messageData[1], **messageData[2]) self.ircd.runActionStandard("endbatchsend", self, batchName, batchType, batchParameters) def startErrorBatch(self, batchName): """ Used to start an error batch when sending multiple error messages to a user from a command's parseParams or from the commandpermission action. """ if not self._errorBatchName or not self._errorBatch: # Only the first batch should apply self._errorBatchName = batchName def sendBatchedError(self, batchName, command, *args, **kw): """ Adds an error to the current error batch if the specified error batch is the current error batch. """ if batchName and self._errorBatchName == batchName: self._errorBatch.append((command, args, kw)) def sendSingleError(self, batchName, command, *args, **kw): """ Creates a batch containing a single error and adds the specified error to it. """ if not self._errorBatchName: self._errorBatchName = batchName self._errorBatch.append((command, args, kw)) def _hasBatchedErrors(self): if self._errorBatch: return True return False def _clearErrorBatch(self): self._errorBatchName = None self._errorBatch = [] def _dispatchErrorBatch(self): for error in self._errorBatch: self.sendMessage(error[0], *error[1], **error[2]) self._clearErrorBatch() def filterConditionalTags(self, conditionalTags): applyTags = {} for tag, data in conditionalTags.iteritems(): value, check = data if check(self): applyTags[tag] = value return applyTags def connectionLost(self, reason): if self.uuid in self.ircd.users: self.disconnect("Connection reset") self.disconnectedDeferred.callback(None) def disconnect(self, reason): """ Disconnects the user from the server. """ self.ircd.log.debug("Disconnecting user {user.uuid} ({user.hostmask()}): {reason}", user=self, reason=reason) if self._pinger: if self._pinger.running: self._pinger.stop() self._pinger = None if self._registrationTimeoutTimer: if self._registrationTimeoutTimer.active(): self._registrationTimeoutTimer.cancel() self._registrationTimeoutTimer = None self.ircd.recentlyQuitUsers[self.uuid] = now() del self.ircd.users[self.uuid] if self.isRegistered(): del self.ircd.userNicks[self.nick] userSendList = [self] while self.channels: channel = self.channels[0] userSendList.extend(channel.users.keys()) self._leaveChannel(channel) userSendList = [u for u in set(userSendList) if u.uuid[:3] == self.ircd.serverID] userSendList.remove(self) self.ircd.runActionProcessing("quitmessage", userSendList, self, reason, users=[self] + userSendList) self.ircd.runActionStandard("quit", self, reason, users=self) self.transport.loseConnection() def _timeoutRegistration(self): if self.isRegistered(): self._pinger.start(self.ircd.config.get("user_ping_frequency", 60), False) return self.disconnect("Registration timeout") def _ping(self): self.ircd.runActionStandard("pinguser", self) def isRegistered(self): """ Returns True if this user session is fully registered. """ return not self._registerHolds def register(self, holdName): """ Removes the specified hold on a user's registratrion. If this is the last hold on a user, completes registration on the user. """ if holdName not in self._registerHolds: return self._registerHolds.remove(holdName) if not self._registerHolds: if not self.nick or self.nick in self.ircd.userNicks: self._registerHolds.add("NICK") if not self.ident or not self.gecos: self._registerHolds.add("USER") if self._registerHolds: return self._registerHolds.add("registercheck") # The user shouldn't be considered registered until we complete these final checks if self.ircd.runActionUntilFalse("register", self, users=[self]): self.transport.loseConnection() return self._registerHolds.remove("registercheck") self.ircd.userNicks[self.nick] = self.uuid self.ircd.log.debug("Registering user {user.uuid} ({user.hostmask()})", user=self) versionWithName = "txircd-{}".format(version) self.sendMessage(irc.RPL_WELCOME, "Welcome to the {} Internet Relay Chat Network {}".format(self.ircd.config["network_name"], self.hostmask())) self.sendMessage(irc.RPL_YOURHOST, "Your host is {}, running version {}".format(self.ircd.name, versionWithName)) self.sendMessage(irc.RPL_CREATED, "This server was created {}".format(self.ircd.startupTime.replace(microsecond=0))) chanModes = "".join(["".join(modes.keys()) for modes in self.ircd.channelModes]) chanModes += "".join(self.ircd.channelStatuses.keys()) self.sendMessage(irc.RPL_MYINFO, self.ircd.name, versionWithName, "".join(["".join(modes.keys()) for modes in self.ircd.userModes]), chanModes) self.sendISupport() self.ircd.runActionStandard("welcome", self, users=[self]) def addRegisterHold(self, holdName): """ Adds a register hold to this user if the user is not yet registered. """ if not self._registerHolds: return self._registerHolds.add(holdName) def sendISupport(self): """ Sends ISUPPORT to this user.""" isupportList = self.ircd.generateISupportList() isupportMsgList = splitMessage(" ".join(isupportList), 350) for line in isupportMsgList: lineArgs = line.split(" ") lineArgs.append("are supported by this server") self.sendMessage(irc.RPL_ISUPPORT, *lineArgs) def hostmask(self): """ Returns the user's hostmask. """ return "{}!{}@{}".format(self.nick, self.ident, self.host()) def hostmaskWithRealHost(self): """ Returns the user's hostmask using the user's real host rather than any vhost that may have been applied. """ return "{}!{}@{}".format(self.nick, self.ident, self.realHost) def hostmaskWithIP(self): """ Returns the user's hostmask using the user's IP address instead of the host. """ return "{}!{}@{}".format(self.nick, self.ident, self.ip) def changeNick(self, newNick, fromServer = None): """ Changes this user's nickname. If initiated by a remote server, that server should be specified in the fromServer parameter. """ if newNick == self.nick: return if newNick in self.ircd.userNicks and self.ircd.userNicks[newNick] != self.uuid: return oldNick = self.nick if oldNick and oldNick in self.ircd.userNicks: del self.ircd.userNicks[self.nick] self.nick = newNick self.nickSince = now() if self.isRegistered(): self.ircd.userNicks[self.nick] = self.uuid userSendList = [self] for channel in self.channels: userSendList.extend(channel.users.keys()) userSendList = [u for u in set(userSendList) if u.uuid[:3] == self.ircd.serverID] self.ircd.runActionProcessing("changenickmessage", userSendList, self, oldNick, users=userSendList) self.ircd.runActionStandard("changenick", self, oldNick, fromServer, users=[self]) def changeIdent(self, newIdent, fromServer = None): """ Changes this user's ident. If initiated by a remote server, that server should be specified in the fromServer parameter. """ if newIdent == self.ident: return if len(newIdent) > self.ircd.config.get("ident_length", 12): return oldIdent = self.ident self.ident = newIdent if self.isRegistered(): self.ircd.runActionStandard("changeident", self, oldIdent, fromServer, users=[self]) def host(self): if not self._hostStack: return self.realHost return self._hostsByType[self._hostStack[-1]] def changeHost(self, hostType, newHost, fromServer = None): """ Changes a user's host. If initiated by a remote server, that server should be specified in the fromServer parameter. """ if hostType == "*": return if len(newHost) > self.ircd.config.get("hostname_length", 64): return if hostType in self._hostsByType and self._hostsByType[hostType] == newHost: return oldHost = self.host() self._hostsByType[hostType] = newHost if hostType in self._hostStack: self._hostStack.remove(hostType) self._hostStack.append(hostType) if self.isRegistered(): self.ircd.runComboActionStandard((("changehost", self, hostType, oldHost, fromServer), ("updatehost", self, hostType, oldHost, newHost, fromServer)), users=[self]) def updateHost(self, hostType, newHost, fromServer = None): """ Updates the host of a given host type for the user. If initiated by a remote server, that server should be specified in the fromServer parameter. """ if hostType not in self._hostStack: self.changeHost(hostType, newHost, fromServer) return if hostType == "*": return if len(newHost) > self.ircd.config.get("hostname_length", 64): return if hostType in self._hostsByType and self._hostsByType[hostType] == newHost: return oldHost = self.host() oldHostOfType = None if hostType in self._hostsByType: oldHostOfType = self._hostsByType[hostType] self._hostsByType[hostType] = newHost changedUserHost = (oldHost != self.host()) changedHostOfType = (oldHostOfType != newHost) if self.isRegistered(): if changedUserHost and changedHostOfType: self.ircd.runComboActionStandard((("changehost", self, hostType, oldHost, fromServer), ("updatehost", self, hostType, oldHost, newHost, fromServer)), users=[self]) elif changedHostOfType: self.ircd.runActionStandard("updatehost", self, hostType, oldHost, newHost, fromServer, users=[self]) def resetHost(self, hostType, fromServer = None): """ Resets the user's host to the real host. """ if hostType not in self._hostsByType: return oldHost = self.host() if hostType in self._hostStack: self._hostStack.remove(hostType) del self._hostsByType[hostType] currentHost = self.host() if currentHost != oldHost: self.ircd.runComboActionStandard((("changehost", self, hostType, oldHost, fromServer), ("updatehost", self, hostType, oldHost, None, fromServer)), users=[self]) else: self.ircd.runActionStandard("updatehost", self, hostType, oldHost, None, fromServer, users=[self]) def currentHostType(self): if self._hostStack: return self._hostStack[-1] return "*" def changeGecos(self, newGecos, fromServer = None): """ Changes a user's real name. If initiated by a remote server, that server should be specified in the fromServer parameter. """ if len(newGecos) > self.ircd.config.get("gecos_length", 128): return if newGecos == self.gecos: return oldGecos = self.gecos self.gecos = newGecos if self.isRegistered(): self.ircd.runActionStandard("changegecos", self, oldGecos, fromServer, users=[self]) def metadataKeyExists(self, key): """ Checks whether the specified key exists in the user's metadata. """ return key in self._metadata def metadataKeyCase(self, key): """ Returns the specified key in the user's metadata in its original case. Returns None if the given key is not in the user's metadata. """ if key not in self._metadata: return None return self._metadata[key][0] def metadataValue(self, key): """ Returns the value of the given key in the user's metadata or None if the given key is not in the user's metadata. """ if key not in self._metadata: return None return self._metadata[key][1] def metadataVisibility(self, key): """ Returns the visibility value of the given key in the user's metadata or None if the given key is not in the user's metadata. """ if key not in self._metadata: return None return self._metadata[key][2] def metadataSetByUser(self, key): """ Returns whether the given key in the user's metadata was set by a user or None if the given key is not in the user's metadata. """ if key not in self._metadata: return None return self._metadata[key][3] def metadataList(self): """ Returns the list of metadata keys/values for the user as a list of tuples in the format [ (key, value, visibility, setByUser) ] """ return self._metadata.values() def setMetadata(self, key, value, visibility, setByUser, fromServer = None): """ Sets metadata for the user. If initiated by a remote server, that server should be specified in the fromServer parameter. If the value is None, deletes the metadata at the provided key. """ if not isValidMetadataKey(key): return False oldData = None if key in self._metadata: oldData = self._metadata[key] if setByUser and oldData and not oldData[3]: return False if setByUser and self.ircd.runActionUntilValue("usercansetmetadata", key, users=[self]) is False: return False if value is None: if key in self._metadata: del self._metadata[key] elif not visibility: return False else: self._metadata[key] = (key, value, visibility, setByUser) oldValue = oldData[1] if oldData else None self.ircd.runActionStandard("usermetadataupdate", self, key, oldValue, value, visibility, setByUser, fromServer, users=[self]) return True def canSeeMetadataVisibility(self, visibility): if visibility == "*": return True return self.ircd.runActionUntilValue("usercanseemetadata", self, visibility) is not False def joinChannel(self, channel, override = False): """ Joins the user to a channel. Specify the override parameter only if all permission checks should be bypassed. """ if channel in self.channels: return if not override: if self.ircd.runActionUntilValue("joinpermission", channel, self, users=[self], channels=[channel]) is False: return channel.users[self] = { "status": "" } self.channels.append(channel) newChannel = False if channel.name not in self.ircd.channels: newChannel = True self.ircd.channels[channel.name] = channel self.ircd.recentlyDestroyedChannels[channel.name] = False # We need to send the JOIN message before doing other processing, as chancreate will do things like # mode defaulting, which will send messages about the channel before the JOIN message, which is bad. messageUsers = [u for u in channel.users.iterkeys() if u.uuid[:3] == self.ircd.serverID] self.ircd.runActionProcessing("joinmessage", messageUsers, channel, self, users=messageUsers, channels=[channel]) if newChannel: self.ircd.runActionStandard("channelcreate", channel, self, channels=[channel]) self.ircd.runActionStandard("join", channel, self, users=[self], channels=[channel]) def leaveChannel(self, channel, partType = "PART", typeData = {}, fromServer = None): """ Removes the user from a channel. The partType and typeData are used for the leavemessage action to send the parting message. If the channel leaving is initiated by a remote server, that server should be specified in the fromServer parameter. """ if channel not in self.channels: return messageUsers = [u for u in channel.users.iterkeys() if u.uuid[:3] == self.ircd.serverID] self.ircd.runActionProcessing("leavemessage", messageUsers, channel, self, partType, typeData, fromServer, users=[self], channels=[channel]) self._leaveChannel(channel) def _leaveChannel(self, channel): self.ircd.runActionStandard("leave", channel, self, users=[self], channels=[channel]) self.channels.remove(channel) del channel.users[self] def setModes(self, modes, defaultSource): """ Sets modes on the user. Accepts modes as a list of tuples in the format: [ (adding, mode, param, setBy, setTime) ] - adding: True if we're setting the mode; False if unsetting - mode: The mode letter - param: The mode's parameter; None if no parameter is needed for that mode - setBy: Optional, only used for list modes; a human-readable string (typically server name or nick!user@host) for who/what set this mode) - setTime: Optional, only used for list modes; a datetime object containing when the mode was set The defaultSource is a valid user ID or server ID of someone who set the modes. It is used as the source for announcements about the mode change and as the default setter for any list modes who do not have the setBy parameter specified. The default time for list modes with no setTime specified is now(). """ modeChanges = [] defaultSourceName = self._sourceName(defaultSource) if defaultSourceName is None: raise ValueError ("Source must be a valid user or server ID.") nowTime = now() for modeData in modes: mode = modeData[1] if mode not in self.ircd.userModeTypes: continue setBy = defaultSourceName setTime = nowTime modeType = self.ircd.userModeTypes[mode] adding = modeData[0] if modeType in (ModeType.List, ModeType.ParamOnUnset, ModeType.Param): param = modeData[2] else: param = None if modeType == ModeType.List: dataCount = len(modeData) if dataCount >= 4: setBy = modeData[3] if dataCount >= 5: setTime = modeData[4] if adding: paramList = self.ircd.userModes[modeType][mode].checkSet(self, param) else: paramList = self.ircd.userModes[modeType][mode].checkUnset(self, param) if paramList is None: continue for parameter in paramList: if self._applyMode(adding, modeType, mode, parameter, setBy, setTime): modeChanges.append((adding, mode, parameter, setBy, setTime)) self._notifyModeChanges(modeChanges, defaultSource, defaultSourceName) return modeChanges def setModesByUser(self, user, modes, params, override = False): """ Parses a mode string specified by a user and sets those modes on the user. The user parameter should be the user who set the modes (usually, but not always, this user). The modes parameter is the actual modes string; parameters specified by the user should be as a list of strings in params. The override parameter should be used only when all permission checks should be overridden. """ adding = True changes = [] setBy = self._sourceName(user.uuid) setTime = now() for mode in modes: if len(changes) >= self.ircd.config.get("modes_per_line", 20): break if mode == "+": adding = True continue if mode == "-": adding = False continue if mode not in self.ircd.userModeTypes: user.sendMessage(irc.ERR_UNKNOWNMODE, mode, "is unknown mode char to me") continue modeType = self.ircd.userModeTypes[mode] param = None if modeType in (ModeType.List, ModeType.ParamOnUnset) or (adding and modeType == ModeType.Param): try: param = params.pop(0) except IndexError: if modeType == ModeType.List: self.ircd.userModes[modeType][mode].showListParams(user, self) continue if adding: paramList = self.ircd.userModes[modeType][mode].checkSet(self, param) else: paramList = self.ircd.userModes[modeType][mode].checkUnset(self, param) if paramList is None: continue for parameter in paramList: if len(changes) >= self.ircd.config.get("modes_per_line", 20): break if not override and self.ircd.runActionUntilValue("modepermission-user-{}".format(mode), self, user, adding, parameter, users=[self, user]) is False: continue if adding: if modeType == ModeType.List: if mode in self.modes and len(self.modes[mode]) > self.ircd.config.get("user_listmode_limit", 128): user.sendMessage(irc.ERR_BANLISTFULL, self.name, parameter, "Channel +{} list is full".format(mode)) continue if self._applyMode(adding, modeType, mode, parameter, setBy, setTime): changes.append((adding, mode, parameter, setBy, setTime)) self._notifyModeChanges(changes, user.uuid, setBy) return changes def _applyMode(self, adding, modeType, mode, parameter, setBy, setTime): if parameter: if len(parameter) > 255: return False if " " in parameter: return False if adding: if modeType == ModeType.List: if mode in self.modes: if len(self.modes[mode]) > self.ircd.config.get("user_listmode_limit", 128): return False for paramData in self.modes[mode]: if parameter == paramData[0]: return False else: self.modes[mode] = [] self.modes[mode].append((parameter, setBy, setTime)) return True if mode in self.modes and self.modes[mode] == parameter: return False self.modes[mode] = parameter return True if modeType == ModeType.List: if mode not in self.modes: return False for index, paramData in enumerate(self.modes[mode]): if paramData[0] == parameter: del self.modes[mode][index] break else: return False if not self.modes[mode]: del self.modes[mode] return True if mode not in self.modes: return False if modeType == ModeType.ParamOnUnset and parameter != self.modes[mode]: return False del self.modes[mode] return True def _notifyModeChanges(self, modeChanges, source, sourceName): if not modeChanges: return for change in modeChanges: self.ircd.runActionStandard("modechange-user-{}".format(change[1]), self, change[3], change[0], change[2], users=[self]) users = [] if source in self.ircd.users and source[:3] == self.ircd.serverID: users.append(self.ircd.users[source]) if self.uuid[:3] == self.ircd.serverID: users.append(self) if users: self.ircd.runActionProcessing("modemessage-user", users, self, source, sourceName, modeChanges, users=users) self.ircd.runActionStandard("modechanges-user", self, source, sourceName, modeChanges, users=[self]) def _sourceName(self, source): if source in self.ircd.users: return self.ircd.users[source].hostmask() if source == self.ircd.serverID: return self.ircd.name if source in self.ircd.servers: return self.ircd.servers[source].name return None def modeString(self, toUser): """ Get a user-reportable mode string for the modes set on the user. """ modeStr = ["+"] params = [] for mode in self.modes: modeType = self.ircd.userModeTypes[mode] if modeType not in (ModeType.ParamOnUnset, ModeType.Param, ModeType.NoParam): continue if modeType != ModeType.NoParam: param = None if toUser: param = self.ircd.userModes[modeType][mode].showParam(toUser, self) if not param: param = self.modes[mode] else: param = None modeStr.append(mode) if param: params.append(param) if params: return "{} {}".format("".join(modeStr), " ".join(params)) return "".join(modeStr)