aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/Twisted/py2/twisted/words/protocols
diff options
context:
space:
mode:
authorshmel1k <shmel1k@ydb.tech>2023-11-26 18:16:14 +0300
committershmel1k <shmel1k@ydb.tech>2023-11-26 18:43:30 +0300
commitb8cf9e88f4c5c64d9406af533d8948deb050d695 (patch)
tree218eb61fb3c3b96ec08b4d8cdfef383104a87d63 /contrib/python/Twisted/py2/twisted/words/protocols
parent523f645a83a0ec97a0332dbc3863bb354c92a328 (diff)
downloadydb-b8cf9e88f4c5c64d9406af533d8948deb050d695.tar.gz
add kikimr_configure
Diffstat (limited to 'contrib/python/Twisted/py2/twisted/words/protocols')
-rw-r--r--contrib/python/Twisted/py2/twisted/words/protocols/__init__.py6
-rw-r--r--contrib/python/Twisted/py2/twisted/words/protocols/irc.py4074
-rw-r--r--contrib/python/Twisted/py2/twisted/words/protocols/jabber/__init__.py8
-rw-r--r--contrib/python/Twisted/py2/twisted/words/protocols/jabber/client.py408
-rw-r--r--contrib/python/Twisted/py2/twisted/words/protocols/jabber/component.py475
-rw-r--r--contrib/python/Twisted/py2/twisted/words/protocols/jabber/error.py331
-rw-r--r--contrib/python/Twisted/py2/twisted/words/protocols/jabber/ijabber.py201
-rw-r--r--contrib/python/Twisted/py2/twisted/words/protocols/jabber/jid.py253
-rw-r--r--contrib/python/Twisted/py2/twisted/words/protocols/jabber/jstrports.py33
-rw-r--r--contrib/python/Twisted/py2/twisted/words/protocols/jabber/sasl.py233
-rw-r--r--contrib/python/Twisted/py2/twisted/words/protocols/jabber/sasl_mechanisms.py293
-rw-r--r--contrib/python/Twisted/py2/twisted/words/protocols/jabber/xmlstream.py1170
-rw-r--r--contrib/python/Twisted/py2/twisted/words/protocols/jabber/xmpp_stringprep.py244
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()