diff options
author | shmel1k <shmel1k@ydb.tech> | 2023-11-26 18:16:14 +0300 |
---|---|---|
committer | shmel1k <shmel1k@ydb.tech> | 2023-11-26 18:43:30 +0300 |
commit | b8cf9e88f4c5c64d9406af533d8948deb050d695 (patch) | |
tree | 218eb61fb3c3b96ec08b4d8cdfef383104a87d63 /contrib/python/Twisted/py2/twisted/words/protocols | |
parent | 523f645a83a0ec97a0332dbc3863bb354c92a328 (diff) | |
download | ydb-b8cf9e88f4c5c64d9406af533d8948deb050d695.tar.gz |
add kikimr_configure
Diffstat (limited to 'contrib/python/Twisted/py2/twisted/words/protocols')
13 files changed, 7729 insertions, 0 deletions
diff --git a/contrib/python/Twisted/py2/twisted/words/protocols/__init__.py b/contrib/python/Twisted/py2/twisted/words/protocols/__init__.py new file mode 100644 index 0000000000..59dc76c0f6 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/words/protocols/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Chat protocols. +""" diff --git a/contrib/python/Twisted/py2/twisted/words/protocols/irc.py b/contrib/python/Twisted/py2/twisted/words/protocols/irc.py new file mode 100644 index 0000000000..389e10b9ba --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/words/protocols/irc.py @@ -0,0 +1,4074 @@ +# -*- test-case-name: twisted.words.test.test_irc -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Internet Relay Chat protocol for client and server. + +Future Plans +============ + +The way the IRCClient class works here encourages people to implement +IRC clients by subclassing the ephemeral protocol class, and it tends +to end up with way more state than it should for an object which will +be destroyed as soon as the TCP transport drops. Someone oughta do +something about that, ya know? + +The DCC support needs to have more hooks for the client for it to be +able to ask the user things like "Do you want to accept this session?" +and "Transfer #2 is 67% done." and otherwise manage the DCC sessions. + +Test coverage needs to be better. + +@var MAX_COMMAND_LENGTH: The maximum length of a command, as defined by RFC + 2812 section 2.3. + +@var attributes: Singleton instance of L{_CharacterAttributes}, used for + constructing formatted text information. + +@author: Kevin Turner + +@see: RFC 1459: Internet Relay Chat Protocol +@see: RFC 2812: Internet Relay Chat: Client Protocol +@see: U{The Client-To-Client-Protocol +<http://www.irchelp.org/irchelp/rfc/ctcpspec.html>} +""" + +import errno, os, random, re, stat, struct, sys, time, traceback +import operator +import string, socket +import textwrap +import shlex +from functools import reduce +from os import path + +from twisted.internet import reactor, protocol, task +from twisted.persisted import styles +from twisted.protocols import basic +from twisted.python import log, reflect, _textattributes +from twisted.python.compat import unicode, range + +NUL = chr(0) +CR = chr(0o15) +NL = chr(0o12) +LF = NL +SPC = chr(0o40) + +# This includes the CRLF terminator characters. +MAX_COMMAND_LENGTH = 512 + +CHANNEL_PREFIXES = '&#!+' + +class IRCBadMessage(Exception): + pass + +class IRCPasswordMismatch(Exception): + pass + + + +class IRCBadModes(ValueError): + """ + A malformed mode was encountered while attempting to parse a mode string. + """ + + + +def parsemsg(s): + """ + Breaks a message from an IRC server into its prefix, command, and + arguments. + + @param s: The message to break. + @type s: L{bytes} + + @return: A tuple of (prefix, command, args). + @rtype: L{tuple} + """ + prefix = '' + trailing = [] + if not s: + raise IRCBadMessage("Empty line.") + if s[0:1] == ':': + prefix, s = s[1:].split(' ', 1) + if s.find(' :') != -1: + s, trailing = s.split(' :', 1) + args = s.split() + args.append(trailing) + else: + args = s.split() + command = args.pop(0) + return prefix, command, args + + + +def split(str, length=80): + """ + Split a string into multiple lines. + + Whitespace near C{str[length]} will be preferred as a breaking point. + C{"\\n"} will also be used as a breaking point. + + @param str: The string to split. + @type str: C{str} + + @param length: The maximum length which will be allowed for any string in + the result. + @type length: C{int} + + @return: C{list} of C{str} + """ + return [chunk + for line in str.split('\n') + for chunk in textwrap.wrap(line, length)] + + +def _intOrDefault(value, default=None): + """ + Convert a value to an integer if possible. + + @rtype: C{int} or type of L{default} + @return: An integer when C{value} can be converted to an integer, + otherwise return C{default} + """ + if value: + try: + return int(value) + except (TypeError, ValueError): + pass + return default + + + +class UnhandledCommand(RuntimeError): + """ + A command dispatcher could not locate an appropriate command handler. + """ + + + +class _CommandDispatcherMixin(object): + """ + Dispatch commands to handlers based on their name. + + Command handler names should be of the form C{prefix_commandName}, + where C{prefix} is the value specified by L{prefix}, and must + accept the parameters as given to L{dispatch}. + + Attempting to mix this in more than once for a single class will cause + strange behaviour, due to L{prefix} being overwritten. + + @type prefix: C{str} + @ivar prefix: Command handler prefix, used to locate handler attributes + """ + prefix = None + + def dispatch(self, commandName, *args): + """ + Perform actual command dispatch. + """ + def _getMethodName(command): + return '%s_%s' % (self.prefix, command) + + def _getMethod(name): + return getattr(self, _getMethodName(name), None) + + method = _getMethod(commandName) + if method is not None: + return method(*args) + + method = _getMethod('unknown') + if method is None: + raise UnhandledCommand("No handler for %r could be found" % (_getMethodName(commandName),)) + return method(commandName, *args) + + + + + +def parseModes(modes, params, paramModes=('', '')): + """ + Parse an IRC mode string. + + The mode string is parsed into two lists of mode changes (added and + removed), with each mode change represented as C{(mode, param)} where mode + is the mode character, and param is the parameter passed for that mode, or + L{None} if no parameter is required. + + @type modes: C{str} + @param modes: Modes string to parse. + + @type params: C{list} + @param params: Parameters specified along with L{modes}. + + @type paramModes: C{(str, str)} + @param paramModes: A pair of strings (C{(add, remove)}) that indicate which modes take + parameters when added or removed. + + @returns: Two lists of mode changes, one for modes added and the other for + modes removed respectively, mode changes in each list are represented as + C{(mode, param)}. + """ + if len(modes) == 0: + raise IRCBadModes('Empty mode string') + + if modes[0] not in '+-': + raise IRCBadModes('Malformed modes string: %r' % (modes,)) + + changes = ([], []) + + direction = None + count = -1 + for ch in modes: + if ch in '+-': + if count == 0: + raise IRCBadModes('Empty mode sequence: %r' % (modes,)) + direction = '+-'.index(ch) + count = 0 + else: + param = None + if ch in paramModes[direction]: + try: + param = params.pop(0) + except IndexError: + raise IRCBadModes('Not enough parameters: %r' % (ch,)) + changes[direction].append((ch, param)) + count += 1 + + if len(params) > 0: + raise IRCBadModes('Too many parameters: %r %r' % (modes, params)) + + if count == 0: + raise IRCBadModes('Empty mode sequence: %r' % (modes,)) + + return changes + + + +class IRC(protocol.Protocol): + """ + Internet Relay Chat server protocol. + """ + + buffer = "" + hostname = None + + encoding = None + + def connectionMade(self): + self.channels = [] + if self.hostname is None: + self.hostname = socket.getfqdn() + + + def sendLine(self, line): + line = line + CR + LF + if isinstance(line, unicode): + useEncoding = self.encoding if self.encoding else "utf-8" + line = line.encode(useEncoding) + self.transport.write(line) + + + def sendMessage(self, command, *parameter_list, **prefix): + """ + Send a line formatted as an IRC message. + + First argument is the command, all subsequent arguments are parameters + to that command. If a prefix is desired, it may be specified with the + keyword argument 'prefix'. + + The L{sendCommand} method is generally preferred over this one. + Notably, this method does not support sending message tags, while the + L{sendCommand} method does. + """ + if not command: + raise ValueError("IRC message requires a command.") + + if ' ' in command or command[0] == ':': + # Not the ONLY way to screw up, but provides a little + # sanity checking to catch likely dumb mistakes. + raise ValueError("Somebody screwed up, 'cuz this doesn't" \ + " look like a command to me: %s" % command) + + line = ' '.join([command] + list(parameter_list)) + if 'prefix' in prefix: + line = ":%s %s" % (prefix['prefix'], line) + self.sendLine(line) + + if len(parameter_list) > 15: + log.msg("Message has %d parameters (RFC allows 15):\n%s" % + (len(parameter_list), line)) + + + def sendCommand(self, command, parameters, prefix=None, tags=None): + """ + Send to the remote peer a line formatted as an IRC message. + + @param command: The command or numeric to send. + @type command: L{unicode} + + @param parameters: The parameters to send with the command. + @type parameters: A L{tuple} or L{list} of L{unicode} parameters + + @param prefix: The prefix to send with the command. If not + given, no prefix is sent. + @type prefix: L{unicode} + + @param tags: A dict of message tags. If not given, no message + tags are sent. The dict key should be the name of the tag + to send as a string; the value should be the unescaped value + to send with the tag, or either None or "" if no value is to + be sent with the tag. + @type tags: L{dict} of tags (L{unicode}) => values (L{unicode}) + @see: U{https://ircv3.net/specs/core/message-tags-3.2.html} + """ + if not command: + raise ValueError("IRC message requires a command.") + + if " " in command or command[0] == ":": + # Not the ONLY way to screw up, but provides a little + # sanity checking to catch likely dumb mistakes. + raise ValueError('Invalid command: "%s"' % (command,)) + + if tags is None: + tags = {} + + line = " ".join([command] + list(parameters)) + if prefix: + line = ":%s %s" % (prefix, line) + if tags: + tagStr = self._stringTags(tags) + line = "@%s %s" % (tagStr, line) + self.sendLine(line) + + if len(parameters) > 15: + log.msg("Message has %d parameters (RFC allows 15):\n%s" % + (len(parameters), line)) + + + def _stringTags(self, tags): + """ + Converts a tag dictionary to a string. + + @param tags: The tag dict passed to sendMsg. + + @rtype: L{unicode} + @return: IRCv3-format tag string + """ + self._validateTags(tags) + tagStrings = [] + for tag, value in tags.items(): + if value: + tagStrings.append("%s=%s" % (tag, self._escapeTagValue(value))) + else: + tagStrings.append(tag) + return ";".join(tagStrings) + + + def _validateTags(self, tags): + """ + Checks the tag dict for errors and raises L{ValueError} if an + error is found. + + @param tags: The tag dict passed to sendMsg. + """ + for tag, value in tags.items(): + if not tag: + raise ValueError("A tag name is required.") + for char in tag: + if not char.isalnum() and char not in ("-", "/", "."): + raise ValueError("Tag contains invalid characters.") + + + def _escapeTagValue(self, value): + """ + Escape the given tag value according to U{escaping rules in IRCv3 + <https://ircv3.net/specs/core/message-tags-3.2.html>}. + + @param value: The string value to escape. + @type value: L{str} + + @return: The escaped string for sending as a message value + @rtype: L{str} + """ + return (value.replace("\\", "\\\\") + .replace(";", "\\:") + .replace(" ", "\\s") + .replace("\r", "\\r") + .replace("\n", "\\n") + ) + + + def dataReceived(self, data): + """ + This hack is to support mIRC, which sends LF only, even though the RFC + says CRLF. (Also, the flexibility of LineReceiver to turn "line mode" + on and off was not required.) + """ + if isinstance(data, bytes): + data = data.decode("utf-8") + lines = (self.buffer + data).split(LF) + # Put the (possibly empty) element after the last LF back in the + # buffer + self.buffer = lines.pop() + + for line in lines: + if len(line) <= 2: + # This is a blank line, at best. + continue + if line[-1] == CR: + line = line[:-1] + prefix, command, params = parsemsg(line) + # mIRC is a big pile of doo-doo + command = command.upper() + # DEBUG: log.msg( "%s %s %s" % (prefix, command, params)) + + self.handleCommand(command, prefix, params) + + + def handleCommand(self, command, prefix, params): + """ + Determine the function to call for the given command and call it with + the given arguments. + + @param command: The IRC command to determine the function for. + @type command: L{bytes} + + @param prefix: The prefix of the IRC message (as returned by + L{parsemsg}). + @type prefix: L{bytes} + + @param params: A list of parameters to call the function with. + @type params: L{list} + """ + method = getattr(self, "irc_%s" % command, None) + try: + if method is not None: + method(prefix, params) + else: + self.irc_unknown(prefix, command, params) + except: + log.deferr() + + + def irc_unknown(self, prefix, command, params): + """ + Called by L{handleCommand} on a command that doesn't have a defined + handler. Subclasses should override this method. + """ + raise NotImplementedError(command, prefix, params) + + + # Helper methods + def privmsg(self, sender, recip, message): + """ + Send a message to a channel or user + + @type sender: C{str} or C{unicode} + @param sender: Who is sending this message. Should be of the form + username!ident@hostmask (unless you know better!). + + @type recip: C{str} or C{unicode} + @param recip: The recipient of this message. If a channel, it must + start with a channel prefix. + + @type message: C{str} or C{unicode} + @param message: The message being sent. + """ + self.sendCommand("PRIVMSG", (recip, ":%s" % (lowQuote(message),)), sender) + + + def notice(self, sender, recip, message): + """ + Send a "notice" to a channel or user. + + Notices differ from privmsgs in that the RFC claims they are different. + Robots are supposed to send notices and not respond to them. Clients + typically display notices differently from privmsgs. + + @type sender: C{str} or C{unicode} + @param sender: Who is sending this message. Should be of the form + username!ident@hostmask (unless you know better!). + + @type recip: C{str} or C{unicode} + @param recip: The recipient of this message. If a channel, it must + start with a channel prefix. + + @type message: C{str} or C{unicode} + @param message: The message being sent. + """ + self.sendCommand("NOTICE", (recip, ":%s" % (message,)), sender) + + + def action(self, sender, recip, message): + """ + Send an action to a channel or user. + + @type sender: C{str} or C{unicode} + @param sender: Who is sending this message. Should be of the form + username!ident@hostmask (unless you know better!). + + @type recip: C{str} or C{unicode} + @param recip: The recipient of this message. If a channel, it must + start with a channel prefix. + + @type message: C{str} or C{unicode} + @param message: The action being sent. + """ + self.sendLine(":%s ACTION %s :%s" % (sender, recip, message)) + + + def topic(self, user, channel, topic, author=None): + """ + Send the topic to a user. + + @type user: C{str} or C{unicode} + @param user: The user receiving the topic. Only their nickname, not + the full hostmask. + + @type channel: C{str} or C{unicode} + @param channel: The channel for which this is the topic. + + @type topic: C{str} or C{unicode} or L{None} + @param topic: The topic string, unquoted, or None if there is no topic. + + @type author: C{str} or C{unicode} + @param author: If the topic is being changed, the full username and + hostmask of the person changing it. + """ + if author is None: + if topic is None: + self.sendLine(':%s %s %s %s :%s' % ( + self.hostname, RPL_NOTOPIC, user, channel, 'No topic is set.')) + else: + self.sendLine(":%s %s %s %s :%s" % ( + self.hostname, RPL_TOPIC, user, channel, lowQuote(topic))) + else: + self.sendLine(":%s TOPIC %s :%s" % (author, channel, lowQuote(topic))) + + + def topicAuthor(self, user, channel, author, date): + """ + Send the author of and time at which a topic was set for the given + channel. + + This sends a 333 reply message, which is not part of the IRC RFC. + + @type user: C{str} or C{unicode} + @param user: The user receiving the topic. Only their nickname, not + the full hostmask. + + @type channel: C{str} or C{unicode} + @param channel: The channel for which this information is relevant. + + @type author: C{str} or C{unicode} + @param author: The nickname (without hostmask) of the user who last set + the topic. + + @type date: C{int} + @param date: A POSIX timestamp (number of seconds since the epoch) at + which the topic was last set. + """ + self.sendLine(':%s %d %s %s %s %d' % ( + self.hostname, 333, user, channel, author, date)) + + + def names(self, user, channel, names): + """ + Send the names of a channel's participants to a user. + + @type user: C{str} or C{unicode} + @param user: The user receiving the name list. Only their nickname, + not the full hostmask. + + @type channel: C{str} or C{unicode} + @param channel: The channel for which this is the namelist. + + @type names: C{list} of C{str} or C{unicode} + @param names: The names to send. + """ + # XXX If unicode is given, these limits are not quite correct + prefixLength = len(channel) + len(user) + 10 + namesLength = 512 - prefixLength + + L = [] + count = 0 + for n in names: + if count + len(n) + 1 > namesLength: + self.sendLine(":%s %s %s = %s :%s" % ( + self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L))) + L = [n] + count = len(n) + else: + L.append(n) + count += len(n) + 1 + if L: + self.sendLine(":%s %s %s = %s :%s" % ( + self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L))) + self.sendLine(":%s %s %s %s :End of /NAMES list" % ( + self.hostname, RPL_ENDOFNAMES, user, channel)) + + + def who(self, user, channel, memberInfo): + """ + Send a list of users participating in a channel. + + @type user: C{str} or C{unicode} + @param user: The user receiving this member information. Only their + nickname, not the full hostmask. + + @type channel: C{str} or C{unicode} + @param channel: The channel for which this is the member information. + + @type memberInfo: C{list} of C{tuples} + @param memberInfo: For each member of the given channel, a 7-tuple + containing their username, their hostmask, the server to which they + are connected, their nickname, the letter "H" or "G" (standing for + "Here" or "Gone"), the hopcount from C{user} to this member, and + this member's real name. + """ + for info in memberInfo: + (username, hostmask, server, nickname, flag, hops, realName) = info + assert flag in ("H", "G") + self.sendLine(":%s %s %s %s %s %s %s %s %s :%d %s" % ( + self.hostname, RPL_WHOREPLY, user, channel, + username, hostmask, server, nickname, flag, hops, realName)) + + self.sendLine(":%s %s %s %s :End of /WHO list." % ( + self.hostname, RPL_ENDOFWHO, user, channel)) + + + def whois(self, user, nick, username, hostname, realName, server, serverInfo, oper, idle, signOn, channels): + """ + Send information about the state of a particular user. + + @type user: C{str} or C{unicode} + @param user: The user receiving this information. Only their nickname, + not the full hostmask. + + @type nick: C{str} or C{unicode} + @param nick: The nickname of the user this information describes. + + @type username: C{str} or C{unicode} + @param username: The user's username (eg, ident response) + + @type hostname: C{str} + @param hostname: The user's hostmask + + @type realName: C{str} or C{unicode} + @param realName: The user's real name + + @type server: C{str} or C{unicode} + @param server: The name of the server to which the user is connected + + @type serverInfo: C{str} or C{unicode} + @param serverInfo: A descriptive string about that server + + @type oper: C{bool} + @param oper: Indicates whether the user is an IRC operator + + @type idle: C{int} + @param idle: The number of seconds since the user last sent a message + + @type signOn: C{int} + @param signOn: A POSIX timestamp (number of seconds since the epoch) + indicating the time the user signed on + + @type channels: C{list} of C{str} or C{unicode} + @param channels: A list of the channels which the user is participating in + """ + self.sendLine(":%s %s %s %s %s %s * :%s" % ( + self.hostname, RPL_WHOISUSER, user, nick, username, hostname, realName)) + self.sendLine(":%s %s %s %s %s :%s" % ( + self.hostname, RPL_WHOISSERVER, user, nick, server, serverInfo)) + if oper: + self.sendLine(":%s %s %s %s :is an IRC operator" % ( + self.hostname, RPL_WHOISOPERATOR, user, nick)) + self.sendLine(":%s %s %s %s %d %d :seconds idle, signon time" % ( + self.hostname, RPL_WHOISIDLE, user, nick, idle, signOn)) + self.sendLine(":%s %s %s %s :%s" % ( + self.hostname, RPL_WHOISCHANNELS, user, nick, ' '.join(channels))) + self.sendLine(":%s %s %s %s :End of WHOIS list." % ( + self.hostname, RPL_ENDOFWHOIS, user, nick)) + + + def join(self, who, where): + """ + Send a join message. + + @type who: C{str} or C{unicode} + @param who: The name of the user joining. Should be of the form + username!ident@hostmask (unless you know better!). + + @type where: C{str} or C{unicode} + @param where: The channel the user is joining. + """ + self.sendLine(":%s JOIN %s" % (who, where)) + + + def part(self, who, where, reason=None): + """ + Send a part message. + + @type who: C{str} or C{unicode} + @param who: The name of the user joining. Should be of the form + username!ident@hostmask (unless you know better!). + + @type where: C{str} or C{unicode} + @param where: The channel the user is joining. + + @type reason: C{str} or C{unicode} + @param reason: A string describing the misery which caused this poor + soul to depart. + """ + if reason: + self.sendLine(":%s PART %s :%s" % (who, where, reason)) + else: + self.sendLine(":%s PART %s" % (who, where)) + + + def channelMode(self, user, channel, mode, *args): + """ + Send information about the mode of a channel. + + @type user: C{str} or C{unicode} + @param user: The user receiving the name list. Only their nickname, + not the full hostmask. + + @type channel: C{str} or C{unicode} + @param channel: The channel for which this is the namelist. + + @type mode: C{str} + @param mode: A string describing this channel's modes. + + @param args: Any additional arguments required by the modes. + """ + self.sendLine(":%s %s %s %s %s %s" % ( + self.hostname, RPL_CHANNELMODEIS, user, channel, mode, ' '.join(args))) + + + +class ServerSupportedFeatures(_CommandDispatcherMixin): + """ + Handle ISUPPORT messages. + + Feature names match those in the ISUPPORT RFC draft identically. + + Information regarding the specifics of ISUPPORT was gleaned from + <http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt>. + """ + prefix = 'isupport' + + def __init__(self): + self._features = { + 'CHANNELLEN': 200, + 'CHANTYPES': tuple('#&'), + 'MODES': 3, + 'NICKLEN': 9, + 'PREFIX': self._parsePrefixParam('(ovh)@+%'), + # The ISUPPORT draft explicitly says that there is no default for + # CHANMODES, but we're defaulting it here to handle the case where + # the IRC server doesn't send us any ISUPPORT information, since + # IRCClient.getChannelModeParams relies on this value. + 'CHANMODES': self._parseChanModesParam(['b', '', 'lk', ''])} + + + @classmethod + def _splitParamArgs(cls, params, valueProcessor=None): + """ + Split ISUPPORT parameter arguments. + + Values can optionally be processed by C{valueProcessor}. + + For example:: + + >>> ServerSupportedFeatures._splitParamArgs(['A:1', 'B:2']) + (('A', '1'), ('B', '2')) + + @type params: C{iterable} of C{str} + + @type valueProcessor: C{callable} taking {str} + @param valueProcessor: Callable to process argument values, or L{None} + to perform no processing + + @rtype: C{list} of C{(str, object)} + @return: Sequence of C{(name, processedValue)} + """ + if valueProcessor is None: + valueProcessor = lambda x: x + + def _parse(): + for param in params: + if ':' not in param: + param += ':' + a, b = param.split(':', 1) + yield a, valueProcessor(b) + return list(_parse()) + + + @classmethod + def _unescapeParamValue(cls, value): + """ + Unescape an ISUPPORT parameter. + + The only form of supported escape is C{\\xHH}, where HH must be a valid + 2-digit hexadecimal number. + + @rtype: C{str} + """ + def _unescape(): + parts = value.split('\\x') + # The first part can never be preceded by the escape. + yield parts.pop(0) + for s in parts: + octet, rest = s[:2], s[2:] + try: + octet = int(octet, 16) + except ValueError: + raise ValueError('Invalid hex octet: %r' % (octet,)) + yield chr(octet) + rest + + if '\\x' not in value: + return value + return ''.join(_unescape()) + + + @classmethod + def _splitParam(cls, param): + """ + Split an ISUPPORT parameter. + + @type param: C{str} + + @rtype: C{(str, list)} + @return C{(key, arguments)} + """ + if '=' not in param: + param += '=' + key, value = param.split('=', 1) + return key, [cls._unescapeParamValue(v) for v in value.split(',')] + + + @classmethod + def _parsePrefixParam(cls, prefix): + """ + Parse the ISUPPORT "PREFIX" parameter. + + The order in which the parameter arguments appear is significant, the + earlier a mode appears the more privileges it gives. + + @rtype: C{dict} mapping C{str} to C{(str, int)} + @return: A dictionary mapping a mode character to a two-tuple of + C({symbol, priority)}, the lower a priority (the lowest being + C{0}) the more privileges it gives + """ + if not prefix: + return None + if prefix[0] != '(' and ')' not in prefix: + raise ValueError('Malformed PREFIX parameter') + modes, symbols = prefix.split(')', 1) + symbols = zip(symbols, range(len(symbols))) + modes = modes[1:] + return dict(zip(modes, symbols)) + + + @classmethod + def _parseChanModesParam(self, params): + """ + Parse the ISUPPORT "CHANMODES" parameter. + + See L{isupport_CHANMODES} for a detailed explanation of this parameter. + """ + names = ('addressModes', 'param', 'setParam', 'noParam') + if len(params) > len(names): + raise ValueError( + 'Expecting a maximum of %d channel mode parameters, got %d' % ( + len(names), len(params))) + items = map(lambda key, value: (key, value or ''), names, params) + return dict(items) + + + def getFeature(self, feature, default=None): + """ + Get a server supported feature's value. + + A feature with the value L{None} is equivalent to the feature being + unsupported. + + @type feature: C{str} + @param feature: Feature name + + @type default: C{object} + @param default: The value to default to, assuming that C{feature} + is not supported + + @return: Feature value + """ + return self._features.get(feature, default) + + + def hasFeature(self, feature): + """ + Determine whether a feature is supported or not. + + @rtype: C{bool} + """ + return self.getFeature(feature) is not None + + + def parse(self, params): + """ + Parse ISUPPORT parameters. + + If an unknown parameter is encountered, it is simply added to the + dictionary, keyed by its name, as a tuple of the parameters provided. + + @type params: C{iterable} of C{str} + @param params: Iterable of ISUPPORT parameters to parse + """ + for param in params: + key, value = self._splitParam(param) + if key.startswith('-'): + self._features.pop(key[1:], None) + else: + self._features[key] = self.dispatch(key, value) + + + def isupport_unknown(self, command, params): + """ + Unknown ISUPPORT parameter. + """ + return tuple(params) + + + def isupport_CHANLIMIT(self, params): + """ + The maximum number of each channel type a user may join. + """ + return self._splitParamArgs(params, _intOrDefault) + + + def isupport_CHANMODES(self, params): + """ + Available channel modes. + + There are 4 categories of channel mode:: + + addressModes - Modes that add or remove an address to or from a + list, these modes always take a parameter. + + param - Modes that change a setting on a channel, these modes + always take a parameter. + + setParam - Modes that change a setting on a channel, these modes + only take a parameter when being set. + + noParam - Modes that change a setting on a channel, these modes + never take a parameter. + """ + try: + return self._parseChanModesParam(params) + except ValueError: + return self.getFeature('CHANMODES') + + + def isupport_CHANNELLEN(self, params): + """ + Maximum length of a channel name a client may create. + """ + return _intOrDefault(params[0], self.getFeature('CHANNELLEN')) + + + def isupport_CHANTYPES(self, params): + """ + Valid channel prefixes. + """ + return tuple(params[0]) + + + def isupport_EXCEPTS(self, params): + """ + Mode character for "ban exceptions". + + The presence of this parameter indicates that the server supports + this functionality. + """ + return params[0] or 'e' + + + def isupport_IDCHAN(self, params): + """ + Safe channel identifiers. + + The presence of this parameter indicates that the server supports + this functionality. + """ + return self._splitParamArgs(params) + + + def isupport_INVEX(self, params): + """ + Mode character for "invite exceptions". + + The presence of this parameter indicates that the server supports + this functionality. + """ + return params[0] or 'I' + + + def isupport_KICKLEN(self, params): + """ + Maximum length of a kick message a client may provide. + """ + return _intOrDefault(params[0]) + + + def isupport_MAXLIST(self, params): + """ + Maximum number of "list modes" a client may set on a channel at once. + + List modes are identified by the "addressModes" key in CHANMODES. + """ + return self._splitParamArgs(params, _intOrDefault) + + + def isupport_MODES(self, params): + """ + Maximum number of modes accepting parameters that may be sent, by a + client, in a single MODE command. + """ + return _intOrDefault(params[0]) + + + def isupport_NETWORK(self, params): + """ + IRC network name. + """ + return params[0] + + + def isupport_NICKLEN(self, params): + """ + Maximum length of a nickname the client may use. + """ + return _intOrDefault(params[0], self.getFeature('NICKLEN')) + + + def isupport_PREFIX(self, params): + """ + Mapping of channel modes that clients may have to status flags. + """ + try: + return self._parsePrefixParam(params[0]) + except ValueError: + return self.getFeature('PREFIX') + + + def isupport_SAFELIST(self, params): + """ + Flag indicating that a client may request a LIST without being + disconnected due to the large amount of data generated. + """ + return True + + + def isupport_STATUSMSG(self, params): + """ + The server supports sending messages to only to clients on a channel + with a specific status. + """ + return params[0] + + + def isupport_TARGMAX(self, params): + """ + Maximum number of targets allowable for commands that accept multiple + targets. + """ + return dict(self._splitParamArgs(params, _intOrDefault)) + + + def isupport_TOPICLEN(self, params): + """ + Maximum length of a topic that may be set. + """ + return _intOrDefault(params[0]) + + + +class IRCClient(basic.LineReceiver): + """ + Internet Relay Chat client protocol, with sprinkles. + + In addition to providing an interface for an IRC client protocol, + this class also contains reasonable implementations of many common + CTCP methods. + + TODO + ==== + - Limit the length of messages sent (because the IRC server probably + does). + - Add flood protection/rate limiting for my CTCP replies. + - NickServ cooperation. (a mix-in?) + + @ivar nickname: Nickname the client will use. + @ivar password: Password used to log on to the server. May be L{None}. + @ivar realname: Supplied to the server during login as the "Real name" + or "ircname". May be L{None}. + @ivar username: Supplied to the server during login as the "User name". + May be L{None} + + @ivar userinfo: Sent in reply to a C{USERINFO} CTCP query. If L{None}, no + USERINFO reply will be sent. + "This is used to transmit a string which is settable by + the user (and never should be set by the client)." + @ivar fingerReply: Sent in reply to a C{FINGER} CTCP query. If L{None}, no + FINGER reply will be sent. + @type fingerReply: Callable or String + + @ivar versionName: CTCP VERSION reply, client name. If L{None}, no VERSION + reply will be sent. + @type versionName: C{str}, or None. + @ivar versionNum: CTCP VERSION reply, client version. + @type versionNum: C{str}, or None. + @ivar versionEnv: CTCP VERSION reply, environment the client is running in. + @type versionEnv: C{str}, or None. + + @ivar sourceURL: CTCP SOURCE reply, a URL where the source code of this + client may be found. If L{None}, no SOURCE reply will be sent. + + @ivar lineRate: Minimum delay between lines sent to the server. If + L{None}, no delay will be imposed. + @type lineRate: Number of Seconds. + + @ivar motd: Either L{None} or, between receipt of I{RPL_MOTDSTART} and + I{RPL_ENDOFMOTD}, a L{list} of L{str}, each of which is the content + of an I{RPL_MOTD} message. + + @ivar erroneousNickFallback: Default nickname assigned when an unregistered + client triggers an C{ERR_ERRONEUSNICKNAME} while trying to register + with an illegal nickname. + @type erroneousNickFallback: C{str} + + @ivar _registered: Whether or not the user is registered. It becomes True + once a welcome has been received from the server. + @type _registered: C{bool} + + @ivar _attemptedNick: The nickname that will try to get registered. It may + change if it is illegal or already taken. L{nickname} becomes the + L{_attemptedNick} that is successfully registered. + @type _attemptedNick: C{str} + + @type supported: L{ServerSupportedFeatures} + @ivar supported: Available ISUPPORT features on the server + + @type hostname: C{str} + @ivar hostname: Host name of the IRC server the client is connected to. + Initially the host name is L{None} and later is set to the host name + from which the I{RPL_WELCOME} message is received. + + @type _heartbeat: L{task.LoopingCall} + @ivar _heartbeat: Looping call to perform the keepalive by calling + L{IRCClient._sendHeartbeat} every L{heartbeatInterval} seconds, or + L{None} if there is no heartbeat. + + @type heartbeatInterval: C{float} + @ivar heartbeatInterval: Interval, in seconds, to send I{PING} messages to + the server as a form of keepalive, defaults to 120 seconds. Use L{None} + to disable the heartbeat. + """ + hostname = None + motd = None + nickname = 'irc' + password = None + realname = None + username = None + ### Responses to various CTCP queries. + + userinfo = None + # fingerReply is a callable returning a string, or a str()able object. + fingerReply = None + versionName = None + versionNum = None + versionEnv = None + + sourceURL = "http://twistedmatrix.com/downloads/" + + dcc_destdir = '.' + dcc_sessions = None + + # If this is false, no attempt will be made to identify + # ourself to the server. + performLogin = 1 + + lineRate = None + _queue = None + _queueEmptying = None + + delimiter = b'\n' # b'\r\n' will also work (see dataReceived) + + __pychecker__ = 'unusednames=params,prefix,channel' + + _registered = False + _attemptedNick = '' + erroneousNickFallback = 'defaultnick' + + _heartbeat = None + heartbeatInterval = 120 + + + def _reallySendLine(self, line): + quoteLine = lowQuote(line) + if isinstance(quoteLine, unicode): + quoteLine = quoteLine.encode("utf-8") + quoteLine += b'\r' + return basic.LineReceiver.sendLine(self, quoteLine) + + def sendLine(self, line): + if self.lineRate is None: + self._reallySendLine(line) + else: + self._queue.append(line) + if not self._queueEmptying: + self._sendLine() + + def _sendLine(self): + if self._queue: + self._reallySendLine(self._queue.pop(0)) + self._queueEmptying = reactor.callLater(self.lineRate, + self._sendLine) + else: + self._queueEmptying = None + + + def connectionLost(self, reason): + basic.LineReceiver.connectionLost(self, reason) + self.stopHeartbeat() + + + def _createHeartbeat(self): + """ + Create the heartbeat L{LoopingCall}. + """ + return task.LoopingCall(self._sendHeartbeat) + + + def _sendHeartbeat(self): + """ + Send a I{PING} message to the IRC server as a form of keepalive. + """ + self.sendLine('PING ' + self.hostname) + + + def stopHeartbeat(self): + """ + Stop sending I{PING} messages to keep the connection to the server + alive. + + @since: 11.1 + """ + if self._heartbeat is not None: + self._heartbeat.stop() + self._heartbeat = None + + + def startHeartbeat(self): + """ + Start sending I{PING} messages every L{IRCClient.heartbeatInterval} + seconds to keep the connection to the server alive during periods of no + activity. + + @since: 11.1 + """ + self.stopHeartbeat() + if self.heartbeatInterval is None: + return + self._heartbeat = self._createHeartbeat() + self._heartbeat.start(self.heartbeatInterval, now=False) + + + ### Interface level client->user output methods + ### + ### You'll want to override these. + + ### Methods relating to the server itself + + def created(self, when): + """ + Called with creation date information about the server, usually at logon. + + @type when: C{str} + @param when: A string describing when the server was created, probably. + """ + + def yourHost(self, info): + """ + Called with daemon information about the server, usually at logon. + + @type info: C{str} + @param when: A string describing what software the server is running, probably. + """ + + def myInfo(self, servername, version, umodes, cmodes): + """ + Called with information about the server, usually at logon. + + @type servername: C{str} + @param servername: The hostname of this server. + + @type version: C{str} + @param version: A description of what software this server runs. + + @type umodes: C{str} + @param umodes: All the available user modes. + + @type cmodes: C{str} + @param cmodes: All the available channel modes. + """ + + def luserClient(self, info): + """ + Called with information about the number of connections, usually at logon. + + @type info: C{str} + @param info: A description of the number of clients and servers + connected to the network, probably. + """ + + def bounce(self, info): + """ + Called with information about where the client should reconnect. + + @type info: C{str} + @param info: A plaintext description of the address that should be + connected to. + """ + + def isupport(self, options): + """ + Called with various information about what the server supports. + + @type options: C{list} of C{str} + @param options: Descriptions of features or limits of the server, possibly + in the form "NAME=VALUE". + """ + + def luserChannels(self, channels): + """ + Called with the number of channels existent on the server. + + @type channels: C{int} + """ + + def luserOp(self, ops): + """ + Called with the number of ops logged on to the server. + + @type ops: C{int} + """ + + def luserMe(self, info): + """ + Called with information about the server connected to. + + @type info: C{str} + @param info: A plaintext string describing the number of users and servers + connected to this server. + """ + + ### Methods involving me directly + + def privmsg(self, user, channel, message): + """ + Called when I have a message from a user to me or a channel. + """ + pass + + def joined(self, channel): + """ + Called when I finish joining a channel. + + channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'}) + intact. + """ + + def left(self, channel): + """ + Called when I have left a channel. + + channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'}) + intact. + """ + + + def noticed(self, user, channel, message): + """ + Called when I have a notice from a user to me or a channel. + + If the client makes any automated replies, it must not do so in + response to a NOTICE message, per the RFC:: + + The difference between NOTICE and PRIVMSG is that + automatic replies MUST NEVER be sent in response to a + NOTICE message. [...] The object of this rule is to avoid + loops between clients automatically sending something in + response to something it received. + """ + + + def modeChanged(self, user, channel, set, modes, args): + """ + Called when users or channel's modes are changed. + + @type user: C{str} + @param user: The user and hostmask which instigated this change. + + @type channel: C{str} + @param channel: The channel where the modes are changed. If args is + empty the channel for which the modes are changing. If the changes are + at server level it could be equal to C{user}. + + @type set: C{bool} or C{int} + @param set: True if the mode(s) is being added, False if it is being + removed. If some modes are added and others removed at the same time + this function will be called twice, the first time with all the added + modes, the second with the removed ones. (To change this behaviour + override the irc_MODE method) + + @type modes: C{str} + @param modes: The mode or modes which are being changed. + + @type args: C{tuple} + @param args: Any additional information required for the mode + change. + """ + + def pong(self, user, secs): + """ + Called with the results of a CTCP PING query. + """ + pass + + def signedOn(self): + """ + Called after successfully signing on to the server. + """ + pass + + def kickedFrom(self, channel, kicker, message): + """ + Called when I am kicked from a channel. + """ + pass + + def nickChanged(self, nick): + """ + Called when my nick has been changed. + """ + self.nickname = nick + + + ### Things I observe other people doing in a channel. + + def userJoined(self, user, channel): + """ + Called when I see another user joining a channel. + """ + pass + + def userLeft(self, user, channel): + """ + Called when I see another user leaving a channel. + """ + pass + + def userQuit(self, user, quitMessage): + """ + Called when I see another user disconnect from the network. + """ + pass + + def userKicked(self, kickee, channel, kicker, message): + """ + Called when I observe someone else being kicked from a channel. + """ + pass + + def action(self, user, channel, data): + """ + Called when I see a user perform an ACTION on a channel. + """ + pass + + def topicUpdated(self, user, channel, newTopic): + """ + In channel, user changed the topic to newTopic. + + Also called when first joining a channel. + """ + pass + + def userRenamed(self, oldname, newname): + """ + A user changed their name from oldname to newname. + """ + pass + + ### Information from the server. + + def receivedMOTD(self, motd): + """ + I received a message-of-the-day banner from the server. + + motd is a list of strings, where each string was sent as a separate + message from the server. To display, you might want to use:: + + '\\n'.join(motd) + + to get a nicely formatted string. + """ + pass + + ### user input commands, client->server + ### Your client will want to invoke these. + + def join(self, channel, key=None): + """ + Join a channel. + + @type channel: C{str} + @param channel: The name of the channel to join. If it has no prefix, + C{'#'} will be prepended to it. + @type key: C{str} + @param key: If specified, the key used to join the channel. + """ + if channel[0] not in CHANNEL_PREFIXES: + channel = '#' + channel + if key: + self.sendLine("JOIN %s %s" % (channel, key)) + else: + self.sendLine("JOIN %s" % (channel,)) + + def leave(self, channel, reason=None): + """ + Leave a channel. + + @type channel: C{str} + @param channel: The name of the channel to leave. If it has no prefix, + C{'#'} will be prepended to it. + @type reason: C{str} + @param reason: If given, the reason for leaving. + """ + if channel[0] not in CHANNEL_PREFIXES: + channel = '#' + channel + if reason: + self.sendLine("PART %s :%s" % (channel, reason)) + else: + self.sendLine("PART %s" % (channel,)) + + def kick(self, channel, user, reason=None): + """ + Attempt to kick a user from a channel. + + @type channel: C{str} + @param channel: The name of the channel to kick the user from. If it has + no prefix, C{'#'} will be prepended to it. + @type user: C{str} + @param user: The nick of the user to kick. + @type reason: C{str} + @param reason: If given, the reason for kicking the user. + """ + if channel[0] not in CHANNEL_PREFIXES: + channel = '#' + channel + if reason: + self.sendLine("KICK %s %s :%s" % (channel, user, reason)) + else: + self.sendLine("KICK %s %s" % (channel, user)) + + part = leave + + + def invite(self, user, channel): + """ + Attempt to invite user to channel + + @type user: C{str} + @param user: The user to invite + @type channel: C{str} + @param channel: The channel to invite the user too + + @since: 11.0 + """ + if channel[0] not in CHANNEL_PREFIXES: + channel = '#' + channel + self.sendLine("INVITE %s %s" % (user, channel)) + + + def topic(self, channel, topic=None): + """ + Attempt to set the topic of the given channel, or ask what it is. + + If topic is None, then I sent a topic query instead of trying to set the + topic. The server should respond with a TOPIC message containing the + current topic of the given channel. + + @type channel: C{str} + @param channel: The name of the channel to change the topic on. If it + has no prefix, C{'#'} will be prepended to it. + @type topic: C{str} + @param topic: If specified, what to set the topic to. + """ + # << TOPIC #xtestx :fff + if channel[0] not in CHANNEL_PREFIXES: + channel = '#' + channel + if topic != None: + self.sendLine("TOPIC %s :%s" % (channel, topic)) + else: + self.sendLine("TOPIC %s" % (channel,)) + + + def mode(self, chan, set, modes, limit = None, user = None, mask = None): + """ + Change the modes on a user or channel. + + The C{limit}, C{user}, and C{mask} parameters are mutually exclusive. + + @type chan: C{str} + @param chan: The name of the channel to operate on. + @type set: C{bool} + @param set: True to give the user or channel permissions and False to + remove them. + @type modes: C{str} + @param modes: The mode flags to set on the user or channel. + @type limit: C{int} + @param limit: In conjunction with the C{'l'} mode flag, limits the + number of users on the channel. + @type user: C{str} + @param user: The user to change the mode on. + @type mask: C{str} + @param mask: In conjunction with the C{'b'} mode flag, sets a mask of + users to be banned from the channel. + """ + if set: + line = 'MODE %s +%s' % (chan, modes) + else: + line = 'MODE %s -%s' % (chan, modes) + if limit is not None: + line = '%s %d' % (line, limit) + elif user is not None: + line = '%s %s' % (line, user) + elif mask is not None: + line = '%s %s' % (line, mask) + self.sendLine(line) + + + def say(self, channel, message, length=None): + """ + Send a message to a channel + + @type channel: C{str} + @param channel: The channel to say the message on. If it has no prefix, + C{'#'} will be prepended to it. + @type message: C{str} + @param message: The message to say. + @type length: C{int} + @param length: The maximum number of octets to send at a time. This has + the effect of turning a single call to C{msg()} into multiple + commands to the server. This is useful when long messages may be + sent that would otherwise cause the server to kick us off or + silently truncate the text we are sending. If None is passed, the + entire message is always send in one command. + """ + if channel[0] not in CHANNEL_PREFIXES: + channel = '#' + channel + self.msg(channel, message, length) + + + def _safeMaximumLineLength(self, command): + """ + Estimate a safe maximum line length for the given command. + + This is done by assuming the maximum values for nickname length, + realname and hostname combined with the command that needs to be sent + and some guessing. A theoretical maximum value is used because it is + possible that our nickname, username or hostname changes (on the server + side) while the length is still being calculated. + """ + # :nickname!realname@hostname COMMAND ... + theoretical = ':%s!%s@%s %s' % ( + 'a' * self.supported.getFeature('NICKLEN'), + # This value is based on observation. + 'b' * 10, + # See <http://tools.ietf.org/html/rfc2812#section-2.3.1>. + 'c' * 63, + command) + # Fingers crossed. + fudge = 10 + return MAX_COMMAND_LENGTH - len(theoretical) - fudge + + + def msg(self, user, message, length=None): + """ + Send a message to a user or channel. + + The message will be split into multiple commands to the server if: + - The message contains any newline characters + - Any span between newline characters is longer than the given + line-length. + + @param user: Username or channel name to which to direct the + message. + @type user: C{str} + + @param message: Text to send. + @type message: C{str} + + @param length: Maximum number of octets to send in a single + command, including the IRC protocol framing. If L{None} is given + then L{IRCClient._safeMaximumLineLength} is used to determine a + value. + @type length: C{int} + """ + fmt = 'PRIVMSG %s :' % (user,) + + if length is None: + length = self._safeMaximumLineLength(fmt) + + # Account for the line terminator. + minimumLength = len(fmt) + 2 + if length <= minimumLength: + raise ValueError("Maximum length must exceed %d for message " + "to %s" % (minimumLength, user)) + for line in split(message, length - minimumLength): + self.sendLine(fmt + line) + + + def notice(self, user, message): + """ + Send a notice to a user. + + Notices are like normal message, but should never get automated + replies. + + @type user: C{str} + @param user: The user to send a notice to. + @type message: C{str} + @param message: The contents of the notice to send. + """ + self.sendLine("NOTICE %s :%s" % (user, message)) + + + def away(self, message=''): + """ + Mark this client as away. + + @type message: C{str} + @param message: If specified, the away message. + """ + self.sendLine("AWAY :%s" % message) + + + def back(self): + """ + Clear the away status. + """ + # An empty away marks us as back + self.away() + + + def whois(self, nickname, server=None): + """ + Retrieve user information about the given nickname. + + @type nickname: C{str} + @param nickname: The nickname about which to retrieve information. + + @since: 8.2 + """ + if server is None: + self.sendLine('WHOIS ' + nickname) + else: + self.sendLine('WHOIS %s %s' % (server, nickname)) + + + def register(self, nickname, hostname='foo', servername='bar'): + """ + Login to the server. + + @type nickname: C{str} + @param nickname: The nickname to register. + @type hostname: C{str} + @param hostname: If specified, the hostname to logon as. + @type servername: C{str} + @param servername: If specified, the servername to logon as. + """ + if self.password is not None: + self.sendLine("PASS %s" % self.password) + self.setNick(nickname) + if self.username is None: + self.username = nickname + self.sendLine("USER %s %s %s :%s" % (self.username, hostname, servername, self.realname)) + + + def setNick(self, nickname): + """ + Set this client's nickname. + + @type nickname: C{str} + @param nickname: The nickname to change to. + """ + self._attemptedNick = nickname + self.sendLine("NICK %s" % nickname) + + + def quit(self, message = ''): + """ + Disconnect from the server + + @type message: C{str} + + @param message: If specified, the message to give when quitting the + server. + """ + self.sendLine("QUIT :%s" % message) + + ### user input commands, client->client + + def describe(self, channel, action): + """ + Strike a pose. + + @type channel: C{str} + @param channel: The name of the channel to have an action on. If it + has no prefix, it is sent to the user of that name. + @type action: C{str} + @param action: The action to preform. + @since: 9.0 + """ + self.ctcpMakeQuery(channel, [('ACTION', action)]) + + + _pings = None + _MAX_PINGRING = 12 + + def ping(self, user, text = None): + """ + Measure round-trip delay to another IRC client. + """ + if self._pings is None: + self._pings = {} + + if text is None: + chars = string.ascii_letters + string.digits + string.punctuation + key = ''.join([random.choice(chars) for i in range(12)]) + else: + key = str(text) + self._pings[(user, key)] = time.time() + self.ctcpMakeQuery(user, [('PING', key)]) + + if len(self._pings) > self._MAX_PINGRING: + # Remove some of the oldest entries. + byValue = [(v, k) for (k, v) in self._pings.items()] + byValue.sort() + excess = len(self._pings) - self._MAX_PINGRING + for i in range(excess): + del self._pings[byValue[i][1]] + + + def dccSend(self, user, file): + """ + This is supposed to send a user a file directly. This generally + doesn't work on any client, and this method is included only for + backwards compatibility and completeness. + + @param user: C{str} representing the user + @param file: an open file (unknown, since this is not implemented) + """ + raise NotImplementedError( + "XXX!!! Help! I need to bind a socket, have it listen, and tell me its address. " + "(and stop accepting once we've made a single connection.)") + + + def dccResume(self, user, fileName, port, resumePos): + """ + Send a DCC RESUME request to another user. + """ + self.ctcpMakeQuery(user, [ + ('DCC', ['RESUME', fileName, port, resumePos])]) + + + def dccAcceptResume(self, user, fileName, port, resumePos): + """ + Send a DCC ACCEPT response to clients who have requested a resume. + """ + self.ctcpMakeQuery(user, [ + ('DCC', ['ACCEPT', fileName, port, resumePos])]) + + ### server->client messages + ### You might want to fiddle with these, + ### but it is safe to leave them alone. + + def irc_ERR_NICKNAMEINUSE(self, prefix, params): + """ + Called when we try to register or change to a nickname that is already + taken. + """ + self._attemptedNick = self.alterCollidedNick(self._attemptedNick) + self.setNick(self._attemptedNick) + + + def alterCollidedNick(self, nickname): + """ + Generate an altered version of a nickname that caused a collision in an + effort to create an unused related name for subsequent registration. + + @param nickname: The nickname a user is attempting to register. + @type nickname: C{str} + + @returns: A string that is in some way different from the nickname. + @rtype: C{str} + """ + return nickname + '_' + + + def irc_ERR_ERRONEUSNICKNAME(self, prefix, params): + """ + Called when we try to register or change to an illegal nickname. + + The server should send this reply when the nickname contains any + disallowed characters. The bot will stall, waiting for RPL_WELCOME, if + we don't handle this during sign-on. + + @note: The method uses the spelling I{erroneus}, as it appears in + the RFC, section 6.1. + """ + if not self._registered: + self.setNick(self.erroneousNickFallback) + + + def irc_ERR_PASSWDMISMATCH(self, prefix, params): + """ + Called when the login was incorrect. + """ + raise IRCPasswordMismatch("Password Incorrect.") + + + def irc_RPL_WELCOME(self, prefix, params): + """ + Called when we have received the welcome from the server. + """ + self.hostname = prefix + self._registered = True + self.nickname = self._attemptedNick + self.signedOn() + self.startHeartbeat() + + + def irc_JOIN(self, prefix, params): + """ + Called when a user joins a channel. + """ + nick = prefix.split('!')[0] + channel = params[-1] + if nick == self.nickname: + self.joined(channel) + else: + self.userJoined(nick, channel) + + def irc_PART(self, prefix, params): + """ + Called when a user leaves a channel. + """ + nick = prefix.split('!')[0] + channel = params[0] + if nick == self.nickname: + self.left(channel) + else: + self.userLeft(nick, channel) + + def irc_QUIT(self, prefix, params): + """ + Called when a user has quit. + """ + nick = prefix.split('!')[0] + self.userQuit(nick, params[0]) + + + def irc_MODE(self, user, params): + """ + Parse a server mode change message. + """ + channel, modes, args = params[0], params[1], params[2:] + + if modes[0] not in '-+': + modes = '+' + modes + + if channel == self.nickname: + # This is a mode change to our individual user, not a channel mode + # that involves us. + paramModes = self.getUserModeParams() + else: + paramModes = self.getChannelModeParams() + + try: + added, removed = parseModes(modes, args, paramModes) + except IRCBadModes: + log.err(None, 'An error occurred while parsing the following ' + 'MODE message: MODE %s' % (' '.join(params),)) + else: + if added: + modes, params = zip(*added) + self.modeChanged(user, channel, True, ''.join(modes), params) + + if removed: + modes, params = zip(*removed) + self.modeChanged(user, channel, False, ''.join(modes), params) + + + def irc_PING(self, prefix, params): + """ + Called when some has pinged us. + """ + self.sendLine("PONG %s" % params[-1]) + + def irc_PRIVMSG(self, prefix, params): + """ + Called when we get a message. + """ + user = prefix + channel = params[0] + message = params[-1] + + if not message: + # Don't raise an exception if we get blank message. + return + + if message[0] == X_DELIM: + m = ctcpExtract(message) + if m['extended']: + self.ctcpQuery(user, channel, m['extended']) + + if not m['normal']: + return + + message = ' '.join(m['normal']) + + self.privmsg(user, channel, message) + + def irc_NOTICE(self, prefix, params): + """ + Called when a user gets a notice. + """ + user = prefix + channel = params[0] + message = params[-1] + + if message[0]==X_DELIM: + m = ctcpExtract(message) + if m['extended']: + self.ctcpReply(user, channel, m['extended']) + + if not m['normal']: + return + + message = ' '.join(m['normal']) + + self.noticed(user, channel, message) + + def irc_NICK(self, prefix, params): + """ + Called when a user changes their nickname. + """ + nick = prefix.split('!', 1)[0] + if nick == self.nickname: + self.nickChanged(params[0]) + else: + self.userRenamed(nick, params[0]) + + def irc_KICK(self, prefix, params): + """ + Called when a user is kicked from a channel. + """ + kicker = prefix.split('!')[0] + channel = params[0] + kicked = params[1] + message = params[-1] + if kicked.lower() == self.nickname.lower(): + # Yikes! + self.kickedFrom(channel, kicker, message) + else: + self.userKicked(kicked, channel, kicker, message) + + def irc_TOPIC(self, prefix, params): + """ + Someone in the channel set the topic. + """ + user = prefix.split('!')[0] + channel = params[0] + newtopic = params[1] + self.topicUpdated(user, channel, newtopic) + + def irc_RPL_TOPIC(self, prefix, params): + """ + Called when the topic for a channel is initially reported or when it + subsequently changes. + """ + user = prefix.split('!')[0] + channel = params[1] + newtopic = params[2] + self.topicUpdated(user, channel, newtopic) + + def irc_RPL_NOTOPIC(self, prefix, params): + user = prefix.split('!')[0] + channel = params[1] + newtopic = "" + self.topicUpdated(user, channel, newtopic) + + def irc_RPL_MOTDSTART(self, prefix, params): + if params[-1].startswith("- "): + params[-1] = params[-1][2:] + self.motd = [params[-1]] + + def irc_RPL_MOTD(self, prefix, params): + if params[-1].startswith("- "): + params[-1] = params[-1][2:] + if self.motd is None: + self.motd = [] + self.motd.append(params[-1]) + + + def irc_RPL_ENDOFMOTD(self, prefix, params): + """ + I{RPL_ENDOFMOTD} indicates the end of the message of the day + messages. Deliver the accumulated lines to C{receivedMOTD}. + """ + motd = self.motd + self.motd = None + self.receivedMOTD(motd) + + + def irc_RPL_CREATED(self, prefix, params): + self.created(params[1]) + + def irc_RPL_YOURHOST(self, prefix, params): + self.yourHost(params[1]) + + def irc_RPL_MYINFO(self, prefix, params): + info = params[1].split(None, 3) + while len(info) < 4: + info.append(None) + self.myInfo(*info) + + def irc_RPL_BOUNCE(self, prefix, params): + self.bounce(params[1]) + + def irc_RPL_ISUPPORT(self, prefix, params): + args = params[1:-1] + # Several ISUPPORT messages, in no particular order, may be sent + # to the client at any given point in time (usually only on connect, + # though.) For this reason, ServerSupportedFeatures.parse is intended + # to mutate the supported feature list. + self.supported.parse(args) + self.isupport(args) + + def irc_RPL_LUSERCLIENT(self, prefix, params): + self.luserClient(params[1]) + + def irc_RPL_LUSEROP(self, prefix, params): + try: + self.luserOp(int(params[1])) + except ValueError: + pass + + def irc_RPL_LUSERCHANNELS(self, prefix, params): + try: + self.luserChannels(int(params[1])) + except ValueError: + pass + + def irc_RPL_LUSERME(self, prefix, params): + self.luserMe(params[1]) + + def irc_unknown(self, prefix, command, params): + pass + + ### Receiving a CTCP query from another party + ### It is safe to leave these alone. + + + def ctcpQuery(self, user, channel, messages): + """ + Dispatch method for any CTCP queries received. + + Duplicated CTCP queries are ignored and no dispatch is + made. Unrecognized CTCP queries invoke L{IRCClient.ctcpUnknownQuery}. + """ + seen = set() + for tag, data in messages: + method = getattr(self, 'ctcpQuery_%s' % tag, None) + if tag not in seen: + if method is not None: + method(user, channel, data) + else: + self.ctcpUnknownQuery(user, channel, tag, data) + seen.add(tag) + + + def ctcpUnknownQuery(self, user, channel, tag, data): + """ + Fallback handler for unrecognized CTCP queries. + + No CTCP I{ERRMSG} reply is made to remove a potential denial of service + avenue. + """ + log.msg('Unknown CTCP query from %r: %r %r' % (user, tag, data)) + + + def ctcpQuery_ACTION(self, user, channel, data): + self.action(user, channel, data) + + def ctcpQuery_PING(self, user, channel, data): + nick = user.split('!')[0] + self.ctcpMakeReply(nick, [("PING", data)]) + + def ctcpQuery_FINGER(self, user, channel, data): + if data is not None: + self.quirkyMessage("Why did %s send '%s' with a FINGER query?" + % (user, data)) + if not self.fingerReply: + return + + if callable(self.fingerReply): + reply = self.fingerReply() + else: + reply = str(self.fingerReply) + + nick = user.split('!')[0] + self.ctcpMakeReply(nick, [('FINGER', reply)]) + + def ctcpQuery_VERSION(self, user, channel, data): + if data is not None: + self.quirkyMessage("Why did %s send '%s' with a VERSION query?" + % (user, data)) + + if self.versionName: + nick = user.split('!')[0] + self.ctcpMakeReply(nick, [('VERSION', '%s:%s:%s' % + (self.versionName, + self.versionNum or '', + self.versionEnv or ''))]) + + def ctcpQuery_SOURCE(self, user, channel, data): + if data is not None: + self.quirkyMessage("Why did %s send '%s' with a SOURCE query?" + % (user, data)) + if self.sourceURL: + nick = user.split('!')[0] + # The CTCP document (Zeuge, Rollo, Mesander 1994) says that SOURCE + # replies should be responded to with the location of an anonymous + # FTP server in host:directory:file format. I'm taking the liberty + # of bringing it into the 21st century by sending a URL instead. + self.ctcpMakeReply(nick, [('SOURCE', self.sourceURL), + ('SOURCE', None)]) + + def ctcpQuery_USERINFO(self, user, channel, data): + if data is not None: + self.quirkyMessage("Why did %s send '%s' with a USERINFO query?" + % (user, data)) + if self.userinfo: + nick = user.split('!')[0] + self.ctcpMakeReply(nick, [('USERINFO', self.userinfo)]) + + def ctcpQuery_CLIENTINFO(self, user, channel, data): + """ + A master index of what CTCP tags this client knows. + + If no arguments are provided, respond with a list of known tags, sorted + in alphabetical order. + If an argument is provided, provide human-readable help on + the usage of that tag. + """ + nick = user.split('!')[0] + if not data: + # XXX: prefixedMethodNames gets methods from my *class*, + # but it's entirely possible that this *instance* has more + # methods. + names = sorted(reflect.prefixedMethodNames(self.__class__, + 'ctcpQuery_')) + + self.ctcpMakeReply(nick, [('CLIENTINFO', ' '.join(names))]) + else: + args = data.split() + method = getattr(self, 'ctcpQuery_%s' % (args[0],), None) + if not method: + self.ctcpMakeReply(nick, [('ERRMSG', + "CLIENTINFO %s :" + "Unknown query '%s'" + % (data, args[0]))]) + return + doc = getattr(method, '__doc__', '') + self.ctcpMakeReply(nick, [('CLIENTINFO', doc)]) + + + def ctcpQuery_ERRMSG(self, user, channel, data): + # Yeah, this seems strange, but that's what the spec says to do + # when faced with an ERRMSG query (not a reply). + nick = user.split('!')[0] + self.ctcpMakeReply(nick, [('ERRMSG', + "%s :No error has occurred." % data)]) + + def ctcpQuery_TIME(self, user, channel, data): + if data is not None: + self.quirkyMessage("Why did %s send '%s' with a TIME query?" + % (user, data)) + nick = user.split('!')[0] + self.ctcpMakeReply(nick, + [('TIME', ':%s' % + time.asctime(time.localtime(time.time())))]) + + def ctcpQuery_DCC(self, user, channel, data): + """ + Initiate a Direct Client Connection + + @param user: The hostmask of the user/client. + @type user: L{bytes} + + @param channel: The name of the IRC channel. + @type channel: L{bytes} + + @param data: The DCC request message. + @type data: L{bytes} + """ + + if not data: return + dcctype = data.split(None, 1)[0].upper() + handler = getattr(self, "dcc_" + dcctype, None) + if handler: + if self.dcc_sessions is None: + self.dcc_sessions = [] + data = data[len(dcctype)+1:] + handler(user, channel, data) + else: + nick = user.split('!')[0] + self.ctcpMakeReply(nick, [('ERRMSG', + "DCC %s :Unknown DCC type '%s'" + % (data, dcctype))]) + self.quirkyMessage("%s offered unknown DCC type %s" + % (user, dcctype)) + + + def dcc_SEND(self, user, channel, data): + # Use shlex.split for those who send files with spaces in the names. + data = shlex.split(data) + if len(data) < 3: + raise IRCBadMessage("malformed DCC SEND request: %r" % (data,)) + + (filename, address, port) = data[:3] + + address = dccParseAddress(address) + try: + port = int(port) + except ValueError: + raise IRCBadMessage("Indecipherable port %r" % (port,)) + + size = -1 + if len(data) >= 4: + try: + size = int(data[3]) + except ValueError: + pass + + # XXX Should we bother passing this data? + self.dccDoSend(user, address, port, filename, size, data) + + + def dcc_ACCEPT(self, user, channel, data): + data = shlex.split(data) + if len(data) < 3: + raise IRCBadMessage("malformed DCC SEND ACCEPT request: %r" % ( + data,)) + (filename, port, resumePos) = data[:3] + try: + port = int(port) + resumePos = int(resumePos) + except ValueError: + return + + self.dccDoAcceptResume(user, filename, port, resumePos) + + + def dcc_RESUME(self, user, channel, data): + data = shlex.split(data) + if len(data) < 3: + raise IRCBadMessage("malformed DCC SEND RESUME request: %r" % ( + data,)) + (filename, port, resumePos) = data[:3] + try: + port = int(port) + resumePos = int(resumePos) + except ValueError: + return + + self.dccDoResume(user, filename, port, resumePos) + + + def dcc_CHAT(self, user, channel, data): + data = shlex.split(data) + if len(data) < 3: + raise IRCBadMessage("malformed DCC CHAT request: %r" % (data,)) + + (filename, address, port) = data[:3] + + address = dccParseAddress(address) + try: + port = int(port) + except ValueError: + raise IRCBadMessage("Indecipherable port %r" % (port,)) + + self.dccDoChat(user, channel, address, port, data) + + ### The dccDo methods are the slightly higher-level siblings of + ### common dcc_ methods; the arguments have been parsed for them. + + def dccDoSend(self, user, address, port, fileName, size, data): + """ + Called when I receive a DCC SEND offer from a client. + + By default, I do nothing here. + + @param user: The hostmask of the requesting user. + @type user: L{bytes} + + @param address: The IP address of the requesting user. + @type address: L{bytes} + + @param port: An integer representing the port of the requesting user. + @type port: L{int} + + @param fileName: The name of the file to be transferred. + @type fileName: L{bytes} + + @param size: The size of the file to be transferred, which may be C{-1} + if the size of the file was not specified in the DCC SEND request. + @type size: L{int} + + @param data: A 3-list of [fileName, address, port]. + @type data: L{list} + """ + + + def dccDoResume(self, user, file, port, resumePos): + """ + Called when a client is trying to resume an offered file via DCC send. + It should be either replied to with a DCC ACCEPT or ignored (default). + + @param user: The hostmask of the user who wants to resume the transfer + of a file previously offered via DCC send. + @type user: L{bytes} + + @param file: The name of the file to resume the transfer of. + @type file: L{bytes} + + @param port: An integer representing the port of the requesting user. + @type port: L{int} + + @param resumePos: The position in the file from where the transfer + should resume. + @type resumePos: L{int} + """ + pass + + + def dccDoAcceptResume(self, user, file, port, resumePos): + """ + Called when a client has verified and accepted a DCC resume request + made by us. By default it will do nothing. + + @param user: The hostmask of the user who has accepted the DCC resume + request. + @type user: L{bytes} + + @param file: The name of the file to resume the transfer of. + @type file: L{bytes} + + @param port: An integer representing the port of the accepting user. + @type port: L{int} + + @param resumePos: The position in the file from where the transfer + should resume. + @type resumePos: L{int} + """ + pass + + + def dccDoChat(self, user, channel, address, port, data): + pass + #factory = DccChatFactory(self, queryData=(user, channel, data)) + #reactor.connectTCP(address, port, factory) + #self.dcc_sessions.append(factory) + + #def ctcpQuery_SED(self, user, data): + # """Simple Encryption Doodoo + # + # Feel free to implement this, but no specification is available. + # """ + # raise NotImplementedError + + + def ctcpMakeReply(self, user, messages): + """ + Send one or more C{extended messages} as a CTCP reply. + + @type messages: a list of extended messages. An extended + message is a (tag, data) tuple, where 'data' may be L{None}. + """ + self.notice(user, ctcpStringify(messages)) + + ### client CTCP query commands + + def ctcpMakeQuery(self, user, messages): + """ + Send one or more C{extended messages} as a CTCP query. + + @type messages: a list of extended messages. An extended + message is a (tag, data) tuple, where 'data' may be L{None}. + """ + self.msg(user, ctcpStringify(messages)) + + ### Receiving a response to a CTCP query (presumably to one we made) + ### You may want to add methods here, or override UnknownReply. + + def ctcpReply(self, user, channel, messages): + """ + Dispatch method for any CTCP replies received. + """ + for m in messages: + method = getattr(self, "ctcpReply_%s" % m[0], None) + if method: + method(user, channel, m[1]) + else: + self.ctcpUnknownReply(user, channel, m[0], m[1]) + + def ctcpReply_PING(self, user, channel, data): + nick = user.split('!', 1)[0] + if (not self._pings) or ((nick, data) not in self._pings): + raise IRCBadMessage( + "Bogus PING response from %s: %s" % (user, data)) + + t0 = self._pings[(nick, data)] + self.pong(user, time.time() - t0) + + def ctcpUnknownReply(self, user, channel, tag, data): + """ + Called when a fitting ctcpReply_ method is not found. + + @param user: The hostmask of the user. + @type user: L{bytes} + + @param channel: The name of the IRC channel. + @type channel: L{bytes} + + @param tag: The CTCP request tag for which no fitting method is found. + @type tag: L{bytes} + + @param data: The CTCP message. + @type data: L{bytes} + """ + # FIXME:7560: + # Add code for handling arbitrary queries and not treat them as + # anomalies. + + log.msg("Unknown CTCP reply from %s: %s %s\n" + % (user, tag, data)) + + ### Error handlers + ### You may override these with something more appropriate to your UI. + + def badMessage(self, line, excType, excValue, tb): + """ + When I get a message that's so broken I can't use it. + + @param line: The indecipherable message. + @type line: L{bytes} + + @param excType: The exception type of the exception raised by the + message. + @type excType: L{type} + + @param excValue: The exception parameter of excType or its associated + value(the second argument to C{raise}). + @type excValue: L{BaseException} + + @param tb: The Traceback as a traceback object. + @type tb: L{traceback} + """ + log.msg(line) + log.msg(''.join(traceback.format_exception(excType, excValue, tb))) + + + def quirkyMessage(self, s): + """ + This is called when I receive a message which is peculiar, but not + wholly indecipherable. + + @param s: The peculiar message. + @type s: L{bytes} + """ + log.msg(s + '\n') + + ### Protocol methods + + def connectionMade(self): + self.supported = ServerSupportedFeatures() + self._queue = [] + if self.performLogin: + self.register(self.nickname) + + def dataReceived(self, data): + if isinstance(data, unicode): + data = data.encode("utf-8") + data = data.replace(b'\r', b'') + basic.LineReceiver.dataReceived(self, data) + + + def lineReceived(self, line): + if bytes != str and isinstance(line, bytes): + # decode bytes from transport to unicode + line = line.decode("utf-8") + + line = lowDequote(line) + try: + prefix, command, params = parsemsg(line) + if command in numeric_to_symbolic: + command = numeric_to_symbolic[command] + self.handleCommand(command, prefix, params) + except IRCBadMessage: + self.badMessage(line, *sys.exc_info()) + + + def getUserModeParams(self): + """ + Get user modes that require parameters for correct parsing. + + @rtype: C{[str, str]} + @return C{[add, remove]} + """ + return ['', ''] + + + def getChannelModeParams(self): + """ + Get channel modes that require parameters for correct parsing. + + @rtype: C{[str, str]} + @return C{[add, remove]} + """ + # PREFIX modes are treated as "type B" CHANMODES, they always take + # parameter. + params = ['', ''] + prefixes = self.supported.getFeature('PREFIX', {}) + params[0] = params[1] = ''.join(prefixes.keys()) + + chanmodes = self.supported.getFeature('CHANMODES') + if chanmodes is not None: + params[0] += chanmodes.get('addressModes', '') + params[0] += chanmodes.get('param', '') + params[1] = params[0] + params[0] += chanmodes.get('setParam', '') + return params + + + def handleCommand(self, command, prefix, params): + """ + Determine the function to call for the given command and call it with + the given arguments. + + @param command: The IRC command to determine the function for. + @type command: L{bytes} + + @param prefix: The prefix of the IRC message (as returned by + L{parsemsg}). + @type prefix: L{bytes} + + @param params: A list of parameters to call the function with. + @type params: L{list} + """ + method = getattr(self, "irc_%s" % command, None) + try: + if method is not None: + method(prefix, params) + else: + self.irc_unknown(prefix, command, params) + except: + log.deferr() + + + def __getstate__(self): + dct = self.__dict__.copy() + dct['dcc_sessions'] = None + dct['_pings'] = None + return dct + + +def dccParseAddress(address): + if '.' in address: + pass + else: + try: + address = int(address) + except ValueError: + raise IRCBadMessage("Indecipherable address %r" % (address,)) + else: + address = ( + (address >> 24) & 0xFF, + (address >> 16) & 0xFF, + (address >> 8) & 0xFF, + address & 0xFF, + ) + address = '.'.join(map(str,address)) + return address + + +class DccFileReceiveBasic(protocol.Protocol, styles.Ephemeral): + """ + Bare protocol to receive a Direct Client Connection SEND stream. + + This does enough to keep the other guy talking, but you'll want to extend + my dataReceived method to *do* something with the data I get. + + @ivar bytesReceived: An integer representing the number of bytes of data + received. + @type bytesReceived: L{int} + """ + + bytesReceived = 0 + + def __init__(self, resumeOffset=0): + """ + @param resumeOffset: An integer representing the amount of bytes from + where the transfer of data should be resumed. + @type resumeOffset: L{int} + """ + self.bytesReceived = resumeOffset + self.resume = (resumeOffset != 0) + + def dataReceived(self, data): + """ + See: L{protocol.Protocol.dataReceived} + + Warning: This just acknowledges to the remote host that the data has + been received; it doesn't I{do} anything with the data, so you'll want + to override this. + """ + self.bytesReceived = self.bytesReceived + len(data) + self.transport.write(struct.pack('!i', self.bytesReceived)) + + +class DccSendProtocol(protocol.Protocol, styles.Ephemeral): + """ + Protocol for an outgoing Direct Client Connection SEND. + + @ivar blocksize: An integer representing the size of an individual block of + data. + @type blocksize: L{int} + + @ivar file: The file to be sent. This can be either a file object or + simply the name of the file. + @type file: L{file} or L{bytes} + + @ivar bytesSent: An integer representing the number of bytes sent. + @type bytesSent: L{int} + + @ivar completed: An integer representing whether the transfer has been + completed or not. + @type completed: L{int} + + @ivar connected: An integer representing whether the connection has been + established or not. + @type connected: L{int} + """ + + blocksize = 1024 + file = None + bytesSent = 0 + completed = 0 + connected = 0 + + def __init__(self, file): + if type(file) is str: + self.file = open(file, 'r') + + def connectionMade(self): + self.connected = 1 + self.sendBlock() + + def dataReceived(self, data): + # XXX: Do we need to check to see if len(data) != fmtsize? + + bytesShesGot = struct.unpack("!I", data) + if bytesShesGot < self.bytesSent: + # Wait for her. + # XXX? Add some checks to see if we've stalled out? + return + elif bytesShesGot > self.bytesSent: + # self.transport.log("DCC SEND %s: She says she has %d bytes " + # "but I've only sent %d. I'm stopping " + # "this screwy transfer." + # % (self.file, + # bytesShesGot, self.bytesSent)) + self.transport.loseConnection() + return + + self.sendBlock() + + def sendBlock(self): + block = self.file.read(self.blocksize) + if block: + self.transport.write(block) + self.bytesSent = self.bytesSent + len(block) + else: + # Nothing more to send, transfer complete. + self.transport.loseConnection() + self.completed = 1 + + def connectionLost(self, reason): + self.connected = 0 + if hasattr(self.file, "close"): + self.file.close() + + +class DccSendFactory(protocol.Factory): + protocol = DccSendProtocol + def __init__(self, file): + self.file = file + + def buildProtocol(self, connection): + p = self.protocol(self.file) + p.factory = self + return p + + +def fileSize(file): + """ + I'll try my damndest to determine the size of this file object. + + @param file: The file object to determine the size of. + @type file: L{file} + + @rtype: L{int} or L{None} + @return: The size of the file object as an integer if it can be determined, + otherwise return L{None}. + """ + size = None + if hasattr(file, "fileno"): + fileno = file.fileno() + try: + stat_ = os.fstat(fileno) + size = stat_[stat.ST_SIZE] + except: + pass + else: + return size + + if hasattr(file, "name") and path.exists(file.name): + try: + size = path.getsize(file.name) + except: + pass + else: + return size + + if hasattr(file, "seek") and hasattr(file, "tell"): + try: + try: + file.seek(0, 2) + size = file.tell() + finally: + file.seek(0, 0) + except: + pass + else: + return size + + return size + +class DccChat(basic.LineReceiver, styles.Ephemeral): + """ + Direct Client Connection protocol type CHAT. + + DCC CHAT is really just your run o' the mill basic.LineReceiver + protocol. This class only varies from that slightly, accepting + either LF or CR LF for a line delimeter for incoming messages + while always using CR LF for outgoing. + + The lineReceived method implemented here uses the DCC connection's + 'client' attribute (provided upon construction) to deliver incoming + lines from the DCC chat via IRCClient's normal privmsg interface. + That's something of a spoof, which you may well want to override. + """ + + queryData = None + delimiter = CR + NL + client = None + remoteParty = None + buffer = b"" + + def __init__(self, client, queryData=None): + """ + Initialize a new DCC CHAT session. + + queryData is a 3-tuple of + (fromUser, targetUserOrChannel, data) + as received by the CTCP query. + + (To be honest, fromUser is the only thing that's currently + used here. targetUserOrChannel is potentially useful, while + the 'data' argument is solely for informational purposes.) + """ + self.client = client + if queryData: + self.queryData = queryData + self.remoteParty = self.queryData[0] + + def dataReceived(self, data): + self.buffer = self.buffer + data + lines = self.buffer.split(LF) + # Put the (possibly empty) element after the last LF back in the + # buffer + self.buffer = lines.pop() + + for line in lines: + if line[-1] == CR: + line = line[:-1] + self.lineReceived(line) + + def lineReceived(self, line): + log.msg("DCC CHAT<%s> %s" % (self.remoteParty, line)) + self.client.privmsg(self.remoteParty, + self.client.nickname, line) + + +class DccChatFactory(protocol.ClientFactory): + protocol = DccChat + noisy = 0 + def __init__(self, client, queryData): + self.client = client + self.queryData = queryData + + + def buildProtocol(self, addr): + p = self.protocol(client=self.client, queryData=self.queryData) + p.factory = self + return p + + + def clientConnectionFailed(self, unused_connector, unused_reason): + self.client.dcc_sessions.remove(self) + + def clientConnectionLost(self, unused_connector, unused_reason): + self.client.dcc_sessions.remove(self) + + +def dccDescribe(data): + """ + Given the data chunk from a DCC query, return a descriptive string. + + @param data: The data from a DCC query. + @type data: L{bytes} + + @rtype: L{bytes} + @return: A descriptive string. + """ + + orig_data = data + data = data.split() + if len(data) < 4: + return orig_data + + (dcctype, arg, address, port) = data[:4] + + if '.' in address: + pass + else: + try: + address = int(address) + except ValueError: + pass + else: + address = ( + (address >> 24) & 0xFF, + (address >> 16) & 0xFF, + (address >> 8) & 0xFF, + address & 0xFF, + ) + address = '.'.join(map(str, address)) + + if dcctype == 'SEND': + filename = arg + + size_txt = '' + if len(data) >= 5: + try: + size = int(data[4]) + size_txt = ' of size %d bytes' % (size,) + except ValueError: + pass + + dcc_text = ("SEND for file '%s'%s at host %s, port %s" + % (filename, size_txt, address, port)) + elif dcctype == 'CHAT': + dcc_text = ("CHAT for host %s, port %s" + % (address, port)) + else: + dcc_text = orig_data + + return dcc_text + + +class DccFileReceive(DccFileReceiveBasic): + """ + Higher-level coverage for getting a file from DCC SEND. + + I allow you to change the file's name and destination directory. I won't + overwrite an existing file unless I've been told it's okay to do so. If + passed the resumeOffset keyword argument I will attempt to resume the file + from that amount of bytes. + + XXX: I need to let the client know when I am finished. + XXX: I need to decide how to keep a progress indicator updated. + XXX: Client needs a way to tell me "Do not finish until I say so." + XXX: I need to make sure the client understands if the file cannot be written. + + @ivar filename: The name of the file to get. + @type filename: L{bytes} + + @ivar fileSize: The size of the file to get, which has a default value of + C{-1} if the size of the file was not specified in the DCC SEND + request. + @type fileSize: L{int} + + @ivar destDir: The destination directory for the file to be received. + @type destDir: L{bytes} + + @ivar overwrite: An integer representing whether an existing file should be + overwritten or not. This initially is an L{int} but can be modified to + be a L{bool} using the L{set_overwrite} method. + @type overwrite: L{int} or L{bool} + + @ivar queryData: queryData is a 3-tuple of (user, channel, data). + @type queryData: L{tuple} + + @ivar fromUser: This is the hostmask of the requesting user and is found at + index 0 of L{queryData}. + @type fromUser: L{bytes} + """ + + filename = 'dcc' + fileSize = -1 + destDir = '.' + overwrite = 0 + fromUser = None + queryData = None + + def __init__(self, filename, fileSize=-1, queryData=None, + destDir='.', resumeOffset=0): + DccFileReceiveBasic.__init__(self, resumeOffset=resumeOffset) + self.filename = filename + self.destDir = destDir + self.fileSize = fileSize + self._resumeOffset = resumeOffset + + if queryData: + self.queryData = queryData + self.fromUser = self.queryData[0] + + def set_directory(self, directory): + """ + Set the directory where the downloaded file will be placed. + + May raise OSError if the supplied directory path is not suitable. + + @param directory: The directory where the file to be received will be + placed. + @type directory: L{bytes} + """ + if not path.exists(directory): + raise OSError(errno.ENOENT, "You see no directory there.", + directory) + if not path.isdir(directory): + raise OSError(errno.ENOTDIR, "You cannot put a file into " + "something which is not a directory.", + directory) + if not os.access(directory, os.X_OK | os.W_OK): + raise OSError(errno.EACCES, + "This directory is too hard to write in to.", + directory) + self.destDir = directory + + def set_filename(self, filename): + """ + Change the name of the file being transferred. + + This replaces the file name provided by the sender. + + @param filename: The new name for the file. + @type filename: L{bytes} + """ + self.filename = filename + + def set_overwrite(self, boolean): + """ + May I overwrite existing files? + + @param boolean: A boolean value representing whether existing files + should be overwritten or not. + @type boolean: L{bool} + """ + self.overwrite = boolean + + + # Protocol-level methods. + + def connectionMade(self): + dst = path.abspath(path.join(self.destDir,self.filename)) + exists = path.exists(dst) + if self.resume and exists: + # I have been told I want to resume, and a file already + # exists - Here we go + self.file = open(dst, 'rb+') + self.file.seek(self._resumeOffset) + self.file.truncate() + log.msg("Attempting to resume %s - starting from %d bytes" % + (self.file, self.file.tell())) + elif self.resume and not exists: + raise OSError(errno.ENOENT, + "You cannot resume writing to a file " + "that does not exist!", + dst) + elif self.overwrite or not exists: + self.file = open(dst, 'wb') + else: + raise OSError(errno.EEXIST, + "There's a file in the way. " + "Perhaps that's why you cannot open it.", + dst) + + def dataReceived(self, data): + self.file.write(data) + DccFileReceiveBasic.dataReceived(self, data) + + # XXX: update a progress indicator here? + + def connectionLost(self, reason): + """ + When the connection is lost, I close the file. + + @param reason: The reason why the connection was lost. + @type reason: L{Failure} + """ + self.connected = 0 + logmsg = ("%s closed." % (self,)) + if self.fileSize > 0: + logmsg = ("%s %d/%d bytes received" + % (logmsg, self.bytesReceived, self.fileSize)) + if self.bytesReceived == self.fileSize: + pass # Hooray! + elif self.bytesReceived < self.fileSize: + logmsg = ("%s (Warning: %d bytes short)" + % (logmsg, self.fileSize - self.bytesReceived)) + else: + logmsg = ("%s (file larger than expected)" + % (logmsg,)) + else: + logmsg = ("%s %d bytes received" + % (logmsg, self.bytesReceived)) + + if hasattr(self, 'file'): + logmsg = "%s and written to %s.\n" % (logmsg, self.file.name) + if hasattr(self.file, 'close'): self.file.close() + + # self.transport.log(logmsg) + + def __str__(self): + if not self.connected: + return "<Unconnected DccFileReceive object at %x>" % (id(self),) + from_ = self.transport.getPeer() + if self.fromUser: + from_ = "%s (%s)" % (self.fromUser, from_) + + s = ("DCC transfer of '%s' from %s" % (self.filename, from_)) + return s + + def __repr__(self): + s = ("<%s at %x: GET %s>" + % (self.__class__, id(self), self.filename)) + return s + + + +_OFF = '\x0f' +_BOLD = '\x02' +_COLOR = '\x03' +_REVERSE_VIDEO = '\x16' +_UNDERLINE = '\x1f' + +# Mapping of IRC color names to their color values. +_IRC_COLORS = dict( + zip(['white', 'black', 'blue', 'green', 'lightRed', 'red', 'magenta', + 'orange', 'yellow', 'lightGreen', 'cyan', 'lightCyan', 'lightBlue', + 'lightMagenta', 'gray', 'lightGray'], range(16))) + +# Mapping of IRC color values to their color names. +_IRC_COLOR_NAMES = dict((code, name) for name, code in _IRC_COLORS.items()) + + + +class _CharacterAttributes(_textattributes.CharacterAttributesMixin): + """ + Factory for character attributes, including foreground and background color + and non-color attributes such as bold, reverse video and underline. + + Character attributes are applied to actual text by using object + indexing-syntax (C{obj['abc']}) after accessing a factory attribute, for + example:: + + attributes.bold['Some text'] + + These can be nested to mix attributes:: + + attributes.bold[attributes.underline['Some text']] + + And multiple values can be passed:: + + attributes.normal[attributes.bold['Some'], ' text'] + + Non-color attributes can be accessed by attribute name, available + attributes are: + + - bold + - reverseVideo + - underline + + Available colors are: + + 0. white + 1. black + 2. blue + 3. green + 4. light red + 5. red + 6. magenta + 7. orange + 8. yellow + 9. light green + 10. cyan + 11. light cyan + 12. light blue + 13. light magenta + 14. gray + 15. light gray + + @ivar fg: Foreground colors accessed by attribute name, see above + for possible names. + + @ivar bg: Background colors accessed by attribute name, see above + for possible names. + + @since: 13.1 + """ + fg = _textattributes._ColorAttribute( + _textattributes._ForegroundColorAttr, _IRC_COLORS) + bg = _textattributes._ColorAttribute( + _textattributes._BackgroundColorAttr, _IRC_COLORS) + + attrs = { + 'bold': _BOLD, + 'reverseVideo': _REVERSE_VIDEO, + 'underline': _UNDERLINE} + + + +attributes = _CharacterAttributes() + + + +class _FormattingState(_textattributes._FormattingStateMixin): + """ + Formatting state/attributes of a single character. + + Attributes include: + - Formatting nullifier + - Bold + - Underline + - Reverse video + - Foreground color + - Background color + + @since: 13.1 + """ + compareAttributes = ( + 'off', 'bold', 'underline', 'reverseVideo', 'foreground', 'background') + + + def __init__(self, off=False, bold=False, underline=False, + reverseVideo=False, foreground=None, background=None): + self.off = off + self.bold = bold + self.underline = underline + self.reverseVideo = reverseVideo + self.foreground = foreground + self.background = background + + + def toMIRCControlCodes(self): + """ + Emit a mIRC control sequence that will set up all the attributes this + formatting state has set. + + @return: A string containing mIRC control sequences that mimic this + formatting state. + """ + attrs = [] + if self.bold: + attrs.append(_BOLD) + if self.underline: + attrs.append(_UNDERLINE) + if self.reverseVideo: + attrs.append(_REVERSE_VIDEO) + if self.foreground is not None or self.background is not None: + c = '' + if self.foreground is not None: + c += '%02d' % (self.foreground,) + if self.background is not None: + c += ',%02d' % (self.background,) + attrs.append(_COLOR + c) + return _OFF + ''.join(map(str, attrs)) + + + +def _foldr(f, z, xs): + """ + Apply a function of two arguments cumulatively to the items of + a sequence, from right to left, so as to reduce the sequence to + a single value. + + @type f: C{callable} taking 2 arguments + + @param z: Initial value. + + @param xs: Sequence to reduce. + + @return: Single value resulting from reducing C{xs}. + """ + return reduce(lambda x, y: f(y, x), reversed(xs), z) + + + +class _FormattingParser(_CommandDispatcherMixin): + """ + A finite-state machine that parses formatted IRC text. + + Currently handled formatting includes: bold, reverse, underline, + mIRC color codes and the ability to remove all current formatting. + + @see: U{http://www.mirc.co.uk/help/color.txt} + + @type _formatCodes: C{dict} mapping C{str} to C{str} + @cvar _formatCodes: Mapping of format code values to names. + + @type state: C{str} + @ivar state: Current state of the finite-state machine. + + @type _buffer: C{str} + @ivar _buffer: Buffer, containing the text content, of the formatting + sequence currently being parsed, the buffer is used as the content for + L{_attrs} before being added to L{_result} and emptied upon calling + L{emit}. + + @type _attrs: C{set} + @ivar _attrs: Set of the applicable formatting states (bold, underline, + etc.) for the current L{_buffer}, these are applied to L{_buffer} when + calling L{emit}. + + @type foreground: L{_ForegroundColorAttr} + @ivar foreground: Current foreground color attribute, or L{None}. + + @type background: L{_BackgroundColorAttr} + @ivar background: Current background color attribute, or L{None}. + + @ivar _result: Current parse result. + """ + prefix = 'state' + + + _formatCodes = { + _OFF: 'off', + _BOLD: 'bold', + _COLOR: 'color', + _REVERSE_VIDEO: 'reverseVideo', + _UNDERLINE: 'underline'} + + + def __init__(self): + self.state = 'TEXT' + self._buffer = '' + self._attrs = set() + self._result = None + self.foreground = None + self.background = None + + + def process(self, ch): + """ + Handle input. + + @type ch: C{str} + @param ch: A single character of input to process + """ + self.dispatch(self.state, ch) + + + def complete(self): + """ + Flush the current buffer and return the final parsed result. + + @return: Structured text and attributes. + """ + self.emit() + if self._result is None: + self._result = attributes.normal + return self._result + + + def emit(self): + """ + Add the currently parsed input to the result. + """ + if self._buffer: + attrs = [getattr(attributes, name) for name in self._attrs] + attrs.extend(filter(None, [self.foreground, self.background])) + if not attrs: + attrs.append(attributes.normal) + attrs.append(self._buffer) + + attr = _foldr(operator.getitem, attrs.pop(), attrs) + if self._result is None: + self._result = attr + else: + self._result[attr] + self._buffer = '' + + + def state_TEXT(self, ch): + """ + Handle the "text" state. + + Along with regular text, single token formatting codes are handled + in this state too. + + @param ch: The character being processed. + """ + formatName = self._formatCodes.get(ch) + if formatName == 'color': + self.emit() + self.state = 'COLOR_FOREGROUND' + else: + if formatName is None: + self._buffer += ch + else: + self.emit() + if formatName == 'off': + self._attrs = set() + self.foreground = self.background = None + else: + self._attrs.symmetric_difference_update([formatName]) + + + def state_COLOR_FOREGROUND(self, ch): + """ + Handle the foreground color state. + + Foreground colors can consist of up to two digits and may optionally + end in a I{,}. Any non-digit or non-comma characters are treated as + invalid input and result in the state being reset to "text". + + @param ch: The character being processed. + """ + # Color codes may only be a maximum of two characters. + if ch.isdigit() and len(self._buffer) < 2: + self._buffer += ch + else: + if self._buffer: + # Wrap around for color numbers higher than we support, like + # most other IRC clients. + col = int(self._buffer) % len(_IRC_COLORS) + self.foreground = getattr(attributes.fg, _IRC_COLOR_NAMES[col]) + else: + # If there were no digits, then this has been an empty color + # code and we can reset the color state. + self.foreground = self.background = None + + if ch == ',' and self._buffer: + # If there's a comma and it's not the first thing, move on to + # the background state. + self._buffer = '' + self.state = 'COLOR_BACKGROUND' + else: + # Otherwise, this is a bogus color code, fall back to text. + self._buffer = '' + self.state = 'TEXT' + self.emit() + self.process(ch) + + + def state_COLOR_BACKGROUND(self, ch): + """ + Handle the background color state. + + Background colors can consist of up to two digits and must occur after + a foreground color and must be preceded by a I{,}. Any non-digit + character is treated as invalid input and results in the state being + set to "text". + + @param ch: The character being processed. + """ + # Color codes may only be a maximum of two characters. + if ch.isdigit() and len(self._buffer) < 2: + self._buffer += ch + else: + if self._buffer: + # Wrap around for color numbers higher than we support, like + # most other IRC clients. + col = int(self._buffer) % len(_IRC_COLORS) + self.background = getattr(attributes.bg, _IRC_COLOR_NAMES[col]) + self._buffer = '' + + self.emit() + self.state = 'TEXT' + self.process(ch) + + + +def parseFormattedText(text): + """ + Parse text containing IRC formatting codes into structured information. + + Color codes are mapped from 0 to 15 and wrap around if greater than 15. + + @type text: C{str} + @param text: Formatted text to parse. + + @return: Structured text and attributes. + + @since: 13.1 + """ + state = _FormattingParser() + for ch in text: + state.process(ch) + return state.complete() + + + +def assembleFormattedText(formatted): + """ + Assemble formatted text from structured information. + + Currently handled formatting includes: bold, reverse, underline, + mIRC color codes and the ability to remove all current formatting. + + It is worth noting that assembled text will always begin with the control + code to disable other attributes for the sake of correctness. + + For example:: + + from twisted.words.protocols.irc import attributes as A + assembleFormattedText( + A.normal[A.bold['Time: '], A.fg.lightRed['Now!']]) + + Would produce "Time: " in bold formatting, followed by "Now!" with a + foreground color of light red and without any additional formatting. + + Available attributes are: + - bold + - reverseVideo + - underline + + Available colors are: + 0. white + 1. black + 2. blue + 3. green + 4. light red + 5. red + 6. magenta + 7. orange + 8. yellow + 9. light green + 10. cyan + 11. light cyan + 12. light blue + 13. light magenta + 14. gray + 15. light gray + + @see: U{http://www.mirc.co.uk/help/color.txt} + + @param formatted: Structured text and attributes. + + @rtype: C{str} + @return: String containing mIRC control sequences that mimic those + specified by I{formatted}. + + @since: 13.1 + """ + return _textattributes.flatten( + formatted, _FormattingState(), 'toMIRCControlCodes') + + + +def stripFormatting(text): + """ + Remove all formatting codes from C{text}, leaving only the text. + + @type text: C{str} + @param text: Formatted text to parse. + + @rtype: C{str} + @return: Plain text without any control sequences. + + @since: 13.1 + """ + formatted = parseFormattedText(text) + return _textattributes.flatten( + formatted, _textattributes.DefaultFormattingState()) + + + +# CTCP constants and helper functions + +X_DELIM = chr(0o01) + +def ctcpExtract(message): + """ + Extract CTCP data from a string. + + @return: A C{dict} containing two keys: + - C{'extended'}: A list of CTCP (tag, data) tuples. + - C{'normal'}: A list of strings which were not inside a CTCP delimiter. + """ + extended_messages = [] + normal_messages = [] + retval = {'extended': extended_messages, + 'normal': normal_messages } + + messages = message.split(X_DELIM) + odd = 0 + + # X1 extended data X2 nomal data X3 extended data X4 normal... + while messages: + if odd: + extended_messages.append(messages.pop(0)) + else: + normal_messages.append(messages.pop(0)) + odd = not odd + + extended_messages[:] = filter(None, extended_messages) + normal_messages[:] = filter(None, normal_messages) + + extended_messages[:] = map(ctcpDequote, extended_messages) + for i in range(len(extended_messages)): + m = extended_messages[i].split(SPC, 1) + tag = m[0] + if len(m) > 1: + data = m[1] + else: + data = None + + extended_messages[i] = (tag, data) + + return retval + +# CTCP escaping + +M_QUOTE= chr(0o20) + +mQuoteTable = { + NUL: M_QUOTE + '0', + NL: M_QUOTE + 'n', + CR: M_QUOTE + 'r', + M_QUOTE: M_QUOTE + M_QUOTE + } + +mDequoteTable = {} +for k, v in mQuoteTable.items(): + mDequoteTable[v[-1]] = k +del k, v + +mEscape_re = re.compile('%s.' % (re.escape(M_QUOTE),), re.DOTALL) + +def lowQuote(s): + for c in (M_QUOTE, NUL, NL, CR): + s = s.replace(c, mQuoteTable[c]) + return s + +def lowDequote(s): + def sub(matchobj, mDequoteTable=mDequoteTable): + s = matchobj.group()[1] + try: + s = mDequoteTable[s] + except KeyError: + s = s + return s + + return mEscape_re.sub(sub, s) + +X_QUOTE = '\\' + +xQuoteTable = { + X_DELIM: X_QUOTE + 'a', + X_QUOTE: X_QUOTE + X_QUOTE + } + +xDequoteTable = {} + +for k, v in xQuoteTable.items(): + xDequoteTable[v[-1]] = k + +xEscape_re = re.compile('%s.' % (re.escape(X_QUOTE),), re.DOTALL) + +def ctcpQuote(s): + for c in (X_QUOTE, X_DELIM): + s = s.replace(c, xQuoteTable[c]) + return s + +def ctcpDequote(s): + def sub(matchobj, xDequoteTable=xDequoteTable): + s = matchobj.group()[1] + try: + s = xDequoteTable[s] + except KeyError: + s = s + return s + + return xEscape_re.sub(sub, s) + +def ctcpStringify(messages): + """ + @type messages: a list of extended messages. An extended + message is a (tag, data) tuple, where 'data' may be L{None}, a + string, or a list of strings to be joined with whitespace. + + @returns: String + """ + coded_messages = [] + for (tag, data) in messages: + if data: + if not isinstance(data, str): + try: + # data as list-of-strings + data = " ".join(map(str, data)) + except TypeError: + # No? Then use it's %s representation. + pass + m = "%s %s" % (tag, data) + else: + m = str(tag) + m = ctcpQuote(m) + m = "%s%s%s" % (X_DELIM, m, X_DELIM) + coded_messages.append(m) + + line = ''.join(coded_messages) + return line + + +# Constants (from RFC 2812) +RPL_WELCOME = '001' +RPL_YOURHOST = '002' +RPL_CREATED = '003' +RPL_MYINFO = '004' +RPL_ISUPPORT = '005' +RPL_BOUNCE = '010' +RPL_USERHOST = '302' +RPL_ISON = '303' +RPL_AWAY = '301' +RPL_UNAWAY = '305' +RPL_NOWAWAY = '306' +RPL_WHOISUSER = '311' +RPL_WHOISSERVER = '312' +RPL_WHOISOPERATOR = '313' +RPL_WHOISIDLE = '317' +RPL_ENDOFWHOIS = '318' +RPL_WHOISCHANNELS = '319' +RPL_WHOWASUSER = '314' +RPL_ENDOFWHOWAS = '369' +RPL_LISTSTART = '321' +RPL_LIST = '322' +RPL_LISTEND = '323' +RPL_UNIQOPIS = '325' +RPL_CHANNELMODEIS = '324' +RPL_NOTOPIC = '331' +RPL_TOPIC = '332' +RPL_INVITING = '341' +RPL_SUMMONING = '342' +RPL_INVITELIST = '346' +RPL_ENDOFINVITELIST = '347' +RPL_EXCEPTLIST = '348' +RPL_ENDOFEXCEPTLIST = '349' +RPL_VERSION = '351' +RPL_WHOREPLY = '352' +RPL_ENDOFWHO = '315' +RPL_NAMREPLY = '353' +RPL_ENDOFNAMES = '366' +RPL_LINKS = '364' +RPL_ENDOFLINKS = '365' +RPL_BANLIST = '367' +RPL_ENDOFBANLIST = '368' +RPL_INFO = '371' +RPL_ENDOFINFO = '374' +RPL_MOTDSTART = '375' +RPL_MOTD = '372' +RPL_ENDOFMOTD = '376' +RPL_YOUREOPER = '381' +RPL_REHASHING = '382' +RPL_YOURESERVICE = '383' +RPL_TIME = '391' +RPL_USERSSTART = '392' +RPL_USERS = '393' +RPL_ENDOFUSERS = '394' +RPL_NOUSERS = '395' +RPL_TRACELINK = '200' +RPL_TRACECONNECTING = '201' +RPL_TRACEHANDSHAKE = '202' +RPL_TRACEUNKNOWN = '203' +RPL_TRACEOPERATOR = '204' +RPL_TRACEUSER = '205' +RPL_TRACESERVER = '206' +RPL_TRACESERVICE = '207' +RPL_TRACENEWTYPE = '208' +RPL_TRACECLASS = '209' +RPL_TRACERECONNECT = '210' +RPL_TRACELOG = '261' +RPL_TRACEEND = '262' +RPL_STATSLINKINFO = '211' +RPL_STATSCOMMANDS = '212' +RPL_ENDOFSTATS = '219' +RPL_STATSUPTIME = '242' +RPL_STATSOLINE = '243' +RPL_UMODEIS = '221' +RPL_SERVLIST = '234' +RPL_SERVLISTEND = '235' +RPL_LUSERCLIENT = '251' +RPL_LUSEROP = '252' +RPL_LUSERUNKNOWN = '253' +RPL_LUSERCHANNELS = '254' +RPL_LUSERME = '255' +RPL_ADMINME = '256' +RPL_ADMINLOC1 = '257' +RPL_ADMINLOC2 = '258' +RPL_ADMINEMAIL = '259' +RPL_TRYAGAIN = '263' +ERR_NOSUCHNICK = '401' +ERR_NOSUCHSERVER = '402' +ERR_NOSUCHCHANNEL = '403' +ERR_CANNOTSENDTOCHAN = '404' +ERR_TOOMANYCHANNELS = '405' +ERR_WASNOSUCHNICK = '406' +ERR_TOOMANYTARGETS = '407' +ERR_NOSUCHSERVICE = '408' +ERR_NOORIGIN = '409' +ERR_NORECIPIENT = '411' +ERR_NOTEXTTOSEND = '412' +ERR_NOTOPLEVEL = '413' +ERR_WILDTOPLEVEL = '414' +ERR_BADMASK = '415' +# Defined in errata. +# https://www.rfc-editor.org/errata_search.php?rfc=2812&eid=2822 +ERR_TOOMANYMATCHES = '416' +ERR_UNKNOWNCOMMAND = '421' +ERR_NOMOTD = '422' +ERR_NOADMININFO = '423' +ERR_FILEERROR = '424' +ERR_NONICKNAMEGIVEN = '431' +ERR_ERRONEUSNICKNAME = '432' +ERR_NICKNAMEINUSE = '433' +ERR_NICKCOLLISION = '436' +ERR_UNAVAILRESOURCE = '437' +ERR_USERNOTINCHANNEL = '441' +ERR_NOTONCHANNEL = '442' +ERR_USERONCHANNEL = '443' +ERR_NOLOGIN = '444' +ERR_SUMMONDISABLED = '445' +ERR_USERSDISABLED = '446' +ERR_NOTREGISTERED = '451' +ERR_NEEDMOREPARAMS = '461' +ERR_ALREADYREGISTRED = '462' +ERR_NOPERMFORHOST = '463' +ERR_PASSWDMISMATCH = '464' +ERR_YOUREBANNEDCREEP = '465' +ERR_YOUWILLBEBANNED = '466' +ERR_KEYSET = '467' +ERR_CHANNELISFULL = '471' +ERR_UNKNOWNMODE = '472' +ERR_INVITEONLYCHAN = '473' +ERR_BANNEDFROMCHAN = '474' +ERR_BADCHANNELKEY = '475' +ERR_BADCHANMASK = '476' +ERR_NOCHANMODES = '477' +ERR_BANLISTFULL = '478' +ERR_NOPRIVILEGES = '481' +ERR_CHANOPRIVSNEEDED = '482' +ERR_CANTKILLSERVER = '483' +ERR_RESTRICTED = '484' +ERR_UNIQOPPRIVSNEEDED = '485' +ERR_NOOPERHOST = '491' +ERR_NOSERVICEHOST = '492' +ERR_UMODEUNKNOWNFLAG = '501' +ERR_USERSDONTMATCH = '502' + +# And hey, as long as the strings are already intern'd... +symbolic_to_numeric = { + "RPL_WELCOME": '001', + "RPL_YOURHOST": '002', + "RPL_CREATED": '003', + "RPL_MYINFO": '004', + "RPL_ISUPPORT": '005', + "RPL_BOUNCE": '010', + "RPL_USERHOST": '302', + "RPL_ISON": '303', + "RPL_AWAY": '301', + "RPL_UNAWAY": '305', + "RPL_NOWAWAY": '306', + "RPL_WHOISUSER": '311', + "RPL_WHOISSERVER": '312', + "RPL_WHOISOPERATOR": '313', + "RPL_WHOISIDLE": '317', + "RPL_ENDOFWHOIS": '318', + "RPL_WHOISCHANNELS": '319', + "RPL_WHOWASUSER": '314', + "RPL_ENDOFWHOWAS": '369', + "RPL_LISTSTART": '321', + "RPL_LIST": '322', + "RPL_LISTEND": '323', + "RPL_UNIQOPIS": '325', + "RPL_CHANNELMODEIS": '324', + "RPL_NOTOPIC": '331', + "RPL_TOPIC": '332', + "RPL_INVITING": '341', + "RPL_SUMMONING": '342', + "RPL_INVITELIST": '346', + "RPL_ENDOFINVITELIST": '347', + "RPL_EXCEPTLIST": '348', + "RPL_ENDOFEXCEPTLIST": '349', + "RPL_VERSION": '351', + "RPL_WHOREPLY": '352', + "RPL_ENDOFWHO": '315', + "RPL_NAMREPLY": '353', + "RPL_ENDOFNAMES": '366', + "RPL_LINKS": '364', + "RPL_ENDOFLINKS": '365', + "RPL_BANLIST": '367', + "RPL_ENDOFBANLIST": '368', + "RPL_INFO": '371', + "RPL_ENDOFINFO": '374', + "RPL_MOTDSTART": '375', + "RPL_MOTD": '372', + "RPL_ENDOFMOTD": '376', + "RPL_YOUREOPER": '381', + "RPL_REHASHING": '382', + "RPL_YOURESERVICE": '383', + "RPL_TIME": '391', + "RPL_USERSSTART": '392', + "RPL_USERS": '393', + "RPL_ENDOFUSERS": '394', + "RPL_NOUSERS": '395', + "RPL_TRACELINK": '200', + "RPL_TRACECONNECTING": '201', + "RPL_TRACEHANDSHAKE": '202', + "RPL_TRACEUNKNOWN": '203', + "RPL_TRACEOPERATOR": '204', + "RPL_TRACEUSER": '205', + "RPL_TRACESERVER": '206', + "RPL_TRACESERVICE": '207', + "RPL_TRACENEWTYPE": '208', + "RPL_TRACECLASS": '209', + "RPL_TRACERECONNECT": '210', + "RPL_TRACELOG": '261', + "RPL_TRACEEND": '262', + "RPL_STATSLINKINFO": '211', + "RPL_STATSCOMMANDS": '212', + "RPL_ENDOFSTATS": '219', + "RPL_STATSUPTIME": '242', + "RPL_STATSOLINE": '243', + "RPL_UMODEIS": '221', + "RPL_SERVLIST": '234', + "RPL_SERVLISTEND": '235', + "RPL_LUSERCLIENT": '251', + "RPL_LUSEROP": '252', + "RPL_LUSERUNKNOWN": '253', + "RPL_LUSERCHANNELS": '254', + "RPL_LUSERME": '255', + "RPL_ADMINME": '256', + "RPL_ADMINLOC1": '257', + "RPL_ADMINLOC2": '258', + "RPL_ADMINEMAIL": '259', + "RPL_TRYAGAIN": '263', + "ERR_NOSUCHNICK": '401', + "ERR_NOSUCHSERVER": '402', + "ERR_NOSUCHCHANNEL": '403', + "ERR_CANNOTSENDTOCHAN": '404', + "ERR_TOOMANYCHANNELS": '405', + "ERR_WASNOSUCHNICK": '406', + "ERR_TOOMANYTARGETS": '407', + "ERR_NOSUCHSERVICE": '408', + "ERR_NOORIGIN": '409', + "ERR_NORECIPIENT": '411', + "ERR_NOTEXTTOSEND": '412', + "ERR_NOTOPLEVEL": '413', + "ERR_WILDTOPLEVEL": '414', + "ERR_BADMASK": '415', + "ERR_TOOMANYMATCHES": '416', + "ERR_UNKNOWNCOMMAND": '421', + "ERR_NOMOTD": '422', + "ERR_NOADMININFO": '423', + "ERR_FILEERROR": '424', + "ERR_NONICKNAMEGIVEN": '431', + "ERR_ERRONEUSNICKNAME": '432', + "ERR_NICKNAMEINUSE": '433', + "ERR_NICKCOLLISION": '436', + "ERR_UNAVAILRESOURCE": '437', + "ERR_USERNOTINCHANNEL": '441', + "ERR_NOTONCHANNEL": '442', + "ERR_USERONCHANNEL": '443', + "ERR_NOLOGIN": '444', + "ERR_SUMMONDISABLED": '445', + "ERR_USERSDISABLED": '446', + "ERR_NOTREGISTERED": '451', + "ERR_NEEDMOREPARAMS": '461', + "ERR_ALREADYREGISTRED": '462', + "ERR_NOPERMFORHOST": '463', + "ERR_PASSWDMISMATCH": '464', + "ERR_YOUREBANNEDCREEP": '465', + "ERR_YOUWILLBEBANNED": '466', + "ERR_KEYSET": '467', + "ERR_CHANNELISFULL": '471', + "ERR_UNKNOWNMODE": '472', + "ERR_INVITEONLYCHAN": '473', + "ERR_BANNEDFROMCHAN": '474', + "ERR_BADCHANNELKEY": '475', + "ERR_BADCHANMASK": '476', + "ERR_NOCHANMODES": '477', + "ERR_BANLISTFULL": '478', + "ERR_NOPRIVILEGES": '481', + "ERR_CHANOPRIVSNEEDED": '482', + "ERR_CANTKILLSERVER": '483', + "ERR_RESTRICTED": '484', + "ERR_UNIQOPPRIVSNEEDED": '485', + "ERR_NOOPERHOST": '491', + "ERR_NOSERVICEHOST": '492', + "ERR_UMODEUNKNOWNFLAG": '501', + "ERR_USERSDONTMATCH": '502', +} + +numeric_to_symbolic = {} +for k, v in symbolic_to_numeric.items(): + numeric_to_symbolic[v] = k diff --git a/contrib/python/Twisted/py2/twisted/words/protocols/jabber/__init__.py b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/__init__.py new file mode 100644 index 0000000000..ad95b6853e --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/__init__.py @@ -0,0 +1,8 @@ +# -*- test-case-name: twisted.words.test -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + + +""" +Twisted Jabber: Jabber Protocol Helpers +""" diff --git a/contrib/python/Twisted/py2/twisted/words/protocols/jabber/client.py b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/client.py new file mode 100644 index 0000000000..8f197cdafe --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/client.py @@ -0,0 +1,408 @@ +# -*- test-case-name: twisted.words.test.test_jabberclient -*- +# +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +from __future__ import absolute_import, division + +from twisted.python.compat import _coercedUnicode, unicode +from twisted.words.protocols.jabber import xmlstream, sasl, error +from twisted.words.protocols.jabber.jid import JID +from twisted.words.xish import domish, xpath, utility + +NS_XMPP_STREAMS = 'urn:ietf:params:xml:ns:xmpp-streams' +NS_XMPP_BIND = 'urn:ietf:params:xml:ns:xmpp-bind' +NS_XMPP_SESSION = 'urn:ietf:params:xml:ns:xmpp-session' +NS_IQ_AUTH_FEATURE = 'http://jabber.org/features/iq-auth' + +DigestAuthQry = xpath.internQuery("/iq/query/digest") +PlaintextAuthQry = xpath.internQuery("/iq/query/password") + +def basicClientFactory(jid, secret): + a = BasicAuthenticator(jid, secret) + return xmlstream.XmlStreamFactory(a) + +class IQ(domish.Element): + """ + Wrapper for a Info/Query packet. + + This provides the necessary functionality to send IQs and get notified when + a result comes back. It's a subclass from L{domish.Element}, so you can use + the standard DOM manipulation calls to add data to the outbound request. + + @type callbacks: L{utility.CallbackList} + @cvar callbacks: Callback list to be notified when response comes back + + """ + def __init__(self, xmlstream, type = "set"): + """ + @type xmlstream: L{xmlstream.XmlStream} + @param xmlstream: XmlStream to use for transmission of this IQ + + @type type: C{str} + @param type: IQ type identifier ('get' or 'set') + """ + + domish.Element.__init__(self, ("jabber:client", "iq")) + self.addUniqueId() + self["type"] = type + self._xmlstream = xmlstream + self.callbacks = utility.CallbackList() + + def addCallback(self, fn, *args, **kwargs): + """ + Register a callback for notification when the IQ result is available. + """ + + self.callbacks.addCallback(True, fn, *args, **kwargs) + + def send(self, to = None): + """ + Call this method to send this IQ request via the associated XmlStream. + + @param to: Jabber ID of the entity to send the request to + @type to: C{str} + + @returns: Callback list for this IQ. Any callbacks added to this list + will be fired when the result comes back. + """ + if to != None: + self["to"] = to + self._xmlstream.addOnetimeObserver("/iq[@id='%s']" % self["id"], \ + self._resultEvent) + self._xmlstream.send(self) + + def _resultEvent(self, iq): + self.callbacks.callback(iq) + self.callbacks = None + + + +class IQAuthInitializer(object): + """ + Non-SASL Authentication initializer for the initiating entity. + + This protocol is defined in + U{JEP-0078<http://www.jabber.org/jeps/jep-0078.html>} and mainly serves for + compatibility with pre-XMPP-1.0 server implementations. + + @cvar INVALID_USER_EVENT: Token to signal that authentication failed, due + to invalid username. + @type INVALID_USER_EVENT: L{str} + + @cvar AUTH_FAILED_EVENT: Token to signal that authentication failed, due to + invalid password. + @type AUTH_FAILED_EVENT: L{str} + """ + + INVALID_USER_EVENT = "//event/client/basicauth/invaliduser" + AUTH_FAILED_EVENT = "//event/client/basicauth/authfailed" + + def __init__(self, xs): + self.xmlstream = xs + + + def initialize(self): + # Send request for auth fields + iq = xmlstream.IQ(self.xmlstream, "get") + iq.addElement(("jabber:iq:auth", "query")) + jid = self.xmlstream.authenticator.jid + iq.query.addElement("username", content = jid.user) + + d = iq.send() + d.addCallbacks(self._cbAuthQuery, self._ebAuthQuery) + return d + + + def _cbAuthQuery(self, iq): + jid = self.xmlstream.authenticator.jid + password = _coercedUnicode(self.xmlstream.authenticator.password) + + # Construct auth request + reply = xmlstream.IQ(self.xmlstream, "set") + reply.addElement(("jabber:iq:auth", "query")) + reply.query.addElement("username", content = jid.user) + reply.query.addElement("resource", content = jid.resource) + + # Prefer digest over plaintext + if DigestAuthQry.matches(iq): + digest = xmlstream.hashPassword(self.xmlstream.sid, password) + reply.query.addElement("digest", content=unicode(digest)) + else: + reply.query.addElement("password", content = password) + + d = reply.send() + d.addCallbacks(self._cbAuth, self._ebAuth) + return d + + + def _ebAuthQuery(self, failure): + failure.trap(error.StanzaError) + e = failure.value + if e.condition == 'not-authorized': + self.xmlstream.dispatch(e.stanza, self.INVALID_USER_EVENT) + else: + self.xmlstream.dispatch(e.stanza, self.AUTH_FAILED_EVENT) + + return failure + + + def _cbAuth(self, iq): + pass + + + def _ebAuth(self, failure): + failure.trap(error.StanzaError) + self.xmlstream.dispatch(failure.value.stanza, self.AUTH_FAILED_EVENT) + return failure + + + +class BasicAuthenticator(xmlstream.ConnectAuthenticator): + """ + Authenticates an XmlStream against a Jabber server as a Client. + + This only implements non-SASL authentication, per + U{JEP-0078<http://www.jabber.org/jeps/jep-0078.html>}. Additionally, this + authenticator provides the ability to perform inline registration, per + U{JEP-0077<http://www.jabber.org/jeps/jep-0077.html>}. + + Under normal circumstances, the BasicAuthenticator generates the + L{xmlstream.STREAM_AUTHD_EVENT} once the stream has authenticated. However, + it can also generate other events, such as: + - L{INVALID_USER_EVENT} : Authentication failed, due to invalid username + - L{AUTH_FAILED_EVENT} : Authentication failed, due to invalid password + - L{REGISTER_FAILED_EVENT} : Registration failed + + If authentication fails for any reason, you can attempt to register by + calling the L{registerAccount} method. If the registration succeeds, a + L{xmlstream.STREAM_AUTHD_EVENT} will be fired. Otherwise, one of the above + errors will be generated (again). + + + @cvar INVALID_USER_EVENT: See L{IQAuthInitializer.INVALID_USER_EVENT}. + @type INVALID_USER_EVENT: L{str} + + @cvar AUTH_FAILED_EVENT: See L{IQAuthInitializer.AUTH_FAILED_EVENT}. + @type AUTH_FAILED_EVENT: L{str} + + @cvar REGISTER_FAILED_EVENT: Token to signal that registration failed. + @type REGISTER_FAILED_EVENT: L{str} + + """ + + namespace = "jabber:client" + + INVALID_USER_EVENT = IQAuthInitializer.INVALID_USER_EVENT + AUTH_FAILED_EVENT = IQAuthInitializer.AUTH_FAILED_EVENT + REGISTER_FAILED_EVENT = "//event/client/basicauth/registerfailed" + + def __init__(self, jid, password): + xmlstream.ConnectAuthenticator.__init__(self, jid.host) + self.jid = jid + self.password = password + + def associateWithStream(self, xs): + xs.version = (0, 0) + xmlstream.ConnectAuthenticator.associateWithStream(self, xs) + + xs.initializers = [ + xmlstream.TLSInitiatingInitializer(xs, required=False), + IQAuthInitializer(xs), + ] + + # TODO: move registration into an Initializer? + + def registerAccount(self, username = None, password = None): + if username: + self.jid.user = username + if password: + self.password = password + + iq = IQ(self.xmlstream, "set") + iq.addElement(("jabber:iq:register", "query")) + iq.query.addElement("username", content = self.jid.user) + iq.query.addElement("password", content = self.password) + + iq.addCallback(self._registerResultEvent) + + iq.send() + + def _registerResultEvent(self, iq): + if iq["type"] == "result": + # Registration succeeded -- go ahead and auth + self.streamStarted() + else: + # Registration failed + self.xmlstream.dispatch(iq, self.REGISTER_FAILED_EVENT) + + + +class CheckVersionInitializer(object): + """ + Initializer that checks if the minimum common stream version number is 1.0. + """ + + def __init__(self, xs): + self.xmlstream = xs + + + def initialize(self): + if self.xmlstream.version < (1, 0): + raise error.StreamError('unsupported-version') + + + +class BindInitializer(xmlstream.BaseFeatureInitiatingInitializer): + """ + Initializer that implements Resource Binding for the initiating entity. + + This protocol is documented in U{RFC 3920, section + 7<http://www.xmpp.org/specs/rfc3920.html#bind>}. + """ + + feature = (NS_XMPP_BIND, 'bind') + + def start(self): + iq = xmlstream.IQ(self.xmlstream, 'set') + bind = iq.addElement((NS_XMPP_BIND, 'bind')) + resource = self.xmlstream.authenticator.jid.resource + if resource: + bind.addElement('resource', content=resource) + d = iq.send() + d.addCallback(self.onBind) + return d + + + def onBind(self, iq): + if iq.bind: + self.xmlstream.authenticator.jid = JID(unicode(iq.bind.jid)) + + + +class SessionInitializer(xmlstream.BaseFeatureInitiatingInitializer): + """ + Initializer that implements session establishment for the initiating + entity. + + This protocol is defined in U{RFC 3921, section + 3<http://www.xmpp.org/specs/rfc3921.html#session>}. + """ + + feature = (NS_XMPP_SESSION, 'session') + + def start(self): + iq = xmlstream.IQ(self.xmlstream, 'set') + iq.addElement((NS_XMPP_SESSION, 'session')) + return iq.send() + + + +def XMPPClientFactory(jid, password, configurationForTLS=None): + """ + Client factory for XMPP 1.0 (only). + + This returns a L{xmlstream.XmlStreamFactory} with an L{XMPPAuthenticator} + object to perform the stream initialization steps (such as authentication). + + @see: The notes at L{XMPPAuthenticator} describe how the L{jid} and + L{password} parameters are to be used. + + @param jid: Jabber ID to connect with. + @type jid: L{jid.JID} + + @param password: password to authenticate with. + @type password: L{unicode} + + @param configurationForTLS: An object which creates appropriately + configured TLS connections. This is passed to C{startTLS} on the + transport and is preferably created using + L{twisted.internet.ssl.optionsForClientTLS}. If C{None}, the default is + to verify the server certificate against the trust roots as provided by + the platform. See L{twisted.internet._sslverify.platformTrust}. + @type configurationForTLS: L{IOpenSSLClientConnectionCreator} or C{None} + + @return: XML stream factory. + @rtype: L{xmlstream.XmlStreamFactory} + """ + a = XMPPAuthenticator(jid, password, + configurationForTLS=configurationForTLS) + return xmlstream.XmlStreamFactory(a) + + + +class XMPPAuthenticator(xmlstream.ConnectAuthenticator): + """ + Initializes an XmlStream connecting to an XMPP server as a Client. + + This authenticator performs the initialization steps needed to start + exchanging XML stanzas with an XMPP server as an XMPP client. It checks if + the server advertises XML stream version 1.0, negotiates TLS (when + available), performs SASL authentication, binds a resource and establishes + a session. + + Upon successful stream initialization, the L{xmlstream.STREAM_AUTHD_EVENT} + event will be dispatched through the XML stream object. Otherwise, the + L{xmlstream.INIT_FAILED_EVENT} event will be dispatched with a failure + object. + + After inspection of the failure, initialization can then be restarted by + calling L{ConnectAuthenticator.initializeStream}. For example, in case of + authentication failure, a user may be given the opportunity to input the + correct password. By setting the L{password} instance variable and restarting + initialization, the stream authentication step is then retried, and subsequent + steps are performed if successful. + + @ivar jid: Jabber ID to authenticate with. This may contain a resource + part, as a suggestion to the server for resource binding. A + server may override this, though. If the resource part is left + off, the server will generate a unique resource identifier. + The server will always return the full Jabber ID in the + resource binding step, and this is stored in this instance + variable. + @type jid: L{jid.JID} + + @ivar password: password to be used during SASL authentication. + @type password: L{unicode} + """ + + namespace = 'jabber:client' + + def __init__(self, jid, password, configurationForTLS=None): + """ + @param configurationForTLS: An object which creates appropriately + configured TLS connections. This is passed to C{startTLS} on the + transport and is preferably created using + L{twisted.internet.ssl.optionsForClientTLS}. If C{None}, the + default is to verify the server certificate against the trust roots + as provided by the platform. See + L{twisted.internet._sslverify.platformTrust}. + @type configurationForTLS: L{IOpenSSLClientConnectionCreator} or + C{None} + """ + xmlstream.ConnectAuthenticator.__init__(self, jid.host) + self.jid = jid + self.password = password + self._configurationForTLS = configurationForTLS + + + def associateWithStream(self, xs): + """ + Register with the XML stream. + + Populates stream's list of initializers, along with their + requiredness. This list is used by + L{ConnectAuthenticator.initializeStream} to perform the initialization + steps. + """ + xmlstream.ConnectAuthenticator.associateWithStream(self, xs) + + xs.initializers = [ + CheckVersionInitializer(xs), + xmlstream.TLSInitiatingInitializer( + xs, required=True, + configurationForTLS=self._configurationForTLS), + sasl.SASLInitiatingInitializer(xs, required=True), + BindInitializer(xs, required=True), + SessionInitializer(xs, required=False), + ] diff --git a/contrib/python/Twisted/py2/twisted/words/protocols/jabber/component.py b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/component.py new file mode 100644 index 0000000000..796550577a --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/component.py @@ -0,0 +1,475 @@ +# -*- test-case-name: twisted.words.test.test_jabbercomponent -*- +# +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +External server-side components. + +Most Jabber server implementations allow for add-on components that act as a +separate entity on the Jabber network, but use the server-to-server +functionality of a regular Jabber IM server. These so-called 'external +components' are connected to the Jabber server using the Jabber Component +Protocol as defined in U{JEP-0114<http://www.jabber.org/jeps/jep-0114.html>}. + +This module allows for writing external server-side component by assigning one +or more services implementing L{ijabber.IService} up to L{ServiceManager}. The +ServiceManager connects to the Jabber server and is responsible for the +corresponding XML stream. +""" + +from zope.interface import implementer + +from twisted.application import service +from twisted.internet import defer +from twisted.python import log +from twisted.python.compat import _coercedUnicode, unicode +from twisted.words.xish import domish +from twisted.words.protocols.jabber import error, ijabber, jstrports, xmlstream +from twisted.words.protocols.jabber.jid import internJID as JID + +NS_COMPONENT_ACCEPT = 'jabber:component:accept' + +def componentFactory(componentid, password): + """ + XML stream factory for external server-side components. + + @param componentid: JID of the component. + @type componentid: L{unicode} + @param password: password used to authenticate to the server. + @type password: C{str} + """ + a = ConnectComponentAuthenticator(componentid, password) + return xmlstream.XmlStreamFactory(a) + +class ComponentInitiatingInitializer(object): + """ + External server-side component authentication initializer for the + initiating entity. + + @ivar xmlstream: XML stream between server and component. + @type xmlstream: L{xmlstream.XmlStream} + """ + + def __init__(self, xs): + self.xmlstream = xs + self._deferred = None + + def initialize(self): + xs = self.xmlstream + hs = domish.Element((self.xmlstream.namespace, "handshake")) + digest = xmlstream.hashPassword( + xs.sid, + _coercedUnicode(xs.authenticator.password)) + hs.addContent(unicode(digest)) + + # Setup observer to watch for handshake result + xs.addOnetimeObserver("/handshake", self._cbHandshake) + xs.send(hs) + self._deferred = defer.Deferred() + return self._deferred + + def _cbHandshake(self, _): + # we have successfully shaken hands and can now consider this + # entity to represent the component JID. + self.xmlstream.thisEntity = self.xmlstream.otherEntity + self._deferred.callback(None) + + + +class ConnectComponentAuthenticator(xmlstream.ConnectAuthenticator): + """ + Authenticator to permit an XmlStream to authenticate against a Jabber + server as an external component (where the Authenticator is initiating the + stream). + """ + namespace = NS_COMPONENT_ACCEPT + + def __init__(self, componentjid, password): + """ + @type componentjid: C{str} + @param componentjid: Jabber ID that this component wishes to bind to. + + @type password: C{str} + @param password: Password/secret this component uses to authenticate. + """ + # Note that we are sending 'to' our desired component JID. + xmlstream.ConnectAuthenticator.__init__(self, componentjid) + self.password = password + + def associateWithStream(self, xs): + xs.version = (0, 0) + xmlstream.ConnectAuthenticator.associateWithStream(self, xs) + + xs.initializers = [ComponentInitiatingInitializer(xs)] + + + +class ListenComponentAuthenticator(xmlstream.ListenAuthenticator): + """ + Authenticator for accepting components. + + @since: 8.2 + @ivar secret: The shared secret used to authorized incoming component + connections. + @type secret: C{unicode}. + """ + + namespace = NS_COMPONENT_ACCEPT + + def __init__(self, secret): + self.secret = secret + xmlstream.ListenAuthenticator.__init__(self) + + + def associateWithStream(self, xs): + """ + Associate the authenticator with a stream. + + This sets the stream's version to 0.0, because the XEP-0114 component + protocol was not designed for XMPP 1.0. + """ + xs.version = (0, 0) + xmlstream.ListenAuthenticator.associateWithStream(self, xs) + + + def streamStarted(self, rootElement): + """ + Called by the stream when it has started. + + This examines the default namespace of the incoming stream and whether + there is a requested hostname for the component. Then it generates a + stream identifier, sends a response header and adds an observer for + the first incoming element, triggering L{onElement}. + """ + + xmlstream.ListenAuthenticator.streamStarted(self, rootElement) + + if rootElement.defaultUri != self.namespace: + exc = error.StreamError('invalid-namespace') + self.xmlstream.sendStreamError(exc) + return + + # self.xmlstream.thisEntity is set to the address the component + # wants to assume. + if not self.xmlstream.thisEntity: + exc = error.StreamError('improper-addressing') + self.xmlstream.sendStreamError(exc) + return + + self.xmlstream.sendHeader() + self.xmlstream.addOnetimeObserver('/*', self.onElement) + + + def onElement(self, element): + """ + Called on incoming XML Stanzas. + + The very first element received should be a request for handshake. + Otherwise, the stream is dropped with a 'not-authorized' error. If a + handshake request was received, the hash is extracted and passed to + L{onHandshake}. + """ + if (element.uri, element.name) == (self.namespace, 'handshake'): + self.onHandshake(unicode(element)) + else: + exc = error.StreamError('not-authorized') + self.xmlstream.sendStreamError(exc) + + + def onHandshake(self, handshake): + """ + Called upon receiving the handshake request. + + This checks that the given hash in C{handshake} is equal to a + calculated hash, responding with a handshake reply or a stream error. + If the handshake was ok, the stream is authorized, and XML Stanzas may + be exchanged. + """ + calculatedHash = xmlstream.hashPassword(self.xmlstream.sid, + unicode(self.secret)) + if handshake != calculatedHash: + exc = error.StreamError('not-authorized', text='Invalid hash') + self.xmlstream.sendStreamError(exc) + else: + self.xmlstream.send('<handshake/>') + self.xmlstream.dispatch(self.xmlstream, + xmlstream.STREAM_AUTHD_EVENT) + + + +@implementer(ijabber.IService) +class Service(service.Service): + """ + External server-side component service. + """ + + def componentConnected(self, xs): + pass + + def componentDisconnected(self): + pass + + def transportConnected(self, xs): + pass + + def send(self, obj): + """ + Send data over service parent's XML stream. + + @note: L{ServiceManager} maintains a queue for data sent using this + method when there is no current established XML stream. This data is + then sent as soon as a new stream has been established and initialized. + Subsequently, L{componentConnected} will be called again. If this + queueing is not desired, use C{send} on the XmlStream object (passed to + L{componentConnected}) directly. + + @param obj: data to be sent over the XML stream. This is usually an + object providing L{domish.IElement}, or serialized XML. See + L{xmlstream.XmlStream} for details. + """ + + self.parent.send(obj) + +class ServiceManager(service.MultiService): + """ + Business logic for a managed component connection to a Jabber router. + + This service maintains a single connection to a Jabber router and provides + facilities for packet routing and transmission. Business logic modules are + services implementing L{ijabber.IService} (like subclasses of L{Service}), + and added as sub-service. + """ + + def __init__(self, jid, password): + service.MultiService.__init__(self) + + # Setup defaults + self.jabberId = jid + self.xmlstream = None + + # Internal buffer of packets + self._packetQueue = [] + + # Setup the xmlstream factory + self._xsFactory = componentFactory(self.jabberId, password) + + # Register some lambda functions to keep the self.xmlstream var up to + # date + self._xsFactory.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT, + self._connected) + self._xsFactory.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self._authd) + self._xsFactory.addBootstrap(xmlstream.STREAM_END_EVENT, + self._disconnected) + + # Map addBootstrap and removeBootstrap to the underlying factory -- is + # this right? I have no clue...but it'll work for now, until i can + # think about it more. + self.addBootstrap = self._xsFactory.addBootstrap + self.removeBootstrap = self._xsFactory.removeBootstrap + + def getFactory(self): + return self._xsFactory + + def _connected(self, xs): + self.xmlstream = xs + for c in self: + if ijabber.IService.providedBy(c): + c.transportConnected(xs) + + def _authd(self, xs): + # Flush all pending packets + for p in self._packetQueue: + self.xmlstream.send(p) + self._packetQueue = [] + + # Notify all child services which implement the IService interface + for c in self: + if ijabber.IService.providedBy(c): + c.componentConnected(xs) + + def _disconnected(self, _): + self.xmlstream = None + + # Notify all child services which implement + # the IService interface + for c in self: + if ijabber.IService.providedBy(c): + c.componentDisconnected() + + def send(self, obj): + """ + Send data over the XML stream. + + When there is no established XML stream, the data is queued and sent + out when a new XML stream has been established and initialized. + + @param obj: data to be sent over the XML stream. This is usually an + object providing L{domish.IElement}, or serialized XML. See + L{xmlstream.XmlStream} for details. + """ + + if self.xmlstream != None: + self.xmlstream.send(obj) + else: + self._packetQueue.append(obj) + +def buildServiceManager(jid, password, strport): + """ + Constructs a pre-built L{ServiceManager}, using the specified strport + string. + """ + + svc = ServiceManager(jid, password) + client_svc = jstrports.client(strport, svc.getFactory()) + client_svc.setServiceParent(svc) + return svc + + + +class Router(object): + """ + XMPP Server's Router. + + A router connects the different components of the XMPP service and routes + messages between them based on the given routing table. + + Connected components are trusted to have correct addressing in the + stanzas they offer for routing. + + A route destination of L{None} adds a default route. Traffic for which no + specific route exists, will be routed to this default route. + + @since: 8.2 + @ivar routes: Routes based on the host part of JIDs. Maps host names to the + L{EventDispatcher<utility.EventDispatcher>}s that should + receive the traffic. A key of L{None} means the default + route. + @type routes: C{dict} + """ + + def __init__(self): + self.routes = {} + + + def addRoute(self, destination, xs): + """ + Add a new route. + + The passed XML Stream C{xs} will have an observer for all stanzas + added to route its outgoing traffic. In turn, traffic for + C{destination} will be passed to this stream. + + @param destination: Destination of the route to be added as a host name + or L{None} for the default route. + @type destination: C{str} or L{None}. + @param xs: XML Stream to register the route for. + @type xs: L{EventDispatcher<utility.EventDispatcher>}. + """ + self.routes[destination] = xs + xs.addObserver('/*', self.route) + + + def removeRoute(self, destination, xs): + """ + Remove a route. + + @param destination: Destination of the route that should be removed. + @type destination: C{str}. + @param xs: XML Stream to remove the route for. + @type xs: L{EventDispatcher<utility.EventDispatcher>}. + """ + xs.removeObserver('/*', self.route) + if (xs == self.routes[destination]): + del self.routes[destination] + + + def route(self, stanza): + """ + Route a stanza. + + @param stanza: The stanza to be routed. + @type stanza: L{domish.Element}. + """ + destination = JID(stanza['to']) + + log.msg("Routing to %s: %r" % (destination.full(), stanza.toXml())) + + if destination.host in self.routes: + self.routes[destination.host].send(stanza) + else: + self.routes[None].send(stanza) + + + +class XMPPComponentServerFactory(xmlstream.XmlStreamServerFactory): + """ + XMPP Component Server factory. + + This factory accepts XMPP external component connections and makes + the router service route traffic for a component's bound domain + to that component. + + @since: 8.2 + """ + + logTraffic = False + + def __init__(self, router, secret='secret'): + self.router = router + self.secret = secret + + def authenticatorFactory(): + return ListenComponentAuthenticator(self.secret) + + xmlstream.XmlStreamServerFactory.__init__(self, authenticatorFactory) + self.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT, + self.onConnectionMade) + self.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, + self.onAuthenticated) + + self.serial = 0 + + + def onConnectionMade(self, xs): + """ + Called when a component connection was made. + + This enables traffic debugging on incoming streams. + """ + xs.serial = self.serial + self.serial += 1 + + def logDataIn(buf): + log.msg("RECV (%d): %r" % (xs.serial, buf)) + + def logDataOut(buf): + log.msg("SEND (%d): %r" % (xs.serial, buf)) + + if self.logTraffic: + xs.rawDataInFn = logDataIn + xs.rawDataOutFn = logDataOut + + xs.addObserver(xmlstream.STREAM_ERROR_EVENT, self.onError) + + + def onAuthenticated(self, xs): + """ + Called when a component has successfully authenticated. + + Add the component to the routing table and establish a handler + for a closed connection. + """ + destination = xs.thisEntity.host + + self.router.addRoute(destination, xs) + xs.addObserver(xmlstream.STREAM_END_EVENT, self.onConnectionLost, 0, + destination, xs) + + + def onError(self, reason): + log.err(reason, "Stream Error") + + + def onConnectionLost(self, destination, xs, reason): + self.router.removeRoute(destination, xs) diff --git a/contrib/python/Twisted/py2/twisted/words/protocols/jabber/error.py b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/error.py new file mode 100644 index 0000000000..481ae1de7e --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/error.py @@ -0,0 +1,331 @@ +# -*- test-case-name: twisted.words.test.test_jabbererror -*- +# +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +XMPP Error support. +""" + +from __future__ import absolute_import, division + +import copy + +from twisted.python.compat import unicode +from twisted.words.xish import domish + +NS_XML = "http://www.w3.org/XML/1998/namespace" +NS_XMPP_STREAMS = "urn:ietf:params:xml:ns:xmpp-streams" +NS_XMPP_STANZAS = "urn:ietf:params:xml:ns:xmpp-stanzas" + +STANZA_CONDITIONS = { + 'bad-request': {'code': '400', 'type': 'modify'}, + 'conflict': {'code': '409', 'type': 'cancel'}, + 'feature-not-implemented': {'code': '501', 'type': 'cancel'}, + 'forbidden': {'code': '403', 'type': 'auth'}, + 'gone': {'code': '302', 'type': 'modify'}, + 'internal-server-error': {'code': '500', 'type': 'wait'}, + 'item-not-found': {'code': '404', 'type': 'cancel'}, + 'jid-malformed': {'code': '400', 'type': 'modify'}, + 'not-acceptable': {'code': '406', 'type': 'modify'}, + 'not-allowed': {'code': '405', 'type': 'cancel'}, + 'not-authorized': {'code': '401', 'type': 'auth'}, + 'payment-required': {'code': '402', 'type': 'auth'}, + 'recipient-unavailable': {'code': '404', 'type': 'wait'}, + 'redirect': {'code': '302', 'type': 'modify'}, + 'registration-required': {'code': '407', 'type': 'auth'}, + 'remote-server-not-found': {'code': '404', 'type': 'cancel'}, + 'remote-server-timeout': {'code': '504', 'type': 'wait'}, + 'resource-constraint': {'code': '500', 'type': 'wait'}, + 'service-unavailable': {'code': '503', 'type': 'cancel'}, + 'subscription-required': {'code': '407', 'type': 'auth'}, + 'undefined-condition': {'code': '500', 'type': None}, + 'unexpected-request': {'code': '400', 'type': 'wait'}, +} + +CODES_TO_CONDITIONS = { + '302': ('gone', 'modify'), + '400': ('bad-request', 'modify'), + '401': ('not-authorized', 'auth'), + '402': ('payment-required', 'auth'), + '403': ('forbidden', 'auth'), + '404': ('item-not-found', 'cancel'), + '405': ('not-allowed', 'cancel'), + '406': ('not-acceptable', 'modify'), + '407': ('registration-required', 'auth'), + '408': ('remote-server-timeout', 'wait'), + '409': ('conflict', 'cancel'), + '500': ('internal-server-error', 'wait'), + '501': ('feature-not-implemented', 'cancel'), + '502': ('service-unavailable', 'wait'), + '503': ('service-unavailable', 'cancel'), + '504': ('remote-server-timeout', 'wait'), + '510': ('service-unavailable', 'cancel'), +} + +class BaseError(Exception): + """ + Base class for XMPP error exceptions. + + @cvar namespace: The namespace of the C{error} element generated by + C{getElement}. + @type namespace: C{str} + @ivar condition: The error condition. The valid values are defined by + subclasses of L{BaseError}. + @type contition: C{str} + @ivar text: Optional text message to supplement the condition or application + specific condition. + @type text: C{unicode} + @ivar textLang: Identifier of the language used for the message in C{text}. + Values are as described in RFC 3066. + @type textLang: C{str} + @ivar appCondition: Application specific condition element, supplementing + the error condition in C{condition}. + @type appCondition: object providing L{domish.IElement}. + """ + + namespace = None + + def __init__(self, condition, text=None, textLang=None, appCondition=None): + Exception.__init__(self) + self.condition = condition + self.text = text + self.textLang = textLang + self.appCondition = appCondition + + + def __str__(self): + message = "%s with condition %r" % (self.__class__.__name__, + self.condition) + + if self.text: + message += ': ' + self.text + + return message + + + def getElement(self): + """ + Get XML representation from self. + + The method creates an L{domish} representation of the + error data contained in this exception. + + @rtype: L{domish.Element} + """ + error = domish.Element((None, 'error')) + error.addElement((self.namespace, self.condition)) + if self.text: + text = error.addElement((self.namespace, 'text'), + content=self.text) + if self.textLang: + text[(NS_XML, 'lang')] = self.textLang + if self.appCondition: + error.addChild(self.appCondition) + return error + + + +class StreamError(BaseError): + """ + Stream Error exception. + + Refer to RFC 3920, section 4.7.3, for the allowed values for C{condition}. + """ + + namespace = NS_XMPP_STREAMS + + def getElement(self): + """ + Get XML representation from self. + + Overrides the base L{BaseError.getElement} to make sure the returned + element is in the XML Stream namespace. + + @rtype: L{domish.Element} + """ + from twisted.words.protocols.jabber.xmlstream import NS_STREAMS + + error = BaseError.getElement(self) + error.uri = NS_STREAMS + return error + + + +class StanzaError(BaseError): + """ + Stanza Error exception. + + Refer to RFC 3920, section 9.3, for the allowed values for C{condition} and + C{type}. + + @ivar type: The stanza error type. Gives a suggestion to the recipient + of the error on how to proceed. + @type type: C{str} + @ivar code: A numeric identifier for the error condition for backwards + compatibility with pre-XMPP Jabber implementations. + """ + + namespace = NS_XMPP_STANZAS + + def __init__(self, condition, type=None, text=None, textLang=None, + appCondition=None): + BaseError.__init__(self, condition, text, textLang, appCondition) + + if type is None: + try: + type = STANZA_CONDITIONS[condition]['type'] + except KeyError: + pass + self.type = type + + try: + self.code = STANZA_CONDITIONS[condition]['code'] + except KeyError: + self.code = None + + self.children = [] + self.iq = None + + + def getElement(self): + """ + Get XML representation from self. + + Overrides the base L{BaseError.getElement} to make sure the returned + element has a C{type} attribute and optionally a legacy C{code} + attribute. + + @rtype: L{domish.Element} + """ + error = BaseError.getElement(self) + error['type'] = self.type + if self.code: + error['code'] = self.code + return error + + + def toResponse(self, stanza): + """ + Construct error response stanza. + + The C{stanza} is transformed into an error response stanza by + swapping the C{to} and C{from} addresses and inserting an error + element. + + @note: This creates a shallow copy of the list of child elements of the + stanza. The child elements themselves are not copied themselves, + and references to their parent element will still point to the + original stanza element. + + The serialization of an element does not use the reference to + its parent, so the typical use case of immediately sending out + the constructed error response is not affected. + + @param stanza: the stanza to respond to + @type stanza: L{domish.Element} + """ + from twisted.words.protocols.jabber.xmlstream import toResponse + response = toResponse(stanza, stanzaType='error') + response.children = copy.copy(stanza.children) + response.addChild(self.getElement()) + return response + + + +def _parseError(error, errorNamespace): + """ + Parses an error element. + + @param error: The error element to be parsed + @type error: L{domish.Element} + @param errorNamespace: The namespace of the elements that hold the error + condition and text. + @type errorNamespace: C{str} + @return: Dictionary with extracted error information. If present, keys + C{condition}, C{text}, C{textLang} have a string value, + and C{appCondition} has an L{domish.Element} value. + @rtype: C{dict} + """ + condition = None + text = None + textLang = None + appCondition = None + + for element in error.elements(): + if element.uri == errorNamespace: + if element.name == 'text': + text = unicode(element) + textLang = element.getAttribute((NS_XML, 'lang')) + else: + condition = element.name + else: + appCondition = element + + return { + 'condition': condition, + 'text': text, + 'textLang': textLang, + 'appCondition': appCondition, + } + + + +def exceptionFromStreamError(element): + """ + Build an exception object from a stream error. + + @param element: the stream error + @type element: L{domish.Element} + @return: the generated exception object + @rtype: L{StreamError} + """ + error = _parseError(element, NS_XMPP_STREAMS) + + exception = StreamError(error['condition'], + error['text'], + error['textLang'], + error['appCondition']) + + return exception + + + +def exceptionFromStanza(stanza): + """ + Build an exception object from an error stanza. + + @param stanza: the error stanza + @type stanza: L{domish.Element} + @return: the generated exception object + @rtype: L{StanzaError} + """ + children = [] + condition = text = textLang = appCondition = type = code = None + + for element in stanza.elements(): + if element.name == 'error' and element.uri == stanza.uri: + code = element.getAttribute('code') + type = element.getAttribute('type') + error = _parseError(element, NS_XMPP_STANZAS) + condition = error['condition'] + text = error['text'] + textLang = error['textLang'] + appCondition = error['appCondition'] + + if not condition and code: + condition, type = CODES_TO_CONDITIONS[code] + text = unicode(stanza.error) + else: + children.append(element) + + if condition is None: + # TODO: raise exception instead? + return StanzaError(None) + + exception = StanzaError(condition, type, text, textLang, appCondition) + + exception.children = children + exception.stanza = stanza + + return exception diff --git a/contrib/python/Twisted/py2/twisted/words/protocols/jabber/ijabber.py b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/ijabber.py new file mode 100644 index 0000000000..e6745b10a4 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/ijabber.py @@ -0,0 +1,201 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Public Jabber Interfaces. +""" + +from zope.interface import Attribute, Interface + +class IInitializer(Interface): + """ + Interface for XML stream initializers. + + Initializers perform a step in getting the XML stream ready to be + used for the exchange of XML stanzas. + """ + + + +class IInitiatingInitializer(IInitializer): + """ + Interface for XML stream initializers for the initiating entity. + """ + + xmlstream = Attribute("""The associated XML stream""") + + def initialize(): + """ + Initiate the initialization step. + + May return a deferred when the initialization is done asynchronously. + """ + + + +class IIQResponseTracker(Interface): + """ + IQ response tracker interface. + + The XMPP stanza C{iq} has a request-response nature that fits + naturally with deferreds. You send out a request and when the response + comes back a deferred is fired. + + The L{twisted.words.protocols.jabber.client.IQ} class implements a C{send} + method that returns a deferred. This deferred is put in a dictionary that + is kept in an L{XmlStream} object, keyed by the request stanzas C{id} + attribute. + + An object providing this interface (usually an instance of L{XmlStream}), + keeps the said dictionary and sets observers on the iq stanzas of type + C{result} and C{error} and lets the callback fire the associated deferred. + """ + iqDeferreds = Attribute("Dictionary of deferreds waiting for an iq " + "response") + + + +class IXMPPHandler(Interface): + """ + Interface for XMPP protocol handlers. + + Objects that provide this interface can be added to a stream manager to + handle of (part of) an XMPP extension protocol. + """ + + parent = Attribute("""XML stream manager for this handler""") + xmlstream = Attribute("""The managed XML stream""") + + def setHandlerParent(parent): + """ + Set the parent of the handler. + + @type parent: L{IXMPPHandlerCollection} + """ + + + def disownHandlerParent(parent): + """ + Remove the parent of the handler. + + @type parent: L{IXMPPHandlerCollection} + """ + + + def makeConnection(xs): + """ + A connection over the underlying transport of the XML stream has been + established. + + At this point, no traffic has been exchanged over the XML stream + given in C{xs}. + + This should setup L{xmlstream} and call L{connectionMade}. + + @type xs: + L{twisted.words.protocols.jabber.xmlstream.XmlStream} + """ + + + def connectionMade(): + """ + Called after a connection has been established. + + This method can be used to change properties of the XML Stream, its + authenticator or the stream manager prior to stream initialization + (including authentication). + """ + + + def connectionInitialized(): + """ + The XML stream has been initialized. + + At this point, authentication was successful, and XML stanzas can be + exchanged over the XML stream L{xmlstream}. This method can be + used to setup observers for incoming stanzas. + """ + + + def connectionLost(reason): + """ + The XML stream has been closed. + + Subsequent use of C{parent.send} will result in data being queued + until a new connection has been established. + + @type reason: L{twisted.python.failure.Failure} + """ + + + +class IXMPPHandlerCollection(Interface): + """ + Collection of handlers. + + Contain several handlers and manage their connection. + """ + + def __iter__(): + """ + Get an iterator over all child handlers. + """ + + + def addHandler(handler): + """ + Add a child handler. + + @type handler: L{IXMPPHandler} + """ + + + def removeHandler(handler): + """ + Remove a child handler. + + @type handler: L{IXMPPHandler} + """ + + + +class IService(Interface): + """ + External server-side component service interface. + + Services that provide this interface can be added to L{ServiceManager} to + implement (part of) the functionality of the server-side component. + """ + + def componentConnected(xs): + """ + Parent component has established a connection. + + At this point, authentication was successful, and XML stanzas + can be exchanged over the XML stream C{xs}. This method can be used + to setup observers for incoming stanzas. + + @param xs: XML Stream that represents the established connection. + @type xs: L{xmlstream.XmlStream} + """ + + + def componentDisconnected(): + """ + Parent component has lost the connection to the Jabber server. + + Subsequent use of C{self.parent.send} will result in data being + queued until a new connection has been established. + """ + + + def transportConnected(xs): + """ + Parent component has established a connection over the underlying + transport. + + At this point, no traffic has been exchanged over the XML stream. This + method can be used to change properties of the XML Stream (in C{xs}), + the service manager or it's authenticator prior to stream + initialization (including authentication). + """ diff --git a/contrib/python/Twisted/py2/twisted/words/protocols/jabber/jid.py b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/jid.py new file mode 100644 index 0000000000..a5b012c682 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/jid.py @@ -0,0 +1,253 @@ +# -*- test-case-name: twisted.words.test.test_jabberjid -*- +# +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Jabber Identifier support. + +This module provides an object to represent Jabber Identifiers (JIDs) and +parse string representations into them with proper checking for illegal +characters, case folding and canonicalisation through L{stringprep<twisted.words.protocols.jabber.xmpp_stringprep>}. +""" + +from twisted.python.compat import _PY3, unicode +from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep, resourceprep, nameprep + +class InvalidFormat(Exception): + """ + The given string could not be parsed into a valid Jabber Identifier (JID). + """ + +def parse(jidstring): + """ + Parse given JID string into its respective parts and apply stringprep. + + @param jidstring: string representation of a JID. + @type jidstring: L{unicode} + @return: tuple of (user, host, resource), each of type L{unicode} as + the parsed and stringprep'd parts of the given JID. If the + given string did not have a user or resource part, the respective + field in the tuple will hold L{None}. + @rtype: L{tuple} + """ + user = None + host = None + resource = None + + # Search for delimiters + user_sep = jidstring.find("@") + res_sep = jidstring.find("/") + + if user_sep == -1: + if res_sep == -1: + # host + host = jidstring + else: + # host/resource + host = jidstring[0:res_sep] + resource = jidstring[res_sep + 1:] or None + else: + if res_sep == -1: + # user@host + user = jidstring[0:user_sep] or None + host = jidstring[user_sep + 1:] + else: + if user_sep < res_sep: + # user@host/resource + user = jidstring[0:user_sep] or None + host = jidstring[user_sep + 1:user_sep + (res_sep - user_sep)] + resource = jidstring[res_sep + 1:] or None + else: + # host/resource (with an @ in resource) + host = jidstring[0:res_sep] + resource = jidstring[res_sep + 1:] or None + + return prep(user, host, resource) + +def prep(user, host, resource): + """ + Perform stringprep on all JID fragments. + + @param user: The user part of the JID. + @type user: L{unicode} + @param host: The host part of the JID. + @type host: L{unicode} + @param resource: The resource part of the JID. + @type resource: L{unicode} + @return: The given parts with stringprep applied. + @rtype: L{tuple} + """ + + if user: + try: + user = nodeprep.prepare(unicode(user)) + except UnicodeError: + raise InvalidFormat("Invalid character in username") + else: + user = None + + if not host: + raise InvalidFormat("Server address required.") + else: + try: + host = nameprep.prepare(unicode(host)) + except UnicodeError: + raise InvalidFormat("Invalid character in hostname") + + if resource: + try: + resource = resourceprep.prepare(unicode(resource)) + except UnicodeError: + raise InvalidFormat("Invalid character in resource") + else: + resource = None + + return (user, host, resource) + +__internJIDs = {} + +def internJID(jidstring): + """ + Return interned JID. + + @rtype: L{JID} + """ + + if jidstring in __internJIDs: + return __internJIDs[jidstring] + else: + j = JID(jidstring) + __internJIDs[jidstring] = j + return j + +class JID(object): + """ + Represents a stringprep'd Jabber ID. + + JID objects are hashable so they can be used in sets and as keys in + dictionaries. + """ + + def __init__(self, str=None, tuple=None): + if not (str or tuple): + raise RuntimeError("You must provide a value for either 'str' or " + "'tuple' arguments.") + + if str: + user, host, res = parse(str) + else: + user, host, res = prep(*tuple) + + self.user = user + self.host = host + self.resource = res + + def userhost(self): + """ + Extract the bare JID as a unicode string. + + A bare JID does not have a resource part, so this returns either + C{user@host} or just C{host}. + + @rtype: L{unicode} + """ + if self.user: + return u"%s@%s" % (self.user, self.host) + else: + return self.host + + def userhostJID(self): + """ + Extract the bare JID. + + A bare JID does not have a resource part, so this returns a + L{JID} object representing either C{user@host} or just C{host}. + + If the object this method is called upon doesn't have a resource + set, it will return itself. Otherwise, the bare JID object will + be created, interned using L{internJID}. + + @rtype: L{JID} + """ + if self.resource: + return internJID(self.userhost()) + else: + return self + + def full(self): + """ + Return the string representation of this JID. + + @rtype: L{unicode} + """ + if self.user: + if self.resource: + return u"%s@%s/%s" % (self.user, self.host, self.resource) + else: + return u"%s@%s" % (self.user, self.host) + else: + if self.resource: + return u"%s/%s" % (self.host, self.resource) + else: + return self.host + + def __eq__(self, other): + """ + Equality comparison. + + L{JID}s compare equal if their user, host and resource parts all + compare equal. When comparing against instances of other types, it + uses the default comparison. + """ + if isinstance(other, JID): + return (self.user == other.user and + self.host == other.host and + self.resource == other.resource) + else: + return NotImplemented + + def __ne__(self, other): + """ + Inequality comparison. + + This negates L{__eq__} for comparison with JIDs and uses the default + comparison for other types. + """ + result = self.__eq__(other) + if result is NotImplemented: + return result + else: + return not result + + def __hash__(self): + """ + Calculate hash. + + L{JID}s with identical constituent user, host and resource parts have + equal hash values. In combination with the comparison defined on JIDs, + this allows for using L{JID}s in sets and as dictionary keys. + """ + return hash((self.user, self.host, self.resource)) + + def __unicode__(self): + """ + Get unicode representation. + + Return the string representation of this JID as a unicode string. + @see: L{full} + """ + + return self.full() + + if _PY3: + __str__ = __unicode__ + + def __repr__(self): + """ + Get object representation. + + Returns a string that would create a new JID object that compares equal + to this one. + """ + return 'JID(%r)' % self.full() diff --git a/contrib/python/Twisted/py2/twisted/words/protocols/jabber/jstrports.py b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/jstrports.py new file mode 100644 index 0000000000..61f4cc8953 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/jstrports.py @@ -0,0 +1,33 @@ +# -*- test-case-name: twisted.words.test -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + + +""" A temporary placeholder for client-capable strports, until we +sufficient use cases get identified """ + +from __future__ import absolute_import, division + +from twisted.internet.endpoints import _parse + +def _parseTCPSSL(factory, domain, port): + """ For the moment, parse TCP or SSL connections the same """ + return (domain, int(port), factory), {} + +def _parseUNIX(factory, address): + return (address, factory), {} + + +_funcs = { "tcp" : _parseTCPSSL, + "unix" : _parseUNIX, + "ssl" : _parseTCPSSL } + + +def parse(description, factory): + args, kw = _parse(description) + return (args[0].upper(),) + _funcs[args[0]](factory, *args[1:], **kw) + +def client(description, factory): + from twisted.application import internet + name, args, kw = parse(description, factory) + return getattr(internet, name + 'Client')(*args, **kw) diff --git a/contrib/python/Twisted/py2/twisted/words/protocols/jabber/sasl.py b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/sasl.py new file mode 100644 index 0000000000..3dc92d87d3 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/sasl.py @@ -0,0 +1,233 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +XMPP-specific SASL profile. +""" + +from __future__ import absolute_import, division + +from base64 import b64decode, b64encode +import re +from twisted.internet import defer +from twisted.python.compat import unicode +from twisted.words.protocols.jabber import sasl_mechanisms, xmlstream +from twisted.words.xish import domish + +NS_XMPP_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl' + +def get_mechanisms(xs): + """ + Parse the SASL feature to extract the available mechanism names. + """ + mechanisms = [] + for element in xs.features[(NS_XMPP_SASL, 'mechanisms')].elements(): + if element.name == 'mechanism': + mechanisms.append(unicode(element)) + + return mechanisms + + +class SASLError(Exception): + """ + SASL base exception. + """ + + +class SASLNoAcceptableMechanism(SASLError): + """ + The server did not present an acceptable SASL mechanism. + """ + + +class SASLAuthError(SASLError): + """ + SASL Authentication failed. + """ + def __init__(self, condition=None): + self.condition = condition + + + def __str__(self): + return "SASLAuthError with condition %r" % self.condition + + +class SASLIncorrectEncodingError(SASLError): + """ + SASL base64 encoding was incorrect. + + RFC 3920 specifies that any characters not in the base64 alphabet + and padding characters present elsewhere than at the end of the string + MUST be rejected. See also L{fromBase64}. + + This exception is raised whenever the encoded string does not adhere + to these additional restrictions or when the decoding itself fails. + + The recommended behaviour for so-called receiving entities (like servers in + client-to-server connections, see RFC 3920 for terminology) is to fail the + SASL negotiation with a C{'incorrect-encoding'} condition. For initiating + entities, one should assume the receiving entity to be either buggy or + malevolent. The stream should be terminated and reconnecting is not + advised. + """ + +base64Pattern = re.compile("^[0-9A-Za-z+/]*[0-9A-Za-z+/=]{,2}$") + +def fromBase64(s): + """ + Decode base64 encoded string. + + This helper performs regular decoding of a base64 encoded string, but also + rejects any characters that are not in the base64 alphabet and padding + occurring elsewhere from the last or last two characters, as specified in + section 14.9 of RFC 3920. This safeguards against various attack vectors + among which the creation of a covert channel that "leaks" information. + """ + + if base64Pattern.match(s) is None: + raise SASLIncorrectEncodingError() + + try: + return b64decode(s) + except Exception as e: + raise SASLIncorrectEncodingError(str(e)) + + + +class SASLInitiatingInitializer(xmlstream.BaseFeatureInitiatingInitializer): + """ + Stream initializer that performs SASL authentication. + + The supported mechanisms by this initializer are C{DIGEST-MD5}, C{PLAIN} + and C{ANONYMOUS}. The C{ANONYMOUS} SASL mechanism is used when the JID, set + on the authenticator, does not have a localpart (username), requesting an + anonymous session where the username is generated by the server. + Otherwise, C{DIGEST-MD5} and C{PLAIN} are attempted, in that order. + """ + + feature = (NS_XMPP_SASL, 'mechanisms') + _deferred = None + + def setMechanism(self): + """ + Select and setup authentication mechanism. + + Uses the authenticator's C{jid} and C{password} attribute for the + authentication credentials. If no supported SASL mechanisms are + advertized by the receiving party, a failing deferred is returned with + a L{SASLNoAcceptableMechanism} exception. + """ + + jid = self.xmlstream.authenticator.jid + password = self.xmlstream.authenticator.password + + mechanisms = get_mechanisms(self.xmlstream) + if jid.user is not None: + if 'DIGEST-MD5' in mechanisms: + self.mechanism = sasl_mechanisms.DigestMD5('xmpp', jid.host, None, + jid.user, password) + elif 'PLAIN' in mechanisms: + self.mechanism = sasl_mechanisms.Plain(None, jid.user, password) + else: + raise SASLNoAcceptableMechanism() + else: + if 'ANONYMOUS' in mechanisms: + self.mechanism = sasl_mechanisms.Anonymous() + else: + raise SASLNoAcceptableMechanism() + + + def start(self): + """ + Start SASL authentication exchange. + """ + + self.setMechanism() + self._deferred = defer.Deferred() + self.xmlstream.addObserver('/challenge', self.onChallenge) + self.xmlstream.addOnetimeObserver('/success', self.onSuccess) + self.xmlstream.addOnetimeObserver('/failure', self.onFailure) + self.sendAuth(self.mechanism.getInitialResponse()) + return self._deferred + + + def sendAuth(self, data=None): + """ + Initiate authentication protocol exchange. + + If an initial client response is given in C{data}, it will be + sent along. + + @param data: initial client response. + @type data: C{str} or L{None}. + """ + + auth = domish.Element((NS_XMPP_SASL, 'auth')) + auth['mechanism'] = self.mechanism.name + if data is not None: + auth.addContent(b64encode(data).decode('ascii') or u'=') + self.xmlstream.send(auth) + + + def sendResponse(self, data=b''): + """ + Send response to a challenge. + + @param data: client response. + @type data: L{bytes}. + """ + + response = domish.Element((NS_XMPP_SASL, 'response')) + if data: + response.addContent(b64encode(data).decode('ascii')) + self.xmlstream.send(response) + + + def onChallenge(self, element): + """ + Parse challenge and send response from the mechanism. + + @param element: the challenge protocol element. + @type element: L{domish.Element}. + """ + + try: + challenge = fromBase64(unicode(element)) + except SASLIncorrectEncodingError: + self._deferred.errback() + else: + self.sendResponse(self.mechanism.getResponse(challenge)) + + + def onSuccess(self, success): + """ + Clean up observers, reset the XML stream and send a new header. + + @param success: the success protocol element. For now unused, but + could hold additional data. + @type success: L{domish.Element} + """ + + self.xmlstream.removeObserver('/challenge', self.onChallenge) + self.xmlstream.removeObserver('/failure', self.onFailure) + self.xmlstream.reset() + self.xmlstream.sendHeader() + self._deferred.callback(xmlstream.Reset) + + + def onFailure(self, failure): + """ + Clean up observers, parse the failure and errback the deferred. + + @param failure: the failure protocol element. Holds details on + the error condition. + @type failure: L{domish.Element} + """ + + self.xmlstream.removeObserver('/challenge', self.onChallenge) + self.xmlstream.removeObserver('/success', self.onSuccess) + try: + condition = failure.firstChildElement().name + except AttributeError: + condition = None + self._deferred.errback(SASLAuthError(condition)) diff --git a/contrib/python/Twisted/py2/twisted/words/protocols/jabber/sasl_mechanisms.py b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/sasl_mechanisms.py new file mode 100644 index 0000000000..cbc7a90a5a --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/sasl_mechanisms.py @@ -0,0 +1,293 @@ +# -*- test-case-name: twisted.words.test.test_jabbersaslmechanisms -*- +# +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Protocol agnostic implementations of SASL authentication mechanisms. +""" + +from __future__ import absolute_import, division + +import binascii, random, time, os +from hashlib import md5 + +from zope.interface import Interface, Attribute, implementer + +from twisted.python.compat import iteritems, networkString + + +class ISASLMechanism(Interface): + name = Attribute("""Common name for the SASL Mechanism.""") + + def getInitialResponse(): + """ + Get the initial client response, if defined for this mechanism. + + @return: initial client response string. + @rtype: C{str}. + """ + + + def getResponse(challenge): + """ + Get the response to a server challenge. + + @param challenge: server challenge. + @type challenge: C{str}. + @return: client response. + @rtype: C{str}. + """ + + + +@implementer(ISASLMechanism) +class Anonymous(object): + """ + Implements the ANONYMOUS SASL authentication mechanism. + + This mechanism is defined in RFC 2245. + """ + name = 'ANONYMOUS' + + def getInitialResponse(self): + return None + + + +@implementer(ISASLMechanism) +class Plain(object): + """ + Implements the PLAIN SASL authentication mechanism. + + The PLAIN SASL authentication mechanism is defined in RFC 2595. + """ + name = 'PLAIN' + + def __init__(self, authzid, authcid, password): + """ + @param authzid: The authorization identity. + @type authzid: L{unicode} + + @param authcid: The authentication identity. + @type authcid: L{unicode} + + @param password: The plain-text password. + @type password: L{unicode} + """ + + self.authzid = authzid or u'' + self.authcid = authcid or u'' + self.password = password or u'' + + + def getInitialResponse(self): + return (self.authzid.encode('utf-8') + b"\x00" + + self.authcid.encode('utf-8') + b"\x00" + + self.password.encode('utf-8')) + + + +@implementer(ISASLMechanism) +class DigestMD5(object): + """ + Implements the DIGEST-MD5 SASL authentication mechanism. + + The DIGEST-MD5 SASL authentication mechanism is defined in RFC 2831. + """ + name = 'DIGEST-MD5' + + def __init__(self, serv_type, host, serv_name, username, password): + """ + @param serv_type: An indication of what kind of server authentication + is being attempted against. For example, C{u"xmpp"}. + @type serv_type: C{unicode} + + @param host: The authentication hostname. Also known as the realm. + This is used as a scope to help select the right credentials. + @type host: C{unicode} + + @param serv_name: An additional identifier for the server. + @type serv_name: C{unicode} + + @param username: The authentication username to use to respond to a + challenge. + @type username: C{unicode} + + @param username: The authentication password to use to respond to a + challenge. + @type password: C{unicode} + """ + self.username = username + self.password = password + self.defaultRealm = host + + self.digest_uri = u'%s/%s' % (serv_type, host) + if serv_name is not None: + self.digest_uri += u'/%s' % (serv_name,) + + + def getInitialResponse(self): + return None + + + def getResponse(self, challenge): + directives = self._parse(challenge) + + # Compat for implementations that do not send this along with + # a successful authentication. + if b'rspauth' in directives: + return b'' + + charset = directives[b'charset'].decode('ascii') + + try: + realm = directives[b'realm'] + except KeyError: + realm = self.defaultRealm.encode(charset) + + return self._genResponse(charset, + realm, + directives[b'nonce']) + + + def _parse(self, challenge): + """ + Parses the server challenge. + + Splits the challenge into a dictionary of directives with values. + + @return: challenge directives and their values. + @rtype: C{dict} of C{str} to C{str}. + """ + s = challenge + paramDict = {} + cur = 0 + remainingParams = True + while remainingParams: + # Parse a param. We can't just split on commas, because there can + # be some commas inside (quoted) param values, e.g.: + # qop="auth,auth-int" + + middle = s.index(b"=", cur) + name = s[cur:middle].lstrip() + middle += 1 + if s[middle:middle+1] == b'"': + middle += 1 + end = s.index(b'"', middle) + value = s[middle:end] + cur = s.find(b',', end) + 1 + if cur == 0: + remainingParams = False + else: + end = s.find(b',', middle) + if end == -1: + value = s[middle:].rstrip() + remainingParams = False + else: + value = s[middle:end].rstrip() + cur = end + 1 + paramDict[name] = value + + for param in (b'qop', b'cipher'): + if param in paramDict: + paramDict[param] = paramDict[param].split(b',') + + return paramDict + + def _unparse(self, directives): + """ + Create message string from directives. + + @param directives: dictionary of directives (names to their values). + For certain directives, extra quotes are added, as + needed. + @type directives: C{dict} of C{str} to C{str} + @return: message string. + @rtype: C{str}. + """ + + directive_list = [] + for name, value in iteritems(directives): + if name in (b'username', b'realm', b'cnonce', + b'nonce', b'digest-uri', b'authzid', b'cipher'): + directive = name + b'=' + value + else: + directive = name + b'=' + value + + directive_list.append(directive) + + return b','.join(directive_list) + + + def _calculateResponse(self, cnonce, nc, nonce, + username, password, realm, uri): + """ + Calculates response with given encoded parameters. + + @return: The I{response} field of a response to a Digest-MD5 challenge + of the given parameters. + @rtype: L{bytes} + """ + def H(s): + return md5(s).digest() + + def HEX(n): + return binascii.b2a_hex(n) + + def KD(k, s): + return H(k + b':' + s) + + a1 = (H(username + b":" + realm + b":" + password) + b":" + + nonce + b":" + + cnonce) + a2 = b"AUTHENTICATE:" + uri + + response = HEX(KD(HEX(H(a1)), + nonce + b":" + nc + b":" + cnonce + b":" + + b"auth" + b":" + HEX(H(a2)))) + return response + + + def _genResponse(self, charset, realm, nonce): + """ + Generate response-value. + + Creates a response to a challenge according to section 2.1.2.1 of + RFC 2831 using the C{charset}, C{realm} and C{nonce} directives + from the challenge. + """ + try: + username = self.username.encode(charset) + password = self.password.encode(charset) + digest_uri = self.digest_uri.encode(charset) + except UnicodeError: + # TODO - add error checking + raise + + nc = networkString('%08x' % (1,)) # TODO: support subsequent auth. + cnonce = self._gen_nonce() + qop = b'auth' + + # TODO - add support for authzid + response = self._calculateResponse(cnonce, nc, nonce, + username, password, realm, + digest_uri) + + directives = {b'username': username, + b'realm' : realm, + b'nonce' : nonce, + b'cnonce' : cnonce, + b'nc' : nc, + b'qop' : qop, + b'digest-uri': digest_uri, + b'response': response, + b'charset': charset.encode('ascii')} + + return self._unparse(directives) + + + def _gen_nonce(self): + nonceString = "%f:%f:%d" % (random.random(), time.time(), os.getpid()) + nonceBytes = networkString(nonceString) + return md5(nonceBytes).hexdigest().encode('ascii') diff --git a/contrib/python/Twisted/py2/twisted/words/protocols/jabber/xmlstream.py b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/xmlstream.py new file mode 100644 index 0000000000..20948c6d3b --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/xmlstream.py @@ -0,0 +1,1170 @@ +# -*- test-case-name: twisted.words.test.test_jabberxmlstream -*- +# +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +XMPP XML Streams + +Building blocks for setting up XML Streams, including helping classes for +doing authentication on either client or server side, and working with XML +Stanzas. + +@var STREAM_AUTHD_EVENT: Token dispatched by L{Authenticator} when the + stream has been completely initialized +@type STREAM_AUTHD_EVENT: L{str}. + +@var INIT_FAILED_EVENT: Token dispatched by L{Authenticator} when the + stream has failed to be initialized +@type INIT_FAILED_EVENT: L{str}. + +@var Reset: Token to signal that the XML stream has been reset. +@type Reset: Basic object. +""" + +from __future__ import absolute_import, division + +from binascii import hexlify +from hashlib import sha1 +from zope.interface import directlyProvides, implementer + +from twisted.internet import defer, protocol +from twisted.internet.error import ConnectionLost +from twisted.python import failure, log, randbytes +from twisted.python.compat import intern, iteritems, itervalues, unicode +from twisted.words.protocols.jabber import error, ijabber, jid +from twisted.words.xish import domish, xmlstream +from twisted.words.xish.xmlstream import STREAM_CONNECTED_EVENT +from twisted.words.xish.xmlstream import STREAM_START_EVENT +from twisted.words.xish.xmlstream import STREAM_END_EVENT +from twisted.words.xish.xmlstream import STREAM_ERROR_EVENT + +try: + from twisted.internet import ssl +except ImportError: + ssl = None +if ssl and not ssl.supported: + ssl = None + +STREAM_AUTHD_EVENT = intern("//event/stream/authd") +INIT_FAILED_EVENT = intern("//event/xmpp/initfailed") + +NS_STREAMS = 'http://etherx.jabber.org/streams' +NS_XMPP_TLS = 'urn:ietf:params:xml:ns:xmpp-tls' + +Reset = object() + +def hashPassword(sid, password): + """ + Create a SHA1-digest string of a session identifier and password. + + @param sid: The stream session identifier. + @type sid: C{unicode}. + @param password: The password to be hashed. + @type password: C{unicode}. + """ + if not isinstance(sid, unicode): + raise TypeError("The session identifier must be a unicode object") + if not isinstance(password, unicode): + raise TypeError("The password must be a unicode object") + input = u"%s%s" % (sid, password) + return sha1(input.encode('utf-8')).hexdigest() + + + +class Authenticator: + """ + Base class for business logic of initializing an XmlStream + + Subclass this object to enable an XmlStream to initialize and authenticate + to different types of stream hosts (such as clients, components, etc.). + + Rules: + 1. The Authenticator MUST dispatch a L{STREAM_AUTHD_EVENT} when the + stream has been completely initialized. + 2. The Authenticator SHOULD reset all state information when + L{associateWithStream} is called. + 3. The Authenticator SHOULD override L{streamStarted}, and start + initialization there. + + @type xmlstream: L{XmlStream} + @ivar xmlstream: The XmlStream that needs authentication + + @note: the term authenticator is historical. Authenticators perform + all steps required to prepare the stream for the exchange + of XML stanzas. + """ + + def __init__(self): + self.xmlstream = None + + + def connectionMade(self): + """ + Called by the XmlStream when the underlying socket connection is + in place. + + This allows the Authenticator to send an initial root element, if it's + connecting, or wait for an inbound root from the peer if it's accepting + the connection. + + Subclasses can use self.xmlstream.send() to send any initial data to + the peer. + """ + + + def streamStarted(self, rootElement): + """ + Called by the XmlStream when the stream has started. + + A stream is considered to have started when the start tag of the root + element has been received. + + This examines C{rootElement} to see if there is a version attribute. + If absent, C{0.0} is assumed per RFC 3920. Subsequently, the + minimum of the version from the received stream header and the + value stored in L{xmlstream} is taken and put back in L{xmlstream}. + + Extensions of this method can extract more information from the + stream header and perform checks on them, optionally sending + stream errors and closing the stream. + """ + if rootElement.hasAttribute("version"): + version = rootElement["version"].split(".") + try: + version = (int(version[0]), int(version[1])) + except (IndexError, ValueError): + version = (0, 0) + else: + version = (0, 0) + + self.xmlstream.version = min(self.xmlstream.version, version) + + + def associateWithStream(self, xmlstream): + """ + Called by the XmlStreamFactory when a connection has been made + to the requested peer, and an XmlStream object has been + instantiated. + + The default implementation just saves a handle to the new + XmlStream. + + @type xmlstream: L{XmlStream} + @param xmlstream: The XmlStream that will be passing events to this + Authenticator. + + """ + self.xmlstream = xmlstream + + + +class ConnectAuthenticator(Authenticator): + """ + Authenticator for initiating entities. + """ + + namespace = None + + def __init__(self, otherHost): + self.otherHost = otherHost + + + def connectionMade(self): + self.xmlstream.namespace = self.namespace + self.xmlstream.otherEntity = jid.internJID(self.otherHost) + self.xmlstream.sendHeader() + + + def initializeStream(self): + """ + Perform stream initialization procedures. + + An L{XmlStream} holds a list of initializer objects in its + C{initializers} attribute. This method calls these initializers in + order and dispatches the L{STREAM_AUTHD_EVENT} event when the list has + been successfully processed. Otherwise it dispatches the + C{INIT_FAILED_EVENT} event with the failure. + + Initializers may return the special L{Reset} object to halt the + initialization processing. It signals that the current initializer was + successfully processed, but that the XML Stream has been reset. An + example is the TLSInitiatingInitializer. + """ + + def remove_first(result): + self.xmlstream.initializers.pop(0) + + return result + + def do_next(result): + """ + Take the first initializer and process it. + + On success, the initializer is removed from the list and + then next initializer will be tried. + """ + + if result is Reset: + return None + + try: + init = self.xmlstream.initializers[0] + except IndexError: + self.xmlstream.dispatch(self.xmlstream, STREAM_AUTHD_EVENT) + return None + else: + d = defer.maybeDeferred(init.initialize) + d.addCallback(remove_first) + d.addCallback(do_next) + return d + + d = defer.succeed(None) + d.addCallback(do_next) + d.addErrback(self.xmlstream.dispatch, INIT_FAILED_EVENT) + + + def streamStarted(self, rootElement): + """ + Called by the XmlStream when the stream has started. + + This extends L{Authenticator.streamStarted} to extract further stream + headers from C{rootElement}, optionally wait for stream features being + received and then call C{initializeStream}. + """ + + Authenticator.streamStarted(self, rootElement) + + self.xmlstream.sid = rootElement.getAttribute("id") + + if rootElement.hasAttribute("from"): + self.xmlstream.otherEntity = jid.internJID(rootElement["from"]) + + # Setup observer for stream features, if applicable + if self.xmlstream.version >= (1, 0): + def onFeatures(element): + features = {} + for feature in element.elements(): + features[(feature.uri, feature.name)] = feature + + self.xmlstream.features = features + self.initializeStream() + + self.xmlstream.addOnetimeObserver('/features[@xmlns="%s"]' % + NS_STREAMS, + onFeatures) + else: + self.initializeStream() + + + +class ListenAuthenticator(Authenticator): + """ + Authenticator for receiving entities. + """ + + namespace = None + + def associateWithStream(self, xmlstream): + """ + Called by the XmlStreamFactory when a connection has been made. + + Extend L{Authenticator.associateWithStream} to set the L{XmlStream} + to be non-initiating. + """ + Authenticator.associateWithStream(self, xmlstream) + self.xmlstream.initiating = False + + + def streamStarted(self, rootElement): + """ + Called by the XmlStream when the stream has started. + + This extends L{Authenticator.streamStarted} to extract further + information from the stream headers from C{rootElement}. + """ + Authenticator.streamStarted(self, rootElement) + + self.xmlstream.namespace = rootElement.defaultUri + + if rootElement.hasAttribute("to"): + self.xmlstream.thisEntity = jid.internJID(rootElement["to"]) + + self.xmlstream.prefixes = {} + for prefix, uri in iteritems(rootElement.localPrefixes): + self.xmlstream.prefixes[uri] = prefix + + self.xmlstream.sid = hexlify(randbytes.secureRandom(8)).decode('ascii') + + + +class FeatureNotAdvertized(Exception): + """ + Exception indicating a stream feature was not advertized, while required by + the initiating entity. + """ + + + +@implementer(ijabber.IInitiatingInitializer) +class BaseFeatureInitiatingInitializer(object): + """ + Base class for initializers with a stream feature. + + This assumes the associated XmlStream represents the initiating entity + of the connection. + + @cvar feature: tuple of (uri, name) of the stream feature root element. + @type feature: tuple of (C{str}, C{str}) + + @ivar required: whether the stream feature is required to be advertized + by the receiving entity. + @type required: C{bool} + """ + + feature = None + + def __init__(self, xs, required=False): + self.xmlstream = xs + self.required = required + + + def initialize(self): + """ + Initiate the initialization. + + Checks if the receiving entity advertizes the stream feature. If it + does, the initialization is started. If it is not advertized, and the + C{required} instance variable is C{True}, it raises + L{FeatureNotAdvertized}. Otherwise, the initialization silently + succeeds. + """ + + if self.feature in self.xmlstream.features: + return self.start() + elif self.required: + raise FeatureNotAdvertized + else: + return None + + + def start(self): + """ + Start the actual initialization. + + May return a deferred for asynchronous initialization. + """ + + + +class TLSError(Exception): + """ + TLS base exception. + """ + + + +class TLSFailed(TLSError): + """ + Exception indicating failed TLS negotiation + """ + + + +class TLSRequired(TLSError): + """ + Exception indicating required TLS negotiation. + + This exception is raised when the receiving entity requires TLS + negotiation and the initiating does not desire to negotiate TLS. + """ + + + +class TLSNotSupported(TLSError): + """ + Exception indicating missing TLS support. + + This exception is raised when the initiating entity wants and requires to + negotiate TLS when the OpenSSL library is not available. + """ + + + +class TLSInitiatingInitializer(BaseFeatureInitiatingInitializer): + """ + TLS stream initializer for the initiating entity. + + It is strongly required to include this initializer in the list of + initializers for an XMPP stream. By default it will try to negotiate TLS. + An XMPP server may indicate that TLS is required. If TLS is not desired, + set the C{wanted} attribute to False instead of removing it from the list + of initializers, so a proper exception L{TLSRequired} can be raised. + + @ivar wanted: indicates if TLS negotiation is wanted. + @type wanted: C{bool} + """ + + feature = (NS_XMPP_TLS, 'starttls') + wanted = True + _deferred = None + _configurationForTLS = None + + def __init__(self, xs, required=True, configurationForTLS=None): + """ + @param configurationForTLS: An object which creates appropriately + configured TLS connections. This is passed to C{startTLS} on the + transport and is preferably created using + L{twisted.internet.ssl.optionsForClientTLS}. If C{None}, the + default is to verify the server certificate against the trust roots + as provided by the platform. See + L{twisted.internet._sslverify.platformTrust}. + @type configurationForTLS: L{IOpenSSLClientConnectionCreator} or + C{None} + """ + super(TLSInitiatingInitializer, self).__init__( + xs, required=required) + self._configurationForTLS = configurationForTLS + + + def onProceed(self, obj): + """ + Proceed with TLS negotiation and reset the XML stream. + """ + + self.xmlstream.removeObserver('/failure', self.onFailure) + if self._configurationForTLS: + ctx = self._configurationForTLS + else: + ctx = ssl.optionsForClientTLS(self.xmlstream.otherEntity.host) + self.xmlstream.transport.startTLS(ctx) + self.xmlstream.reset() + self.xmlstream.sendHeader() + self._deferred.callback(Reset) + + + def onFailure(self, obj): + self.xmlstream.removeObserver('/proceed', self.onProceed) + self._deferred.errback(TLSFailed()) + + + def start(self): + """ + Start TLS negotiation. + + This checks if the receiving entity requires TLS, the SSL library is + available and uses the C{required} and C{wanted} instance variables to + determine what to do in the various different cases. + + For example, if the SSL library is not available, and wanted and + required by the user, it raises an exception. However if it is not + required by both parties, initialization silently succeeds, moving + on to the next step. + """ + if self.wanted: + if ssl is None: + if self.required: + return defer.fail(TLSNotSupported()) + else: + return defer.succeed(None) + else: + pass + elif self.xmlstream.features[self.feature].required: + return defer.fail(TLSRequired()) + else: + return defer.succeed(None) + + self._deferred = defer.Deferred() + self.xmlstream.addOnetimeObserver("/proceed", self.onProceed) + self.xmlstream.addOnetimeObserver("/failure", self.onFailure) + self.xmlstream.send(domish.Element((NS_XMPP_TLS, "starttls"))) + return self._deferred + + + +class XmlStream(xmlstream.XmlStream): + """ + XMPP XML Stream protocol handler. + + @ivar version: XML stream version as a tuple (major, minor). Initially, + this is set to the minimally supported version. Upon + receiving the stream header of the peer, it is set to the + minimum of that value and the version on the received + header. + @type version: (C{int}, C{int}) + @ivar namespace: default namespace URI for stream + @type namespace: C{unicode} + @ivar thisEntity: JID of this entity + @type thisEntity: L{JID} + @ivar otherEntity: JID of the peer entity + @type otherEntity: L{JID} + @ivar sid: session identifier + @type sid: C{unicode} + @ivar initiating: True if this is the initiating stream + @type initiating: C{bool} + @ivar features: map of (uri, name) to stream features element received from + the receiving entity. + @type features: C{dict} of (C{unicode}, C{unicode}) to L{domish.Element}. + @ivar prefixes: map of URI to prefixes that are to appear on stream + header. + @type prefixes: C{dict} of C{unicode} to C{unicode} + @ivar initializers: list of stream initializer objects + @type initializers: C{list} of objects that provide L{IInitializer} + @ivar authenticator: associated authenticator that uses C{initializers} to + initialize the XML stream. + """ + + version = (1, 0) + namespace = 'invalid' + thisEntity = None + otherEntity = None + sid = None + initiating = True + + _headerSent = False # True if the stream header has been sent + + def __init__(self, authenticator): + xmlstream.XmlStream.__init__(self) + + self.prefixes = {NS_STREAMS: 'stream'} + self.authenticator = authenticator + self.initializers = [] + self.features = {} + + # Reset the authenticator + authenticator.associateWithStream(self) + + + def _callLater(self, *args, **kwargs): + from twisted.internet import reactor + return reactor.callLater(*args, **kwargs) + + + def reset(self): + """ + Reset XML Stream. + + Resets the XML Parser for incoming data. This is to be used after + successfully negotiating a new layer, e.g. TLS and SASL. Note that + registered event observers will continue to be in place. + """ + self._headerSent = False + self._initializeStream() + + + def onStreamError(self, errelem): + """ + Called when a stream:error element has been received. + + Dispatches a L{STREAM_ERROR_EVENT} event with the error element to + allow for cleanup actions and drops the connection. + + @param errelem: The received error element. + @type errelem: L{domish.Element} + """ + self.dispatch(failure.Failure(error.exceptionFromStreamError(errelem)), + STREAM_ERROR_EVENT) + self.transport.loseConnection() + + + def sendHeader(self): + """ + Send stream header. + """ + # set up optional extra namespaces + localPrefixes = {} + for uri, prefix in iteritems(self.prefixes): + if uri != NS_STREAMS: + localPrefixes[prefix] = uri + + rootElement = domish.Element((NS_STREAMS, 'stream'), self.namespace, + localPrefixes=localPrefixes) + + if self.otherEntity: + rootElement['to'] = self.otherEntity.userhost() + + if self.thisEntity: + rootElement['from'] = self.thisEntity.userhost() + + if not self.initiating and self.sid: + rootElement['id'] = self.sid + + if self.version >= (1, 0): + rootElement['version'] = "%d.%d" % self.version + + self.send(rootElement.toXml(prefixes=self.prefixes, closeElement=0)) + self._headerSent = True + + + def sendFooter(self): + """ + Send stream footer. + """ + self.send('</stream:stream>') + + + def sendStreamError(self, streamError): + """ + Send stream level error. + + If we are the receiving entity, and haven't sent the header yet, + we sent one first. + + After sending the stream error, the stream is closed and the transport + connection dropped. + + @param streamError: stream error instance + @type streamError: L{error.StreamError} + """ + if not self._headerSent and not self.initiating: + self.sendHeader() + + if self._headerSent: + self.send(streamError.getElement()) + self.sendFooter() + + self.transport.loseConnection() + + + def send(self, obj): + """ + Send data over the stream. + + This overrides L{xmlstream.XmlStream.send} to use the default namespace + of the stream header when serializing L{domish.IElement}s. It is + assumed that if you pass an object that provides L{domish.IElement}, + it represents a direct child of the stream's root element. + """ + if domish.IElement.providedBy(obj): + obj = obj.toXml(prefixes=self.prefixes, + defaultUri=self.namespace, + prefixesInScope=list(self.prefixes.values())) + + xmlstream.XmlStream.send(self, obj) + + + def connectionMade(self): + """ + Called when a connection is made. + + Notifies the authenticator when a connection has been made. + """ + xmlstream.XmlStream.connectionMade(self) + self.authenticator.connectionMade() + + + def onDocumentStart(self, rootElement): + """ + Called when the stream header has been received. + + Extracts the header's C{id} and C{version} attributes from the root + element. The C{id} attribute is stored in our C{sid} attribute and the + C{version} attribute is parsed and the minimum of the version we sent + and the parsed C{version} attribute is stored as a tuple (major, minor) + in this class' C{version} attribute. If no C{version} attribute was + present, we assume version 0.0. + + If appropriate (we are the initiating stream and the minimum of our and + the other party's version is at least 1.0), a one-time observer is + registered for getting the stream features. The registered function is + C{onFeatures}. + + Ultimately, the authenticator's C{streamStarted} method will be called. + + @param rootElement: The root element. + @type rootElement: L{domish.Element} + """ + xmlstream.XmlStream.onDocumentStart(self, rootElement) + + # Setup observer for stream errors + self.addOnetimeObserver("/error[@xmlns='%s']" % NS_STREAMS, + self.onStreamError) + + self.authenticator.streamStarted(rootElement) + + + +class XmlStreamFactory(xmlstream.XmlStreamFactory): + """ + Factory for Jabber XmlStream objects as a reconnecting client. + + Note that this differs from L{xmlstream.XmlStreamFactory} in that + it generates Jabber specific L{XmlStream} instances that have + authenticators. + """ + + protocol = XmlStream + + def __init__(self, authenticator): + xmlstream.XmlStreamFactory.__init__(self, authenticator) + self.authenticator = authenticator + + + +class XmlStreamServerFactory(xmlstream.BootstrapMixin, + protocol.ServerFactory): + """ + Factory for Jabber XmlStream objects as a server. + + @since: 8.2. + @ivar authenticatorFactory: Factory callable that takes no arguments, to + create a fresh authenticator to be associated + with the XmlStream. + """ + + protocol = XmlStream + + def __init__(self, authenticatorFactory): + xmlstream.BootstrapMixin.__init__(self) + self.authenticatorFactory = authenticatorFactory + + + def buildProtocol(self, addr): + """ + Create an instance of XmlStream. + + A new authenticator instance will be created and passed to the new + XmlStream. Registered bootstrap event observers are installed as well. + """ + authenticator = self.authenticatorFactory() + xs = self.protocol(authenticator) + xs.factory = self + self.installBootstraps(xs) + return xs + + + +class TimeoutError(Exception): + """ + Exception raised when no IQ response has been received before the + configured timeout. + """ + + + +def upgradeWithIQResponseTracker(xs): + """ + Enhances an XmlStream for iq response tracking. + + This makes an L{XmlStream} object provide L{IIQResponseTracker}. When a + response is an error iq stanza, the deferred has its errback invoked with a + failure that holds a L{StanzaError<error.StanzaError>} that is + easier to examine. + """ + def callback(iq): + """ + Handle iq response by firing associated deferred. + """ + if getattr(iq, 'handled', False): + return + + try: + d = xs.iqDeferreds[iq["id"]] + except KeyError: + pass + else: + del xs.iqDeferreds[iq["id"]] + iq.handled = True + if iq['type'] == 'error': + d.errback(error.exceptionFromStanza(iq)) + else: + d.callback(iq) + + + def disconnected(_): + """ + Make sure deferreds do not linger on after disconnect. + + This errbacks all deferreds of iq's for which no response has been + received with a L{ConnectionLost} failure. Otherwise, the deferreds + will never be fired. + """ + iqDeferreds = xs.iqDeferreds + xs.iqDeferreds = {} + for d in itervalues(iqDeferreds): + d.errback(ConnectionLost()) + + xs.iqDeferreds = {} + xs.iqDefaultTimeout = getattr(xs, 'iqDefaultTimeout', None) + xs.addObserver(xmlstream.STREAM_END_EVENT, disconnected) + xs.addObserver('/iq[@type="result"]', callback) + xs.addObserver('/iq[@type="error"]', callback) + directlyProvides(xs, ijabber.IIQResponseTracker) + + + +class IQ(domish.Element): + """ + Wrapper for an iq stanza. + + Iq stanzas are used for communications with a request-response behaviour. + Each iq request is associated with an XML stream and has its own unique id + to be able to track the response. + + @ivar timeout: if set, a timeout period after which the deferred returned + by C{send} will have its errback called with a + L{TimeoutError} failure. + @type timeout: C{float} + """ + + timeout = None + + def __init__(self, xmlstream, stanzaType="set"): + """ + @type xmlstream: L{xmlstream.XmlStream} + @param xmlstream: XmlStream to use for transmission of this IQ + + @type stanzaType: C{str} + @param stanzaType: IQ type identifier ('get' or 'set') + """ + domish.Element.__init__(self, (None, "iq")) + self.addUniqueId() + self["type"] = stanzaType + self._xmlstream = xmlstream + + + def send(self, to=None): + """ + Send out this iq. + + Returns a deferred that is fired when an iq response with the same id + is received. Result responses will be passed to the deferred callback. + Error responses will be transformed into a + L{StanzaError<error.StanzaError>} and result in the errback of the + deferred being invoked. + + @rtype: L{defer.Deferred} + """ + if to is not None: + self["to"] = to + + if not ijabber.IIQResponseTracker.providedBy(self._xmlstream): + upgradeWithIQResponseTracker(self._xmlstream) + + d = defer.Deferred() + self._xmlstream.iqDeferreds[self['id']] = d + + timeout = self.timeout or self._xmlstream.iqDefaultTimeout + if timeout is not None: + def onTimeout(): + del self._xmlstream.iqDeferreds[self['id']] + d.errback(TimeoutError("IQ timed out")) + + call = self._xmlstream._callLater(timeout, onTimeout) + + def cancelTimeout(result): + if call.active(): + call.cancel() + + return result + + d.addBoth(cancelTimeout) + + self._xmlstream.send(self) + return d + + + +def toResponse(stanza, stanzaType=None): + """ + Create a response stanza from another stanza. + + This takes the addressing and id attributes from a stanza to create a (new, + empty) response stanza. The addressing attributes are swapped and the id + copied. Optionally, the stanza type of the response can be specified. + + @param stanza: the original stanza + @type stanza: L{domish.Element} + @param stanzaType: optional response stanza type + @type stanzaType: C{str} + @return: the response stanza. + @rtype: L{domish.Element} + """ + + toAddr = stanza.getAttribute('from') + fromAddr = stanza.getAttribute('to') + stanzaID = stanza.getAttribute('id') + + response = domish.Element((None, stanza.name)) + if toAddr: + response['to'] = toAddr + if fromAddr: + response['from'] = fromAddr + if stanzaID: + response['id'] = stanzaID + if stanzaType: + response['type'] = stanzaType + + return response + + + +@implementer(ijabber.IXMPPHandler) +class XMPPHandler(object): + """ + XMPP protocol handler. + + Classes derived from this class implement (part of) one or more XMPP + extension protocols, and are referred to as a subprotocol implementation. + """ + + def __init__(self): + self.parent = None + self.xmlstream = None + + + def setHandlerParent(self, parent): + self.parent = parent + self.parent.addHandler(self) + + + def disownHandlerParent(self, parent): + self.parent.removeHandler(self) + self.parent = None + + + def makeConnection(self, xs): + self.xmlstream = xs + self.connectionMade() + + + def connectionMade(self): + """ + Called after a connection has been established. + + Can be overridden to perform work before stream initialization. + """ + + + def connectionInitialized(self): + """ + The XML stream has been initialized. + + Can be overridden to perform work after stream initialization, e.g. to + set up observers and start exchanging XML stanzas. + """ + + + def connectionLost(self, reason): + """ + The XML stream has been closed. + + This method can be extended to inspect the C{reason} argument and + act on it. + """ + self.xmlstream = None + + + def send(self, obj): + """ + Send data over the managed XML stream. + + @note: The stream manager maintains a queue for data sent using this + method when there is no current initialized XML stream. This + data is then sent as soon as a new stream has been established + and initialized. Subsequently, L{connectionInitialized} will be + called again. If this queueing is not desired, use C{send} on + C{self.xmlstream}. + + @param obj: data to be sent over the XML stream. This is usually an + object providing L{domish.IElement}, or serialized XML. See + L{xmlstream.XmlStream} for details. + """ + self.parent.send(obj) + + + +@implementer(ijabber.IXMPPHandlerCollection) +class XMPPHandlerCollection(object): + """ + Collection of XMPP subprotocol handlers. + + This allows for grouping of subprotocol handlers, but is not an + L{XMPPHandler} itself, so this is not recursive. + + @ivar handlers: List of protocol handlers. + @type handlers: C{list} of objects providing + L{IXMPPHandler} + """ + + def __init__(self): + self.handlers = [] + + + def __iter__(self): + """ + Act as a container for handlers. + """ + return iter(self.handlers) + + + def addHandler(self, handler): + """ + Add protocol handler. + + Protocol handlers are expected to provide L{ijabber.IXMPPHandler}. + """ + self.handlers.append(handler) + + + def removeHandler(self, handler): + """ + Remove protocol handler. + """ + self.handlers.remove(handler) + + + +class StreamManager(XMPPHandlerCollection): + """ + Business logic representing a managed XMPP connection. + + This maintains a single XMPP connection and provides facilities for packet + routing and transmission. Business logic modules are objects providing + L{ijabber.IXMPPHandler} (like subclasses of L{XMPPHandler}), and added + using L{addHandler}. + + @ivar xmlstream: currently managed XML stream + @type xmlstream: L{XmlStream} + @ivar logTraffic: if true, log all traffic. + @type logTraffic: C{bool} + @ivar _initialized: Whether the stream represented by L{xmlstream} has + been initialized. This is used when caching outgoing + stanzas. + @type _initialized: C{bool} + @ivar _packetQueue: internal buffer of unsent data. See L{send} for details. + @type _packetQueue: C{list} + """ + + logTraffic = False + + def __init__(self, factory): + XMPPHandlerCollection.__init__(self) + self.xmlstream = None + self._packetQueue = [] + self._initialized = False + + factory.addBootstrap(STREAM_CONNECTED_EVENT, self._connected) + factory.addBootstrap(STREAM_AUTHD_EVENT, self._authd) + factory.addBootstrap(INIT_FAILED_EVENT, self.initializationFailed) + factory.addBootstrap(STREAM_END_EVENT, self._disconnected) + self.factory = factory + + + def addHandler(self, handler): + """ + Add protocol handler. + + When an XML stream has already been established, the handler's + C{connectionInitialized} will be called to get it up to speed. + """ + XMPPHandlerCollection.addHandler(self, handler) + + # get protocol handler up to speed when a connection has already + # been established + if self.xmlstream and self._initialized: + handler.makeConnection(self.xmlstream) + handler.connectionInitialized() + + + def _connected(self, xs): + """ + Called when the transport connection has been established. + + Here we optionally set up traffic logging (depending on L{logTraffic}) + and call each handler's C{makeConnection} method with the L{XmlStream} + instance. + """ + def logDataIn(buf): + log.msg("RECV: %r" % buf) + + def logDataOut(buf): + log.msg("SEND: %r" % buf) + + if self.logTraffic: + xs.rawDataInFn = logDataIn + xs.rawDataOutFn = logDataOut + + self.xmlstream = xs + + for e in self: + e.makeConnection(xs) + + + def _authd(self, xs): + """ + Called when the stream has been initialized. + + Send out cached stanzas and call each handler's + C{connectionInitialized} method. + """ + # Flush all pending packets + for p in self._packetQueue: + xs.send(p) + self._packetQueue = [] + self._initialized = True + + # Notify all child services which implement + # the IService interface + for e in self: + e.connectionInitialized() + + + def initializationFailed(self, reason): + """ + Called when stream initialization has failed. + + Stream initialization has halted, with the reason indicated by + C{reason}. It may be retried by calling the authenticator's + C{initializeStream}. See the respective authenticators for details. + + @param reason: A failure instance indicating why stream initialization + failed. + @type reason: L{failure.Failure} + """ + + + def _disconnected(self, reason): + """ + Called when the stream has been closed. + + From this point on, the manager doesn't interact with the + L{XmlStream} anymore and notifies each handler that the connection + was lost by calling its C{connectionLost} method. + """ + self.xmlstream = None + self._initialized = False + + # Notify all child services which implement + # the IService interface + for e in self: + e.connectionLost(reason) + + + def send(self, obj): + """ + Send data over the XML stream. + + When there is no established XML stream, the data is queued and sent + out when a new XML stream has been established and initialized. + + @param obj: data to be sent over the XML stream. See + L{xmlstream.XmlStream.send} for details. + """ + if self._initialized: + self.xmlstream.send(obj) + else: + self._packetQueue.append(obj) + + + +__all__ = ['Authenticator', 'BaseFeatureInitiatingInitializer', + 'ConnectAuthenticator', 'FeatureNotAdvertized', + 'INIT_FAILED_EVENT', 'IQ', 'ListenAuthenticator', 'NS_STREAMS', + 'NS_XMPP_TLS', 'Reset', 'STREAM_AUTHD_EVENT', + 'STREAM_CONNECTED_EVENT', 'STREAM_END_EVENT', 'STREAM_ERROR_EVENT', + 'STREAM_START_EVENT', 'StreamManager', 'TLSError', 'TLSFailed', + 'TLSInitiatingInitializer', 'TLSNotSupported', 'TLSRequired', + 'TimeoutError', 'XMPPHandler', 'XMPPHandlerCollection', 'XmlStream', + 'XmlStreamFactory', 'XmlStreamServerFactory', 'hashPassword', + 'toResponse', 'upgradeWithIQResponseTracker'] diff --git a/contrib/python/Twisted/py2/twisted/words/protocols/jabber/xmpp_stringprep.py b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/xmpp_stringprep.py new file mode 100644 index 0000000000..1723856a09 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/words/protocols/jabber/xmpp_stringprep.py @@ -0,0 +1,244 @@ +# -*- test-case-name: twisted.words.test.test_jabberxmppstringprep -*- +# +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +from encodings import idna +from itertools import chain +import stringprep + +# We require Unicode version 3.2. +from unicodedata import ucd_3_2_0 as unicodedata + +from twisted.python.compat import unichr +from twisted.python.deprecate import deprecatedModuleAttribute +from incremental import Version + +from zope.interface import Interface, implementer + + +crippled = False +deprecatedModuleAttribute( + Version("Twisted", 13, 1, 0), + "crippled is always False", + __name__, + "crippled") + + + +class ILookupTable(Interface): + """ + Interface for character lookup classes. + """ + + def lookup(c): + """ + Return whether character is in this table. + """ + + + +class IMappingTable(Interface): + """ + Interface for character mapping classes. + """ + + def map(c): + """ + Return mapping for character. + """ + + + +@implementer(ILookupTable) +class LookupTableFromFunction: + + def __init__(self, in_table_function): + self.lookup = in_table_function + + + +@implementer(ILookupTable) +class LookupTable: + + def __init__(self, table): + self._table = table + + def lookup(self, c): + return c in self._table + + + +@implementer(IMappingTable) +class MappingTableFromFunction: + + def __init__(self, map_table_function): + self.map = map_table_function + + + +@implementer(IMappingTable) +class EmptyMappingTable: + + def __init__(self, in_table_function): + self._in_table_function = in_table_function + + def map(self, c): + if self._in_table_function(c): + return None + else: + return c + + + +class Profile: + def __init__(self, mappings=[], normalize=True, prohibiteds=[], + check_unassigneds=True, check_bidi=True): + self.mappings = mappings + self.normalize = normalize + self.prohibiteds = prohibiteds + self.do_check_unassigneds = check_unassigneds + self.do_check_bidi = check_bidi + + def prepare(self, string): + result = self.map(string) + if self.normalize: + result = unicodedata.normalize("NFKC", result) + self.check_prohibiteds(result) + if self.do_check_unassigneds: + self.check_unassigneds(result) + if self.do_check_bidi: + self.check_bidirectionals(result) + return result + + def map(self, string): + result = [] + + for c in string: + result_c = c + + for mapping in self.mappings: + result_c = mapping.map(c) + if result_c != c: + break + + if result_c is not None: + result.append(result_c) + + return u"".join(result) + + def check_prohibiteds(self, string): + for c in string: + for table in self.prohibiteds: + if table.lookup(c): + raise UnicodeError("Invalid character %s" % repr(c)) + + def check_unassigneds(self, string): + for c in string: + if stringprep.in_table_a1(c): + raise UnicodeError("Unassigned code point %s" % repr(c)) + + def check_bidirectionals(self, string): + found_LCat = False + found_RandALCat = False + + for c in string: + if stringprep.in_table_d1(c): + found_RandALCat = True + if stringprep.in_table_d2(c): + found_LCat = True + + if found_LCat and found_RandALCat: + raise UnicodeError("Violation of BIDI Requirement 2") + + if found_RandALCat and not (stringprep.in_table_d1(string[0]) and + stringprep.in_table_d1(string[-1])): + raise UnicodeError("Violation of BIDI Requirement 3") + + +class NamePrep: + """ Implements preparation of internationalized domain names. + + This class implements preparing internationalized domain names using the + rules defined in RFC 3491, section 4 (Conversion operations). + + We do not perform step 4 since we deal with unicode representations of + domain names and do not convert from or to ASCII representations using + punycode encoding. When such a conversion is needed, the C{idna} standard + library provides the C{ToUnicode()} and C{ToASCII()} functions. Note that + C{idna} itself assumes UseSTD3ASCIIRules to be false. + + The following steps are performed by C{prepare()}: + + - Split the domain name in labels at the dots (RFC 3490, 3.1) + - Apply nameprep proper on each label (RFC 3491) + - Enforce the restrictions on ASCII characters in host names by + assuming STD3ASCIIRules to be true. (STD 3) + - Rejoin the labels using the label separator U+002E (full stop). + + """ + + # Prohibited characters. + prohibiteds = [unichr(n) for n in chain(range(0x00, 0x2c + 1), + range(0x2e, 0x2f + 1), + range(0x3a, 0x40 + 1), + range(0x5b, 0x60 + 1), + range(0x7b, 0x7f + 1))] + + def prepare(self, string): + result = [] + + labels = idna.dots.split(string) + + if labels and len(labels[-1]) == 0: + trailing_dot = u'.' + del labels[-1] + else: + trailing_dot = u'' + + for label in labels: + result.append(self.nameprep(label)) + + return u".".join(result) + trailing_dot + + def check_prohibiteds(self, string): + for c in string: + if c in self.prohibiteds: + raise UnicodeError("Invalid character %s" % repr(c)) + + def nameprep(self, label): + label = idna.nameprep(label) + self.check_prohibiteds(label) + if label[0] == u'-': + raise UnicodeError("Invalid leading hyphen-minus") + if label[-1] == u'-': + raise UnicodeError("Invalid trailing hyphen-minus") + return label + + +C_11 = LookupTableFromFunction(stringprep.in_table_c11) +C_12 = LookupTableFromFunction(stringprep.in_table_c12) +C_21 = LookupTableFromFunction(stringprep.in_table_c21) +C_22 = LookupTableFromFunction(stringprep.in_table_c22) +C_3 = LookupTableFromFunction(stringprep.in_table_c3) +C_4 = LookupTableFromFunction(stringprep.in_table_c4) +C_5 = LookupTableFromFunction(stringprep.in_table_c5) +C_6 = LookupTableFromFunction(stringprep.in_table_c6) +C_7 = LookupTableFromFunction(stringprep.in_table_c7) +C_8 = LookupTableFromFunction(stringprep.in_table_c8) +C_9 = LookupTableFromFunction(stringprep.in_table_c9) + +B_1 = EmptyMappingTable(stringprep.in_table_b1) +B_2 = MappingTableFromFunction(stringprep.map_table_b2) + +nodeprep = Profile(mappings=[B_1, B_2], + prohibiteds=[C_11, C_12, C_21, C_22, + C_3, C_4, C_5, C_6, C_7, C_8, C_9, + LookupTable([u'"', u'&', u"'", u'/', + u':', u'<', u'>', u'@'])]) + +resourceprep = Profile(mappings=[B_1,], + prohibiteds=[C_12, C_21, C_22, + C_3, C_4, C_5, C_6, C_7, C_8, C_9]) + +nameprep = NamePrep() |