RAHHH
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# -*- test-case-name: twisted.words.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Twisted Words: Client and server implementations for IRC, XMPP, and other chat
|
||||
services.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
41
backend/lib/python3.12/site-packages/twisted/words/ewords.py
Normal file
41
backend/lib/python3.12/site-packages/twisted/words/ewords.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# -*- test-case-name: twisted.words.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""Exception definitions for Words
|
||||
"""
|
||||
|
||||
|
||||
class WordsError(Exception):
|
||||
def __str__(self) -> str:
|
||||
return self.__class__.__name__ + ": " + Exception.__str__(self)
|
||||
|
||||
|
||||
class NoSuchUser(WordsError):
|
||||
pass
|
||||
|
||||
|
||||
class DuplicateUser(WordsError):
|
||||
pass
|
||||
|
||||
|
||||
class NoSuchGroup(WordsError):
|
||||
pass
|
||||
|
||||
|
||||
class DuplicateGroup(WordsError):
|
||||
pass
|
||||
|
||||
|
||||
class AlreadyLoggedIn(WordsError):
|
||||
pass
|
||||
|
||||
|
||||
__all__ = [
|
||||
"WordsError",
|
||||
"NoSuchUser",
|
||||
"DuplicateUser",
|
||||
"NoSuchGroup",
|
||||
"DuplicateGroup",
|
||||
"AlreadyLoggedIn",
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
"""
|
||||
Instance Messenger, Pan-protocol chat client.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,69 @@
|
||||
# -*- Python -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
#
|
||||
|
||||
|
||||
class AccountManager:
|
||||
"""I am responsible for managing a user's accounts.
|
||||
|
||||
That is, remembering what accounts are available, their settings,
|
||||
adding and removal of accounts, etc.
|
||||
|
||||
@ivar accounts: A collection of available accounts.
|
||||
@type accounts: mapping of strings to L{Account<interfaces.IAccount>}s.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.accounts = {}
|
||||
|
||||
def getSnapShot(self):
|
||||
"""A snapshot of all the accounts and their status.
|
||||
|
||||
@returns: A list of tuples, each of the form
|
||||
(string:accountName, boolean:isOnline,
|
||||
boolean:autoLogin, string:gatewayType)
|
||||
"""
|
||||
data = []
|
||||
for account in self.accounts.values():
|
||||
data.append(
|
||||
(
|
||||
account.accountName,
|
||||
account.isOnline(),
|
||||
account.autoLogin,
|
||||
account.gatewayType,
|
||||
)
|
||||
)
|
||||
return data
|
||||
|
||||
def isEmpty(self):
|
||||
return len(self.accounts) == 0
|
||||
|
||||
def getConnectionInfo(self):
|
||||
connectioninfo = []
|
||||
for account in self.accounts.values():
|
||||
connectioninfo.append(account.isOnline())
|
||||
return connectioninfo
|
||||
|
||||
def addAccount(self, account):
|
||||
self.accounts[account.accountName] = account
|
||||
|
||||
def delAccount(self, accountName):
|
||||
del self.accounts[accountName]
|
||||
|
||||
def connect(self, accountName, chatui):
|
||||
"""
|
||||
@returntype: Deferred L{interfaces.IClient}
|
||||
"""
|
||||
return self.accounts[accountName].logOn(chatui)
|
||||
|
||||
def disconnect(self, accountName):
|
||||
pass
|
||||
# self.accounts[accountName].logOff() - not yet implemented
|
||||
|
||||
def quit(self):
|
||||
pass
|
||||
# for account in self.accounts.values():
|
||||
# account.logOff() - not yet implemented
|
||||
@@ -0,0 +1,486 @@
|
||||
# -*- test-case-name: twisted.words.test.test_basechat -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Base classes for Instance Messenger clients.
|
||||
"""
|
||||
|
||||
from twisted.words.im.locals import AWAY, OFFLINE, ONLINE
|
||||
|
||||
|
||||
class ContactsList:
|
||||
"""
|
||||
A GUI object that displays a contacts list.
|
||||
|
||||
@ivar chatui: The GUI chat client associated with this contacts list.
|
||||
@type chatui: L{ChatUI}
|
||||
|
||||
@ivar contacts: The contacts.
|
||||
@type contacts: C{dict} mapping C{str} to a L{IPerson<interfaces.IPerson>}
|
||||
provider
|
||||
|
||||
@ivar onlineContacts: The contacts who are currently online (have a status
|
||||
that is not C{OFFLINE}).
|
||||
@type onlineContacts: C{dict} mapping C{str} to a
|
||||
L{IPerson<interfaces.IPerson>} provider
|
||||
|
||||
@ivar clients: The signed-on clients.
|
||||
@type clients: C{list} of L{IClient<interfaces.IClient>} providers
|
||||
"""
|
||||
|
||||
def __init__(self, chatui):
|
||||
"""
|
||||
@param chatui: The GUI chat client associated with this contacts list.
|
||||
@type chatui: L{ChatUI}
|
||||
"""
|
||||
self.chatui = chatui
|
||||
self.contacts = {}
|
||||
self.onlineContacts = {}
|
||||
self.clients = []
|
||||
|
||||
def setContactStatus(self, person):
|
||||
"""
|
||||
Inform the user that a person's status has changed.
|
||||
|
||||
@param person: The person whose status has changed.
|
||||
@type person: L{IPerson<interfaces.IPerson>} provider
|
||||
"""
|
||||
if person.name not in self.contacts:
|
||||
self.contacts[person.name] = person
|
||||
if person.name not in self.onlineContacts and (
|
||||
person.status == ONLINE or person.status == AWAY
|
||||
):
|
||||
self.onlineContacts[person.name] = person
|
||||
if person.name in self.onlineContacts and person.status == OFFLINE:
|
||||
del self.onlineContacts[person.name]
|
||||
|
||||
def registerAccountClient(self, client):
|
||||
"""
|
||||
Notify the user that an account client has been signed on to.
|
||||
|
||||
@param client: The client being added to your list of account clients.
|
||||
@type client: L{IClient<interfaces.IClient>} provider
|
||||
"""
|
||||
if client not in self.clients:
|
||||
self.clients.append(client)
|
||||
|
||||
def unregisterAccountClient(self, client):
|
||||
"""
|
||||
Notify the user that an account client has been signed off or
|
||||
disconnected from.
|
||||
|
||||
@param client: The client being removed from the list of account
|
||||
clients.
|
||||
@type client: L{IClient<interfaces.IClient>} provider
|
||||
"""
|
||||
if client in self.clients:
|
||||
self.clients.remove(client)
|
||||
|
||||
def contactChangedNick(self, person, newnick):
|
||||
"""
|
||||
Update your contact information to reflect a change to a contact's
|
||||
nickname.
|
||||
|
||||
@param person: The person in your contacts list whose nickname is
|
||||
changing.
|
||||
@type person: L{IPerson<interfaces.IPerson>} provider
|
||||
|
||||
@param newnick: The new nickname for this person.
|
||||
@type newnick: C{str}
|
||||
"""
|
||||
oldname = person.name
|
||||
if oldname in self.contacts:
|
||||
del self.contacts[oldname]
|
||||
person.name = newnick
|
||||
self.contacts[newnick] = person
|
||||
if oldname in self.onlineContacts:
|
||||
del self.onlineContacts[oldname]
|
||||
self.onlineContacts[newnick] = person
|
||||
|
||||
|
||||
class Conversation:
|
||||
"""
|
||||
A GUI window of a conversation with a specific person.
|
||||
|
||||
@ivar person: The person who you're having this conversation with.
|
||||
@type person: L{IPerson<interfaces.IPerson>} provider
|
||||
|
||||
@ivar chatui: The GUI chat client associated with this conversation.
|
||||
@type chatui: L{ChatUI}
|
||||
"""
|
||||
|
||||
def __init__(self, person, chatui):
|
||||
"""
|
||||
@param person: The person who you're having this conversation with.
|
||||
@type person: L{IPerson<interfaces.IPerson>} provider
|
||||
|
||||
@param chatui: The GUI chat client associated with this conversation.
|
||||
@type chatui: L{ChatUI}
|
||||
"""
|
||||
self.chatui = chatui
|
||||
self.person = person
|
||||
|
||||
def show(self):
|
||||
"""
|
||||
Display the ConversationWindow.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def hide(self):
|
||||
"""
|
||||
Hide the ConversationWindow.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def sendText(self, text):
|
||||
"""
|
||||
Send text to the person with whom the user is conversing.
|
||||
|
||||
@param text: The text to be sent.
|
||||
@type text: C{str}
|
||||
"""
|
||||
self.person.sendMessage(text, None)
|
||||
|
||||
def showMessage(self, text, metadata=None):
|
||||
"""
|
||||
Display a message sent from the person with whom the user is conversing.
|
||||
|
||||
@param text: The sent message.
|
||||
@type text: C{str}
|
||||
|
||||
@param metadata: Metadata associated with this message.
|
||||
@type metadata: C{dict}
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def contactChangedNick(self, person, newnick):
|
||||
"""
|
||||
Change a person's name.
|
||||
|
||||
@param person: The person whose nickname is changing.
|
||||
@type person: L{IPerson<interfaces.IPerson>} provider
|
||||
|
||||
@param newnick: The new nickname for this person.
|
||||
@type newnick: C{str}
|
||||
"""
|
||||
self.person.name = newnick
|
||||
|
||||
|
||||
class GroupConversation:
|
||||
"""
|
||||
A GUI window of a conversation with a group of people.
|
||||
|
||||
@ivar chatui: The GUI chat client associated with this conversation.
|
||||
@type chatui: L{ChatUI}
|
||||
|
||||
@ivar group: The group of people that are having this conversation.
|
||||
@type group: L{IGroup<interfaces.IGroup>} provider
|
||||
|
||||
@ivar members: The names of the people in this conversation.
|
||||
@type members: C{list} of C{str}
|
||||
"""
|
||||
|
||||
def __init__(self, group, chatui):
|
||||
"""
|
||||
@param chatui: The GUI chat client associated with this conversation.
|
||||
@type chatui: L{ChatUI}
|
||||
|
||||
@param group: The group of people that are having this conversation.
|
||||
@type group: L{IGroup<interfaces.IGroup>} provider
|
||||
"""
|
||||
self.chatui = chatui
|
||||
self.group = group
|
||||
self.members = []
|
||||
|
||||
def show(self):
|
||||
"""
|
||||
Display the GroupConversationWindow.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def hide(self):
|
||||
"""
|
||||
Hide the GroupConversationWindow.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def sendText(self, text):
|
||||
"""
|
||||
Send text to the group.
|
||||
|
||||
@param text: The text to be sent.
|
||||
@type text: C{str}
|
||||
"""
|
||||
self.group.sendGroupMessage(text, None)
|
||||
|
||||
def showGroupMessage(self, sender, text, metadata=None):
|
||||
"""
|
||||
Display to the user a message sent to this group from the given sender.
|
||||
|
||||
@param sender: The person sending the message.
|
||||
@type sender: C{str}
|
||||
|
||||
@param text: The sent message.
|
||||
@type text: C{str}
|
||||
|
||||
@param metadata: Metadata associated with this message.
|
||||
@type metadata: C{dict}
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def setGroupMembers(self, members):
|
||||
"""
|
||||
Set the list of members in the group.
|
||||
|
||||
@param members: The names of the people that will be in this group.
|
||||
@type members: C{list} of C{str}
|
||||
"""
|
||||
self.members = members
|
||||
|
||||
def setTopic(self, topic, author):
|
||||
"""
|
||||
Change the topic for the group conversation window and display this
|
||||
change to the user.
|
||||
|
||||
@param topic: This group's topic.
|
||||
@type topic: C{str}
|
||||
|
||||
@param author: The person changing the topic.
|
||||
@type author: C{str}
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def memberJoined(self, member):
|
||||
"""
|
||||
Add the given member to the list of members in the group conversation
|
||||
and displays this to the user.
|
||||
|
||||
@param member: The person joining the group conversation.
|
||||
@type member: C{str}
|
||||
"""
|
||||
if member not in self.members:
|
||||
self.members.append(member)
|
||||
|
||||
def memberChangedNick(self, oldnick, newnick):
|
||||
"""
|
||||
Change the nickname for a member of the group conversation and displays
|
||||
this change to the user.
|
||||
|
||||
@param oldnick: The old nickname.
|
||||
@type oldnick: C{str}
|
||||
|
||||
@param newnick: The new nickname.
|
||||
@type newnick: C{str}
|
||||
"""
|
||||
if oldnick in self.members:
|
||||
self.members.remove(oldnick)
|
||||
self.members.append(newnick)
|
||||
|
||||
def memberLeft(self, member):
|
||||
"""
|
||||
Delete the given member from the list of members in the group
|
||||
conversation and displays the change to the user.
|
||||
|
||||
@param member: The person leaving the group conversation.
|
||||
@type member: C{str}
|
||||
"""
|
||||
if member in self.members:
|
||||
self.members.remove(member)
|
||||
|
||||
|
||||
class ChatUI:
|
||||
"""
|
||||
A GUI chat client.
|
||||
|
||||
@type conversations: C{dict} of L{Conversation}
|
||||
@ivar conversations: A cache of all the direct windows.
|
||||
|
||||
@type groupConversations: C{dict} of L{GroupConversation}
|
||||
@ivar groupConversations: A cache of all the group windows.
|
||||
|
||||
@type persons: C{dict} with keys that are a C{tuple} of (C{str},
|
||||
L{IAccount<interfaces.IAccount>} provider) and values that are
|
||||
L{IPerson<interfaces.IPerson>} provider
|
||||
@ivar persons: A cache of all the users associated with this client.
|
||||
|
||||
@type groups: C{dict} with keys that are a C{tuple} of (C{str},
|
||||
L{IAccount<interfaces.IAccount>} provider) and values that are
|
||||
L{IGroup<interfaces.IGroup>} provider
|
||||
@ivar groups: A cache of all the groups associated with this client.
|
||||
|
||||
@type onlineClients: C{list} of L{IClient<interfaces.IClient>} providers
|
||||
@ivar onlineClients: A list of message sources currently online.
|
||||
|
||||
@type contactsList: L{ContactsList}
|
||||
@ivar contactsList: A contacts list.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.conversations = {}
|
||||
self.groupConversations = {}
|
||||
self.persons = {}
|
||||
self.groups = {}
|
||||
self.onlineClients = []
|
||||
self.contactsList = ContactsList(self)
|
||||
|
||||
def registerAccountClient(self, client):
|
||||
"""
|
||||
Notify the user that an account has been signed on to.
|
||||
|
||||
@type client: L{IClient<interfaces.IClient>} provider
|
||||
@param client: The client account for the person who has just signed on.
|
||||
|
||||
@rtype: L{IClient<interfaces.IClient>} provider
|
||||
@return: The client, so that it may be used in a callback chain.
|
||||
"""
|
||||
self.onlineClients.append(client)
|
||||
self.contactsList.registerAccountClient(client)
|
||||
return client
|
||||
|
||||
def unregisterAccountClient(self, client):
|
||||
"""
|
||||
Notify the user that an account has been signed off or disconnected.
|
||||
|
||||
@type client: L{IClient<interfaces.IClient>} provider
|
||||
@param client: The client account for the person who has just signed
|
||||
off.
|
||||
"""
|
||||
self.onlineClients.remove(client)
|
||||
self.contactsList.unregisterAccountClient(client)
|
||||
|
||||
def getContactsList(self):
|
||||
"""
|
||||
Get the contacts list associated with this chat window.
|
||||
|
||||
@rtype: L{ContactsList}
|
||||
@return: The contacts list associated with this chat window.
|
||||
"""
|
||||
return self.contactsList
|
||||
|
||||
def getConversation(self, person, Class=Conversation, stayHidden=False):
|
||||
"""
|
||||
For the given person object, return the conversation window or create
|
||||
and return a new conversation window if one does not exist.
|
||||
|
||||
@type person: L{IPerson<interfaces.IPerson>} provider
|
||||
@param person: The person whose conversation window we want to get.
|
||||
|
||||
@type Class: L{IConversation<interfaces.IConversation>} implementor
|
||||
@param Class: The kind of conversation window we want. If the conversation
|
||||
window for this person didn't already exist, create one of this type.
|
||||
|
||||
@type stayHidden: C{bool}
|
||||
@param stayHidden: Whether or not the conversation window should stay
|
||||
hidden.
|
||||
|
||||
@rtype: L{IConversation<interfaces.IConversation>} provider
|
||||
@return: The conversation window.
|
||||
"""
|
||||
conv = self.conversations.get(person)
|
||||
if not conv:
|
||||
conv = Class(person, self)
|
||||
self.conversations[person] = conv
|
||||
if stayHidden:
|
||||
conv.hide()
|
||||
else:
|
||||
conv.show()
|
||||
return conv
|
||||
|
||||
def getGroupConversation(self, group, Class=GroupConversation, stayHidden=False):
|
||||
"""
|
||||
For the given group object, return the group conversation window or
|
||||
create and return a new group conversation window if it doesn't exist.
|
||||
|
||||
@type group: L{IGroup<interfaces.IGroup>} provider
|
||||
@param group: The group whose conversation window we want to get.
|
||||
|
||||
@type Class: L{IConversation<interfaces.IConversation>} implementor
|
||||
@param Class: The kind of conversation window we want. If the conversation
|
||||
window for this person didn't already exist, create one of this type.
|
||||
|
||||
@type stayHidden: C{bool}
|
||||
@param stayHidden: Whether or not the conversation window should stay
|
||||
hidden.
|
||||
|
||||
@rtype: L{IGroupConversation<interfaces.IGroupConversation>} provider
|
||||
@return: The group conversation window.
|
||||
"""
|
||||
conv = self.groupConversations.get(group)
|
||||
if not conv:
|
||||
conv = Class(group, self)
|
||||
self.groupConversations[group] = conv
|
||||
if stayHidden:
|
||||
conv.hide()
|
||||
else:
|
||||
conv.show()
|
||||
return conv
|
||||
|
||||
def getPerson(self, name, client):
|
||||
"""
|
||||
For the given name and account client, return an instance of a
|
||||
L{IGroup<interfaces.IPerson>} provider or create and return a new
|
||||
instance of a L{IGroup<interfaces.IPerson>} provider.
|
||||
|
||||
@type name: C{str}
|
||||
@param name: The name of the person of interest.
|
||||
|
||||
@type client: L{IClient<interfaces.IClient>} provider
|
||||
@param client: The client account of interest.
|
||||
|
||||
@rtype: L{IPerson<interfaces.IPerson>} provider
|
||||
@return: The person with that C{name}.
|
||||
"""
|
||||
account = client.account
|
||||
p = self.persons.get((name, account))
|
||||
if not p:
|
||||
p = account.getPerson(name)
|
||||
self.persons[name, account] = p
|
||||
return p
|
||||
|
||||
def getGroup(self, name, client):
|
||||
"""
|
||||
For the given name and account client, return an instance of a
|
||||
L{IGroup<interfaces.IGroup>} provider or create and return a new instance
|
||||
of a L{IGroup<interfaces.IGroup>} provider.
|
||||
|
||||
@type name: C{str}
|
||||
@param name: The name of the group of interest.
|
||||
|
||||
@type client: L{IClient<interfaces.IClient>} provider
|
||||
@param client: The client account of interest.
|
||||
|
||||
@rtype: L{IGroup<interfaces.IGroup>} provider
|
||||
@return: The group with that C{name}.
|
||||
"""
|
||||
# I accept 'client' instead of 'account' in my signature for
|
||||
# backwards compatibility. (Groups changed to be Account-oriented
|
||||
# in CVS revision 1.8.)
|
||||
account = client.account
|
||||
g = self.groups.get((name, account))
|
||||
if not g:
|
||||
g = account.getGroup(name)
|
||||
self.groups[name, account] = g
|
||||
return g
|
||||
|
||||
def contactChangedNick(self, person, newnick):
|
||||
"""
|
||||
For the given C{person}, change the C{person}'s C{name} to C{newnick}
|
||||
and tell the contact list and any conversation windows with that
|
||||
C{person} to change as well.
|
||||
|
||||
@type person: L{IPerson<interfaces.IPerson>} provider
|
||||
@param person: The person whose nickname will get changed.
|
||||
|
||||
@type newnick: C{str}
|
||||
@param newnick: The new C{name} C{person} will take.
|
||||
"""
|
||||
oldnick = person.name
|
||||
if (oldnick, person.account) in self.persons:
|
||||
conv = self.conversations.get(person)
|
||||
if conv:
|
||||
conv.contactChangedNick(person, newnick)
|
||||
self.contactsList.contactChangedNick(person, newnick)
|
||||
del self.persons[oldnick, person.account]
|
||||
person.name = newnick
|
||||
self.persons[person.name, person.account] = person
|
||||
@@ -0,0 +1,275 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
#
|
||||
|
||||
"""Instance Messenger base classes for protocol support.
|
||||
|
||||
You will find these useful if you're adding a new protocol to IM.
|
||||
"""
|
||||
from typing import Type
|
||||
|
||||
from twisted.internet import error
|
||||
from twisted.internet.protocol import Protocol, connectionDone
|
||||
from twisted.persisted import styles
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.reflect import prefixedMethods
|
||||
from twisted.words.im.locals import OFFLINE, OfflineError
|
||||
|
||||
# Abstract representation of chat "model" classes
|
||||
|
||||
|
||||
class AbstractGroup:
|
||||
def __init__(self, name, account):
|
||||
self.name = name
|
||||
self.account = account
|
||||
|
||||
def getGroupCommands(self):
|
||||
"""finds group commands
|
||||
|
||||
these commands are methods on me that start with imgroup_; they are
|
||||
called with no arguments
|
||||
"""
|
||||
return prefixedMethods(self, "imgroup_")
|
||||
|
||||
def getTargetCommands(self, target):
|
||||
"""finds group commands
|
||||
|
||||
these commands are methods on me that start with imgroup_; they are
|
||||
called with a user present within this room as an argument
|
||||
|
||||
you may want to override this in your group in order to filter for
|
||||
appropriate commands on the given user
|
||||
"""
|
||||
return prefixedMethods(self, "imtarget_")
|
||||
|
||||
def join(self):
|
||||
if not self.account.client:
|
||||
raise OfflineError
|
||||
self.account.client.joinGroup(self.name)
|
||||
|
||||
def leave(self):
|
||||
if not self.account.client:
|
||||
raise OfflineError
|
||||
self.account.client.leaveGroup(self.name)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__} {self.name!r}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name}@{self.account.accountName}"
|
||||
|
||||
|
||||
class AbstractPerson:
|
||||
def __init__(self, name, baseAccount):
|
||||
self.name = name
|
||||
self.account = baseAccount
|
||||
self.status = OFFLINE
|
||||
|
||||
def getPersonCommands(self):
|
||||
"""finds person commands
|
||||
|
||||
these commands are methods on me that start with imperson_; they are
|
||||
called with no arguments
|
||||
"""
|
||||
return prefixedMethods(self, "imperson_")
|
||||
|
||||
def getIdleTime(self):
|
||||
"""
|
||||
Returns a string.
|
||||
"""
|
||||
return "--"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__} {self.name!r}/{self.status}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name}@{self.account.accountName}"
|
||||
|
||||
|
||||
class AbstractClientMixin:
|
||||
"""Designed to be mixed in to a Protocol implementing class.
|
||||
|
||||
Inherit from me first.
|
||||
|
||||
@ivar _logonDeferred: Fired when I am done logging in.
|
||||
"""
|
||||
|
||||
_protoBase: Type[Protocol] = None # type: ignore[assignment]
|
||||
|
||||
def __init__(self, account, chatui, logonDeferred):
|
||||
for base in self.__class__.__bases__:
|
||||
if issubclass(base, Protocol):
|
||||
self.__class__._protoBase = base
|
||||
break
|
||||
else:
|
||||
pass
|
||||
self.account = account
|
||||
self.chat = chatui
|
||||
self._logonDeferred = logonDeferred
|
||||
|
||||
def connectionMade(self):
|
||||
self._protoBase.connectionMade(self)
|
||||
|
||||
def connectionLost(self, reason: Failure = connectionDone) -> None:
|
||||
self.account._clientLost(self, reason)
|
||||
self.unregisterAsAccountClient()
|
||||
return self._protoBase.connectionLost(self, reason) # type: ignore[arg-type]
|
||||
|
||||
def unregisterAsAccountClient(self):
|
||||
"""Tell the chat UI that I have `signed off'."""
|
||||
self.chat.unregisterAccountClient(self)
|
||||
|
||||
|
||||
class AbstractAccount(styles.Versioned):
|
||||
"""Base class for Accounts.
|
||||
|
||||
I am the start of an implementation of L{IAccount<interfaces.IAccount>}, I
|
||||
implement L{isOnline} and most of L{logOn}, though you'll need to implement
|
||||
L{_startLogOn} in a subclass.
|
||||
|
||||
@cvar _groupFactory: A Callable that will return a L{IGroup} appropriate
|
||||
for this account type.
|
||||
@cvar _personFactory: A Callable that will return a L{IPerson} appropriate
|
||||
for this account type.
|
||||
|
||||
@type _isConnecting: boolean
|
||||
@ivar _isConnecting: Whether I am in the process of establishing a
|
||||
connection to the server.
|
||||
@type _isOnline: boolean
|
||||
@ivar _isOnline: Whether I am currently on-line with the server.
|
||||
|
||||
@ivar accountName:
|
||||
@ivar autoLogin:
|
||||
@ivar username:
|
||||
@ivar password:
|
||||
@ivar host:
|
||||
@ivar port:
|
||||
"""
|
||||
|
||||
_isOnline = 0
|
||||
_isConnecting = 0
|
||||
client = None
|
||||
|
||||
_groupFactory = AbstractGroup
|
||||
_personFactory = AbstractPerson
|
||||
|
||||
persistanceVersion = 2
|
||||
|
||||
def __init__(self, accountName, autoLogin, username, password, host, port):
|
||||
self.accountName = accountName
|
||||
self.autoLogin = autoLogin
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
self._groups = {}
|
||||
self._persons = {}
|
||||
|
||||
def upgrateToVersion2(self):
|
||||
# Added in CVS revision 1.16.
|
||||
for k in ("_groups", "_persons"):
|
||||
if not hasattr(self, k):
|
||||
setattr(self, k, {})
|
||||
|
||||
def __getstate__(self):
|
||||
state = styles.Versioned.__getstate__(self)
|
||||
for k in ("client", "_isOnline", "_isConnecting"):
|
||||
try:
|
||||
del state[k]
|
||||
except KeyError:
|
||||
pass
|
||||
return state
|
||||
|
||||
def isOnline(self):
|
||||
return self._isOnline
|
||||
|
||||
def logOn(self, chatui):
|
||||
"""Log on to this account.
|
||||
|
||||
Takes care to not start a connection if a connection is
|
||||
already in progress. You will need to implement
|
||||
L{_startLogOn} for this to work, and it would be a good idea
|
||||
to override L{_loginFailed} too.
|
||||
|
||||
@returntype: Deferred L{interfaces.IClient}
|
||||
"""
|
||||
if (not self._isConnecting) and (not self._isOnline):
|
||||
self._isConnecting = 1
|
||||
d = self._startLogOn(chatui)
|
||||
d.addCallback(self._cb_logOn)
|
||||
# if chatui is not None:
|
||||
# (I don't particularly like having to pass chatUI to this function,
|
||||
# but we haven't factored it out yet.)
|
||||
d.addCallback(chatui.registerAccountClient)
|
||||
d.addErrback(self._loginFailed)
|
||||
return d
|
||||
else:
|
||||
raise error.ConnectError("Connection in progress")
|
||||
|
||||
def getGroup(self, name):
|
||||
"""Group factory.
|
||||
|
||||
@param name: Name of the group on this account.
|
||||
@type name: string
|
||||
"""
|
||||
group = self._groups.get(name)
|
||||
if group is None:
|
||||
group = self._groupFactory(name, self)
|
||||
self._groups[name] = group
|
||||
return group
|
||||
|
||||
def getPerson(self, name):
|
||||
"""Person factory.
|
||||
|
||||
@param name: Name of the person on this account.
|
||||
@type name: string
|
||||
"""
|
||||
person = self._persons.get(name)
|
||||
if person is None:
|
||||
person = self._personFactory(name, self)
|
||||
self._persons[name] = person
|
||||
return person
|
||||
|
||||
def _startLogOn(self, chatui):
|
||||
"""Start the sign on process.
|
||||
|
||||
Factored out of L{logOn}.
|
||||
|
||||
@returntype: Deferred L{interfaces.IClient}
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _cb_logOn(self, client):
|
||||
self._isConnecting = 0
|
||||
self._isOnline = 1
|
||||
self.client = client
|
||||
return client
|
||||
|
||||
def _loginFailed(self, reason):
|
||||
"""Errorback for L{logOn}.
|
||||
|
||||
@type reason: Failure
|
||||
|
||||
@returns: I{reason}, for further processing in the callback chain.
|
||||
@returntype: Failure
|
||||
"""
|
||||
self._isConnecting = 0
|
||||
self._isOnline = 0 # just in case
|
||||
return reason
|
||||
|
||||
def _clientLost(self, client, reason):
|
||||
self.client = None
|
||||
self._isConnecting = 0
|
||||
self._isOnline = 0
|
||||
return reason
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<{}: {} ({}@{}:{})>".format(
|
||||
self.__class__,
|
||||
self.accountName,
|
||||
self.username,
|
||||
self.host,
|
||||
self.port,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,362 @@
|
||||
# -*- Python -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Pan-protocol chat client.
|
||||
"""
|
||||
|
||||
from zope.interface import Attribute, Interface
|
||||
|
||||
# (Random musings, may not reflect on current state of code:)
|
||||
#
|
||||
# Accounts have Protocol components (clients)
|
||||
# Persons have Conversation components
|
||||
# Groups have GroupConversation components
|
||||
# Persons and Groups are associated with specific Accounts
|
||||
# At run-time, Clients/Accounts are slaved to a User Interface
|
||||
# (Note: User may be a bot, so don't assume all UIs are built on gui toolkits)
|
||||
|
||||
|
||||
class IAccount(Interface):
|
||||
"""
|
||||
I represent a user's account with a chat service.
|
||||
"""
|
||||
|
||||
client = Attribute("The L{IClient} currently connecting to this account, if any.")
|
||||
gatewayType = Attribute(
|
||||
"A C{str} that identifies the protocol used by this account."
|
||||
)
|
||||
|
||||
def __init__(accountName, autoLogin, username, password, host, port):
|
||||
"""
|
||||
@type accountName: string
|
||||
@param accountName: A name to refer to the account by locally.
|
||||
@type autoLogin: boolean
|
||||
@type username: string
|
||||
@type password: string
|
||||
@type host: string
|
||||
@type port: integer
|
||||
"""
|
||||
|
||||
def isOnline():
|
||||
"""
|
||||
Am I online?
|
||||
|
||||
@rtype: boolean
|
||||
"""
|
||||
|
||||
def logOn(chatui):
|
||||
"""
|
||||
Go on-line.
|
||||
|
||||
@type chatui: Implementor of C{IChatUI}
|
||||
|
||||
@rtype: L{Deferred} with an eventual L{IClient} result.
|
||||
"""
|
||||
|
||||
def logOff():
|
||||
"""
|
||||
Sign off.
|
||||
"""
|
||||
|
||||
def getGroup(groupName):
|
||||
"""
|
||||
@rtype: L{Group<IGroup>}
|
||||
"""
|
||||
|
||||
def getPerson(personName):
|
||||
"""
|
||||
@rtype: L{Person<IPerson>}
|
||||
"""
|
||||
|
||||
|
||||
class IClient(Interface):
|
||||
account = Attribute("The L{IAccount} I am a Client for")
|
||||
|
||||
def __init__(account, chatui, logonDeferred):
|
||||
"""
|
||||
@type account: L{IAccount}
|
||||
@type chatui: L{IChatUI}
|
||||
@param logonDeferred: Will be called back once I am logged on.
|
||||
@type logonDeferred: L{Deferred<twisted.internet.defer.Deferred>}
|
||||
"""
|
||||
|
||||
def joinGroup(groupName):
|
||||
"""
|
||||
@param groupName: The name of the group to join.
|
||||
@type groupName: string
|
||||
"""
|
||||
|
||||
def leaveGroup(groupName):
|
||||
"""
|
||||
@param groupName: The name of the group to leave.
|
||||
@type groupName: string
|
||||
"""
|
||||
|
||||
def getGroupConversation(name, hide=0):
|
||||
pass
|
||||
|
||||
def getPerson(name):
|
||||
pass
|
||||
|
||||
|
||||
class IPerson(Interface):
|
||||
def __init__(name, account):
|
||||
"""
|
||||
Initialize me.
|
||||
|
||||
@param name: My name, as the server knows me.
|
||||
@type name: string
|
||||
@param account: The account I am accessed through.
|
||||
@type account: I{Account}
|
||||
"""
|
||||
|
||||
def isOnline():
|
||||
"""
|
||||
Am I online right now?
|
||||
|
||||
@rtype: boolean
|
||||
"""
|
||||
|
||||
def getStatus():
|
||||
"""
|
||||
What is my on-line status?
|
||||
|
||||
@return: L{locals.StatusEnum}
|
||||
"""
|
||||
|
||||
def getIdleTime():
|
||||
"""
|
||||
@rtype: string (XXX: How about a scalar?)
|
||||
"""
|
||||
|
||||
def sendMessage(text, metadata=None):
|
||||
"""
|
||||
Send a message to this person.
|
||||
|
||||
@type text: string
|
||||
@type metadata: dict
|
||||
"""
|
||||
|
||||
|
||||
class IGroup(Interface):
|
||||
"""
|
||||
A group which you may have a conversation with.
|
||||
|
||||
Groups generally have a loosely-defined set of members, who may
|
||||
leave and join at any time.
|
||||
"""
|
||||
|
||||
name = Attribute("My C{str} name, as the server knows me.")
|
||||
account = Attribute("The L{Account<IAccount>} I am accessed through.")
|
||||
|
||||
def __init__(name, account):
|
||||
"""
|
||||
Initialize me.
|
||||
|
||||
@param name: My name, as the server knows me.
|
||||
@type name: str
|
||||
@param account: The account I am accessed through.
|
||||
@type account: L{Account<IAccount>}
|
||||
"""
|
||||
|
||||
def setTopic(text):
|
||||
"""
|
||||
Set this Groups topic on the server.
|
||||
|
||||
@type text: string
|
||||
"""
|
||||
|
||||
def sendGroupMessage(text, metadata=None):
|
||||
"""
|
||||
Send a message to this group.
|
||||
|
||||
@type text: str
|
||||
|
||||
@type metadata: dict
|
||||
@param metadata: Valid keys for this dictionary include:
|
||||
|
||||
- C{'style'}: associated with one of:
|
||||
- C{'emote'}: indicates this is an action
|
||||
"""
|
||||
|
||||
def join():
|
||||
"""
|
||||
Join this group.
|
||||
"""
|
||||
|
||||
def leave():
|
||||
"""
|
||||
Depart this group.
|
||||
"""
|
||||
|
||||
|
||||
class IConversation(Interface):
|
||||
"""
|
||||
A conversation with a specific person.
|
||||
"""
|
||||
|
||||
def __init__(person, chatui):
|
||||
"""
|
||||
@type person: L{IPerson}
|
||||
"""
|
||||
|
||||
def show():
|
||||
"""
|
||||
doesn't seem like it belongs in this interface.
|
||||
"""
|
||||
|
||||
def hide():
|
||||
"""
|
||||
nor this neither.
|
||||
"""
|
||||
|
||||
def sendText(text, metadata):
|
||||
pass
|
||||
|
||||
def showMessage(text, metadata):
|
||||
pass
|
||||
|
||||
def changedNick(person, newnick):
|
||||
"""
|
||||
@param person: XXX Shouldn't this always be Conversation.person?
|
||||
"""
|
||||
|
||||
|
||||
class IGroupConversation(Interface):
|
||||
def show():
|
||||
"""
|
||||
doesn't seem like it belongs in this interface.
|
||||
"""
|
||||
|
||||
def hide():
|
||||
"""
|
||||
nor this neither.
|
||||
"""
|
||||
|
||||
def sendText(text, metadata):
|
||||
pass
|
||||
|
||||
def showGroupMessage(sender, text, metadata):
|
||||
pass
|
||||
|
||||
def setGroupMembers(members):
|
||||
"""
|
||||
Sets the list of members in the group and displays it to the user.
|
||||
"""
|
||||
|
||||
def setTopic(topic, author):
|
||||
"""
|
||||
Displays the topic (from the server) for the group conversation window.
|
||||
|
||||
@type topic: string
|
||||
@type author: string (XXX: Not Person?)
|
||||
"""
|
||||
|
||||
def memberJoined(member):
|
||||
"""
|
||||
Adds the given member to the list of members in the group conversation
|
||||
and displays this to the user,
|
||||
|
||||
@type member: string (XXX: Not Person?)
|
||||
"""
|
||||
|
||||
def memberChangedNick(oldnick, newnick):
|
||||
"""
|
||||
Changes the oldnick in the list of members to C{newnick} and displays this
|
||||
change to the user,
|
||||
|
||||
@type oldnick: string (XXX: Not Person?)
|
||||
@type newnick: string
|
||||
"""
|
||||
|
||||
def memberLeft(member):
|
||||
"""
|
||||
Deletes the given member from the list of members in the group
|
||||
conversation and displays the change to the user.
|
||||
|
||||
@type member: string (XXX: Not Person?)
|
||||
"""
|
||||
|
||||
|
||||
class IChatUI(Interface):
|
||||
def registerAccountClient(client):
|
||||
"""
|
||||
Notifies user that an account has been signed on to.
|
||||
|
||||
@type client: L{Client<IClient>}
|
||||
"""
|
||||
|
||||
def unregisterAccountClient(client):
|
||||
"""
|
||||
Notifies user that an account has been signed off or disconnected.
|
||||
|
||||
@type client: L{Client<IClient>}
|
||||
"""
|
||||
|
||||
def getContactsList():
|
||||
"""
|
||||
@rtype: L{ContactsList}
|
||||
"""
|
||||
|
||||
# WARNING: You'll want to be polymorphed into something with
|
||||
# intrinsic stoning resistance before continuing.
|
||||
|
||||
def getConversation(person, Class, stayHidden=0):
|
||||
"""
|
||||
For the given person object, returns the conversation window
|
||||
or creates and returns a new conversation window if one does not exist.
|
||||
|
||||
@type person: L{Person<IPerson>}
|
||||
@type Class: L{Conversation<IConversation>} class
|
||||
@type stayHidden: boolean
|
||||
|
||||
@rtype: L{Conversation<IConversation>}
|
||||
"""
|
||||
|
||||
def getGroupConversation(group, Class, stayHidden=0):
|
||||
"""
|
||||
For the given group object, returns the group conversation window or
|
||||
creates and returns a new group conversation window if it doesn't exist.
|
||||
|
||||
@type group: L{Group<interfaces.IGroup>}
|
||||
@type Class: L{Conversation<interfaces.IConversation>} class
|
||||
@type stayHidden: boolean
|
||||
|
||||
@rtype: L{GroupConversation<interfaces.IGroupConversation>}
|
||||
"""
|
||||
|
||||
def getPerson(name, client):
|
||||
"""
|
||||
Get a Person for a client.
|
||||
|
||||
Duplicates L{IAccount.getPerson}.
|
||||
|
||||
@type name: string
|
||||
@type client: L{Client<IClient>}
|
||||
|
||||
@rtype: L{Person<IPerson>}
|
||||
"""
|
||||
|
||||
def getGroup(name, client):
|
||||
"""
|
||||
Get a Group for a client.
|
||||
|
||||
Duplicates L{IAccount.getGroup}.
|
||||
|
||||
@type name: string
|
||||
@type client: L{Client<IClient>}
|
||||
|
||||
@rtype: L{Group<IGroup>}
|
||||
"""
|
||||
|
||||
def contactChangedNick(oldnick, newnick):
|
||||
"""
|
||||
For the given person, changes the person's name to newnick, and
|
||||
tells the contact list and any conversation windows with that person
|
||||
to change as well.
|
||||
|
||||
@type oldnick: string
|
||||
@type newnick: string
|
||||
"""
|
||||
@@ -0,0 +1,273 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
IRC support for Instance Messenger.
|
||||
"""
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.internet import defer, protocol, reactor
|
||||
from twisted.internet.defer import succeed
|
||||
from twisted.words.im import basesupport, interfaces, locals
|
||||
from twisted.words.im.locals import ONLINE
|
||||
from twisted.words.protocols import irc
|
||||
|
||||
|
||||
class IRCPerson(basesupport.AbstractPerson):
|
||||
def imperson_whois(self):
|
||||
if self.account.client is None:
|
||||
raise locals.OfflineError
|
||||
self.account.client.sendLine("WHOIS %s" % self.name)
|
||||
|
||||
### interface impl
|
||||
def isOnline(self):
|
||||
return ONLINE
|
||||
|
||||
def getStatus(self):
|
||||
return ONLINE
|
||||
|
||||
def setStatus(self, status):
|
||||
self.status = status
|
||||
self.chat.getContactsList().setContactStatus(self)
|
||||
|
||||
def sendMessage(self, text, meta=None):
|
||||
if self.account.client is None:
|
||||
raise locals.OfflineError
|
||||
for line in text.split("\n"):
|
||||
if meta and meta.get("style", None) == "emote":
|
||||
self.account.client.ctcpMakeQuery(self.name, [("ACTION", line)])
|
||||
else:
|
||||
self.account.client.msg(self.name, line)
|
||||
return succeed(text)
|
||||
|
||||
|
||||
@implementer(interfaces.IGroup)
|
||||
class IRCGroup(basesupport.AbstractGroup):
|
||||
def imgroup_testAction(self):
|
||||
pass
|
||||
|
||||
def imtarget_kick(self, target):
|
||||
if self.account.client is None:
|
||||
raise locals.OfflineError
|
||||
reason = "for great justice!"
|
||||
self.account.client.sendLine(f"KICK #{self.name} {target.name} :{reason}")
|
||||
|
||||
### Interface Implementation
|
||||
def setTopic(self, topic):
|
||||
if self.account.client is None:
|
||||
raise locals.OfflineError
|
||||
self.account.client.topic(self.name, topic)
|
||||
|
||||
def sendGroupMessage(self, text, meta={}):
|
||||
if self.account.client is None:
|
||||
raise locals.OfflineError
|
||||
if meta and meta.get("style", None) == "emote":
|
||||
self.account.client.ctcpMakeQuery(self.name, [("ACTION", text)])
|
||||
return succeed(text)
|
||||
# standard shmandard, clients don't support plain escaped newlines!
|
||||
for line in text.split("\n"):
|
||||
self.account.client.say(self.name, line)
|
||||
return succeed(text)
|
||||
|
||||
def leave(self):
|
||||
if self.account.client is None:
|
||||
raise locals.OfflineError
|
||||
self.account.client.leave(self.name)
|
||||
self.account.client.getGroupConversation(self.name, 1)
|
||||
|
||||
|
||||
class IRCProto(basesupport.AbstractClientMixin, irc.IRCClient):
|
||||
def __init__(self, account, chatui, logonDeferred=None):
|
||||
basesupport.AbstractClientMixin.__init__(self, account, chatui, logonDeferred)
|
||||
self._namreplies = {}
|
||||
self._ingroups = {}
|
||||
self._groups = {}
|
||||
self._topics = {}
|
||||
|
||||
def getGroupConversation(self, name, hide=0):
|
||||
name = name.lower()
|
||||
return self.chat.getGroupConversation(
|
||||
self.chat.getGroup(name, self), stayHidden=hide
|
||||
)
|
||||
|
||||
def getPerson(self, name):
|
||||
return self.chat.getPerson(name, self)
|
||||
|
||||
def connectionMade(self):
|
||||
# XXX: Why do I duplicate code in IRCClient.register?
|
||||
try:
|
||||
self.performLogin = True
|
||||
self.nickname = self.account.username
|
||||
self.password = self.account.password
|
||||
self.realname = "Twisted-IM user"
|
||||
|
||||
irc.IRCClient.connectionMade(self)
|
||||
|
||||
for channel in self.account.channels:
|
||||
self.joinGroup(channel)
|
||||
self.account._isOnline = 1
|
||||
if self._logonDeferred is not None:
|
||||
self._logonDeferred.callback(self)
|
||||
self.chat.getContactsList()
|
||||
except BaseException:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
def setNick(self, nick):
|
||||
self.name = nick
|
||||
self.accountName = "%s (IRC)" % nick
|
||||
irc.IRCClient.setNick(self, nick)
|
||||
|
||||
def kickedFrom(self, channel, kicker, message):
|
||||
"""
|
||||
Called when I am kicked from a channel.
|
||||
"""
|
||||
return self.chat.getGroupConversation(self.chat.getGroup(channel[1:], self), 1)
|
||||
|
||||
def userKicked(self, kickee, channel, kicker, message):
|
||||
pass
|
||||
|
||||
def noticed(self, username, channel, message):
|
||||
self.privmsg(username, channel, message, {"dontAutoRespond": 1})
|
||||
|
||||
def privmsg(self, username, channel, message, metadata=None):
|
||||
if metadata is None:
|
||||
metadata = {}
|
||||
username = username.split("!", 1)[0]
|
||||
if username == self.name:
|
||||
return
|
||||
if channel[0] == "#":
|
||||
group = channel[1:]
|
||||
self.getGroupConversation(group).showGroupMessage(
|
||||
username, message, metadata
|
||||
)
|
||||
return
|
||||
self.chat.getConversation(self.getPerson(username)).showMessage(
|
||||
message, metadata
|
||||
)
|
||||
|
||||
def action(self, username, channel, emote):
|
||||
username = username.split("!", 1)[0]
|
||||
if username == self.name:
|
||||
return
|
||||
meta = {"style": "emote"}
|
||||
if channel[0] == "#":
|
||||
group = channel[1:]
|
||||
self.getGroupConversation(group).showGroupMessage(username, emote, meta)
|
||||
return
|
||||
self.chat.getConversation(self.getPerson(username)).showMessage(emote, meta)
|
||||
|
||||
def irc_RPL_NAMREPLY(self, prefix, params):
|
||||
"""
|
||||
RPL_NAMREPLY
|
||||
>> NAMES #bnl
|
||||
<< :Arlington.VA.US.Undernet.Org 353 z3p = #bnl :pSwede Dan-- SkOyg AG
|
||||
"""
|
||||
group = params[2][1:].lower()
|
||||
users = params[3].split()
|
||||
for ui in range(len(users)):
|
||||
while users[ui][0] in ["@", "+"]: # channel modes
|
||||
users[ui] = users[ui][1:]
|
||||
if group not in self._namreplies:
|
||||
self._namreplies[group] = []
|
||||
self._namreplies[group].extend(users)
|
||||
for nickname in users:
|
||||
try:
|
||||
self._ingroups[nickname].append(group)
|
||||
except BaseException:
|
||||
self._ingroups[nickname] = [group]
|
||||
|
||||
def irc_RPL_ENDOFNAMES(self, prefix, params):
|
||||
group = params[1][1:]
|
||||
self.getGroupConversation(group).setGroupMembers(
|
||||
self._namreplies[group.lower()]
|
||||
)
|
||||
del self._namreplies[group.lower()]
|
||||
|
||||
def irc_RPL_TOPIC(self, prefix, params):
|
||||
self._topics[params[1][1:]] = params[2]
|
||||
|
||||
def irc_333(self, prefix, params):
|
||||
group = params[1][1:]
|
||||
self.getGroupConversation(group).setTopic(self._topics[group], params[2])
|
||||
del self._topics[group]
|
||||
|
||||
def irc_TOPIC(self, prefix, params):
|
||||
nickname = prefix.split("!")[0]
|
||||
group = params[0][1:]
|
||||
topic = params[1]
|
||||
self.getGroupConversation(group).setTopic(topic, nickname)
|
||||
|
||||
def irc_JOIN(self, prefix, params):
|
||||
nickname = prefix.split("!")[0]
|
||||
group = params[0][1:].lower()
|
||||
if nickname != self.nickname:
|
||||
try:
|
||||
self._ingroups[nickname].append(group)
|
||||
except BaseException:
|
||||
self._ingroups[nickname] = [group]
|
||||
self.getGroupConversation(group).memberJoined(nickname)
|
||||
|
||||
def irc_PART(self, prefix, params):
|
||||
nickname = prefix.split("!")[0]
|
||||
group = params[0][1:].lower()
|
||||
if nickname != self.nickname:
|
||||
if group in self._ingroups[nickname]:
|
||||
self._ingroups[nickname].remove(group)
|
||||
self.getGroupConversation(group).memberLeft(nickname)
|
||||
|
||||
def irc_QUIT(self, prefix, params):
|
||||
nickname = prefix.split("!")[0]
|
||||
if nickname in self._ingroups:
|
||||
for group in self._ingroups[nickname]:
|
||||
self.getGroupConversation(group).memberLeft(nickname)
|
||||
self._ingroups[nickname] = []
|
||||
|
||||
def irc_NICK(self, prefix, params):
|
||||
fromNick = prefix.split("!")[0]
|
||||
toNick = params[0]
|
||||
if fromNick not in self._ingroups:
|
||||
return
|
||||
for group in self._ingroups[fromNick]:
|
||||
self.getGroupConversation(group).memberChangedNick(fromNick, toNick)
|
||||
self._ingroups[toNick] = self._ingroups[fromNick]
|
||||
del self._ingroups[fromNick]
|
||||
|
||||
def irc_unknown(self, prefix, command, params):
|
||||
pass
|
||||
|
||||
# GTKIM calls
|
||||
def joinGroup(self, name):
|
||||
self.join(name)
|
||||
self.getGroupConversation(name)
|
||||
|
||||
|
||||
@implementer(interfaces.IAccount)
|
||||
class IRCAccount(basesupport.AbstractAccount):
|
||||
gatewayType = "IRC"
|
||||
|
||||
_groupFactory = IRCGroup
|
||||
_personFactory = IRCPerson
|
||||
|
||||
def __init__(
|
||||
self, accountName, autoLogin, username, password, host, port, channels=""
|
||||
):
|
||||
basesupport.AbstractAccount.__init__(
|
||||
self, accountName, autoLogin, username, password, host, port
|
||||
)
|
||||
self.channels = [channel.strip() for channel in channels.split(",")]
|
||||
if self.channels == [""]:
|
||||
self.channels = []
|
||||
|
||||
def _startLogOn(self, chatui):
|
||||
logonDeferred = defer.Deferred()
|
||||
cc = protocol.ClientCreator(reactor, IRCProto, self, chatui, logonDeferred)
|
||||
d = cc.connectTCP(self.host, self.port)
|
||||
d.addErrback(logonDeferred.errback)
|
||||
return logonDeferred
|
||||
|
||||
def logOff(self):
|
||||
# IAccount.logOff
|
||||
pass
|
||||
@@ -0,0 +1,30 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Enum:
|
||||
group: Optional[str] = None
|
||||
|
||||
def __init__(self, label: str) -> None:
|
||||
self.label = label
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.group}: {self.label}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.label
|
||||
|
||||
|
||||
class StatusEnum(Enum):
|
||||
group = "Status"
|
||||
|
||||
|
||||
OFFLINE = Enum("Offline")
|
||||
ONLINE = Enum("Online")
|
||||
AWAY = Enum("Away")
|
||||
|
||||
|
||||
class OfflineError(Exception):
|
||||
"""The requested action can't happen while offline."""
|
||||
@@ -0,0 +1,278 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
"""
|
||||
L{twisted.words} support for Instance Messenger.
|
||||
"""
|
||||
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.internet import defer, error
|
||||
from twisted.python import log
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.spread import pb
|
||||
from twisted.words.im import basesupport, interfaces
|
||||
from twisted.words.im.locals import AWAY, OFFLINE, ONLINE
|
||||
|
||||
|
||||
class TwistedWordsPerson(basesupport.AbstractPerson):
|
||||
"""I a facade for a person you can talk to through a twisted.words service."""
|
||||
|
||||
def __init__(self, name, wordsAccount):
|
||||
basesupport.AbstractPerson.__init__(self, name, wordsAccount)
|
||||
self.status = OFFLINE
|
||||
|
||||
def isOnline(self):
|
||||
return (self.status == ONLINE) or (self.status == AWAY)
|
||||
|
||||
def getStatus(self):
|
||||
return self.status
|
||||
|
||||
def sendMessage(self, text, metadata):
|
||||
"""Return a deferred..."""
|
||||
if metadata:
|
||||
d = self.account.client.perspective.directMessage(self.name, text, metadata)
|
||||
d.addErrback(self.metadataFailed, "* " + text)
|
||||
return d
|
||||
else:
|
||||
return self.account.client.perspective.callRemote(
|
||||
"directMessage", self.name, text
|
||||
)
|
||||
|
||||
def metadataFailed(self, result, text):
|
||||
print("result:", result, "text:", text)
|
||||
return self.account.client.perspective.directMessage(self.name, text)
|
||||
|
||||
def setStatus(self, status):
|
||||
self.status = status
|
||||
self.chat.getContactsList().setContactStatus(self)
|
||||
|
||||
|
||||
@implementer(interfaces.IGroup)
|
||||
class TwistedWordsGroup(basesupport.AbstractGroup):
|
||||
def __init__(self, name, wordsClient):
|
||||
basesupport.AbstractGroup.__init__(self, name, wordsClient)
|
||||
self.joined = 0
|
||||
|
||||
def sendGroupMessage(self, text, metadata=None):
|
||||
"""Return a deferred."""
|
||||
# for backwards compatibility with older twisted.words servers.
|
||||
if metadata:
|
||||
d = self.account.client.perspective.callRemote(
|
||||
"groupMessage", self.name, text, metadata
|
||||
)
|
||||
d.addErrback(self.metadataFailed, "* " + text)
|
||||
return d
|
||||
else:
|
||||
return self.account.client.perspective.callRemote(
|
||||
"groupMessage", self.name, text
|
||||
)
|
||||
|
||||
def setTopic(self, text):
|
||||
self.account.client.perspective.callRemote(
|
||||
"setGroupMetadata",
|
||||
{"topic": text, "topic_author": self.client.name},
|
||||
self.name,
|
||||
)
|
||||
|
||||
def metadataFailed(self, result, text):
|
||||
print("result:", result, "text:", text)
|
||||
return self.account.client.perspective.callRemote(
|
||||
"groupMessage", self.name, text
|
||||
)
|
||||
|
||||
def joining(self):
|
||||
self.joined = 1
|
||||
|
||||
def leaving(self):
|
||||
self.joined = 0
|
||||
|
||||
def leave(self):
|
||||
return self.account.client.perspective.callRemote("leaveGroup", self.name)
|
||||
|
||||
|
||||
class TwistedWordsClient(pb.Referenceable, basesupport.AbstractClientMixin):
|
||||
"""In some cases, this acts as an Account, since it a source of text
|
||||
messages (multiple Words instances may be on a single PB connection)
|
||||
"""
|
||||
|
||||
def __init__(self, acct, serviceName, perspectiveName, chatui, _logonDeferred=None):
|
||||
self.accountName = "{} ({}:{})".format(
|
||||
acct.accountName,
|
||||
serviceName,
|
||||
perspectiveName,
|
||||
)
|
||||
self.name = perspectiveName
|
||||
print("HELLO I AM A PB SERVICE", serviceName, perspectiveName)
|
||||
self.chat = chatui
|
||||
self.account = acct
|
||||
self._logonDeferred = _logonDeferred
|
||||
|
||||
def getPerson(self, name):
|
||||
return self.chat.getPerson(name, self)
|
||||
|
||||
def getGroup(self, name):
|
||||
return self.chat.getGroup(name, self)
|
||||
|
||||
def getGroupConversation(self, name):
|
||||
return self.chat.getGroupConversation(self.getGroup(name))
|
||||
|
||||
def addContact(self, name):
|
||||
self.perspective.callRemote("addContact", name)
|
||||
|
||||
def remote_receiveGroupMembers(self, names, group):
|
||||
print("received group members:", names, group)
|
||||
self.getGroupConversation(group).setGroupMembers(names)
|
||||
|
||||
def remote_receiveGroupMessage(self, sender, group, message, metadata=None):
|
||||
print("received a group message", sender, group, message, metadata)
|
||||
self.getGroupConversation(group).showGroupMessage(sender, message, metadata)
|
||||
|
||||
def remote_memberJoined(self, member, group):
|
||||
print("member joined", member, group)
|
||||
self.getGroupConversation(group).memberJoined(member)
|
||||
|
||||
def remote_memberLeft(self, member, group):
|
||||
print("member left")
|
||||
self.getGroupConversation(group).memberLeft(member)
|
||||
|
||||
def remote_notifyStatusChanged(self, name, status):
|
||||
self.chat.getPerson(name, self).setStatus(status)
|
||||
|
||||
def remote_receiveDirectMessage(self, name, message, metadata=None):
|
||||
self.chat.getConversation(self.chat.getPerson(name, self)).showMessage(
|
||||
message, metadata
|
||||
)
|
||||
|
||||
def remote_receiveContactList(self, clist):
|
||||
for name, status in clist:
|
||||
self.chat.getPerson(name, self).setStatus(status)
|
||||
|
||||
def remote_setGroupMetadata(self, dict_, groupName):
|
||||
if "topic" in dict_:
|
||||
self.getGroupConversation(groupName).setTopic(
|
||||
dict_["topic"], dict_.get("topic_author", None)
|
||||
)
|
||||
|
||||
def joinGroup(self, name):
|
||||
self.getGroup(name).joining()
|
||||
return self.perspective.callRemote("joinGroup", name).addCallback(
|
||||
self._cbGroupJoined, name
|
||||
)
|
||||
|
||||
def leaveGroup(self, name):
|
||||
self.getGroup(name).leaving()
|
||||
return self.perspective.callRemote("leaveGroup", name).addCallback(
|
||||
self._cbGroupLeft, name
|
||||
)
|
||||
|
||||
def _cbGroupJoined(self, result, name):
|
||||
groupConv = self.chat.getGroupConversation(self.getGroup(name))
|
||||
groupConv.showGroupMessage("sys", "you joined")
|
||||
self.perspective.callRemote("getGroupMembers", name)
|
||||
|
||||
def _cbGroupLeft(self, result, name):
|
||||
print("left", name)
|
||||
groupConv = self.chat.getGroupConversation(self.getGroup(name), 1)
|
||||
groupConv.showGroupMessage("sys", "you left")
|
||||
|
||||
def connected(self, perspective):
|
||||
print("Connected Words Client!", perspective)
|
||||
if self._logonDeferred is not None:
|
||||
self._logonDeferred.callback(self)
|
||||
self.perspective = perspective
|
||||
self.chat.getContactsList()
|
||||
|
||||
|
||||
pbFrontEnds = {"twisted.words": TwistedWordsClient, "twisted.reality": None}
|
||||
|
||||
|
||||
@implementer(interfaces.IAccount)
|
||||
class PBAccount(basesupport.AbstractAccount):
|
||||
gatewayType = "PB"
|
||||
_groupFactory = TwistedWordsGroup
|
||||
_personFactory = TwistedWordsPerson
|
||||
|
||||
def __init__(
|
||||
self, accountName, autoLogin, username, password, host, port, services=None
|
||||
):
|
||||
"""
|
||||
@param username: The name of your PB Identity.
|
||||
@type username: string
|
||||
"""
|
||||
basesupport.AbstractAccount.__init__(
|
||||
self, accountName, autoLogin, username, password, host, port
|
||||
)
|
||||
self.services = []
|
||||
if not services:
|
||||
services = [("twisted.words", "twisted.words", username)]
|
||||
for serviceType, serviceName, perspectiveName in services:
|
||||
self.services.append(
|
||||
[pbFrontEnds[serviceType], serviceName, perspectiveName]
|
||||
)
|
||||
|
||||
def logOn(self, chatui):
|
||||
"""
|
||||
@returns: this breaks with L{interfaces.IAccount}
|
||||
@returntype: DeferredList of L{interfaces.IClient}s
|
||||
"""
|
||||
# Overriding basesupport's implementation on account of the
|
||||
# fact that _startLogOn tends to return a deferredList rather
|
||||
# than a simple Deferred, and we need to do registerAccountClient.
|
||||
if (not self._isConnecting) and (not self._isOnline):
|
||||
self._isConnecting = 1
|
||||
d = self._startLogOn(chatui)
|
||||
d.addErrback(self._loginFailed)
|
||||
|
||||
def registerMany(results):
|
||||
for success, result in results:
|
||||
if success:
|
||||
chatui.registerAccountClient(result)
|
||||
self._cb_logOn(result)
|
||||
else:
|
||||
log.err(result)
|
||||
|
||||
d.addCallback(registerMany)
|
||||
return d
|
||||
else:
|
||||
raise error.ConnectionError("Connection in progress")
|
||||
|
||||
def logOff(self):
|
||||
# IAccount.logOff
|
||||
pass
|
||||
|
||||
def _startLogOn(self, chatui):
|
||||
print("Connecting...", end=" ")
|
||||
d = pb.getObjectAt(self.host, self.port)
|
||||
d.addCallbacks(self._cbConnected, self._ebConnected, callbackArgs=(chatui,))
|
||||
return d
|
||||
|
||||
def _cbConnected(self, root, chatui):
|
||||
print("Connected!")
|
||||
print("Identifying...", end=" ")
|
||||
d = pb.authIdentity(root, self.username, self.password)
|
||||
d.addCallbacks(self._cbIdent, self._ebConnected, callbackArgs=(chatui,))
|
||||
return d
|
||||
|
||||
def _cbIdent(self, ident, chatui):
|
||||
if not ident:
|
||||
print("falsely identified.")
|
||||
return self._ebConnected(
|
||||
Failure(Exception("username or password incorrect"))
|
||||
)
|
||||
print("Identified!")
|
||||
dl = []
|
||||
for handlerClass, sname, pname in self.services:
|
||||
d = defer.Deferred()
|
||||
dl.append(d)
|
||||
handler = handlerClass(self, sname, pname, chatui, d)
|
||||
ident.callRemote("attach", sname, pname, handler).addCallback(
|
||||
handler.connected
|
||||
)
|
||||
return defer.DeferredList(dl)
|
||||
|
||||
def _ebConnected(self, error):
|
||||
print("Not connected.")
|
||||
return error
|
||||
281
backend/lib/python3.12/site-packages/twisted/words/iwords.py
Normal file
281
backend/lib/python3.12/site-packages/twisted/words/iwords.py
Normal file
@@ -0,0 +1,281 @@
|
||||
# -*- test-case-name: twisted.words.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from zope.interface import Attribute, Interface
|
||||
|
||||
|
||||
class IProtocolPlugin(Interface):
|
||||
"""Interface for plugins providing an interface to a Words service"""
|
||||
|
||||
name = Attribute(
|
||||
"A single word describing what kind of interface this is (eg, irc or web)"
|
||||
)
|
||||
|
||||
def getFactory(realm, portal):
|
||||
"""Retrieve a C{twisted.internet.interfaces.IServerFactory} provider
|
||||
|
||||
@param realm: An object providing C{twisted.cred.portal.IRealm} and
|
||||
L{IChatService}, with which service information should be looked up.
|
||||
|
||||
@param portal: An object providing C{twisted.cred.portal.IPortal},
|
||||
through which logins should be performed.
|
||||
"""
|
||||
|
||||
|
||||
class IGroup(Interface):
|
||||
name = Attribute("A short string, unique among groups.")
|
||||
|
||||
def add(user):
|
||||
"""Include the given user in this group.
|
||||
|
||||
@type user: L{IUser}
|
||||
"""
|
||||
|
||||
def remove(user, reason=None):
|
||||
"""Remove the given user from this group.
|
||||
|
||||
@type user: L{IUser}
|
||||
@type reason: C{unicode}
|
||||
"""
|
||||
|
||||
def size():
|
||||
"""Return the number of participants in this group.
|
||||
|
||||
@rtype: L{twisted.internet.defer.Deferred}
|
||||
@return: A Deferred which fires with an C{int} representing the
|
||||
number of participants in this group.
|
||||
"""
|
||||
|
||||
def receive(sender, recipient, message):
|
||||
"""
|
||||
Broadcast the given message from the given sender to other
|
||||
users in group.
|
||||
|
||||
The message is not re-transmitted to the sender.
|
||||
|
||||
@param sender: L{IUser}
|
||||
|
||||
@type recipient: L{IGroup}
|
||||
@param recipient: This is probably a wart. Maybe it will be removed
|
||||
in the future. For now, it should be the group object the message
|
||||
is being delivered to.
|
||||
|
||||
@param message: C{dict}
|
||||
|
||||
@rtype: L{twisted.internet.defer.Deferred}
|
||||
@return: A Deferred which fires with None when delivery has been
|
||||
attempted for all users.
|
||||
"""
|
||||
|
||||
def setMetadata(meta):
|
||||
"""Change the metadata associated with this group.
|
||||
|
||||
@type meta: C{dict}
|
||||
"""
|
||||
|
||||
def iterusers():
|
||||
"""Return an iterator of all users in this group."""
|
||||
|
||||
|
||||
class IChatClient(Interface):
|
||||
"""Interface through which IChatService interacts with clients."""
|
||||
|
||||
name = Attribute(
|
||||
"A short string, unique among users. This will be set by the L{IChatService} at login time."
|
||||
)
|
||||
|
||||
def receive(sender, recipient, message):
|
||||
"""
|
||||
Callback notifying this user of the given message sent by the
|
||||
given user.
|
||||
|
||||
This will be invoked whenever another user sends a message to a
|
||||
group this user is participating in, or whenever another user sends
|
||||
a message directly to this user. In the former case, C{recipient}
|
||||
will be the group to which the message was sent; in the latter, it
|
||||
will be the same object as the user who is receiving the message.
|
||||
|
||||
@type sender: L{IUser}
|
||||
@type recipient: L{IUser} or L{IGroup}
|
||||
@type message: C{dict}
|
||||
|
||||
@rtype: L{twisted.internet.defer.Deferred}
|
||||
@return: A Deferred which fires when the message has been delivered,
|
||||
or which fails in some way. If the Deferred fails and the message
|
||||
was directed at a group, this user will be removed from that group.
|
||||
"""
|
||||
|
||||
def groupMetaUpdate(group, meta):
|
||||
"""
|
||||
Callback notifying this user that the metadata for the given
|
||||
group has changed.
|
||||
|
||||
@type group: L{IGroup}
|
||||
@type meta: C{dict}
|
||||
|
||||
@rtype: L{twisted.internet.defer.Deferred}
|
||||
"""
|
||||
|
||||
def userJoined(group, user):
|
||||
"""
|
||||
Callback notifying this user that the given user has joined
|
||||
the given group.
|
||||
|
||||
@type group: L{IGroup}
|
||||
@type user: L{IUser}
|
||||
|
||||
@rtype: L{twisted.internet.defer.Deferred}
|
||||
"""
|
||||
|
||||
def userLeft(group, user, reason=None):
|
||||
"""
|
||||
Callback notifying this user that the given user has left the
|
||||
given group for the given reason.
|
||||
|
||||
@type group: L{IGroup}
|
||||
@type user: L{IUser}
|
||||
@type reason: C{unicode}
|
||||
|
||||
@rtype: L{twisted.internet.defer.Deferred}
|
||||
"""
|
||||
|
||||
|
||||
class IUser(Interface):
|
||||
"""Interface through which clients interact with IChatService."""
|
||||
|
||||
realm = Attribute(
|
||||
"A reference to the Realm to which this user belongs. Set if and only if the user is logged in."
|
||||
)
|
||||
mind = Attribute(
|
||||
"A reference to the mind which logged in to this user. Set if and only if the user is logged in."
|
||||
)
|
||||
name = Attribute("A short string, unique among users.")
|
||||
|
||||
lastMessage = Attribute(
|
||||
"A POSIX timestamp indicating the time of the last message received from this user."
|
||||
)
|
||||
signOn = Attribute(
|
||||
"A POSIX timestamp indicating this user's most recent sign on time."
|
||||
)
|
||||
|
||||
def loggedIn(realm, mind):
|
||||
"""Invoked by the associated L{IChatService} when login occurs.
|
||||
|
||||
@param realm: The L{IChatService} through which login is occurring.
|
||||
@param mind: The mind object used for cred login.
|
||||
"""
|
||||
|
||||
def send(recipient, message):
|
||||
"""Send the given message to the given user or group.
|
||||
|
||||
@type recipient: Either L{IUser} or L{IGroup}
|
||||
@type message: C{dict}
|
||||
"""
|
||||
|
||||
def join(group):
|
||||
"""Attempt to join the given group.
|
||||
|
||||
@type group: L{IGroup}
|
||||
@rtype: L{twisted.internet.defer.Deferred}
|
||||
"""
|
||||
|
||||
def leave(group):
|
||||
"""Discontinue participation in the given group.
|
||||
|
||||
@type group: L{IGroup}
|
||||
@rtype: L{twisted.internet.defer.Deferred}
|
||||
"""
|
||||
|
||||
def itergroups():
|
||||
"""
|
||||
Return an iterator of all groups of which this user is a
|
||||
member.
|
||||
"""
|
||||
|
||||
|
||||
class IChatService(Interface):
|
||||
name = Attribute("A short string identifying this chat service (eg, a hostname)")
|
||||
|
||||
createGroupOnRequest = Attribute(
|
||||
"A boolean indicating whether L{getGroup} should implicitly "
|
||||
"create groups which are requested but which do not yet exist."
|
||||
)
|
||||
|
||||
createUserOnRequest = Attribute(
|
||||
"A boolean indicating whether L{getUser} should implicitly "
|
||||
"create users which are requested but which do not yet exist."
|
||||
)
|
||||
|
||||
def itergroups():
|
||||
"""Return all groups available on this service.
|
||||
|
||||
@rtype: C{twisted.internet.defer.Deferred}
|
||||
@return: A Deferred which fires with a list of C{IGroup} providers.
|
||||
"""
|
||||
|
||||
def getGroup(name):
|
||||
"""Retrieve the group by the given name.
|
||||
|
||||
@type name: C{str}
|
||||
|
||||
@rtype: L{twisted.internet.defer.Deferred}
|
||||
@return: A Deferred which fires with the group with the given
|
||||
name if one exists (or if one is created due to the setting of
|
||||
L{IChatService.createGroupOnRequest}, or which fails with
|
||||
L{twisted.words.ewords.NoSuchGroup} if no such group exists.
|
||||
"""
|
||||
|
||||
def createGroup(name):
|
||||
"""Create a new group with the given name.
|
||||
|
||||
@type name: C{str}
|
||||
|
||||
@rtype: L{twisted.internet.defer.Deferred}
|
||||
@return: A Deferred which fires with the created group, or
|
||||
with fails with L{twisted.words.ewords.DuplicateGroup} if a
|
||||
group by that name exists already.
|
||||
"""
|
||||
|
||||
def lookupGroup(name):
|
||||
"""Retrieve a group by name.
|
||||
|
||||
Unlike C{getGroup}, this will never implicitly create a group.
|
||||
|
||||
@type name: C{str}
|
||||
|
||||
@rtype: L{twisted.internet.defer.Deferred}
|
||||
@return: A Deferred which fires with the group by the given
|
||||
name, or which fails with L{twisted.words.ewords.NoSuchGroup}.
|
||||
"""
|
||||
|
||||
def getUser(name):
|
||||
"""Retrieve the user by the given name.
|
||||
|
||||
@type name: C{str}
|
||||
|
||||
@rtype: L{twisted.internet.defer.Deferred}
|
||||
@return: A Deferred which fires with the user with the given
|
||||
name if one exists (or if one is created due to the setting of
|
||||
L{IChatService.createUserOnRequest}, or which fails with
|
||||
L{twisted.words.ewords.NoSuchUser} if no such user exists.
|
||||
"""
|
||||
|
||||
def createUser(name):
|
||||
"""Create a new user with the given name.
|
||||
|
||||
@type name: C{str}
|
||||
|
||||
@rtype: L{twisted.internet.defer.Deferred}
|
||||
@return: A Deferred which fires with the created user, or
|
||||
with fails with L{twisted.words.ewords.DuplicateUser} if a
|
||||
user by that name exists already.
|
||||
"""
|
||||
|
||||
|
||||
__all__ = [
|
||||
"IGroup",
|
||||
"IChatClient",
|
||||
"IUser",
|
||||
"IChatService",
|
||||
]
|
||||
1
backend/lib/python3.12/site-packages/twisted/words/newsfragments/.gitignore
vendored
Normal file
1
backend/lib/python3.12/site-packages/twisted/words/newsfragments/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
@@ -0,0 +1,6 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Chat protocols.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
4117
backend/lib/python3.12/site-packages/twisted/words/protocols/irc.py
Normal file
4117
backend/lib/python3.12/site-packages/twisted/words/protocols/irc.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
# -*- test-case-name: twisted.words.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
"""
|
||||
Twisted Jabber: Jabber Protocol Helpers
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,394 @@
|
||||
# -*- test-case-name: twisted.words.test.test_jabberclient -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
from twisted.words.protocols.jabber import error, sasl, xmlstream
|
||||
from twisted.words.protocols.jabber.jid import JID
|
||||
from twisted.words.xish import domish, utility, xpath
|
||||
|
||||
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:
|
||||
"""
|
||||
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 = 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=str(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:
|
||||
"""
|
||||
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(str(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 C{jid} and
|
||||
C{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 L{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 L{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),
|
||||
]
|
||||
@@ -0,0 +1,456 @@
|
||||
# -*- 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.words.protocols.jabber import error, ijabber, jstrports, xmlstream
|
||||
from twisted.words.protocols.jabber.jid import internJID as JID
|
||||
from twisted.words.xish import domish
|
||||
|
||||
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:
|
||||
"""
|
||||
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, xs.authenticator.password)
|
||||
hs.addContent(str(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(str(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, str(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:
|
||||
"""
|
||||
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(f"Routing to {destination.full()}: {stanza.toXml()!r}")
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,323 @@
|
||||
# -*- test-case-name: twisted.words.test.test_jabbererror -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
XMPP Error support.
|
||||
"""
|
||||
|
||||
|
||||
import copy
|
||||
from typing import Optional
|
||||
|
||||
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: Optional[str] = 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) -> str:
|
||||
message = "{} with condition {!r}".format(
|
||||
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 = str(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 = str(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
|
||||
@@ -0,0 +1,188 @@
|
||||
# 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).
|
||||
"""
|
||||
@@ -0,0 +1,259 @@
|
||||
# -*- 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 typing import Dict, Tuple, Union
|
||||
|
||||
from twisted.words.protocols.jabber.xmpp_stringprep import (
|
||||
nameprep,
|
||||
nodeprep,
|
||||
resourceprep,
|
||||
)
|
||||
|
||||
|
||||
class InvalidFormat(Exception):
|
||||
"""
|
||||
The given string could not be parsed into a valid Jabber Identifier (JID).
|
||||
"""
|
||||
|
||||
|
||||
def parse(jidstring: str) -> Tuple[Union[str, None], str, Union[str, None]]:
|
||||
"""
|
||||
Parse given JID string into its respective parts and apply stringprep.
|
||||
|
||||
@param jidstring: string representation of a JID.
|
||||
@type jidstring: L{str}
|
||||
@return: tuple of (user, host, resource), each of type L{str} 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: Union[str, None], host: str, resource: Union[str, None]
|
||||
) -> Tuple[Union[str, None], str, Union[str, None]]:
|
||||
"""
|
||||
Perform stringprep on all JID fragments.
|
||||
|
||||
@param user: The user part of the JID.
|
||||
@type user: L{str}
|
||||
@param host: The host part of the JID.
|
||||
@type host: L{str}
|
||||
@param resource: The resource part of the JID.
|
||||
@type resource: L{str}
|
||||
@return: The given parts with stringprep applied.
|
||||
@rtype: L{tuple}
|
||||
"""
|
||||
|
||||
if user:
|
||||
try:
|
||||
user = nodeprep.prepare(str(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(str(host))
|
||||
except UnicodeError:
|
||||
raise InvalidFormat("Invalid character in hostname")
|
||||
|
||||
if resource:
|
||||
try:
|
||||
resource = resourceprep.prepare(str(resource))
|
||||
except UnicodeError:
|
||||
raise InvalidFormat("Invalid character in resource")
|
||||
else:
|
||||
resource = None
|
||||
|
||||
return (user, host, resource)
|
||||
|
||||
|
||||
__internJIDs: Dict[str, "JID"] = {}
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
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: Union[str, None] = None,
|
||||
tuple: Union[Tuple[Union[str, None], str, Union[str, None]], None] = None,
|
||||
):
|
||||
if str:
|
||||
user, host, res = parse(str)
|
||||
elif tuple:
|
||||
user, host, res = prep(*tuple)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"You must provide a value for either 'str' or 'tuple' arguments."
|
||||
)
|
||||
|
||||
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{str}
|
||||
"""
|
||||
if self.user:
|
||||
return f"{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{str}
|
||||
"""
|
||||
if self.user:
|
||||
if self.resource:
|
||||
return f"{self.user}@{self.host}/{self.resource}"
|
||||
else:
|
||||
return f"{self.user}@{self.host}"
|
||||
else:
|
||||
if self.resource:
|
||||
return f"{self.host}/{self.resource}"
|
||||
else:
|
||||
return self.host
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""
|
||||
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 __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()
|
||||
|
||||
__str__ = __unicode__
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Get object representation.
|
||||
|
||||
Returns a string that would create a new JID object that compares equal
|
||||
to this one.
|
||||
"""
|
||||
return "JID(%r)" % self.full()
|
||||
@@ -0,0 +1,34 @@
|
||||
# -*- 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 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)
|
||||
@@ -0,0 +1,229 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
XMPP-specific SASL profile.
|
||||
"""
|
||||
|
||||
|
||||
import re
|
||||
from base64 import b64decode, b64encode
|
||||
|
||||
from twisted.internet import defer
|
||||
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(str(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) -> str:
|
||||
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 "=")
|
||||
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(str(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))
|
||||
@@ -0,0 +1,307 @@
|
||||
# -*- test-case-name: twisted.words.test.test_jabbersaslmechanisms -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Protocol agnostic implementations of SASL authentication mechanisms.
|
||||
"""
|
||||
|
||||
|
||||
import binascii
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
from hashlib import md5
|
||||
|
||||
from zope.interface import Attribute, Interface, implementer
|
||||
|
||||
from twisted.python.compat import 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:
|
||||
"""
|
||||
Implements the ANONYMOUS SASL authentication mechanism.
|
||||
|
||||
This mechanism is defined in RFC 2245.
|
||||
"""
|
||||
|
||||
name = "ANONYMOUS"
|
||||
|
||||
def getInitialResponse(self):
|
||||
return None
|
||||
|
||||
def getResponse(self, challenge):
|
||||
# ISASLMechanism.getResponse
|
||||
pass
|
||||
|
||||
|
||||
@implementer(ISASLMechanism)
|
||||
class Plain:
|
||||
"""
|
||||
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 ""
|
||||
self.authcid = authcid or ""
|
||||
self.password = password or ""
|
||||
|
||||
def getInitialResponse(self):
|
||||
return (
|
||||
self.authzid.encode("utf-8")
|
||||
+ b"\x00"
|
||||
+ self.authcid.encode("utf-8")
|
||||
+ b"\x00"
|
||||
+ self.password.encode("utf-8")
|
||||
)
|
||||
|
||||
def getResponse(self, challenge):
|
||||
# ISASLMechanism.getResponse
|
||||
pass
|
||||
|
||||
|
||||
@implementer(ISASLMechanism)
|
||||
class DigestMD5:
|
||||
"""
|
||||
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 password: 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 = f"{serv_type}/{host}"
|
||||
if serv_name is not None:
|
||||
self.digest_uri += f"/{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 directives.items():
|
||||
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(f"{1:08x}") # 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")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,257 @@
|
||||
# -*- test-case-name: twisted.words.test.test_jabberxmppstringprep -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
import stringprep
|
||||
from encodings import idna
|
||||
from itertools import chain
|
||||
|
||||
# We require Unicode version 3.2.
|
||||
from unicodedata import ucd_3_2_0 as unicodedata
|
||||
|
||||
from zope.interface import Interface, implementer
|
||||
|
||||
from incremental import Version
|
||||
|
||||
from twisted.python.deprecate import deprecatedModuleAttribute
|
||||
|
||||
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 "".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 = [
|
||||
chr(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 = "."
|
||||
del labels[-1]
|
||||
else:
|
||||
trailing_dot = ""
|
||||
|
||||
for label in labels:
|
||||
result.append(self.nameprep(label))
|
||||
|
||||
return ".".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] == "-":
|
||||
raise UnicodeError("Invalid leading hyphen-minus")
|
||||
if label[-1] == "-":
|
||||
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(['"', "&", "'", "/", ":", "<", ">", "@"]),
|
||||
],
|
||||
)
|
||||
|
||||
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()
|
||||
1276
backend/lib/python3.12/site-packages/twisted/words/service.py
Normal file
1276
backend/lib/python3.12/site-packages/twisted/words/service.py
Normal file
File diff suppressed because it is too large
Load Diff
89
backend/lib/python3.12/site-packages/twisted/words/tap.py
Normal file
89
backend/lib/python3.12/site-packages/twisted/words/tap.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# -*- test-case-name: twisted.words.test.test_tap -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
"""
|
||||
Shiny new words service maker
|
||||
"""
|
||||
|
||||
import socket
|
||||
import sys
|
||||
from typing import List, Optional, Sequence
|
||||
|
||||
from twisted import plugin
|
||||
from twisted.application import strports
|
||||
from twisted.application.service import MultiService
|
||||
from twisted.cred import checkers, credentials, portal, strcred
|
||||
from twisted.python import usage
|
||||
from twisted.words import iwords, service
|
||||
|
||||
|
||||
class Options(usage.Options, strcred.AuthOptionMixin):
|
||||
supportedInterfaces = [credentials.IUsernamePassword]
|
||||
optParameters: List[Sequence[Optional[str]]] = [
|
||||
(
|
||||
"hostname",
|
||||
None,
|
||||
socket.gethostname(),
|
||||
"Name of this server; purely an informative",
|
||||
)
|
||||
]
|
||||
|
||||
compData = usage.Completions(multiUse=["group"])
|
||||
|
||||
interfacePlugins = {}
|
||||
plg = None
|
||||
for plg in plugin.getPlugins(iwords.IProtocolPlugin):
|
||||
assert plg.name not in interfacePlugins
|
||||
interfacePlugins[plg.name] = plg
|
||||
optParameters.append(
|
||||
(
|
||||
plg.name + "-port",
|
||||
None,
|
||||
None,
|
||||
"strports description of the port to bind for the "
|
||||
+ plg.name
|
||||
+ " server",
|
||||
)
|
||||
)
|
||||
del plg
|
||||
|
||||
def __init__(self, *a, **kw):
|
||||
usage.Options.__init__(self, *a, **kw)
|
||||
self["groups"] = []
|
||||
|
||||
def opt_group(self, name):
|
||||
"""Specify a group which should exist"""
|
||||
self["groups"].append(name.decode(sys.stdin.encoding))
|
||||
|
||||
def opt_passwd(self, filename):
|
||||
"""
|
||||
Name of a passwd-style file. (This is for
|
||||
backwards-compatibility only; you should use the --auth
|
||||
command instead.)
|
||||
"""
|
||||
self.addChecker(checkers.FilePasswordDB(filename))
|
||||
|
||||
|
||||
def makeService(config):
|
||||
credCheckers = config.get("credCheckers", [])
|
||||
wordsRealm = service.InMemoryWordsRealm(config["hostname"])
|
||||
wordsPortal = portal.Portal(wordsRealm, credCheckers)
|
||||
|
||||
msvc = MultiService()
|
||||
|
||||
# XXX Attribute lookup on config is kind of bad - hrm.
|
||||
for plgName in config.interfacePlugins:
|
||||
port = config.get(plgName + "-port")
|
||||
if port is not None:
|
||||
factory = config.interfacePlugins[plgName].getFactory(
|
||||
wordsRealm, wordsPortal
|
||||
)
|
||||
svc = strports.service(port, factory)
|
||||
svc.setServiceParent(msvc)
|
||||
|
||||
# This is bogus. createGroup is async. makeService must be
|
||||
# allowed to return a Deferred or some crap.
|
||||
for g in config["groups"]:
|
||||
wordsRealm.createGroup(g)
|
||||
|
||||
return msvc
|
||||
@@ -0,0 +1 @@
|
||||
"Words tests"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,67 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.words.im.basechat}.
|
||||
"""
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.words.im import basechat, basesupport
|
||||
|
||||
|
||||
class ChatUITests(unittest.TestCase):
|
||||
"""
|
||||
Tests for the L{basechat.ChatUI} chat client.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.ui = basechat.ChatUI()
|
||||
self.account = basesupport.AbstractAccount(
|
||||
"fooAccount", False, "foo", "password", "host", "port"
|
||||
)
|
||||
self.person = basesupport.AbstractPerson("foo", self.account)
|
||||
|
||||
def test_contactChangedNickNoKey(self) -> None:
|
||||
"""
|
||||
L{basechat.ChatUI.contactChangedNick} on an
|
||||
L{twisted.words.im.interfaces.IPerson} who doesn't have an account
|
||||
associated with the L{basechat.ChatUI} instance has no effect.
|
||||
"""
|
||||
self.assertEqual(self.person.name, "foo")
|
||||
self.assertEqual(self.person.account, self.account)
|
||||
|
||||
self.ui.contactChangedNick(self.person, "bar")
|
||||
self.assertEqual(self.person.name, "foo")
|
||||
self.assertEqual(self.person.account, self.account)
|
||||
|
||||
def test_contactChangedNickNoConversation(self) -> None:
|
||||
"""
|
||||
L{basechat.ChatUI.contactChangedNick} changes the name for an
|
||||
L{twisted.words.im.interfaces.IPerson}.
|
||||
"""
|
||||
self.ui.persons[self.person.name, self.person.account] = self.person
|
||||
|
||||
self.assertEqual(self.person.name, "foo")
|
||||
self.assertEqual(self.person.account, self.account)
|
||||
|
||||
self.ui.contactChangedNick(self.person, "bar")
|
||||
self.assertEqual(self.person.name, "bar")
|
||||
self.assertEqual(self.person.account, self.account)
|
||||
|
||||
def test_contactChangedNickHasConversation(self) -> None:
|
||||
"""
|
||||
If an L{twisted.words.im.interfaces.IPerson} is in a
|
||||
L{basechat.Conversation}, L{basechat.ChatUI.contactChangedNick} causes a
|
||||
name change for that person in both the L{basechat.Conversation} and the
|
||||
L{basechat.ChatUI}.
|
||||
"""
|
||||
self.ui.persons[self.person.name, self.person.account] = self.person
|
||||
conversation = basechat.Conversation(self.person, self.ui)
|
||||
self.ui.conversations[self.person] = conversation
|
||||
|
||||
self.assertEqual(self.person.name, "foo")
|
||||
self.assertEqual(self.person.account, self.account)
|
||||
|
||||
self.ui.contactChangedNick(self.person, "bar")
|
||||
self.assertEqual(self.person.name, "bar")
|
||||
self.assertEqual(self.person.account, self.account)
|
||||
@@ -0,0 +1,100 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from twisted.internet import defer, error
|
||||
from twisted.trial import unittest
|
||||
from twisted.words.im import basesupport
|
||||
|
||||
|
||||
class DummyAccount(basesupport.AbstractAccount):
|
||||
"""
|
||||
An account object that will do nothing when asked to start to log on.
|
||||
"""
|
||||
|
||||
loginHasFailed = False
|
||||
loginCallbackCalled = False
|
||||
|
||||
def _startLogOn(self, *args):
|
||||
"""
|
||||
Set self.loginDeferred to the same as the deferred returned, allowing a
|
||||
testcase to .callback or .errback.
|
||||
|
||||
@return: A deferred.
|
||||
"""
|
||||
self.loginDeferred = defer.Deferred()
|
||||
return self.loginDeferred
|
||||
|
||||
def _loginFailed(self, result):
|
||||
self.loginHasFailed = True
|
||||
return basesupport.AbstractAccount._loginFailed(self, result)
|
||||
|
||||
def _cb_logOn(self, result):
|
||||
self.loginCallbackCalled = True
|
||||
return basesupport.AbstractAccount._cb_logOn(self, result)
|
||||
|
||||
|
||||
class DummyUI:
|
||||
"""
|
||||
Provide just the interface required to be passed to AbstractAccount.logOn.
|
||||
"""
|
||||
|
||||
clientRegistered = False
|
||||
|
||||
def registerAccountClient(self, result):
|
||||
self.clientRegistered = True
|
||||
|
||||
|
||||
class ClientMsgTests(unittest.TestCase):
|
||||
def makeUI(self):
|
||||
return DummyUI()
|
||||
|
||||
def makeAccount(self):
|
||||
return DummyAccount("la", False, "la", None, "localhost", 6667)
|
||||
|
||||
def test_connect(self):
|
||||
"""
|
||||
Test that account.logOn works, and it calls the right callback when a
|
||||
connection is established.
|
||||
"""
|
||||
account = self.makeAccount()
|
||||
ui = self.makeUI()
|
||||
d = account.logOn(ui)
|
||||
account.loginDeferred.callback(None)
|
||||
|
||||
def check(result):
|
||||
self.assertFalse(account.loginHasFailed, "Login shouldn't have failed")
|
||||
self.assertTrue(account.loginCallbackCalled, "We should be logged in")
|
||||
|
||||
d.addCallback(check)
|
||||
return d
|
||||
|
||||
def test_failedConnect(self):
|
||||
"""
|
||||
Test that account.logOn works, and it calls the right callback when a
|
||||
connection is established.
|
||||
"""
|
||||
account = self.makeAccount()
|
||||
ui = self.makeUI()
|
||||
d = account.logOn(ui)
|
||||
account.loginDeferred.errback(Exception())
|
||||
|
||||
def err(reason):
|
||||
self.assertTrue(account.loginHasFailed, "Login should have failed")
|
||||
self.assertFalse(account.loginCallbackCalled, "We shouldn't be logged in")
|
||||
self.assertTrue(
|
||||
not ui.clientRegistered, "Client shouldn't be registered in the UI"
|
||||
)
|
||||
|
||||
cb = lambda r: self.assertTrue(False, "Shouldn't get called back")
|
||||
d.addCallbacks(cb, err)
|
||||
return d
|
||||
|
||||
def test_alreadyConnecting(self):
|
||||
"""
|
||||
Test that it can fail sensibly when someone tried to connect before
|
||||
we did.
|
||||
"""
|
||||
account = self.makeAccount()
|
||||
ui = self.makeUI()
|
||||
account.logOn(ui)
|
||||
self.assertRaises(error.ConnectError, account.logOn, ui)
|
||||
@@ -0,0 +1,551 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.words.xish.domish}, a DOM-like library for XMPP.
|
||||
"""
|
||||
|
||||
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.trial import unittest
|
||||
from twisted.words.xish import domish
|
||||
|
||||
|
||||
class ElementTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{domish.Element}.
|
||||
"""
|
||||
|
||||
def test_interface(self):
|
||||
"""
|
||||
L{domish.Element} implements L{domish.IElement}.
|
||||
"""
|
||||
verifyObject(domish.IElement, domish.Element((None, "foo")))
|
||||
|
||||
def test_escaping(self):
|
||||
"""
|
||||
The built-in entity references are properly encoded.
|
||||
"""
|
||||
s = "&<>'\""
|
||||
self.assertEqual(domish.escapeToXml(s), "&<>'\"")
|
||||
self.assertEqual(domish.escapeToXml(s, 1), "&<>'"")
|
||||
|
||||
def test_namespace(self):
|
||||
"""
|
||||
An attribute on L{domish.Namespace} yields a qualified name.
|
||||
"""
|
||||
ns = domish.Namespace("testns")
|
||||
self.assertEqual(ns.foo, ("testns", "foo"))
|
||||
|
||||
def test_elementInit(self):
|
||||
"""
|
||||
Basic L{domish.Element} initialization tests.
|
||||
"""
|
||||
e = domish.Element((None, "foo"))
|
||||
self.assertEqual(e.name, "foo")
|
||||
self.assertEqual(e.uri, None)
|
||||
self.assertEqual(e.defaultUri, None)
|
||||
self.assertEqual(e.parent, None)
|
||||
|
||||
e = domish.Element(("", "foo"))
|
||||
self.assertEqual(e.name, "foo")
|
||||
self.assertEqual(e.uri, "")
|
||||
self.assertEqual(e.defaultUri, "")
|
||||
self.assertEqual(e.parent, None)
|
||||
|
||||
e = domish.Element(("testns", "foo"))
|
||||
self.assertEqual(e.name, "foo")
|
||||
self.assertEqual(e.uri, "testns")
|
||||
self.assertEqual(e.defaultUri, "testns")
|
||||
self.assertEqual(e.parent, None)
|
||||
|
||||
e = domish.Element(("testns", "foo"), "test2ns")
|
||||
self.assertEqual(e.name, "foo")
|
||||
self.assertEqual(e.uri, "testns")
|
||||
self.assertEqual(e.defaultUri, "test2ns")
|
||||
|
||||
def test_childOps(self):
|
||||
"""
|
||||
Basic L{domish.Element} child tests.
|
||||
"""
|
||||
e = domish.Element(("testns", "foo"))
|
||||
e.addContent("somecontent")
|
||||
b2 = e.addElement(("testns2", "bar2"))
|
||||
e["attrib1"] = "value1"
|
||||
e[("testns2", "attrib2")] = "value2"
|
||||
e.addElement("bar")
|
||||
e.addElement("bar")
|
||||
e.addContent("abc")
|
||||
e.addContent("123")
|
||||
|
||||
# Check content merging
|
||||
self.assertEqual(e.children[-1], "abc123")
|
||||
|
||||
# Check direct child accessor
|
||||
self.assertEqual(e.bar2, b2)
|
||||
e.bar2.addContent("subcontent")
|
||||
e.bar2["bar2value"] = "somevalue"
|
||||
|
||||
# Check child ops
|
||||
self.assertEqual(e.children[1], e.bar2)
|
||||
self.assertEqual(e.children[2], e.bar)
|
||||
|
||||
# Check attribute ops
|
||||
self.assertEqual(e["attrib1"], "value1")
|
||||
del e["attrib1"]
|
||||
self.assertEqual(e.hasAttribute("attrib1"), 0)
|
||||
self.assertEqual(e.hasAttribute("attrib2"), 0)
|
||||
self.assertEqual(e[("testns2", "attrib2")], "value2")
|
||||
|
||||
def test_characterData(self):
|
||||
"""
|
||||
Extract character data using L{str}.
|
||||
"""
|
||||
element = domish.Element(("testns", "foo"))
|
||||
element.addContent("somecontent")
|
||||
|
||||
text = str(element)
|
||||
self.assertEqual("somecontent", text)
|
||||
self.assertIsInstance(text, str)
|
||||
|
||||
def test_characterDataNativeString(self):
|
||||
"""
|
||||
Extract ascii character data using L{str}.
|
||||
"""
|
||||
element = domish.Element(("testns", "foo"))
|
||||
element.addContent("somecontent")
|
||||
|
||||
text = str(element)
|
||||
self.assertEqual("somecontent", text)
|
||||
self.assertIsInstance(text, str)
|
||||
|
||||
def test_characterDataUnicode(self):
|
||||
"""
|
||||
Extract character data using L{str}.
|
||||
"""
|
||||
element = domish.Element(("testns", "foo"))
|
||||
element.addContent("\N{SNOWMAN}")
|
||||
|
||||
text = str(element)
|
||||
self.assertEqual("\N{SNOWMAN}", text)
|
||||
self.assertIsInstance(text, str)
|
||||
|
||||
def test_characterDataBytes(self):
|
||||
"""
|
||||
Extract character data as UTF-8 using L{bytes}.
|
||||
"""
|
||||
element = domish.Element(("testns", "foo"))
|
||||
element.addContent("\N{SNOWMAN}")
|
||||
|
||||
text = bytes(element)
|
||||
self.assertEqual("\N{SNOWMAN}".encode(), text)
|
||||
self.assertIsInstance(text, bytes)
|
||||
|
||||
def test_characterDataMixed(self):
|
||||
"""
|
||||
Mixing addChild with cdata and element, the first cdata is returned.
|
||||
"""
|
||||
element = domish.Element(("testns", "foo"))
|
||||
element.addChild("abc")
|
||||
element.addElement("bar")
|
||||
element.addChild("def")
|
||||
self.assertEqual("abc", str(element))
|
||||
|
||||
def test_addContent(self):
|
||||
"""
|
||||
Unicode strings passed to C{addContent} become the character data.
|
||||
"""
|
||||
element = domish.Element(("testns", "foo"))
|
||||
element.addContent("unicode")
|
||||
self.assertEqual("unicode", str(element))
|
||||
|
||||
def test_addContentNativeStringASCII(self):
|
||||
"""
|
||||
ASCII native strings passed to C{addContent} become the character data.
|
||||
"""
|
||||
element = domish.Element(("testns", "foo"))
|
||||
element.addContent("native")
|
||||
self.assertEqual("native", str(element))
|
||||
|
||||
def test_addContentBytes(self):
|
||||
"""
|
||||
Byte strings passed to C{addContent} are not acceptable on Python 3.
|
||||
"""
|
||||
element = domish.Element(("testns", "foo"))
|
||||
self.assertRaises(TypeError, element.addContent, b"bytes")
|
||||
|
||||
def test_addElementContent(self):
|
||||
"""
|
||||
Content passed to addElement becomes character data on the new child.
|
||||
"""
|
||||
element = domish.Element(("testns", "foo"))
|
||||
child = element.addElement("bar", content="abc")
|
||||
self.assertEqual("abc", str(child))
|
||||
|
||||
def test_elements(self):
|
||||
"""
|
||||
Calling C{elements} without arguments on a L{domish.Element} returns
|
||||
all child elements, whatever the qualified name.
|
||||
"""
|
||||
e = domish.Element(("testns", "foo"))
|
||||
c1 = e.addElement("name")
|
||||
c2 = e.addElement(("testns2", "baz"))
|
||||
c3 = e.addElement("quux")
|
||||
c4 = e.addElement(("testns", "name"))
|
||||
|
||||
elts = list(e.elements())
|
||||
|
||||
self.assertIn(c1, elts)
|
||||
self.assertIn(c2, elts)
|
||||
self.assertIn(c3, elts)
|
||||
self.assertIn(c4, elts)
|
||||
|
||||
def test_elementsWithQN(self):
|
||||
"""
|
||||
Calling C{elements} with a namespace and local name on a
|
||||
L{domish.Element} returns all child elements with that qualified name.
|
||||
"""
|
||||
e = domish.Element(("testns", "foo"))
|
||||
c1 = e.addElement("name")
|
||||
c2 = e.addElement(("testns2", "baz"))
|
||||
c3 = e.addElement("quux")
|
||||
c4 = e.addElement(("testns", "name"))
|
||||
|
||||
elts = list(e.elements("testns", "name"))
|
||||
|
||||
self.assertIn(c1, elts)
|
||||
self.assertNotIn(c2, elts)
|
||||
self.assertNotIn(c3, elts)
|
||||
self.assertIn(c4, elts)
|
||||
|
||||
|
||||
class DomishStreamTestsMixin:
|
||||
"""
|
||||
Mixin defining tests for different stream implementations.
|
||||
|
||||
@ivar streamClass: A no-argument callable which will be used to create an
|
||||
XML parser which can produce a stream of elements from incremental
|
||||
input.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.doc_started = False
|
||||
self.doc_ended = False
|
||||
self.root = None
|
||||
self.elements = []
|
||||
self.stream = self.streamClass()
|
||||
self.stream.DocumentStartEvent = self._docStarted
|
||||
self.stream.ElementEvent = self.elements.append
|
||||
self.stream.DocumentEndEvent = self._docEnded
|
||||
|
||||
def _docStarted(self, root):
|
||||
self.root = root
|
||||
self.doc_started = True
|
||||
|
||||
def _docEnded(self):
|
||||
self.doc_ended = True
|
||||
|
||||
def doTest(self, xml):
|
||||
self.stream.parse(xml)
|
||||
|
||||
def testHarness(self):
|
||||
xml = b"<root><child/><child2/></root>"
|
||||
self.stream.parse(xml)
|
||||
self.assertEqual(self.doc_started, True)
|
||||
self.assertEqual(self.root.name, "root")
|
||||
self.assertEqual(self.elements[0].name, "child")
|
||||
self.assertEqual(self.elements[1].name, "child2")
|
||||
self.assertEqual(self.doc_ended, True)
|
||||
|
||||
def testBasic(self):
|
||||
xml = (
|
||||
b"<stream:stream xmlns:stream='etherx' xmlns='jabber'>\n"
|
||||
+ b" <message to='bar'>"
|
||||
+ b" <x xmlns='xdelay'>some&data></x>"
|
||||
+ b" </message>"
|
||||
+ b"</stream:stream>"
|
||||
)
|
||||
|
||||
self.stream.parse(xml)
|
||||
self.assertEqual(self.root.name, "stream")
|
||||
self.assertEqual(self.root.uri, "etherx")
|
||||
self.assertEqual(self.elements[0].name, "message")
|
||||
self.assertEqual(self.elements[0].uri, "jabber")
|
||||
self.assertEqual(self.elements[0]["to"], "bar")
|
||||
self.assertEqual(self.elements[0].x.uri, "xdelay")
|
||||
self.assertEqual(str(self.elements[0].x), "some&data>")
|
||||
|
||||
def testNoRootNS(self):
|
||||
xml = b"<stream><error xmlns='etherx'/></stream>"
|
||||
|
||||
self.stream.parse(xml)
|
||||
self.assertEqual(self.root.uri, "")
|
||||
self.assertEqual(self.elements[0].uri, "etherx")
|
||||
|
||||
def testNoDefaultNS(self):
|
||||
xml = b"<stream:stream xmlns:stream='etherx'><error/></stream:stream>"
|
||||
|
||||
self.stream.parse(xml)
|
||||
self.assertEqual(self.root.uri, "etherx")
|
||||
self.assertEqual(self.root.defaultUri, "")
|
||||
self.assertEqual(self.elements[0].uri, "")
|
||||
self.assertEqual(self.elements[0].defaultUri, "")
|
||||
|
||||
def testChildDefaultNS(self):
|
||||
xml = b"<root xmlns='testns'><child/></root>"
|
||||
|
||||
self.stream.parse(xml)
|
||||
self.assertEqual(self.root.uri, "testns")
|
||||
self.assertEqual(self.elements[0].uri, "testns")
|
||||
|
||||
def testEmptyChildNS(self):
|
||||
xml = b"""<root xmlns='testns'>
|
||||
<child1><child2 xmlns=''/></child1>
|
||||
</root>"""
|
||||
|
||||
self.stream.parse(xml)
|
||||
self.assertEqual(self.elements[0].child2.uri, "")
|
||||
|
||||
def test_attributesWithNamespaces(self):
|
||||
"""
|
||||
Attributes with namespace are parsed without Exception.
|
||||
(https://twistedmatrix.com/trac/ticket/9730 regression test)
|
||||
"""
|
||||
|
||||
xml = b"""<root xmlns:test='http://example.org' xml:lang='en'>
|
||||
<test:test>test</test:test>
|
||||
</root>"""
|
||||
|
||||
# with Python 3.8 and without #9730 fix, the following error would
|
||||
# happen at next line:
|
||||
# ``RuntimeError: dictionary keys changed during iteration``
|
||||
self.stream.parse(xml)
|
||||
self.assertEqual(self.elements[0].uri, "http://example.org")
|
||||
|
||||
def testChildPrefix(self):
|
||||
xml = b"<root xmlns='testns' xmlns:foo='testns2'><foo:child/></root>"
|
||||
|
||||
self.stream.parse(xml)
|
||||
self.assertEqual(self.root.localPrefixes["foo"], "testns2")
|
||||
self.assertEqual(self.elements[0].uri, "testns2")
|
||||
|
||||
def testUnclosedElement(self):
|
||||
self.assertRaises(
|
||||
domish.ParserError, self.stream.parse, b"<root><error></root>"
|
||||
)
|
||||
|
||||
def test_namespaceReuse(self):
|
||||
"""
|
||||
Test that reuse of namespaces does affect an element's serialization.
|
||||
|
||||
When one element uses a prefix for a certain namespace, this is
|
||||
stored in the C{localPrefixes} attribute of the element. We want
|
||||
to make sure that elements created after such use, won't have this
|
||||
prefix end up in their C{localPrefixes} attribute, too.
|
||||
"""
|
||||
|
||||
xml = b"""<root>
|
||||
<foo:child1 xmlns:foo='testns'/>
|
||||
<child2 xmlns='testns'/>
|
||||
</root>"""
|
||||
|
||||
self.stream.parse(xml)
|
||||
self.assertEqual("child1", self.elements[0].name)
|
||||
self.assertEqual("testns", self.elements[0].uri)
|
||||
self.assertEqual("", self.elements[0].defaultUri)
|
||||
self.assertEqual({"foo": "testns"}, self.elements[0].localPrefixes)
|
||||
self.assertEqual("child2", self.elements[1].name)
|
||||
self.assertEqual("testns", self.elements[1].uri)
|
||||
self.assertEqual("testns", self.elements[1].defaultUri)
|
||||
self.assertEqual({}, self.elements[1].localPrefixes)
|
||||
|
||||
|
||||
class DomishExpatStreamTests(DomishStreamTestsMixin, unittest.TestCase):
|
||||
"""
|
||||
Tests for L{domish.ExpatElementStream}, the expat-based element stream
|
||||
implementation.
|
||||
"""
|
||||
|
||||
streamClass = domish.ExpatElementStream
|
||||
|
||||
if requireModule("pyexpat", default=None) is None:
|
||||
skip = "pyexpat is required for ExpatElementStream tests."
|
||||
|
||||
|
||||
class DomishSuxStreamTests(DomishStreamTestsMixin, unittest.TestCase):
|
||||
"""
|
||||
Tests for L{domish.SuxElementStream}, the L{twisted.web.sux}-based element
|
||||
stream implementation.
|
||||
"""
|
||||
|
||||
streamClass = domish.SuxElementStream
|
||||
|
||||
|
||||
class SerializerTests(unittest.TestCase):
|
||||
def testNoNamespace(self):
|
||||
e = domish.Element((None, "foo"))
|
||||
self.assertEqual(e.toXml(), "<foo/>")
|
||||
self.assertEqual(e.toXml(closeElement=0), "<foo>")
|
||||
|
||||
def testDefaultNamespace(self):
|
||||
e = domish.Element(("testns", "foo"))
|
||||
self.assertEqual(e.toXml(), "<foo xmlns='testns'/>")
|
||||
|
||||
def testOtherNamespace(self):
|
||||
e = domish.Element(("testns", "foo"), "testns2")
|
||||
self.assertEqual(
|
||||
e.toXml({"testns": "bar"}), "<bar:foo xmlns:bar='testns' xmlns='testns2'/>"
|
||||
)
|
||||
|
||||
def testChildDefaultNamespace(self):
|
||||
e = domish.Element(("testns", "foo"))
|
||||
e.addElement("bar")
|
||||
self.assertEqual(e.toXml(), "<foo xmlns='testns'><bar/></foo>")
|
||||
|
||||
def testChildSameNamespace(self):
|
||||
e = domish.Element(("testns", "foo"))
|
||||
e.addElement(("testns", "bar"))
|
||||
self.assertEqual(e.toXml(), "<foo xmlns='testns'><bar/></foo>")
|
||||
|
||||
def testChildSameDefaultNamespace(self):
|
||||
e = domish.Element(("testns", "foo"))
|
||||
e.addElement("bar", "testns")
|
||||
self.assertEqual(e.toXml(), "<foo xmlns='testns'><bar/></foo>")
|
||||
|
||||
def testChildOtherDefaultNamespace(self):
|
||||
e = domish.Element(("testns", "foo"))
|
||||
e.addElement(("testns2", "bar"), "testns2")
|
||||
self.assertEqual(e.toXml(), "<foo xmlns='testns'><bar xmlns='testns2'/></foo>")
|
||||
|
||||
def testOnlyChildDefaultNamespace(self):
|
||||
e = domish.Element((None, "foo"))
|
||||
e.addElement(("ns2", "bar"), "ns2")
|
||||
self.assertEqual(e.toXml(), "<foo><bar xmlns='ns2'/></foo>")
|
||||
|
||||
def testOnlyChildDefaultNamespace2(self):
|
||||
e = domish.Element((None, "foo"))
|
||||
e.addElement("bar")
|
||||
self.assertEqual(e.toXml(), "<foo><bar/></foo>")
|
||||
|
||||
def testChildInDefaultNamespace(self):
|
||||
e = domish.Element(("testns", "foo"), "testns2")
|
||||
e.addElement(("testns2", "bar"))
|
||||
self.assertEqual(
|
||||
e.toXml(), "<xn0:foo xmlns:xn0='testns' xmlns='testns2'><bar/></xn0:foo>"
|
||||
)
|
||||
|
||||
def testQualifiedAttribute(self):
|
||||
e = domish.Element((None, "foo"), attribs={("testns2", "bar"): "baz"})
|
||||
self.assertEqual(e.toXml(), "<foo xmlns:xn0='testns2' xn0:bar='baz'/>")
|
||||
|
||||
def testQualifiedAttributeDefaultNS(self):
|
||||
e = domish.Element(("testns", "foo"), attribs={("testns", "bar"): "baz"})
|
||||
self.assertEqual(
|
||||
e.toXml(), "<foo xmlns='testns' xmlns:xn0='testns' xn0:bar='baz'/>"
|
||||
)
|
||||
|
||||
def testTwoChilds(self):
|
||||
e = domish.Element(("", "foo"))
|
||||
child1 = e.addElement(("testns", "bar"), "testns2")
|
||||
child1.addElement(("testns2", "quux"))
|
||||
child2 = e.addElement(("testns3", "baz"), "testns4")
|
||||
child2.addElement(("testns", "quux"))
|
||||
self.assertEqual(
|
||||
e.toXml(),
|
||||
"<foo><xn0:bar xmlns:xn0='testns' xmlns='testns2'><quux/></xn0:bar><xn1:baz xmlns:xn1='testns3' xmlns='testns4'><xn0:quux xmlns:xn0='testns'/></xn1:baz></foo>",
|
||||
)
|
||||
|
||||
def testXMLNamespace(self):
|
||||
e = domish.Element(
|
||||
(None, "foo"),
|
||||
attribs={("http://www.w3.org/XML/1998/namespace", "lang"): "en_US"},
|
||||
)
|
||||
self.assertEqual(e.toXml(), "<foo xml:lang='en_US'/>")
|
||||
|
||||
def testQualifiedAttributeGivenListOfPrefixes(self):
|
||||
e = domish.Element((None, "foo"), attribs={("testns2", "bar"): "baz"})
|
||||
self.assertEqual(
|
||||
e.toXml({"testns2": "qux"}), "<foo xmlns:qux='testns2' qux:bar='baz'/>"
|
||||
)
|
||||
|
||||
def testNSPrefix(self):
|
||||
e = domish.Element((None, "foo"), attribs={("testns2", "bar"): "baz"})
|
||||
c = e.addElement(("testns2", "qux"))
|
||||
c[("testns2", "bar")] = "quux"
|
||||
|
||||
self.assertEqual(
|
||||
e.toXml(),
|
||||
"<foo xmlns:xn0='testns2' xn0:bar='baz'><xn0:qux xn0:bar='quux'/></foo>",
|
||||
)
|
||||
|
||||
def testDefaultNSPrefix(self):
|
||||
e = domish.Element((None, "foo"), attribs={("testns2", "bar"): "baz"})
|
||||
c = e.addElement(("testns2", "qux"))
|
||||
c[("testns2", "bar")] = "quux"
|
||||
c.addElement("foo")
|
||||
|
||||
self.assertEqual(
|
||||
e.toXml(),
|
||||
"<foo xmlns:xn0='testns2' xn0:bar='baz'><xn0:qux xn0:bar='quux'><xn0:foo/></xn0:qux></foo>",
|
||||
)
|
||||
|
||||
def testPrefixScope(self):
|
||||
e = domish.Element(("testns", "foo"))
|
||||
|
||||
self.assertEqual(
|
||||
e.toXml(prefixes={"testns": "bar"}, prefixesInScope=["bar"]), "<bar:foo/>"
|
||||
)
|
||||
|
||||
def testLocalPrefixes(self):
|
||||
e = domish.Element(("testns", "foo"), localPrefixes={"bar": "testns"})
|
||||
self.assertEqual(e.toXml(), "<bar:foo xmlns:bar='testns'/>")
|
||||
|
||||
def testLocalPrefixesWithChild(self):
|
||||
e = domish.Element(("testns", "foo"), localPrefixes={"bar": "testns"})
|
||||
e.addElement("baz")
|
||||
self.assertIdentical(e.baz.defaultUri, None)
|
||||
self.assertEqual(e.toXml(), "<bar:foo xmlns:bar='testns'><baz/></bar:foo>")
|
||||
|
||||
def test_prefixesReuse(self):
|
||||
"""
|
||||
Test that prefixes passed to serialization are not modified.
|
||||
|
||||
This test makes sure that passing a dictionary of prefixes repeatedly
|
||||
to C{toXml} of elements does not cause serialization errors. A
|
||||
previous implementation changed the passed in dictionary internally,
|
||||
causing havoc later on.
|
||||
"""
|
||||
prefixes = {"testns": "foo"}
|
||||
|
||||
# test passing of dictionary
|
||||
s = domish.SerializerClass(prefixes=prefixes)
|
||||
self.assertNotIdentical(prefixes, s.prefixes)
|
||||
|
||||
# test proper serialization on prefixes reuse
|
||||
e = domish.Element(("testns2", "foo"), localPrefixes={"quux": "testns2"})
|
||||
self.assertEqual("<quux:foo xmlns:quux='testns2'/>", e.toXml(prefixes=prefixes))
|
||||
e = domish.Element(("testns2", "foo"))
|
||||
self.assertEqual("<foo xmlns='testns2'/>", e.toXml(prefixes=prefixes))
|
||||
|
||||
def testRawXMLSerialization(self):
|
||||
e = domish.Element((None, "foo"))
|
||||
e.addRawXml("<abc123>")
|
||||
# The testcase below should NOT generate valid XML -- that's
|
||||
# the whole point of using the raw XML call -- it's the callers
|
||||
# responsibility to ensure that the data inserted is valid
|
||||
self.assertEqual(e.toXml(), "<foo><abc123></foo>")
|
||||
|
||||
def testRawXMLWithUnicodeSerialization(self):
|
||||
e = domish.Element((None, "foo"))
|
||||
e.addRawXml("<degree>\u00B0</degree>")
|
||||
self.assertEqual(e.toXml(), "<foo><degree>\u00B0</degree></foo>")
|
||||
|
||||
def testUnicodeSerialization(self):
|
||||
e = domish.Element((None, "foo"))
|
||||
e["test"] = "my value\u0221e"
|
||||
e.addContent("A degree symbol...\u00B0")
|
||||
self.assertEqual(
|
||||
e.toXml(), "<foo test='my value\u0221e'" ">A degree symbol...\u00B0</foo>"
|
||||
)
|
||||
2951
backend/lib/python3.12/site-packages/twisted/words/test/test_irc.py
Normal file
2951
backend/lib/python3.12/site-packages/twisted/words/test/test_irc.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,298 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for IRC portions of L{twisted.words.service}.
|
||||
"""
|
||||
|
||||
from twisted.cred import checkers, portal
|
||||
from twisted.test import proto_helpers
|
||||
from twisted.words.protocols import irc
|
||||
from twisted.words.service import InMemoryWordsRealm, IRCFactory, IRCUser
|
||||
from twisted.words.test.test_irc import IRCTestCase
|
||||
|
||||
|
||||
class IRCUserTests(IRCTestCase):
|
||||
"""
|
||||
Isolated tests for L{IRCUser}
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Sets up a Realm, Portal, Factory, IRCUser, Transport, and Connection
|
||||
for our tests.
|
||||
"""
|
||||
self.realm = InMemoryWordsRealm("example.com")
|
||||
self.checker = checkers.InMemoryUsernamePasswordDatabaseDontUse()
|
||||
self.portal = portal.Portal(self.realm, [self.checker])
|
||||
self.checker.addUser("john", "pass")
|
||||
self.factory = IRCFactory(self.realm, self.portal)
|
||||
self.ircUser = self.factory.buildProtocol(None)
|
||||
self.stringTransport = proto_helpers.StringTransport()
|
||||
self.ircUser.makeConnection(self.stringTransport)
|
||||
|
||||
def test_sendMessage(self):
|
||||
"""
|
||||
Sending a message to a user after they have sent NICK, but before they
|
||||
have authenticated, results in a message from "example.com".
|
||||
"""
|
||||
self.ircUser.irc_NICK("", ["mynick"])
|
||||
self.stringTransport.clear()
|
||||
self.ircUser.sendMessage("foo")
|
||||
self.assertEqualBufferValue(
|
||||
self.stringTransport.value(), ":example.com foo mynick\r\n"
|
||||
)
|
||||
|
||||
def test_utf8Messages(self):
|
||||
"""
|
||||
When a UTF8 message is sent with sendMessage and the current IRCUser
|
||||
has a UTF8 nick and is set to UTF8 encoding, the message will be
|
||||
written to the transport.
|
||||
"""
|
||||
expectedResult = ":example.com тест ник\r\n".encode()
|
||||
|
||||
self.ircUser.irc_NICK("", ["\u043d\u0438\u043a".encode()])
|
||||
self.stringTransport.clear()
|
||||
self.ircUser.sendMessage("\u0442\u0435\u0441\u0442".encode())
|
||||
self.assertEqualBufferValue(self.stringTransport.value(), expectedResult)
|
||||
|
||||
def test_invalidEncodingNick(self):
|
||||
"""
|
||||
A NICK command sent with a nickname that cannot be decoded with the
|
||||
current IRCUser's encoding results in a PRIVMSG from NickServ
|
||||
indicating that the nickname could not be decoded.
|
||||
"""
|
||||
self.ircUser.irc_NICK("", [b"\xd4\xc5\xd3\xd4"])
|
||||
self.assertRaises(UnicodeError)
|
||||
|
||||
def response(self):
|
||||
"""
|
||||
Grabs our responses and then clears the transport
|
||||
"""
|
||||
response = self.ircUser.transport.value()
|
||||
self.ircUser.transport.clear()
|
||||
response = response.decode("utf-8")
|
||||
response = response.splitlines()
|
||||
return [irc.parsemsg(r) for r in response]
|
||||
|
||||
def scanResponse(self, response, messageType):
|
||||
"""
|
||||
Gets messages out of a response
|
||||
|
||||
@param response: The parsed IRC messages of the response, as returned
|
||||
by L{IRCUserTests.response}
|
||||
|
||||
@param messageType: The string type of the desired messages.
|
||||
|
||||
@return: An iterator which yields 2-tuples of C{(index, ircMessage)}
|
||||
"""
|
||||
for n, message in enumerate(response):
|
||||
if message[1] == messageType:
|
||||
yield n, message
|
||||
|
||||
def test_sendNickSendsGreeting(self):
|
||||
"""
|
||||
Receiving NICK without authenticating sends the MOTD Start and MOTD End
|
||||
messages, which is required by certain popular IRC clients (such as
|
||||
Pidgin) before a connection is considered to be fully established.
|
||||
"""
|
||||
self.ircUser.irc_NICK("", ["mynick"])
|
||||
response = self.response()
|
||||
start = list(self.scanResponse(response, irc.RPL_MOTDSTART))
|
||||
end = list(self.scanResponse(response, irc.RPL_ENDOFMOTD))
|
||||
self.assertEqual(
|
||||
start,
|
||||
[
|
||||
(
|
||||
0,
|
||||
(
|
||||
"example.com",
|
||||
"375",
|
||||
["mynick", "- example.com Message of the Day - "],
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
end, [(1, ("example.com", "376", ["mynick", "End of /MOTD command."]))]
|
||||
)
|
||||
|
||||
def test_fullLogin(self):
|
||||
"""
|
||||
Receiving USER, PASS, NICK will log in the user, and transmit the
|
||||
appropriate response messages.
|
||||
"""
|
||||
self.ircUser.irc_USER("", ["john doe"])
|
||||
self.ircUser.irc_PASS("", ["pass"])
|
||||
self.ircUser.irc_NICK("", ["john"])
|
||||
|
||||
version = "Your host is example.com, running version {}".format(
|
||||
self.factory._serverInfo["serviceVersion"],
|
||||
)
|
||||
|
||||
creation = "This server was created on {}".format(
|
||||
self.factory._serverInfo["creationDate"],
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.response(),
|
||||
[
|
||||
("example.com", "375", ["john", "- example.com Message of the Day - "]),
|
||||
("example.com", "376", ["john", "End of /MOTD command."]),
|
||||
("example.com", "001", ["john", "connected to Twisted IRC"]),
|
||||
("example.com", "002", ["john", version]),
|
||||
("example.com", "003", ["john", creation]),
|
||||
(
|
||||
"example.com",
|
||||
"004",
|
||||
[
|
||||
"john",
|
||||
"example.com",
|
||||
self.factory._serverInfo["serviceVersion"],
|
||||
"w",
|
||||
"n",
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def test_PART(self):
|
||||
"""
|
||||
irc_PART
|
||||
"""
|
||||
self.ircUser.irc_NICK("testuser", ["mynick"])
|
||||
response = self.response()
|
||||
self.ircUser.transport.clear()
|
||||
self.assertEqual(response[0][1], irc.RPL_MOTDSTART)
|
||||
self.ircUser.irc_JOIN("testuser", ["somechannel"])
|
||||
response = self.response()
|
||||
self.ircUser.transport.clear()
|
||||
self.assertEqual(response[0][1], irc.ERR_NOSUCHCHANNEL)
|
||||
self.ircUser.irc_PART("testuser", [b"somechannel", b"booga"])
|
||||
response = self.response()
|
||||
self.ircUser.transport.clear()
|
||||
self.assertEqual(response[0][1], irc.ERR_NOTONCHANNEL)
|
||||
self.ircUser.irc_PART("testuser", ["somechannel", "booga"])
|
||||
response = self.response()
|
||||
self.ircUser.transport.clear()
|
||||
self.assertEqual(response[0][1], irc.ERR_NOTONCHANNEL)
|
||||
|
||||
def test_NAMES(self):
|
||||
"""
|
||||
irc_NAMES
|
||||
"""
|
||||
self.ircUser.irc_NICK("", ["testuser"])
|
||||
self.ircUser.irc_JOIN("", ["somechannel"])
|
||||
self.ircUser.transport.clear()
|
||||
self.ircUser.irc_NAMES("", ["somechannel"])
|
||||
response = self.response()
|
||||
self.assertEqual(response[0][1], irc.RPL_ENDOFNAMES)
|
||||
|
||||
|
||||
class MocksyIRCUser(IRCUser):
|
||||
def __init__(self):
|
||||
self.realm = InMemoryWordsRealm("example.com")
|
||||
self.mockedCodes = []
|
||||
|
||||
def sendMessage(self, code, *_, **__):
|
||||
self.mockedCodes.append(code)
|
||||
|
||||
|
||||
BADTEXT = b"\xff"
|
||||
|
||||
|
||||
class IRCUserBadEncodingTests(IRCTestCase):
|
||||
"""
|
||||
Verifies that L{IRCUser} sends the correct error messages back to clients
|
||||
when given indecipherable bytes
|
||||
"""
|
||||
|
||||
# TODO: irc_NICK -- but NICKSERV is used for that, so it isn't as easy.
|
||||
|
||||
def setUp(self):
|
||||
self.ircUser = MocksyIRCUser()
|
||||
|
||||
def assertChokesOnBadBytes(self, irc_x, error):
|
||||
"""
|
||||
Asserts that IRCUser sends the relevant error code when a given irc_x
|
||||
dispatch method is given undecodable bytes.
|
||||
|
||||
@param irc_x: the name of the irc_FOO method to test.
|
||||
For example, irc_x = 'PRIVMSG' will check irc_PRIVMSG
|
||||
|
||||
@param error: the error code irc_x should send. For example,
|
||||
irc.ERR_NOTONCHANNEL
|
||||
"""
|
||||
getattr(self.ircUser, "irc_%s" % irc_x)(None, [BADTEXT])
|
||||
self.assertEqual(self.ircUser.mockedCodes, [error])
|
||||
|
||||
# No such channel
|
||||
|
||||
def test_JOIN(self):
|
||||
"""
|
||||
Tests that irc_JOIN sends ERR_NOSUCHCHANNEL if the channel name can't
|
||||
be decoded.
|
||||
"""
|
||||
self.assertChokesOnBadBytes("JOIN", irc.ERR_NOSUCHCHANNEL)
|
||||
|
||||
def test_NAMES(self):
|
||||
"""
|
||||
Tests that irc_NAMES sends ERR_NOSUCHCHANNEL if the channel name can't
|
||||
be decoded.
|
||||
"""
|
||||
self.assertChokesOnBadBytes("NAMES", irc.ERR_NOSUCHCHANNEL)
|
||||
|
||||
def test_TOPIC(self):
|
||||
"""
|
||||
Tests that irc_TOPIC sends ERR_NOSUCHCHANNEL if the channel name can't
|
||||
be decoded.
|
||||
"""
|
||||
self.assertChokesOnBadBytes("TOPIC", irc.ERR_NOSUCHCHANNEL)
|
||||
|
||||
def test_LIST(self):
|
||||
"""
|
||||
Tests that irc_LIST sends ERR_NOSUCHCHANNEL if the channel name can't
|
||||
be decoded.
|
||||
"""
|
||||
self.assertChokesOnBadBytes("LIST", irc.ERR_NOSUCHCHANNEL)
|
||||
|
||||
# No such nick
|
||||
|
||||
def test_MODE(self):
|
||||
"""
|
||||
Tests that irc_MODE sends ERR_NOSUCHNICK if the target name can't
|
||||
be decoded.
|
||||
"""
|
||||
self.assertChokesOnBadBytes("MODE", irc.ERR_NOSUCHNICK)
|
||||
|
||||
def test_PRIVMSG(self):
|
||||
"""
|
||||
Tests that irc_PRIVMSG sends ERR_NOSUCHNICK if the target name can't
|
||||
be decoded.
|
||||
"""
|
||||
self.assertChokesOnBadBytes("PRIVMSG", irc.ERR_NOSUCHNICK)
|
||||
|
||||
def test_WHOIS(self):
|
||||
"""
|
||||
Tests that irc_WHOIS sends ERR_NOSUCHNICK if the target name can't
|
||||
be decoded.
|
||||
"""
|
||||
self.assertChokesOnBadBytes("WHOIS", irc.ERR_NOSUCHNICK)
|
||||
|
||||
# Not on channel
|
||||
|
||||
def test_PART(self):
|
||||
"""
|
||||
Tests that irc_PART sends ERR_NOTONCHANNEL if the target name can't
|
||||
be decoded.
|
||||
"""
|
||||
self.assertChokesOnBadBytes("PART", irc.ERR_NOTONCHANNEL)
|
||||
|
||||
# Probably nothing
|
||||
|
||||
def test_WHO(self):
|
||||
"""
|
||||
Tests that irc_WHO immediately ends the WHO list if the target name
|
||||
can't be decoded.
|
||||
"""
|
||||
self.assertChokesOnBadBytes("WHO", irc.RPL_ENDOFWHO)
|
||||
@@ -0,0 +1,272 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.words.im.ircsupport}.
|
||||
"""
|
||||
|
||||
from twisted.internet.testing import StringTransport
|
||||
from twisted.words.im.basechat import ChatUI, Conversation, GroupConversation
|
||||
from twisted.words.im.ircsupport import IRCAccount, IRCProto
|
||||
from twisted.words.im.locals import OfflineError
|
||||
from twisted.words.test.test_irc import IRCTestCase
|
||||
|
||||
|
||||
class StubConversation(Conversation):
|
||||
def show(self):
|
||||
pass
|
||||
|
||||
def showMessage(self, message, metadata):
|
||||
self.message = message
|
||||
self.metadata = metadata
|
||||
|
||||
|
||||
class StubGroupConversation(GroupConversation):
|
||||
def setTopic(self, topic, nickname):
|
||||
self.topic = topic
|
||||
self.topicSetBy = nickname
|
||||
|
||||
def show(self):
|
||||
pass
|
||||
|
||||
def showGroupMessage(self, sender, text, metadata=None):
|
||||
self.metadata = metadata
|
||||
self.text = text
|
||||
self.metadata = metadata
|
||||
|
||||
|
||||
class StubChatUI(ChatUI):
|
||||
def getConversation(self, group, Class=StubConversation, stayHidden=0):
|
||||
return ChatUI.getGroupConversation(self, group, Class, stayHidden)
|
||||
|
||||
def getGroupConversation(self, group, Class=StubGroupConversation, stayHidden=0):
|
||||
return ChatUI.getGroupConversation(self, group, Class, stayHidden)
|
||||
|
||||
|
||||
class IRCProtoTests(IRCTestCase):
|
||||
"""
|
||||
Tests for L{IRCProto}.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.account = IRCAccount(
|
||||
"Some account", False, "alice", None, "example.com", 6667
|
||||
)
|
||||
self.proto = IRCProto(self.account, StubChatUI(), None)
|
||||
self.transport = StringTransport()
|
||||
|
||||
def test_login(self) -> None:
|
||||
"""
|
||||
When L{IRCProto} is connected to a transport, it sends I{NICK} and
|
||||
I{USER} commands with the username from the account object.
|
||||
"""
|
||||
self.proto.makeConnection(self.transport)
|
||||
self.assertEqualBufferValue(
|
||||
self.transport.value(),
|
||||
"NICK alice\r\n" "USER alice foo bar :Twisted-IM user\r\n",
|
||||
)
|
||||
|
||||
def test_authenticate(self) -> None:
|
||||
"""
|
||||
If created with an account with a password, L{IRCProto} sends a
|
||||
I{PASS} command before the I{NICK} and I{USER} commands.
|
||||
"""
|
||||
self.account.password = "secret"
|
||||
self.proto.makeConnection(self.transport)
|
||||
self.assertEqualBufferValue(
|
||||
self.transport.value(),
|
||||
"PASS secret\r\n"
|
||||
"NICK alice\r\n"
|
||||
"USER alice foo bar :Twisted-IM user\r\n",
|
||||
)
|
||||
|
||||
def test_channels(self) -> None:
|
||||
"""
|
||||
If created with an account with a list of channels, L{IRCProto}
|
||||
joins each of those channels after registering.
|
||||
"""
|
||||
self.account.channels = ["#foo", "#bar"]
|
||||
self.proto.makeConnection(self.transport)
|
||||
self.assertEqualBufferValue(
|
||||
self.transport.value(),
|
||||
"NICK alice\r\n"
|
||||
"USER alice foo bar :Twisted-IM user\r\n"
|
||||
"JOIN #foo\r\n"
|
||||
"JOIN #bar\r\n",
|
||||
)
|
||||
|
||||
def test_isupport(self) -> None:
|
||||
"""
|
||||
L{IRCProto} can interpret I{ISUPPORT} (I{005}) messages from the server
|
||||
and reflect their information in its C{supported} attribute.
|
||||
"""
|
||||
self.proto.makeConnection(self.transport)
|
||||
self.proto.dataReceived(":irc.example.com 005 alice MODES=4 CHANLIMIT=#:20\r\n")
|
||||
self.assertEqual(4, self.proto.supported.getFeature("MODES"))
|
||||
|
||||
def test_nick(self) -> None:
|
||||
"""
|
||||
IRC NICK command changes the nickname of a user.
|
||||
"""
|
||||
self.proto.makeConnection(self.transport)
|
||||
self.proto.dataReceived(":alice JOIN #group1\r\n")
|
||||
self.proto.dataReceived(":alice1 JOIN #group1\r\n")
|
||||
self.proto.dataReceived(":alice1 NICK newnick\r\n")
|
||||
self.proto.dataReceived(":alice3 NICK newnick3\r\n")
|
||||
self.assertIn("newnick", self.proto._ingroups)
|
||||
self.assertNotIn("alice1", self.proto._ingroups)
|
||||
|
||||
def test_part(self) -> None:
|
||||
"""
|
||||
IRC PART command removes a user from an IRC channel.
|
||||
"""
|
||||
self.proto.makeConnection(self.transport)
|
||||
self.proto.dataReceived(":alice1 JOIN #group1\r\n")
|
||||
self.assertIn("group1", self.proto._ingroups["alice1"])
|
||||
self.assertNotIn("group2", self.proto._ingroups["alice1"])
|
||||
self.proto.dataReceived(":alice PART #group1\r\n")
|
||||
self.proto.dataReceived(":alice1 PART #group1\r\n")
|
||||
self.proto.dataReceived(":alice1 PART #group2\r\n")
|
||||
self.assertNotIn("group1", self.proto._ingroups["alice1"])
|
||||
self.assertNotIn("group2", self.proto._ingroups["alice1"])
|
||||
|
||||
def test_quit(self) -> None:
|
||||
"""
|
||||
IRC QUIT command removes a user from all IRC channels.
|
||||
"""
|
||||
self.proto.makeConnection(self.transport)
|
||||
self.proto.dataReceived(":alice1 JOIN #group1\r\n")
|
||||
self.assertIn("group1", self.proto._ingroups["alice1"])
|
||||
self.assertNotIn("group2", self.proto._ingroups["alice1"])
|
||||
self.proto.dataReceived(":alice1 JOIN #group3\r\n")
|
||||
self.assertIn("group3", self.proto._ingroups["alice1"])
|
||||
self.proto.dataReceived(":alice1 QUIT\r\n")
|
||||
self.assertTrue(len(self.proto._ingroups["alice1"]) == 0)
|
||||
self.proto.dataReceived(":alice3 QUIT\r\n")
|
||||
|
||||
def test_topic(self) -> None:
|
||||
"""
|
||||
IRC TOPIC command changes the topic of an IRC channel.
|
||||
"""
|
||||
self.proto.makeConnection(self.transport)
|
||||
self.proto.dataReceived(":alice1 JOIN #group1\r\n")
|
||||
self.proto.dataReceived(":alice1 TOPIC #group1 newtopic\r\n")
|
||||
groupConversation = self.proto.getGroupConversation("group1")
|
||||
self.assertEqual(groupConversation.topic, "newtopic")
|
||||
self.assertEqual(groupConversation.topicSetBy, "alice1")
|
||||
|
||||
def test_privmsg(self) -> None:
|
||||
"""
|
||||
PRIVMSG sends a private message to a user or channel.
|
||||
"""
|
||||
self.proto.makeConnection(self.transport)
|
||||
self.proto.dataReceived(":alice1 PRIVMSG t2 test_message_1\r\n")
|
||||
conversation = self.proto.chat.getConversation(self.proto.getPerson("alice1"))
|
||||
self.assertEqual(conversation.message, "test_message_1")
|
||||
|
||||
self.proto.dataReceived(":alice1 PRIVMSG #group1 test_message_2\r\n")
|
||||
groupConversation = self.proto.getGroupConversation("group1")
|
||||
self.assertEqual(groupConversation.text, "test_message_2")
|
||||
|
||||
self.proto.setNick("alice")
|
||||
self.proto.dataReceived(":alice PRIVMSG #foo test_message_3\r\n")
|
||||
groupConversation = self.proto.getGroupConversation("foo")
|
||||
self.assertFalse(hasattr(groupConversation, "text"))
|
||||
conversation = self.proto.chat.getConversation(self.proto.getPerson("alice"))
|
||||
self.assertFalse(hasattr(conversation, "message"))
|
||||
|
||||
def test_action(self) -> None:
|
||||
"""
|
||||
CTCP ACTION to a user or channel.
|
||||
"""
|
||||
self.proto.makeConnection(self.transport)
|
||||
self.proto.dataReceived(":alice1 PRIVMSG alice1 :\01ACTION smiles\01\r\n")
|
||||
conversation = self.proto.chat.getConversation(self.proto.getPerson("alice1"))
|
||||
self.assertEqual(conversation.message, "smiles")
|
||||
|
||||
self.proto.dataReceived(":alice1 PRIVMSG #group1 :\01ACTION laughs\01\r\n")
|
||||
groupConversation = self.proto.getGroupConversation("group1")
|
||||
self.assertEqual(groupConversation.text, "laughs")
|
||||
|
||||
self.proto.setNick("alice")
|
||||
self.proto.dataReceived(":alice PRIVMSG #group1 :\01ACTION cries\01\r\n")
|
||||
groupConversation = self.proto.getGroupConversation("foo")
|
||||
self.assertFalse(hasattr(groupConversation, "text"))
|
||||
conversation = self.proto.chat.getConversation(self.proto.getPerson("alice"))
|
||||
self.assertFalse(hasattr(conversation, "message"))
|
||||
|
||||
def test_rplNamreply(self) -> None:
|
||||
"""
|
||||
RPL_NAMREPLY server response (353) lists all the users in a channel.
|
||||
RPL_ENDOFNAMES server response (363) is sent at the end of RPL_NAMREPLY
|
||||
to indicate that there are no more names.
|
||||
"""
|
||||
self.proto.makeConnection(self.transport)
|
||||
self.proto.dataReceived(
|
||||
":example.com 353 z3p = #bnl :pSwede Dan- SkOyg @MrOp +MrPlus\r\n"
|
||||
)
|
||||
expectedInGroups = {
|
||||
"Dan-": ["bnl"],
|
||||
"pSwede": ["bnl"],
|
||||
"SkOyg": ["bnl"],
|
||||
"MrOp": ["bnl"],
|
||||
"MrPlus": ["bnl"],
|
||||
}
|
||||
expectedNamReplies = {"bnl": ["pSwede", "Dan-", "SkOyg", "MrOp", "MrPlus"]}
|
||||
self.assertEqual(expectedInGroups, self.proto._ingroups)
|
||||
self.assertEqual(expectedNamReplies, self.proto._namreplies)
|
||||
|
||||
self.proto.dataReceived(":example.com 366 alice #bnl :End of /NAMES list\r\n")
|
||||
self.assertEqual({}, self.proto._namreplies)
|
||||
groupConversation = self.proto.getGroupConversation("bnl")
|
||||
self.assertEqual(expectedNamReplies["bnl"], groupConversation.members)
|
||||
|
||||
def test_rplTopic(self) -> None:
|
||||
"""
|
||||
RPL_TOPIC server response (332) is sent when a channel's topic is changed
|
||||
"""
|
||||
self.proto.makeConnection(self.transport)
|
||||
self.proto.dataReceived(":example.com 332 alice, #foo :Some random topic\r\n")
|
||||
self.assertEqual("Some random topic", self.proto._topics["foo"])
|
||||
|
||||
def test_sendMessage(self) -> None:
|
||||
"""
|
||||
L{IRCPerson.sendMessage}
|
||||
"""
|
||||
self.proto.makeConnection(self.transport)
|
||||
person = self.proto.getPerson("alice")
|
||||
self.assertRaises(OfflineError, person.sendMessage, "Some message")
|
||||
|
||||
person.account.client = self.proto
|
||||
self.transport.clear()
|
||||
person.sendMessage("Some message 2")
|
||||
self.assertEqual(
|
||||
self.transport.io.getvalue(), b"PRIVMSG alice :Some message 2\r\n"
|
||||
)
|
||||
|
||||
self.transport.clear()
|
||||
person.sendMessage("smiles", {"style": "emote"})
|
||||
self.assertEqual(
|
||||
self.transport.io.getvalue(), b"PRIVMSG alice :\x01ACTION smiles\x01\r\n"
|
||||
)
|
||||
|
||||
def test_sendGroupMessage(self) -> None:
|
||||
"""
|
||||
L{IRCGroup.sendGroupMessage}
|
||||
"""
|
||||
self.proto.makeConnection(self.transport)
|
||||
group = self.proto.chat.getGroup("#foo", self.proto)
|
||||
self.assertRaises(OfflineError, group.sendGroupMessage, "Some message")
|
||||
|
||||
group.account.client = self.proto
|
||||
self.transport.clear()
|
||||
group.sendGroupMessage("Some message 2")
|
||||
self.assertEqual(
|
||||
self.transport.io.getvalue(), b"PRIVMSG #foo :Some message 2\r\n"
|
||||
)
|
||||
|
||||
self.transport.clear()
|
||||
group.sendGroupMessage("smiles", {"style": "emote"})
|
||||
self.assertEqual(
|
||||
self.transport.io.getvalue(), b"PRIVMSG #foo :\x01ACTION smiles\x01\r\n"
|
||||
)
|
||||
@@ -0,0 +1,478 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.words.protocols.jabber.client}
|
||||
"""
|
||||
|
||||
|
||||
from hashlib import sha1
|
||||
from unittest import skipIf
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.trial import unittest
|
||||
from twisted.words.protocols.jabber import client, error, jid, xmlstream
|
||||
from twisted.words.protocols.jabber.sasl import SASLInitiatingInitializer
|
||||
from twisted.words.xish import utility
|
||||
|
||||
try:
|
||||
from twisted.internet import ssl
|
||||
except ImportError:
|
||||
ssl = None # type: ignore[assignment]
|
||||
skipWhenNoSSL = (True, "SSL not available")
|
||||
else:
|
||||
skipWhenNoSSL = (False, "")
|
||||
|
||||
IQ_AUTH_GET = '/iq[@type="get"]/query[@xmlns="jabber:iq:auth"]'
|
||||
IQ_AUTH_SET = '/iq[@type="set"]/query[@xmlns="jabber:iq:auth"]'
|
||||
NS_BIND = "urn:ietf:params:xml:ns:xmpp-bind"
|
||||
IQ_BIND_SET = '/iq[@type="set"]/bind[@xmlns="%s"]' % NS_BIND
|
||||
NS_SESSION = "urn:ietf:params:xml:ns:xmpp-session"
|
||||
IQ_SESSION_SET = '/iq[@type="set"]/session[@xmlns="%s"]' % NS_SESSION
|
||||
|
||||
|
||||
class CheckVersionInitializerTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
a = xmlstream.Authenticator()
|
||||
xs = xmlstream.XmlStream(a)
|
||||
self.init = client.CheckVersionInitializer(xs)
|
||||
|
||||
def testSupported(self):
|
||||
"""
|
||||
Test supported version number 1.0
|
||||
"""
|
||||
self.init.xmlstream.version = (1, 0)
|
||||
self.init.initialize()
|
||||
|
||||
def testNotSupported(self):
|
||||
"""
|
||||
Test unsupported version number 0.0, and check exception.
|
||||
"""
|
||||
self.init.xmlstream.version = (0, 0)
|
||||
exc = self.assertRaises(error.StreamError, self.init.initialize)
|
||||
self.assertEqual("unsupported-version", exc.condition)
|
||||
|
||||
|
||||
class InitiatingInitializerHarness:
|
||||
"""
|
||||
Testing harness for interacting with XML stream initializers.
|
||||
|
||||
This sets up an L{utility.XmlPipe} to create a communication channel between
|
||||
the initializer and the stubbed receiving entity. It features a sink and
|
||||
source side that both act similarly to a real L{xmlstream.XmlStream}. The
|
||||
sink is augmented with an authenticator to which initializers can be added.
|
||||
|
||||
The harness also provides some utility methods to work with event observers
|
||||
and deferreds.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.output = []
|
||||
self.pipe = utility.XmlPipe()
|
||||
self.xmlstream = self.pipe.sink
|
||||
self.authenticator = xmlstream.ConnectAuthenticator("example.org")
|
||||
self.xmlstream.authenticator = self.authenticator
|
||||
|
||||
def waitFor(self, event, handler):
|
||||
"""
|
||||
Observe an output event, returning a deferred.
|
||||
|
||||
The returned deferred will be fired when the given event has been
|
||||
observed on the source end of the L{XmlPipe} tied to the protocol
|
||||
under test. The handler is added as the first callback.
|
||||
|
||||
@param event: The event to be observed. See
|
||||
L{utility.EventDispatcher.addOnetimeObserver}.
|
||||
@param handler: The handler to be called with the observed event object.
|
||||
@rtype: L{defer.Deferred}.
|
||||
"""
|
||||
d = defer.Deferred()
|
||||
d.addCallback(handler)
|
||||
self.pipe.source.addOnetimeObserver(event, d.callback)
|
||||
return d
|
||||
|
||||
|
||||
class IQAuthInitializerTests(InitiatingInitializerHarness, unittest.TestCase):
|
||||
"""
|
||||
Tests for L{client.IQAuthInitializer}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.init = client.IQAuthInitializer(self.xmlstream)
|
||||
self.authenticator.jid = jid.JID("user@example.com/resource")
|
||||
self.authenticator.password = "secret"
|
||||
|
||||
def testPlainText(self):
|
||||
"""
|
||||
Test plain-text authentication.
|
||||
|
||||
Act as a server supporting plain-text authentication and expect the
|
||||
C{password} field to be filled with the password. Then act as if
|
||||
authentication succeeds.
|
||||
"""
|
||||
|
||||
def onAuthGet(iq):
|
||||
"""
|
||||
Called when the initializer sent a query for authentication methods.
|
||||
|
||||
The response informs the client that plain-text authentication
|
||||
is supported.
|
||||
"""
|
||||
|
||||
# Create server response
|
||||
response = xmlstream.toResponse(iq, "result")
|
||||
response.addElement(("jabber:iq:auth", "query"))
|
||||
response.query.addElement("username")
|
||||
response.query.addElement("password")
|
||||
response.query.addElement("resource")
|
||||
|
||||
# Set up an observer for the next request we expect.
|
||||
d = self.waitFor(IQ_AUTH_SET, onAuthSet)
|
||||
|
||||
# Send server response
|
||||
self.pipe.source.send(response)
|
||||
|
||||
return d
|
||||
|
||||
def onAuthSet(iq):
|
||||
"""
|
||||
Called when the initializer sent the authentication request.
|
||||
|
||||
The server checks the credentials and responds with an empty result
|
||||
signalling success.
|
||||
"""
|
||||
self.assertEqual("user", str(iq.query.username))
|
||||
self.assertEqual("secret", str(iq.query.password))
|
||||
self.assertEqual("resource", str(iq.query.resource))
|
||||
|
||||
# Send server response
|
||||
response = xmlstream.toResponse(iq, "result")
|
||||
self.pipe.source.send(response)
|
||||
|
||||
# Set up an observer for the request for authentication fields
|
||||
d1 = self.waitFor(IQ_AUTH_GET, onAuthGet)
|
||||
|
||||
# Start the initializer
|
||||
d2 = self.init.initialize()
|
||||
return defer.gatherResults([d1, d2])
|
||||
|
||||
def testDigest(self):
|
||||
"""
|
||||
Test digest authentication.
|
||||
|
||||
Act as a server supporting digest authentication and expect the
|
||||
C{digest} field to be filled with a sha1 digest of the concatenated
|
||||
stream session identifier and password. Then act as if authentication
|
||||
succeeds.
|
||||
"""
|
||||
|
||||
def onAuthGet(iq):
|
||||
"""
|
||||
Called when the initializer sent a query for authentication methods.
|
||||
|
||||
The response informs the client that digest authentication is
|
||||
supported.
|
||||
"""
|
||||
|
||||
# Create server response
|
||||
response = xmlstream.toResponse(iq, "result")
|
||||
response.addElement(("jabber:iq:auth", "query"))
|
||||
response.query.addElement("username")
|
||||
response.query.addElement("digest")
|
||||
response.query.addElement("resource")
|
||||
|
||||
# Set up an observer for the next request we expect.
|
||||
d = self.waitFor(IQ_AUTH_SET, onAuthSet)
|
||||
|
||||
# Send server response
|
||||
self.pipe.source.send(response)
|
||||
|
||||
return d
|
||||
|
||||
def onAuthSet(iq):
|
||||
"""
|
||||
Called when the initializer sent the authentication request.
|
||||
|
||||
The server checks the credentials and responds with an empty result
|
||||
signalling success.
|
||||
"""
|
||||
self.assertEqual("user", str(iq.query.username))
|
||||
self.assertEqual(sha1(b"12345secret").hexdigest(), str(iq.query.digest))
|
||||
self.assertEqual("resource", str(iq.query.resource))
|
||||
|
||||
# Send server response
|
||||
response = xmlstream.toResponse(iq, "result")
|
||||
self.pipe.source.send(response)
|
||||
|
||||
# Digest authentication relies on the stream session identifier. Set it.
|
||||
self.xmlstream.sid = "12345"
|
||||
|
||||
# Set up an observer for the request for authentication fields
|
||||
d1 = self.waitFor(IQ_AUTH_GET, onAuthGet)
|
||||
|
||||
# Start the initializer
|
||||
d2 = self.init.initialize()
|
||||
|
||||
return defer.gatherResults([d1, d2])
|
||||
|
||||
def testFailRequestFields(self):
|
||||
"""
|
||||
Test initializer failure of request for fields for authentication.
|
||||
"""
|
||||
|
||||
def onAuthGet(iq):
|
||||
"""
|
||||
Called when the initializer sent a query for authentication methods.
|
||||
|
||||
The server responds that the client is not authorized to authenticate.
|
||||
"""
|
||||
response = error.StanzaError("not-authorized").toResponse(iq)
|
||||
self.pipe.source.send(response)
|
||||
|
||||
# Set up an observer for the request for authentication fields
|
||||
d1 = self.waitFor(IQ_AUTH_GET, onAuthGet)
|
||||
|
||||
# Start the initializer
|
||||
d2 = self.init.initialize()
|
||||
|
||||
# The initialized should fail with a stanza error.
|
||||
self.assertFailure(d2, error.StanzaError)
|
||||
|
||||
return defer.gatherResults([d1, d2])
|
||||
|
||||
def testFailAuth(self):
|
||||
"""
|
||||
Test initializer failure to authenticate.
|
||||
"""
|
||||
|
||||
def onAuthGet(iq):
|
||||
"""
|
||||
Called when the initializer sent a query for authentication methods.
|
||||
|
||||
The response informs the client that plain-text authentication
|
||||
is supported.
|
||||
"""
|
||||
|
||||
# Send server response
|
||||
response = xmlstream.toResponse(iq, "result")
|
||||
response.addElement(("jabber:iq:auth", "query"))
|
||||
response.query.addElement("username")
|
||||
response.query.addElement("password")
|
||||
response.query.addElement("resource")
|
||||
|
||||
# Set up an observer for the next request we expect.
|
||||
d = self.waitFor(IQ_AUTH_SET, onAuthSet)
|
||||
|
||||
# Send server response
|
||||
self.pipe.source.send(response)
|
||||
|
||||
return d
|
||||
|
||||
def onAuthSet(iq):
|
||||
"""
|
||||
Called when the initializer sent the authentication request.
|
||||
|
||||
The server checks the credentials and responds with a not-authorized
|
||||
stanza error.
|
||||
"""
|
||||
response = error.StanzaError("not-authorized").toResponse(iq)
|
||||
self.pipe.source.send(response)
|
||||
|
||||
# Set up an observer for the request for authentication fields
|
||||
d1 = self.waitFor(IQ_AUTH_GET, onAuthGet)
|
||||
|
||||
# Start the initializer
|
||||
d2 = self.init.initialize()
|
||||
|
||||
# The initializer should fail with a stanza error.
|
||||
self.assertFailure(d2, error.StanzaError)
|
||||
|
||||
return defer.gatherResults([d1, d2])
|
||||
|
||||
|
||||
class BindInitializerTests(InitiatingInitializerHarness, unittest.TestCase):
|
||||
"""
|
||||
Tests for L{client.BindInitializer}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.init = client.BindInitializer(self.xmlstream)
|
||||
self.authenticator.jid = jid.JID("user@example.com/resource")
|
||||
|
||||
def testBasic(self):
|
||||
"""
|
||||
Set up a stream, and act as if resource binding succeeds.
|
||||
"""
|
||||
|
||||
def onBind(iq):
|
||||
response = xmlstream.toResponse(iq, "result")
|
||||
response.addElement((NS_BIND, "bind"))
|
||||
response.bind.addElement("jid", content="user@example.com/other resource")
|
||||
self.pipe.source.send(response)
|
||||
|
||||
def cb(result):
|
||||
self.assertEqual(
|
||||
jid.JID("user@example.com/other resource"), self.authenticator.jid
|
||||
)
|
||||
|
||||
d1 = self.waitFor(IQ_BIND_SET, onBind)
|
||||
d2 = self.init.start()
|
||||
d2.addCallback(cb)
|
||||
return defer.gatherResults([d1, d2])
|
||||
|
||||
def testFailure(self):
|
||||
"""
|
||||
Set up a stream, and act as if resource binding fails.
|
||||
"""
|
||||
|
||||
def onBind(iq):
|
||||
response = error.StanzaError("conflict").toResponse(iq)
|
||||
self.pipe.source.send(response)
|
||||
|
||||
d1 = self.waitFor(IQ_BIND_SET, onBind)
|
||||
d2 = self.init.start()
|
||||
self.assertFailure(d2, error.StanzaError)
|
||||
return defer.gatherResults([d1, d2])
|
||||
|
||||
|
||||
class SessionInitializerTests(InitiatingInitializerHarness, unittest.TestCase):
|
||||
"""
|
||||
Tests for L{client.SessionInitializer}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.init = client.SessionInitializer(self.xmlstream)
|
||||
|
||||
def testSuccess(self):
|
||||
"""
|
||||
Set up a stream, and act as if session establishment succeeds.
|
||||
"""
|
||||
|
||||
def onSession(iq):
|
||||
response = xmlstream.toResponse(iq, "result")
|
||||
self.pipe.source.send(response)
|
||||
|
||||
d1 = self.waitFor(IQ_SESSION_SET, onSession)
|
||||
d2 = self.init.start()
|
||||
return defer.gatherResults([d1, d2])
|
||||
|
||||
def testFailure(self):
|
||||
"""
|
||||
Set up a stream, and act as if session establishment fails.
|
||||
"""
|
||||
|
||||
def onSession(iq):
|
||||
response = error.StanzaError("forbidden").toResponse(iq)
|
||||
self.pipe.source.send(response)
|
||||
|
||||
d1 = self.waitFor(IQ_SESSION_SET, onSession)
|
||||
d2 = self.init.start()
|
||||
self.assertFailure(d2, error.StanzaError)
|
||||
return defer.gatherResults([d1, d2])
|
||||
|
||||
|
||||
class BasicAuthenticatorTests(unittest.TestCase):
|
||||
"""
|
||||
Test for both BasicAuthenticator and basicClientFactory.
|
||||
"""
|
||||
|
||||
def test_basic(self):
|
||||
"""
|
||||
Authenticator and stream are properly constructed by the factory.
|
||||
|
||||
The L{xmlstream.XmlStream} protocol created by the factory has the new
|
||||
L{client.BasicAuthenticator} instance in its C{authenticator}
|
||||
attribute. It is set up with C{jid} and C{password} as passed to the
|
||||
factory, C{otherHost} taken from the client JID. The stream futher has
|
||||
two initializers, for TLS and authentication, of which the first has
|
||||
its C{required} attribute set to C{True}.
|
||||
"""
|
||||
self.client_jid = jid.JID("user@example.com/resource")
|
||||
|
||||
# Get an XmlStream instance. Note that it gets initialized with the
|
||||
# XMPPAuthenticator (that has its associateWithXmlStream called) that
|
||||
# is in turn initialized with the arguments to the factory.
|
||||
xs = client.basicClientFactory(self.client_jid, "secret").buildProtocol(None)
|
||||
|
||||
# test authenticator's instance variables
|
||||
self.assertEqual("example.com", xs.authenticator.otherHost)
|
||||
self.assertEqual(self.client_jid, xs.authenticator.jid)
|
||||
self.assertEqual("secret", xs.authenticator.password)
|
||||
|
||||
# test list of initializers
|
||||
tls, auth = xs.initializers
|
||||
|
||||
self.assertIsInstance(tls, xmlstream.TLSInitiatingInitializer)
|
||||
self.assertIsInstance(auth, client.IQAuthInitializer)
|
||||
|
||||
self.assertFalse(tls.required)
|
||||
|
||||
|
||||
class XMPPAuthenticatorTests(unittest.TestCase):
|
||||
"""
|
||||
Test for both XMPPAuthenticator and XMPPClientFactory.
|
||||
"""
|
||||
|
||||
def test_basic(self):
|
||||
"""
|
||||
Test basic operations.
|
||||
|
||||
Setup an XMPPClientFactory, which sets up an XMPPAuthenticator, and let
|
||||
it produce a protocol instance. Then inspect the instance variables of
|
||||
the authenticator and XML stream objects.
|
||||
"""
|
||||
self.client_jid = jid.JID("user@example.com/resource")
|
||||
|
||||
# Get an XmlStream instance. Note that it gets initialized with the
|
||||
# XMPPAuthenticator (that has its associateWithXmlStream called) that
|
||||
# is in turn initialized with the arguments to the factory.
|
||||
xs = client.XMPPClientFactory(self.client_jid, "secret").buildProtocol(None)
|
||||
|
||||
# test authenticator's instance variables
|
||||
self.assertEqual("example.com", xs.authenticator.otherHost)
|
||||
self.assertEqual(self.client_jid, xs.authenticator.jid)
|
||||
self.assertEqual("secret", xs.authenticator.password)
|
||||
|
||||
# test list of initializers
|
||||
version, tls, sasl, bind, session = xs.initializers
|
||||
|
||||
self.assertIsInstance(tls, xmlstream.TLSInitiatingInitializer)
|
||||
self.assertIsInstance(sasl, SASLInitiatingInitializer)
|
||||
self.assertIsInstance(bind, client.BindInitializer)
|
||||
self.assertIsInstance(session, client.SessionInitializer)
|
||||
|
||||
self.assertTrue(tls.required)
|
||||
self.assertTrue(sasl.required)
|
||||
self.assertTrue(bind.required)
|
||||
self.assertFalse(session.required)
|
||||
|
||||
@skipIf(*skipWhenNoSSL)
|
||||
def test_tlsConfiguration(self):
|
||||
"""
|
||||
A TLS configuration is passed to the TLS initializer.
|
||||
"""
|
||||
configs = []
|
||||
|
||||
def init(self, xs, required=True, configurationForTLS=None):
|
||||
configs.append(configurationForTLS)
|
||||
|
||||
self.client_jid = jid.JID("user@example.com/resource")
|
||||
|
||||
# Get an XmlStream instance. Note that it gets initialized with the
|
||||
# XMPPAuthenticator (that has its associateWithXmlStream called) that
|
||||
# is in turn initialized with the arguments to the factory.
|
||||
configurationForTLS = ssl.CertificateOptions()
|
||||
factory = client.XMPPClientFactory(
|
||||
self.client_jid, "secret", configurationForTLS=configurationForTLS
|
||||
)
|
||||
self.patch(xmlstream.TLSInitiatingInitializer, "__init__", init)
|
||||
xs = factory.buildProtocol(None)
|
||||
|
||||
# test list of initializers
|
||||
version, tls, sasl, bind, session = xs.initializers
|
||||
|
||||
self.assertIsInstance(tls, xmlstream.TLSInitiatingInitializer)
|
||||
self.assertIs(configurationForTLS, configs[0])
|
||||
@@ -0,0 +1,426 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.words.protocols.jabber.component}
|
||||
"""
|
||||
from hashlib import sha1
|
||||
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.python import failure
|
||||
from twisted.trial import unittest
|
||||
from twisted.words.protocols.jabber import component, ijabber, xmlstream
|
||||
from twisted.words.protocols.jabber.jid import JID
|
||||
from twisted.words.xish import domish
|
||||
from twisted.words.xish.utility import XmlPipe
|
||||
|
||||
|
||||
class DummyTransport:
|
||||
def __init__(self, list):
|
||||
self.list = list
|
||||
|
||||
def write(self, bytes):
|
||||
self.list.append(bytes)
|
||||
|
||||
|
||||
class ComponentInitiatingInitializerTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.output = []
|
||||
|
||||
self.authenticator = xmlstream.Authenticator()
|
||||
self.authenticator.password = "secret"
|
||||
self.xmlstream = xmlstream.XmlStream(self.authenticator)
|
||||
self.xmlstream.namespace = "test:component"
|
||||
self.xmlstream.send = self.output.append
|
||||
self.xmlstream.connectionMade()
|
||||
self.xmlstream.dataReceived(
|
||||
"<stream:stream xmlns='test:component' "
|
||||
"xmlns:stream='http://etherx.jabber.org/streams' "
|
||||
"from='example.com' id='12345' version='1.0'>"
|
||||
)
|
||||
self.xmlstream.sid = "12345"
|
||||
self.init = component.ComponentInitiatingInitializer(self.xmlstream)
|
||||
|
||||
def testHandshake(self):
|
||||
"""
|
||||
Test basic operations of component handshake.
|
||||
"""
|
||||
|
||||
d = self.init.initialize()
|
||||
|
||||
# the initializer should have sent the handshake request
|
||||
|
||||
handshake = self.output[-1]
|
||||
self.assertEqual("handshake", handshake.name)
|
||||
self.assertEqual("test:component", handshake.uri)
|
||||
self.assertEqual(sha1(b"12345" + b"secret").hexdigest(), str(handshake))
|
||||
|
||||
# successful authentication
|
||||
|
||||
handshake.children = []
|
||||
self.xmlstream.dataReceived(handshake.toXml())
|
||||
|
||||
return d
|
||||
|
||||
|
||||
class ComponentAuthTests(unittest.TestCase):
|
||||
def authPassed(self, stream):
|
||||
self.authComplete = True
|
||||
|
||||
def testAuth(self):
|
||||
self.authComplete = False
|
||||
outlist = []
|
||||
|
||||
ca = component.ConnectComponentAuthenticator("cjid", "secret")
|
||||
xs = xmlstream.XmlStream(ca)
|
||||
xs.transport = DummyTransport(outlist)
|
||||
|
||||
xs.addObserver(xmlstream.STREAM_AUTHD_EVENT, self.authPassed)
|
||||
|
||||
# Go...
|
||||
xs.connectionMade()
|
||||
xs.dataReceived(
|
||||
b"<stream:stream xmlns='jabber:component:accept' xmlns:stream='http://etherx.jabber.org/streams' from='cjid' id='12345'>"
|
||||
)
|
||||
|
||||
# Calculate what we expect the handshake value to be
|
||||
hv = sha1(b"12345" + b"secret").hexdigest().encode("ascii")
|
||||
|
||||
self.assertEqual(outlist[1], b"<handshake>" + hv + b"</handshake>")
|
||||
|
||||
xs.dataReceived("<handshake/>")
|
||||
|
||||
self.assertEqual(self.authComplete, True)
|
||||
|
||||
|
||||
class ServiceTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{component.Service}.
|
||||
"""
|
||||
|
||||
def test_interface(self):
|
||||
"""
|
||||
L{component.Service} implements L{ijabber.IService}.
|
||||
"""
|
||||
service = component.Service()
|
||||
verifyObject(ijabber.IService, service)
|
||||
|
||||
|
||||
class JabberServiceHarness(component.Service):
|
||||
def __init__(self):
|
||||
self.componentConnectedFlag = False
|
||||
self.componentDisconnectedFlag = False
|
||||
self.transportConnectedFlag = False
|
||||
|
||||
def componentConnected(self, xmlstream):
|
||||
self.componentConnectedFlag = True
|
||||
|
||||
def componentDisconnected(self):
|
||||
self.componentDisconnectedFlag = True
|
||||
|
||||
def transportConnected(self, xmlstream):
|
||||
self.transportConnectedFlag = True
|
||||
|
||||
|
||||
class JabberServiceManagerTests(unittest.TestCase):
|
||||
def testSM(self):
|
||||
# Setup service manager and test harnes
|
||||
sm = component.ServiceManager("foo", "password")
|
||||
svc = JabberServiceHarness()
|
||||
svc.setServiceParent(sm)
|
||||
|
||||
# Create a write list
|
||||
wlist = []
|
||||
|
||||
# Setup a XmlStream
|
||||
xs = sm.getFactory().buildProtocol(None)
|
||||
xs.transport = self
|
||||
xs.transport.write = wlist.append
|
||||
|
||||
# Indicate that it's connected
|
||||
xs.connectionMade()
|
||||
|
||||
# Ensure the test service harness got notified
|
||||
self.assertEqual(True, svc.transportConnectedFlag)
|
||||
|
||||
# Jump ahead and pretend like the stream got auth'd
|
||||
xs.dispatch(xs, xmlstream.STREAM_AUTHD_EVENT)
|
||||
|
||||
# Ensure the test service harness got notified
|
||||
self.assertEqual(True, svc.componentConnectedFlag)
|
||||
|
||||
# Pretend to drop the connection
|
||||
xs.connectionLost(None)
|
||||
|
||||
# Ensure the test service harness got notified
|
||||
self.assertEqual(True, svc.componentDisconnectedFlag)
|
||||
|
||||
|
||||
class RouterTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{component.Router}.
|
||||
"""
|
||||
|
||||
def test_addRoute(self):
|
||||
"""
|
||||
Test route registration and routing on incoming stanzas.
|
||||
"""
|
||||
router = component.Router()
|
||||
routed = []
|
||||
router.route = lambda element: routed.append(element)
|
||||
|
||||
pipe = XmlPipe()
|
||||
router.addRoute("example.org", pipe.sink)
|
||||
self.assertEqual(1, len(router.routes))
|
||||
self.assertEqual(pipe.sink, router.routes["example.org"])
|
||||
|
||||
element = domish.Element(("testns", "test"))
|
||||
pipe.source.send(element)
|
||||
self.assertEqual([element], routed)
|
||||
|
||||
def test_route(self):
|
||||
"""
|
||||
Test routing of a message.
|
||||
"""
|
||||
component1 = XmlPipe()
|
||||
component2 = XmlPipe()
|
||||
router = component.Router()
|
||||
router.addRoute("component1.example.org", component1.sink)
|
||||
router.addRoute("component2.example.org", component2.sink)
|
||||
|
||||
outgoing = []
|
||||
component2.source.addObserver("/*", lambda element: outgoing.append(element))
|
||||
stanza = domish.Element((None, "presence"))
|
||||
stanza["from"] = "component1.example.org"
|
||||
stanza["to"] = "component2.example.org"
|
||||
component1.source.send(stanza)
|
||||
self.assertEqual([stanza], outgoing)
|
||||
|
||||
def test_routeDefault(self):
|
||||
"""
|
||||
Test routing of a message using the default route.
|
||||
|
||||
The default route is the one with L{None} as its key in the
|
||||
routing table. It is taken when there is no more specific route
|
||||
in the routing table that matches the stanza's destination.
|
||||
"""
|
||||
component1 = XmlPipe()
|
||||
s2s = XmlPipe()
|
||||
router = component.Router()
|
||||
router.addRoute("component1.example.org", component1.sink)
|
||||
router.addRoute(None, s2s.sink)
|
||||
|
||||
outgoing = []
|
||||
s2s.source.addObserver("/*", lambda element: outgoing.append(element))
|
||||
stanza = domish.Element((None, "presence"))
|
||||
stanza["from"] = "component1.example.org"
|
||||
stanza["to"] = "example.com"
|
||||
component1.source.send(stanza)
|
||||
self.assertEqual([stanza], outgoing)
|
||||
|
||||
|
||||
class ListenComponentAuthenticatorTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{component.ListenComponentAuthenticator}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.output = []
|
||||
authenticator = component.ListenComponentAuthenticator("secret")
|
||||
self.xmlstream = xmlstream.XmlStream(authenticator)
|
||||
self.xmlstream.send = self.output.append
|
||||
|
||||
def loseConnection(self):
|
||||
"""
|
||||
Stub loseConnection because we are a transport.
|
||||
"""
|
||||
self.xmlstream.connectionLost("no reason")
|
||||
|
||||
def test_streamStarted(self):
|
||||
"""
|
||||
The received stream header should set several attributes.
|
||||
"""
|
||||
observers = []
|
||||
|
||||
def addOnetimeObserver(event, observerfn):
|
||||
observers.append((event, observerfn))
|
||||
|
||||
xs = self.xmlstream
|
||||
xs.addOnetimeObserver = addOnetimeObserver
|
||||
|
||||
xs.makeConnection(self)
|
||||
self.assertIdentical(None, xs.sid)
|
||||
self.assertFalse(xs._headerSent)
|
||||
|
||||
xs.dataReceived(
|
||||
"<stream:stream xmlns='jabber:component:accept' "
|
||||
"xmlns:stream='http://etherx.jabber.org/streams' "
|
||||
"to='component.example.org'>"
|
||||
)
|
||||
self.assertEqual((0, 0), xs.version)
|
||||
self.assertNotIdentical(None, xs.sid)
|
||||
self.assertTrue(xs._headerSent)
|
||||
self.assertEqual(("/*", xs.authenticator.onElement), observers[-1])
|
||||
|
||||
def test_streamStartedWrongNamespace(self):
|
||||
"""
|
||||
The received stream header should have a correct namespace.
|
||||
"""
|
||||
streamErrors = []
|
||||
|
||||
xs = self.xmlstream
|
||||
xs.sendStreamError = streamErrors.append
|
||||
xs.makeConnection(self)
|
||||
xs.dataReceived(
|
||||
"<stream:stream xmlns='jabber:client' "
|
||||
"xmlns:stream='http://etherx.jabber.org/streams' "
|
||||
"to='component.example.org'>"
|
||||
)
|
||||
self.assertEqual(1, len(streamErrors))
|
||||
self.assertEqual("invalid-namespace", streamErrors[-1].condition)
|
||||
|
||||
def test_streamStartedNoTo(self):
|
||||
"""
|
||||
The received stream header should have a 'to' attribute.
|
||||
"""
|
||||
streamErrors = []
|
||||
|
||||
xs = self.xmlstream
|
||||
xs.sendStreamError = streamErrors.append
|
||||
xs.makeConnection(self)
|
||||
xs.dataReceived(
|
||||
"<stream:stream xmlns='jabber:component:accept' "
|
||||
"xmlns:stream='http://etherx.jabber.org/streams'>"
|
||||
)
|
||||
self.assertEqual(1, len(streamErrors))
|
||||
self.assertEqual("improper-addressing", streamErrors[-1].condition)
|
||||
|
||||
def test_onElement(self):
|
||||
"""
|
||||
We expect a handshake element with a hash.
|
||||
"""
|
||||
handshakes = []
|
||||
|
||||
xs = self.xmlstream
|
||||
xs.authenticator.onHandshake = handshakes.append
|
||||
|
||||
handshake = domish.Element(("jabber:component:accept", "handshake"))
|
||||
handshake.addContent("1234")
|
||||
xs.authenticator.onElement(handshake)
|
||||
self.assertEqual("1234", handshakes[-1])
|
||||
|
||||
def test_onElementNotHandshake(self):
|
||||
"""
|
||||
Reject elements that are not handshakes
|
||||
"""
|
||||
handshakes = []
|
||||
streamErrors = []
|
||||
|
||||
xs = self.xmlstream
|
||||
xs.authenticator.onHandshake = handshakes.append
|
||||
xs.sendStreamError = streamErrors.append
|
||||
|
||||
element = domish.Element(("jabber:component:accept", "message"))
|
||||
xs.authenticator.onElement(element)
|
||||
self.assertFalse(handshakes)
|
||||
self.assertEqual("not-authorized", streamErrors[-1].condition)
|
||||
|
||||
def test_onHandshake(self):
|
||||
"""
|
||||
Receiving a handshake matching the secret authenticates the stream.
|
||||
"""
|
||||
authd = []
|
||||
|
||||
def authenticated(xs):
|
||||
authd.append(xs)
|
||||
|
||||
xs = self.xmlstream
|
||||
xs.addOnetimeObserver(xmlstream.STREAM_AUTHD_EVENT, authenticated)
|
||||
xs.sid = "1234"
|
||||
theHash = "32532c0f7dbf1253c095b18b18e36d38d94c1256"
|
||||
xs.authenticator.onHandshake(theHash)
|
||||
self.assertEqual("<handshake/>", self.output[-1])
|
||||
self.assertEqual(1, len(authd))
|
||||
|
||||
def test_onHandshakeWrongHash(self):
|
||||
"""
|
||||
Receiving a bad handshake should yield a stream error.
|
||||
"""
|
||||
streamErrors = []
|
||||
authd = []
|
||||
|
||||
def authenticated(xs):
|
||||
authd.append(xs)
|
||||
|
||||
xs = self.xmlstream
|
||||
xs.addOnetimeObserver(xmlstream.STREAM_AUTHD_EVENT, authenticated)
|
||||
xs.sendStreamError = streamErrors.append
|
||||
|
||||
xs.sid = "1234"
|
||||
theHash = "1234"
|
||||
xs.authenticator.onHandshake(theHash)
|
||||
self.assertEqual("not-authorized", streamErrors[-1].condition)
|
||||
self.assertEqual(0, len(authd))
|
||||
|
||||
|
||||
class XMPPComponentServerFactoryTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{component.XMPPComponentServerFactory}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.router = component.Router()
|
||||
self.factory = component.XMPPComponentServerFactory(self.router, "secret")
|
||||
self.xmlstream = self.factory.buildProtocol(None)
|
||||
self.xmlstream.thisEntity = JID("component.example.org")
|
||||
|
||||
def test_makeConnection(self):
|
||||
"""
|
||||
A new connection increases the stream serial count. No logs by default.
|
||||
"""
|
||||
self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_CONNECTED_EVENT)
|
||||
self.assertEqual(0, self.xmlstream.serial)
|
||||
self.assertEqual(1, self.factory.serial)
|
||||
self.assertIdentical(None, self.xmlstream.rawDataInFn)
|
||||
self.assertIdentical(None, self.xmlstream.rawDataOutFn)
|
||||
|
||||
def test_makeConnectionLogTraffic(self):
|
||||
"""
|
||||
Setting logTraffic should set up raw data loggers.
|
||||
"""
|
||||
self.factory.logTraffic = True
|
||||
self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_CONNECTED_EVENT)
|
||||
self.assertNotIdentical(None, self.xmlstream.rawDataInFn)
|
||||
self.assertNotIdentical(None, self.xmlstream.rawDataOutFn)
|
||||
|
||||
def test_onError(self):
|
||||
"""
|
||||
An observer for stream errors should trigger onError to log it.
|
||||
"""
|
||||
self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_CONNECTED_EVENT)
|
||||
|
||||
class TestError(Exception):
|
||||
pass
|
||||
|
||||
reason = failure.Failure(TestError())
|
||||
self.xmlstream.dispatch(reason, xmlstream.STREAM_ERROR_EVENT)
|
||||
self.assertEqual(1, len(self.flushLoggedErrors(TestError)))
|
||||
|
||||
def test_connectionInitialized(self):
|
||||
"""
|
||||
Make sure a new stream is added to the routing table.
|
||||
"""
|
||||
self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
|
||||
self.assertIn("component.example.org", self.router.routes)
|
||||
self.assertIdentical(
|
||||
self.xmlstream, self.router.routes["component.example.org"]
|
||||
)
|
||||
|
||||
def test_connectionLost(self):
|
||||
"""
|
||||
Make sure a stream is removed from the routing table on disconnect.
|
||||
"""
|
||||
self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
|
||||
self.xmlstream.dispatch(None, xmlstream.STREAM_END_EVENT)
|
||||
self.assertNotIn("component.example.org", self.router.routes)
|
||||
@@ -0,0 +1,315 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.words.protocols.jabber.error}.
|
||||
"""
|
||||
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.words.protocols.jabber import error
|
||||
from twisted.words.xish import domish
|
||||
|
||||
NS_XML = "http://www.w3.org/XML/1998/namespace"
|
||||
NS_STREAMS = "http://etherx.jabber.org/streams"
|
||||
NS_XMPP_STREAMS = "urn:ietf:params:xml:ns:xmpp-streams"
|
||||
NS_XMPP_STANZAS = "urn:ietf:params:xml:ns:xmpp-stanzas"
|
||||
|
||||
|
||||
class BaseErrorTests(unittest.TestCase):
|
||||
def test_getElementPlain(self) -> None:
|
||||
"""
|
||||
Test getting an element for a plain error.
|
||||
"""
|
||||
e = error.BaseError("feature-not-implemented")
|
||||
element = e.getElement()
|
||||
self.assertIdentical(element.uri, None)
|
||||
self.assertEqual(len(element.children), 1)
|
||||
|
||||
def test_getElementText(self) -> None:
|
||||
"""
|
||||
Test getting an element for an error with a text.
|
||||
"""
|
||||
e = error.BaseError("feature-not-implemented", "text")
|
||||
element = e.getElement()
|
||||
self.assertEqual(len(element.children), 2)
|
||||
self.assertEqual(str(element.text), "text")
|
||||
self.assertEqual(element.text.getAttribute((NS_XML, "lang")), None)
|
||||
|
||||
def test_getElementTextLang(self) -> None:
|
||||
"""
|
||||
Test getting an element for an error with a text and language.
|
||||
"""
|
||||
e = error.BaseError("feature-not-implemented", "text", "en_US")
|
||||
element = e.getElement()
|
||||
self.assertEqual(len(element.children), 2)
|
||||
self.assertEqual(str(element.text), "text")
|
||||
self.assertEqual(element.text[(NS_XML, "lang")], "en_US")
|
||||
|
||||
def test_getElementAppCondition(self) -> None:
|
||||
"""
|
||||
Test getting an element for an error with an app specific condition.
|
||||
"""
|
||||
ac = domish.Element(("testns", "myerror"))
|
||||
e = error.BaseError("feature-not-implemented", appCondition=ac)
|
||||
element = e.getElement()
|
||||
self.assertEqual(len(element.children), 2)
|
||||
self.assertEqual(element.myerror, ac)
|
||||
|
||||
|
||||
class StreamErrorTests(unittest.TestCase):
|
||||
def test_getElementPlain(self) -> None:
|
||||
"""
|
||||
Test namespace of the element representation of an error.
|
||||
"""
|
||||
e = error.StreamError("feature-not-implemented")
|
||||
element = e.getElement()
|
||||
self.assertEqual(element.uri, NS_STREAMS)
|
||||
|
||||
def test_getElementConditionNamespace(self) -> None:
|
||||
"""
|
||||
Test that the error condition element has the correct namespace.
|
||||
"""
|
||||
e = error.StreamError("feature-not-implemented")
|
||||
element = e.getElement()
|
||||
self.assertEqual(
|
||||
NS_XMPP_STREAMS, getattr(element, "feature-not-implemented").uri
|
||||
)
|
||||
|
||||
def test_getElementTextNamespace(self) -> None:
|
||||
"""
|
||||
Test that the error text element has the correct namespace.
|
||||
"""
|
||||
e = error.StreamError("feature-not-implemented", "text")
|
||||
element = e.getElement()
|
||||
self.assertEqual(NS_XMPP_STREAMS, element.text.uri)
|
||||
|
||||
|
||||
class StanzaErrorTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{error.StreamError}.
|
||||
"""
|
||||
|
||||
def test_typeRemoteServerTimeout(self) -> None:
|
||||
"""
|
||||
Remote Server Timeout should yield type wait, code 504.
|
||||
"""
|
||||
e = error.StanzaError("remote-server-timeout")
|
||||
self.assertEqual("wait", e.type)
|
||||
self.assertEqual("504", e.code)
|
||||
|
||||
def test_getElementPlain(self) -> None:
|
||||
"""
|
||||
Test getting an element for a plain stanza error.
|
||||
"""
|
||||
e = error.StanzaError("feature-not-implemented")
|
||||
element = e.getElement()
|
||||
self.assertEqual(element.uri, None)
|
||||
self.assertEqual(element["type"], "cancel")
|
||||
self.assertEqual(element["code"], "501")
|
||||
|
||||
def test_getElementType(self) -> None:
|
||||
"""
|
||||
Test getting an element for a stanza error with a given type.
|
||||
"""
|
||||
e = error.StanzaError("feature-not-implemented", "auth")
|
||||
element = e.getElement()
|
||||
self.assertEqual(element.uri, None)
|
||||
self.assertEqual(element["type"], "auth")
|
||||
self.assertEqual(element["code"], "501")
|
||||
|
||||
def test_getElementConditionNamespace(self) -> None:
|
||||
"""
|
||||
Test that the error condition element has the correct namespace.
|
||||
"""
|
||||
e = error.StanzaError("feature-not-implemented")
|
||||
element = e.getElement()
|
||||
self.assertEqual(
|
||||
NS_XMPP_STANZAS, getattr(element, "feature-not-implemented").uri
|
||||
)
|
||||
|
||||
def test_getElementTextNamespace(self) -> None:
|
||||
"""
|
||||
Test that the error text element has the correct namespace.
|
||||
"""
|
||||
e = error.StanzaError("feature-not-implemented", text="text")
|
||||
element = e.getElement()
|
||||
self.assertEqual(NS_XMPP_STANZAS, element.text.uri)
|
||||
|
||||
def test_toResponse(self) -> None:
|
||||
"""
|
||||
Test an error response is generated from a stanza.
|
||||
|
||||
The addressing on the (new) response stanza should be reversed, an
|
||||
error child (with proper properties) added and the type set to
|
||||
C{'error'}.
|
||||
"""
|
||||
stanza = domish.Element(("jabber:client", "message"))
|
||||
stanza["type"] = "chat"
|
||||
stanza["to"] = "user1@example.com"
|
||||
stanza["from"] = "user2@example.com/resource"
|
||||
e = error.StanzaError("service-unavailable")
|
||||
response = e.toResponse(stanza)
|
||||
self.assertNotIdentical(response, stanza)
|
||||
self.assertEqual(response["from"], "user1@example.com")
|
||||
self.assertEqual(response["to"], "user2@example.com/resource")
|
||||
self.assertEqual(response["type"], "error")
|
||||
self.assertEqual(response.error.children[0].name, "service-unavailable")
|
||||
self.assertEqual(response.error["type"], "cancel")
|
||||
self.assertNotEqual(stanza.children, response.children)
|
||||
|
||||
|
||||
class ParseErrorTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{error._parseError}.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.error = domish.Element((None, "error"))
|
||||
|
||||
def test_empty(self) -> None:
|
||||
"""
|
||||
Test parsing of the empty error element.
|
||||
"""
|
||||
result = error._parseError(self.error, "errorns")
|
||||
self.assertEqual(
|
||||
{"condition": None, "text": None, "textLang": None, "appCondition": None},
|
||||
result,
|
||||
)
|
||||
|
||||
def test_condition(self) -> None:
|
||||
"""
|
||||
Test parsing of an error element with a condition.
|
||||
"""
|
||||
self.error.addElement(("errorns", "bad-request"))
|
||||
result = error._parseError(self.error, "errorns")
|
||||
self.assertEqual("bad-request", result["condition"])
|
||||
|
||||
def test_text(self) -> None:
|
||||
"""
|
||||
Test parsing of an error element with a text.
|
||||
"""
|
||||
text = self.error.addElement(("errorns", "text"))
|
||||
text.addContent("test")
|
||||
result = error._parseError(self.error, "errorns")
|
||||
self.assertEqual("test", result["text"])
|
||||
self.assertEqual(None, result["textLang"])
|
||||
|
||||
def test_textLang(self) -> None:
|
||||
"""
|
||||
Test parsing of an error element with a text with a defined language.
|
||||
"""
|
||||
text = self.error.addElement(("errorns", "text"))
|
||||
text[NS_XML, "lang"] = "en_US"
|
||||
text.addContent("test")
|
||||
result = error._parseError(self.error, "errorns")
|
||||
self.assertEqual("en_US", result["textLang"])
|
||||
|
||||
def test_appCondition(self) -> None:
|
||||
"""
|
||||
Test parsing of an error element with an app specific condition.
|
||||
"""
|
||||
condition = self.error.addElement(("testns", "condition"))
|
||||
result = error._parseError(self.error, "errorns")
|
||||
self.assertEqual(condition, result["appCondition"])
|
||||
|
||||
def test_appConditionMultiple(self) -> None:
|
||||
"""
|
||||
Test parsing of an error element with multiple app specific conditions.
|
||||
"""
|
||||
self.error.addElement(("testns", "condition"))
|
||||
condition = self.error.addElement(("testns", "condition2"))
|
||||
result = error._parseError(self.error, "errorns")
|
||||
self.assertEqual(condition, result["appCondition"])
|
||||
|
||||
|
||||
class ExceptionFromStanzaTests(unittest.TestCase):
|
||||
def test_basic(self) -> None:
|
||||
"""
|
||||
Test basic operations of exceptionFromStanza.
|
||||
|
||||
Given a realistic stanza, check if a sane exception is returned.
|
||||
|
||||
Using this stanza::
|
||||
|
||||
<iq type='error'
|
||||
from='pubsub.shakespeare.lit'
|
||||
to='francisco@denmark.lit/barracks'
|
||||
id='subscriptions1'>
|
||||
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
|
||||
<subscriptions/>
|
||||
</pubsub>
|
||||
<error type='cancel'>
|
||||
<feature-not-implemented
|
||||
xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||
<unsupported xmlns='http://jabber.org/protocol/pubsub#errors'
|
||||
feature='retrieve-subscriptions'/>
|
||||
</error>
|
||||
</iq>
|
||||
"""
|
||||
|
||||
stanza = domish.Element((None, "stanza"))
|
||||
p = stanza.addElement(("http://jabber.org/protocol/pubsub", "pubsub"))
|
||||
p.addElement("subscriptions")
|
||||
e = stanza.addElement("error")
|
||||
e["type"] = "cancel"
|
||||
e.addElement((NS_XMPP_STANZAS, "feature-not-implemented"))
|
||||
uc = e.addElement(("http://jabber.org/protocol/pubsub#errors", "unsupported"))
|
||||
uc["feature"] = "retrieve-subscriptions"
|
||||
|
||||
result = error.exceptionFromStanza(stanza)
|
||||
self.assertIsInstance(result, error.StanzaError)
|
||||
self.assertEqual("feature-not-implemented", result.condition)
|
||||
self.assertEqual("cancel", result.type)
|
||||
self.assertEqual(uc, result.appCondition)
|
||||
self.assertEqual([p], result.children)
|
||||
|
||||
def test_legacy(self) -> None:
|
||||
"""
|
||||
Test legacy operations of exceptionFromStanza.
|
||||
|
||||
Given a realistic stanza with only legacy (pre-XMPP) error information,
|
||||
check if a sane exception is returned.
|
||||
|
||||
Using this stanza::
|
||||
|
||||
<message type='error'
|
||||
to='piers@pipetree.com/Home'
|
||||
from='qmacro@jaber.org'>
|
||||
<body>Are you there?</body>
|
||||
<error code='502'>Unable to resolve hostname.</error>
|
||||
</message>
|
||||
"""
|
||||
stanza = domish.Element((None, "stanza"))
|
||||
p = stanza.addElement("body", content="Are you there?")
|
||||
e = stanza.addElement("error", content="Unable to resolve hostname.")
|
||||
e["code"] = "502"
|
||||
|
||||
result = error.exceptionFromStanza(stanza)
|
||||
self.assertIsInstance(result, error.StanzaError)
|
||||
self.assertEqual("service-unavailable", result.condition)
|
||||
self.assertEqual("wait", result.type)
|
||||
self.assertEqual("Unable to resolve hostname.", result.text)
|
||||
self.assertEqual([p], result.children)
|
||||
|
||||
|
||||
class ExceptionFromStreamErrorTests(unittest.TestCase):
|
||||
def test_basic(self) -> None:
|
||||
"""
|
||||
Test basic operations of exceptionFromStreamError.
|
||||
|
||||
Given a realistic stream error, check if a sane exception is returned.
|
||||
|
||||
Using this error::
|
||||
|
||||
<stream:error xmlns:stream='http://etherx.jabber.org/streams'>
|
||||
<xml-not-well-formed xmlns='urn:ietf:params:xml:ns:xmpp-streams'/>
|
||||
</stream:error>
|
||||
"""
|
||||
|
||||
e = domish.Element(("http://etherx.jabber.org/streams", "error"))
|
||||
e.addElement((NS_XMPP_STREAMS, "xml-not-well-formed"))
|
||||
|
||||
result = error.exceptionFromStreamError(e)
|
||||
self.assertIsInstance(result, error.StreamError)
|
||||
self.assertEqual("xml-not-well-formed", result.condition)
|
||||
@@ -0,0 +1,220 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.words.protocols.jabber.jid}.
|
||||
"""
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.words.protocols.jabber import jid
|
||||
|
||||
|
||||
class JIDParsingTests(unittest.TestCase):
|
||||
def test_parse(self) -> None:
|
||||
"""
|
||||
Test different forms of JIDs.
|
||||
"""
|
||||
# Basic forms
|
||||
self.assertEqual(jid.parse("user@host/resource"), ("user", "host", "resource"))
|
||||
self.assertEqual(jid.parse("user@host"), ("user", "host", None))
|
||||
self.assertEqual(jid.parse("host"), (None, "host", None))
|
||||
self.assertEqual(jid.parse("host/resource"), (None, "host", "resource"))
|
||||
|
||||
# More interesting forms
|
||||
self.assertEqual(jid.parse("foo/bar@baz"), (None, "foo", "bar@baz"))
|
||||
self.assertEqual(jid.parse("boo@foo/bar@baz"), ("boo", "foo", "bar@baz"))
|
||||
self.assertEqual(jid.parse("boo@foo/bar/baz"), ("boo", "foo", "bar/baz"))
|
||||
self.assertEqual(jid.parse("boo/foo@bar@baz"), (None, "boo", "foo@bar@baz"))
|
||||
self.assertEqual(jid.parse("boo/foo/bar"), (None, "boo", "foo/bar"))
|
||||
self.assertEqual(jid.parse("boo//foo"), (None, "boo", "/foo"))
|
||||
|
||||
def test_noHost(self) -> None:
|
||||
"""
|
||||
Test for failure on no host part.
|
||||
"""
|
||||
self.assertRaises(jid.InvalidFormat, jid.parse, "user@")
|
||||
|
||||
def test_doubleAt(self) -> None:
|
||||
"""
|
||||
Test for failure on double @ signs.
|
||||
|
||||
This should fail because @ is not a valid character for the host
|
||||
part of the JID.
|
||||
"""
|
||||
self.assertRaises(jid.InvalidFormat, jid.parse, "user@@host")
|
||||
|
||||
def test_multipleAt(self) -> None:
|
||||
"""
|
||||
Test for failure on two @ signs.
|
||||
|
||||
This should fail because @ is not a valid character for the host
|
||||
part of the JID.
|
||||
"""
|
||||
self.assertRaises(jid.InvalidFormat, jid.parse, "user@host@host")
|
||||
|
||||
# Basic tests for case mapping. These are fallback tests for the
|
||||
# prepping done in twisted.words.protocols.jabber.xmpp_stringprep
|
||||
|
||||
def test_prepCaseMapUser(self) -> None:
|
||||
"""
|
||||
Test case mapping of the user part of the JID.
|
||||
"""
|
||||
self.assertEqual(
|
||||
jid.prep("UsEr", "host", "resource"), ("user", "host", "resource")
|
||||
)
|
||||
|
||||
def test_prepCaseMapHost(self) -> None:
|
||||
"""
|
||||
Test case mapping of the host part of the JID.
|
||||
"""
|
||||
self.assertEqual(
|
||||
jid.prep("user", "hoST", "resource"), ("user", "host", "resource")
|
||||
)
|
||||
|
||||
def test_prepNoCaseMapResource(self) -> None:
|
||||
"""
|
||||
Test no case mapping of the resourcce part of the JID.
|
||||
"""
|
||||
self.assertEqual(
|
||||
jid.prep("user", "hoST", "resource"), ("user", "host", "resource")
|
||||
)
|
||||
self.assertNotEqual(
|
||||
jid.prep("user", "host", "Resource"), ("user", "host", "resource")
|
||||
)
|
||||
|
||||
|
||||
class JIDTests(unittest.TestCase):
|
||||
def test_noneArguments(self) -> None:
|
||||
"""
|
||||
Test that using no arguments raises an exception.
|
||||
"""
|
||||
self.assertRaises(RuntimeError, jid.JID)
|
||||
|
||||
def test_attributes(self) -> None:
|
||||
"""
|
||||
Test that the attributes correspond with the JID parts.
|
||||
"""
|
||||
j = jid.JID("user@host/resource")
|
||||
self.assertEqual(j.user, "user")
|
||||
self.assertEqual(j.host, "host")
|
||||
self.assertEqual(j.resource, "resource")
|
||||
|
||||
def test_userhost(self) -> None:
|
||||
"""
|
||||
Test the extraction of the bare JID.
|
||||
"""
|
||||
j = jid.JID("user@host/resource")
|
||||
self.assertEqual("user@host", j.userhost())
|
||||
|
||||
def test_userhostOnlyHost(self) -> None:
|
||||
"""
|
||||
Test the extraction of the bare JID of the full form host/resource.
|
||||
"""
|
||||
j = jid.JID("host/resource")
|
||||
self.assertEqual("host", j.userhost())
|
||||
|
||||
def test_userhostJID(self) -> None:
|
||||
"""
|
||||
Test getting a JID object of the bare JID.
|
||||
"""
|
||||
j1 = jid.JID("user@host/resource")
|
||||
j2 = jid.internJID("user@host")
|
||||
self.assertIdentical(j2, j1.userhostJID())
|
||||
|
||||
def test_userhostJIDNoResource(self) -> None:
|
||||
"""
|
||||
Test getting a JID object of the bare JID when there was no resource.
|
||||
"""
|
||||
j = jid.JID("user@host")
|
||||
self.assertIdentical(j, j.userhostJID())
|
||||
|
||||
def test_fullHost(self) -> None:
|
||||
"""
|
||||
Test giving a string representation of the JID with only a host part.
|
||||
"""
|
||||
j = jid.JID(tuple=(None, "host", None))
|
||||
self.assertEqual("host", j.full())
|
||||
|
||||
def test_fullHostResource(self) -> None:
|
||||
"""
|
||||
Test giving a string representation of the JID with host, resource.
|
||||
"""
|
||||
j = jid.JID(tuple=(None, "host", "resource"))
|
||||
self.assertEqual("host/resource", j.full())
|
||||
|
||||
def test_fullUserHost(self) -> None:
|
||||
"""
|
||||
Test giving a string representation of the JID with user, host.
|
||||
"""
|
||||
j = jid.JID(tuple=("user", "host", None))
|
||||
self.assertEqual("user@host", j.full())
|
||||
|
||||
def test_fullAll(self) -> None:
|
||||
"""
|
||||
Test giving a string representation of the JID.
|
||||
"""
|
||||
j = jid.JID(tuple=("user", "host", "resource"))
|
||||
self.assertEqual("user@host/resource", j.full())
|
||||
|
||||
def test_equality(self) -> None:
|
||||
"""
|
||||
Test JID equality.
|
||||
"""
|
||||
j1 = jid.JID("user@host/resource")
|
||||
j2 = jid.JID("user@host/resource")
|
||||
self.assertNotIdentical(j1, j2)
|
||||
self.assertEqual(j1, j2)
|
||||
|
||||
def test_equalityWithNonJIDs(self) -> None:
|
||||
"""
|
||||
Test JID equality.
|
||||
"""
|
||||
j = jid.JID("user@host/resource")
|
||||
self.assertFalse(j == "user@host/resource")
|
||||
|
||||
def test_inequality(self) -> None:
|
||||
"""
|
||||
Test JID inequality.
|
||||
"""
|
||||
j1 = jid.JID("user1@host/resource")
|
||||
j2 = jid.JID("user2@host/resource")
|
||||
self.assertNotEqual(j1, j2)
|
||||
|
||||
def test_inequalityWithNonJIDs(self) -> None:
|
||||
"""
|
||||
Test JID equality.
|
||||
"""
|
||||
j = jid.JID("user@host/resource")
|
||||
self.assertNotEqual(j, "user@host/resource")
|
||||
|
||||
def test_hashable(self) -> None:
|
||||
"""
|
||||
Test JID hashability.
|
||||
"""
|
||||
j1 = jid.JID("user@host/resource")
|
||||
j2 = jid.JID("user@host/resource")
|
||||
self.assertEqual(hash(j1), hash(j2))
|
||||
|
||||
def test_str(self) -> None:
|
||||
"""
|
||||
Test unicode representation of JIDs.
|
||||
"""
|
||||
j = jid.JID(tuple=("user", "host", "resource"))
|
||||
self.assertEqual("user@host/resource", str(j))
|
||||
|
||||
def test_repr(self) -> None:
|
||||
"""
|
||||
Test representation of JID objects.
|
||||
"""
|
||||
j = jid.JID(tuple=("user", "host", "resource"))
|
||||
self.assertEqual("JID(%s)" % repr("user@host/resource"), repr(j))
|
||||
|
||||
|
||||
class InternJIDTests(unittest.TestCase):
|
||||
def test_identity(self) -> None:
|
||||
"""
|
||||
Test that two interned JIDs yield the same object.
|
||||
"""
|
||||
j1 = jid.internJID("user@host")
|
||||
j2 = jid.internJID("user@host")
|
||||
self.assertIdentical(j1, j2)
|
||||
@@ -0,0 +1,33 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.words.protocols.jabber.jstrports}.
|
||||
"""
|
||||
|
||||
|
||||
from twisted.application.internet import TCPClient
|
||||
from twisted.trial import unittest
|
||||
from twisted.words.protocols.jabber import jstrports
|
||||
|
||||
|
||||
class JabberStrPortsPlaceHolderTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{jstrports}
|
||||
"""
|
||||
|
||||
def test_parse(self):
|
||||
"""
|
||||
L{jstrports.parse} accepts an endpoint description string and returns a
|
||||
tuple and dict of parsed endpoint arguments.
|
||||
"""
|
||||
expected = ("TCP", ("DOMAIN", 65535, "Factory"), {})
|
||||
got = jstrports.parse("tcp:DOMAIN:65535", "Factory")
|
||||
self.assertEqual(expected, got)
|
||||
|
||||
def test_client(self):
|
||||
"""
|
||||
L{jstrports.client} returns a L{TCPClient} service.
|
||||
"""
|
||||
got = jstrports.client("tcp:DOMAIN:65535", "Factory")
|
||||
self.assertIsInstance(got, TCPClient)
|
||||
@@ -0,0 +1,279 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.trial import unittest
|
||||
from twisted.words.protocols.jabber import jid, sasl, sasl_mechanisms, xmlstream
|
||||
from twisted.words.xish import domish
|
||||
|
||||
NS_XMPP_SASL = "urn:ietf:params:xml:ns:xmpp-sasl"
|
||||
|
||||
|
||||
@implementer(sasl_mechanisms.ISASLMechanism)
|
||||
class DummySASLMechanism:
|
||||
"""
|
||||
Dummy SASL mechanism.
|
||||
|
||||
This just returns the initialResponse passed on creation, stores any
|
||||
challenges and replies with the value of C{response}.
|
||||
|
||||
@ivar challenge: Last received challenge.
|
||||
@type challenge: C{unicode}.
|
||||
@ivar initialResponse: Initial response to be returned when requested
|
||||
via C{getInitialResponse} or L{None}.
|
||||
@type initialResponse: C{unicode}
|
||||
"""
|
||||
|
||||
challenge = None
|
||||
name = "DUMMY"
|
||||
response = b""
|
||||
|
||||
def __init__(self, initialResponse):
|
||||
self.initialResponse = initialResponse
|
||||
|
||||
def getInitialResponse(self):
|
||||
return self.initialResponse
|
||||
|
||||
def getResponse(self, challenge):
|
||||
self.challenge = challenge
|
||||
return self.response
|
||||
|
||||
|
||||
class DummySASLInitiatingInitializer(sasl.SASLInitiatingInitializer):
|
||||
"""
|
||||
Dummy SASL Initializer for initiating entities.
|
||||
|
||||
This hardwires the SASL mechanism to L{DummySASLMechanism}, that is
|
||||
instantiated with the value of C{initialResponse}.
|
||||
|
||||
@ivar initialResponse: The initial response to be returned by the
|
||||
dummy SASL mechanism or L{None}.
|
||||
@type initialResponse: C{unicode}.
|
||||
"""
|
||||
|
||||
initialResponse = None
|
||||
|
||||
def setMechanism(self):
|
||||
self.mechanism = DummySASLMechanism(self.initialResponse)
|
||||
|
||||
|
||||
class SASLInitiatingInitializerTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{sasl.SASLInitiatingInitializer}
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.output = []
|
||||
|
||||
self.authenticator = xmlstream.Authenticator()
|
||||
self.xmlstream = xmlstream.XmlStream(self.authenticator)
|
||||
self.xmlstream.send = self.output.append
|
||||
self.xmlstream.connectionMade()
|
||||
self.xmlstream.dataReceived(
|
||||
b"<stream:stream xmlns='jabber:client' "
|
||||
b"xmlns:stream='http://etherx.jabber.org/streams' "
|
||||
b"from='example.com' id='12345' version='1.0'>"
|
||||
)
|
||||
self.init = DummySASLInitiatingInitializer(self.xmlstream)
|
||||
|
||||
def test_onFailure(self):
|
||||
"""
|
||||
Test that the SASL error condition is correctly extracted.
|
||||
"""
|
||||
failure = domish.Element(("urn:ietf:params:xml:ns:xmpp-sasl", "failure"))
|
||||
failure.addElement("not-authorized")
|
||||
self.init._deferred = defer.Deferred()
|
||||
self.init.onFailure(failure)
|
||||
self.assertFailure(self.init._deferred, sasl.SASLAuthError)
|
||||
self.init._deferred.addCallback(
|
||||
lambda e: self.assertEqual("not-authorized", e.condition)
|
||||
)
|
||||
return self.init._deferred
|
||||
|
||||
def test_sendAuthInitialResponse(self):
|
||||
"""
|
||||
Test starting authentication with an initial response.
|
||||
"""
|
||||
self.init.initialResponse = b"dummy"
|
||||
self.init.start()
|
||||
auth = self.output[0]
|
||||
self.assertEqual(NS_XMPP_SASL, auth.uri)
|
||||
self.assertEqual("auth", auth.name)
|
||||
self.assertEqual("DUMMY", auth["mechanism"])
|
||||
self.assertEqual("ZHVtbXk=", str(auth))
|
||||
|
||||
def test_sendAuthNoInitialResponse(self):
|
||||
"""
|
||||
Test starting authentication without an initial response.
|
||||
"""
|
||||
self.init.initialResponse = None
|
||||
self.init.start()
|
||||
auth = self.output[0]
|
||||
self.assertEqual("", str(auth))
|
||||
|
||||
def test_sendAuthEmptyInitialResponse(self):
|
||||
"""
|
||||
Test starting authentication where the initial response is empty.
|
||||
"""
|
||||
self.init.initialResponse = b""
|
||||
self.init.start()
|
||||
auth = self.output[0]
|
||||
self.assertEqual("=", str(auth))
|
||||
|
||||
def test_onChallenge(self):
|
||||
"""
|
||||
Test receiving a challenge message.
|
||||
"""
|
||||
d = self.init.start()
|
||||
challenge = domish.Element((NS_XMPP_SASL, "challenge"))
|
||||
challenge.addContent("bXkgY2hhbGxlbmdl")
|
||||
self.init.onChallenge(challenge)
|
||||
self.assertEqual(b"my challenge", self.init.mechanism.challenge)
|
||||
self.init.onSuccess(None)
|
||||
return d
|
||||
|
||||
def test_onChallengeResponse(self):
|
||||
"""
|
||||
A non-empty response gets encoded and included as character data.
|
||||
"""
|
||||
d = self.init.start()
|
||||
challenge = domish.Element((NS_XMPP_SASL, "challenge"))
|
||||
challenge.addContent("bXkgY2hhbGxlbmdl")
|
||||
self.init.mechanism.response = b"response"
|
||||
self.init.onChallenge(challenge)
|
||||
response = self.output[1]
|
||||
self.assertEqual("cmVzcG9uc2U=", str(response))
|
||||
self.init.onSuccess(None)
|
||||
return d
|
||||
|
||||
def test_onChallengeEmpty(self):
|
||||
"""
|
||||
Test receiving an empty challenge message.
|
||||
"""
|
||||
d = self.init.start()
|
||||
challenge = domish.Element((NS_XMPP_SASL, "challenge"))
|
||||
self.init.onChallenge(challenge)
|
||||
self.assertEqual(b"", self.init.mechanism.challenge)
|
||||
self.init.onSuccess(None)
|
||||
return d
|
||||
|
||||
def test_onChallengeIllegalPadding(self):
|
||||
"""
|
||||
Test receiving a challenge message with illegal padding.
|
||||
"""
|
||||
d = self.init.start()
|
||||
challenge = domish.Element((NS_XMPP_SASL, "challenge"))
|
||||
challenge.addContent("bXkg=Y2hhbGxlbmdl")
|
||||
self.init.onChallenge(challenge)
|
||||
self.assertFailure(d, sasl.SASLIncorrectEncodingError)
|
||||
return d
|
||||
|
||||
def test_onChallengeIllegalCharacters(self):
|
||||
"""
|
||||
Test receiving a challenge message with illegal characters.
|
||||
"""
|
||||
d = self.init.start()
|
||||
challenge = domish.Element((NS_XMPP_SASL, "challenge"))
|
||||
challenge.addContent("bXkg*Y2hhbGxlbmdl")
|
||||
self.init.onChallenge(challenge)
|
||||
self.assertFailure(d, sasl.SASLIncorrectEncodingError)
|
||||
return d
|
||||
|
||||
def test_onChallengeMalformed(self):
|
||||
"""
|
||||
Test receiving a malformed challenge message.
|
||||
"""
|
||||
d = self.init.start()
|
||||
challenge = domish.Element((NS_XMPP_SASL, "challenge"))
|
||||
challenge.addContent("a")
|
||||
self.init.onChallenge(challenge)
|
||||
self.assertFailure(d, sasl.SASLIncorrectEncodingError)
|
||||
return d
|
||||
|
||||
|
||||
class SASLInitiatingInitializerSetMechanismTests(unittest.TestCase):
|
||||
"""
|
||||
Test for L{sasl.SASLInitiatingInitializer.setMechanism}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.output = []
|
||||
|
||||
self.authenticator = xmlstream.Authenticator()
|
||||
self.xmlstream = xmlstream.XmlStream(self.authenticator)
|
||||
self.xmlstream.send = self.output.append
|
||||
self.xmlstream.connectionMade()
|
||||
self.xmlstream.dataReceived(
|
||||
"<stream:stream xmlns='jabber:client' "
|
||||
"xmlns:stream='http://etherx.jabber.org/streams' "
|
||||
"from='example.com' id='12345' version='1.0'>"
|
||||
)
|
||||
|
||||
self.init = sasl.SASLInitiatingInitializer(self.xmlstream)
|
||||
|
||||
def _setMechanism(self, name):
|
||||
"""
|
||||
Set up the XML Stream to have a SASL feature with the given mechanism.
|
||||
"""
|
||||
feature = domish.Element((NS_XMPP_SASL, "mechanisms"))
|
||||
feature.addElement("mechanism", content=name)
|
||||
self.xmlstream.features[(feature.uri, feature.name)] = feature
|
||||
|
||||
self.init.setMechanism()
|
||||
return self.init.mechanism.name
|
||||
|
||||
def test_anonymous(self):
|
||||
"""
|
||||
Test setting ANONYMOUS as the authentication mechanism.
|
||||
"""
|
||||
self.authenticator.jid = jid.JID("example.com")
|
||||
self.authenticator.password = None
|
||||
name = "ANONYMOUS"
|
||||
|
||||
self.assertEqual(name, self._setMechanism(name))
|
||||
|
||||
def test_plain(self):
|
||||
"""
|
||||
Test setting PLAIN as the authentication mechanism.
|
||||
"""
|
||||
self.authenticator.jid = jid.JID("test@example.com")
|
||||
self.authenticator.password = "secret"
|
||||
name = "PLAIN"
|
||||
|
||||
self.assertEqual(name, self._setMechanism(name))
|
||||
|
||||
def test_digest(self):
|
||||
"""
|
||||
Test setting DIGEST-MD5 as the authentication mechanism.
|
||||
"""
|
||||
self.authenticator.jid = jid.JID("test@example.com")
|
||||
self.authenticator.password = "secret"
|
||||
name = "DIGEST-MD5"
|
||||
|
||||
self.assertEqual(name, self._setMechanism(name))
|
||||
|
||||
def test_notAcceptable(self):
|
||||
"""
|
||||
Test using an unacceptable SASL authentication mechanism.
|
||||
"""
|
||||
|
||||
self.authenticator.jid = jid.JID("test@example.com")
|
||||
self.authenticator.password = "secret"
|
||||
|
||||
self.assertRaises(
|
||||
sasl.SASLNoAcceptableMechanism, self._setMechanism, "SOMETHING_UNACCEPTABLE"
|
||||
)
|
||||
|
||||
def test_notAcceptableWithoutUser(self):
|
||||
"""
|
||||
Test using an unacceptable SASL authentication mechanism with no JID.
|
||||
"""
|
||||
self.authenticator.jid = jid.JID("example.com")
|
||||
self.authenticator.password = "secret"
|
||||
|
||||
self.assertRaises(
|
||||
sasl.SASLNoAcceptableMechanism, self._setMechanism, "SOMETHING_UNACCEPTABLE"
|
||||
)
|
||||
@@ -0,0 +1,180 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.words.protocols.jabber.sasl_mechanisms}.
|
||||
"""
|
||||
|
||||
|
||||
from twisted.python.compat import networkString
|
||||
from twisted.trial import unittest
|
||||
from twisted.words.protocols.jabber import sasl_mechanisms
|
||||
|
||||
|
||||
class PlainTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{twisted.words.protocols.jabber.sasl_mechanisms.Plain}.
|
||||
"""
|
||||
|
||||
def test_getInitialResponse(self) -> None:
|
||||
"""
|
||||
Test the initial response.
|
||||
"""
|
||||
m = sasl_mechanisms.Plain(None, "test", "secret")
|
||||
self.assertEqual(m.getInitialResponse(), b"\x00test\x00secret")
|
||||
|
||||
|
||||
class AnonymousTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{twisted.words.protocols.jabber.sasl_mechanisms.Anonymous}.
|
||||
"""
|
||||
|
||||
def test_getInitialResponse(self) -> None:
|
||||
"""
|
||||
Test the initial response to be empty.
|
||||
"""
|
||||
m = sasl_mechanisms.Anonymous()
|
||||
self.assertEqual(m.getInitialResponse(), None)
|
||||
|
||||
|
||||
class DigestMD5Tests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{twisted.words.protocols.jabber.sasl_mechanisms.DigestMD5}.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.mechanism = sasl_mechanisms.DigestMD5(
|
||||
"xmpp", "example.org", None, "test", "secret"
|
||||
)
|
||||
|
||||
def test_getInitialResponse(self) -> None:
|
||||
"""
|
||||
Test that no initial response is generated.
|
||||
"""
|
||||
self.assertIdentical(self.mechanism.getInitialResponse(), None)
|
||||
|
||||
def test_getResponse(self) -> None:
|
||||
"""
|
||||
The response to a Digest-MD5 challenge includes the parameters from the
|
||||
challenge.
|
||||
"""
|
||||
challenge = (
|
||||
b'realm="localhost",nonce="1234",qop="auth",charset=utf-8,'
|
||||
b"algorithm=md5-sess"
|
||||
)
|
||||
directives = self.mechanism._parse(self.mechanism.getResponse(challenge))
|
||||
del directives[b"cnonce"], directives[b"response"]
|
||||
self.assertEqual(
|
||||
{
|
||||
b"username": b"test",
|
||||
b"nonce": b"1234",
|
||||
b"nc": b"00000001",
|
||||
b"qop": [b"auth"],
|
||||
b"charset": b"utf-8",
|
||||
b"realm": b"localhost",
|
||||
b"digest-uri": b"xmpp/example.org",
|
||||
},
|
||||
directives,
|
||||
)
|
||||
|
||||
def test_getResponseNonAsciiRealm(self) -> None:
|
||||
"""
|
||||
Bytes outside the ASCII range in the challenge are nevertheless
|
||||
included in the response.
|
||||
"""
|
||||
challenge = (
|
||||
b'realm="\xc3\xa9chec.example.org",nonce="1234",'
|
||||
b'qop="auth",charset=utf-8,algorithm=md5-sess'
|
||||
)
|
||||
directives = self.mechanism._parse(self.mechanism.getResponse(challenge))
|
||||
del directives[b"cnonce"], directives[b"response"]
|
||||
self.assertEqual(
|
||||
{
|
||||
b"username": b"test",
|
||||
b"nonce": b"1234",
|
||||
b"nc": b"00000001",
|
||||
b"qop": [b"auth"],
|
||||
b"charset": b"utf-8",
|
||||
b"realm": b"\xc3\xa9chec.example.org",
|
||||
b"digest-uri": b"xmpp/example.org",
|
||||
},
|
||||
directives,
|
||||
)
|
||||
|
||||
def test_getResponseNoRealm(self) -> None:
|
||||
"""
|
||||
The response to a challenge without a realm uses the host part of the
|
||||
JID as the realm.
|
||||
"""
|
||||
challenge = b'nonce="1234",qop="auth",charset=utf-8,algorithm=md5-sess'
|
||||
directives = self.mechanism._parse(self.mechanism.getResponse(challenge))
|
||||
self.assertEqual(directives[b"realm"], b"example.org")
|
||||
|
||||
def test_getResponseNoRealmIDN(self) -> None:
|
||||
"""
|
||||
If the challenge does not include a realm and the host part of the JID
|
||||
includes bytes outside of the ASCII range, the response still includes
|
||||
the host part of the JID as the realm.
|
||||
"""
|
||||
self.mechanism = sasl_mechanisms.DigestMD5(
|
||||
"xmpp", "\u00e9chec.example.org", None, "test", "secret"
|
||||
)
|
||||
challenge = b'nonce="1234",qop="auth",charset=utf-8,algorithm=md5-sess'
|
||||
directives = self.mechanism._parse(self.mechanism.getResponse(challenge))
|
||||
self.assertEqual(directives[b"realm"], b"\xc3\xa9chec.example.org")
|
||||
|
||||
def test_getResponseRspauth(self) -> None:
|
||||
"""
|
||||
If the challenge just has a rspauth directive, the response is empty.
|
||||
"""
|
||||
challenge = b"rspauth=cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA=="
|
||||
response = self.mechanism.getResponse(challenge)
|
||||
self.assertEqual(b"", response)
|
||||
|
||||
def test_calculateResponse(self) -> None:
|
||||
"""
|
||||
The response to a Digest-MD5 challenge is computed according to RFC
|
||||
2831.
|
||||
"""
|
||||
charset = "utf-8"
|
||||
nonce = b"OA6MG9tEQGm2hh"
|
||||
nc = networkString(f"{1:08x}")
|
||||
cnonce = b"OA6MHXh6VqTrRk"
|
||||
|
||||
username = "\u0418chris"
|
||||
password = "\u0418secret"
|
||||
host = "\u0418elwood.innosoft.com"
|
||||
digestURI = "imap/\u0418elwood.innosoft.com".encode(charset)
|
||||
|
||||
mechanism = sasl_mechanisms.DigestMD5(b"imap", host, None, username, password)
|
||||
response = mechanism._calculateResponse(
|
||||
cnonce,
|
||||
nc,
|
||||
nonce,
|
||||
username.encode(charset),
|
||||
password.encode(charset),
|
||||
host.encode(charset),
|
||||
digestURI,
|
||||
)
|
||||
self.assertEqual(response, b"7928f233258be88392424d094453c5e3")
|
||||
|
||||
def test_parse(self) -> None:
|
||||
"""
|
||||
A challenge can be parsed into a L{dict} with L{bytes} or L{list}
|
||||
values.
|
||||
"""
|
||||
challenge = (
|
||||
b'nonce="1234",qop="auth,auth-conf",charset=utf-8,'
|
||||
b'algorithm=md5-sess,cipher="des,3des"'
|
||||
)
|
||||
directives = self.mechanism._parse(challenge)
|
||||
self.assertEqual(
|
||||
{
|
||||
b"algorithm": b"md5-sess",
|
||||
b"nonce": b"1234",
|
||||
b"charset": b"utf-8",
|
||||
b"qop": [b"auth", b"auth-conf"],
|
||||
b"cipher": [b"des", b"3des"],
|
||||
},
|
||||
directives,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,118 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.words.protocols.jabber.xmpp_stringprep import (
|
||||
nameprep,
|
||||
nodeprep,
|
||||
resourceprep,
|
||||
)
|
||||
|
||||
|
||||
class DeprecationTests(unittest.TestCase):
|
||||
"""
|
||||
Deprecations in L{twisted.words.protocols.jabber.xmpp_stringprep}.
|
||||
"""
|
||||
|
||||
def test_crippled(self) -> None:
|
||||
"""
|
||||
L{xmpp_stringprep.crippled} is deprecated and always returns C{False}.
|
||||
"""
|
||||
from twisted.words.protocols.jabber.xmpp_stringprep import crippled
|
||||
|
||||
warnings = self.flushWarnings(offendingFunctions=[self.test_crippled])
|
||||
self.assertEqual(DeprecationWarning, warnings[0]["category"])
|
||||
self.assertEqual(
|
||||
"twisted.words.protocols.jabber.xmpp_stringprep.crippled was "
|
||||
"deprecated in Twisted 13.1.0: crippled is always False",
|
||||
warnings[0]["message"],
|
||||
)
|
||||
self.assertEqual(1, len(warnings))
|
||||
self.assertEqual(crippled, False)
|
||||
|
||||
|
||||
class XMPPStringPrepTests(unittest.TestCase):
|
||||
"""
|
||||
The nodeprep stringprep profile is similar to the resourceprep profile,
|
||||
but does an extra mapping of characters (table B.2) and disallows
|
||||
more characters (table C.1.1 and eight extra punctuation characters).
|
||||
Due to this similarity, the resourceprep tests are more extensive, and
|
||||
the nodeprep tests only address the mappings additional restrictions.
|
||||
|
||||
The nameprep profile is nearly identical to the nameprep implementation in
|
||||
L{encodings.idna}, but that implementation assumes the C{UseSTD4ASCIIRules}
|
||||
flag to be false. This implementation assumes it to be true, and restricts
|
||||
the allowed set of characters. The tests here only check for the
|
||||
differences.
|
||||
"""
|
||||
|
||||
def testResourcePrep(self) -> None:
|
||||
self.assertEqual(resourceprep.prepare("resource"), "resource")
|
||||
self.assertNotEqual(resourceprep.prepare("Resource"), "resource")
|
||||
self.assertEqual(resourceprep.prepare(" "), " ")
|
||||
|
||||
self.assertEqual(resourceprep.prepare("Henry \u2163"), "Henry IV")
|
||||
self.assertEqual(
|
||||
resourceprep.prepare(
|
||||
"foo\xad\u034f\u1806\u180b"
|
||||
"bar\u200b\u2060"
|
||||
"baz\ufe00\ufe08\ufe0f\ufeff"
|
||||
),
|
||||
"foobarbaz",
|
||||
)
|
||||
self.assertEqual(resourceprep.prepare("\u00a0"), " ")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "\u1680")
|
||||
self.assertEqual(resourceprep.prepare("\u2000"), " ")
|
||||
self.assertEqual(resourceprep.prepare("\u200b"), "")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "\u0010\u007f")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "\u0085")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "\u180e")
|
||||
self.assertEqual(resourceprep.prepare("\ufeff"), "")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "\uf123")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "\U000f1234")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "\U0010f234")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "\U0008fffe")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "\U0010ffff")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "\udf42")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "\ufffd")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "\u2ff5")
|
||||
self.assertEqual(resourceprep.prepare("\u0341"), "\u0301")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "\u200e")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "\u202a")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "\U000e0001")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "\U000e0042")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "foo\u05bebar")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "foo\ufd50bar")
|
||||
# self.assertEqual(resourceprep.prepare(u'foo\ufb38bar'),
|
||||
# u'foo\u064ebar')
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "\u06271")
|
||||
self.assertEqual(resourceprep.prepare("\u06271\u0628"), "\u06271\u0628")
|
||||
self.assertRaises(UnicodeError, resourceprep.prepare, "\U000e0002")
|
||||
|
||||
def testNodePrep(self) -> None:
|
||||
self.assertEqual(nodeprep.prepare("user"), "user")
|
||||
self.assertEqual(nodeprep.prepare("User"), "user")
|
||||
self.assertRaises(UnicodeError, nodeprep.prepare, "us&er")
|
||||
|
||||
def test_nodeprepUnassignedInUnicode32(self) -> None:
|
||||
"""
|
||||
Make sure unassigned code points from Unicode 3.2 are rejected.
|
||||
"""
|
||||
self.assertRaises(UnicodeError, nodeprep.prepare, "\u1d39")
|
||||
|
||||
def testNamePrep(self) -> None:
|
||||
self.assertEqual(nameprep.prepare("example.com"), "example.com")
|
||||
self.assertEqual(nameprep.prepare("Example.com"), "example.com")
|
||||
self.assertRaises(UnicodeError, nameprep.prepare, "ex@mple.com")
|
||||
self.assertRaises(UnicodeError, nameprep.prepare, "-example.com")
|
||||
self.assertRaises(UnicodeError, nameprep.prepare, "example-.com")
|
||||
|
||||
self.assertEqual(
|
||||
nameprep.prepare("stra\u00dfe.example.com"), "strasse.example.com"
|
||||
)
|
||||
|
||||
def test_nameprepTrailingDot(self) -> None:
|
||||
"""
|
||||
A trailing dot in domain names is preserved.
|
||||
"""
|
||||
self.assertEqual(nameprep.prepare("example.com."), "example.com.")
|
||||
@@ -0,0 +1,841 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.words.service}.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from twisted.cred import checkers, credentials, portal
|
||||
from twisted.internet import address, defer, reactor
|
||||
from twisted.internet.defer import Deferred, DeferredList, maybeDeferred, succeed
|
||||
from twisted.spread import pb
|
||||
from twisted.test import proto_helpers
|
||||
from twisted.trial import unittest
|
||||
from twisted.words import ewords, service
|
||||
from twisted.words.protocols import irc
|
||||
|
||||
|
||||
class RealmTests(unittest.TestCase):
|
||||
def _entityCreationTest(self, kind):
|
||||
# Kind is "user" or "group"
|
||||
realm = service.InMemoryWordsRealm("realmname")
|
||||
|
||||
name = "test" + kind.lower()
|
||||
create = getattr(realm, "create" + kind.title())
|
||||
get = getattr(realm, "get" + kind.title())
|
||||
flag = "create" + kind.title() + "OnRequest"
|
||||
dupExc = getattr(ewords, "Duplicate" + kind.title())
|
||||
noSuchExc = getattr(ewords, "NoSuch" + kind.title())
|
||||
|
||||
# Creating should succeed
|
||||
p = self.successResultOf(create(name))
|
||||
self.assertEqual(name, p.name)
|
||||
|
||||
# Creating the same user again should not
|
||||
self.failureResultOf(create(name)).trap(dupExc)
|
||||
|
||||
# Getting a non-existent user should succeed if createUserOnRequest is True
|
||||
setattr(realm, flag, True)
|
||||
p = self.successResultOf(get("new" + kind.lower()))
|
||||
self.assertEqual("new" + kind.lower(), p.name)
|
||||
|
||||
# Getting that user again should return the same object
|
||||
newp = self.successResultOf(get("new" + kind.lower()))
|
||||
self.assertIdentical(p, newp)
|
||||
|
||||
# Getting a non-existent user should fail if createUserOnRequest is False
|
||||
setattr(realm, flag, False)
|
||||
self.failureResultOf(get("another" + kind.lower())).trap(noSuchExc)
|
||||
|
||||
def testUserCreation(self):
|
||||
return self._entityCreationTest("User")
|
||||
|
||||
def testGroupCreation(self):
|
||||
return self._entityCreationTest("Group")
|
||||
|
||||
def testUserRetrieval(self):
|
||||
realm = service.InMemoryWordsRealm("realmname")
|
||||
|
||||
# Make a user to play around with
|
||||
user = self.successResultOf(realm.createUser("testuser"))
|
||||
|
||||
# Make sure getting the user returns the same object
|
||||
retrieved = self.successResultOf(realm.getUser("testuser"))
|
||||
self.assertIdentical(user, retrieved)
|
||||
|
||||
# Make sure looking up the user also returns the same object
|
||||
lookedUp = self.successResultOf(realm.lookupUser("testuser"))
|
||||
self.assertIdentical(retrieved, lookedUp)
|
||||
|
||||
# Make sure looking up a user who does not exist fails
|
||||
(self.failureResultOf(realm.lookupUser("nosuchuser")).trap(ewords.NoSuchUser))
|
||||
|
||||
def testUserAddition(self):
|
||||
realm = service.InMemoryWordsRealm("realmname")
|
||||
|
||||
# Create and manually add a user to the realm
|
||||
p = service.User("testuser")
|
||||
user = self.successResultOf(realm.addUser(p))
|
||||
self.assertIdentical(p, user)
|
||||
|
||||
# Make sure getting that user returns the same object
|
||||
retrieved = self.successResultOf(realm.getUser("testuser"))
|
||||
self.assertIdentical(user, retrieved)
|
||||
|
||||
# Make sure looking up that user returns the same object
|
||||
lookedUp = self.successResultOf(realm.lookupUser("testuser"))
|
||||
self.assertIdentical(retrieved, lookedUp)
|
||||
|
||||
def testGroupRetrieval(self):
|
||||
realm = service.InMemoryWordsRealm("realmname")
|
||||
|
||||
group = self.successResultOf(realm.createGroup("testgroup"))
|
||||
|
||||
retrieved = self.successResultOf(realm.getGroup("testgroup"))
|
||||
|
||||
self.assertIdentical(group, retrieved)
|
||||
|
||||
(self.failureResultOf(realm.getGroup("nosuchgroup")).trap(ewords.NoSuchGroup))
|
||||
|
||||
def testGroupAddition(self):
|
||||
realm = service.InMemoryWordsRealm("realmname")
|
||||
|
||||
p = service.Group("testgroup")
|
||||
self.successResultOf(realm.addGroup(p))
|
||||
group = self.successResultOf(realm.getGroup("testGroup"))
|
||||
self.assertIdentical(p, group)
|
||||
|
||||
def testGroupUsernameCollision(self):
|
||||
"""
|
||||
Try creating a group with the same name as an existing user and
|
||||
assert that it succeeds, since users and groups should not be in the
|
||||
same namespace and collisions should be impossible.
|
||||
"""
|
||||
realm = service.InMemoryWordsRealm("realmname")
|
||||
|
||||
self.successResultOf(realm.createUser("test"))
|
||||
self.successResultOf(realm.createGroup("test"))
|
||||
|
||||
def testEnumeration(self):
|
||||
realm = service.InMemoryWordsRealm("realmname")
|
||||
self.successResultOf(realm.createGroup("groupone"))
|
||||
|
||||
self.successResultOf(realm.createGroup("grouptwo"))
|
||||
|
||||
groups = self.successResultOf(realm.itergroups())
|
||||
|
||||
n = [g.name for g in groups]
|
||||
n.sort()
|
||||
self.assertEqual(n, ["groupone", "grouptwo"])
|
||||
|
||||
|
||||
class TestCaseUserAgg:
|
||||
def __init__(
|
||||
self,
|
||||
user,
|
||||
realm,
|
||||
factory,
|
||||
address=address.IPv4Address("TCP", "127.0.0.1", 54321),
|
||||
):
|
||||
self.user = user
|
||||
self.transport = proto_helpers.StringTransportWithDisconnection()
|
||||
self.protocol = factory.buildProtocol(address)
|
||||
self.transport.protocol = self.protocol
|
||||
self.user.mind = self.protocol
|
||||
self.protocol.makeConnection(self.transport)
|
||||
|
||||
def write(self, stuff):
|
||||
self.protocol.dataReceived(stuff)
|
||||
|
||||
|
||||
class IRCProtocolTests(unittest.TestCase):
|
||||
STATIC_USERS = [
|
||||
"useruser",
|
||||
"otheruser",
|
||||
"someguy",
|
||||
"firstuser",
|
||||
"username",
|
||||
"userone",
|
||||
"usertwo",
|
||||
"userthree",
|
||||
"userfour",
|
||||
b"userfive",
|
||||
"someuser",
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
self.realm = service.InMemoryWordsRealm("realmname")
|
||||
self.checker = checkers.InMemoryUsernamePasswordDatabaseDontUse()
|
||||
self.portal = portal.Portal(self.realm, [self.checker])
|
||||
self.factory = service.IRCFactory(self.realm, self.portal)
|
||||
|
||||
c = []
|
||||
for nick in self.STATIC_USERS:
|
||||
if isinstance(nick, bytes):
|
||||
nick = nick.decode("utf-8")
|
||||
c.append(self.realm.createUser(nick))
|
||||
self.checker.addUser(nick, nick + "_password")
|
||||
return DeferredList(c)
|
||||
|
||||
def _assertGreeting(self, user):
|
||||
"""
|
||||
The user has been greeted with the four messages that are (usually)
|
||||
considered to start an IRC session.
|
||||
|
||||
Asserts that the required responses were received.
|
||||
"""
|
||||
# Make sure we get 1-4 at least
|
||||
response = self._response(user)
|
||||
expected = [irc.RPL_WELCOME, irc.RPL_YOURHOST, irc.RPL_CREATED, irc.RPL_MYINFO]
|
||||
for prefix, command, args in response:
|
||||
if command in expected:
|
||||
expected.remove(command)
|
||||
self.assertFalse(expected, f"Missing responses for {expected!r}")
|
||||
|
||||
def _login(self, user, nick, password=None):
|
||||
if password is None:
|
||||
password = nick + "_password"
|
||||
user.write(f"PASS {password}\r\n")
|
||||
user.write(f"NICK {nick} extrainfo\r\n")
|
||||
|
||||
def _loggedInUser(self, name):
|
||||
user = self.successResultOf(self.realm.lookupUser(name))
|
||||
agg = TestCaseUserAgg(user, self.realm, self.factory)
|
||||
self._login(agg, name)
|
||||
return agg
|
||||
|
||||
def _response(self, user, messageType=None):
|
||||
"""
|
||||
Extracts the user's response, and returns a list of parsed lines.
|
||||
If messageType is defined, only messages of that type will be returned.
|
||||
"""
|
||||
response = user.transport.value()
|
||||
response = response.decode("utf-8")
|
||||
response = response.splitlines()
|
||||
user.transport.clear()
|
||||
result = []
|
||||
for message in map(irc.parsemsg, response):
|
||||
if messageType is None or message[1] == messageType:
|
||||
result.append(message)
|
||||
return result
|
||||
|
||||
def testPASSLogin(self):
|
||||
user = self._loggedInUser("firstuser")
|
||||
self._assertGreeting(user)
|
||||
|
||||
def test_nickServLogin(self):
|
||||
"""
|
||||
Sending NICK without PASS will prompt the user for their password.
|
||||
When the user sends their password to NickServ, it will respond with a
|
||||
Greeting.
|
||||
"""
|
||||
firstuser = self.successResultOf(self.realm.lookupUser("firstuser"))
|
||||
|
||||
user = TestCaseUserAgg(firstuser, self.realm, self.factory)
|
||||
user.write("NICK firstuser extrainfo\r\n")
|
||||
response = self._response(user, "PRIVMSG")
|
||||
self.assertEqual(len(response), 1)
|
||||
self.assertEqual(response[0][0], service.NICKSERV)
|
||||
self.assertEqual(response[0][1], "PRIVMSG")
|
||||
self.assertEqual(response[0][2], ["firstuser", "Password?"])
|
||||
user.transport.clear()
|
||||
|
||||
user.write("PRIVMSG nickserv firstuser_password\r\n")
|
||||
self._assertGreeting(user)
|
||||
|
||||
def testFailedLogin(self):
|
||||
firstuser = self.successResultOf(self.realm.lookupUser("firstuser"))
|
||||
|
||||
user = TestCaseUserAgg(firstuser, self.realm, self.factory)
|
||||
self._login(user, "firstuser", "wrongpass")
|
||||
response = self._response(user, "PRIVMSG")
|
||||
self.assertEqual(len(response), 1)
|
||||
self.assertEqual(response[0][2], ["firstuser", "Login failed. Goodbye."])
|
||||
|
||||
def testLogout(self):
|
||||
logout = []
|
||||
firstuser = self.successResultOf(self.realm.lookupUser("firstuser"))
|
||||
|
||||
user = TestCaseUserAgg(firstuser, self.realm, self.factory)
|
||||
self._login(user, "firstuser")
|
||||
user.protocol.logout = lambda: logout.append(True)
|
||||
user.write("QUIT\r\n")
|
||||
self.assertEqual(logout, [True])
|
||||
|
||||
def testJoin(self):
|
||||
firstuser = self.successResultOf(self.realm.lookupUser("firstuser"))
|
||||
|
||||
somechannel = self.successResultOf(self.realm.createGroup("somechannel"))
|
||||
|
||||
somechannel.meta["topic"] = "some random topic"
|
||||
|
||||
# Bring in one user, make sure he gets into the channel sanely
|
||||
user = TestCaseUserAgg(firstuser, self.realm, self.factory)
|
||||
self._login(user, "firstuser")
|
||||
user.transport.clear()
|
||||
user.write("JOIN #somechannel\r\n")
|
||||
|
||||
response = self._response(user)
|
||||
self.assertEqual(len(response), 5)
|
||||
|
||||
# Join message
|
||||
self.assertEqual(response[0][0], "firstuser!firstuser@realmname")
|
||||
self.assertEqual(response[0][1], "JOIN")
|
||||
self.assertEqual(response[0][2], ["#somechannel"])
|
||||
|
||||
# User list
|
||||
self.assertEqual(response[1][1], "353")
|
||||
self.assertEqual(response[2][1], "366")
|
||||
|
||||
# Topic (or lack thereof, as the case may be)
|
||||
self.assertEqual(response[3][1], "332")
|
||||
self.assertEqual(response[4][1], "333")
|
||||
|
||||
# Hook up another client! It is a CHAT SYSTEM!!!!!!!
|
||||
other = self._loggedInUser("otheruser")
|
||||
|
||||
other.transport.clear()
|
||||
user.transport.clear()
|
||||
other.write("JOIN #somechannel\r\n")
|
||||
|
||||
# At this point, both users should be in the channel
|
||||
response = self._response(other)
|
||||
|
||||
event = self._response(user)
|
||||
self.assertEqual(len(event), 1)
|
||||
self.assertEqual(event[0][0], "otheruser!otheruser@realmname")
|
||||
self.assertEqual(event[0][1], "JOIN")
|
||||
self.assertEqual(event[0][2], ["#somechannel"])
|
||||
|
||||
self.assertEqual(response[1][0], "realmname")
|
||||
self.assertEqual(response[1][1], "353")
|
||||
self.assertIn(
|
||||
response[1][2],
|
||||
[
|
||||
["otheruser", "=", "#somechannel", "firstuser otheruser"],
|
||||
["otheruser", "=", "#somechannel", "otheruser firstuser"],
|
||||
],
|
||||
)
|
||||
|
||||
def test_joinTopicless(self):
|
||||
"""
|
||||
When a user joins a group without a topic, no topic information is
|
||||
sent to that user.
|
||||
"""
|
||||
firstuser = self.successResultOf(self.realm.lookupUser("firstuser"))
|
||||
|
||||
self.successResultOf(self.realm.createGroup("somechannel"))
|
||||
|
||||
# Bring in one user, make sure he gets into the channel sanely
|
||||
user = TestCaseUserAgg(firstuser, self.realm, self.factory)
|
||||
self._login(user, "firstuser")
|
||||
user.transport.clear()
|
||||
user.write("JOIN #somechannel\r\n")
|
||||
|
||||
response = self._response(user)
|
||||
responseCodes = [r[1] for r in response]
|
||||
self.assertNotIn("332", responseCodes)
|
||||
self.assertNotIn("333", responseCodes)
|
||||
|
||||
def testLeave(self):
|
||||
user = self._loggedInUser("useruser")
|
||||
|
||||
self.successResultOf(self.realm.createGroup("somechannel"))
|
||||
|
||||
user.write("JOIN #somechannel\r\n")
|
||||
user.transport.clear()
|
||||
|
||||
other = self._loggedInUser("otheruser")
|
||||
|
||||
other.write("JOIN #somechannel\r\n")
|
||||
|
||||
user.transport.clear()
|
||||
other.transport.clear()
|
||||
|
||||
user.write("PART #somechannel\r\n")
|
||||
|
||||
response = self._response(user)
|
||||
event = self._response(other)
|
||||
|
||||
self.assertEqual(len(response), 1)
|
||||
self.assertEqual(response[0][0], "useruser!useruser@realmname")
|
||||
self.assertEqual(response[0][1], "PART")
|
||||
self.assertEqual(response[0][2], ["#somechannel", "leaving"])
|
||||
self.assertEqual(response, event)
|
||||
|
||||
# Now again, with a part message
|
||||
user.write("JOIN #somechannel\r\n")
|
||||
|
||||
user.transport.clear()
|
||||
other.transport.clear()
|
||||
|
||||
user.write("PART #somechannel :goodbye stupidheads\r\n")
|
||||
|
||||
response = self._response(user)
|
||||
event = self._response(other)
|
||||
|
||||
self.assertEqual(len(response), 1)
|
||||
self.assertEqual(response[0][0], "useruser!useruser@realmname")
|
||||
self.assertEqual(response[0][1], "PART")
|
||||
self.assertEqual(response[0][2], ["#somechannel", "goodbye stupidheads"])
|
||||
self.assertEqual(response, event)
|
||||
|
||||
user.write(b"JOIN #somechannel\r\n")
|
||||
|
||||
user.transport.clear()
|
||||
other.transport.clear()
|
||||
|
||||
user.write(b"PART #somechannel :goodbye stupidheads1\r\n")
|
||||
|
||||
response = self._response(user)
|
||||
event = self._response(other)
|
||||
|
||||
self.assertEqual(len(response), 1)
|
||||
self.assertEqual(response[0][0], "useruser!useruser@realmname")
|
||||
self.assertEqual(response[0][1], "PART")
|
||||
self.assertEqual(response[0][2], ["#somechannel", "goodbye stupidheads1"])
|
||||
self.assertEqual(response, event)
|
||||
|
||||
def testGetTopic(self):
|
||||
user = self._loggedInUser("useruser")
|
||||
|
||||
group = service.Group("somechannel")
|
||||
group.meta["topic"] = "This is a test topic."
|
||||
group.meta["topic_author"] = "some_fellow"
|
||||
group.meta["topic_date"] = 77777777
|
||||
|
||||
self.successResultOf(self.realm.addGroup(group))
|
||||
|
||||
user.transport.clear()
|
||||
user.write("JOIN #somechannel\r\n")
|
||||
|
||||
response = self._response(user)
|
||||
|
||||
self.assertEqual(response[3][0], "realmname")
|
||||
self.assertEqual(response[3][1], "332")
|
||||
|
||||
# XXX Sigh. irc.parsemsg() is not as correct as one might hope.
|
||||
self.assertEqual(
|
||||
response[3][2], ["useruser", "#somechannel", "This is a test topic."]
|
||||
)
|
||||
self.assertEqual(response[4][1], "333")
|
||||
self.assertEqual(
|
||||
response[4][2], ["useruser", "#somechannel", "some_fellow", "77777777"]
|
||||
)
|
||||
|
||||
user.transport.clear()
|
||||
|
||||
user.write("TOPIC #somechannel\r\n")
|
||||
|
||||
response = self._response(user)
|
||||
|
||||
self.assertEqual(response[0][1], "332")
|
||||
self.assertEqual(
|
||||
response[0][2], ["useruser", "#somechannel", "This is a test topic."]
|
||||
)
|
||||
self.assertEqual(response[1][1], "333")
|
||||
self.assertEqual(
|
||||
response[1][2], ["useruser", "#somechannel", "some_fellow", "77777777"]
|
||||
)
|
||||
|
||||
def testSetTopic(self):
|
||||
user = self._loggedInUser("useruser")
|
||||
|
||||
somechannel = self.successResultOf(self.realm.createGroup("somechannel"))
|
||||
|
||||
user.write("JOIN #somechannel\r\n")
|
||||
|
||||
other = self._loggedInUser("otheruser")
|
||||
|
||||
other.write("JOIN #somechannel\r\n")
|
||||
|
||||
user.transport.clear()
|
||||
other.transport.clear()
|
||||
|
||||
other.write("TOPIC #somechannel :This is the new topic.\r\n")
|
||||
|
||||
response = self._response(other)
|
||||
event = self._response(user)
|
||||
|
||||
self.assertEqual(response, event)
|
||||
|
||||
self.assertEqual(response[0][0], "otheruser!otheruser@realmname")
|
||||
self.assertEqual(response[0][1], "TOPIC")
|
||||
self.assertEqual(response[0][2], ["#somechannel", "This is the new topic."])
|
||||
|
||||
other.transport.clear()
|
||||
|
||||
somechannel.meta["topic_date"] = 12345
|
||||
other.write("TOPIC #somechannel\r\n")
|
||||
|
||||
response = self._response(other)
|
||||
self.assertEqual(response[0][1], "332")
|
||||
self.assertEqual(
|
||||
response[0][2], ["otheruser", "#somechannel", "This is the new topic."]
|
||||
)
|
||||
self.assertEqual(response[1][1], "333")
|
||||
self.assertEqual(
|
||||
response[1][2], ["otheruser", "#somechannel", "otheruser", "12345"]
|
||||
)
|
||||
|
||||
other.transport.clear()
|
||||
other.write("TOPIC #asdlkjasd\r\n")
|
||||
|
||||
response = self._response(other)
|
||||
self.assertEqual(response[0][1], "403")
|
||||
|
||||
def testGroupMessage(self):
|
||||
user = self._loggedInUser("useruser")
|
||||
|
||||
self.successResultOf(self.realm.createGroup("somechannel"))
|
||||
|
||||
user.write("JOIN #somechannel\r\n")
|
||||
|
||||
other = self._loggedInUser("otheruser")
|
||||
|
||||
other.write("JOIN #somechannel\r\n")
|
||||
|
||||
user.transport.clear()
|
||||
other.transport.clear()
|
||||
|
||||
user.write("PRIVMSG #somechannel :Hello, world.\r\n")
|
||||
|
||||
response = self._response(user)
|
||||
event = self._response(other)
|
||||
|
||||
self.assertFalse(response)
|
||||
self.assertEqual(len(event), 1)
|
||||
self.assertEqual(event[0][0], "useruser!useruser@realmname")
|
||||
self.assertEqual(event[0][1], "PRIVMSG", -1)
|
||||
self.assertEqual(event[0][2], ["#somechannel", "Hello, world."])
|
||||
|
||||
def testPrivateMessage(self):
|
||||
user = self._loggedInUser("useruser")
|
||||
|
||||
other = self._loggedInUser("otheruser")
|
||||
|
||||
user.transport.clear()
|
||||
other.transport.clear()
|
||||
|
||||
user.write("PRIVMSG otheruser :Hello, monkey.\r\n")
|
||||
|
||||
response = self._response(user)
|
||||
event = self._response(other)
|
||||
|
||||
self.assertFalse(response)
|
||||
self.assertEqual(len(event), 1)
|
||||
self.assertEqual(event[0][0], "useruser!useruser@realmname")
|
||||
self.assertEqual(event[0][1], "PRIVMSG")
|
||||
self.assertEqual(event[0][2], ["otheruser", "Hello, monkey."])
|
||||
|
||||
user.write("PRIVMSG nousernamedthis :Hello, monkey.\r\n")
|
||||
|
||||
response = self._response(user)
|
||||
|
||||
self.assertEqual(len(response), 1)
|
||||
self.assertEqual(response[0][0], "realmname")
|
||||
self.assertEqual(response[0][1], "401")
|
||||
self.assertEqual(
|
||||
response[0][2], ["useruser", "nousernamedthis", "No such nick/channel."]
|
||||
)
|
||||
|
||||
def testOper(self):
|
||||
user = self._loggedInUser("useruser")
|
||||
|
||||
user.transport.clear()
|
||||
user.write("OPER user pass\r\n")
|
||||
response = self._response(user)
|
||||
|
||||
self.assertEqual(len(response), 1)
|
||||
self.assertEqual(response[0][1], "491")
|
||||
|
||||
def testGetUserMode(self):
|
||||
user = self._loggedInUser("useruser")
|
||||
|
||||
user.transport.clear()
|
||||
user.write("MODE useruser\r\n")
|
||||
|
||||
response = self._response(user)
|
||||
self.assertEqual(len(response), 1)
|
||||
self.assertEqual(response[0][0], "realmname")
|
||||
self.assertEqual(response[0][1], "221")
|
||||
self.assertEqual(response[0][2], ["useruser", "+"])
|
||||
|
||||
def testSetUserMode(self):
|
||||
user = self._loggedInUser("useruser")
|
||||
|
||||
user.transport.clear()
|
||||
user.write("MODE useruser +abcd\r\n")
|
||||
|
||||
response = self._response(user)
|
||||
self.assertEqual(len(response), 1)
|
||||
self.assertEqual(response[0][1], "472")
|
||||
|
||||
def testGetGroupMode(self):
|
||||
user = self._loggedInUser("useruser")
|
||||
|
||||
self.successResultOf(self.realm.createGroup("somechannel"))
|
||||
|
||||
user.write("JOIN #somechannel\r\n")
|
||||
|
||||
user.transport.clear()
|
||||
user.write("MODE #somechannel\r\n")
|
||||
|
||||
response = self._response(user)
|
||||
self.assertEqual(len(response), 1)
|
||||
self.assertEqual(response[0][1], "324")
|
||||
|
||||
def testSetGroupMode(self):
|
||||
user = self._loggedInUser("useruser")
|
||||
|
||||
self.successResultOf(self.realm.createGroup("groupname"))
|
||||
|
||||
user.write("JOIN #groupname\r\n")
|
||||
|
||||
user.transport.clear()
|
||||
user.write("MODE #groupname +abcd\r\n")
|
||||
|
||||
response = self._response(user)
|
||||
self.assertEqual(len(response), 1)
|
||||
self.assertEqual(response[0][1], "472")
|
||||
|
||||
def testWho(self):
|
||||
group = service.Group("groupname")
|
||||
self.successResultOf(self.realm.addGroup(group))
|
||||
|
||||
users = []
|
||||
for nick in "userone", "usertwo", "userthree":
|
||||
u = self._loggedInUser(nick)
|
||||
users.append(u)
|
||||
users[-1].write("JOIN #groupname\r\n")
|
||||
for user in users:
|
||||
user.transport.clear()
|
||||
|
||||
users[0].write("WHO #groupname\r\n")
|
||||
|
||||
r = self._response(users[0])
|
||||
self.assertFalse(self._response(users[1]))
|
||||
self.assertFalse(self._response(users[2]))
|
||||
|
||||
wantusers = ["userone", "usertwo", "userthree"]
|
||||
for prefix, code, stuff in r[:-1]:
|
||||
self.assertEqual(prefix, "realmname")
|
||||
self.assertEqual(code, "352")
|
||||
|
||||
(
|
||||
myname,
|
||||
group,
|
||||
theirname,
|
||||
theirhost,
|
||||
theirserver,
|
||||
theirnick,
|
||||
flag,
|
||||
extra,
|
||||
) = stuff
|
||||
self.assertEqual(myname, "userone")
|
||||
self.assertEqual(group, "#groupname")
|
||||
self.assertTrue(theirname in wantusers)
|
||||
self.assertEqual(theirhost, "realmname")
|
||||
self.assertEqual(theirserver, "realmname")
|
||||
wantusers.remove(theirnick)
|
||||
self.assertEqual(flag, "H")
|
||||
self.assertEqual(extra, "0 " + theirnick)
|
||||
self.assertFalse(wantusers)
|
||||
|
||||
prefix, code, stuff = r[-1]
|
||||
self.assertEqual(prefix, "realmname")
|
||||
self.assertEqual(code, "315")
|
||||
myname, channel, extra = stuff
|
||||
self.assertEqual(myname, "userone")
|
||||
self.assertEqual(channel, "#groupname")
|
||||
self.assertEqual(extra, "End of /WHO list.")
|
||||
|
||||
def testList(self):
|
||||
user = self._loggedInUser("someuser")
|
||||
user.transport.clear()
|
||||
|
||||
somegroup = self.successResultOf(self.realm.createGroup("somegroup"))
|
||||
somegroup.size = lambda: succeed(17)
|
||||
somegroup.meta["topic"] = "this is the topic woo"
|
||||
|
||||
# Test one group
|
||||
user.write("LIST #somegroup\r\n")
|
||||
|
||||
r = self._response(user)
|
||||
self.assertEqual(len(r), 2)
|
||||
resp, end = r
|
||||
|
||||
self.assertEqual(resp[0], "realmname")
|
||||
self.assertEqual(resp[1], "322")
|
||||
self.assertEqual(resp[2][0], "someuser")
|
||||
self.assertEqual(resp[2][1], "somegroup")
|
||||
self.assertEqual(resp[2][2], "17")
|
||||
self.assertEqual(resp[2][3], "this is the topic woo")
|
||||
|
||||
self.assertEqual(end[0], "realmname")
|
||||
self.assertEqual(end[1], "323")
|
||||
self.assertEqual(end[2][0], "someuser")
|
||||
self.assertEqual(end[2][1], "End of /LIST")
|
||||
|
||||
user.transport.clear()
|
||||
# Test all groups
|
||||
|
||||
user.write("LIST\r\n")
|
||||
r = self._response(user)
|
||||
self.assertEqual(len(r), 2)
|
||||
|
||||
fg1, end = r
|
||||
|
||||
self.assertEqual(fg1[1], "322")
|
||||
self.assertEqual(fg1[2][1], "somegroup")
|
||||
self.assertEqual(fg1[2][2], "17")
|
||||
self.assertEqual(fg1[2][3], "this is the topic woo")
|
||||
|
||||
self.assertEqual(end[1], "323")
|
||||
|
||||
def testWhois(self):
|
||||
user = self._loggedInUser("someguy")
|
||||
|
||||
otherguy = service.User("otherguy")
|
||||
otherguy.itergroups = lambda: iter(
|
||||
[service.Group("groupA"), service.Group("groupB")]
|
||||
)
|
||||
otherguy.signOn = 10
|
||||
otherguy.lastMessage = time.time() - 15
|
||||
|
||||
self.successResultOf(self.realm.addUser(otherguy))
|
||||
|
||||
user.transport.clear()
|
||||
user.write("WHOIS otherguy\r\n")
|
||||
r = self._response(user)
|
||||
|
||||
self.assertEqual(len(r), 5)
|
||||
wuser, wserver, idle, channels, end = r
|
||||
|
||||
self.assertEqual(wuser[0], "realmname")
|
||||
self.assertEqual(wuser[1], "311")
|
||||
self.assertEqual(wuser[2][0], "someguy")
|
||||
self.assertEqual(wuser[2][1], "otherguy")
|
||||
self.assertEqual(wuser[2][2], "otherguy")
|
||||
self.assertEqual(wuser[2][3], "realmname")
|
||||
self.assertEqual(wuser[2][4], "*")
|
||||
self.assertEqual(wuser[2][5], "otherguy")
|
||||
|
||||
self.assertEqual(wserver[0], "realmname")
|
||||
self.assertEqual(wserver[1], "312")
|
||||
self.assertEqual(wserver[2][0], "someguy")
|
||||
self.assertEqual(wserver[2][1], "otherguy")
|
||||
self.assertEqual(wserver[2][2], "realmname")
|
||||
self.assertEqual(wserver[2][3], "Hi mom!")
|
||||
|
||||
self.assertEqual(idle[0], "realmname")
|
||||
self.assertEqual(idle[1], "317")
|
||||
self.assertEqual(idle[2][0], "someguy")
|
||||
self.assertEqual(idle[2][1], "otherguy")
|
||||
self.assertEqual(idle[2][2], "15")
|
||||
self.assertEqual(idle[2][3], "10")
|
||||
self.assertEqual(idle[2][4], "seconds idle, signon time")
|
||||
|
||||
self.assertEqual(channels[0], "realmname")
|
||||
self.assertEqual(channels[1], "319")
|
||||
self.assertEqual(channels[2][0], "someguy")
|
||||
self.assertEqual(channels[2][1], "otherguy")
|
||||
self.assertEqual(channels[2][2], "#groupA #groupB")
|
||||
|
||||
self.assertEqual(end[0], "realmname")
|
||||
self.assertEqual(end[1], "318")
|
||||
self.assertEqual(end[2][0], "someguy")
|
||||
self.assertEqual(end[2][1], "otherguy")
|
||||
self.assertEqual(end[2][2], "End of WHOIS list.")
|
||||
|
||||
|
||||
class TestMind(service.PBMind):
|
||||
def __init__(self, *a, **kw):
|
||||
self.joins = []
|
||||
self.parts = []
|
||||
self.messages = []
|
||||
self.meta = []
|
||||
|
||||
def remote_userJoined(self, user, group):
|
||||
self.joins.append((user, group))
|
||||
|
||||
def remote_userLeft(self, user, group, reason):
|
||||
self.parts.append((user, group, reason))
|
||||
|
||||
def remote_receive(self, sender, recipient, message):
|
||||
self.messages.append((sender, recipient, message))
|
||||
|
||||
def remote_groupMetaUpdate(self, group, meta):
|
||||
self.meta.append((group, meta))
|
||||
|
||||
|
||||
pb.setUnjellyableForClass(TestMind, service.PBMindReference)
|
||||
|
||||
|
||||
class PBProtocolTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.realm = service.InMemoryWordsRealm("realmname")
|
||||
self.checker = checkers.InMemoryUsernamePasswordDatabaseDontUse()
|
||||
self.portal = portal.Portal(self.realm, [self.checker])
|
||||
self.serverFactory = pb.PBServerFactory(self.portal)
|
||||
self.serverFactory.protocol = self._protocolFactory
|
||||
self.serverFactory.unsafeTracebacks = True
|
||||
self.clientFactory = pb.PBClientFactory()
|
||||
self.clientFactory.unsafeTracebacks = True
|
||||
self.serverPort = reactor.listenTCP(0, self.serverFactory)
|
||||
self.clientConn = reactor.connectTCP(
|
||||
"127.0.0.1", self.serverPort.getHost().port, self.clientFactory
|
||||
)
|
||||
|
||||
def _protocolFactory(self, *args, **kw):
|
||||
self._serverProtocol = pb.Broker(0)
|
||||
return self._serverProtocol
|
||||
|
||||
def tearDown(self):
|
||||
d3 = Deferred()
|
||||
self._serverProtocol.notifyOnDisconnect(lambda: d3.callback(None))
|
||||
return DeferredList(
|
||||
[
|
||||
maybeDeferred(self.serverPort.stopListening),
|
||||
maybeDeferred(self.clientConn.disconnect),
|
||||
d3,
|
||||
]
|
||||
)
|
||||
|
||||
def _loggedInAvatar(self, name, password, mind):
|
||||
nameBytes = name
|
||||
if isinstance(name, str):
|
||||
nameBytes = name.encode("ascii")
|
||||
creds = credentials.UsernamePassword(nameBytes, password)
|
||||
self.checker.addUser(nameBytes, password)
|
||||
d = self.realm.createUser(name)
|
||||
d.addCallback(lambda ign: self.clientFactory.login(creds, mind))
|
||||
return d
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def testGroups(self):
|
||||
mindone = TestMind()
|
||||
one = yield self._loggedInAvatar("one", b"p1", mindone)
|
||||
|
||||
mindtwo = TestMind()
|
||||
two = yield self._loggedInAvatar("two", b"p2", mindtwo)
|
||||
|
||||
mindThree = TestMind()
|
||||
three = yield self._loggedInAvatar(b"three", b"p3", mindThree)
|
||||
|
||||
yield self.realm.createGroup("foobar")
|
||||
yield self.realm.createGroup(b"barfoo")
|
||||
|
||||
groupone = yield one.join("foobar")
|
||||
grouptwo = yield two.join(b"barfoo")
|
||||
|
||||
yield two.join("foobar")
|
||||
yield two.join(b"barfoo")
|
||||
yield three.join("foobar")
|
||||
|
||||
yield groupone.send({b"text": b"hello, monkeys"})
|
||||
|
||||
yield groupone.leave()
|
||||
yield grouptwo.leave()
|
||||
@@ -0,0 +1,78 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from twisted.cred import credentials, error
|
||||
from twisted.cred.checkers import FilePasswordDB
|
||||
from twisted.internet import defer
|
||||
from twisted.trial import unittest
|
||||
from twisted.words import tap
|
||||
|
||||
|
||||
class WordsTapTests(unittest.TestCase):
|
||||
"""
|
||||
Ensures that the twisted.words.tap API works.
|
||||
"""
|
||||
|
||||
PASSWD_TEXT = b"admin:admin\njoe:foo\n"
|
||||
admin = credentials.UsernamePassword(b"admin", b"admin")
|
||||
joeWrong = credentials.UsernamePassword(b"joe", b"bar")
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""
|
||||
Create a file with two users.
|
||||
"""
|
||||
self.filename = self.mktemp()
|
||||
self.file = open(self.filename, "wb")
|
||||
self.file.write(self.PASSWD_TEXT)
|
||||
self.file.flush()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
"""
|
||||
Close the dummy user database.
|
||||
"""
|
||||
self.file.close()
|
||||
|
||||
def test_hostname(self) -> None:
|
||||
"""
|
||||
Tests that the --hostname parameter gets passed to Options.
|
||||
"""
|
||||
opt = tap.Options()
|
||||
opt.parseOptions(["--hostname", "myhost"])
|
||||
self.assertEqual(opt["hostname"], "myhost")
|
||||
|
||||
def test_passwd(self) -> None:
|
||||
"""
|
||||
Tests the --passwd command for backwards-compatibility.
|
||||
"""
|
||||
opt = tap.Options()
|
||||
opt.parseOptions(["--passwd", self.file.name])
|
||||
self._loginTest(opt)
|
||||
|
||||
def test_auth(self) -> None:
|
||||
"""
|
||||
Tests that the --auth command generates a checker.
|
||||
"""
|
||||
opt = tap.Options()
|
||||
opt.parseOptions(["--auth", "file:" + self.file.name])
|
||||
self._loginTest(opt)
|
||||
|
||||
def _loginTest(self, opt: tap.Options) -> defer.Deferred[None]:
|
||||
"""
|
||||
This method executes both positive and negative authentication
|
||||
tests against whatever credentials checker has been stored in
|
||||
the Options class.
|
||||
|
||||
@param opt: An instance of L{tap.Options}.
|
||||
"""
|
||||
self.assertEqual(len(opt["credCheckers"]), 1)
|
||||
checker: FilePasswordDB = opt["credCheckers"][0]
|
||||
self.assertFailure(
|
||||
checker.requestAvatarId(self.joeWrong), error.UnauthorizedLogin
|
||||
)
|
||||
|
||||
def _gotAvatar(username: bytes | tuple[()]) -> None:
|
||||
self.assertEqual(username, self.admin.username)
|
||||
|
||||
return checker.requestAvatarId(self.admin).addCallback(_gotAvatar)
|
||||
@@ -0,0 +1,331 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for twisted.words.xish.utility
|
||||
"""
|
||||
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.words.xish import utility
|
||||
from twisted.words.xish.domish import Element
|
||||
from twisted.words.xish.utility import EventDispatcher
|
||||
|
||||
|
||||
class CallbackTracker:
|
||||
"""
|
||||
Test helper for tracking callbacks.
|
||||
|
||||
Increases a counter on each call to L{call} and stores the object
|
||||
passed in the call.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.called = 0
|
||||
self.obj = None
|
||||
|
||||
def call(self, obj):
|
||||
self.called = self.called + 1
|
||||
self.obj = obj
|
||||
|
||||
|
||||
class OrderedCallbackTracker:
|
||||
"""
|
||||
Test helper for tracking callbacks and their order.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.callList = []
|
||||
|
||||
def call1(self, object):
|
||||
self.callList.append(self.call1)
|
||||
|
||||
def call2(self, object):
|
||||
self.callList.append(self.call2)
|
||||
|
||||
def call3(self, object):
|
||||
self.callList.append(self.call3)
|
||||
|
||||
|
||||
class EventDispatcherTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{EventDispatcher}.
|
||||
"""
|
||||
|
||||
def testStuff(self):
|
||||
d = EventDispatcher()
|
||||
cb1 = CallbackTracker()
|
||||
cb2 = CallbackTracker()
|
||||
cb3 = CallbackTracker()
|
||||
|
||||
d.addObserver("/message/body", cb1.call)
|
||||
d.addObserver("/message", cb1.call)
|
||||
d.addObserver("/presence", cb2.call)
|
||||
d.addObserver("//event/testevent", cb3.call)
|
||||
|
||||
msg = Element(("ns", "message"))
|
||||
msg.addElement("body")
|
||||
|
||||
pres = Element(("ns", "presence"))
|
||||
pres.addElement("presence")
|
||||
|
||||
d.dispatch(msg)
|
||||
self.assertEqual(cb1.called, 2)
|
||||
self.assertEqual(cb1.obj, msg)
|
||||
self.assertEqual(cb2.called, 0)
|
||||
|
||||
d.dispatch(pres)
|
||||
self.assertEqual(cb1.called, 2)
|
||||
self.assertEqual(cb2.called, 1)
|
||||
self.assertEqual(cb2.obj, pres)
|
||||
self.assertEqual(cb3.called, 0)
|
||||
|
||||
d.dispatch(d, "//event/testevent")
|
||||
self.assertEqual(cb3.called, 1)
|
||||
self.assertEqual(cb3.obj, d)
|
||||
|
||||
d.removeObserver("/presence", cb2.call)
|
||||
d.dispatch(pres)
|
||||
self.assertEqual(cb2.called, 1)
|
||||
|
||||
def test_addObserverTwice(self):
|
||||
"""
|
||||
Test adding two observers for the same query.
|
||||
|
||||
When the event is dispatched both of the observers need to be called.
|
||||
"""
|
||||
d = EventDispatcher()
|
||||
cb1 = CallbackTracker()
|
||||
cb2 = CallbackTracker()
|
||||
|
||||
d.addObserver("//event/testevent", cb1.call)
|
||||
d.addObserver("//event/testevent", cb2.call)
|
||||
d.dispatch(d, "//event/testevent")
|
||||
|
||||
self.assertEqual(cb1.called, 1)
|
||||
self.assertEqual(cb1.obj, d)
|
||||
self.assertEqual(cb2.called, 1)
|
||||
self.assertEqual(cb2.obj, d)
|
||||
|
||||
def test_addObserverInDispatch(self):
|
||||
"""
|
||||
Test for registration of an observer during dispatch.
|
||||
"""
|
||||
d = EventDispatcher()
|
||||
msg = Element(("ns", "message"))
|
||||
cb = CallbackTracker()
|
||||
|
||||
def onMessage(_):
|
||||
d.addObserver("/message", cb.call)
|
||||
|
||||
d.addOnetimeObserver("/message", onMessage)
|
||||
|
||||
d.dispatch(msg)
|
||||
self.assertEqual(cb.called, 0)
|
||||
|
||||
d.dispatch(msg)
|
||||
self.assertEqual(cb.called, 1)
|
||||
|
||||
d.dispatch(msg)
|
||||
self.assertEqual(cb.called, 2)
|
||||
|
||||
def test_addOnetimeObserverInDispatch(self):
|
||||
"""
|
||||
Test for registration of a onetime observer during dispatch.
|
||||
"""
|
||||
d = EventDispatcher()
|
||||
msg = Element(("ns", "message"))
|
||||
cb = CallbackTracker()
|
||||
|
||||
def onMessage(msg):
|
||||
d.addOnetimeObserver("/message", cb.call)
|
||||
|
||||
d.addOnetimeObserver("/message", onMessage)
|
||||
|
||||
d.dispatch(msg)
|
||||
self.assertEqual(cb.called, 0)
|
||||
|
||||
d.dispatch(msg)
|
||||
self.assertEqual(cb.called, 1)
|
||||
|
||||
d.dispatch(msg)
|
||||
self.assertEqual(cb.called, 1)
|
||||
|
||||
def testOnetimeDispatch(self):
|
||||
d = EventDispatcher()
|
||||
msg = Element(("ns", "message"))
|
||||
cb = CallbackTracker()
|
||||
|
||||
d.addOnetimeObserver("/message", cb.call)
|
||||
d.dispatch(msg)
|
||||
self.assertEqual(cb.called, 1)
|
||||
d.dispatch(msg)
|
||||
self.assertEqual(cb.called, 1)
|
||||
|
||||
def testDispatcherResult(self):
|
||||
d = EventDispatcher()
|
||||
msg = Element(("ns", "message"))
|
||||
pres = Element(("ns", "presence"))
|
||||
cb = CallbackTracker()
|
||||
|
||||
d.addObserver("/presence", cb.call)
|
||||
result = d.dispatch(msg)
|
||||
self.assertEqual(False, result)
|
||||
|
||||
result = d.dispatch(pres)
|
||||
self.assertEqual(True, result)
|
||||
|
||||
def testOrderedXPathDispatch(self):
|
||||
d = EventDispatcher()
|
||||
cb = OrderedCallbackTracker()
|
||||
d.addObserver("/message/body", cb.call2)
|
||||
d.addObserver("/message", cb.call3, -1)
|
||||
d.addObserver("/message/body", cb.call1, 1)
|
||||
|
||||
msg = Element(("ns", "message"))
|
||||
msg.addElement("body")
|
||||
d.dispatch(msg)
|
||||
self.assertEqual(
|
||||
cb.callList,
|
||||
[cb.call1, cb.call2, cb.call3],
|
||||
"Calls out of order: %s" % repr([c.__name__ for c in cb.callList]),
|
||||
)
|
||||
|
||||
# Observers are put into CallbackLists that are then put into dictionaries
|
||||
# keyed by the event trigger. Upon removal of the last observer for a
|
||||
# particular event trigger, the (now empty) CallbackList and corresponding
|
||||
# event trigger should be removed from those dictionaries to prevent
|
||||
# slowdown and memory leakage.
|
||||
|
||||
def test_cleanUpRemoveEventObserver(self):
|
||||
"""
|
||||
Test observer clean-up after removeObserver for named events.
|
||||
"""
|
||||
|
||||
d = EventDispatcher()
|
||||
cb = CallbackTracker()
|
||||
|
||||
d.addObserver("//event/test", cb.call)
|
||||
d.dispatch(None, "//event/test")
|
||||
self.assertEqual(1, cb.called)
|
||||
d.removeObserver("//event/test", cb.call)
|
||||
self.assertEqual(0, len(d._eventObservers.pop(0)))
|
||||
|
||||
def test_cleanUpRemoveXPathObserver(self):
|
||||
"""
|
||||
Test observer clean-up after removeObserver for XPath events.
|
||||
"""
|
||||
|
||||
d = EventDispatcher()
|
||||
cb = CallbackTracker()
|
||||
msg = Element((None, "message"))
|
||||
|
||||
d.addObserver("/message", cb.call)
|
||||
d.dispatch(msg)
|
||||
self.assertEqual(1, cb.called)
|
||||
d.removeObserver("/message", cb.call)
|
||||
self.assertEqual(0, len(d._xpathObservers.pop(0)))
|
||||
|
||||
def test_cleanUpOnetimeEventObserver(self):
|
||||
"""
|
||||
Test observer clean-up after onetime named events.
|
||||
"""
|
||||
|
||||
d = EventDispatcher()
|
||||
cb = CallbackTracker()
|
||||
|
||||
d.addOnetimeObserver("//event/test", cb.call)
|
||||
d.dispatch(None, "//event/test")
|
||||
self.assertEqual(1, cb.called)
|
||||
self.assertEqual(0, len(d._eventObservers.pop(0)))
|
||||
|
||||
def test_cleanUpOnetimeXPathObserver(self):
|
||||
"""
|
||||
Test observer clean-up after onetime XPath events.
|
||||
"""
|
||||
|
||||
d = EventDispatcher()
|
||||
cb = CallbackTracker()
|
||||
msg = Element((None, "message"))
|
||||
|
||||
d.addOnetimeObserver("/message", cb.call)
|
||||
d.dispatch(msg)
|
||||
self.assertEqual(1, cb.called)
|
||||
self.assertEqual(0, len(d._xpathObservers.pop(0)))
|
||||
|
||||
def test_observerRaisingException(self):
|
||||
"""
|
||||
Test that exceptions in observers do not bubble up to dispatch.
|
||||
|
||||
The exceptions raised in observers should be logged and other
|
||||
observers should be called as if nothing happened.
|
||||
"""
|
||||
|
||||
class OrderedCallbackList(utility.CallbackList):
|
||||
def __init__(self):
|
||||
self.callbacks = OrderedDict()
|
||||
|
||||
class TestError(Exception):
|
||||
pass
|
||||
|
||||
def raiseError(_):
|
||||
raise TestError()
|
||||
|
||||
d = EventDispatcher()
|
||||
cb = CallbackTracker()
|
||||
|
||||
originalCallbackList = utility.CallbackList
|
||||
|
||||
try:
|
||||
utility.CallbackList = OrderedCallbackList
|
||||
|
||||
d.addObserver("//event/test", raiseError)
|
||||
d.addObserver("//event/test", cb.call)
|
||||
try:
|
||||
d.dispatch(None, "//event/test")
|
||||
except TestError:
|
||||
self.fail("TestError raised. Should have been logged instead.")
|
||||
|
||||
self.assertEqual(1, len(self.flushLoggedErrors(TestError)))
|
||||
self.assertEqual(1, cb.called)
|
||||
finally:
|
||||
utility.CallbackList = originalCallbackList
|
||||
|
||||
|
||||
class XmlPipeTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{twisted.words.xish.utility.XmlPipe}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.pipe = utility.XmlPipe()
|
||||
|
||||
def test_sendFromSource(self):
|
||||
"""
|
||||
Send an element from the source and observe it from the sink.
|
||||
"""
|
||||
|
||||
def cb(obj):
|
||||
called.append(obj)
|
||||
|
||||
called = []
|
||||
self.pipe.sink.addObserver('/test[@xmlns="testns"]', cb)
|
||||
element = Element(("testns", "test"))
|
||||
self.pipe.source.send(element)
|
||||
self.assertEqual([element], called)
|
||||
|
||||
def test_sendFromSink(self):
|
||||
"""
|
||||
Send an element from the sink and observe it from the source.
|
||||
"""
|
||||
|
||||
def cb(obj):
|
||||
called.append(obj)
|
||||
|
||||
called = []
|
||||
self.pipe.source.addObserver('/test[@xmlns="testns"]', cb)
|
||||
element = Element(("testns", "test"))
|
||||
self.pipe.sink.send(element)
|
||||
self.assertEqual([element], called)
|
||||
@@ -0,0 +1,208 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.words.xish.xmlstream}.
|
||||
"""
|
||||
|
||||
|
||||
from twisted.internet import protocol
|
||||
from twisted.python import failure
|
||||
from twisted.trial import unittest
|
||||
from twisted.words.xish import domish, utility, xmlstream
|
||||
|
||||
|
||||
class XmlStreamTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.connectionLostMsg = "no reason"
|
||||
self.outlist = []
|
||||
self.xmlstream = xmlstream.XmlStream()
|
||||
self.xmlstream.transport = self
|
||||
self.xmlstream.transport.write = self.outlist.append
|
||||
|
||||
def loseConnection(self):
|
||||
"""
|
||||
Stub loseConnection because we are a transport.
|
||||
"""
|
||||
self.xmlstream.connectionLost(
|
||||
failure.Failure(Exception(self.connectionLostMsg))
|
||||
)
|
||||
|
||||
def test_send(self):
|
||||
"""
|
||||
Calling L{xmlstream.XmlStream.send} results in the data being written
|
||||
to the transport.
|
||||
"""
|
||||
self.xmlstream.connectionMade()
|
||||
self.xmlstream.send(b"<root>")
|
||||
self.assertEqual(self.outlist[0], b"<root>")
|
||||
|
||||
def test_receiveRoot(self):
|
||||
"""
|
||||
Receiving the starttag of the root element results in stream start.
|
||||
"""
|
||||
streamStarted = []
|
||||
|
||||
def streamStartEvent(rootelem):
|
||||
streamStarted.append(None)
|
||||
|
||||
self.xmlstream.addObserver(xmlstream.STREAM_START_EVENT, streamStartEvent)
|
||||
self.xmlstream.connectionMade()
|
||||
self.xmlstream.dataReceived("<root>")
|
||||
self.assertEqual(1, len(streamStarted))
|
||||
|
||||
def test_receiveBadXML(self):
|
||||
"""
|
||||
Receiving malformed XML results in an L{STREAM_ERROR_EVENT}.
|
||||
"""
|
||||
streamError = []
|
||||
streamEnd = []
|
||||
|
||||
def streamErrorEvent(reason):
|
||||
streamError.append(reason)
|
||||
|
||||
def streamEndEvent(_):
|
||||
streamEnd.append(None)
|
||||
|
||||
self.xmlstream.addObserver(xmlstream.STREAM_ERROR_EVENT, streamErrorEvent)
|
||||
self.xmlstream.addObserver(xmlstream.STREAM_END_EVENT, streamEndEvent)
|
||||
self.xmlstream.connectionMade()
|
||||
|
||||
self.xmlstream.dataReceived("<root>")
|
||||
self.assertEqual(0, len(streamError))
|
||||
self.assertEqual(0, len(streamEnd))
|
||||
|
||||
self.xmlstream.dataReceived("<child><unclosed></child>")
|
||||
self.assertEqual(1, len(streamError))
|
||||
self.assertTrue(streamError[0].check(domish.ParserError))
|
||||
self.assertEqual(1, len(streamEnd))
|
||||
|
||||
def test_streamEnd(self):
|
||||
"""
|
||||
Ending the stream fires a L{STREAM_END_EVENT}.
|
||||
"""
|
||||
streamEnd = []
|
||||
|
||||
def streamEndEvent(reason):
|
||||
streamEnd.append(reason)
|
||||
|
||||
self.xmlstream.addObserver(xmlstream.STREAM_END_EVENT, streamEndEvent)
|
||||
self.xmlstream.connectionMade()
|
||||
self.loseConnection()
|
||||
self.assertEqual(1, len(streamEnd))
|
||||
self.assertIsInstance(streamEnd[0], failure.Failure)
|
||||
self.assertEqual(streamEnd[0].getErrorMessage(), self.connectionLostMsg)
|
||||
|
||||
|
||||
class DummyProtocol(protocol.Protocol, utility.EventDispatcher):
|
||||
"""
|
||||
I am a protocol with an event dispatcher without further processing.
|
||||
|
||||
This protocol is only used for testing XmlStreamFactoryMixin to make
|
||||
sure the bootstrap observers are added to the protocol instance.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.observers = []
|
||||
|
||||
utility.EventDispatcher.__init__(self)
|
||||
|
||||
|
||||
class BootstrapMixinTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{xmlstream.BootstrapMixin}.
|
||||
|
||||
@ivar factory: Instance of the factory or mixin under test.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = xmlstream.BootstrapMixin()
|
||||
|
||||
def test_installBootstraps(self):
|
||||
"""
|
||||
Dispatching an event fires registered bootstrap observers.
|
||||
"""
|
||||
called = []
|
||||
|
||||
def cb(data):
|
||||
called.append(data)
|
||||
|
||||
dispatcher = DummyProtocol()
|
||||
self.factory.addBootstrap("//event/myevent", cb)
|
||||
self.factory.installBootstraps(dispatcher)
|
||||
|
||||
dispatcher.dispatch(None, "//event/myevent")
|
||||
self.assertEqual(1, len(called))
|
||||
|
||||
def test_addAndRemoveBootstrap(self):
|
||||
"""
|
||||
Test addition and removal of a bootstrap event handler.
|
||||
"""
|
||||
|
||||
called = []
|
||||
|
||||
def cb(data):
|
||||
called.append(data)
|
||||
|
||||
self.factory.addBootstrap("//event/myevent", cb)
|
||||
self.factory.removeBootstrap("//event/myevent", cb)
|
||||
|
||||
dispatcher = DummyProtocol()
|
||||
self.factory.installBootstraps(dispatcher)
|
||||
|
||||
dispatcher.dispatch(None, "//event/myevent")
|
||||
self.assertFalse(called)
|
||||
|
||||
|
||||
class GenericXmlStreamFactoryTestsMixin(BootstrapMixinTests):
|
||||
"""
|
||||
Generic tests for L{XmlStream} factories.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = xmlstream.XmlStreamFactory()
|
||||
|
||||
def test_buildProtocolInstallsBootstraps(self):
|
||||
"""
|
||||
The protocol factory installs bootstrap event handlers on the protocol.
|
||||
"""
|
||||
called = []
|
||||
|
||||
def cb(data):
|
||||
called.append(data)
|
||||
|
||||
self.factory.addBootstrap("//event/myevent", cb)
|
||||
|
||||
xs = self.factory.buildProtocol(None)
|
||||
xs.dispatch(None, "//event/myevent")
|
||||
|
||||
self.assertEqual(1, len(called))
|
||||
|
||||
def test_buildProtocolStoresFactory(self):
|
||||
"""
|
||||
The protocol factory is saved in the protocol.
|
||||
"""
|
||||
xs = self.factory.buildProtocol(None)
|
||||
self.assertIdentical(self.factory, xs.factory)
|
||||
|
||||
|
||||
class XmlStreamFactoryMixinTests(GenericXmlStreamFactoryTestsMixin):
|
||||
"""
|
||||
Tests for L{xmlstream.XmlStreamFactoryMixin}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = xmlstream.XmlStreamFactoryMixin(None, test=None)
|
||||
self.factory.protocol = DummyProtocol
|
||||
|
||||
def test_buildProtocolFactoryArguments(self):
|
||||
"""
|
||||
Arguments passed to the factory are passed to protocol on
|
||||
instantiation.
|
||||
"""
|
||||
xs = self.factory.buildProtocol(None)
|
||||
|
||||
self.assertEqual((None,), xs.args)
|
||||
self.assertEqual({"test": None}, xs.kwargs)
|
||||
@@ -0,0 +1,78 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.words.xmpproutertap}.
|
||||
"""
|
||||
|
||||
from twisted.application import internet
|
||||
from twisted.trial import unittest
|
||||
from twisted.words import xmpproutertap as tap
|
||||
from twisted.words.protocols.jabber import component
|
||||
|
||||
|
||||
class XMPPRouterTapTests(unittest.TestCase):
|
||||
def test_port(self) -> None:
|
||||
"""
|
||||
The port option is recognised as a parameter.
|
||||
"""
|
||||
opt = tap.Options()
|
||||
opt.parseOptions(["--port", "7001"])
|
||||
self.assertEqual(opt["port"], "7001")
|
||||
|
||||
def test_portDefault(self) -> None:
|
||||
"""
|
||||
The port option has '5347' as default value
|
||||
"""
|
||||
opt = tap.Options()
|
||||
opt.parseOptions([])
|
||||
self.assertEqual(opt["port"], "tcp:5347:interface=127.0.0.1")
|
||||
|
||||
def test_secret(self) -> None:
|
||||
"""
|
||||
The secret option is recognised as a parameter.
|
||||
"""
|
||||
opt = tap.Options()
|
||||
opt.parseOptions(["--secret", "hushhush"])
|
||||
self.assertEqual(opt["secret"], "hushhush")
|
||||
|
||||
def test_secretDefault(self) -> None:
|
||||
"""
|
||||
The secret option has 'secret' as default value
|
||||
"""
|
||||
opt = tap.Options()
|
||||
opt.parseOptions([])
|
||||
self.assertEqual(opt["secret"], "secret")
|
||||
|
||||
def test_verbose(self) -> None:
|
||||
"""
|
||||
The verbose option is recognised as a flag.
|
||||
"""
|
||||
opt = tap.Options()
|
||||
opt.parseOptions(["--verbose"])
|
||||
self.assertTrue(opt["verbose"])
|
||||
|
||||
def test_makeService(self) -> None:
|
||||
"""
|
||||
The service gets set up with a router and factory.
|
||||
"""
|
||||
opt = tap.Options()
|
||||
opt.parseOptions([])
|
||||
s = tap.makeService(opt)
|
||||
self.assertIsInstance(s, internet.StreamServerEndpointService)
|
||||
self.assertEqual("127.0.0.1", s.endpoint._interface)
|
||||
self.assertEqual(5347, s.endpoint._port)
|
||||
factory = s.factory
|
||||
self.assertIsInstance(factory, component.XMPPComponentServerFactory)
|
||||
self.assertIsInstance(factory.router, component.Router)
|
||||
self.assertEqual("secret", factory.secret)
|
||||
self.assertFalse(factory.logTraffic)
|
||||
|
||||
def test_makeServiceVerbose(self) -> None:
|
||||
"""
|
||||
The verbose flag enables traffic logging.
|
||||
"""
|
||||
opt = tap.Options()
|
||||
opt.parseOptions(["--verbose"])
|
||||
s = tap.makeService(opt)
|
||||
self.assertTrue(s.factory.logTraffic)
|
||||
@@ -0,0 +1,314 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.words.xish import xpath
|
||||
from twisted.words.xish.domish import Element
|
||||
from twisted.words.xish.xpath import XPathQuery
|
||||
from twisted.words.xish.xpathparser import SyntaxError # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class XPathTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
# Build element:
|
||||
# <foo xmlns='testns' attrib1='value1' attrib3="user@host/resource">
|
||||
# somecontent
|
||||
# <bar>
|
||||
# <foo>
|
||||
# <gar>DEF</gar>
|
||||
# </foo>
|
||||
# </bar>
|
||||
# somemorecontent
|
||||
# <bar attrib2="value2">
|
||||
# <bar>
|
||||
# <foo/>
|
||||
# <gar>ABC</gar>
|
||||
# </bar>
|
||||
# <bar/>
|
||||
# <bar attrib4='value4' attrib5='value5'>
|
||||
# <foo/>
|
||||
# <gar>JKL</gar>
|
||||
# </bar>
|
||||
# <bar attrib4='value4' attrib5='value4'>
|
||||
# <foo/>
|
||||
# <gar>MNO</gar>
|
||||
# </bar>
|
||||
# <bar attrib4='value4' attrib5='value6' attrib6='á'>
|
||||
# <quux>☃</quux>
|
||||
# </bar>
|
||||
# </foo>
|
||||
self.e = Element(("testns", "foo"))
|
||||
self.e["attrib1"] = "value1"
|
||||
self.e["attrib3"] = "user@host/resource"
|
||||
self.e.addContent("somecontent")
|
||||
self.bar1 = self.e.addElement("bar")
|
||||
self.subfoo = self.bar1.addElement("foo")
|
||||
self.gar1 = self.subfoo.addElement("gar")
|
||||
self.gar1.addContent("DEF")
|
||||
self.e.addContent("somemorecontent")
|
||||
self.bar2 = self.e.addElement("bar")
|
||||
self.bar2["attrib2"] = "value2"
|
||||
self.bar3 = self.bar2.addElement("bar")
|
||||
self.subfoo2 = self.bar3.addElement("foo")
|
||||
self.gar2 = self.bar3.addElement("gar")
|
||||
self.gar2.addContent("ABC")
|
||||
self.bar4 = self.e.addElement("bar")
|
||||
self.bar5 = self.e.addElement("bar")
|
||||
self.bar5["attrib4"] = "value4"
|
||||
self.bar5["attrib5"] = "value5"
|
||||
self.subfoo3 = self.bar5.addElement("foo")
|
||||
self.gar3 = self.bar5.addElement("gar")
|
||||
self.gar3.addContent("JKL")
|
||||
self.bar6 = self.e.addElement("bar")
|
||||
self.bar6["attrib4"] = "value4"
|
||||
self.bar6["attrib5"] = "value4"
|
||||
self.subfoo4 = self.bar6.addElement("foo")
|
||||
self.gar4 = self.bar6.addElement("gar")
|
||||
self.gar4.addContent("MNO")
|
||||
self.bar7 = self.e.addElement("bar")
|
||||
self.bar7["attrib4"] = "value4"
|
||||
self.bar7["attrib5"] = "value6"
|
||||
self.bar7["attrib6"] = "á"
|
||||
self.quux = self.bar7.addElement("quux")
|
||||
self.quux.addContent("\N{SNOWMAN}")
|
||||
|
||||
def test_staticMethods(self) -> None:
|
||||
"""
|
||||
Test basic operation of the static methods.
|
||||
"""
|
||||
self.assertEqual(xpath.matches("/foo/bar", self.e), True)
|
||||
self.assertEqual(
|
||||
xpath.queryForNodes("/foo/bar", self.e),
|
||||
[self.bar1, self.bar2, self.bar4, self.bar5, self.bar6, self.bar7],
|
||||
)
|
||||
self.assertEqual(xpath.queryForString("/foo", self.e), "somecontent")
|
||||
self.assertEqual(
|
||||
xpath.queryForStringList("/foo", self.e), ["somecontent", "somemorecontent"]
|
||||
)
|
||||
|
||||
def test_locationFooBar(self) -> None:
|
||||
"""
|
||||
Test matching foo with child bar.
|
||||
"""
|
||||
xp = XPathQuery("/foo/bar")
|
||||
self.assertEqual(xp.matches(self.e), 1)
|
||||
|
||||
def test_locationFooBarFoo(self) -> None:
|
||||
"""
|
||||
Test finding foos at the second level.
|
||||
"""
|
||||
xp = XPathQuery("/foo/bar/foo")
|
||||
self.assertEqual(xp.matches(self.e), 1)
|
||||
self.assertEqual(
|
||||
xp.queryForNodes(self.e), [self.subfoo, self.subfoo3, self.subfoo4]
|
||||
)
|
||||
|
||||
def test_locationNoBar3(self) -> None:
|
||||
"""
|
||||
Test not finding bar3.
|
||||
"""
|
||||
xp = XPathQuery("/foo/bar3")
|
||||
self.assertEqual(xp.matches(self.e), 0)
|
||||
|
||||
def test_locationAllChilds(self) -> None:
|
||||
"""
|
||||
Test finding childs of foo.
|
||||
"""
|
||||
xp = XPathQuery("/foo/*")
|
||||
self.assertEqual(xp.matches(self.e), True)
|
||||
self.assertEqual(
|
||||
xp.queryForNodes(self.e),
|
||||
[self.bar1, self.bar2, self.bar4, self.bar5, self.bar6, self.bar7],
|
||||
)
|
||||
|
||||
def test_attribute(self) -> None:
|
||||
"""
|
||||
Test matching foo with attribute.
|
||||
"""
|
||||
xp = XPathQuery("/foo[@attrib1]")
|
||||
self.assertEqual(xp.matches(self.e), True)
|
||||
|
||||
def test_attributeWithValueAny(self) -> None:
|
||||
"""
|
||||
Test find nodes with attribute having value.
|
||||
"""
|
||||
xp = XPathQuery("/foo/*[@attrib2='value2']")
|
||||
self.assertEqual(xp.matches(self.e), True)
|
||||
self.assertEqual(xp.queryForNodes(self.e), [self.bar2])
|
||||
|
||||
def test_locationWithValueUnicode(self) -> None:
|
||||
"""
|
||||
Nodes' attributes can be matched with non-ASCII values.
|
||||
"""
|
||||
xp = XPathQuery("/foo/*[@attrib6='á']")
|
||||
self.assertEqual(xp.matches(self.e), True)
|
||||
self.assertEqual(xp.queryForNodes(self.e), [self.bar7])
|
||||
|
||||
def test_namespaceFound(self) -> None:
|
||||
"""
|
||||
Test matching node with namespace.
|
||||
"""
|
||||
xp = XPathQuery("/foo[@xmlns='testns']/bar")
|
||||
self.assertEqual(xp.matches(self.e), 1)
|
||||
|
||||
def test_namespaceNotFound(self) -> None:
|
||||
"""
|
||||
Test not matching node with wrong namespace.
|
||||
"""
|
||||
xp = XPathQuery("/foo[@xmlns='badns']/bar2")
|
||||
self.assertEqual(xp.matches(self.e), 0)
|
||||
|
||||
def test_attributeWithValue(self) -> None:
|
||||
"""
|
||||
Test matching node with attribute having value.
|
||||
"""
|
||||
xp = XPathQuery("/foo[@attrib1='value1']")
|
||||
self.assertEqual(xp.matches(self.e), 1)
|
||||
|
||||
def test_queryForString(self) -> None:
|
||||
"""
|
||||
queryforString on absolute paths returns their first CDATA.
|
||||
"""
|
||||
xp = XPathQuery("/foo")
|
||||
self.assertEqual(xp.queryForString(self.e), "somecontent")
|
||||
|
||||
def test_queryForStringList(self) -> None:
|
||||
"""
|
||||
queryforStringList on absolute paths returns all their CDATA.
|
||||
"""
|
||||
xp = XPathQuery("/foo")
|
||||
self.assertEqual(
|
||||
xp.queryForStringList(self.e), ["somecontent", "somemorecontent"]
|
||||
)
|
||||
|
||||
def test_queryForStringListAnyLocation(self) -> None:
|
||||
"""
|
||||
queryforStringList on relative paths returns all their CDATA.
|
||||
"""
|
||||
xp = XPathQuery("//foo")
|
||||
self.assertEqual(
|
||||
xp.queryForStringList(self.e), ["somecontent", "somemorecontent"]
|
||||
)
|
||||
|
||||
def test_queryForNodes(self) -> None:
|
||||
"""
|
||||
Test finding nodes.
|
||||
"""
|
||||
xp = XPathQuery("/foo/bar")
|
||||
self.assertEqual(
|
||||
xp.queryForNodes(self.e),
|
||||
[self.bar1, self.bar2, self.bar4, self.bar5, self.bar6, self.bar7],
|
||||
)
|
||||
|
||||
def test_textCondition(self) -> None:
|
||||
"""
|
||||
Test matching a node with given text.
|
||||
"""
|
||||
xp = XPathQuery("/foo[text() = 'somecontent']")
|
||||
self.assertEqual(xp.matches(self.e), True)
|
||||
|
||||
def test_textConditionUnicode(self) -> None:
|
||||
"""
|
||||
A node can be matched by text with non-ascii code points.
|
||||
"""
|
||||
xp = XPathQuery("//*[text()='\N{SNOWMAN}']")
|
||||
self.assertEqual(xp.matches(self.e), True)
|
||||
self.assertEqual(xp.queryForNodes(self.e), [self.quux])
|
||||
|
||||
def test_textNotOperator(self) -> None:
|
||||
"""
|
||||
Test for not operator.
|
||||
"""
|
||||
xp = XPathQuery("/foo[not(@nosuchattrib)]")
|
||||
self.assertEqual(xp.matches(self.e), True)
|
||||
|
||||
def test_anyLocationAndText(self) -> None:
|
||||
"""
|
||||
Test finding any nodes named gar and getting their text contents.
|
||||
"""
|
||||
xp = XPathQuery("//gar")
|
||||
self.assertEqual(xp.matches(self.e), True)
|
||||
self.assertEqual(
|
||||
xp.queryForNodes(self.e), [self.gar1, self.gar2, self.gar3, self.gar4]
|
||||
)
|
||||
self.assertEqual(xp.queryForStringList(self.e), ["DEF", "ABC", "JKL", "MNO"])
|
||||
|
||||
def test_anyLocation(self) -> None:
|
||||
"""
|
||||
Test finding any nodes named bar.
|
||||
"""
|
||||
xp = XPathQuery("//bar")
|
||||
self.assertEqual(xp.matches(self.e), True)
|
||||
self.assertEqual(
|
||||
xp.queryForNodes(self.e),
|
||||
[
|
||||
self.bar1,
|
||||
self.bar2,
|
||||
self.bar3,
|
||||
self.bar4,
|
||||
self.bar5,
|
||||
self.bar6,
|
||||
self.bar7,
|
||||
],
|
||||
)
|
||||
|
||||
def test_anyLocationQueryForString(self) -> None:
|
||||
"""
|
||||
L{XPathQuery.queryForString} should raise a L{NotImplementedError}
|
||||
for any location.
|
||||
"""
|
||||
xp = XPathQuery("//bar")
|
||||
self.assertRaises(NotImplementedError, xp.queryForString, None)
|
||||
|
||||
def test_andOperator(self) -> None:
|
||||
"""
|
||||
Test boolean and operator in condition.
|
||||
"""
|
||||
xp = XPathQuery("//bar[@attrib4='value4' and @attrib5='value5']")
|
||||
self.assertEqual(xp.matches(self.e), True)
|
||||
self.assertEqual(xp.queryForNodes(self.e), [self.bar5])
|
||||
|
||||
def test_orOperator(self) -> None:
|
||||
"""
|
||||
Test boolean or operator in condition.
|
||||
"""
|
||||
xp = XPathQuery("//bar[@attrib5='value4' or @attrib5='value5']")
|
||||
self.assertEqual(xp.matches(self.e), True)
|
||||
self.assertEqual(xp.queryForNodes(self.e), [self.bar5, self.bar6])
|
||||
|
||||
def test_booleanOperatorsParens(self) -> None:
|
||||
"""
|
||||
Test multiple boolean operators in condition with parens.
|
||||
"""
|
||||
xp = XPathQuery(
|
||||
"""//bar[@attrib4='value4' and
|
||||
(@attrib5='value4' or @attrib5='value6')]"""
|
||||
)
|
||||
self.assertEqual(xp.matches(self.e), True)
|
||||
self.assertEqual(xp.queryForNodes(self.e), [self.bar6, self.bar7])
|
||||
|
||||
def test_booleanOperatorsNoParens(self) -> None:
|
||||
"""
|
||||
Test multiple boolean operators in condition without parens.
|
||||
"""
|
||||
xp = XPathQuery(
|
||||
"""//bar[@attrib5='value4' or
|
||||
@attrib5='value5' or
|
||||
@attrib5='value6']"""
|
||||
)
|
||||
self.assertEqual(xp.matches(self.e), True)
|
||||
self.assertEqual(xp.queryForNodes(self.e), [self.bar5, self.bar6, self.bar7])
|
||||
|
||||
def test_badXPathNoClosingBracket(self) -> None:
|
||||
"""
|
||||
A missing closing bracket raises a SyntaxError.
|
||||
|
||||
This test excercises the most common failure mode.
|
||||
"""
|
||||
exc = self.assertRaises(SyntaxError, XPathQuery, """//bar[@attrib1""")
|
||||
self.assertTrue(
|
||||
exc.msg.startswith("Trying to find one of"),
|
||||
("SyntaxError message '%s' doesn't start with " "'Trying to find one of'")
|
||||
% exc.msg,
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- test-case-name: twisted.words.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
Twisted X-ish: XML-ish DOM and XPath-ish engine
|
||||
|
||||
"""
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user