[meego-commits] 9357: Changes to Trunk/papyon
Peter Zhu
no_reply at build.meego.com
Wed Nov 10 08:36:41 UTC 2010
Hi,
I have made the following changes to papyon in project Trunk. Please review and accept ASAP.
Thank You,
Peter Zhu
[This message was auto-generated]
---
Request #9357:
submit: Trunk:Testing/papyon(r4) -> Trunk/papyon
Message:
Move to Trunk
State: new 2010-11-10T00:36:40 peter
Comment: None
changes files:
--------------
--- papyon.changes
+++ papyon.changes
@@ -0,0 +1,3 @@
+* Mon Nov 08 2010 Rob Bradford <rob at linux.intel.com> - 0.5.2
+- Update to latest release to support MSN protocol changes (BMC#8988)
+
old:
----
papyon-0.4.9.tar.gz
new:
----
papyon-0.5.2.tar.gz
spec files:
-----------
--- papyon.spec
+++ papyon.spec
@@ -1,6 +1,6 @@
#
-# Do not Edit! Generated by:
-# spectacle version 0.18
+# Do NOT Edit the Auto-generated Part!
+# Generated by: spectacle version 0.18
#
# >> macros
# << macros
@@ -8,7 +8,7 @@
%{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")}
Name: papyon
Summary: Python libraries for MSN Messenger network
-Version: 0.4.9
+Version: 0.5.2
Release: 1
Group: System/Libraries
License: GPLv2+
other changes:
--------------
++++++ papyon-0.4.9.tar.gz -> papyon-0.5.2.tar.gz
--- NEWS
+++ NEWS
@@ -1,3 +1,39 @@
+papyon-0.5.2 (2010-10-21)
+=========================
+
+Fixes:
+ * Use the right policy reference when requesting security token (fdo #31004)
+ * Set the peer end-points before requesting his display picture (fdo #30411)
+ * Don't queue all P2P chunks right away when using SB transport (fdo #29512)
+ * Don't send invalid command when contact is in Allow and Block lists
+ * Don't fail when current media that is not music (fdo #30625)
+
+papyon-0.5.1 (2010-09-10)
+=========================
+
+Fixes:
+ * Multiple fixes to video-conference and proxy support
+ * Only update MSN Object once we received extended presence (fdo #29967)
+ * Fallback on public picture when default one fails (roaming) (fdo #29965)
+ * Support MSNP18-style offline messaging
+ * Better handling of switchboard sessions
+ * Minor fixes for bugs fdo #29742, #29763 and #29764
+
+papyon-0.5.0 (2010-08-12)
+=========================
+
+Fixes:
+ * Multiple fixes to file-transfer support
+ * Crop password to first 16 characters (fdo #27613)
+ * Decode name and subject in new email notifications (fdo #27987)
+ * Use timeout_add_seconds instead of timeout_add where possible
+
+Enhancements:
+ * Added MPOP support (Multiple Points of Presence)
+ * Added back video-conference support through tunneled SIP
+ * Added P2Pv2 support
+ * Added SOCKS5 proxy implementation and improved proxy support globally.
+
papyon-0.4.9 (2010-07-09)
=========================
--- PKG-INFO
+++ PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.0
Name: papyon
-Version: 0.4.9
+Version: 0.5.2
Summary: Python msn client library
Home-page: http://telepathy.freedesktop.org/wiki/Papyon
Author: Youness Alaoui
--- README
+++ README
@@ -1,7 +1,7 @@
-papyon - Python msn client library
+papyon - Python MSN client library
=================================
-Homepage: http://telepathy.freedesktop.org/wiki/Papyon
+Homepage: http://www.freedesktop.org/wiki/Software/papyon
papyon is an MSN client library, that tries to abstract the MSN protocol
gory details. papyon is a fork of the unmaintained library pymsn
@@ -10,5 +10,8 @@
Dependencies
============
python (>= 2.5)
-python-gobject (>=2.10)
-pyOpenSSL (>=0.6)
+python-gobject (>= 2.10)
+pyOpenSSL (>= 0.6)
+gst-python (>= 0.10)
+python-farsight
+pycrypto
--- papyon/__init__.py
+++ papyon/__init__.py
@@ -26,7 +26,7 @@
@group Network Layer: gnet
"""
-version = (0, 4, 9)
+version = (0, 5, 2)
__version__ = ".".join(str(x) for x in version)
__author__ = "Youness Alaoui <kakaroto at users.sourceforge.net>"
@@ -40,5 +40,5 @@
import sip
import gnet.proxy
-Proxy = gnet.proxy.ProxyFactory
+Proxy = gnet.proxy.ProxyInfos.from_string
ProxyInfos = gnet.proxy.ProxyInfos
--- papyon/client.py
+++ papyon/client.py
@@ -87,13 +87,16 @@
import papyon.service.AddressBook as AB
import papyon.service.OfflineIM as OIM
import papyon.service.Spaces as Spaces
+import papyon.service.ContentRoaming as CR
from papyon.util.decorator import rw_property
from papyon.transport import *
from papyon.switchboard_manager import SwitchboardManager
+from papyon.media import RTCActivityManager
from papyon.msnp2p import P2PSessionManager
+from papyon.msnp2p.transport.switchboard import SwitchboardP2PTransport
from papyon.p2p import MSNObjectStore, FileTransferManager, WebcamHandler
-from papyon.sip import SIPConnectionManager
+from papyon.sip import SIPCallManager
from papyon.conversation import SwitchboardConversation, \
ExternalNetworkConversation
from papyon.event import ClientState, ClientErrorType, \
@@ -121,6 +124,7 @@
@param proxies: proxies that we can use to connect
@type proxies: {type: string => L{gnet.proxy.ProxyInfos}}
+ @note the key should be 'http', 'https' or 'direct'
@param transport_class: the transport class to use for the network
connection
@@ -145,7 +149,8 @@
self._webcam_handler = WebcamHandler(self)
self._p2p_session_manager.register_handler(self._webcam_handler)
- self._call_manager = SIPConnectionManager(self, self._protocol)
+ self._call_manager = SIPCallManager(self)
+ self._rtc_activity_manager = RTCActivityManager(self, self._protocol)
self._msn_object_store = MSNObjectStore(self)
self._p2p_session_manager.register_handler(self._msn_object_store)
@@ -160,6 +165,7 @@
self._address_book = None
self._oim_box = None
self._mailbox = None
+ self._roaming = None
self.__die = False
self.__connect_transport_signals()
@@ -194,17 +200,27 @@
@property
def call_manager(self):
- """The SIP connection manager
- @type: L{SIPConnectionManager<papyon.sip.SIPConnectionManager>}"""
+ """The SIP call manager
+ @type: L{SIPCallManager<papyon.sip.SIPCallManager>}"""
return self._call_manager
@property
+ def content_roaming(self):
+ return self._roaming
+
+ @property
def ft_manager(self):
"""The files transfer manager
@type: L{FileTransferManager<papyon.p2p.FileTransferManager>}"""
return self._ft_manager
@property
+ def rtc_activity_manager(self):
+ """The RTC activity manager
+ @type: L{RTCActivityManager<papyon.media.RTCActivityManager>}"""
+ return self._rtc_activity_manager
+
+ @property
def oim_box(self):
"""The offline IM for the current user
@rtype: L{OfflineIM<papyon.service.OfflineIM>}"""
@@ -305,6 +321,12 @@
### private:
def __connect_profile_signals(self):
"""Connect profile signals"""
+ def event(contact, *args):
+ event_name = args[-1]
+ event_args = args[:-1]
+ method_name = "on_profile_%s" % event_name.replace("-", "_")
+ self._dispatch(method_name, *event_args)
+
def property_changed(profile, pspec):
method_name = "on_profile_%s_changed" % pspec.name.replace("-", "_")
self._dispatch(method_name)
@@ -314,6 +336,12 @@
self.profile.connect("notify::personal-message", property_changed)
self.profile.connect("notify::current-media", property_changed)
self.profile.connect("notify::msn-object", property_changed)
+ self.profile.connect("notify::end-points", property_changed)
+
+ def connect_signal(name):
+ self.profile.connect(name, event, name)
+ connect_signal("end-point-added")
+ connect_signal("end-point-removed")
def __connect_mailbox_signals(self):
"""Connect mailbox signals"""
@@ -346,6 +374,7 @@
contact.connect("notify::current-media", property_changed)
contact.connect("notify::msn-object", property_changed)
contact.connect("notify::client-capabilities", property_changed)
+ contact.connect("notify::end-points", property_changed)
def connect_signal(name):
contact.connect(name, event, name)
@@ -362,6 +391,7 @@
self._oim_box = OIM.OfflineMessagesBox(self._sso, self, self._proxies)
self.__connect_oim_box_signals()
self._spaces = Spaces.Spaces(self._sso, self._proxies)
+ self._roaming = CR.ContentRoaming(self._sso, self._address_book, self._proxies)
self._state = ClientState.CONNECTED
@@ -438,6 +468,8 @@
if handler_class is SwitchboardConversation:
if self._dispatch("on_invite_conversation", handler) == 0:
logger.warning("No event handler attached for conversations")
+ elif handler_class in [SwitchboardP2PTransport]:
+ pass
else:
logger.warning("Unknown Switchboard Handler class %s" % handler_class)
@@ -465,6 +497,7 @@
self.address_book.connect(name, event, name)
connect_signal("contact-added")
+ connect_signal("contact-pending")
connect_signal("contact-deleted")
connect_signal("contact-blocked")
connect_signal("contact-unblocked")
@@ -506,7 +539,7 @@
def invite_received(call_manager, call):
self._dispatch("on_invite_conference", call)
- self._call_manager.connect("invite-received", invite_received)
+ self._call_manager.connect("incoming-call", invite_received)
def __connect_ft_manager_signals(self):
"""Connect File Transfer Manager signals"""
--- papyon/conversation.py
+++ papyon/conversation.py
@@ -26,7 +26,7 @@
import msnp
import p2p
-from switchboard_manager import SwitchboardClient
+from switchboard_manager import SwitchboardHandler
from papyon.event import EventsDispatcher
from papyon.profile import NetworkID
@@ -59,7 +59,7 @@
external_contacts = set(contacts) - msn_contacts
if len(external_contacts) == 0:
- return SwitchboardConversation(client, contacts)
+ return SwitchboardConversation(client, None, contacts)
elif len(msn_contacts) != 0:
raise NotImplementedError("The protocol doesn't allow mixing " \
"contacts from different networks in a single conversation")
@@ -290,7 +290,7 @@
self.__last_received_msn_objects = {}
- def send_text_message(self, message):
+ def send_text_message(self, message, callback=None, errback=None):
if len(message.msn_objects) > 0:
body = []
for alias, msn_object in message.msn_objects.iteritems():
@@ -308,7 +308,7 @@
if message.formatting is not None:
headers["X-MMS-IM-Format"] = str(message.formatting)
- self._send_message(content_type, body, headers, ack)
+ self._send_message(content_type, body, headers, ack, callback, errback)
def send_nudge(self):
content_type = "text/x-msnmsgr-datacast"
@@ -330,7 +330,7 @@
raise NotImplementedError
def _send_message(self, content_type, body, headers={},
- ack=msnp.MessageAcknowledgement.HALF):
+ ack=msnp.MessageAcknowledgement.HALF, callback=None, errback=None):
raise NotImplementedError
def _on_contact_joined(self, contact):
@@ -404,7 +404,7 @@
self._client._unregister_external_conversation(self)
def _send_message(self, content_type, body, headers={},
- ack=msnp.MessageAcknowledgement.HALF):
+ ack=msnp.MessageAcknowledgement.HALF, callback=None, errback=None):
if content_type[0] in ['text/x-mms-emoticon',
'text/x-mms-animemoticon']:
return
@@ -418,9 +418,9 @@
send_unmanaged_message(contact, message)
-class SwitchboardConversation(AbstractConversation, SwitchboardClient):
- def __init__(self, client, contacts):
- SwitchboardClient.__init__(self, client, contacts, priority=0)
+class SwitchboardConversation(AbstractConversation, SwitchboardHandler):
+ def __init__(self, client, switchboard, contacts):
+ SwitchboardHandler.__init__(self, client, switchboard, contacts, priority=0)
AbstractConversation.__init__(self, client)
@staticmethod
@@ -433,17 +433,32 @@
'text/x-msnmsgr-datacast', 'text/x-mms-emoticon',
'text/x-mms-animemoticon')
+ @staticmethod
+ def handle_message(client, switchboard, message):
+ return SwitchboardConversation(client, switchboard, ())
+
def invite_user(self, contact):
"""Request a contact to join in the conversation.
@param contact: the contact to invite.
@type contact: L{profile.Contact}"""
- SwitchboardClient._invite_user(self, contact)
+ SwitchboardHandler._invite_user(self, contact)
def leave(self):
"""Leave the conversation."""
- SwitchboardClient._leave(self)
+ SwitchboardHandler._leave(self)
def _send_message(self, content_type, body, headers={},
- ack=msnp.MessageAcknowledgement.HALF):
- SwitchboardClient._send_message(self, content_type, body, headers, ack)
+ ack=msnp.MessageAcknowledgement.HALF, callback=None, errback=None):
+ SwitchboardHandler._send_message(self, content_type, body, headers,
+ ack, callback=callback, errback=errback)
+
+ def _on_closed(self):
+ self._dispatch("on_conversation_closed")
+
+ def _on_switchboard_closed(self):
+ pass
+
+ def __repr__(self):
+ participants = ",".join(map(lambda p: p.account, self.total_participants))
+ return '<SwitchboardConversation participants="%s">' % participants
--- papyon/event/address_book.py
+++ papyon/event/address_book.py
@@ -31,6 +31,9 @@
def on_addressbook_contact_added(self, contact):
pass
+ def on_addressbook_contact_pending(self, contact):
+ pass
+
def on_addressbook_contact_deleted(self, contact):
pass
--- papyon/event/contact.py
+++ papyon/event/contact.py
@@ -82,10 +82,15 @@
def on_contact_msn_object_changed(self, contact):
"""Called when the MSNObject of a contact changes.
- @param contact: the contact whose presence changed
+ @param contact: the contact whose msn object changed
@type contact: L{Contact<papyon.profile.Contact>}
@see: L{MSNObjectStore<papyon.p2p.MSNObjectStore>},
L{MSNObject<papyon.p2p.MSNObject>}"""
pass
+ def on_contact_end_points_changed(self, contact):
+ """Called when the end points of a contact change.
+ @param contact: the contact whose end points changed
+ @type contact: L{Contact<papyon.profile.Contact>}"""
+ pass
--- papyon/event/conversation.py
+++ papyon/event/conversation.py
@@ -118,3 +118,6 @@
@type sender: L{Contact<papyon.profile.Contact>}"""
pass
+ def on_conversation_closed(self):
+ """Called when the conversation is closed."""
+ pass
--- papyon/event/profile.py
+++ papyon/event/profile.py
@@ -55,3 +55,15 @@
def on_profile_msn_object_changed(self):
"""Called when the MSNObject changes."""
pass
+
+ def on_profile_end_points_changed(self):
+ """Called when end points change."""
+ pass
+
+ def on_profile_end_point_added(self, end_point):
+ """Called when a new end point connects."""
+ pass
+
+ def on_profile_end_point_removed(self, end_point):
+ """Called when an end point disconnects."""
+ pass
--- papyon/gnet/io/iochannel.py
+++ papyon/gnet/io/iochannel.py
@@ -23,6 +23,7 @@
from abstract import AbstractClient
import gobject
+import socket
from errno import *
__all__ = ['GIOChannelClient']
@@ -160,6 +161,13 @@
self._transport.close()
self._status = IoStatus.CLOSED
+ def disable(self):
+ # Disable the channel without actually closing the socket
+ # Rationnale: some other client might have the ownership of the socket
+ if self._status in (IoStatus.CLOSING, IoStatus.CLOSED):
+ return
+ self._watch_remove()
+
def send(self, buffer, callback=None, *args):
assert(self._status == IoStatus.OPEN), self._status
self._outgoing_queue.append(OutgoingPacket(buffer, len(buffer),
--- papyon/gnet/io/ssl_socket.py
+++ papyon/gnet/io/ssl_socket.py
@@ -64,7 +64,7 @@
return False
if self._status == IoStatus.OPENING:
try:
- self._transport.do_handshake()
+ self._transport.set_connect_state()
except (OpenSSL.WantX509LookupError,
OpenSSL.WantReadError, OpenSSL.WantWriteError):
return True
--- papyon/gnet/parser.py
+++ papyon/gnet/parser.py
@@ -43,11 +43,25 @@
@type transport: an object derived from
L{io.AbstractClient}"""
gobject.GObject.__init__(self)
- if connect_signals:
- transport.connect("received", self._on_received)
- transport.connect("notify::status", self._on_status_change)
self._transport = transport
self._reset_state()
+ self._handles = []
+ if connect_signals:
+ self.enable()
+
+ def enable(self):
+ if self._handles:
+ self.disable()
+ self._handles.append(self._transport.connect("received",
+ self._on_received))
+ self._handles.append(self._transport.connect("notify::status",
+ self._on_status_change))
+
+ def disable(self):
+ for handle in self._handles:
+ self._transport.disconnect(handle)
+ self._handles = []
+ self._reset_state()
def _reset_state(self):
"""Needs to be overriden in order to implement the default
@@ -140,12 +154,23 @@
transport.connect("notify::status", self._on_status_change)
AbstractParser.__init__(self, transport, connect_signals=False)
+ def enable(self):
+ AbstractParser.enable(self)
+ self._parser.enable()
+
+ def disable(self):
+ AbstractParser.disable(self)
+ self._parser.disable()
+
def _reset_state(self):
self._next_chunk = self.CHUNK_START_LINE
self._receive_buffer = ""
- self._content_length = None
+ self._content_length = 0
self._parser.delimiter = "\r\n"
+ def _on_received(self, transport, buf, length):
+ pass
+
def _on_status_change(self, transport, param):
status = transport.get_property("status")
if status == IoStatus.OPEN:
--- papyon/gnet/protocol/HTTP.py
+++ papyon/gnet/protocol/HTTP.py
@@ -22,13 +22,17 @@
from papyon.gnet.message.HTTP import HTTPRequest
from papyon.gnet.io import TCPClient
from papyon.gnet.parser import HTTPParser
+from papyon.gnet.proxy.factory import ProxyFactory
import gobject
import base64
+import logging
import platform
__all__ = ['HTTP']
+logger = logging.getLogger('papyon.gnet.HTTP')
+
class HTTP(gobject.GObject):
"""HTTP protocol client class."""
@@ -47,7 +51,7 @@
(object,)), # HTTPRequest
}
- def __init__(self, host, port=80, proxy=None):
+ def __init__(self, host, port=80, proxies={}):
"""Connection initialization
@param host: the host to connect to.
@@ -56,18 +60,22 @@
@param port: the port number to connect to
@type port: integer
- @param proxy: proxy that we can use to connect
- @type proxy: L{gnet.proxy.ProxyInfos}"""
+ @param proxies: proxies that we can use to connect
+ @type proxies: L{gnet.proxy.ProxyInfos}"""
gobject.GObject.__init__(self)
- assert(proxy is None or proxy.type == 'http') # TODO: add support for other proxies (socks4 and 5)
self._host = host
self._port = port
- self.__proxy = proxy
+ self._proxies = proxies
+ self._http_proxy = None
self._transport = None
self._http_parser = None
self._outgoing_queue = []
self._waiting_response = False
+ if self._proxies and self._proxies.get('http', None):
+ if self._proxies['http'].type == 'http':
+ self._http_proxy = self._proxies['http']
+
self._errored = False
self.connect("error", self._on_self_error)
@@ -76,19 +84,26 @@
def _setup_transport(self):
if self._transport is None:
- if self.__proxy is not None:
- self._transport = TCPClient(self.__proxy.host, self.__proxy.port)
+ if self._http_proxy:
+ self._transport = TCPClient(self._http_proxy.host,
+ self._http_proxy.port)
else:
self._transport = TCPClient(self._host, self._port)
- self._http_parser = HTTPParser(self._transport)
- self._http_parser.connect("received", self._on_response_received)
- self._transport.connect("notify::status", self._on_status_change)
- self._transport.connect("error", self._on_error)
- self._transport.connect("sent", self._on_request_sent)
+ if self._proxies:
+ self._transport = ProxyFactory(self._transport,
+ self._proxies, 'http')
+ self._setup_parser()
if self._transport.get_property("status") != IoStatus.OPEN:
self._transport.open()
+ def _setup_parser(self):
+ self._http_parser = HTTPParser(self._transport)
+ self._http_parser.connect("received", self._on_response_received)
+ self._transport.connect("notify::status", self._on_status_change)
+ self._transport.connect("error", self._on_error)
+ self._transport.connect("sent", self._on_request_sent)
+
def _on_status_change(self, transport, param):
if transport.get_property("status") == IoStatus.OPEN:
self._process_queue()
@@ -122,7 +137,12 @@
# self._setup_transport()
# return
self._outgoing_queue.pop(0) # pop the request from the queue
- self.emit("response-received", response)
+ if response.status >= 400:
+ logger.error("Received error code %i (%s) from %s:%i" %
+ (response.status, response.reason, self._host, self._port))
+ self.emit("error", response.status)
+ else:
+ self.emit("response-received", response)
self._waiting_response = False
self._process_queue() # next request ?
@@ -148,10 +168,10 @@
user_agent = GNet.NAME, GNet.VERSION, platform.system(), platform.machine()
headers['User-Agent'] = "%s/%s (%s %s)" % user_agent
- if self.__proxy is not None:
+ if self._http_proxy is not None:
url = 'http://%s:%d%s' % (self._host, self._port, resource)
- if self.__proxy.user:
- auth = self.__proxy.user + ':' + self.__proxy.password
+ if self._http_proxy.user:
+ auth = self._http_proxy.user + ':' + self._http_proxy.password
credentials = base64.encodestring(auth).strip()
headers['Proxy-Authorization'] = 'Basic ' + credentials
else:
@@ -159,3 +179,7 @@
request = HTTPRequest(headers, data, method, url)
self._outgoing_queue.append(request)
self._process_queue()
+
+ def close(self):
+ if self._transport:
+ self._transport.close()
--- papyon/gnet/protocol/HTTPS.py
+++ papyon/gnet/protocol/HTTPS.py
@@ -19,7 +19,7 @@
from papyon.gnet.constants import *
from papyon.gnet.io import SSLTCPClient
-from papyon.gnet.proxy.HTTPConnect import HTTPConnectProxy
+from papyon.gnet.proxy.factory import ProxyFactory
from papyon.gnet.parser import HTTPParser
from HTTP import HTTP
@@ -27,7 +27,7 @@
class HTTPS(HTTP):
"""HTTP protocol client class."""
- def __init__(self, host, port=443, proxy=None):
+ def __init__(self, host, port=443, proxies={}):
"""Connection initialization
@param host: the host to connect to.
@@ -36,25 +36,18 @@
@param port: the port number to connect to
@type port: integer
- @param proxy: proxy that we can use to connect
- @type proxy: L{gnet.proxy.ProxyInfos}"""
- HTTP.__init__(self, host, port)
- assert(proxy is None or proxy.type == 'https')
- self.__proxy = proxy
+ @param proxies: proxies that we can use to connect
+ @type proxies: L{gnet.proxy.ProxyInfos}"""
+ HTTP.__init__(self, host, port, proxies)
+ self._http_proxy = None
def _setup_transport(self):
if self._transport is None:
- transport = SSLTCPClient(self._host, self._port)
- if self.__proxy is not None:
- print 'Using proxy : ', repr(self.__proxy)
- self._transport = HTTPConnectProxy(transport, self.__proxy)
- else:
- self._transport = transport
- self._http_parser = HTTPParser(self._transport)
- self._http_parser.connect("received", self._on_response_received)
- self._transport.connect("notify::status", self._on_status_change)
- self._transport.connect("error", self._on_error)
- self._transport.connect("sent", self._on_request_sent)
+ self._transport = SSLTCPClient(self._host, self._port)
+ if self._proxies:
+ self._transport = ProxyFactory(self._transport, self._proxies,
+ preferred='https')
+ self._setup_parser()
if self._transport.get_property("status") != IoStatus.OPEN:
self._transport.open()
--- papyon/gnet/protocol/__init__.py
+++ papyon/gnet/protocol/__init__.py
@@ -22,14 +22,14 @@
from HTTP import *
from HTTPS import *
-def ProtocolFactory(protocol, host, port=None, proxy=None):
+def ProtocolFactory(protocol, host, port=None, proxies={}):
if protocol == "http":
klass = HTTP
elif protocol == "https":
klass = HTTPS
if port is None:
- return klass(host, proxy=proxy)
+ return klass(host, proxies=proxies)
else:
- return klass(host, port, proxy=proxy)
+ return klass(host, port, proxies=proxies)
--- papyon/gnet/proxy/HTTPConnect.py
+++ papyon/gnet/proxy/HTTPConnect.py
@@ -47,9 +47,7 @@
def _post_open(self):
AbstractProxy._post_open(self)
- host = self._client.get_property("host")
- port = self._client.get_property("port")
- proxy_protocol = 'CONNECT %s:%s HTTP/1.1\r\n' % (host, port)
+ proxy_protocol = 'CONNECT %s:%s HTTP/1.1\r\n' % (self.host, self.port)
proxy_protocol += 'Proxy-Connection: Keep-Alive\r\n'
proxy_protocol += 'Pragma: no-cache\r\n'
proxy_protocol += 'User-Agent: %s/%s\r\n' % (GNet.NAME, GNet.VERSION)
@@ -57,6 +55,8 @@
auth = base64.encodestring(self._proxy.user + ':' + self._proxy.password).strip()
proxy_protocol += 'Proxy-authorization: Basic ' + auth + '\r\n'
proxy_protocol += '\r\n'
+
+ self._http_parser.enable()
self._transport.send(proxy_protocol)
# public API
@@ -72,7 +72,9 @@
def close(self):
"""Close the connection."""
+ self._http_parser.disable()
self._client._proxy_closed()
+ self._transport.close()
def send(self, buffer, callback=None, *args):
self._client.send(buffer, callback, *args)
@@ -96,8 +98,8 @@
def _on_proxy_response(self, parser, response):
if self.status == IoStatus.OPENING:
if response.status == 200:
- del self._http_parser
- self._transport._watch_remove() # HACK: ok this is ugly !
+ self._http_parser.disable()
+ self._transport.disable()
self._client._proxy_open()
elif response.status == 100:
return True
--- papyon/gnet/proxy/SOCKS4.py
+++ papyon/gnet/proxy/SOCKS4.py
@@ -59,19 +59,18 @@
def _post_open(self):
AbstractProxy._post_open(self)
- host = self._client.get_property("host")
- port = self._client.get_property("port")
user = self._proxy.user
proxy_protocol = struct.pack('!BBH', SOCKS4Proxy.PROTOCOL_VERSION,
- SOCKS4Proxy.CONNECT_COMMAND, port)
+ SOCKS4Proxy.CONNECT_COMMAND, self.port)
- for part in host.split('.'):
+ for part in self.host.split('.'):
proxy_protocol += struct.pack('B', int(part))
proxy_protocol += user
proxy_protocol += struct.pack('B', 0)
+ self._delimiter_parser.enable()
self._transport.send(proxy_protocol)
# Public API
@@ -87,7 +86,9 @@
def close(self):
"""Close the connection."""
+ self._delimiter_parser.disable()
self._client._proxy_closed()
+ self._transport.close()
def send(self, buffer, callback=None, *args):
self._client.send(buffer, callback, *args)
@@ -113,8 +114,8 @@
assert(version == 0)
if self.status == IoStatus.OPENING:
if response_code == 90:
- del self._delimiter_parser
- self._transport._watch_remove() # HACK: ok this is ugly !
+ self._delimiter_parser.disable()
+ self._transport.disable()
self._client._proxy_open()
elif response_code == 91:
self.close()
--- papyon/gnet/proxy/SOCKS5.py
+++ papyon/gnet/proxy/SOCKS5.py
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+
+from abstract import AbstractProxy
+from papyon.gnet.io import TCPClient
+from papyon.gnet.constants import *
+from papyon.gnet.parser import DelimiterParser
+
+import gobject
+import logging
+import socket
+import struct
+
+__all__ = ['SOCKS5Proxy']
+
+logger = logging.getLogger('papyon.proxy.SOCKS5')
+
+class SOCKS5Proxy(AbstractProxy):
+
+ VERSION = 5
+ CMD_CONNECT = 0x01
+ AUTH_VERSION = 1
+ MAX_LEN = 255
+ RESERVED = 0x00
+
+ AUTH_NONE = 0x00
+ AUTH_GSSAPI = 0x01
+ AUTH_USR_PASS = 0x02
+ AUTH_NO_ACCEPT = 0xff
+
+ ATYP_IPV4 = 0x01
+ ATYP_DOMAINNAME = 0x03
+ ATYP_IPV6 = 0x04
+
+ STATE_NEGO = 0x01
+ STATE_AUTH = 0x02
+ STATE_CONN = 0x03
+ STATE_RECV_IPV4_ADDR = 0x04
+ STATE_RECV_ADDR_LEN = 0x05
+ STATE_RECV_DOMAINNAME = 0x06
+
+ CODE_SUCCEEDED = 0x00
+ CODE_SRV_FAILURE = 0x01
+ CODE_NOT_ALLOWED = 0x02
+ CODE_NET_UNREACH = 0x03
+ CODE_HOST_UNREACH = 0x04
+ CODE_REFUSED = 0x05
+ CODE_TTL_EXPIRED = 0x06
+ CODE_CMD_NOT_SUP = 0x07
+ CODE_ATYPE_NOT_SUP = 0x08
+
+ NEGO_REPLY_LEN = 2
+ AUTH_REPLY_LEN = 2
+ CONN_REPLY_LEN = 4
+ IPV4_ADDR_LEN = 4
+
+ """Proxy class used to communicate with SOCKS5 proxies."""
+ def __init__(self, client, proxy):
+ assert(proxy.type in ('socks', 'socks5')), \
+ "SOCKS5Proxy expects a socks5 proxy description"
+ assert(client.domain == AF_INET), \
+ "SOCKS5 CONNECT only handles INET address family"
+ assert(client.type == SOCK_STREAM), \
+ "SOCKS5 CONNECT only handles SOCK_STREAM"
+ assert(client.status == IoStatus.CLOSED), \
+ "SOCKS5Proxy expects a closed client"
+ AbstractProxy.__init__(self, client, proxy)
+
+ self._transport = TCPClient(self._proxy.host, self._proxy.port)
+ self._transport.connect("notify::status", self._on_transport_status)
+ self._transport.connect("error", self._on_transport_error)
+
+ self._delimiter_parser = DelimiterParser(self._transport)
+ self._delimiter_parser.connect("received", self._on_proxy_response)
+
+ self._state = None
+ self._must_auth = False
+
+ # Opening state methods
+ def _pre_open(self, io_object=None):
+ AbstractProxy._pre_open(self)
+
+ def _post_open(self):
+ AbstractProxy._post_open(self)
+ self._delimiter_parser.enable()
+ self._send_nego_msg()
+
+ # Public API
+ def open(self):
+ """Open the connection."""
+ if not self._configure():
+ return
+ self._pre_open()
+ try:
+ self._transport.open()
+ except:
+ pass
+
+ def close(self):
+ """Close the connection."""
+ self._delimiter_parser.disable()
+ self._client._proxy_closed()
+ self._transport.close()
+
+ def send(self, buffer, callback=None, *args):
+ self._client.send(buffer, callback, *args)
+
+ # Handshake
+ def _send_nego_msg(self):
+ user = self._proxy.user
+ password = self._proxy.password
+
+ methods = [self.AUTH_NONE]
+ if user or password:
+ methods.append(self.AUTH_USR_PASS)
+
+ msg = struct.pack('!BB', SOCKS5Proxy.VERSION,
+ len(methods))
+ for method in methods:
+ msg += struct.pack('B', method)
+
+ logger.info("Sending negotiation request (%i methods)" % len(methods))
+ self._state = SOCKS5Proxy.STATE_NEGO
+ self._delimiter_parser.delimiter = SOCKS5Proxy.NEGO_REPLY_LEN
+ self._transport.send(msg)
+ return True
+
+ def _parse_nego_reply(self, response):
+ version, method = struct.unpack('!BB', response[0:2])
+ if version != SOCKS5Proxy.VERSION:
+ raise Exception("Server is not SOCKS5 compatible")
+ if method == SOCKS5Proxy.AUTH_NO_ACCEPT:
+ raise Exception("Server doesn't support any of the proposed \
+ authentication methods")
+
+ logger.info("Server chose authentication method %i" % method)
+ self._must_auth = (method == SOCKS5Proxy.AUTH_USR_PASS)
+ return True
+
+ def _send_auth_msg(self):
+ user = self._proxy.user
+ password = self._proxy.password
+
+ if len(user) > self.MAX_LEN or len(password) > self.MAX_LEN:
+ raise Exception("User and password need to be less than %i \
+ characters long" % self.MAX_LEN)
+
+ msg = struct.pack('B', SOCKS5Proxy.AUTH_VERSION)
+ msg += struct.pack('B', len(user))
+ if user:
+ msg += user
+ msg += struct.pack('B', len(password))
+ if password:
+ msg += password
+
+ logger.info("Sending authentication request")
+ self._state = SOCKS5Proxy.STATE_AUTH
+ self._delimiter_parser.delimiter = SOCKS5Proxy.AUTH_REPLY_LEN
+ self._transport.send(msg)
+
+ def _check_auth_status(self, response):
+ version, code = struct.unpack('!BB', response[0:2])
+ if (version != SOCKS5Proxy.VERSION or
+ code != SOCKS5Proxy.CODE_SUCCEEDED):
+ raise Exception("Authentication didn't succeed")
+ logger.info("Authentication succeeded")
+ return True
+
+ def _send_connect_msg(self):
+ msg = struct.pack('!BBB', SOCKS5Proxy.VERSION,
+ SOCKS5Proxy.CMD_CONNECT, SOCKS5Proxy.RESERVED)
+ try:
+ addr = socket.inet_aton(self.host)
+ msg += struct.pack('!BI', SOCKS5Proxy.ATYP_IPV4, addr)
+ except:
+ if len(self.host) > SOCKS5Proxy.MAX_LEN:
+ raise Exception
+ msg += struct.pack('!BB', SOCKS5Proxy.ATYP_DOMAINNAME, len(self.host))
+ msg += self.host
+
+ msg += struct.pack('!H', self.port)
+
+ logger.info("Connection request to %s:%u" % (self.host, self.port))
+ self._state = SOCKS5Proxy.STATE_CONN
+ self._delimiter_parser.delimiter = SOCKS5Proxy.CONN_REPLY_LEN
+ self._transport.send(msg)
+
+ def _parse_connect_reply(self, response):
+ version, code, reserved, atyp = struct.unpack('!BBBB', response[0:4])
+ if version != SOCKS5Proxy.VERSION or reserved != SOCKS5Proxy.RESERVED:
+ raise Exception("Connection reply isn't SOCKS5 compatible")
+ if code == SOCKS5Proxy.CODE_SUCCEEDED:
+ logger.info("Connection request has been accepted")
+ else:
+ raise Exception("Connection request has been declined (%i)" % code)
+
+ if atyp == SOCKS5Proxy.ATYP_IPV4:
+ self._state = SOCKS5Proxy.STATE_RECV_IPV4_ADDR
+ self._delimiter_parser.delimiter = SOCKS5Proxy.IPV4_ADDR_LEN
+ elif atyp == SOCKS5Proxy.ATYP_DOMAINNAME:
+ self._state = SOCKS5Proxy.STATE_RECV_ADDR_LEN
+ self._delimiter_parser.delimiter = 1
+ else:
+ raise Exception('Unsupported address type: %i' % atyp)
+
+ def _parse_domain_name_length(self, response):
+ length, = struct.unpack('!B', response)
+ self._state = SOCKS5Proxy.STATE_RECV_DOMAINNAME
+ self._delimiter_parser.delimiter = length
+
+ # Callbacks
+ def _on_transport_status(self, transport, param):
+ if transport.status == IoStatus.OPEN:
+ self._post_open()
+ elif transport.status == IoStatus.OPENING:
+ self._client._proxy_opening(self._transport._transport)
+ self._status = transport.status
+ else:
+ self._status = transport.status
+
+ def _on_transport_error(self, transport, error_code):
+ if error_code == IoError.CONNECTION_FAILED:
+ error_code = IoError.PROXY_CONNECTION_FAILED
+ self.close()
+ self.emit("error", error_code)
+
+ def _on_proxy_response(self, parser, response):
+ try:
+ if self._state == SOCKS5Proxy.STATE_NEGO:
+ self._parse_nego_reply(response)
+ if self._must_auth:
+ self._send_auth_msg()
+ else:
+ self._send_connect_msg()
+ elif self._state == SOCKS5Proxy.STATE_AUTH:
+ self._check_auth_status(response)
+ self._send_connect_msg()
+ elif self._state == SOCKS5Proxy.STATE_CONN:
+ self._parse_connect_reply(response)
+ elif self._state == SOCKS5Proxy.STATE_RECV_ADDR_LEN:
+ self._parse_domain_name_length(response)
+ elif (self._state == SOCKS5Proxy.STATE_RECV_IPV4_ADDR or
+ self._state == SOCKS5Proxy.STATE_RECV_DOMAINNAME):
+ self._delimiter_parser.disable()
+ self._transport.disable()
+ self._client._proxy_open()
+ except Exception, err:
+ logger.error("Handshake failed")
+ logger.exception(err)
+ self.close()
+ self.emit("error", IoError.PROXY_CONNECTION_FAILED)
+ return False
+
+gobject.type_register(SOCKS5Proxy)
--- papyon/gnet/proxy/abstract.py
+++ papyon/gnet/proxy/abstract.py
@@ -31,7 +31,7 @@
self._client.connect("sent", self._on_client_sent)
self._client.connect("received", self._on_client_received)
self._client.connect("notify::status", self._on_client_status)
- AbstractClient.__init__(self, self._proxy.host, self._proxy.port)
+ AbstractClient.__init__(self, client.host, client.port)
def _on_client_status(self, client, param):
status = client.get_property("status")
--- papyon/gnet/proxy/factory.py
+++ papyon/gnet/proxy/factory.py
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+
+from HTTPConnect import *
+from SOCKS4 import *
+from SOCKS5 import *
+
+def ProxyFactory(client, proxies, preferred='direct'):
+ if not proxies or preferred not in proxies or not proxies[preferred]:
+ return client
+
+ proxy = proxies[preferred]
+ if proxy.type == 'http':
+ return HTTPConnectProxy(client, proxy)
+ elif proxy.type == 'https':
+ return HTTPConnectProxy(client, proxy)
+ elif proxy.type in ('socks', 'socks5'):
+ # FIXME we assume "socks://" is a SOCKS5 proxy
+ return SOCKS5Proxy(client, proxy)
+ elif proxy.type == 'socks4':
+ return SOCKS4Proxy(client, proxies['socks4'])
+ else:
+ return client
--- papyon/gnet/proxy/proxy_infos.py
+++ papyon/gnet/proxy/proxy_infos.py
@@ -20,7 +20,7 @@
import base64
import urlparse
-__all__ = ['ProxyInfos', 'ProxyFactory']
+__all__ = ['ProxyInfos']
class ProxyInfos(object):
"""Contain informations needed to make use of a proxy.
@@ -98,7 +98,7 @@
def __get_type(self):
return self._type
def __set_type(self, type):
- assert(type in ('http', 'https', 'socks4', 'socks5'))
+ assert(type in ('http', 'https', 'socks', 'socks4', 'socks5'))
self._type = type
type = property(__get_type, __set_type, doc="Proxy type.")
@@ -115,6 +115,3 @@
auth = '%s:%s' % (self.user, "*" * len(self.password))
host = auth + '@' + host
return self.type + '://' + host + '/'
-
-ProxyFactory = ProxyInfos.from_string
-
--- papyon/gnet/resolver.py
+++ papyon/gnet/resolver.py
@@ -72,7 +72,7 @@
addresses = ((socket.AF_INET, result[0][4][0]),)
self._emit_response(callback, (status, cname, expires, addresses))
- @async
+ #@async
def _emit_response(self, callback, response):
callback[0](HostnameResponse(response), *callback[1:])
return False
--- papyon/media/__init__.py
+++ papyon/media/__init__.py
@@ -24,5 +24,6 @@
from constants import *
from message import *
from relay import *
+from rtc import *
from session import *
from stream import *
--- papyon/media/codec.py
+++ papyon/media/codec.py
@@ -20,6 +20,8 @@
__all__ = ['MediaCodec']
+from papyon.media.constants import CODEC_DETAILS
+
class MediaCodec(object):
"""Class representing a media codec."""
@@ -30,21 +32,8 @@
self.params = params or dict()
if encoding is None or clockrate is None or clockrate == 0:
- if payload == 0:
- self.encoding = "PCMU"
- self.clockrate = 8000
- elif payload == 8:
- self.encoding = "PCMA"
- self.clockrate = 8000
- elif payload == 13:
- self.encoding = "CN"
- self.clockrate = 8000
- elif payload == 31:
- self.encoding = "H261"
- self.clockrate = 90000
- elif payload == 34:
- self.encoding = "H263"
- self.clockrate = 90000
+ if payload in CODEC_DETAILS:
+ self.encoding, self.clockrate = CODEC_DETAILS[payload]
def __eq__(self, other):
return (self.payload == other.payload and
--- papyon/media/conference.py
+++ papyon/media/conference.py
@@ -97,9 +97,10 @@
bus = self._pipeline.get_bus()
bus.add_signal_watch()
bus.connect("message", self.on_bus_message)
- if self._session.type is MediaSessionType.WEBCAM_RECV or\
- self._session.type is MediaSessionType.WEBCAM_SEND:
- name = "fsmsnconference"
+ if self._session.type is MediaSessionType.WEBCAM_RECV:
+ name = "fsmsncamrecvconference"
+ elif self._session.type is MediaSessionType.WEBCAM_SEND:
+ name = "fsmsncamsendconference"
else:
name = "fsrtpconference"
self._conference = gst.element_factory_make(name)
@@ -320,10 +321,6 @@
def make_video_sink(async=False):
"Make a bin with a video sink in it, that will be displayed on xid."
- sink = gst.element_factory_make("filesink", "filesink")
- sink.set_property("location", "/tmp/videosink.log")
- return sink
-
bin = gst.Bin("videosink")
sink = gst.element_factory_make("ximagesink", "imagesink")
sink.set_property("sync", async)
@@ -337,5 +334,5 @@
colorspace.link(sink)
bin.add_pad(gst.GhostPad("sink", videoscale.get_pad("sink")))
#sink.set_data("xid", xid) #Future work - proper gui place for imagesink ?
- return bin.get_pad("sink")
+ return bin
--- papyon/media/constants.py
+++ papyon/media/constants.py
@@ -47,6 +47,14 @@
"video" : ["x-rtvc1", "h263"]
}
+CODEC_DETAILS = {
+ 0 : ("PCMU", 8000),
+ 8 : ("PCMA", 8000),
+ 13: ("CN", 8000),
+ 31: ("H261", 90000),
+ 34: ("H264", 90000)
+}
+
EXTRA_PARAMS = {
34: {"x-modea-only": "1"}
}
--- papyon/media/rtc.py
+++ papyon/media/rtc.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from papyon.msnp.constants import *
+
+import gobject
+import logging
+
+logger = logging.getLogger('papyon.media.rtc')
+
+__all__ = ['RTCActivity', 'RTCActivityManager']
+
+class MessageTypes:
+ REQUESTOR_BYE = 1
+ SERVER_BYE = 2
+ ACCEPT = 3
+ DECLINE = 4
+ CAPABILITIES = 5
+
+class RTCActivityManager(object):
+ """Manage the different RTC activities. Forward the messages received from
+ the transport to the right activity."""
+
+ def __init__(self, client, protocol):
+ self._activities = []
+ self._transport = RTCActivityTunneledTransport(protocol)
+ self._transport.connect("message-received", self._on_message_received)
+
+ def register(self, activity):
+ self._activities.append(activity)
+
+ def unregister(self, activity):
+ self._activities.remove(activity)
+
+ def get_transport(self):
+ return self._transport
+
+ def _get_activity(self, id):
+ for activity in self._activities:
+ if activity.id == id:
+ return activity
+ logger.warning("There is no registered activity with id %s." % id)
+ return None
+
+ def _on_message_received(self, transport, peer, peer_guid, type, arguments):
+ if type == MessageTypes.ACCEPT:
+ id = arguments[1]
+ logger.info("Activity %s has been accepted elsewhere." % id)
+ activity = self._get_activity(id)
+ if activity:
+ activity.on_activity_accepted()
+ elif type == MessageTypes.DECLINE:
+ id = arguments[1]
+ logger.info("Activity %s has been declined elsewhere." % id)
+ activity = self._get_activity(id)
+ if activity:
+ activity.on_activity_declined()
+
+ #FIXME handle BYE on timeout
+
+
+class RTCActivity(object):
+ """RTC activity such as a video or audio call (not sure if there is any
+ other type of activity). Used to communicate between end points about
+ the status of an activity (accepted, declined..) in a MPOP context."""
+
+ def __init__(self, client):
+ logger.info("New RTC activity.")
+ self._client = client
+ self._manager = client.rtc_activity_manager
+ self._manager.register(self)
+ self._transport = self._manager.get_transport()
+
+ @property
+ def id(self):
+ raise NotImplementedError
+
+ @property
+ def peer(self):
+ raise NotImplementedError
+
+ def on_activity_accepted(self):
+ raise NotImplementedError
+
+ def on_activity_declined(self):
+ raise NotImplementedError
+
+ def _accept_activity(self):
+ logger.info("Activity %s has been accepted." % self.id)
+ if self._transport is None:
+ return
+ self._transport.send(self._client.profile.account, None,
+ MessageTypes.ACCEPT, ('1', self.id, self.peer.account, '3'))
+
+ def _decline_activity(self):
+ logger.info("Activity %s has been declined." % self.id)
+ if self._transport is None:
+ return
+ self._transport.send(self._client.profile.account, None,
+ MessageTypes.DECLINE, ('1', self.id, self.peer.account, '3'))
+
+ def _dispose_activity(self):
+ self._manager.unregister(self)
+
+
+class RTCActivityTunneledTransport(gobject.GObject):
+ """Default (and only?) RTC activity transport. The messages are sent to
+ the notification server using UBN commands."""
+
+ __gsignals__ = {
+ "message-received": (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object, object, object, object))
+ }
+
+ def __init__(self, protocol):
+ gobject.GObject.__init__(self)
+ self._protocol = protocol
+ self._protocol.connect("buddy-notification-received",
+ self._on_notification_received)
+
+ def send(self, peer, peer_guid, type, params):
+ message = str(type) + ' ' + ' '.join(params)
+ self._protocol.send_user_notification(message, peer, peer_guid,
+ UserNotificationTypes.RTC_ACTIVITY)
+
+ def _on_notification_received(self, protocol, peer, peer_guid, type, message):
+ if type is not UserNotificationTypes.RTC_ACTIVITY:
+ return
+ logger.debug("<<< " + message)
+ params = message.split(' ')
+ self.emit("message-received", peer, peer_guid, int(params[0]), params[1:])
--- papyon/media/session.py
+++ papyon/media/session.py
@@ -207,6 +207,8 @@
if self.ready:
self.emit("ready")
+ return True
+
def on_stream_prepared(self, stream):
if self.prepared:
logger.debug("All media streams are prepared")
--- papyon/media/stream.py
+++ papyon/media/stream.py
@@ -248,6 +248,7 @@
if self._local_candidates_prepared:
return
+ logger.debug("Media stream %s is prepared" % self.name)
self._local_candidates_prepared = True
if self.prepared:
self.emit("prepared")
--- papyon/msnp/base.py
+++ papyon/msnp/base.py
@@ -35,6 +35,7 @@
SYNCHRONIZING = 4
SYNCHRONIZED = 5
OPEN = 6
+ CLOSING = 7
class BaseProtocol(object):
--- papyon/msnp/constants.py
+++ papyon/msnp/constants.py
@@ -33,8 +33,10 @@
XBOX = 4
class UserNotificationTypes(object):
+ XML_DATA = 1
SIP_INVITE = 2
P2P_DATA = 3
+ CLOSED_CONVERSATION = 5
RESYNCHRONIZE = 6
- SIP_PROGESS = 11
+ RTC_ACTIVITY = 11
TUNNELED_SIP = 12
--- papyon/msnp/message.py
+++ papyon/msnp/message.py
@@ -70,26 +70,12 @@
message = ''
for header_name, header_value in self.headers.iteritems():
message += '\t%s: %s\\r\\n\n' % (header_name, repr(header_value))
- message += '\t\\r\\n\n'
if self.headers['Content-Type'] != "application/x-msnmsgrp2p":
+ message += '\t\\r\\n\n'
message += '\t' + debug.escape_string(self.body).\
replace("\r\n", "\\r\\n\n\t")
else:
- tlp_header = self.body[:48]
- tlp_footer = self.body[-4:]
- tlp_session_id = struct.unpack("<L", self.body[0:4])[0]
- tlp_flags = struct.unpack("<L", self.body[28:32])[0]
- body = self.body[48:-4]
-
- message += "\t" + debug.hexify_string(tlp_header).replace("\r\n", "\n\t")
-
- if tlp_flags == 0 or tlp_session_id == 0:
- message += "\n\t" + debug.escape_string(body).\
- replace("\r\n", "\\r\\n\n\t")
- elif len(body) > 0:
- message += "\n\t" + "[%d bytes of data]" % len(body)
- message += "\n\t" + debug.hexify_string(tlp_footer)
-
+ message += "\t[P2P message (%d bytes)]" % (len(self.body) - 4)
return message.rstrip("\n\t")
def __get_content_type(self):
--- papyon/msnp/notification.py
+++ papyon/msnp/notification.py
@@ -32,6 +32,7 @@
from papyon.gnet.message.HTTP import HTTPMessage
from papyon.util.queue import PriorityQueue, LastElementQueue
from papyon.util.decorator import throttled
+from papyon.util.encoding import decode_rfc2047_string
import papyon.util.element_tree as ElementTree
import papyon.profile as profile
import papyon.service.SingleSignOn as SSO
@@ -74,7 +75,7 @@
"buddy-notification-received" : (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE,
- (object, object,)),
+ (object, object, object, object)),
"mail-received" : (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE,
@@ -135,7 +136,7 @@
raise AttributeError, "unknown property %s" % pspec.name
# Public API -------------------------------------------------------------
- @throttled(2000, LastElementQueue())
+ @throttled(2, LastElementQueue())
def set_presence(self, presence, client_id=0, msn_object=None):
"""Publish the new user presence.
@@ -152,7 +153,7 @@
self._send_command('CHG',
(presence, str(client_id), urllib.quote(str(msn_object))))
- @throttled(2000, LastElementQueue())
+ @throttled(2, LastElementQueue())
def set_display_name(self, display_name):
"""Sets the new display name
@@ -161,7 +162,7 @@
self._send_command('PRP',
('MFN', urllib.quote(display_name)))
- @throttled(2000, LastElementQueue())
+ @throttled(2, LastElementQueue())
def set_personal_message(self, personal_message='', current_media=None,
signature_sound=None):
"""Sets the new personal message
@@ -191,7 +192,7 @@
self._client.profile._server_property_changed("current-media",
current_media)
- @throttled(2000, LastElementQueue())
+ @throttled(2, LastElementQueue())
def set_end_point_name(self, name="Papyon", idle=False):
ep = '<EndpointData>'\
'<Capabilities>%s</Capabilities>'\
@@ -208,12 +209,16 @@
self._send_command('UUX', payload=ep)
self._send_command('UUX', payload=pep)
+ @throttled(2, LastElementQueue())
+ def set_privacy(self, privacy=profile.Privacy.BLOCK):
+ self._send_command("BLP", (privacy,))
+
def signoff(self):
"""Logout from the server"""
self._send_command('OUT')
self._transport.lose_connection()
- @throttled(7600, list())
+ @throttled(7, list())
def request_switchboard(self, priority, callback, *callback_args):
self.__switchboard_callbacks.add((callback, callback_args), priority)
self._send_command('XFR', ('SB',))
@@ -270,8 +275,13 @@
(domain, user, membership, network_id)
self._send_command("RML", payload=payload)
- def send_user_notification(self, message, contact, type):
- self._send_command("UUN", (contact.account, type), message)
+ def send_user_notification(self, message, contact, contact_guid, type,
+ callback=None, *cb_args):
+ account = contact
+ if contact_guid:
+ account += ';{' + contact_guid + '}'
+ arguments = (account, type)
+ self._send_command("UUN", arguments, message, True, callback, *cb_args)
def send_unmanaged_message(self, contact, message):
content_type = message.content_type[0]
@@ -289,7 +299,22 @@
tr_id = self._send_command('URL', url_command_args)
self._url_callbacks[tr_id] = callback
- def _parse_account(self, command, idx=0):
+ # Helpers ----------------------------------------------------------------
+ def __search_account(self, account, network_id=profile.NetworkID.MSN):
+ if account == self._client.profile.account:
+ return [self._client.profile]
+
+ contacts = self._client.address_book.contacts.\
+ search_by_network_id(network_id).\
+ search_by_account(account)
+
+ if len(contacts) == 0:
+ logger.warning("Contact (network_id=%d) %s not found" % \
+ (network_id, account))
+
+ return contacts
+
+ def __parse_network_and_account(self, command, idx=0):
if self._protocol_version >= 18:
temp = command.arguments[idx].split(":")
network_id = int(temp[0])
@@ -301,6 +326,20 @@
idx += 1
return idx, network_id, account
+ def __parse_account_and_guid(self, command, idx=0):
+ account = command.arguments[idx]
+ guid = None
+ if ';' in account:
+ account, guid = account.split(';', 1)
+ guid = guid [1:-1]
+ return idx + 1, account, guid
+
+ def __find_node(self, parent, name, default):
+ node = parent.find(name)
+ if node is not None and node.text is not None:
+ return node.text.encode("utf-8")
+ else:
+ return default
# Handlers ---------------------------------------------------------------
# --------- Connection ---------------------------------------------------
@@ -410,35 +449,19 @@
else:
self._client.profile._server_property_changed("msn_object", None)
- def _handle_ILN(self,command):
+ def _handle_ILN(self, command):
self._handle_NLN(command)
- def _handle_FLN(self,command):
- idx, network_id, account = self._parse_account(command)
-
- contacts = self._client.address_book.contacts.\
- search_by_network_id(network_id).\
- search_by_account(account)
-
- if len(contacts) == 0:
- logger.warning("Contact (network_id=%d) %s not found" % \
- (network_id, account))
-
+ def _handle_FLN(self, command):
+ idx, network_id, account = self.__parse_network_and_account(command)
+ contacts = self.__search_account(account, network_id)
for contact in contacts:
+ contact._remove_flag(profile.ContactFlag.EXTENDED_PRESENCE_KNOWN)
contact._server_property_changed("presence",
profile.Presence.OFFLINE)
- def _handle_NLN(self,command):
- idx, network_id, account = self._parse_account(command, 1)
-
- contacts = self._client.address_book.contacts.\
- search_by_network_id(network_id).\
- search_by_account(account)
-
- if len(contacts) == 0:
- logger.warning("Contact (network_id=%d) %s not found" % \
- (network_id, account))
-
+ def _handle_NLN(self, command):
+ idx, network_id, account = self.__parse_network_and_account(command, 1)
presence = command.arguments[0]
display_name = urllib.unquote(command.arguments[idx])
idx += 1
@@ -447,19 +470,27 @@
msn_object = None
icon_url = None
+
if len(command.arguments) > idx:
if command.arguments[idx] not in ('0', '1'):
msn_object = papyon.p2p.MSNObject.parse(self._client,
urllib.unquote(command.arguments[idx]))
+
idx += 1
if len(command.arguments) > idx:
icon_url = command.arguments[idx]
+ contacts = self.__search_account(account, network_id)
for contact in contacts:
- contact._server_property_changed("presence", presence)
+ # don't change local presence and capabilities
+ if contact is not self._client.profile:
+ contact._server_property_changed("presence", presence)
+ contact._server_property_changed("client-capabilities", capabilities)
contact._server_property_changed("display-name", display_name)
- contact._server_property_changed("client-capabilities", capabilities)
- contact._server_property_changed("msn-object", msn_object)
+ # only change MSNObject if the extended presence is known (MSNP18)
+ if self._protocol_version < 18 or \
+ contact.has_flag(profile.ContactFlag.EXTENDED_PRESENCE_KNOWN):
+ contact._server_property_changed("msn_object", msn_object)
if icon_url is not None:
contact._server_attribute_changed('icon_url', icon_url)
@@ -475,17 +506,20 @@
def _handle_UUX(self, command):
pass
- def _handle_UBN(self,command): # contact infos
+ def _handle_UBN(self, command): # contact infos
if not command.payload:
return
+ idx, account, guid = self.__parse_account_and_guid(command)
+ contact = self.__search_account(account)[0]
type = int(command.arguments[1])
- self.emit("buddy-notification-received", type, command)
+ payload = command.payload
+ self.emit("buddy-notification-received", contact, guid, type, payload)
- def _handle_UBX(self,command): # contact infos
+ def _handle_UBX(self, command): # contact infos
if not command.payload:
return
-
- idx, network_id, account = self._parse_account(command)
+ print command.payload
+ idx, network_id, account = self.__parse_network_and_account(command)
try:
tree = ElementTree.fromstring(command.payload)
@@ -493,42 +527,47 @@
logger.error("Invalid XML data in received UBX command")
return
- cm = tree.find("./CurrentMedia")
- if cm is not None and cm.text is not None:
- parts = cm.text.split('\\0')
- if parts[1] == 'Music' and parts[2] == '1':
- cm = (parts[4].encode("utf-8"), parts[5].encode("utf-8"))
- elif parts[2] == '0':
- cm = None
- else:
- cm = None
-
- pm = tree.find("./PSM")
- if pm is not None and pm.text is not None:
- pm = pm.text.encode("utf-8")
- else:
- pm = ""
-
- ss = tree.find("./SignatureSound")
- if ss is not None and ss.text is not None:
- ss = ss.text.encode("utf-8")
- else:
- ss = None
+ utl = self.__find_node(tree, "./UserTileLocation", "")
+ cm_parts = self.__find_node(tree, "./CurrentMedia", "").split('\\0')
+ pm = self.__find_node(tree, "./PSM", "")
+ rmu = self.__find_node(tree, "./RMU", "")
+ ss = self.__find_node(tree, "./SignatureSound", None)
+ mg = self.__find_node(tree, "./MachineGuid", "{}").lower()[1:-1]
- contacts = self._client.address_book.contacts.\
- search_by_network_id(network_id).\
- search_by_account(account)
+ msn_object = None
+ if utl != "":
+ msn_object = papyon.p2p.MSNObject.parse(self._client, utl)
- if len(contacts) == 0:
- logger.warning("Contact (network_id=%d) %s not found" % \
- (network_id, account))
+ cm = None
+ if len(cm_parts) >= 6 and cm_parts[1] == 'Music' and cm_parts[2] == '1':
+ cm = (cm_parts[4], cm_parts[5])
+
+ eps = tree.findall("./EndpointData")
+ end_points = {}
+ for ep in eps:
+ guid = ep.get("id").encode("utf-8")[1:-1]
+ caps = self.__find_node(ep, "Capabilities", "0:0")
+ end_points[guid] = profile.EndPoint(guid, caps)
+ peps = tree.findall("./PrivateEndpointData")
+ for pep in peps:
+ guid = pep.get("id").encode("utf-8")[1:-1]
+ if guid not in end_points: continue
+ end_point = end_points[guid]
+ end_point.name = self.__find_node(pep, "EpName", "")
+ end_point.idle = bool(self.__find_node(pep, "Idle", "").lower() == "true")
+ end_point.client_type = int(self.__find_node(pep, "ClientType", 0))
+ end_point.state = self.__find_node(pep, "State", "")
+ contacts = self.__search_account(account, network_id)
for contact in contacts:
+ contact._add_flag(profile.ContactFlag.EXTENDED_PRESENCE_KNOWN)
+ contact._server_property_changed("end-points", end_points)
+ contact._server_property_changed("msn-object", msn_object)
contact._server_property_changed("current-media", cm)
contact._server_property_changed("personal-message", pm)
contact._server_property_changed("signature-sound", ss)
- def _handle_UUN(self,command): # UBN acknowledgment
+ def _handle_UUN(self, command): # UBN acknowledgment
pass
# --------- Contact List -------------------------------------------------
@@ -559,8 +598,7 @@
profile[name] = value.strip()
self._client.profile._server_property_changed("profile", profile)
- self._send_command("BLP",
- (self._client.profile.privacy,))
+ self.set_privacy(self._client.profile.privacy)
self._state = ProtocolState.SYNCHRONIZING
self._client.address_book.sync()
elif content_type[0] in \
@@ -590,9 +628,9 @@
#New mail
m = HTTPMessage()
m.parse(message.body)
- name = m.get_header('From')
+ name = decode_rfc2047_string(m.get_header('From'))
address = m.get_header('From-Addr')
- subject = m.get_header('Subject')
+ subject = decode_rfc2047_string(m.get_header('Subject'))
message_url = m.get_header('Message-URL')
post_url = m.get_header('Post-URL')
post_id = m.get_header('id')
@@ -616,16 +654,9 @@
self._client.mailbox._unread_mail_increased(delta)
def _handle_UBM(self, command):
- idx, network_id, account = self._parse_account(command)
-
- contacts = self._client.address_book.contacts.\
- search_by_network_id(network_id).\
- search_by_account(account)
-
- if len(contacts) == 0:
- logger.warning("Contact (network_id=%d) %s not found" % \
- (network_id, account))
- else:
+ idx, network_id, account = self.__parse_network_and_account(command)
+ contacts = self.__search_account(account, network_id)
+ if len(contacts) > 0:
contact = contacts[0]
message = Message(contact, command.payload)
self.emit("unmanaged-message-received", contact, message)
@@ -671,7 +702,7 @@
callback(post_url, form_dict)
# --------- Invitation ---------------------------------------------------
- def _handle_RNG(self,command):
+ def _handle_RNG(self, command):
session_id = command.arguments[0]
host, port = command.arguments[1].split(':',1)
port = int(port)
@@ -684,17 +715,21 @@
self.emit("switchboard-invitation-received", session, inviter)
# --------- Challenge ----------------------------------------------------
- def _handle_QNG(self,command):
+ def _handle_QNG(self, command):
pass
- def _handle_QRY(self,command):
+ def _handle_QRY(self, command):
pass
- def _handle_CHL(self,command):
+ def _handle_CHL(self, command):
response = _msn_challenge(command.arguments[0])
self._send_command('QRY',
(ProtocolConstant.PRODUCT_ID,), payload=response)
+ # --------- Notification -------------------------------------------------
+ def _handle_NOT(self, command):
+ pass
+
# callbacks --------------------------------------------------------------
def _connect_cb(self, transport):
self.__switchboard_callbacks = PriorityQueue()
@@ -733,9 +768,16 @@
address_book.profile.display_name)
contacts = address_book.contacts.group_by_domain()
+ mask = ~(profile.Membership.REVERSE | profile.Membership.PENDING)
+
+ for contact in address_book.contacts:
+ if (contact.memberships & mask & ~profile.Membership.FORWARD) == \
+ (profile.Membership.ALLOW | profile.Membership.BLOCK):
+ logger.warning("Contact is on both Allow and Block list; " \
+ "removing from Allow list (%s)" % contact.account)
+ contact._remove_membership(profile.Membership.ALLOW)
payloads = ['<ml l="1">']
- mask = ~(profile.Membership.REVERSE | profile.Membership.PENDING)
for domain, contacts in contacts.iteritems():
payloads[-1] += '<d n="%s">' % domain
for contact in contacts:
--- papyon/msnp/switchboard.py
+++ papyon/msnp/switchboard.py
@@ -108,6 +108,7 @@
BaseProtocol.__init__(self, client, transport, proxies)
gobject.GObject.__init__(self)
self.participants = {}
+ self.end_points = {}
self.__session_id = session_id
self.__key = key
self.__state = ProtocolState.CLOSED
@@ -115,7 +116,14 @@
self.__invitations = {}
+ logger.info("New switchboard session %s" % session_id)
+ client.profile.connect("end-point-added", self._on_end_point_added)
+
# Properties ------------------------------------------------------------
+ @property
+ def session_id(self):
+ return self.__session_id
+
def __get_state(self):
return self.__state
def __set_state(self, state):
@@ -155,18 +163,25 @@
self._inviting = True
self._send_command('CAL', (contact.account,) )
- def send_message(self, message, ack, callback=None, cb_args=()):
+ def send_message(self, message, ack, callback=None):
"""Send a message to all contacts in this switchboard
@param message: the message to send
@type message: L{message.Message}"""
assert(self.state == ProtocolState.OPEN)
+
+ cb = None
+ cb_args = ()
+ if callback:
+ cb = callback[0]
+ cb_args = callback[1:]
+
return self._send_command('MSG',
(ack,),
message,
True,
self.__on_message_sent,
- message, callback, cb_args)
+ message, cb, cb_args)
def __on_message_sent(self, message, user_callback, user_cb_args):
self.emit("message-sent", message)
@@ -175,8 +190,12 @@
def leave(self):
"""Leave the conversation"""
- assert(self.state == ProtocolState.OPEN)
+ if self.state != ProtocolState.OPEN:
+ return
+ logger.info("Leaving switchboard %s" % self.__session_id)
self._send_command('OUT')
+ self._state = ProtocolState.CLOSING
+
# Handlers ---------------------------------------------------------------
# --------- Authentication -----------------------------------------------
def _handle_ANS(self, command):
@@ -192,70 +211,96 @@
self._state = ProtocolState.SYNCHRONIZING
self._state = ProtocolState.SYNCHRONIZED
self._state = ProtocolState.OPEN
+ if self._client.protocol_version >= 16:
+ self.invite_user(self._client.profile)
def _handle_OUT(self, command):
pass
# --------- Invitation ---------------------------------------------------
- def __participant_join(self, account, display_name, client_id):
- if self._client.protocol_version >= 16:
- if account.split(";")[0] == self._client.profile.account:
- return # ignore our own user
+ def __parse_account(self, account):
+ """Parse account string and extract end-point info if available."""
+ if ';' in account:
+ account, guid = account.split(';', 1)
+ return account, guid[1:-1]
+ else:
+ return account, None
+
+ def __search_account(self, account, display_name):
+ """Search account in address book and make sure it's not ourself."""
+ if account == self._client.profile.account:
+ return self._client.profile
+
contacts = self._client.address_book.contacts.\
search_by_account(account)
if len(contacts) == 0:
- contact = papyon.profile.Contact(id=0,
+ return papyon.profile.Contact(id=0,
network_id=papyon.profile.NetworkID.MSN,
account=account,
display_name=display_name)
else:
- contact = contacts[0]
- if contact in self.participants:
+ return contacts[0]
+
+ def __discard_invitation(self, account):
+ for trid, contact in self.__invitations.items():
+ if contact.account == account:
+ del self.__invitations[trid]
+ return
+
+ def __participant_join(self, account, guid, display_name, client_id):
+ if guid is not None:
+ places = self.end_points.setdefault(account, [])
+ places.append(guid)
+ return # wait for the command without GUID
+ if account == self._client.profile.account:
+ return # ignore our own user
+ if account in self.participants:
return # ignore duplicate users
+ contact = self.__search_account(account, display_name)
contact._server_property_changed("client-capabilities", client_id)
self.participants[account] = contact
self.emit("user-joined", contact)
+ def __participant_left(self, account, guid):
+ if guid is not None:
+ places = self.end_points.setdefault(account, [])
+ places.remove(guid)
+ return # wait for the command without GUID to remove from participants
+ if account == self._client.profile.account:
+ return # ignore our own user
+ self.emit("user-left", self.participants[account])
+ del self.participants[account]
+
def _handle_IRO(self, command):
- account = command.arguments[2]
+ account, guid = self.__parse_account(command.arguments[2])
display_name = urllib.unquote(command.arguments[3])
client_id = command.arguments[4]
- self.__participant_join(account, display_name, client_id)
+ self.__participant_join(account, guid, display_name, client_id)
+
+ def _handle_CAL(self, command):
+ pass
def _handle_JOI(self, command):
- account = command.arguments[0]
+ account, guid = self.__parse_account(command.arguments[0])
display_name = urllib.unquote(command.arguments[1])
client_id = command.arguments[2]
- self.__participant_join(account, display_name, client_id)
- if len(self.__invitations) == 0:
- self._inviting = False
-
- def _handle_CAL(self, command):
- # this should be followed by a JOI, so we only change
- # the self._inviting state until we get the actual JOI
- del self.__invitations[command.transaction_id]
+ self.__participant_join(account, guid, display_name, client_id)
+ if guid is None:
+ self.__discard_invitation(account)
+ if len(self.__invitations) == 0:
+ self._inviting = False
def _handle_BYE(self, command):
- if len(command.arguments) == 1:
- account = command.arguments[0]
- self.emit("user-left", self.participants[account])
- del self.participants[account]
- else:
- self._state = ProtocolState.CLOSED
- self.participants = {}
+ account, guid = self.__parse_account(command.arguments[0])
+ self.__participant_left(account, guid)
+ end_points = self.end_points.get(self._client.profile.account, [])
+ if len(self.participants) == 0 and len(end_points) == 0:
+ self.leave()
# --------- Messenging ---------------------------------------------------
def _handle_MSG(self, command):
account = command.arguments[0]
display_name = urllib.unquote(command.arguments[1])
- contacts = self._client.address_book.contacts.\
- search_by_account(account)
- if len(contacts) == 0:
- contact = papyon.profile.Contact(id=0,
- network_id=papyon.profile.NetworkID.MSN,
- account=account,
- display_name=display_name)
- else:
- contact = contacts[0]
+ contact = self.__search_account(account, display_name)
message = Message(contact, command.payload)
self.emit("message-received", message)
@@ -297,6 +342,11 @@
self._state = ProtocolState.AUTHENTICATING
def _disconnect_cb(self, transport, reason):
- logger.info("Disconnected")
+ logger.info("Disconnected (%s)" % self.__session_id)
self._state = ProtocolState.CLOSED
+ def _on_end_point_added(self, profile, end_point):
+ if self.state != ProtocolState.OPEN:
+ return
+ logger.info("New end point connected, re-invite local user")
+ self.invite_user(profile)
--- papyon/msnp2p/SLP.py
+++ papyon/msnp2p/SLP.py
@@ -21,7 +21,7 @@
from papyon.gnet.message.HTTP import HTTPMessage
from papyon.msnp2p.exceptions import ParseError
-from papyon.msnp2p.constants import SLPContentType
+from papyon.msnp2p.constants import SLPContentType, SLPStatus
import base64
import uuid
@@ -35,7 +35,8 @@
class SLPMessage(HTTPMessage):
STD_HEADERS = ["To", "From", "Via", "CSeq", "Call-ID", "Max-Forwards"]
- def __init__(self, to="", frm="", branch="", cseq=0, call_id="", max_forwards=0):
+ def __init__(self, to="", frm="", branch="", cseq=0, call_id="",
+ max_forwards=0, on_behalf=""):
HTTPMessage.__init__(self)
self.add_header("To", "<msnmsgr:%s>" % to)
self.add_header("From", "<msnmsgr:%s>" % frm)
@@ -45,6 +46,8 @@
if call_id:
self.add_header("Call-ID", call_id)
self.add_header("Max-Forwards", str(max_forwards))
+ if on_behalf:
+ self.add_header("On-Behalf", on_behalf)
# Make the body a SLP Message wih "null" content type
self.body = SLPNullBody()
@@ -374,13 +377,18 @@
SLPMessageBody.register_content(SLPContentType.TRANSFER_RESPONSE, SLPTransferResponseBody)
class SLPSessionCloseBody(SLPMessageBody):
- def __init__(self, context=None, session_id=None, s_channel_state=None,
- capabilities_flags=None):
+ def __init__(self, context=None, session_id=None, reason=None,
+ s_channel_state=None, capabilities_flags=None):
SLPMessageBody.__init__(self, SLPContentType.SESSION_CLOSE,
session_id, s_channel_state, capabilities_flags)
if context is not None:
self.add_header("Context", base64.b64encode(context));
+ if reason is not None:
+ if reason[0] == SLPStatus.ACCEPTED:
+ self.add_header("AcceptedBy", "{%s}" % reason[1].upper())
+ elif reason[0] == SLPStatus.DECLINED:
+ self.add_header("DeclinedBy", "{%s}" % reason[1].upper())
@property
def context(self):
--- papyon/msnp2p/constants.py
+++ papyon/msnp2p/constants.py
@@ -55,3 +55,14 @@
INVITE = 'INVITE'
BYE = 'BYE'
ACK = 'ACK'
+
+class SLPStatus(object):
+ ACCEPTED = 200
+ ERROR = 500
+ DECLINED = 603
+
+class PeerInfo(object):
+ PROTOCOL_VERSION = 512
+ IMPLEMENTATION_ID = 0
+ VERSION = 3584
+ CAPABILITIES = 271
--- papyon/msnp2p/filetransfer.py
+++ papyon/msnp2p/filetransfer.py
@@ -18,18 +18,25 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-from papyon.msnp2p.constants import EufGuid
+from papyon.msnp2p.constants import ApplicationID, EufGuid
from papyon.msnp2p.session import P2PSession
+import gobject
import struct
__all__ = ['FileTransferSession']
class FileTransferSession(P2PSession):
- def __init__(self, session_manager, peer, application_id, message=None):
- P2PSession.__init__(self, session_manager, peer,
- EufGuid.FILE_TRANSFER, application_id, message)
+ __gsignals__ = {
+ "canceled" : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ())
+ }
+
+ def __init__(self, session_manager, peer, guid, message=None):
+ P2PSession.__init__(self, session_manager, peer, guid,
+ EufGuid.FILE_TRANSFER, ApplicationID.FILE_TRANSFER, message)
self._filename = ""
self._size = 0
self._has_preview = False
@@ -38,7 +45,7 @@
self._data = None
if message is not None:
- self.parse_context(message.body.context)
+ self._parse_context(message.body.context)
@property
def filename(self):
@@ -59,7 +66,7 @@
def invite(self, filename, size):
self._filename = filename
self._size = size
- context = self.build_context()
+ context = self._build_context()
self._invite(context)
def accept(self):
@@ -73,18 +80,23 @@
def send(self, data):
self._data = data
- self._send_p2p_data("\x00" * 4)
- self._send_p2p_data(self._data, True)
+ self._send_data("\x00" * 4)
+ self._send_data(self._data)
- def parse_context(self, context):
+ def _parse_context(self, context):
info = struct.unpack("<5I", context[0:20])
self._size = info[2]
self._has_preview = not bool(info[4])
self._filename = unicode(context[20:570], "utf-16-le").rstrip("\x00")
- def build_context(self):
- filename = self._filename.decode('ascii').encode('utf-16_le')
+ def _build_context(self):
+ filename = self._filename.encode('utf-16_le')
context = struct.pack("<5I", 574, 2, self._size, 0, int(self._has_preview))
context += struct.pack("550s", filename)
context += "\xFF" * 4
return context
+
+ def _on_bye_received(self, message):
+ if not self.completed:
+ self.emit("canceled")
+ self._dispose()
--- papyon/msnp2p/msnobject.py
+++ papyon/msnp2p/msnobject.py
@@ -33,25 +33,36 @@
__all__ = ['MSNObjectSession']
class MSNObjectSession(P2PSession):
- def __init__(self, session_manager, peer, application_id, message=None):
- P2PSession.__init__(self, session_manager, peer,
+ def __init__(self, session_manager, peer, peer_guid, application_id,
+ message=None, context=None):
+ P2PSession.__init__(self, session_manager, peer, peer_guid,
EufGuid.MSN_OBJECT, application_id, message)
+ self._context = None
if message is not None:
self._application_id = message.body.application_id
try:
self._context = message.body.context.strip('\x00')
except AttributeError:
raise SLPError("Incoming INVITE without context")
+ if context is not None:
+ self._context = context
+
+ @property
+ def context(self):
+ return self._context
def accept(self, data_file):
self._respond(200)
- self._send_p2p_data("\x00" * 4)
- self._send_p2p_data(data_file)
+ self._send_data("\x00" * 4)
+ self._send_data(data_file)
def reject(self):
self._respond(603)
- def invite(self, context):
- self._invite(context)
+ def invite(self):
+ self._invite(self._context)
return False
+
+ def cancel(self):
+ self._close()
--- papyon/msnp2p/session.py
+++ papyon/msnp2p/session.py
@@ -30,6 +30,7 @@
import logging
import random
import uuid
+import os
__all__ = ['P2PSession']
@@ -45,22 +46,37 @@
"accepted" : (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE,
()),
+ "rejected" : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ()),
"completed" : (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE,
(object,)),
"progressed" : (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE,
- (object,))
+ (object,)),
+ "disposed" : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ())
}
- def __init__(self, session_manager, peer, euf_guid="", application_id=0,
- message=None):
+ def __init__(self, session_manager, peer, peer_guid=None, euf_guid="",
+ application_id=0, message=None):
gobject.GObject.__init__(self)
self._session_manager = session_manager
+ self._transport_manager = session_manager._transport_manager
+ self._client = session_manager._client
self._peer = peer
+ self._peer_guid = peer_guid
self._euf_guid = euf_guid
self._application_id = application_id
+ self._completed = False
+
+ self._version = 1
+ if self._client.profile.client_id.supports_p2pv2 and \
+ peer.client_capabilities.supports_p2pv2:
+ self._version = 2
if message is not None:
self._id = message.body.session_id
@@ -95,6 +111,10 @@
return self._incoming
@property
+ def completed(self):
+ return self._completed
+
+ @property
def call_id(self):
return self._call_id
@@ -102,59 +122,78 @@
def peer(self):
return self._peer
- def set_receive_data_buffer(self, buffer, total_size):
- blob = MessageBlob(self._application_id, buffer, total_size, self.id)
- self._session_manager._transport_manager.register_writable_blob(blob)
+ @property
+ def peer_guid(self):
+ return self._peer_guid
+
+ @property
+ def local_id(self):
+ if self._version >= 2:
+ return "%s;{%s}" % (self._client.profile.account,
+ self._client.machine_guid)
+ return self._client.profile.account
+
+ @property
+ def remote_id(self):
+ if self._version >= 2:
+ return "%s;{%s}" % (self._peer.account, self._peer_guid)
+ return self._peer.account
+
+ def set_receive_data_buffer(self, buffer, size):
+ self._transport_manager.register_data_buffer(self.id, buffer, size)
def _invite(self, context):
body = SLPSessionRequestBody(self._euf_guid, self._application_id,
context, self._id)
message = SLPRequestMessage(SLPRequestMethod.INVITE,
- "MSNMSGR:" + self._peer.account,
- to=self._peer.account,
- frm=self._session_manager._client.profile.account,
+ "MSNMSGR:" + self.remote_id,
+ to=self.remote_id,
+ frm=self.local_id,
branch=self._branch,
cseq=self._cseq,
call_id=self._call_id)
message.body = body
- self._send_p2p_data(message)
+ self._send_slp_message(message)
def _transreq(self):
self._cseq = 0
body = SLPTransferRequestBody(self._id, 0, 1)
message = SLPRequestMessage(SLPRequestMethod.INVITE,
- "MSNMSGR:" + self._peer.account,
- to=self._peer.account,
- frm=self._session_manager._client.profile.account,
+ "MSNMSGR:" + self.remote_id,
+ to=self.remote_id,
+ frm=self.local_id,
branch=self._branch,
cseq=self._cseq,
call_id=self._call_id)
message.body = body
- self._send_p2p_data(message)
+ self._send_slp_message(message)
def _respond(self, status_code):
body = SLPSessionRequestBody(session_id=self._id, capabilities_flags=None,
s_channel_state=None)
self._cseq += 1
response = SLPResponseMessage(status_code,
- to=self._peer.account,
- frm=self._session_manager._client.profile.account,
+ to=self.remote_id,
+ frm=self.local_id,
cseq=self._cseq,
branch=self._branch,
call_id=self._call_id)
response.body = body
- self._send_p2p_data(response)
+ self._send_slp_message(response)
+
+ # close other end points so we are the only one answering
+ self._close_end_points(status_code)
def _respond_transreq(self, transreq, status, body):
self._cseq += 1
response = SLPResponseMessage(status,
- to=self._peer.account,
- frm=self._session_manager._client.profile.account,
+ to=self.remote_id,
+ frm=self.local_id,
cseq=self._cseq,
branch=transreq.branch,
call_id=self._call_id)
response.body = body
- self._send_p2p_data(response)
+ self._send_slp_message(response)
def _accept_transreq(self, transreq, bridge, listening, nonce, local_ip,
local_port, extern_ip, extern_port):
@@ -165,39 +204,66 @@
def _decline_transreq(self, transreq):
body = SLPTransferResponseBody(session_id=self._id)
self._respond_transreq(transreq, 603, body)
+ self._dispose()
- def _close(self, context=None):
+ def _close(self, context=None, reason=None):
body = SLPSessionCloseBody(context=context, session_id=self._id,
- s_channel_state=0)
+ reason=reason, s_channel_state=0)
self._cseq = 0
self._branch = "{%s}" % uuid.uuid4()
message = SLPRequestMessage(SLPRequestMethod.BYE,
- "MSNMSGR:" + self._peer.account,
- to=self._peer.account,
- frm=self._session_manager._client.profile.account,
+ "MSNMSGR:" + self.remote_id,
+ to=self.remote_id,
+ frm=self.local_id,
branch=self._branch,
cseq=self._cseq,
call_id=self._call_id)
message.body = body
- self._send_p2p_data(message)
+ self._send_slp_message(message)
self._dispose()
+ def _close_end_points(self, status):
+ """Send BYE to other end points; this client already answered.
+ @param status: response we sent to the peer"""
+ if len(self._peer.end_points) > 0:
+ return # if the peer supports MPOP, let him do the work
+
+ for end_point in self._client.profile.end_points.values():
+ if end_point.id == self._client.machine_guid:
+ continue
+ self._close_end_point(end_point, status)
+
+ def _close_end_point(self, end_point, status):
+ reason = (status, self._client.machine_guid)
+ body = SLPSessionCloseBody(session_id=self._id, reason=reason,
+ s_channel_state=0)
+ self._cseq = 0
+ self._branch = "{%s}" % uuid.uuid4()
+ message = SLPRequestMessage(SLPRequestMethod.BYE,
+ "MSNMSGR:" + self._client.profile.account,
+ to=self._client.profile.account,
+ frm=self._peer.account,
+ branch=self._branch,
+ cseq=self._cseq,
+ call_id=self._call_id,
+ on_behalf=self._peer.account)
+ message.body = body
+ self._transport_manager.send_slp_message(self._client.profile,
+ end_point.id, self._application_id, message)
+
def _dispose(self):
+ logger.info("Session %s disposed" % self._id)
+ self._session_manager._transport_manager.cleanup(self._id)
self._session_manager._unregister_session(self)
+ self.emit("disposed")
- def _send_p2p_data(self, data_or_file, is_file=False):
- if isinstance(data_or_file, SLPMessage):
- session_id = 0
- data = str(data_or_file)
- total_size = len(data)
- else:
- session_id = self._id
- data = data_or_file
- total_size = None
-
- blob = MessageBlob(self._application_id,
- data, total_size, session_id, None, is_file)
- self._session_manager._transport_manager.send(self.peer, blob)
+ def _send_slp_message(self, message):
+ self._transport_manager.send_slp_message(self.peer, self.peer_guid,
+ self._application_id, message)
+
+ def _send_data(self, data):
+ self._transport_manager.send_data(self.peer, self.peer_guid,
+ self._application_id, self._id, data)
def _on_blob_sent(self, blob):
if blob.session_id == 0:
@@ -225,11 +291,14 @@
print "Unhandled signaling blob :", message
elif isinstance(message, SLPResponseMessage):
if isinstance(message.body, SLPSessionRequestBody):
- if message.status is 200:
- self._on_session_accepted()
+ if message.status == 200:
self.emit("accepted")
- elif message.status is 603:
+ self._on_session_accepted()
+ elif message.status == 603:
+ self.emit("rejected")
self._on_session_rejected(message)
+ else:
+ print "Unhandled response blob :", message
return
self._on_data_blob_received(blob)
@@ -246,12 +315,14 @@
def _on_data_blob_sent(self, blob):
logger.info("Session data transfer completed")
- blob.data.seek(0, 0)
+ blob.data.seek(0, os.SEEK_SET)
+ self._completed = True
self.emit("completed", blob.data)
def _on_data_blob_received(self, blob):
logger.info("Session data transfer completed")
- blob.data.seek(0, 0)
+ blob.data.seek(0, os.SEEK_SET)
+ self._completed = True
self.emit("completed", blob.data)
self._close()
@@ -261,12 +332,12 @@
pass
def _on_bye_received(self, message):
- pass
+ self._dispose()
def _on_session_accepted(self):
pass
def _on_session_rejected(self, message):
- pass
+ self._dispose()
gobject.type_register(P2PSession)
--- papyon/msnp2p/session_manager.py
+++ papyon/msnp2p/session_manager.py
@@ -61,14 +61,14 @@
def _register_session(self, session):
self._sessions[session.id] = session
+ self._transport_manager.remove_from_blacklist(session.id)
def _unregister_session(self, session):
del self._sessions[session.id]
+ self._transport_manager.add_to_blacklist(session.id)
def _on_chunk_transferred(self, chunk):
- session_id = chunk.header.session_id
- if session_id == 0:
- return
+ session_id = chunk.session_id
session = self._get_session(session_id)
if session is None:
return
@@ -86,6 +86,21 @@
return session
return None
+ def _find_contact(self, account):
+ if ';' in account:
+ account, guid = account.split(';', 1)
+ guid = guid[1:-1]
+ else:
+ guid = None
+
+ if account == self._client.profile.account:
+ peer = self._client.profile
+ else:
+ peer = self._client.address_book.search_or_build_contact(
+ account, papyon.profile.NetworkID.MSN)
+
+ return peer, guid
+
def _blob_to_session(self, blob):
session_id = blob.session_id
@@ -144,12 +159,11 @@
message.method == SLPRequestMethod.INVITE:
if isinstance(message.body, SLPSessionRequestBody):
# Find the contact we received the message from
- peer = self._client.address_book.search_or_build_contact(
- message.frm, papyon.profile.NetworkID.MSN)
+ peer, guid = self._find_contact(message.frm)
try:
for handler in self._handlers:
if handler._can_handle_message(message):
- session = handler._handle_message(peer, message)
+ session = handler._handle_message(peer, guid, message)
if session is not None:
self._register_session(session)
break
--- papyon/msnp2p/test.py
+++ papyon/msnp2p/test.py
@@ -10,6 +10,7 @@
import logging
import gobject
+import os
logging.basicConfig(level=logging.DEBUG)
@@ -41,9 +42,9 @@
path = self._client.msn_object_path
f = open(path, 'r')
old_pos = f.tell()
- f.seek(0, 2)
+ f.seek(0, os.SEEK_END)
size = f.tell()
- f.seek(old_pos,0)
+ f.seek(old_pos, os.SEEK_SET)
msn_object = \
papyon.p2p.MSNObject(self._client.profile,
size, papyon.p2p.MSNObjectType.DISPLAY_PICTURE,
--- papyon/msnp2p/transport/TLP.py
+++ papyon/msnp2p/transport/TLP.py
@@ -18,11 +18,13 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+from papyon.msnp2p.transport import TLPv1, TLPv2
import papyon.util.string_io as StringIO
import struct
import random
import logging
+import os
__all__ = ['MessageBlob']
@@ -39,153 +41,28 @@
"""
return random.randint(1000, max)
-_previous_chunk_id = _generate_id(MAX_INT32 - 1)
-def _chunk_id():
- global _previous_chunk_id
- _previous_chunk_id += 1
- if _previous_chunk_id == MAX_INT32:
- _previous_chunk_id = 1
- return _previous_chunk_id
-
-class TLPHeader(object):
- SIZE = 48
-
- def __init__(self, *header):
- header = list(header)
- header[len(header):] = [0] * (9 - len(header))
-
- self.session_id = header[0]
- self.blob_id = header[1]
- self.blob_offset = header[2]
- self.blob_size = header[3]
- self.chunk_size = header[4]
- self.flags = header[5]
- self.dw1 = header[6]
- self.dw2 = header[7]
- self.qw1 = header[8]
-
- def __str__(self):
- return struct.pack("<LLQQLLLLQ", self.session_id,
- self.blob_id,
- self.blob_offset,
- self.blob_size,
- self.chunk_size,
- self.flags,
- self.dw1,
- self.dw2,
- self.qw1)
-
- @staticmethod
- def parse(header_data):
- header = struct.unpack("<LLQQLLLLQ", header_data[:48])
- session_id = header[0]
- blob_id = header[1]
- blob_offset = header[2]
- blob_size = header[3]
- chunk_size = header[4]
- flags = header[5]
- dw1 = header[6]
- dw2 = header[7]
- qw1 = header[8]
- return TLPHeader(session_id, blob_id, blob_offset, blob_size,
- chunk_size, flags, dw1, dw2, qw1)
-
-
-class TLPFlag(object):
- NAK = 0x1
- ACK = 0x2
- RAK = 0x4
- RST = 0x8
- FILE = 0x10
- EACH = 0x20
- CAN = 0x40
- ERR = 0x80
- KEY = 0x100
- CRYPT = 0x200
- UNKNOWN = 0x1000000
-
-
class MessageChunk(object):
- def __init__(self, header=None, body=None):
- if header is None:
- header = TLPHeader()
- self.header = header
- if body is None:
- body = ""
- self.body = body
- self.application_id = 0
-
- def __str__(self):
- return str(self.header) + str(self.body)
-
- def is_control_chunk(self):
- return self.header.flags & 0xCF
-
- def is_ack_chunk(self):
- return self.header.flags & (TLPFlag.NAK | TLPFlag.ACK)
-
- def is_nonce_chunk(self):
- return self.header.flags & TLPFlag.KEY
-
- def is_data_preparation_chunk(self):
- return (self.header.chunk_size == 4 and self.header.blob_size == 4 and
- self.body == "\x00\x00\x00\x00" and
- not self.header.flags & TLPFlag.FILE)
-
- def has_progressed(self):
- return self.header.flags & TLPFlag.EACH
-
- def require_ack(self):
- if self.is_ack_chunk():
- return False
- current_size = self.header.chunk_size + self.header.blob_offset
- if current_size == self.header.blob_size:
- return True
- return False
-
- def get_nonce(self):
- """Get the nonce from the chunk. The chunk needs to have the KEY flag
- for that nonce to make sense (use is_nonce_chunk to check that)"""
-
- if not self.is_nonce_chunk():
- return "00000000-0000-0000-0000-000000000000"
-
- bytes = ""
- bytes += struct.pack(">L", self.header.dw1)
- bytes += struct.pack(">H", self.header.dw2 & 0xFFFF)
- bytes += struct.pack(">H", self.header.dw2 >> 16)
- bytes += struct.pack("<Q", self.header.qw1)
-
- nonce = [("%X" % ord(byte)).zfill(2) for byte in bytes]
- for idx in (4, 7, 10, 13):
- nonce.insert(idx, '-')
- return "".join(nonce)
-
- def set_nonce(self, nonce):
- """Set the chunk headers from a nonce and make it a nonce chunk by
- adding the KEY flag."""
-
- nonce = filter(lambda c: c not in '{-}', nonce)
- bytes = ""
- for i in range(0, len(nonce), 2):
- bytes += chr(int(nonce[i:i+2], 16))
-
- self.header.dw1 = struct.unpack(">L", bytes[0:4])[0]
- self.header.dw2 = struct.unpack(">H", bytes[4:6])[0]
- self.header.dw2 += struct.unpack(">H", bytes[6:8])[0] << 16
- self.header.qw1 = struct.unpack("<Q", bytes[8:16])[0]
- self.header.flags |= TLPFlag.KEY
-
@staticmethod
- def parse(data):
- header = TLPHeader.parse(data[:48])
- body = data[48:]
- return MessageChunk(header, body)
+ def create(version, app_id, session_id, blob_id, offset, blob_size,
+ max_size, sync):
+ if version in (1, 2):
+ module = globals()["TLPv%i" % version]
+ return module.MessageChunk.create(app_id, session_id, blob_id,
+ offset, blob_size, max_size, sync)
+ else:
+ return None
+ @staticmethod
+ def parse(version, data):
+ if version in (1, 2):
+ module = globals()["TLPv%i" % version]
+ return module.MessageChunk.parse(data)
+ else:
+ return None
class MessageBlob(object):
def __init__(self, application_id, data, total_size=None,
- session_id=None, blob_id=None, is_file=False):
+ session_id=None, blob_id=None):
if data is not None:
if isinstance(data, str):
if len(data) > 0:
@@ -195,9 +72,9 @@
data = StringIO.StringIO()
if total_size is None:
- data.seek(0, 2) # relative to the end
+ data.seek(0, os.SEEK_END) # relative to the end
total_size = data.tell()
- data.seek(0, 0)
+ data.seek(0, os.SEEK_SET)
else:
total_size = 0
@@ -208,8 +85,9 @@
if session_id is None:
session_id = _generate_id()
self.session_id = session_id
- self.id = blob_id or _generate_id()
- self.is_file = is_file
+ if blob_id is None:
+ blob_id = _generate_id()
+ self.id = blob_id
def __del__(self):
#if self.data is not None:
@@ -240,68 +118,31 @@
def is_complete(self):
return self.transferred == self.total_size
- def is_data_blob(self):
- return not self.is_control_blob()
-
- def is_control_blob(self):
- return False
-
def read_data(self):
- self.data.seek(0, 0)
+ self.data.seek(0, os.SEEK_SET)
data = self.data.read()
- self.data.seek(0, 0)
+ self.data.seek(0, os.SEEK_SET)
return data
- def get_chunk(self, max_size):
- blob_offset = self.transferred
+ def get_chunk(self, version, max_size, sync=False):
+ chunk = MessageChunk.create(version, self.application_id, self.session_id,
+ self.id, self.transferred, self.total_size, max_size, sync)
if self.data is not None:
- self.data.seek(blob_offset, 0)
- data = self.data.read(max_size - TLPHeader.SIZE)
+ self.data.seek(self.transferred, os.SEEK_SET)
+ data = self.data.read(chunk.size)
assert len(data) > 0, "Trying to read more data than available"
else:
data = ""
- header = TLPHeader()
- header.session_id = self.session_id
- header.blob_id = self.id
- header.blob_offset = blob_offset
- header.blob_size = self.total_size
- header.chunk_size = len(data)
- header.dw1 = _chunk_id()
- if self.session_id != 0 and self.total_size != 4 and data != '\x00' * 4:
- header.flags = TLPFlag.UNKNOWN | TLPFlag.EACH
- if self.is_file:
- header.flags |= TLPFlag.FILE
-
- chunk = MessageChunk(header, data)
- chunk.application_id = self.application_id
- self.current_size += header.chunk_size
+ chunk.set_data(data)
+ self.current_size += chunk.size
return chunk
def append_chunk(self, chunk):
assert self.data is not None, "Trying to write to a Read Only blob"
- assert self.session_id == chunk.header.session_id, "Trying to append a chunk to the wrong blob"
- assert self.id == chunk.header.blob_id, "Trying to append a chunk to the wrong blob"
- self.data.seek(chunk.header.blob_offset, 0)
+ assert self.session_id == chunk.session_id, "Trying to append a chunk to the wrong blob"
+ assert self.id == chunk.blob_id, "Trying to append a chunk to the wrong blob"
+ self.data.seek(0, os.SEEK_END)
self.data.write(chunk.body)
- self.data.seek(0, 2)
self.current_size = self.data.tell()
-
-
-class ControlBlob(MessageBlob):
- def __init__(self, session_id, flags, dw1=0, dw2=0, qw1=0):
- MessageBlob.__init__(self, 0, None)
- header = TLPHeader(session_id, self.id, 0, 0, 0,
- flags, dw1, dw2, qw1)
- self.chunk = MessageChunk(header, "")
-
- def __repr__(self):
- return "<ControlBlob id=%x session_id=%x>" % (self.id, self.session_id)
-
- def get_chunk(self, max_size):
- return self.chunk
-
- def is_control_blob(self):
- return True
-
--- papyon/msnp2p/transport/TLPv1.py
+++ papyon/msnp2p/transport/TLPv1.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from papyon.msnp2p.constants import ApplicationID
+from papyon.util import debug
+from papyon.util.decorator import rw_property
+
+import struct
+import random
+import logging
+
+logger = logging.getLogger('papyon.msnp2p.transport')
+
+
+MAX_INT32 = 2147483647
+
+def _generate_id(max=MAX_INT32):
+ return random.randint(1000, max)
+
+
+class TLPFlag(object):
+ NAK = 0x1
+ ACK = 0x2
+ RAK = 0x4
+ RST = 0x8
+ FILE = 0x10
+ EACH = 0x20
+ CAN = 0x40
+ ERR = 0x80
+ KEY = 0x100
+ CRYPT = 0x200
+ UNKNOWN = 0x1000000
+
+
+class TLPHeader(object):
+ size = 48
+
+ def __init__(self, *header):
+ header = list(header)
+ header[len(header):] = [0] * (9 - len(header))
+
+ self.session_id = header[0]
+ self.blob_id = header[1]
+ self.blob_offset = header[2]
+ self.blob_size = header[3]
+ self.chunk_size = header[4]
+ self.flags = header[5]
+ self.dw1 = header[6]
+ self.dw2 = header[7]
+ self.qw1 = header[8]
+
+ def __str__(self):
+ return struct.pack("<LLQQLLLLQ", self.session_id,
+ self.blob_id,
+ self.blob_offset,
+ self.blob_size,
+ self.chunk_size,
+ self.flags,
+ self.dw1,
+ self.dw2,
+ self.qw1)
+
+ def parse(self, data):
+ fields = struct.unpack("<LLQQLLLLQ", data[:48])
+ self.session_id = fields[0]
+ self.blob_id = fields[1]
+ self.blob_offset = fields[2]
+ self.blob_size = fields[3]
+ self.chunk_size = fields[4]
+ self.flags = fields[5]
+ self.dw1 = fields[6]
+ self.dw2 = fields[7]
+ self.qw1 = fields[8]
+
+
+class MessageChunk(object):
+ def __init__(self, header=TLPHeader(), body="", application_id=0):
+ self.header = header
+ self.body = body
+ self.application_id = application_id
+
+ def __str__(self):
+ return str(self.header) + str(self.body)
+
+ @rw_property
+ def id():
+ def fget(self):
+ return self.header.dw1
+ def fset(self, value):
+ self.header.dw1 = value
+ return locals()
+
+ @property
+ def next_id(self):
+ if self.id + 1 == MAX_INT32:
+ return 1
+ return self.id + 1
+
+ @property
+ def session_id(self):
+ return self.header.session_id
+
+ @property
+ def blob_id(self):
+ return self.header.blob_id
+
+ @property
+ def ack_id(self):
+ return (self.header.blob_id, self.header.dw1)
+
+ @property
+ def acked_id(self):
+ return (self.header.dw1, self.header.dw2)
+
+ @property
+ def size(self):
+ return self.header.chunk_size
+
+ @property
+ def blob_size(self):
+ return self.header.blob_size
+
+ @property
+ def version(self):
+ return 1
+
+ def is_control_chunk(self):
+ return self.header.flags & 0xCF
+
+ def is_ack_chunk(self):
+ return self.header.flags & TLPFlag.ACK
+
+ def is_nak_chunk(self):
+ return self.header.flags & TLPFlag.NAK
+
+ def is_nonce_chunk(self):
+ return self.header.flags & TLPFlag.KEY
+
+ def is_data_preparation_chunk(self):
+ return (self.header.chunk_size == 4 and self.header.blob_size == 4 and
+ self.body == "\x00\x00\x00\x00" and
+ not self.header.flags & TLPFlag.FILE)
+
+ def is_signaling_chunk(self):
+ return (self.session_id == 0)
+
+ def has_progressed(self):
+ return self.header.flags & TLPFlag.EACH
+
+ def set_data(self, data):
+ self.body = data
+ self.header.chunk_size = len(data)
+
+ if self.session_id != 0 and self.blob_size != 4 and data != '\x00' * 4:
+ self.header.flags = TLPFlag.UNKNOWN | TLPFlag.EACH
+ if self.application_id is ApplicationID.FILE_TRANSFER:
+ self.header.flags |= TLPFlag.FILE
+
+ def require_ack(self):
+ if self.is_ack_chunk():
+ return False
+ current_size = self.header.chunk_size + self.header.blob_offset
+ if current_size == self.header.blob_size:
+ return True
+ return False
+
+ def create_ack_chunk(self):
+ flags = TLPFlag.ACK
+ if self.header.flags & TLPFlag.RAK:
+ flags |= TLPFlag.RAK
+
+ blob_id = _generate_id()
+ header = TLPHeader(self.header.session_id, blob_id, 0, 0, 0, flags,
+ self.header.blob_id, self.header.dw1, self.header.blob_size)
+ return MessageChunk(header)
+
+ def get_nonce(self):
+ """Get the nonce from the chunk. The chunk needs to have the KEY flag
+ for that nonce to make sense (use is_nonce_chunk to check that)"""
+
+ if not self.is_nonce_chunk():
+ return "00000000-0000-0000-0000-000000000000"
+
+ bytes = ""
+ bytes += struct.pack(">L", self.header.dw1)
+ bytes += struct.pack(">H", self.header.dw2 & 0xFFFF)
+ bytes += struct.pack(">H", self.header.dw2 >> 16)
+ bytes += struct.pack("<Q", self.header.qw1)
+
+ nonce = [("%X" % ord(byte)).zfill(2) for byte in bytes]
+ for idx in (4, 7, 10, 13):
+ nonce.insert(idx, '-')
+ return "".join(nonce)
+
+ def set_nonce(self, nonce):
+ """Set the chunk headers from a nonce and make it a nonce chunk by
+ adding the KEY flag."""
+
+ nonce = filter(lambda c: c not in '{-}', nonce)
+ bytes = ""
+ for i in range(0, len(nonce), 2):
+ bytes += chr(int(nonce[i:i+2], 16))
+
+ self.header.dw1 = struct.unpack(">L", bytes[0:4])[0]
+ self.header.dw2 = struct.unpack(">H", bytes[4:6])[0]
+ self.header.dw2 += struct.unpack(">H", bytes[6:8])[0] << 16
+ self.header.qw1 = struct.unpack("<Q", bytes[8:16])[0]
+ self.header.flags |= TLPFlag.KEY
+
+
+ @staticmethod
+ def create(app_id, session_id, blob_id, offset, blob_size, max_size, sync):
+ header = TLPHeader()
+ header.session_id = session_id
+ header.blob_id = blob_id
+ header.blob_offset = offset
+ header.blob_size = blob_size
+ header.chunk_size = min(blob_size - offset, max_size - header.size)
+ return MessageChunk(header, application_id=app_id)
+
+ @staticmethod
+ def parse(data):
+ header = TLPHeader()
+ header.parse(data[:48])
+ body = data[48:]
+ return MessageChunk(header, body)
+
+ def __repr__(self):
+ string = "TLPv1 chunk 0x%x: " % self.id
+ string += "blob %i, " % self.blob_id
+ if self.session_id:
+ string += "session %d, " % self.session_id
+ if self.is_ack_chunk():
+ string += "ACK 0x%x 0x%x" % self.acked_id
+ elif self.is_nak_chunk():
+ string += "NAK 0x%x 0x%x" % self.acked_id
+ if self.size > 0:
+ if self.session_id:
+ string += "data [%d bytes]" % self.size
+ else:
+ string += "SLP Message"
+ string += "\n\t" + debug.escape_string(self.body).\
+ replace("\r\n", "\\r\\n\n\t")
+ return string
--- papyon/msnp2p/transport/TLPv2.py
+++ papyon/msnp2p/transport/TLPv2.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from papyon.msnp2p.constants import ApplicationID, PeerInfo
+from papyon.util import debug
+from papyon.util.decorator import rw_property
+from papyon.util.tlv import TLV
+
+import struct
+import random
+import logging
+
+logger = logging.getLogger('msnp2p:transport')
+
+class TLPFlag(object):
+ NONE = 0x0
+ SYN = 0x1
+ RAK = 0x2
+
+class TLPParamType(object):
+ PEER_INFO = 0x1
+ ACK_SEQ = 0x2
+ NAK_SEQ = 0x3
+
+class DLPType(object):
+ SLP = 0x0
+ MSN_OBJECT = 0x4
+ FILE_TRANSFER = 0x6
+
+class DLPParamType(object):
+ DATA_REMAINING = 0x1
+
+TLPParamLength = {
+ TLPParamType.PEER_INFO: 12,
+ TLPParamType.ACK_SEQ: 4,
+ TLPParamType.NAK_SEQ: 4
+}
+
+DLPParamLength = {
+ DLPParamType.DATA_REMAINING: 8,
+}
+
+class TLPHeader(object):
+ """Transport Layer Protocol header v2:
+
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ....
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ |L|O|Len|ChunkID|if L>8 then TLV else skip .... | Payload
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+ L : Header Length
+ O : Operation Code (see TLPFlag)
+ Len : Length of payload (Data Header + Data)
+ ChunkID : ID of chunk (last chunk ID + last chunk Len)
+ TLV : See TLPParamType for possible types
+ Payload : Data Header + Data
+
+ Data Header (if data length > 0)
+
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ....
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ |L|T|PN |Session|if L>8 then TLV else skip .... | Data
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+ L : Data Header Length
+ T : Type-First combination (see DLPType for types)
+ PN : Package number (common between chunks of the same blob)
+ Session : Session Ientifier
+ TLV : See DLPParamType for possible types"""
+
+ def __init__(self):
+ self.op_code = 0
+ self.chunk_size = 0
+ self.chunk_id = 0
+ self.session_id = 0
+ self.data_type = 0
+ self.first = False
+ self.package_number = 0
+ self.tlv = TLV(TLPParamLength)
+ self.data_tlv = TLV(DLPParamLength)
+
+ @property
+ def size(self):
+ size = 8 + len(self.tlv)
+ if self.chunk_size > 0:
+ size += 8 + len(self.data_tlv)
+ return size
+
+ @property
+ def data_size(self):
+ size = self.chunk_size
+ if self.chunk_size > 0:
+ size += 8 + len(self.data_tlv)
+ return size
+
+ @rw_property
+ def peer_info():
+ def fget(self):
+ return self.tlv.get(TLPParamType.PEER_INFO, "")
+ def fset(self, value):
+ self.tlv.update(TLPParamType.PEER_INFO, value)
+ return locals()
+
+ @rw_property
+ def ack_seq():
+ def fget(self):
+ return self.tlv.get(TLPParamType.ACK_SEQ, 0)
+ def fset(self, value):
+ self.tlv.update(TLPParamType.ACK_SEQ, value)
+ return locals()
+
+ @rw_property
+ def nak_seq():
+ def fget(self):
+ return self.tlv.get(TLPParamType.NAK_SEQ, 0)
+ def fset(self, value):
+ self.tlv.update(TLPParamType.NAK_SEQ, value)
+ return locals()
+
+ @rw_property
+ def tf_combination():
+ def fget(self):
+ return self.data_type | self.first
+ def fset(self, value):
+ self.first = bool(value & 0x01)
+ self.data_type = value & 0xFE
+ return locals()
+
+ @rw_property
+ def data_remaining():
+ def fget(self):
+ return self.data_tlv.get(DLPParamType.DATA_REMAINING, 0)
+ def fset(self, value):
+ self.data_tlv.update(DLPParamType.DATA_REMAINING, value)
+ return locals()
+
+ def __str__(self):
+ size = 8 + len(self.tlv)
+ data_size = self.chunk_size
+ data_header = ""
+ if data_size > 0:
+ data_header_size, data_header = self.build_data_header()
+ data_size += data_header_size
+ header = struct.pack(">BBHL", size, self.op_code, data_size,
+ self.chunk_id)
+ header += str(self.tlv)
+ header += data_header
+ return header
+
+ def parse(self, data):
+ fields = struct.unpack(">BBHL", data[:8])
+ size = fields[0]
+ self.op_code = fields[1]
+ self.chunk_size = fields[2]
+ self.chunk_id = fields[3]
+ self.tlv.parse(data[8:], size - 8)
+ if self.chunk_size > 0:
+ dph_size = self.parse_data_header(data[size:])
+ self.chunk_size -= dph_size
+ size += dph_size
+ return size
+
+ def build_data_header(self):
+ size = len(self.data_tlv) + 8
+ header = struct.pack(">BBHL", size, self.tf_combination,
+ self.package_number, self.session_id)
+ header += str(self.data_tlv)
+ return size, header
+
+ def parse_data_header(self, data):
+ fields = struct.unpack(">BBHI", data[:8])
+ size = fields[0]
+ self.tf_combination = fields[1]
+ self.package_number = fields[2]
+ self.session_id = fields[3]
+ self.data_tlv.parse(data[8:], size - 8)
+ return size
+
+
+class MessageChunk(object):
+ def __init__(self, header, body="", application_id=0):
+ self.header = header
+ self.body = body
+ self.application_id = application_id
+
+ @rw_property
+ def id():
+ def fget(self):
+ return self.header.chunk_id
+ def fset(self, value):
+ self.header.chunk_id = value
+ return locals()
+
+ @property
+ def next_id(self):
+ return self.id + self.header.data_size
+
+ @rw_property
+ def application_id():
+ def fget(self):
+ return self._application_id
+ def fset(self, value):
+ self._application_id = value
+ if self.session_id == 0 or self.is_data_preparation_chunk():
+ self.header.data_type = DLPType.SLP
+ elif value is ApplicationID.FILE_TRANSFER:
+ self.header.data_type = DLPType.FILE_TRANSFER
+ elif value is ApplicationID.CUSTOM_EMOTICON_TRANSFER or \
+ value is ApplicationID.DISPLAY_PICTURE_TRANSFER:
+ self.header.data_type = DLPType.MSN_OBJECT
+ return locals()
+
+ @property
+ def session_id(self):
+ return self.header.session_id
+
+ @property
+ def blob_id(self):
+ return self.header.package_number
+
+ @property
+ def ack_id(self):
+ return self.header.chunk_id + self.header.data_size
+
+ @property
+ def acked_id(self):
+ return self.header.ack_seq
+
+ @property
+ def naked_id(self):
+ return self.header.nak_seq
+
+ @property
+ def size(self):
+ return self.header.chunk_size
+
+ @property
+ def blob_size(self):
+ if not self.header.first:
+ return 0
+ return self.header.data_remaining + self.size
+
+ @property
+ def version(self):
+ return 2
+
+ def is_control_chunk(self):
+ return self.is_ack_chunk() or self.is_nak_chunk() or \
+ (self.require_ack() and self.size == 0)
+
+ def is_ack_chunk(self):
+ return self.header.ack_seq > 0
+
+ def is_nak_chunk(self):
+ return self.header.nak_seq > 0
+
+ def is_syn_request(self):
+ return (self.header.op_code & TLPFlag.SYN) and not self.is_ack_chunk()
+
+ def is_signaling_chunk(self):
+ return (self.header.data_type == DLPType.SLP)
+
+ def is_data_preparation_chunk(self):
+ return (self.header.first and self.size == 4)
+
+ def require_ack(self):
+ return self.header.op_code & TLPFlag.RAK
+
+ def has_progressed(self):
+ return True
+
+ def create_ack_chunk(self):
+ header = TLPHeader()
+ header.ack_seq = self.header.chunk_id + self.header.chunk_size
+
+ # if we received a SYN request, send a SYN/ACK with a copy of the
+ # peer info and ask for an ACK
+ if self.is_syn_request():
+ header.peer_info = self.header.peer_info
+ header.op_code = TLPFlag.SYN | TLPFlag.RAK
+
+ return MessageChunk(header)
+
+ def set_data(self, data):
+ self.body = data
+ self.header.chunk_size = len(data)
+
+ @staticmethod
+ def create(app_id, session_id, blob_id, offset, blob_size, max_size, sync):
+ header = TLPHeader()
+ header.session_id = session_id
+ header.first = (offset == 0)
+
+ # if first message of a P2P session, add Peer Info to TLVs
+ if sync:
+ header.op_code = TLPFlag.SYN | TLPFlag.RAK
+ header.peer_info = struct.pack(">HHHHI",
+ PeerInfo.PROTOCOL_VERSION, PeerInfo.IMPLEMENTATION_ID,
+ PeerInfo.VERSION, 0, PeerInfo.CAPABILITIES)
+
+ max_chunk_size = max_size - header.size
+ data_remaining = blob_size - offset
+ if max_chunk_size >= data_remaining:
+ chunk_size = data_remaining
+ else:
+ chunk_size = max_chunk_size
+ header.chunk_size = chunk_size
+ header.data_remaining = data_remaining - chunk_size
+
+ # signaling messages require acknowledgement (last chunk only)
+ if session_id == 0 and header.data_remaining == 0:
+ header.op_code |= TLPFlag.RAK
+
+ # set package number for split signaling messages
+ header.package_number = 0
+ if session_id == 0 and (not header.first or header.data_remaining > 0):
+ header.package_numer = blob_id & 0xFFFF
+
+ return MessageChunk(header, application_id=app_id)
+
+ @staticmethod
+ def parse(data):
+ header = TLPHeader()
+ header_size = header.parse(data)
+ body = data[header_size:]
+ return MessageChunk(header, body)
+
+ def __str__(self):
+ return str(self.header) + str(self.body)
+
+ def __repr__(self):
+ string = "TLPv2 chunk 0x%x: " % self.id
+ string += "blob %i, " % self.blob_id
+ if self.session_id:
+ string += "session %d, " % self.session_id
+ if self.is_ack_chunk():
+ string += "ACK 0x%x, " % self.acked_id
+ if self.is_nak_chunk():
+ string += "NAK 0x%x, " % self.naked_id
+ if self.require_ack():
+ string += "RAK, "
+ if self.header.op_code & TLPFlag.SYN:
+ string += "SYN, "
+ if self.size > 0:
+ if self.session_id:
+ string += "data [%d bytes]" % self.size
+ else:
+ string += "SLP Message"
+ string += "\n\t" + debug.escape_string(self.body).\
+ replace("\r\n", "\\r\\n\n\t")
+ return string
--- papyon/msnp2p/transport/__init__.py
+++ papyon/msnp2p/transport/__init__.py
@@ -19,5 +19,4 @@
"""MSNP2P transport layer"""
-from TLP import *
from transport_manager import *
--- papyon/msnp2p/transport/base.py
+++ papyon/msnp2p/transport/base.py
@@ -18,16 +18,21 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-from papyon.msnp2p.transport.TLP import TLPFlag, MessageChunk, ControlBlob
+from papyon.msnp2p.transport.TLP import MessageBlob
import gobject
import logging
+import random
+import threading
import weakref
__all__ = ['BaseP2PTransport']
logger = logging.getLogger('papyon.msnp2p.transport')
+
+MAX_INT32 = 2147483647
+
class BaseP2PTransport(gobject.GObject):
__gsignals__ = {
"chunk-received": (gobject.SIGNAL_RUN_FIRST,
@@ -37,6 +42,14 @@
"chunk-sent": (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE,
(object,)),
+
+ "blob-received": (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
+
+ "blob-sent": (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
}
def __init__(self, transport_manager, name):
@@ -44,8 +57,13 @@
self._transport_manager = weakref.proxy(transport_manager)
self._client = transport_manager._client
self._name = name
+ self._source = None
+
+ self._local_chunk_id = None
+ self._remote_chunk_id = None
self._transport_manager._register_transport(self)
+ self._queue_lock = threading.Lock()
self._reset()
@property
@@ -64,98 +82,158 @@
def max_chunk_size(self):
raise NotImplementedError
- def send(self, blob, callback=None, errback=None):
- if blob.is_control_blob():
- self._control_blob_queue.append((blob, callback, errback))
+ @property
+ def version(self):
+ if self._client.profile.client_id.supports_p2pv2 and \
+ self.peer.client_capabilities.supports_p2pv2:
+ return 2
else:
- self._data_blob_queue.append((blob, callback, errback))
- gobject.timeout_add(200, self._process_send_queues)
- self._process_send_queues()
+ return 1
+
+ def can_send(self, peer, peer_guid, blob, bootstrap=False):
+ raise NotImplementedError
+
+ def send(self, peer, peer_guid, blob):
+ self._queue_lock.acquire()
+ self._data_blob_queue.append((peer, peer_guid, blob))
+ self._queue_lock.release()
+ self._start_processing()
+
+ def cleanup(self, session_id):
+ # remove this session's blobs from the data queue
+ self._queue_lock.acquire()
+ canceled_blobs = []
+ for blob in self._data_blob_queue:
+ if blob[2].session_id == session_id:
+ canceled_blobs.append(blob)
+ for blob in canceled_blobs:
+ self._data_blob_queue.remove(blob)
+ self._queue_lock.release()
def close(self):
self._transport_manager._unregister_transport(self)
- def _send_chunk(self, chunk):
+ def _ready_to_send(self):
raise NotImplementedError
- # Helper methods
+ def _send_chunk(self, peer, peer_guid, chunk):
+ raise NotImplementedError
+
+ # Helper methods ---------------------------------------------------------
+
def _reset(self):
- self._control_blob_queue = []
+ self._queue_lock.acquire()
+ self._first = True
self._data_blob_queue = []
- self._pending_blob = {} # last_chunk : (blob, callback, errback)
- self._pending_ack = {} # blob_id : [blob_offset1, blob_offset2 ...]
-
- def _add_pending_ack(self, blob_id, chunk_id=0):
- if blob_id not in self._pending_ack:
- self._pending_ack[blob_id] = set()
- self._pending_ack[blob_id].add(chunk_id)
+ self._pending_blob = {} # ack_id : (blob, callback, errback)
+ self._pending_ack = set()
+ self._signaling_blobs = {} # blob_id : blob
+ self._queue_lock.release()
+
+ def _add_pending_ack(self, ack_id):
+ self._pending_ack.add(ack_id)
+
+ def _del_pending_ack(self, ack_id):
+ self._pending_ack.discard(ack_id)
+
+ def _add_pending_blob(self, ack_id, blob):
+ if self.version == 1:
+ self._pending_blob[ack_id] = blob
+ else:
+ self.emit("blob-sent", blob)
- def _del_pending_ack(self, blob_id, chunk_id=0):
- if blob_id not in self._pending_ack:
+ def _del_pending_blob(self, ack_id):
+ if not ack_id in self._pending_blob:
return
- self._pending_ack[blob_id].discard(chunk_id)
+ blob = self._pending_blob.pop(ack_id)
+ self.emit("blob-sent", blob)
- if len(self._pending_ack[blob_id]) == 0:
- del self._pending_ack[blob_id]
+ def _on_chunk_received(self, peer, peer_guid, chunk):
+ if chunk.is_data_preparation_chunk():
+ return
- def _on_chunk_received(self, chunk):
if chunk.require_ack():
- self._send_ack(chunk)
+ ack_chunk = chunk.create_ack_chunk()
+ self.__send_chunk(peer, peer_guid, ack_chunk)
- if chunk.header.flags & TLPFlag.ACK:
- self._del_pending_ack(chunk.header.dw1, chunk.header.dw2)
- if chunk.header.dw1 in self._pending_blob:
- blob, callback, errback = self._pending_blob[chunk.header.dw1]
- del self._pending_blob[blob.id]
- if callback:
- callback[0](*callback[1:])
+ if chunk.is_ack_chunk() or chunk.is_nak_chunk():
+ self._del_pending_ack(chunk.acked_id)
+ self._del_pending_blob(chunk.acked_id)
- #FIXME: handle all the other flags
+ #FIXME: handle all the other flags (NAK...)
if not chunk.is_control_chunk():
- self.emit("chunk-received", chunk)
+ if chunk.is_signaling_chunk(): # signaling chunk
+ self._on_signaling_chunk_received(chunk)
+ else: # data chunk (buffered by the transport manager)
+ self.emit("chunk-received", chunk)
+
+ self._start_processing()
+
+ def _on_signaling_chunk_received(self, chunk):
+ blob_id = chunk.blob_id
+ if blob_id in self._signaling_blobs:
+ blob = self._signaling_blobs[blob_id]
+ else:
+ # create an in-memory blob
+ blob = MessageBlob(chunk.application_id, "",
+ chunk.blob_size, chunk.session_id, blob_id)
+ self._signaling_blobs[blob_id] = blob
- self._process_send_queues()
+ blob.append_chunk(chunk)
+ if blob.is_complete():
+ self.emit("blob-received", blob)
+ del self._signaling_blobs[blob_id]
def _on_chunk_sent(self, chunk):
self.emit("chunk-sent", chunk)
- if chunk in self._pending_blob:
- blob, callback, errback = self._pending_blob.pop(chunk)
- if callback:
- callback[0](*callback[1:])
- self._process_send_queues()
-
- def _process_send_queues(self):
- if len(self._control_blob_queue) > 0:
- queue = self._control_blob_queue
- elif len(self._data_blob_queue) > 0:
- queue = self._data_blob_queue
- else:
+ self._start_processing()
+
+ def _start_processing(self):
+ if self._source is None:
+ self._source = gobject.timeout_add(200, self._process_send_queue)
+ self._process_send_queue()
+
+ def _stop_processing(self):
+ if self._source is not None:
+ gobject.source_remove(self._source)
+ self._source = None
+
+ def _process_send_queue(self):
+ if not self._queue_lock.acquire(False):
+ return True
+ if len(self._data_blob_queue) == 0:
+ self._queue_lock.release()
+ self._stop_processing()
return False
+ if not self._ready_to_send():
+ logger.info("Transport is not ready to send, bail out")
+ self._queue_lock.release()
+ self._stop_processing()
+ return True
+
+ sync = self._first
+ self._first = False
+ (peer, peer_guid, blob) = self._data_blob_queue[0]
+ chunk = blob.get_chunk(self.version, self.max_chunk_size, sync)
+ self.__send_chunk(peer, peer_guid, chunk)
- blob, callback, errback = queue[0]
- chunk = blob.get_chunk(self.max_chunk_size)
if blob.is_complete():
- queue.pop(0)
- self._pending_blob[chunk] = (blob, callback, errback)
-
- if chunk.require_ack() :
- self._add_pending_ack(chunk.header.blob_id, chunk.header.dw1)
- self._send_chunk(chunk)
+ self._data_blob_queue.pop(0)
+ self._add_pending_blob(chunk.ack_id, blob)
+ self._queue_lock.release()
return True
- def _send_ack(self, received_chunk):
- flags = received_chunk.header.flags
+ def __send_chunk(self, peer, peer_guid, chunk):
+ # add local identifier to chunk
+ if self._local_chunk_id is None:
+ self._local_chunk_id = random.randint(1000, MAX_INT32)
+ chunk.id = self._local_chunk_id
+ self._local_chunk_id = chunk.next_id
- flags = TLPFlag.ACK
- if received_chunk.header.flags & TLPFlag.RAK:
- flags |= TLPFlag.RAK
-
- ack_blob = ControlBlob(received_chunk.header.session_id, flags,
- dw1 = received_chunk.header.blob_id,
- dw2 = received_chunk.header.dw1,
- qw1 = received_chunk.header.blob_size)
+ if chunk.require_ack() :
+ self._add_pending_ack(chunk.ack_id)
- self.send(ack_blob)
+ self._send_chunk(peer, peer_guid, chunk)
gobject.type_register(BaseP2PTransport)
--- papyon/msnp2p/transport/notification.py
+++ papyon/msnp2p/transport/notification.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from papyon.msnp.constants import UserNotificationTypes
+from papyon.msnp2p.transport.TLP import MessageBlob
+from papyon.msnp2p.transport.base import BaseP2PTransport
+
+import gobject
+import struct
+import logging
+
+__all__ = ['NotificationP2PTransport']
+
+logger = logging.getLogger('papyon.msnp2p.transport.notification')
+
+class NotificationP2PTransport(BaseP2PTransport):
+ def __init__(self, client, transport_manager):
+ BaseP2PTransport.__init__(self, transport_manager, "notification")
+ self._protocol = client._protocol
+ self._protocol.connect("buddy-notification-received",
+ self._on_notification_received)
+
+ def close(self):
+ BaseP2PTransport.close(self)
+
+ @property
+ def rating(self):
+ return 0
+
+ @property
+ def max_chunk_size(self):
+ return 7500
+
+ def can_send(self, peer, peer_guid, blob, bootstrap=False):
+ if not bootstrap:
+ return False # can only handle bootstrap signaling
+ if blob.total_size > self.max_chunk_size:
+ return False # can't split in chunks
+ if not peer.client_capabilities.p2p_bootstrap_via_uun:
+ return False # peer needs to support UUN bootstrap
+ return True
+
+ def send(self, peer, peer_guid, blob):
+ data = blob.read_data()
+ self._protocol.send_user_notification(data, peer, peer_guid,
+ UserNotificationTypes.P2P_DATA, self._on_notification_sent, data)
+
+ def _on_notification_received(self, protocol, peer, peer_guid, type, data):
+ if type is not UserNotificationTypes.P2P_DATA:
+ return
+ blob = MessageBlob(0, data, None, 0)
+ self.emit("blob-received", blob)
+
+ def _on_notification_sent(self, data):
+ blob = MessageBlob(0, data, None, 0)
+ self.emit("blob-sent", blob)
--- papyon/msnp2p/transport/switchboard.py
+++ papyon/msnp2p/transport/switchboard.py
@@ -21,7 +21,7 @@
from papyon.msnp.message import MessageAcknowledgement
from papyon.msnp2p.transport.TLP import MessageChunk
from papyon.msnp2p.transport.base import BaseP2PTransport
-from papyon.switchboard_manager import SwitchboardClient
+from papyon.switchboard_manager import SwitchboardHandler
import gobject
import struct
@@ -29,14 +29,19 @@
__all__ = ['SwitchboardP2PTransport']
-logger = logging.getLogger('papyon.msnp2p.transport')
+logger = logging.getLogger('papyon.msnp2p.transport.switchboard')
-class SwitchboardP2PTransport(BaseP2PTransport, SwitchboardClient):
- def __init__(self, client, contacts, transport_manager):
- SwitchboardClient.__init__(self, client, contacts)
- BaseP2PTransport.__init__(self, transport_manager, "switchboard")
+class SwitchboardP2PTransport(BaseP2PTransport, SwitchboardHandler):
+
+ MAX_OUTSTANDING_SENDS = 5
+ def __init__(self, client, switchboard, contacts, peer, peer_guid, transport_manager):
+ self._oustanding_sends = 0
+ self._peer = peer
+ self._peer_guid = peer_guid
+ SwitchboardHandler.__init__(self, client, switchboard, contacts)
+ BaseP2PTransport.__init__(self, transport_manager, "switchboard")
def close(self):
BaseP2PTransport.close(self)
@@ -47,11 +52,32 @@
content_type = message.content_type[0]
return content_type == 'application/x-msnmsgrp2p'
+ @staticmethod
+ def handle_peer(client, peer, peer_guid, transport_manager):
+ return SwitchboardP2PTransport(client, None, (peer,), peer, peer_guid,
+ transport_manager)
+
+ @staticmethod
+ def handle_message(client, switchboard, message, transport_manager):
+ guid = None
+ peer = None
+ if 'P2P-Src' in message.headers and ';' in message.headers['P2P-Src']:
+ account, guid = message.headers['P2P-Src'].split(';', 1)
+ guid = guid[1:-1]
+ if account == client.profile.account:
+ peer = client.profile
+ if peer is None:
+ peer = switchboard.participants.values()[0]
+ return SwitchboardP2PTransport(client, switchboard, (), peer, guid,
+ transport_manager)
+
@property
def peer(self):
- for peer in self.total_participants:
- return peer
- return None
+ return self._peer
+
+ @property
+ def peer_guid(self):
+ return self._peer_guid
@property
def rating(self):
@@ -61,21 +87,72 @@
def max_chunk_size(self):
return 1250 # length of the chunk including the header but not the footer
- def _send_chunk(self, chunk):
- headers = {'P2P-Dest': self.peer.account}
+ def can_send(self, peer, peer_guid, blob, bootstrap=False):
+ return (self._peer == peer and self._peer_guid == peer_guid)
+
+ def __parse_guid(self, message, header):
+ if header not in message.headers or ';' not in message.headers[header]:
+ return None
+ return message.headers[header].split(';', 1)[1][1:-1]
+
+ def _ready_to_send(self):
+ return (self._oustanding_sends < self.MAX_OUTSTANDING_SENDS)
+
+ def _send_chunk(self, peer, peer_guid, chunk):
+ logger.debug(">>> %s" % repr(chunk))
+ if chunk.version is 1 or peer_guid is None:
+ headers = {'P2P-Dest': self.peer.account}
+ elif chunk.version is 2:
+ headers = {'P2P-Src' : self._client.profile.account + ";{" +
+ self._client.machine_guid + "}",
+ 'P2P-Dest': peer.account + ";{" +
+ peer_guid + "}"}
content_type = 'application/x-msnmsgrp2p'
body = str(chunk) + struct.pack('>L', chunk.application_id)
+ self._oustanding_sends += 1
self._send_message(content_type, body, headers,
- MessageAcknowledgement.MSNC, self._on_chunk_sent, (chunk,))
+ MessageAcknowledgement.MSNC, (self._on_message_sent, chunk),
+ (self._on_message_error, chunk))
def _on_message_received(self, message):
- chunk = MessageChunk.parse(message.body[:-4])
+ version = 1
+ # if destination contains a GUID, the protocol should be TLPv2
+ dest_guid = self.__parse_guid(message, 'P2P-Dest')
+ src_guid = self.__parse_guid(message, 'P2P-Src')
+ if dest_guid and src_guid:
+ version = 2
+ if dest_guid != self._client.machine_guid or \
+ src_guid != self._peer_guid:
+ return # this chunk is not for us
+
+ chunk = MessageChunk.parse(version, message.body[:-4])
chunk.application_id = struct.unpack('>L', message.body[-4:])[0]
- self._on_chunk_received(chunk)
+ logger.debug("<<< %s" % repr(chunk))
+ self._on_chunk_received(self._peer, self._peer_guid, chunk)
+
+ def _on_message_sent(self, chunk):
+ self._oustanding_sends -= 1
+ self._on_chunk_sent(chunk)
+
+ def _on_message_error(self, chunk):
+ self._oustanding_sends -= 1
+
+ def _on_switchboard_closed(self):
+ pass
+
+ def _on_closed(self):
+ BaseP2PTransport.close(self)
+
+ def _on_error(self, error_type, error):
+ logger.info("Received error %i (type=%i)" % (error, error_type))
def _on_contact_joined(self, contact):
pass
def _on_contact_left(self, contact):
- self.close()
+ if contact == self._peer:
+ self.close()
+ def __repr__(self):
+ return '<SwitchboardP2PTransport peer="%s" guid="%s">' % \
+ (self.peer.account, self.peer_guid)
--- papyon/msnp2p/transport/transport_manager.py
+++ papyon/msnp2p/transport/transport_manager.py
@@ -19,11 +19,13 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
from papyon.msnp2p.transport.switchboard import *
+from papyon.msnp2p.transport.notification import *
from papyon.msnp2p.transport.TLP import MessageBlob
import gobject
import struct
import logging
+import os
__all__ = ['P2PTransportManager']
@@ -51,88 +53,104 @@
self._client = client
switchboard_manager = self._client._switchboard_manager
switchboard_manager.register_handler(SwitchboardP2PTransport, self)
- self._default_transport = \
- lambda transport_mgr, peer : \
- SwitchboardP2PTransport(client, (peer,), transport_mgr)
+ self._default_transport = lambda peer, peer_guid : \
+ SwitchboardP2PTransport.handle_peer(client, peer, peer_guid, self)
self._transports = set()
self._transport_signals = {}
- self._signaling_blobs = {} # blob_id => blob
self._data_blobs = {} # session_id => blob
+ self._blacklist = set() # blacklist of session_id
+ uun_transport = NotificationP2PTransport(client, self)
def _register_transport(self, transport):
+ logger.info("Registering transport %s" % repr(transport))
assert transport not in self._transports, "Trying to register transport twice"
self._transports.add(transport)
signals = []
- signals.append(transport.connect("chunk-received", self._on_chunk_received))
- signals.append(transport.connect("chunk-sent", self._on_chunk_sent))
+ signals.append(transport.connect("chunk-received",
+ self._on_chunk_received))
+ signals.append(transport.connect("chunk-sent",
+ self._on_chunk_sent))
+ signals.append(transport.connect("blob-received",
+ self._on_blob_received))
+ signals.append(transport.connect("blob-sent",
+ self._on_blob_sent))
self._transport_signals[transport] = signals
def _unregister_transport(self, transport):
+ if transport not in self._transports:
+ return
+ logger.info("Unregistering transport %s" % repr(transport))
self._transports.discard(transport)
- signals = self._transport_signals[transport]
+ signals = self._transport_signals.pop(transport, [])
for signal in signals:
transport.disconnect(signal)
- del self._transport_signals[transport]
- def _get_transport(self, peer):
+ def _get_transport(self, peer, peer_guid, blob):
for transport in self._transports:
- if transport.peer == peer:
+ if transport.can_send(peer, peer_guid, blob):
return transport
- return self._default_transport(self, peer)
+ return self._default_transport(peer, peer_guid)
def _on_chunk_received(self, transport, chunk):
self.emit("chunk-transferred", chunk)
- session_id = chunk.header.session_id
- blob_id = chunk.header.blob_id
+ session_id = chunk.session_id
+ blob_id = chunk.blob_id
+
+ if session_id in self._blacklist:
+ return
- if session_id == 0: # signaling blob
- if blob_id in self._signaling_blobs:
- blob = self._signaling_blobs[blob_id]
- else:
- # create an in-memory blob
- blob = MessageBlob(chunk.application_id, "",
- chunk.header.blob_size,
- session_id, chunk.header.blob_id)
- self._signaling_blobs[blob_id] = blob
- else: # data blob
- if chunk.is_data_preparation_chunk():
- return
-
- if session_id in self._data_blobs:
- blob = self._data_blobs[session_id]
- if blob.transferred == 0:
- blob.id = chunk.header.blob_id
- else:
- # create an in-memory blob
- blob = MessageBlob(chunk.application_id, "",
- chunk.header.blob_size,
- session_id, chunk.header.blob_id)
- self._data_blobs[session_id] = blob
+ if session_id in self._data_blobs:
+ blob = self._data_blobs[session_id]
+ if blob.transferred == 0:
+ blob.id = chunk.blob_id
+ else:
+ # create an in-memory blob
+ blob = MessageBlob(chunk.application_id, "",
+ chunk.blob_size, session_id, chunk.blob_id)
+ self._data_blobs[session_id] = blob
blob.append_chunk(chunk)
if blob.is_complete():
- blob.data.seek(0, 0)
+ del self._data_blobs[session_id]
self.emit("blob-received", blob)
- if session_id == 0:
- del self._signaling_blobs[blob_id]
- else:
- del self._data_blobs[session_id]
def _on_chunk_sent(self, transport, chunk):
self.emit("chunk-transferred", chunk)
+ def _on_blob_received(self, transport, blob):
+ self.emit("blob-received", blob)
+
def _on_blob_sent(self, transport, blob):
self.emit("blob-sent", blob)
- def send(self, peer, blob):
- transport = self._get_transport(peer)
- transport.send(blob, (self._on_blob_sent, transport, blob))
+ def send_slp_message(self, peer, peer_guid, application_id, message):
+ self.send_data(peer, peer_guid, application_id, 0, str(message))
+
+ def send_data(self, peer, peer_guid, application_id, session_id, data):
+ blob = MessageBlob(application_id, data, None, session_id, None)
+ transport = self._get_transport(peer, peer_guid, blob)
+ transport.send(peer, peer_guid, blob)
- def register_writable_blob(self, blob):
- if blob.session_id in self._data_blobs:
+ def register_data_buffer(self, session_id, buffer, size):
+ if session_id in self._data_blobs:
logger.warning("registering already registered blob "\
"with session_id=" + str(session_id))
return
- self._data_blobs[blob.session_id] = blob
+ blob = MessageBlob(0, buffer, size, session_id)
+ self._data_blobs[session_id] = blob
+
+ def cleanup(self, session_id):
+ if session_id in self._data_blobs:
+ del self._data_blobs[session_id]
+ for transport in self._transports:
+ transport.cleanup(session_id)
+
+ def add_to_blacklist(self, session_id):
+ # ignore data chunks received for this session_id:
+ # we want to ignore chunks received shortly after closing a session
+ self._blacklist.add(session_id)
+
+ def remove_from_blacklist(self, session_id):
+ self._blacklist.discard(session_id)
gobject.type_register(P2PTransportManager)
--- papyon/msnp2p/webcam.py
+++ papyon/msnp2p/webcam.py
@@ -32,6 +32,7 @@
import gobject
import logging
import base64
+import os
import random
from papyon.media import MediaCall, MediaCandidate, MediaCandidateEncoder, \
@@ -44,14 +45,14 @@
class WebcamSession(P2PSession, MediaCall, EventsDispatcher):
- def __init__(self, producer, session_manager, peer,
+ def __init__(self, producer, session_manager, peer, peer_guid,
euf_guid, message=None):
if producer:
type = MediaSessionType.WEBCAM_SEND
else:
type = MediaSessionType.WEBCAM_RECV
- P2PSession.__init__(self, session_manager, peer, euf_guid,
+ P2PSession.__init__(self, session_manager, peer, peer_guid, euf_guid,
ApplicationID.WEBCAM, message)
MediaCall.__init__(self, type)
EventsDispatcher.__init__(self)
@@ -62,6 +63,10 @@
self._session_id = self._generate_id(9999)
self._xml_needed = False
+ @property
+ def producer(self):
+ return self._producer
+
def invite(self):
self._answered = True
context = "{B8BE70DE-E2CA-4400-AE03-88FF85B9F4E8}"
@@ -80,12 +85,12 @@
self._answered = True
self._respond(603)
- def end(self):
+ def end(self, reason=None):
if not self._answered:
self.reject()
else:
context = '\x74\x03\x00\x81'
- self._close(context)
+ self._close(context, reason)
self.dispose()
def dispose(self):
@@ -111,9 +116,10 @@
def _on_session_rejected(self, message):
self._dispatch("on_call_rejected", message)
+ self.dispose()
def _on_data_blob_received(self, blob):
- blob.data.seek(0, 0)
+ blob.data.seek(0, os.SEEK_SET)
data = blob.data.read()
data = unicode(data[10:], "utf-16-le").rstrip("\x00")
@@ -136,7 +142,7 @@
message_bytes = data.encode("utf-16-le") + "\x00\x00"
id = (self._generate_id() << 8) | 0x80
header = struct.pack("<LHL", id, 8, len(message_bytes))
- self._send_p2p_data(header + message_bytes)
+ self._send_data(header + message_bytes)
def send_binary_syn(self):
self.send_data('syn')
--- papyon/p2p.py
+++ papyon/p2p.py
@@ -30,8 +30,10 @@
from msnp2p.webcam import WebcamSession
from msnp2p import EufGuid, ApplicationID
from msnp2p.exceptions import ParseError
+from msnp2p.constants import SLPStatus
from profile import NetworkID, Contact, Profile
+from papyon.util.encoding import b64_decode
import papyon.util.element_tree as ElementTree
import papyon.util.string_io as StringIO
@@ -41,6 +43,7 @@
import base64
import hashlib
import logging
+import os
__all__ = ['MSNObjectType', 'MSNObject', 'MSNObjectStore',
'FileTransferManager', 'WebcamHandler']
@@ -67,30 +70,6 @@
LOCATION = 14
"Location"
-def _decode_shad(shad, warning=True):
- try:
- shad = base64.b64decode(shad)
- except TypeError:
- # See fd.o#27672 for details on this workaround.
- if ' ' in shad:
- parts = shad.split(' ')
-
- # Try the first part.
- shad = _decode_shad(parts[0], False)
-
- # Try the second part.
- if shad is None:
- shad = _decode_shad(parts[1], False)
-
- else:
- # Only display this warning if we're not in a nested call otherwise the
- # warning will be confusing.
- if warning:
- logger.warning("Invalid SHA1D in MSNObject: %s" % shad)
- shad = None
-
- return shad
-
class MSNObject(object):
"Represents an MSNObject."
def __init__(self, creator, size, typ, location, friendly,
@@ -130,7 +109,7 @@
if shad is None:
if data is None:
- raise NotImplementedError
+ raise ValueError
shad = self.__compute_data_hash(data)
self._data_sha = shad
self.__data = data
@@ -157,9 +136,9 @@
return
old_pos = data.tell()
- data.seek(0, 2)
+ data.seek(0, os.SEEK_END)
self._size = data.tell()
- data.seek(old_pos, 0)
+ data.seek(old_pos, os.SEEK_SET)
self.__data = data
self._checksum_sha = self.__compute_checksum()
@@ -186,33 +165,43 @@
location = "0"
if "Friendly" in element:
- friendly = base64.b64decode(xml.unescape(element["Friendly"]))
+ friendly = b64_decode(xml.unescape(element["Friendly"]))
else:
- friendly = base64.b64decode('AAA=')
+ friendly = '\x00\x00'
shad = element.get("SHA1D", None)
if shad is not None:
- shad = _decode_shad(shad)
+ try:
+ shad = b64_decode(shad)
+ except TypeError:
+ logger.warning("Invalid SHA1D in MSNObject: %s" % shad)
+ shad = None
+
shac = element.get("SHA1C", None)
if shac is not None:
try:
- shac = base64.b64decode(shac)
+ shac = b64_decode(shac)
except TypeError:
logger.warning("Invalid SHA1C in MSNObject: %s" % shac)
shac = None
- result = MSNObject(creator, size, type, location, friendly, shad, shac)
- result._repr = xml_data
+ try:
+ result = MSNObject(creator, size, type, location, friendly, shad, shac)
+ result._repr = xml_data
+ except ValueError:
+ logger.warning("Invalid MSNObject")
+ return None
+
return result
def __compute_data_hash(self, data):
digest = hashlib.sha1()
- data.seek(0, 0)
+ data.seek(0, os.SEEK_SET)
read_data = data.read(1024)
while len(read_data) > 0:
digest.update(read_data)
read_data = data.read(1024)
- data.seek(0, 0)
+ data.seek(0, os.SEEK_SET)
return digest.digest()
def __compute_checksum(self):
@@ -238,12 +227,38 @@
return dump
-class MSNObjectStore(object):
+class P2PSessionHandler(gobject.GObject):
def __init__(self, client):
+ gobject.GObject.__init__(self)
self._client = client
- self._outgoing_sessions = {} # session => (handle_id, callback, errback)
- self._incoming_sessions = {}
+ self._sessions = []
+ self._handles = {} # session : handles
+
+ def _add_session(self, session):
+ self._connect_session(session)
+ self._sessions.append(session)
+
+ def _connect_session(self, session):
+ handles = []
+ handles.append(session.connect("disposed", self._on_session_disposed))
+ self._handles[session] = handles
+
+ def _disconnect_session(self, session):
+ handles = self._handles.pop(session)
+ for handle_id in handles:
+ session.disconnect(handle_id)
+
+ def _on_session_disposed(self, session):
+ self._disconnect_session(session)
+ self._sessions.remove(session)
+
+
+class MSNObjectStore(P2PSessionHandler):
+
+ def __init__(self, client):
+ P2PSessionHandler.__init__(self, client)
+ self._callbacks = {} # session => (callback, errback, msn_object)
self._published_objects = set()
def _can_handle_message (self, message):
@@ -253,46 +268,71 @@
else:
return False
- def _handle_message(self, peer, message):
+ def _handle_message(self, peer, guid, message):
session = MSNObjectSession(self._client._p2p_session_manager,
- peer, message.body.application_id, message)
-
- handle_id = session.connect("completed",
- self._incoming_session_transfer_completed)
- self._incoming_sessions[session] = handle_id
+ peer, guid, message.body.application_id, message)
try:
- msn_object = MSNObject.parse(self._client, session._context)
+ msn_object = MSNObject.parse(self._client, session.context)
except ParseError:
+ logger.error("Error while parsing MSN object from request")
session.reject()
return
+
+ self._add_session(session)
+ self._callbacks[session] = (None, None, msn_object)
+
for obj in self._published_objects:
if obj._data_sha == msn_object._data_sha:
session.accept(obj._data)
return session
- session.reject()
+ logger.warning("Unknown MSN object, another end point might have it")
+
+ def _connect_session(self, session):
+ P2PSessionHandler._connect_session(self, session)
+ self._handles[session].append(session.connect("completed",
+ self._on_session_completed))
+ self._handles[session].append(session.connect("rejected",
+ self._on_session_completed))
+
+ def _on_session_completed(self, session, data):
+ if session in self._callbacks:
+ callback, errback, msn_object = self._callbacks[session]
+ if callback:
+ msn_object._data = data
+ callback[0](msn_object, *callback[1:])
+
+ def _on_session_rejected(self, session):
+ if session in self._callbacks:
+ callback, errback, msn_object = self._callbacks[session]
+ if errback:
+ errback[0](msn_object, *errback[1:])
+
+ ### Public API
def request(self, msn_object, callback, errback=None, peer=None):
if msn_object._data is not None:
callback[0](msn_object, *callback[1:])
+ return
if peer is None:
peer = self._client.address_book.search_contact(msn_object._creator,
NetworkID.MSN)
if msn_object._type == MSNObjectType.CUSTOM_EMOTICON:
- application_id = ApplicationID.CUSTOM_EMOTICON_TRANSFER
+ app_id = ApplicationID.CUSTOM_EMOTICON_TRANSFER
elif msn_object._type == MSNObjectType.DISPLAY_PICTURE:
- application_id = ApplicationID.DISPLAY_PICTURE_TRANSFER
+ app_id = ApplicationID.DISPLAY_PICTURE_TRANSFER
else:
raise NotImplementedError
- session = MSNObjectSession(self._client._p2p_session_manager,
- peer, application_id)
- handle_id = session.connect("completed",
- self._outgoing_session_transfer_completed)
- self._outgoing_sessions[session] = \
- (handle_id, callback, errback, msn_object)
- session.invite(repr(msn_object))
+ context = repr(msn_object)
+ session = MSNObjectMetaSession(self._client, peer, app_id, context)
+ self._add_session(session)
+ self._callbacks[session] = (callback, errback, msn_object)
+ logger.info("Requesting a MSNObject from %s (%i end points)" %
+ (peer.account, session.count))
+ session.invite()
+ return session
def publish(self, msn_object):
if msn_object._data is None:
@@ -300,21 +340,8 @@
else:
self._published_objects.add(msn_object)
- def _outgoing_session_transfer_completed(self, session, data):
- handle_id, callback, errback, msn_object = self._outgoing_sessions[session]
- session.disconnect(handle_id)
- msn_object._data = data
-
- callback[0](msn_object, *callback[1:])
- del self._outgoing_sessions[session]
-
- def _incoming_session_transfer_completed(self, session, data):
- handle_id = self._incoming_sessions[session]
- session.disconnect(handle_id)
- del self._incoming_sessions[session]
-
-class FileTransferManager(gobject.GObject):
+class FileTransferManager(P2PSessionHandler):
__gsignals__ = {
"transfer-requested" : (gobject.SIGNAL_RUN_FIRST,
@@ -323,42 +350,29 @@
}
def __init__(self, client):
- gobject.GObject.__init__(self)
- self._client = client
- self._sessions = {}
+ P2PSessionHandler.__init__(self, client)
def _can_handle_message(self, message):
euf_guid = message.body.euf_guid
return (euf_guid == EufGuid.FILE_TRANSFER)
- def _handle_message(self, peer, message):
+ def _handle_message(self, peer, guid, message):
session = FileTransferSession(self._client._p2p_session_manager,
- peer, message.body.application_id, message)
- self._connect_session(session)
+ peer, guid, message)
+ self._add_session(session)
self.emit("transfer-requested", session)
return session
+ ### Public API
+
def send(self, peer, filename, size):
- session = FileTransferSession(self._client._p2p_session_manager,
- peer, ApplicationID.FILE_TRANSFER)
+ session = FileTransferMetaSession(self._client, peer)
+ self._add_session(session)
session.invite(filename, size)
- self._connect_session(session)
return session
- def _on_transfer_completed(self, session, data):
- self._disconnect_session(session)
- del self._sessions[session]
-
- def _connect_session(self, session):
- handle_id = session.connect("completed", self._on_transfer_completed)
- self._sessions[session] = handle_id
-
- def _disconnect_session(self, session):
- handle_id = self._sessions[session]
- session.disconnect(handle_id)
-
-class WebcamHandler(gobject.GObject):
+class WebcamHandler(P2PSessionHandler):
__gsignals__ = {
"session-created" : (gobject.SIGNAL_RUN_FIRST,
@@ -367,9 +381,7 @@
}
def __init__(self, client):
- gobject.GObject.__init__(self)
- self._client = client
- self._sessions = []
+ P2PSessionHandler.__init__(self, client)
def _can_handle_message (self, message):
euf_guid = message.body.euf_guid
@@ -379,7 +391,7 @@
else:
return False
- def _handle_message (self, peer, message):
+ def _handle_message (self, peer, guid, message):
euf_guid = message.body.euf_guid
if (euf_guid == EufGuid.MEDIA_SESSION):
producer = False
@@ -387,19 +399,225 @@
producer = True
session = WebcamSession(producer, self._client._p2p_session_manager, \
- peer, message.body.euf_guid, message)
- self._sessions.append(session)
+ peer, guid, message.body.euf_guid, message)
+ self._add_session(session)
self.emit("session-created", session, producer)
return session
+ ### Public API
+
def invite(self, peer, producer=True):
- print "Creating New Send Session"
+ """Invite a contact for a uni-directionnal webcam session
+ @param producer: if true, we want to send webcam"""
+
+ session = WebcamMetaSession(self._client, peer, producer)
+ self._add_session(session)
+ session.invite()
+ return session
+
+
+class P2PMetaSession(gobject.GObject):
+ """ A P2PMetaSession is used to wrap multiple outgoing p2p sessions
+ together. This way, we can send a invite to each end point of a peer
+ and still have only one session object for methods and signals. """
+
+ __gsignals__ = {
+ "accepted" : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ()),
+ "rejected" : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ()),
+ "completed" : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
+ "progressed" : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
+ "disposed" : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ())
+ }
+
+ def __init__(self, client, peer, *args):
+ gobject.GObject.__init__(self)
+ self._sessions = []
+ self._handles = {}
+
+ if len(peer.end_points) == 0:
+ session = self._create_session(client, peer, None, *args)
+ self._add_session(session)
+ for end_point in peer.end_points.values():
+ if peer == client.profile and end_point.id == client.machine_guid:
+ continue
+ session = self._create_session(client, peer, end_point.id, *args)
+ self._add_session(session)
+
+ @property
+ def session(self):
+ if len(self._sessions) > 0:
+ return self._sessions[0]
+ return None
+
+ @property
+ def count(self):
+ return len(self._sessions)
+
+ def __getattr__(self, name):
+ if self.session:
+ return getattr(self.session, name)
+ raise AttributeError
+
+ def _connect_session(self, session):
+ handles = []
+ handles.append(session.connect("accepted",
+ self._on_session_accepted))
+ handles.append(session.connect("rejected",
+ self._on_session_rejected))
+ handles.append(session.connect("completed",
+ self._on_session_completed))
+ handles.append(session.connect("progressed",
+ self._on_session_progressed))
+ handles.append(session.connect("disposed",
+ self._on_session_disposed))
+ self._handles[session] = handles
+
+ def _disconnect_session(self, session):
+ handles = self._handles.pop(session, [])
+ for handle in handles:
+ session.disconnect(handle)
+
+ def _add_session(self, session):
+ if session is None:
+ return
+ self._connect_session(session)
+ self._sessions.append(session)
+
+ def _remove_all(self):
+ for session in self._sessions:
+ self._disconnect_session(session)
+ self._sessions = []
+
+ def _cancel_all(self):
+ sessions = self._sessions[:]
+ self._remove_all()
+ for session in sessions:
+ self._cancel_session(session)
+
+ def _keep_session(self, session_to_keep):
+ if session_to_keep not in self._sessions:
+ return False
+ logger.info("Keeping only session %s in meta session" %
+ session_to_keep.id)
+ self._sessions.remove(session_to_keep)
+ handles = self._handles.pop(session_to_keep)
+ self._cancel_all()
+ self._sessions = [session_to_keep]
+ self._handles[session_to_keep] = handles
+
+ def _on_session_accepted(self, session):
+ self._keep_session(session)
+ self.emit("accepted")
+
+ def _on_session_rejected(self, session):
+ self._keep_session(session)
+ self.emit("rejected")
+
+ def _on_session_completed(self, session, data):
+ self.emit("completed", data)
+
+ def _on_session_progressed(self, session, data):
+ self.emit("progressed", data)
+
+ def _on_session_disposed(self, disposed_session):
+ self._remove_all()
+ self.emit("disposed")
+
+
+class FileTransferMetaSession(P2PMetaSession):
+
+ __gsignals__ = {
+ "canceled" : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ())
+ }
+
+ def __init__(self, client, peer):
+ P2PMetaSession.__init__(self, client, peer)
+
+ def _create_session(self, client, peer, guid):
+ session = FileTransferSession(client._p2p_session_manager, peer, guid)
+ return session
+
+ def _connect_session(self, session):
+ P2PMetaSession._connect_session(self, session)
+ self._handles[session].append(session.connect("canceled",
+ self._on_session_canceled))
+
+ def _cancel_session(self, session):
+ session.cancel()
+
+ def _on_session_canceled(self, canceled_session):
+ self.emit("canceled")
+
+ def invite(self, filename, size):
+ for session in self._sessions:
+ session.invite(filename, size)
+
+ def cancel(self):
+ for session in self._sessions:
+ session.cancel()
+
+
+class MSNObjectMetaSession(P2PMetaSession):
+
+ def __init__(self, client, peer, application_id, context):
+ P2PMetaSession.__init__(self, client, peer, application_id, context)
+
+ def _create_session(self, client, peer, guid, application_id, context):
+ session = MSNObjectSession(client._p2p_session_manager, peer, guid,
+ application_id, context=context)
+ return session
+
+ def _cancel_session(self, session):
+ session.cancel()
+
+ def invite(self):
+ for session in self._sessions:
+ session.invite()
+
+ def cancel(self):
+ for session in self._sessions:
+ session.cancel()
+
+
+class WebcamMetaSession(P2PMetaSession):
+
+ def __init__(self, client, peer, producer):
+ P2PMetaSession.__init__(self, client, peer, producer)
+
+ def _create_session(self, client, peer, guid, producer):
+ if guid and peer.end_points[guid]:
+ has_webcam = peer.end_points[guid].capabilities.has_webcam
+ if not producer and not has_webcam:
+ return None
+
if producer:
euf_guid = EufGuid.MEDIA_SESSION
else:
euf_guid = EufGuid.MEDIA_RECEIVE_ONLY
- session = WebcamSession(producer, self._client._p2p_session_manager, \
- peer, euf_guid)
- self._sessions.append(session)
- session.invite()
+
+ session = WebcamSession(client._p2p_session_manager, peer, guid,
+ producer, euf_guid)
return session
+
+ def _cancel_session(self, session):
+ session.end()
+
+ def invite(self):
+ for session in self._sessions:
+ session.invite()
+
+ def end(self, reason=None):
+ for session in self._sessions:
+ session.end(reason)
--- papyon/profile.py
+++ papyon/profile.py
@@ -29,10 +29,13 @@
from papyon.util.decorator import rw_property
import gobject
+import logging
-__all__ = ['Profile', 'Contact', 'Group',
+__all__ = ['Profile', 'Contact', 'Group', 'EndPoint',
'Presence', 'Membership', 'ContactType', 'Privacy', 'NetworkID', 'ClientCapabilities']
+logger = logging.getLogger('papyon.profile')
+
class ClientCapabilities(gobject.GObject):
"""Capabilities of the client. This allow adverstising what the User Agent
@@ -168,7 +171,7 @@
_EXTRA = {
'supports_rtc_video': 0x00000010,
- 'unknown': 0x00000020
+ 'supports_p2pv2': 0x00000030
}
def __init__(self, msnc=0, client_id="0:0"):
@@ -336,91 +339,250 @@
the contact dropped it"""
-class Profile(gobject.GObject):
- """Profile of the User connecting to the service
+class ContactFlag(object):
+ """Internal contact flag"""
- @undocumented: __gsignals__, __gproperties__, do_get_property"""
+ EXTENDED_PRESENCE_KNOWN = 1
+ """Set once we receive the extended presence (UBX) for a buddy"""
+
+
+class BaseContact(gobject.GObject):
+
+ __gsignals__ = {
+ "end-point-added": (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
+
+ "end-point-removed": (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
+ }
__gproperties__ = {
+ "client-capabilities": (gobject.TYPE_STRING,
+ "Client capabilities",
+ "The client capabilities of the contact 's client",
+ "",
+ gobject.PARAM_READABLE),
+
+ "current-media": (gobject.TYPE_PYOBJECT,
+ "Current media",
+ "The current media that the user wants to display",
+ gobject.PARAM_READABLE),
+
"display-name": (gobject.TYPE_STRING,
"Friendly name",
"A nickname that the user chooses to display to others",
"",
gobject.PARAM_READABLE),
+ "end-points": (gobject.TYPE_PYOBJECT,
+ "End points",
+ "List of locations where the user is connected",
+ gobject.PARAM_READABLE),
+
+ "flags": (gobject.TYPE_UINT,
+ "Flags",
+ "Contact flags.",
+ 0, 1, 0, gobject.PARAM_READABLE),
+
+ "msn-object": (gobject.TYPE_STRING,
+ "MSN Object",
+ "MSN Object attached to the user, this generally represent "
+ "its display picture",
+ "",
+ gobject.PARAM_READABLE),
+
"personal-message": (gobject.TYPE_STRING,
"Personal message",
"The personal message that the user wants to display",
"",
gobject.PARAM_READABLE),
- "current-media": (gobject.TYPE_PYOBJECT,
- "Current media",
- "The current media that the user wants to display",
+ "presence": (gobject.TYPE_STRING,
+ "Presence",
+ "The presence to show to others",
+ Presence.OFFLINE,
gobject.PARAM_READABLE),
"signature-sound": (gobject.TYPE_PYOBJECT,
"Signature sound",
"The sound played by others' client when the user connects",
gobject.PARAM_READABLE),
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._client_capabilities = ClientCapabilities()
+ self._current_media = None
+ self._display_name = ""
+ self._end_points = {}
+ self._flags = 0
+ self._personal_message = ""
+ self._presence = Presence.OFFLINE
+ self._msn_object = None
+ self._signature_sound = None
+
+ @property
+ def account(self):
+ """Contact account
+ @rtype: utf-8 encoded string"""
+ return self._account
+
+ @property
+ def client_id(self):
+ """The user capabilities
+ @rtype: ClientCapabilities"""
+ return self._client_capabilities
+
+ @property
+ def client_capabilities(self):
+ """The user capabilities
+ @rtype: ClientCapabilities"""
+ return self._client_capabilities
+
+ @property
+ def current_media(self):
+ """Contact current media
+ @rtype: (artist: string, track: string)"""
+ return self._current_media
+
+ @property
+ def display_name(self):
+ """Contact display name
+ @rtype: utf-8 encoded string"""
+ return self._display_name
+
+ @property
+ def end_points(self):
+ """List of contact's locations
+ @rtype: list of string"""
+ return self._end_points
+
+ @property
+ def flags(self):
+ """Internal contact flags
+ @rtype: bitmask of L{Membership<papyon.profile.ContactFlag}s"""
+ return self._flags
+
+ @property
+ def id(self):
+ """Contact identifier in a GUID form
+ @rtype: GUID string"""
+ return self._id
+
+ @property
+ def msn_object(self):
+ """Contact MSN Object
+ @type: L{MSNObject<papyon.p2p.MSNObject>}"""
+ return self._msn_object
+
+ @property
+ def network_id(self):
+ """Contact network ID
+ @rtype: L{NetworkID<papyon.profile.NetworkID>}"""
+ return self._network_id
+
+ @property
+ def personal_message(self):
+ """Contact personal message
+ @rtype: utf-8 encoded string"""
+ return self._personal_message
+
+ @property
+ def presence(self):
+ """Contact presence
+ @rtype: L{Presence<papyon.profile.Presence>}"""
+ return self._presence
+
+ @property
+ def signature_sound():
+ """Contact signature sound
+ @type: string"""
+ return self._signature_sound
+
+ ### flags management
+ def has_flag(self, flags):
+ return (self.flags & flags) == flags
+
+ def _set_flags(self, flags):
+ logger.info("Set contact %s flags to %i" % (self._account, flags))
+ self._flags = flags
+ self.notify("flags")
+
+ def _add_flag(self, flag):
+ self._set_flags(self._flags | flag)
+
+ def _remove_flag(self, flag):
+ self._set_flags(self._flags & ~flag)
+
+ def _server_property_changed(self, name, value):
+ if name == "client-capabilities":
+ value = ClientCapabilities(client_id=value)
+ attr_name = "_" + name.lower().replace("-", "_")
+ old_value = getattr(self, attr_name)
+ if value != old_value:
+ setattr(self, attr_name, value)
+ self.notify(name)
+ if name == "end-points":
+ self._diff_end_points(old_value, value)
+
+ def _diff_end_points(self, old_eps, new_eps):
+ added_eps = set(new_eps.keys()) - set(old_eps.keys())
+ removed_eps = set(old_eps.keys()) - set(new_eps.keys())
+ for ep in added_eps:
+ self.emit("end-point-added", new_eps[ep])
+ for ep in removed_eps:
+ self.emit("end-point-removed", old_eps[ep])
+
+ def do_get_property(self, pspec):
+ name = pspec.name.lower().replace("-", "_")
+ return getattr(self, name)
+gobject.type_register(BaseContact)
+
+class Profile(BaseContact):
+ """Profile of the User connecting to the service"""
+
+ __gproperties__ = {
"profile": (gobject.TYPE_PYOBJECT,
"Profile",
"the text/x-msmsgsprofile sent by the server",
gobject.PARAM_READABLE),
- "presence": (gobject.TYPE_STRING,
- "Presence",
- "The presence to show to others",
- Presence.OFFLINE,
- gobject.PARAM_READABLE),
-
"privacy": (gobject.TYPE_STRING,
"Privacy",
"The privacy policy to use",
Privacy.BLOCK,
gobject.PARAM_READABLE),
-
- "msn-object": (gobject.TYPE_STRING,
- "MSN Object",
- "MSN Object attached to the user, this generally represent "
- "its display picture",
- "",
- gobject.PARAM_READABLE),
}
def __init__(self, account, ns_client):
- gobject.GObject.__init__(self)
+ BaseContact.__init__(self)
self._ns_client = ns_client
self._account = account[0]
self._password = account[1]
+ self._id = "00000000-0000-0000-0000-000000000000"
self._profile = ""
+ self._network_id = NetworkID.MSN
self._display_name = self._account.split("@", 1)[0]
- self._presence = Presence.OFFLINE
self._privacy = Privacy.BLOCK
- self._personal_message = ""
- self._current_media = None
- self._signature_sound = None
self._end_point_name = ""
- self._client_id = ClientCapabilities(10)
- self._client_id.supports_sip_invite = True
- #self.client_id.supports_tunneled_sip = True
- self._client_id.connect("capability-changed", self._client_capability_changed)
-
- self._msn_object = None
+ self._client_capabilities = ClientCapabilities(10)
+ self._client_capabilities.supports_sip_invite = True
+ self._client_capabilities.supports_tunneled_sip = True
+ self._client_capabilities.supports_p2pv2 = True
+ self._client_capabilities.p2p_bootstrap_via_uun = True
+ self._client_capabilities.connect("capability-changed",
+ self._client_capability_changed)
- self.__pending_set_presence = [self._presence, self._client_id, self._msn_object]
+ self.__pending_set_presence = [self._presence, self._client_capabilities, self._msn_object]
self.__pending_set_personal_message = [self._personal_message, self._current_media]
@property
- def account(self):
- """The user account
- @rtype: utf-8 encoded string"""
- return self._account
-
- @property
def password(self):
"""The user password
@rtype: utf-8 encoded string"""
@@ -432,18 +594,6 @@
@rtype: dict of fields"""
return self._profile
- @property
- def id(self):
- """The user identifier in a GUID form
- @rtype: GUID string"""
- return "00000000-0000-0000-0000-000000000000"
-
- @property
- def client_id(self):
- """The user capabilities
- @rtype: ClientCapabilities"""
- return self._client_id
-
@rw_property
def display_name():
"""The display name shown to you contacts
@@ -474,7 +624,7 @@
"""The default privacy, can be either Privacy.ALLOW or Privacy.BLOCK
@type: L{Privacy<papyon.profile.Privacy>}"""
def fset(self, privacy):
- pass #FIXME: set the privacy setting
+ self._ns_client.set_privacy(privacy)
def fget(self):
return self._privacy
return locals()
@@ -573,27 +723,18 @@
self._ns_client.send_url_request(('PROFILE', '0x0409'), callback)
def _client_capability_changed(self, client, name, value):
- self.__pending_set_presence[1] = self._client_id
+ self.__pending_set_presence[1] = self._client_capabilities
self._ns_client.set_presence(*self.__pending_set_presence)
def _server_property_changed(self, name, value):
- attr_name = "_" + name.lower().replace("-", "_")
- if attr_name == "_msn_object" and value is not None:
- value = self.__pending_set_presence[2]
- old_value = getattr(self, attr_name)
- if value != old_value:
- setattr(self, attr_name, value)
- self.notify(name)
-
- def do_get_property(self, pspec):
- name = pspec.name.lower().replace("-", "_")
- return getattr(self, name)
+ if name == "msn-object" and value is not None:
+ self.__pending_set_presence[2] = value
+ BaseContact._server_property_changed(self, name, value)
gobject.type_register(Profile)
-class Contact(gobject.GObject):
- """Contact related information
- @undocumented: __gsignals__, __gproperties__, do_get_property"""
+class Contact(BaseContact):
+ """Contact related information"""
__gsignals__ = {
"infos-changed": (gobject.SIGNAL_RUN_FIRST,
@@ -605,35 +746,7 @@
"memberships": (gobject.TYPE_UINT,
"Memberships",
"Membership relation with the contact.",
- 0, 15, 0, gobject.PARAM_READABLE),
-
- "display-name": (gobject.TYPE_STRING,
- "Friendly name",
- "A nickname that the user chooses to display to others",
- "",
- gobject.PARAM_READWRITE),
-
- "personal-message": (gobject.TYPE_STRING,
- "Personal message",
- "The personal message that the user wants to display",
- "",
- gobject.PARAM_READABLE),
-
- "current-media": (gobject.TYPE_PYOBJECT,
- "Current media",
- "The current media that the user wants to display",
- gobject.PARAM_READABLE),
-
- "signature-sound": (gobject.TYPE_PYOBJECT,
- "Signature sound",
- "The sound played by others' client when the user connects",
- gobject.PARAM_READABLE),
-
- "presence": (gobject.TYPE_STRING,
- "Presence",
- "The presence to show to others",
- Presence.OFFLINE,
- gobject.PARAM_READABLE),
+ 0, 31, 0, gobject.PARAM_READABLE),
"groups": (gobject.TYPE_PYOBJECT,
"Groups",
@@ -649,43 +762,23 @@
"Contact type",
"The contact automatic update status flag",
gobject.PARAM_READABLE),
-
- "client-capabilities": (gobject.TYPE_STRING,
- "Client capabilities",
- "The client capabilities of the contact 's client",
- "",
- gobject.PARAM_READABLE),
-
- "msn-object": (gobject.TYPE_STRING,
- "MSN Object",
- "MSN Object attached to the contact, this generally represent "
- "its display picture",
- "",
- gobject.PARAM_READABLE),
}
def __init__(self, id, network_id, account, display_name, cid=None,
memberships=Membership.NONE, contact_type=ContactType.REGULAR):
"""Initializer"""
- gobject.GObject.__init__(self)
+ BaseContact.__init__(self)
self._id = id or "00000000-0000-0000-0000-000000000000"
self._cid = cid or "00000000-0000-0000-0000-000000000000"
self._network_id = network_id
self._account = account
-
self._display_name = display_name
- self._presence = Presence.OFFLINE
- self._personal_message = ""
- self._current_media = None
- self._signature_sound = None
- self._groups = set()
+ self._attributes = {'icon_url' : None}
+ self._groups = set()
+ self._infos = {}
self._memberships = memberships
self._contact_type = contact_type
- self._client_capabilities = ClientCapabilities()
- self._msn_object = None
- self._infos = {}
- self._attributes = {'icon_url' : None}
def __repr__(self):
def memberships_str():
@@ -706,12 +799,6 @@
return template % (self._id, self._network_id, self._account, memberships_str())
@property
- def id(self):
- """Contact identifier in a GUID form
- @rtype: GUID string"""
- return self._id
-
- @property
def attributes(self):
"""Contact attributes
@rtype: {key: string => value: string}"""
@@ -724,48 +811,6 @@
return self._cid
@property
- def network_id(self):
- """Contact network ID
- @rtype: L{NetworkID<papyon.profile.NetworkID>}"""
- return self._network_id
-
- @property
- def account(self):
- """Contact account
- @rtype: utf-8 encoded string"""
- return self._account
-
- @property
- def presence(self):
- """Contact presence
- @rtype: L{Presence<papyon.profile.Presence>}"""
- return self._presence
-
- @property
- def display_name(self):
- """Contact display name
- @rtype: utf-8 encoded string"""
- return self._display_name
-
- @property
- def personal_message(self):
- """Contact personal message
- @rtype: utf-8 encoded string"""
- return self._personal_message
-
- @property
- def current_media(self):
- """Contact current media
- @rtype: (artist: string, track: string)"""
- return self._current_media
-
- @property
- def signature_sound():
- """Contact signature sound
- @type: string"""
- return self._signature_sound
-
- @property
def groups(self):
"""Contact list of groups
@rtype: set(L{Group<papyon.profile.Group>}...)"""
@@ -790,18 +835,6 @@
return self._contact_type
@property
- def client_capabilities(self):
- """Contact client capabilities
- @rtype: L{ClientCapabilities}"""
- return self._client_capabilities
-
- @property
- def msn_object(self):
- """Contact MSN Object
- @type: L{MSNObject<papyon.p2p.MSNObject>}"""
- return self._msn_object
-
- @property
def domain(self):
"""Contact domain, which is basically the part after @ in the account
@rtype: utf-8 encoded string"""
@@ -838,22 +871,9 @@
self.notify("memberships")
def _remove_membership(self, membership):
- """removes the given membership from the contact
-
- @param membership: the membership to remove
- @type membership: int L{Membership}"""
self._memberships ^= membership
self.notify("memberships")
- def _server_property_changed(self, name, value): #FIXME, should not be used for memberships
- if name == "client-capabilities":
- value = ClientCapabilities(client_id=value)
- attr_name = "_" + name.lower().replace("-", "_")
- old_value = getattr(self, attr_name)
- if value != old_value:
- setattr(self, attr_name, value)
- self.notify(name)
-
def _server_attribute_changed(self, name, value):
self._attributes[name] = value
@@ -866,6 +886,7 @@
self._id = "00000000-0000-0000-0000-000000000000"
self._cid = "00000000-0000-0000-0000-000000000000"
self._groups = set()
+ self._flags = 0
self._server_property_changed("presence", Presence.OFFLINE)
self._server_property_changed("display-name", self._account)
@@ -873,19 +894,15 @@
self._server_property_changed("current-media", None)
self._server_property_changed("msn-object", None)
self._server_property_changed("client-capabilities", "0:0")
+ self._server_property_changed("end-points", {})
self._server_infos_changed({})
-
### group management
def _add_group_ownership(self, group):
self._groups.add(group)
def _delete_group_ownership(self, group):
self._groups.discard(group)
-
- def do_get_property(self, pspec):
- name = pspec.name.lower().replace("-", "_")
- return getattr(self, name)
gobject.type_register(Contact)
@@ -930,3 +947,21 @@
name = pspec.name.lower().replace("-", "_")
return getattr(self, name)
gobject.type_register(Group)
+
+
+class EndPoint(object):
+ def __init__(self, id, caps):
+ self.id = id
+ self.capabilities = ClientCapabilities(client_id=caps)
+ self.name = ""
+ self.idle = False
+ self.state = ""
+ self.client_type = 0
+
+ def __eq__(self, endpoint):
+ return (self.id == endpoint.id and
+ self.capabilities == endpoint.capabilities and
+ self.name == endpoint.name and
+ self.idle == endpoint.idle and
+ self.state == endpoint.state and
+ self.client_type == endpoint.client_type)
--- papyon/service/AddressBook/address_book.py
+++ papyon/service/AddressBook/address_book.py
@@ -139,6 +139,10 @@
gobject.TYPE_NONE,
(object,)),
+ "contact-pending" : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
+
"contact-deleted" : (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE,
(object,)),
@@ -265,10 +269,14 @@
contact = profile.Contact(None, network_id, account, account)
return contact
- def check_pending_invitations(self):
+ def check_pending_invitations(self, done_cb=None, failed_cb=None):
+ def callback(memberships):
+ self.__update_memberships(memberships)
+ self.__common_callback('contact-pending', done_cb,
+ self.contacts.search_by_memberships(Membership.PENDING))
cp = scenario.CheckPendingInviteScenario(self._sharing,
- (self.__update_memberships,),
- (self.__common_errback, None))
+ (callback,),
+ (self.__common_errback, failed_cb))
cp()
def accept_contact_invitation(self, pending_contact, add_to_contact_list=True,
--- papyon/service/AddressBook/scenario/contacts/check_pending_invite.py
+++ papyon/service/AddressBook/scenario/contacts/check_pending_invite.py
@@ -44,5 +44,10 @@
def __membership_findall_errback(self, error_code):
errcode = AddressBookError.UNKNOWN
+ if error_code == 'FullSyncRequired':
+ self.__sharing.FindMembership((self.__membership_findall_callback,),
+ (self.__membership_findall_errback,),
+ self._scenario, ['Messenger'], False)
+ return
self.errback(errcode)
--- papyon/service/ContentRoaming/scenario/get_stored_profile.py
+++ papyon/service/ContentRoaming/scenario/get_stored_profile.py
@@ -40,18 +40,19 @@
self.__storage.GetProfile((self.__get_profile_callback,),
(self.__get_profile_errback,),
self._scenario, self.cid,
- True, True, True, True, True, True,
+ True, True, True, True, True, True,
True, True, True, True, True)
- def __get_profile_callback(self, profile_rid, expression_profile_rid,
- display_name, personal_msg, photo_rid,
- photo_mime_type, photo_data_size, photo_url):
+ def __get_profile_callback(self, profile_rid, expression_profile_rid,
+ display_name, personal_msg, user_tile_url,
+ photo_rid, photo_mime_type, photo_data_size,
+ photo_url):
callback = self._callback
- callback[0](profile_rid, expression_profile_rid, display_name,
+ callback[0](profile_rid, expression_profile_rid, display_name,
personal_msg, photo_rid, *callback[1:])
if photo_rid is not None:
- self.__storage.get_display_picture(photo_url,
+ self.__storage.get_display_picture(photo_url, user_tile_url,
(self.__get_display_picture_callback,),
(self.__get_display_picture_errback,))
@@ -71,4 +72,3 @@
errback = self._errback[0]
args = self._errback[1:]
errback(errcode, *args)
-
--- papyon/service/ContentRoaming/storage.py
+++ papyon/service/ContentRoaming/storage.py
@@ -22,6 +22,8 @@
from papyon.gnet.protocol import ProtocolFactory
+import urllib
+
__all__ = ['Storage']
class Storage(SOAPService):
@@ -58,6 +60,7 @@
display_name = expression_profile.findtext('./st:DisplayName')
personal_msg = expression_profile.findtext('./st:PersonalStatus')
+ user_tile_url = expression_profile.findtext('./st:StaticUserTilePublicURL')
photo = expression_profile.find('./st:Photo')
if photo is not None:
@@ -70,7 +73,7 @@
photo_rid = photo_mime_type = photo_data_size = photo_url = None
callback[0](profile_rid, expression_profile_rid, display_name, personal_msg,
- photo_rid, photo_mime_type, photo_data_size, photo_url,
+ user_tile_url, photo_rid, photo_mime_type, photo_data_size, photo_url,
*callback[1:])
def UpdateProfile(self, callback, errback, scenario, profile_rid,
@@ -126,28 +129,35 @@
self._soap_request(method, (scenario, token), args, callback, errback)
@RequireSecurityTokens(LiveService.STORAGE)
- def get_display_picture(self, pre_auth_url, callback, errback):
+ def get_display_picture(self, pre_auth_url, user_tile_url, callback, errback):
token = str(self._tokens[LiveService.STORAGE])
- scheme = 'http'
- host = 'byfiles.storage.msn.com'
- port = 80
- resource = '?'.join([pre_auth_url, token.split('&')[0]])
-
- def request_callback(transport, http_response):
- type = http_response.get_header('Content-Type')#.split('/')[1]
+ def success(transport, http_response):
+ type = http_response.get_header('Content-Type')
data = http_response.body
callback[0](type, data, *callback[1:])
+ def total_fail(transport, error_code):
+ errback[0](*errback[1:])
+
+ def request_static_tile(transport, error_code):
+ # Request using the PreAuthURL didn't work, try with static tilephoto
+ scheme, host, port, resource = url_split(user_tile_url)
+ self.get_resource(scheme, host, resource, success, total_fail)
+
+ scheme, host, port, resource = url_split(pre_auth_url)
+ resource += '?t=' + urllib.quote(token.split('&')[0][2:])
+ self.get_resource(scheme, host, resource, success, request_static_tile)
+
+ def get_resource(self, scheme, host, resource, callback, errback):
http_headers = {}
http_headers["Accept"] = "*/*"
http_headers["Proxy-Connection"] = "Keep-Alive"
http_headers["Connection"] = "Keep-Alive"
- proxy = self._proxies.get(scheme, None)
- transport = ProtocolFactory(scheme, host, port, proxy=proxy)
- transport.connect("response-received", request_callback)
+ transport = ProtocolFactory(scheme, host, proxies=self._proxies)
+ transport.connect("response-received", callback)
transport.connect("request-sent", self._request_handler)
- transport.connect("error", errback[0], *errback[1:])
+ transport.connect("error", errback)
transport.request(resource, http_headers, method='GET')
--- papyon/service/OfflineIM/offline_messages_box.py
+++ papyon/service/OfflineIM/offline_messages_box.py
@@ -24,9 +24,11 @@
from papyon.service.SOAPUtils import *
from papyon.service.OfflineIM.constants import *
+from papyon.msnp import Message
from papyon.profile import NetworkID
from papyon.util.decorator import throttled
+from papyon.util.encoding import decode_rfc2047_string
import papyon.util.element_tree as ElementTree
import papyon.util.string_io as StringIO
@@ -270,13 +272,7 @@
# Get the name of the sender
name = m.findtext('./N');
- # Decode it according to RFC 2047
- from email.header import decode_header
- parts = decode_header(name)
- name = ''
- for part in parts:
- name += part[0].decode(part[1]) if part[1] else part[0]
-
+ name = decode_rfc2047_string(name)
date = m.find('./RT')
if date is not None:
date = date.text
@@ -288,6 +284,14 @@
if len(self.__messages) > 0:
self.emit('messages-received', self.__messages)
+ def __send_unmanaged_message(self, recipient, body):
+ """ Send offline message through a UUM command when using MSNP18. """
+ message = Message(self._client.profile)
+ message.content_type = ("text/plain", "utf-8")
+ message.add_header("Dest-Agent", "client")
+ message.body = body
+ self._client._protocol.send_unmanaged_message(recipient, message)
+
# Public API
def fetch_messages(self, messages=None):
if messages is None:
@@ -303,8 +307,12 @@
fm.message_ids = [m.id for m in messages]
fm()
- @throttled(1000, list())
+ @throttled(1, list())
def send_message(self, recipient, message):
+ if self._client.protocol_version >= 18:
+ self.__send_unmanaged_message(recipient, message)
+ return
+
if recipient.network_id == NetworkID.EXTERNAL:
return
--- papyon/service/SOAPService.py
+++ papyon/service/SOAPService.py
@@ -220,9 +220,7 @@
return #FIXME: propagate the error up
if not soap_response.is_fault():
- handler = getattr(self,
- "_Handle" + request_id + "Response",
- None)
+ handler = getattr(self, "_Handle" + request_id + "Response", None)
method = getattr(self._service, request_id)
response = method.process_response(soap_response)
@@ -232,9 +230,7 @@
self._HandleSOAPResponse(request_id, callback, errback,
response, user_data)
else:
- handler = getattr(self,
- "_Handle" + request_id + "Fault",
- None)
+ handler = getattr(self, "_Handle" + request_id + "Fault", None)
if handler is not None:
handler(callback, errback, soap_response, user_data)
else:
@@ -270,11 +266,10 @@
transport = trans[0]
trans[1].append((request_id, callback, errback, user_data))
else:
- proxy = self._proxies.get(scheme, None)
transport = papyon.gnet.protocol.ProtocolFactory(scheme,
- host, port, proxy=proxy)
- handler_id = [transport.connect("response-received",
- self._response_handler),
+ host, port, proxies=self._proxies)
+ handler_id = [
+ transport.connect("response-received", self._response_handler),
transport.connect("request-sent", self._request_handler),
transport.connect("error", self._error_handler)]
--- papyon/service/SingleSignOn.py
+++ papyon/service/SingleSignOn.py
@@ -115,6 +115,10 @@
class SingleSignOn(SOAPService):
def __init__(self, username, password, proxies=None):
+ # Passwords can only be up to 16 characters in length
+ if len(password) > 16:
+ password = password[0:16]
+
self.__credentials = (username, password)
self.__storage = {}
--- papyon/service/description/AB/common.py
+++ papyon/service/description/AB/common.py
@@ -25,7 +25,7 @@
return """
<ABApplicationHeader xmlns="http://www.msn.com/webservices/AddressBook">
- <ApplicationId xmlns="http://www.msn.com/webservices/AddressBook">996CDE1E-AA53-4477-B943-2BE802EA6166</ApplicationId>
+ <ApplicationId xmlns="http://www.msn.com/webservices/AddressBook">CFE80F9D-180F-4399-82AB-413F33A1FA11</ApplicationId>
<IsMigration xmlns="http://www.msn.com/webservices/AddressBook">false</IsMigration>
<PartnerScenario xmlns="http://www.msn.com/webservices/AddressBook">%s</PartnerScenario>
</ABApplicationHeader>
--- papyon/service/description/Sharing/common.py
+++ papyon/service/description/Sharing/common.py
@@ -27,7 +27,7 @@
return """
<ABApplicationHeader xmlns="http://www.msn.com/webservices/AddressBook">
- <ApplicationId xmlns="http://www.msn.com/webservices/AddressBook">996CDE1E-AA53-4477-B943-2BE802EA6166</ApplicationId>
+ <ApplicationId xmlns="http://www.msn.com/webservices/AddressBook">CFE80F9D-180F-4399-82AB-413F33A1FA11</ApplicationId>
<IsMigration xmlns="http://www.msn.com/webservices/AddressBook">false</IsMigration>
<PartnerScenario xmlns="http://www.msn.com/webservices/AddressBook">%s</PartnerScenario>
</ABApplicationHeader>
--- papyon/service/description/SingleSignOn/RequestMultipleSecurityTokens.py
+++ papyon/service/description/SingleSignOn/RequestMultipleSecurityTokens.py
@@ -21,7 +21,7 @@
import xml.sax.saxutils as xml
class LiveService(object):
- CONTACTS = ("contacts.msn.com", "?fs=1&id=24000&kv=7&rn=93S9SWWw&tw=0&ver=2.1.6000.1")
+ CONTACTS = ("contacts.msn.com", "MBI")
MESSENGER = ("messenger.msn.com", "?id=507")
MESSENGER_CLEAR = ("messengerclear.live.com", "MBI_KEY_OLD")
MESSENGER_SECURE = ("messengersecure.live.com", "MBI_SSL")
--- papyon/sip/__init__.py
+++ papyon/sip/__init__.py
@@ -18,4 +18,4 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-from connection_manager import SIPConnectionManager
+from call_manager import SIPCallManager
--- papyon/sip/call.py
+++ papyon/sip/call.py
@@ -2,7 +2,7 @@
#
# papyon - a python client library for Msn
#
-# Copyright (C) 2009 Collabora Ltd.
+# Copyright (C) 2009-2010 Collabora Ltd.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -19,605 +19,278 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
from papyon.event import EventsDispatcher
-from papyon.media import MediaCall, MediaSessionType
-from papyon.profile import Presence
+from papyon.media import MediaCall, MediaSessionType, RTCActivity
+from papyon.profile import NetworkID, Presence
from papyon.service.SingleSignOn import *
from papyon.sip.constants import *
-from papyon.sip.message import SIPRequest, SIPResponse
from papyon.sip.sdp import SDPMessage
-from papyon.sip.turn import TURNClient
+from papyon.util.timer import Timer
-import base64
import gobject
import logging
-import re
-import uuid
-__all__ = ['SIPCall', 'SIPRegistration']
+__all__ = ['SIPCall']
logger = logging.getLogger('papyon.sip.call')
+class SIPCall(gobject.GObject, MediaCall, RTCActivity, EventsDispatcher, Timer):
-class SIPBaseCall(gobject.GObject):
- """Base class representing a transaction between two entities. A transaction
- between an user and a registrar is a SIPRegistration and a transaction
- between two SIP end points is a SIPCall. This class mainly contains
- utility functions to build and parse request/responses."""
+ __gsignals__ = {
+ 'ended': (
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ())
+ }
- def __init__(self, connection, client, id=None):
+ def __init__(self, client, core, id, peer=None, invite=None):
gobject.GObject.__init__(self)
- self._connection = connection
+ MediaCall.__init__(self, MediaSessionType.TUNNELED_SIP)
+ RTCActivity.__init__(self, client)
+ EventsDispatcher.__init__(self)
+ Timer.__init__(self)
+
self._client = client
- self._ip = "127.0.0.1"
- self._port = 50390
- self._transport_protocol = connection.transport.protocol
- self._account = client.profile.account
+ self._core = core
self._id = id
- self._cseq = 0
- self._remote = None
- self._route = None
- self._uri = None
- self._timeout_sources = {}
+ self._peer = peer
+ self._invite = invite
- @property
- def id(self):
- if not self._id:
- self._id = uuid.uuid4().get_hex()
- return self._id
+ self._incoming = (invite is not None)
+ self._ringing = False
+ self._pending_invite = False
+ self._pending_cancel = False
- def get_cseq(self, incr=False):
- if incr:
- self._cseq += 1
- return self._cseq
-
- def get_epid(self):
- if not hasattr(self, '_epid'):
- self._epid = uuid.uuid4().get_hex()[:10]
- return self._epid
-
- def get_mepid(self):
- if self._connection.tunneled:
- mepid = self._connection._client.machine_guid
- mepid = filter(lambda c: c not in "{-}", mepid).upper()
- return ";mepid=%s" % mepid
- else:
- return ""
+ self._dialogs = []
+ self._handles = {}
- def get_tag(self):
- if not hasattr(self, '_tag'):
- self._tag = uuid.uuid4().get_hex()
- return self._tag
-
- def get_sip_instance(self):
- return SIP_INSTANCE
-
- def send(self, message, registration=False):
- message.call = self
- self._connection.send(message, registration)
-
- def find_contact(self, email):
- contacts = self._client.address_book.contacts.search_by_account(email)
- if not contacts:
- return None
- return contacts[0]
+ if self._incoming:
+ self._build_from_invite(invite)
- def parse_contact(self, message):
- if type(message) is SIPRequest:
- header = "From"
- elif type(message) is SIPResponse:
- header = "To"
- else:
- return None
- email = self.parse_email(message, header)
- return self.find_contact(email)
- def parse_email(self, message, name):
- header = message.get_header(name)
- if header is not None:
- return re.search("<sip:([^;>]*)(;|>)", header).group(1)
-
- def parse_uri(self, message, name):
- header = message.get_header(name)
- if header is not None:
- return re.search("<([^>]*)>", header).group(1)
-
- def parse_sip(self, message, name):
- header = message.get_header(name)
- if header is not None:
- return re.search("<sip:[^>]*>", header).group(0)
-
- def build_from_header(self, name="0"):
- return '"%s" <sip:%s%s>;tag=%s;epid=%s' % \
- (name, self._account, self.get_mepid(), self.get_tag(),
- self.get_epid())
-
- def build_request(self, code, uri, to, name="0", incr=False):
- request = SIPRequest(code, uri)
- request.add_header("Via", "SIP/2.0/%s %s:%s" %
- (self._transport_protocol, self._ip, self._port))
- request.add_header("Max-Forwards", 70)
- request.add_header("Call-ID", self.id)
- request.add_header("CSeq", "%i %s" % (self.get_cseq(incr), code))
- request.add_header("To", to)
- request.add_header("From", self.build_from_header(name))
- request.add_header("User-Agent", USER_AGENT)
- return request
-
- def build_response(self, request, status, reason=None):
- response = SIPResponse(status, reason)
- response.clone_headers("From", request)
- response.add_header("To", self.build_from_header())
- response.clone_headers("CSeq", request)
- response.clone_headers("Record-Route", request)
- response.clone_headers("Via", request)
- response.add_header("Call-ID", self.id)
- response.add_header("Max-Forwards", 70)
- response.add_header("User-Agent", USER_AGENT)
- return response
-
- def on_message_received(self, msg):
- route = self.parse_sip(msg, "Record-Route")
- if route is not None:
- self._route = route
- uri = self.parse_uri(msg, "Contact")
- if uri is not None:
- self._uri = uri
-
- if type(msg) is SIPResponse:
- self._remote = msg.get_header("To")
- handler_name = "on_%s_response" % msg.code.lower()
- elif type(msg) is SIPRequest:
- self._remote = msg.get_header("From")
- handler_name = "on_%s_received" % msg.code.lower()
- handler = getattr(self, handler_name, None)
- if handler is not None:
- handler(msg)
- else:
- logger.warning("Unhandled %s message" % msg.code)
+ # Public API -------------------------------------------------------------
@property
- def timeouts(self):
- return self._timeout_sources.keys()
-
- def start_timeout(self, name, time):
- self.stop_timeout(name)
- source = gobject.timeout_add(time * 1000, self.on_timeout, name)
- self._timeout_sources[name] = source
-
- def stop_timeout(self, name):
- source = self._timeout_sources.get(name, None)
- if source is not None:
- gobject.source_remove(source)
- del self._timeout_sources[name]
-
- def stop_all_timeout(self):
- for (name, source) in self._timeout_sources.items():
- if source is not None:
- gobject.source_remove(source)
- self._timeout_sources.clear()
-
- def on_timeout(self, name):
- self.stop_timeout(name)
- handler = getattr(self, "on_%s_timeout" % name, None)
- if handler is not None:
- handler()
-
-
-class SIPCall(SIPBaseCall, MediaCall, EventsDispatcher):
- """Represent a SIP dialog between two end points. A call must be initiated
- by sending or receiving an INVITE request. It is disposed when
- receiving or by sending a CANCEL or BYE request.
-
- For a more complete description of the transactions, see the SIP
- standard memo (RFC 3261) : http://tools.ietf.org/html/rfc3261"""
-
- def __init__(self, connection, client, peer=None, invite=None, id=None):
- session_type = connection.tunneled and MediaSessionType.TUNNELED_SIP \
- or MediaSessionType.SIP
- SIPBaseCall.__init__(self, connection, client, id)
- MediaCall.__init__(self, session_type)
- EventsDispatcher.__init__(self)
-
- self._incoming = (id is not None)
- self._accepted = False
- self._rejected = False
- self._answer_sent = False
- self._early = False
- self._state = None
- self._relay_requested = False
-
- if peer is None and invite is not None:
- peer = self.parse_contact(invite)
- self._peer = peer
- self._invite = invite
+ def id(self):
+ return self._id
@property
def peer(self):
return self._peer
@property
- def conversation_id(self):
- if self.media_session.has_video:
- return 1
- else:
- return 0
+ def incoming(self):
+ return self._incoming
@property
- def answered(self):
- return (self._accepted or self._rejected) and self._answer_sent
+ def peer_uri(self):
+ return "sip:" + self._peer.account
@property
- def incoming(self):
- return self._incoming
-
- def build_invite_contact(self):
- if self._connection.tunneled:
- m = "<sip:%s%s>;proxy=replace;+sip.instance=\"<urn:uuid:%s>\"" % (
- self._account, self.get_mepid(), self.get_sip_instance())
- else:
- m = "<sip:%s:%i;maddr=%s;transport=%s>;proxy=replace" % (
- self._account, self._port, self._ip, self._transport_protocol)
- return m
-
- def build_invite_request(self, uri, to):
- message = SDPMessage(session=self.media_session)
- request = self.build_request("INVITE", uri, to, incr=True)
- request.add_header("Ms-Conversation-ID", "f=%s" % self.conversation_id)
- request.add_header("Contact", self.build_invite_contact())
- request.set_content(str(message), "application/sdp")
- return request
+ def can_answer(self):
+ if not self._incoming:
+ logger.warning("Can't answer to outgoing call %s" % self._id)
+ return False
+ if self._dialog is None:
+ logger.error("No dialog for the incoming call %s" % self._id)
+ return False
+ if self._dialog.answered:
+ logger.warning("Call %s has already been answered" % self._id)
+ return False
+ return True
def invite(self):
- if not self.media_session.prepared:
- return
- logger.info("Send call invitation to %s", self._peer.account)
- self._state = "CALLING"
- self._early = False
- self._uri = "sip:%s" % self._peer.account
- self._remote = "<%s>" % self._uri
- self._invite = self.build_invite_request(self._uri, self._remote)
- self.start_timeout("invite", 30)
- self.send(self._invite)
+ """Send the invitation to the peer. Invite is sent by the UA Core
+ because dialogs are created when receiving response."""
- def reinvite(self):
- if self._incoming or not self.media_session.ready:
+ if self._incoming or self._invite:
+ return
+ if not self.media_session.prepared:
+ self._pending_invite = True
return
- self._state = "REINVITING"
- self._invite = self.build_invite_request(self._uri, self._remote)
- self._invite.add_header("Route", self._route)
- self._invite.add_header("Supported", "ms-dialog-route-set-update")
- self.start_timeout("invite", 10)
- self.send(self._invite)
-
- def answer(self, status):
- response = self.build_response(self._invite, status)
- if status == 200:
- message = SDPMessage(session=self.media_session)
- response.add_header("Contact", self.build_invite_contact())
- response.set_content(str(message), "application/sdp")
- self.send(response)
+ logger.info("Send call invitation to %s" % self._peer.account)
+ offer = SDPMessage(session=self.media_session)
+ self._pending_invite = False
+ self._invite = self._core.invite(self._id, self.peer_uri, offer)
def ring(self):
- if self._invite is None :
+ if not self.can_answer:
return
+ if self._ringing:
+ return
+ self._ringing = True
self.start_timeout("response", 50)
- self.answer(180)
+ self._dialog.ring()
self._dispatch("on_call_incoming")
def accept(self):
- if self.answered:
- return
- self._accepted = True
- if not self.media_session.prepared:
+ if not self.can_answer:
return
self.stop_timeout("response")
- self.start_timeout("ack", 5)
- self._answer_sent = True
- self.answer(200)
+ self._dialog.accept()
+ self._accept_activity()
def reject(self, status=603):
- if self.answered:
+ if not self.can_answer:
return
- self._state = "DISCONNECTING"
self.stop_timeout("response")
- self.start_timeout("ack", 5)
- self._rejected = True
- self._answer_sent = True
- self.answer(status)
-
- def reaccept(self):
- if not self.media_session.ready:
- return
- self._state = "CONFIRMED"
- self.answer(200)
-
- def send_ack(self, response):
- request = self.build_request("ACK", self._uri, self._remote)
- request.add_header("Route", self._route)
- self.send(request)
+ self._dialog.reject(status)
+ self._decline_activity()
def end(self):
- if self._state in ("INCOMING"):
- self.reject()
- elif self._state in ("CALLING", "REINVITING"):
- self.cancel()
- elif self._peer.presence == Presence.OFFLINE:
- self.force_dispose()
- else:
- self.send_bye()
+ if self._peer.presence == Presence.OFFLINE:
+ self._dispose()
- def cancel(self):
- if self._state not in ("CALLING", "REINVITING"):
- return
+ if not self._invite:
+ self._dispose()
+ elif not self._dialogs:
+ self._pending_cancel = True
+ else:
+ for dialog in self._dialogs:
+ dialog.end()
+
+ ### Messages Handling (Outside Dialog) -----------------------------------
+
+ def handle_invite_request(self, invite):
+ logger.info("Establish new UAS dialog to handle INVITE request")
+ dialog = self._core.establish_UAS_dialog(invite, 100)
+ self._add_dialog(dialog)
+ return dialog.handle_request(invite)
+
+ def handle_invite_response(self, response):
+ logger.info("Establish new UAC dialog to handle INVITE response")
+ response.request = self._invite
+ dialog = self._core.establish_UAC_dialog(response)
+ self._add_dialog(dialog)
+ return dialog.handle_response(response)
- if self._state == "CALLING":
- if self._invite is None:
- self.force_dispose()
- return
- elif not self._invite.sent:
- self._invite.cancelled = True
- self.force_dispose()
- return
-
- self._state = "DISCONNECTING"
- request = self.build_request("CANCEL", self._invite.uri, None)
- request.clone_headers("To", self._invite)
- request.clone_headers("Route", self._invite)
- self.start_timeout("cancel", 5)
- self.send(request)
+ ### Private API ----------------------------------------------------------
- def send_bye(self):
- if self._state == "DISCONNECTING":
- return
-
- self._state = "DISCONNECTING"
- request = self.build_request("BYE", self._uri, self._remote, incr=True)
- request.add_header("Route", self._route)
- self.start_timeout("bye", 5)
- self.send(request)
+ @property
+ def _dialog(self):
+ if len(self._dialogs) == 0:
+ return None
+ return self._dialogs[0]
- def force_dispose(self):
- self._state = "DISCONNECTING"
- self.stop_all_timeout()
- self.dispose()
+ def _build_from_invite(self, invite):
+ account = invite.contact.uri.replace("sip:", "")
+ #FIXME hack
+ if ";mepid=" in account:
+ account = account.split(";mepid=")[0]
+ self._peer = self._client.address_book.search_or_build_contact(account,
+ NetworkID.MSN)
- def dispose(self):
- if self.timeouts:
- return # we have to wait some responses
+ def _dispose(self):
+ logger.info("Call has been ended")
MediaCall.dispose(self)
- self._state = "DISCONNECTED"
+ self.stop_all_timeout()
+ self.emit("ended")
self._dispatch("on_call_ended")
- self._connection.remove_call(self)
-
- def on_invite_received(self, invite):
- self._invite = invite
- self.answer(100)
-
- try:
- message = SDPMessage(body=invite.body)
- except:
- logger.error("Malformed body in incoming call invitation")
- self.reject(488)
- return
-
- if self._state is None:
- self._state = "INCOMING"
- self.start_timeout("response", 50)
- self.media_session.process_remote_message(message, True)
- elif self._state == "CONFIRMED":
- self._state = "REINVITED"
- self.media_session.process_remote_message(message, False)
- self.reaccept()
- else:
- self.answer(488) # not acceptable here
-
- def on_ack_received(self, ack):
- self.stop_timeout("ack")
- if self._rejected:
- self.dispose()
- else:
- self._state = "CONFIRMED"
+ self._dispose_activity()
- def on_cancel_received(self, cancel):
- if self.incoming and not self.answered:
- self.reject(487)
- response = self.build_response(cancel, 200)
- self.send(response)
- self.dispose()
-
- def on_bye_received(self, bye):
- response = self.build_response(bye, 200)
- self.send(response)
- self.dispose()
-
- def on_invite_response(self, response):
- if self._state == "REINVITING":
- return self.on_reinvite_response(response)
- elif self._incoming:
- return
+ ### Dialogs Handling -----------------------------------------------------
- self._remote = response.get_header("To")
- if response.status >= 200:
- self.send_ack(response)
- self.stop_timeout("invite")
-
- if response.status is 100:
- self._early = True
- elif response.status is 180:
+ def _add_dialog(self, dialog):
+ """Add dialog to call"""
+ self._connect_dialog(dialog)
+ self._dialogs.append(dialog)
+
+ # We were waiting for a response to cancel...
+ if self._pending_cancel:
+ self.end()
+
+ def _keep_dialog(self, dialog_to_keep):
+ """Remove all dialogs except the one."""
+ if dialog_to_keep not in self._dialogs:
+ return False
+ self._dialogs.remove(dialog_to_keep)
+ handles = self._handles.pop(dialog_to_keep)
+ self._remove_all_dialogs()
+ self._dialogs = [dialog_to_keep]
+ self._handles[dialog_to_keep] = handles
+ return True
+
+ def _remove_all_dialogs(self):
+ """Remove and disconnect all dialogs."""
+ for dialog in self._dialogs:
+ self._disconnect_dialog(dialog)
+ self._dialogs = []
+
+ def _connect_dialog(self, dialog):
+ handles = []
+ handles.append(dialog.connect("ringing", self._on_dialog_ringing))
+ handles.append(dialog.connect("accepted", self._on_dialog_accepted))
+ handles.append(dialog.connect("rejected", self._on_dialog_rejected))
+ handles.append(dialog.connect("ended", self._on_dialog_ended))
+ handles.append(dialog.connect("offer-received", self._on_offer_received))
+ self._handles[dialog] = handles
+
+ def _disconnect_dialog(self, dialog):
+ if not dialog in self._handles:
+ return
+ handles = self._handles.pop(dialog)
+ for handle in handles:
+ dialog.disconnect(handle)
+
+ def _on_dialog_ringing(self, dialog):
+ if not self._ringing:
+ logger.info("Call is ringing")
+ self._ringing = True
self._dispatch("on_call_ringing")
- elif response.status is 200:
- self._state = "CONFIRMED"
- try:
- message = SDPMessage(body=response.body)
- self.media_session.process_remote_message(message)
- except Exception, err:
- logger.error("Malformed body in invite response")
- logger.exception(err)
- self.send_bye()
- else:
- logger.info("Call invitation has been accepted")
- self._dispatch("on_call_accepted")
- self.reinvite()
- elif response.status in (408, 480, 486, 487, 504, 603):
- logger.info("Call invitation has been rejected (%i)", response.status)
- self._dispatch("on_call_rejected", response)
- self.dispose()
- else:
- self._dispatch("on_call_error", response)
- self.send_bye()
-
- def on_reinvite_response(self, response):
- if response.status >= 200:
- self.send_ack(response)
- self.stop_timeout("invite")
-
- if response.status in (100, 180):
- pass
- elif response.status in (200, 488):
- self._state = "CONFIRMED"
- else:
- self.send_bye()
- def on_cancel_response(self, response):
- self.stop_timeout("cancel")
- self.dispose()
-
- def on_bye_response(self, response):
- self.stop_timeout("bye")
- self.dispose()
-
- def on_media_session_prepared(self, session):
- if self._state is None:
- self.invite()
- elif self._state == "INCOMING" and self._accepted:
- self.accept()
+ def _on_dialog_accepted(self, dialog, response):
+ logger.info("Call invitation has been accepted")
+ # ignore other call dialogs
+ if self._keep_dialog(dialog):
+ self._dispatch("on_call_accepted")
+
+ def _on_dialog_rejected(self, dialog, response):
+ if response.status/100 == 6: # global rejection
+ # we don't have any chance to receive a positive answer
+ # keep that dialog for now, it will be disposed soon
+ self._keep_dialog(dialog)
+
+ remaining = len(self._dialogs) - 1
+ logger.info("Call invitation has been rejected (%i)", response.status)
+ logger.info("%i pending dialog(s) remaining for that call" % remaining)
+ if remaining <= 0:
+ self._dispatch("on_call_rejected", response)
- def on_media_session_ready(self, session):
- if self._state == "REINVITED":
- self.reaccept()
- elif self._state == "CONFIRMED":
- self.reinvite()
+ def _on_dialog_ended(self, dialog):
+ self._disconnect_dialog(dialog)
+ self._dialogs.remove(dialog)
+ if len(self._dialogs) == 0:
+ self._dispose()
- def on_invite_timeout(self):
- self.cancel()
+ ### Timer Callbacks ------------------------------------------------------
def on_response_timeout(self):
- self.reject(408)
+ self.reject(408) #FIXME
self._dispatch("on_call_missed")
- def on_ack_timeout(self):
- self.dispose()
+ ### Media Session Callbacks ----------------------------------------------
- def on_cancel_timeout(self):
- self.dispose()
-
- def on_bye_timeout(self):
- self.dispose()
-
- def on_end_timeout(self):
- self.dispose()
-
- def request_turn_relays(self, streams_count):
- # FIXME Request TURN relays before to send an invite or to accept one
- turn_client = TURNClient(self._client._sso, self._account)
- turn_client.connect("done", self.on_turn_relays_discovered)
- turn_client.request_shared_secret(None, None, streams_count * 2)
-
- def on_turn_relays_discovered(self, turn_client, relays):
- logger.debug("Discovered %i TURN relays" % len(relays))
-
-
-class SIPRegistration(SIPBaseCall):
-
- __gsignals__ = {
- 'registered': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ([])),
- 'unregistered': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ([])),
- 'failed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ([]))
- }
-
- def __init__(self, connection, client):
- SIPBaseCall.__init__(self, connection, client)
- self._state = "NEW"
- self._sso = client._sso
- self._src = None
- self._request = None
- self._pending_unregister = False
- self._tokens = {}
-
- @property
- def registered(self):
- return (self._state == "REGISTERED")
+ def _on_offer_received(self, dialog, offer, initial=False):
+ #FIXME shouldn't need initial
+ if self.media_session.process_remote_message(offer, initial):
+ dialog.accept_remote_offer()
+ else:
+ dialog.reject_remote_offer()
- def build_register_request(self, timeout, auth):
- uri = "sip:%s" % self._account.split('@')[1]
- to = "<sip:%s>" % self._account
- request = self.build_request("REGISTER", uri, to, incr=1)
- request.add_header("ms-keep-alive", "UAC;hop-hop=yes")
- request.add_header("Contact", "<sip:%s:%s;transport=%s>;proxy=replace" %
- (self._ip, self._port, self._transport_protocol))
- request.add_header("Event", "registration")
- request.add_header("Expires", timeout)
- request.add_header("Authorization", "Basic %s" % auth)
- return request
+ def on_media_session_prepared(self, session):
+ if self._pending_invite:
+ self.invite()
+ elif self._incoming:
+ offer = SDPMessage(session=self.media_session)
+ self._dialog.update_local_offer(offer)
- def register(self):
- if self._state in ("REGISTERING", "REGISTERED", "CANCELLED"):
- return
- self._state = "REGISTERING"
- self._do_register(None, None)
+ def on_media_session_ready(self, session):
+ offer = SDPMessage(session=self.media_session)
+ self._dialog.update_local_offer(offer)
- @RequireSecurityTokens(LiveService.MESSENGER_SECURE)
- def _do_register(self, callback, errback):
- # Check if state changed while requesting security token
- if self._state != "REGISTERING":
- return
- auth = "msmsgs:RPS_%s" % self._tokens[LiveService.MESSENGER_SECURE]
- auth = base64.b64encode(auth).replace("\n", "")
- self._request = self.build_register_request(900, auth)
- self.send(self._request, True)
+ ### RTC Activity Callbacks -----------------------------------------------
- def unregister(self):
- if self._state in ("NEW", "UNREGISTERING", "UNREGISTERED", "CANCELLED"):
- return
- elif self._state == "REGISTERING":
- if self._request is None:
- self._state = "CANCELLED"
- self.emit("unregistered")
- else:
- self._pending_unregister = True
- return
+ def on_activity_accepted(self):
+ self._dispose()
- self._state = "UNREGISTERING"
- self._pending_unregister = False
- if self._src is not None:
- gobject.source_remove(self._src)
- self._src = None
- self._do_unregister(None, None)
-
- @RequireSecurityTokens(LiveService.MESSENGER_SECURE)
- def _do_unregister(self, callback, errback):
- auth = "%s:%s" % (self._account, self._tokens[LiveService.MESSENGER_SECURE])
- auth = base64.encodestring(auth).replace("\n", "")
- request = self.build_register_request(0, auth)
- self.send(request, True)
-
- def on_expire(self):
- self.register()
- return False
-
- def on_register_response(self, response):
- if self._state == "UNREGISTERING":
- self._state = "UNREGISTERED"
- self.emit("unregistered")
- elif self._state != "REGISTERING":
- return # strange !?
- elif response.status is 200:
- self._state = "REGISTERED"
- self.emit("registered")
- timeout = int(response.get_header("Expires", 30))
- self._src = gobject.timeout_add(timeout * 1000, self.on_expire)
- if self._pending_unregister:
- self.unregister()
- else:
- self._state = "UNREGISTERED"
- self.emit("failed")
+ def on_activity_declined(self):
+ self._dispose()
--- papyon/sip/call_manager.py
+++ papyon/sip/call_manager.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from papyon.msnp.constants import *
+from papyon.sip.call import SIPCall
+from papyon.sip.core import SIPCore
+
+import gobject
+import logging
+import uuid
+
+logger = logging.getLogger('papyon.sip.call_manager')
+
+class SIPCallManager(gobject.GObject):
+
+ __gsignals__ = {
+ 'incoming-call': (
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,))
+ }
+
+ def __init__(self, client):
+ gobject.GObject.__init__(self)
+ self._client = client
+ self._core = SIPCore(self._client)
+ self._core.connect("invite-received", self._on_invite_received)
+ self._core.connect("invite-answered", self._on_invite_answered)
+
+ self._calls = {} # Call-ID => call, handle_id
+
+ def create_call(self, peer):
+ #FIXME check if busy
+ id = self._generate_id()
+ call = SIPCall(self._client, self._core, id, peer=peer)
+ self._add_call(call)
+ return call
+
+ def find_call(self, message):
+ return self._calls.get(message.call_id, (None, None))[0]
+
+ def _add_call(self, call):
+ handle = call.connect("ended", self._remove_call)
+ self._calls[call.id] = (call, handle)
+
+ def _remove_call(self, call):
+ call, handle = self._calls.pop(call.id)
+ call.disconnect(handle)
+
+ def _on_invite_received(self, core, invite):
+ #FIXME check if busy
+ id = invite.call_id
+ if id in self._calls:
+ logger.warning("Call with same id (%s) already exists" % id)
+ return
+
+ call = SIPCall(self._client, core, id, invite=invite)
+ if not call.handle_invite_request(invite):
+ logger.warning("Call ended before we could signal it (%s)" % id)
+ return
+
+ self._add_call(call)
+ self.emit("incoming-call", call)
+
+ def _on_invite_answered(self, core, response):
+ call_id = response.call_id
+ call, handle = self._calls.get(call_id, (None, None))
+ if not call:
+ logger.warning("No call matches with INVITE answer (%s)" % call_id)
+ return
+ call.handle_invite_response(response)
+
+ def _generate_id(self):
+ id = None
+ while True:
+ id = uuid.uuid4().get_hex()
+ if id not in self._calls: break
+ return id
--- papyon/sip/connection.py
+++ papyon/sip/connection.py
-# -*- coding: utf-8 -*-
-#
-# papyon - a python client library for Msn
-#
-# Copyright (C) 2009 Collabora Ltd.
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-
-from papyon.sip.call import SIPCall, SIPRegistration
-from papyon.sip.constants import *
-from papyon.sip.message import SIPRequest
-
-import gobject
-import logging
-
-__all__ = ['SIPConnection', 'SIPTunneledConnection']
-
-logger = logging.getLogger('papyon.sip.connection')
-
-
-class SIPBaseConnection(gobject.GObject):
-
- __gsignals__ = {
- 'invite-received': (gobject.SIGNAL_RUN_FIRST,
- gobject.TYPE_NONE,
- (object,)),
- 'disconnecting': (gobject.SIGNAL_RUN_FIRST,
- gobject.TYPE_NONE,
- ()),
- 'disconnected': (gobject.SIGNAL_RUN_FIRST,
- gobject.TYPE_NONE,
- ())
- }
-
- def __init__(self, client, transport):
- gobject.GObject.__init__(self)
- self._calls = {}
- self._client = client
- self._transport = transport
- self._transport.connect("message-received", self.on_message_received)
-
- @property
- def transport(self):
- return self._transport
-
- def create_call(self, peer=None, invite=None, id=None):
- call = SIPCall(self, self._client, peer, invite, id)
- self.add_call(call)
- return call
-
- def add_call(self, call):
- self._calls[call.id] = call
-
- def remove_call(self, call):
- if call.id in self._calls:
- del self._calls[call.id]
-
- def get_call(self, callid):
- return self._calls.get(callid, None)
-
- def send(self, message, registration=False):
- message.sent = True
- self._transport.send(message)
-
- def on_message_received(self, parser, message):
- callid = message.get_header("Call-ID")
- call = self.get_call(callid)
- if call is None:
- if isinstance(message, SIPRequest) and message.code == "INVITE":
- logger.info("Call invitation received")
- call = self.create_call(invite=message, id=callid)
- self.emit("invite-received", call)
- else:
- logger.info("Message with invalid call-id received")
- if self.registered:
- call = SIPCall(self, self._client, invite=message, id=callid)
- response = call.build_response(message, 481)
- call.send(response) # call/transaction does not exist
- return
- call.on_message_received(message)
-
-
-class SIPConnection(SIPBaseConnection):
-
- def __init__(self, client, transport):
- SIPBaseConnection.__init__(self, client, transport)
- self._tokens = {}
- self._msg_queue = []
- self._registration = SIPRegistration(self, self._client)
- self._registration.connect("registered", self.on_registration_success)
- self._registration.connect("unregistered", self.on_unregistration_success)
- self.add_call(self._registration)
-
- @property
- def registered(self):
- return self._registration.registered
-
- @property
- def tunneled(self):
- return False
-
- def register(self):
- self._registration.register()
-
- def unregister(self):
- self.emit("disconnecting")
- self._msg_queue = []
- self._registration.unregister()
-
- def send(self, message, registration=False):
- if self.registered or registration:
- message.sent = True
- self._transport.send(message)
- else:
- self._msg_queue.append(message)
- self.register()
-
- def remove_call(self, call):
- SIPBaseConnection.remove_call(self, call)
- if len(self._calls) == 1:
- self.unregister()
-
- def on_registration_success(self, registration):
- while len(self._msg_queue) > 0:
- msg = self._msg_queue.pop(0)
- if not msg.cancelled:
- self.send(msg)
-
- def on_unregistration_success(self, registration):
- self.emit("disconnected")
-
-
-class SIPTunneledConnection(SIPBaseConnection):
-
- @property
- def registered(self):
- return True
-
- @property
- def tunneled(self):
- return True
--- papyon/sip/connection_manager.py
+++ papyon/sip/connection_manager.py
-# -*- coding: utf-8 -*-
-#
-# papyon - a python client library for Msn
-#
-# Copyright (C) 2009 Collabora Ltd.
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-
-from papyon.msnp.constants import *
-from papyon.sip.connection import *
-from papyon.sip.transport import *
-
-import gobject
-
-class SIPConnectionManager(gobject.GObject):
-
- __gsignals__ = {
- 'invite-received': (gobject.SIGNAL_RUN_FIRST,
- gobject.TYPE_NONE,
- ([object]))
- }
-
- server = "vp.sip.messenger.msn.com"
- port = 443
-
- def __init__(self, client, protocol):
- gobject.GObject.__init__(self)
- self._client = client
- self._protocol = protocol
- self._protocol.connect("buddy-notification-received",
- self.on_notification_received)
- self._connections = {}
- self._disconnecting = []
-
- self.create_connection(True, "NS")
-
- def create_connection(self, tunneled, host=None):
- if tunneled:
- transport = SIPTunneledTransport(self._protocol)
- connection = SIPTunneledConnection(self._client, transport)
- else:
- transport = SIPTransport(host, self.port)
- connection = SIPConnection(self._client, transport)
- connection.connect("invite-received", self.on_invite_received)
- connection.connect("disconnecting", self.on_connection_disconnecting)
- connection.connect("disconnected", self.on_connection_disconnected)
- self._connections[host] = connection
- return connection
-
- def remove_connection(self, connection):
- host = None
- for k, v in self._connections.iteritems():
- if v == connection:
- host = k
- if host is not None:
- del self._connections[host]
-
- def get_connection(self, tunneled, host=None):
- if tunneled:
- host = "NS"
- connection = self._connections.get(host, None)
- if connection is None:
- connection = self.create_connection(tunneled, host)
- return connection
-
- def create_call(self, peer):
- tunneled = (self._client.profile.client_id.supports_tunneled_sip and
- peer.client_capabilities.supports_tunneled_sip)
- connection = self.get_connection(tunneled, self.server)
- call = connection.create_call(peer=peer)
- return call
-
- def on_notification_received(self, protocol, type, notification):
- if type is not UserNotificationTypes.SIP_INVITE:
- return
- args = notification.payload.split()
- if len(args) == 3 and args[0] == "INVITE":
- # Register to the server so we can take the call
- connection = self.get_connection(False, args[1])
- connection.register()
-
- def on_invite_received(self, connection, call):
- self.emit("invite-received", call)
-
- def on_connection_disconnecting(self, connection):
- self.remove_connection(connection)
- if connection not in self._disconnecting:
- self._disconnecting.append(connection)
-
- def on_connection_disconnected(self, connection):
- if connection in self._disconnecting:
- self._disconnecting.remove(connection)
- else:
- self.remove_connection(connection)
--- papyon/sip/constants.py
+++ papyon/sip/constants.py
@@ -2,7 +2,7 @@
#
# papyon - a python client library for Msn
#
-# Copyright (C) 2009 Collabora Ltd.
+# Copyright (C) 2009-2010 Collabora Ltd.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -104,3 +104,19 @@
SIP_INSTANCE = "0E04CFC3-0272-5A5C-B7C3-6FBE8DA71EAD"
USER_AGENT = "papyon/0.1"
+
+T1 = 0.5 # seconds
+T2 = 4 # seconds
+
+TIMER_A = T1
+TIMER_B = int(64 * T1) # INVITE timer
+TIMER_F = int(64 * T1) # non-INVITE timer
+TIMER_H = int(64 * T1)
+
+class SIPTransactionError(object):
+ TIMEOUT = 1
+ TRANSPORT_ERROR = 2
+
+class SIPMode(object):
+ CLIENT = "UAC"
+ SERVER = "UAS"
--- papyon/sip/core.py
+++ papyon/sip/core.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from papyon.sip.extensions import init_extensions
+from papyon.sip.constants import T1, SIPMode, SIPTransactionError, USER_AGENT
+from papyon.sip.dialog import SIPDialog
+from papyon.sip.message import SIPRequest, SIPResponse, SIPContact, SIPCSeq, SIPVia
+from papyon.sip.transaction import SIPTransactionLayer
+from papyon.sip.transport import SIPTunneledTransport
+from papyon.util.decorator import rw_property
+from papyon.util.timer import Timer
+
+import gobject
+import logging
+import uuid
+
+__all__ = ['SIPCore']
+
+logger = logging.getLogger('papyon.sip.core')
+
+class SIPCore(gobject.GObject, Timer):
+ """ The set of processing functions required at a UAS and a UAC that
+ resides above the transaction and transport layers.
+
+ The core keeps track of the ongoing dialogs with peers. Apart from
+ that, the core is stateless and there is only one instance per UA."""
+
+ __gsignals__ = {
+ 'register-answered': (
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
+ 'invite-received': (
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
+ 'invite-answered': (
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
+ 'cancel-received': (
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,))
+ }
+
+ @property
+ def self_uri(self):
+ return "sip:" + self._client.profile.account
+
+ def __init__(self, client):
+ gobject.GObject.__init__(self)
+ Timer.__init__(self)
+
+ self._client = client
+ self._transport = SIPTunneledTransport(client._protocol)
+ self._transaction_layer = SIPTransactionLayer(self._transport)
+ self._transaction_layer.connect("request-received",
+ self._on_request_received)
+ self._transaction_layer.connect("response-received",
+ self._on_response_received)
+ self._transaction_layer.connect("error",
+ self._on_transaction_layer_error)
+
+ self._supported_methods = set(["INVITE", "ACK", "CANCEL", "BYE",
+ "OPTIONS", "INFO", "UPDATE", "REFER", "NOTIFY", "BENOTIFY"])
+ self._supported_extensions = set(init_extensions(client, self))
+ self._supported_content_types = set(["application/sdp"])
+
+ self._dialogs = {} # (call-id, local-tag, remote-tag) => dialog
+ self._dialog_handles = {} # dialog => handle id
+
+ def send(self, message, use_transaction=True):
+ """Apply extensions and send message to transaction layer."""
+ #self._add_supported_methods(message)
+ self._apply_extensions(message)
+ if not use_transaction:
+ return self._transport.send(message)
+ return self._transaction_layer.send(message)
+
+ def answer(self, request, status, tag=None, extra_headers={}, content=None):
+ """Create response with given status and send to transaction layer."""
+ response = self.create_response(request, status, tag=tag)
+ for (name, value) in extra_headers:
+ response.add_header(name, value)
+ if content is not None:
+ response.set_content(content)
+ return self.send(response)
+
+ # Public API -------------------------------------------------------------
+
+ def register(self, call_id, cseq, timeout, auth):
+ request = self._create_register_request(call_id, cseq, timeout, auth)
+ self.send(request)
+ return request
+
+ def invite(self, call_id, uri, offer):
+ request = self._create_invite_request(call_id, uri, offer)
+ self.request_UAC_dialog(request)
+ return request
+
+ def cancel(self, canceled_request):
+ request = self._create_cancel_request(canceled_request)
+ self.start_timeout("cancel", int(64 * T1), canceled_request)
+ self.send(request)
+ return request
+
+ # Dialog Management ------------------------------------------------------
+
+ def establish_UAS_dialog(self, request, status):
+ # 12.1.1 UAS behavior (Creation of a Dialog)
+ self_tag = self._generate_tag()
+ response = self.create_response(request, status, tag=self_tag)
+ response.clone_headers("Record-Route", request)
+ #response.add_header("Contact", SIPContact(None, self.self_uri, None))
+ request.transaction.send(response)
+ return self._create_dialog(request, response, SIPMode.SERVER)
+
+ def request_UAC_dialog(self, request):
+ # 12.1.2 UAC behavior (Creation of a Dialog)
+ self_tag = request.From.tag
+ request.add_header("Contact", SIPContact(None, self.self_uri, self_tag))
+ self.send(request)
+
+ def establish_UAC_dialog(self, response):
+ # 12.1.2 UAC behavior (Creation of a Dialog)
+ return self._create_dialog(response.request, response, SIPMode.CLIENT)
+
+ def _create_dialog(self, request, response, mode):
+ dialog = SIPDialog(self, request, response, mode)
+ key = (dialog.call_id, dialog.local_tag, dialog.remote_tag)
+ logger.info("Create dialog id=%s, local_tag=%s, remote_tag=%s" % key)
+ handle = dialog.connect("disposed", self._on_dialog_disposed)
+ self._dialogs[key] = dialog
+ self._dialog_handles[dialog] = handle
+ return dialog
+
+ def _find_dialog(self, message):
+ call_id = message.call_id
+ if type(message) is SIPRequest:
+ local_tag = message.To.tag
+ remote_tag = message.From.tag
+ else:
+ local_tag = message.From.tag
+ remote_tag = message.To.tag
+
+ return self._dialogs.get((call_id, local_tag, remote_tag), None)
+
+ def _forward_to_dialog(self, message):
+ dialog = self._find_dialog(message)
+ if dialog is not None:
+ if type(message) is SIPRequest:
+ dialog.handle_request(message)
+ elif type(message) is SIPResponse:
+ dialog.handle_response(message)
+
+ def _on_dialog_disposed(self, dialog):
+ key = (dialog.call_id, dialog.local_tag, dialog.remote_tag)
+ del self._dialogs[key]
+ handle = self._dialog_handles.pop(dialog)
+ dialog.disconnect(handle)
+
+ # Generating Messages -----------------------------------------------------
+
+ def create_request(self, code, to_uri, uri=None, tag=None, call_id=None,
+ cseq=None):
+ """ Create a request outside of a dialog """
+ # 8.1.1 Generating the Request (UAC Behavior)
+
+ if uri is None:
+ uri = to_uri
+ if tag is None:
+ tag = self._generate_tag()
+ if call_id is None:
+ call_id = self._generate_call_id()
+ if cseq is None:
+ cseq = self._generate_cseq()
+
+ request = SIPRequest(code, uri)
+ request.add_header("To", SIPContact("0", to_uri))
+ request.add_header("From", SIPContact("0", self.self_uri, tag))
+ request.add_header("Call-Id", call_id)
+ request.add_header("CSeq", SIPCSeq(cseq, code))
+ return request
+
+ def create_response(self, request, status, tag=None):
+ """ Create a response outside of a dialog """
+ # 8.2.6 Generating the Response (UAS Behavior)
+ response = SIPResponse(status)
+ response.request = request
+
+ # 8.2.6.1 Sending a Provisional Response
+ if status/100 == 1 and request.code != "INVITE":
+ return
+ if status == 100:
+ response.clone_headers("Timestamp", request)
+
+ # 8.2.6.2 Headers and Tags
+ response.clone_headers("To", request)
+ response.clone_headers("From", request)
+ response.clone_headers("Call-ID", request)
+ response.clone_headers("CSeq", request)
+ response.clone_headers("Via", request)
+
+ # Add To tag if missing
+ if not response.to.tag:
+ if tag is None:
+ tag = self._generate_tag()
+ response.to.tag = tag
+
+ return response
+
+ # Extensions Management --------------------------------------------------
+
+ def _apply_extensions(self, message):
+ for extension in self._supported_extensions:
+ extension.extend(message)
+
+ def _add_supported_methods(self, message):
+ methods = ", ".join(self._supported_methods)
+ message.add_header("Allow", methods)
+
+ # Messages Handling ------------------------------------------------------
+
+ def _on_response_received(self, transaction_layer, response):
+ # 8.1.3 Processing Responses (UAC Behavior)
+
+ if len(response.get_headers("Via", [])) >= 2:
+ return # discard message (8.1.3.3)
+
+ if response.status/100 == 3:
+ return # 3xx responses are not handled
+
+ if response.status/100 == 4:
+ pass # 4xx responses are not handled (Bad Request)
+
+ handler = getattr(self, "_handle_%s_response" % response.code.lower(), None)
+ if handler is not None:
+ handler(response)
+ else:
+ self._forward_to_dialog(response)
+
+ def _on_request_received(self, transaction_layer, request):
+ # 8.2 UAS Behavior (General User Agent Behavior)
+ if not self._inspect_request(request):
+ return
+
+ # 8.2.5 Processing the request:
+ handler = getattr(self, "_handle_%s_request" % request.code.lower(), None)
+ if handler is not None:
+ handler(request)
+ else:
+ self._forward_to_dialog(request)
+
+ def _inspect_request(self, request):
+ # 8.2.1 Method Inspection
+
+ # 8.2.2 Header Inspection
+ if not request.uri.startswith("sip:"):
+ logger.warning("Unsupported URI Scheme: %s" % request.uri)
+ self.answer(request, 416)
+ return False # Unsupported URI Scheme
+ #if request.uri != self.self_uri:
+ # logger.warning("Request URI is Not Found: %s" % request.uri)
+ # self.answer(request, 404)
+ # return False # Not Found
+ if not request.to.tag and self._transaction_layer.is_merged_request(request):
+ logger.warning("Received merged request (Loop Detected)")
+ self.answer(request, 482)
+ return False # Loop Detected
+ unsupported = request.required_extensions - self._supported_extensions
+ if not request.code in ("CANCEL", "ACK") and unsupported:
+ headers = {"Unsupported": " ".join(unsupported)}
+ logger.warning("Unsupported Extensions: %s" % unsupported)
+ self.answer(request, 420, headers)
+ return False # Bad Extension
+
+ # 8.2.3 Content Processing
+ if (request.content_type and
+ request.content_type not in self._supported_content_types):
+ logger.warning("Unsupport Media Type: %s" % request.content_type)
+ self.answer(request, 415)
+ return False # Unsupported Media Type
+
+ return True
+
+ # CANCEL Method ----------------------------------------------------------
+
+ def _create_cancel_request(self, original_request):
+ # 9.1 Client Behavior (Canceling a request)
+ request = SIPRequest("CANCEL", original_request.uri)
+ request.clone_headers("Route", original_request)
+ request.clone_headers("To", original_request)
+ request.clone_headers("From", original_request)
+ request.clone_headers("Call-ID", original_request)
+ cseq = original_request.cseq.number
+ request.add_header("CSeq", SIPCSeq(cseq, "CANCEL"))
+ return request
+
+ def _handle_cancel_request(self, request):
+ # 9.2 Server Behavior (Canceling a Request)
+ transaction = self._transaction_layer.find_canceled_transaction(request)
+ if transaction is None:
+ logger.warning("Transaction to cancel doesn't exist.")
+ return self.answer(request, 481) # Transaction Does Not Exist
+ if type(transaction) is SIPClientTransaction:
+ logger.warning("Can't remotely cancel client transaction.")
+ return self.answer(request, 500) # Server Error
+ if transaction.answered:
+ return
+
+ dialog = self._find_dialog(request)
+ if dialog is None:
+ self.emit("cancel-received", request)
+ else:
+ dialog.handle_request(request)
+ self.answer(request, 200)
+
+ # REGISTER Method --------------------------------------------------------
+
+ def _create_register_request(self, tag, call_id, cseq, timeout, auth):
+ # 10.2 Constructing the REGISTER Request
+ uri = "sip:%s" % self.self_uri('@')[1]
+ to = "sip:%s" % self.self_uri
+ request = self.create_request("REGISTER", to, uri, tag, call_id, cseq)
+ request.add_header("Contact", "<sip:%s:%s;transport=%s>" %
+ (self._ip, self._port, self._transport_protocol))
+ request.add_header("Event", "registration")
+ request.add_header("Expires", timeout)
+ request.add_header("Authorization", "Basic %s" % auth)
+ return request
+
+ def _handle_register_response(self, response):
+ registration = self._registrations.get(call_id, None)
+ if registration:
+ registration.handle_response(response)
+
+ # OPTIONS Method ---------------------------------------------------------
+
+ def _create_options_request(self, uri):
+ # 11.1 Construction of OPTIONS Request
+ request = self.create_request("OPTIONS", uri)
+ request.add_header("Accept", "application/sdp")
+ return request
+
+ def _handle_options_request(self, request):
+ self.emit("options-received", request)
+
+ def _handle_options_response(self, response):
+ self.emit("options-answered", response)
+
+ # INVITE Method ----------------------------------------------------------
+
+ def _create_invite_request(self, call_id, uri, offer):
+ # 13.2.1 Creating the Initial INVITE
+ request = self.create_request("INVITE", uri, call_id=call_id)
+ request.add_header("User-Agent", USER_AGENT)
+ request.set_content(offer)
+ return request
+
+ def _handle_invite_response(self, response):
+ # 13.2.2 Processing INVITE Responses (UAC Processing)
+ dialog = self._find_dialog(response)
+ if dialog is None:
+ self.emit("invite-answered", response)
+ else:
+ dialog.handle_response(response)
+
+ def _handle_invite_request(self, request):
+ # 13.3.1 Processing of the INVITE (UAS Processing)
+ dialog = self._find_dialog(request)
+ if dialog is not None:
+ dialog.handle_request(request)
+ elif request.to.tag:
+ logger.warning("Dialog for target refresh (INVITE) doesn't exist.")
+ self.answer(request, 481) # Call Leg Does Not Exist
+ else:
+ self.emit("invite-received", request)
+
+ # BYE Method -------------------------------------------------------------
+
+ def _handle_bye_request(self, request):
+ # 15.1.2 UAS Behavior (Terminating a Session with a BYE Request)
+ dialog = self._find_dialog(request)
+ if dialog is None:
+ logger.warning("Dialog to terminate (BYE) doesn't exist.")
+ self.answer(request, 481) # Call Leg Does Not Exist
+ else:
+ dialog.handle_request(request)
+ self.answer(request, 200)
+
+ # Timeouts Callbacks -----------------------------------------------------
+
+ def on_cancel_timeout(self, canceled_request):
+ transaction = canceled_request.transaction
+ if transaction is not None:
+ logger.info("Canceled request not responsed, destroy transaction")
+ transaction.destroy()
+
+ # Errors Handling --------------------------------------------------------
+
+ def _on_transaction_layer_error(self, transaction_layer, transaction, error):
+ # 8.1.3.1 Transaction Layer Errors
+ if error is SIPTransactionError.TIMEOUT: # Treat as 408
+ logger.error("Received Timeout error from transaction layer")
+ fake_response = self.create_response(transaction.request, 408)
+ self._on_response_received(transaction_layer, fake_response)
+ elif error is SIPTransactionError.TRANSPORT_ERROR: # Treat as 503
+ logger.error("Received Transport error from transaction layer")
+ fake_response = self.create_response(transaction.request, 503)
+ self._on_response_received(transaction_layer, fake_response)
+
+ # Utils Functions --------------------------------------------------------
+
+ def _generate_tag(self):
+ # 19.3 Tags
+ return uuid.uuid4().get_hex()
+
+ def _generate_call_id(self):
+ # 20.8 Call-ID
+ return uuid.uuid4().get_hex()
+
+ def _generate_cseq(self):
+ return 1 #FIXME random cseq
--- papyon/sip/dialog.py
+++ papyon/sip/dialog.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from papyon.sip.constants import SIPMode, T1, T2
+from papyon.sip.message import SIPRequest, SIPContact, SIPCSeq, SIPRoute
+from papyon.sip.sdp import SDPMessage
+from papyon.util.decorator import rw_property
+from papyon.util.timer import Timer
+
+import gobject
+import logging
+import re
+
+__all__ = ['SIPDialog']
+
+logger = logging.getLogger('papyon.sip.dialog')
+
+class SIPDialog(gobject.GObject, Timer):
+ """Represent a SIP dialog between two end points. A dialog must be
+ initiated by sending or receiving a response to an INVITE request.
+ It is disposed when receiving or sending a CANCEL or BYE request.
+
+ See details in section 12 of RFC 3261"""
+
+ __gsignals__ = {
+ 'ringing': (
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ()),
+ 'accepted': (
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
+ 'rejected': (
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
+ 'offer-received': (
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object, object)),
+ 'ended': (
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ()),
+ 'disposed': (
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ())
+ }
+
+ @property
+ def answered(self):
+ return (self._state != "INVITED" or self._pending_accept)
+
+ @property
+ def call_id(self):
+ return self._call_id
+
+ @property
+ def local_tag(self):
+ return self._local_tag
+
+ @property
+ def remote_tag(self):
+ return self._remote_tag
+
+ def __init__(self, core, request, response, mode):
+ gobject.GObject.__init__(self)
+ Timer.__init__(self)
+ self._core = core
+ self._initial_request = request
+ self._last_request = request
+
+ self._incoming = False
+ self._pending_incoming_requests = []
+ self._pending_outgoing_requests = []
+ self._local_offer = None
+ self._pending_local_offer = False
+ self._pending_remote_offer = False
+ self._pending_accept = False
+
+ self._remote_target = None
+ self._route_set = []
+
+ self.__state = None
+ self._early = bool(response.status/100 == 1)
+
+ if mode is SIPMode.SERVER:
+ self._build_UAS_dialog(request, response)
+ elif mode is SIPMode.CLIENT:
+ self._build_UAC_dialog(request, response)
+
+ # Public API -------------------------------------------------------------
+
+ def ring(self):
+ # 13.3.1.1 Progress (Processing of the INVITE)
+ if self._state != "INVITED":
+ return
+ self.on_ringing_timeout()
+
+ def accept(self):
+ """Accept last request"""
+ if self._state not in ("INVITED", "REINVITED"):
+ return
+ if not self._local_offer or self._pending_remote_offer:
+ self._pending_accept = True
+ return
+ self._pending_accept = False
+ self._early = False
+ self.stop_timeout("ringing")
+ self.start_timeout("ack", int(64 * T1))
+ self.on_accept_timeout()
+
+ def reject(self, status):
+ """Reject last request"""
+ if self._state not in ("INVITED", "REINVITED"):
+ return
+ self._state = "CONFIRMED"
+ self._early = False
+ self._pending_accept = False
+ self.stop_timeout("ringing")
+ self.answer(self._last_request, status)
+
+ # when rejecting the initial request, the dialog is ended
+ if self._initial_request == self._last_request:
+ self._state = "ENDED"
+
+ def ack(self, response):
+ """Acknowledge response"""
+ if self._state not in ("INVITING", "REINVITING"):
+ return
+ self._state = "CONFIRMED"
+ request = self._create_ack_request(response)
+ self._core.send(request, use_transaction=False)
+
+ def reinvite(self):
+ if self._state == "ENDED":
+ return
+ if self._state != "CONFIRMED":
+ self._pending_local_offer = True
+ return
+ logger.info("Send re-invite to %s", self._remote_uri)
+ self._state = "REINVITING"
+ self._pending_local_offer = None
+ request = self._create_reinvite_request(self._local_offer)
+ self.send_request(request)
+
+ def end(self):
+ if self._state == "ENDED":
+ return
+ if self._state == "INVITED":
+ self.reject(603)
+ elif self._state == "INVITING":
+ self.cancel()
+ else:
+ bye = self._create_bye_request()
+ self.send_request(bye)
+ self._state = "ENDED"
+
+ def reset(self):
+ self.stop_all_timeout()
+ self._pending_incoming_requests = []
+ self._pending_local_offer = False
+ self._pending_remote_offer = False
+ self._pending_accept = False
+
+ def dispose(self):
+ if self._pending_outgoing_requests:
+ return
+ self._state = "DISPOSED"
+
+ # States Handling --------------------------------------------------------
+
+ @rw_property
+ def _state():
+ def fget(self):
+ return self.__state
+ def fset(self, value):
+ if value == self.__state:
+ return
+ old_state = self.__state
+ self.__state = value
+ self._on_state_changed(old_state)
+ return locals()
+
+ def _on_state_changed(self, old_state):
+ if self._state == "CONFIRMED":
+ self.stop_timeout("accept")
+ self.stop_timeout("ack")
+ if self._pending_local_offer and not self._incoming:
+ self.reinvite()
+ elif self._state == "ENDED":
+ for request in self._pending_incoming_requests:
+ logger.info("Respond to pending incoming %s request" % request.code)
+ self._core.answer(request, 487)
+ self.reset()
+ self.emit("ended")
+ self.dispose()
+ elif self._state == "DISPOSED":
+ self.emit("disposed")
+
+ # Offer/Answer Session Negotiation----------------------------------------
+
+ def update_local_offer(self, offer):
+ if self._local_offer == offer:
+ return
+ self._local_offer = offer
+ if self._pending_accept:
+ self.accept()
+ elif not self._incoming:
+ self.reinvite()
+
+ def accept_remote_offer(self):
+ if not self._pending_remote_offer:
+ logger.warning("No pending remote session offer to accept")
+ return True
+ self._pending_remote_offer = False
+ if self._state == "REINVITED":
+ self.accept()
+ return True
+
+ def reject_remote_offer(self):
+ if not self._pending_remote_offer:
+ logger.warning("No pending remote session offer to reject")
+ return False
+ self._pending_remote_offer = False
+ if self._state == "INVITED":
+ self.reject(488)
+ elif self._state == "INVITING":
+ self.end()
+ elif self._state == "REINVITED":
+ self.reject(403)
+ else:
+ self.end()
+ return True
+
+ def _handle_offer(self, message, initial):
+ if not message.body:
+ logger.info("Message doesn't contain a media session offer")
+ return True
+ try:
+ offer = SDPMessage(body=message.body)
+ self._pending_remote_offer = True
+ self.emit("offer-received", offer, initial)
+ except Exception, err:
+ logger.error("Malformed body in media session offer")
+ logger.exception(err)
+ return False
+ return True
+
+ # Creation of Dialogs ----------------------------------------------------
+
+ def _build_UAS_dialog(self, request, response):
+ # 12.1.1 UAS behavior (Creation of Dialog)
+ if self._early:
+ self._pending_incoming_requests.append(request)
+ if response.record_route:
+ self._route_set = response.record_route.route_set
+ self._incoming = True
+ self._local_cseq = None
+ self._remote_cseq = request.cseq.number
+ self._call_id = request.call_id
+ self._local_tag = response.to.tag
+ self._remote_tag = request.From.tag
+ self._local_uri = request.to.uri
+ self._remote_uri = request.From.uri
+
+ def _build_UAC_dialog(self, request, response):
+ # 12.1.2 UAC behavior (Creation of Dialog)
+ self._state = "INVITING"
+ self._incoming = False
+ self._pending_outgoing_requests.append(request)
+ if response.record_route:
+ self._route_set = response.record_route.route_set.reverse()
+ if response.contact:
+ self._remote_target = response.contact.uri
+ self._local_cseq = request.cseq.number
+ self._remote_cseq = None
+ self._call_id = response.call_id
+ self._local_tag = response.From.tag
+ self._remote_tag = response.to.tag
+ self._local_uri = response.From.uri
+ self._remote_uri = response.to.uri
+
+ def _create_request(self, code, cseq=None):
+ # 12.2.1 UAC Behavior (Requests within a Dialog)
+ route = self._route_set
+ if not route:
+ uri = self._remote_target
+ elif ';lr' in route[0]:
+ uri = self._remote_target
+ else:
+ uri = route.pop(0)
+ route.append(self._remote_target)
+
+ if not self._local_cseq:
+ self._local_cseq = 1 # FIXME 8.1.1.5
+ if not cseq:
+ self._local_cseq += 1
+ cseq = self._local_cseq
+
+ request = SIPRequest(code, self._remote_target)
+ if route:
+ request.add_header("Route", SIPRoute(route))
+ request.add_header("To", SIPContact(None, self._remote_uri, self._remote_tag))
+ request.add_header("From", SIPContact(None, self._local_uri, self._local_tag))
+ request.add_header("Call-ID", self._call_id)
+ request.add_header("CSeq", SIPCSeq(cseq, code))
+ return request
+
+ # Messages Handling ------------------------------------------------------
+
+ def send_request(self, request):
+ self._pending_outgoing_requests.append(request)
+ self._core.send(request)
+
+ def answer(self, request, status, extra_headers={}, content=None):
+ if request in self._pending_incoming_requests and status >= 200:
+ self._pending_incoming_requests.remove(request)
+ return self._core.answer(request, status, tag=self._local_tag,
+ extra_headers=extra_headers, content=content)
+
+ def handle_response(self, response):
+ # 12.2.1 UAC Behavior (Requests within a Dialog)
+ request = response.request
+ if request in self._pending_outgoing_requests and response.status >= 200:
+ self._pending_outgoing_requests.remove(request)
+ if self._state == "ENDED" and not self._pending_outgoing_requests:
+ self.dispose()
+ return False
+
+ if response.status in (408, 481) and not self._early:
+ self.end()
+ return False
+
+ # Target refresh
+ if response.code == "INVITE" and response.contact:
+ self._remote_target = response.contact.uri
+
+ # Method specific handler
+ handler = getattr(self, "_handle_%s_response" % response.code.lower(), None)
+ if handler is not None:
+ return handler(response)
+ return True
+
+ def handle_request(self, request):
+ # 12.2.2 UAS Behavior (Requests within a Dialog)
+ if self._state == "ENDED":
+ return
+
+ self._last_request = request
+ self._pending_incoming_requests.append(request)
+ if not self._remote_cseq:
+ self._remote_cseq = request.cseq.number
+ elif self._remote_cseq > request.cseq.number: # Out of order
+ logger.warning("CSeq is out of order (%i > %i)" %
+ (self._remote_cseq, request.cseq.number))
+ self.answer(request, 500)
+ return False
+
+ # Target refresh
+ if request.code == "INVITE" and request.contact:
+ self._remote_target = request.contact.uri
+
+ # Method specific handler
+ handler = getattr(self, "_handle_%s_request" % request.code.lower(), None)
+ if handler is not None:
+ return handler(request)
+ return True
+
+ # INVITE Method ----------------------------------------------------------
+
+ def _handle_invite_response(self, response):
+ # 13.2.2 Processing INVITE Responses (UAC Processing)
+ if not self._early:
+ return self._handle_reinvite_response(response)
+
+ if response.status == 180:
+ self.emit("ringing")
+ return True
+
+ if response.status >= 200:
+ self._early = False
+
+ # 13.2.2.3 4xx, 5xx and 6xx Responses
+ if response.status >= 300:
+ self.emit("rejected", response)
+ self._state = "ENDED"
+ return False
+
+ # 13.2.2.4 2xx Responses
+ if response.status/100 == 2:
+ success = self._handle_offer(response, False)
+ self.ack(response)
+ if success:
+ logger.info("Call dialog is confirmed")
+ self.emit("accepted", response)
+ return True
+ else:
+ self.end()
+ return False
+
+ def _handle_invite_request(self, request):
+ # 13.3.1 Processing of the INVITE (UAS Processing)
+ if self._state is not None:
+ return self._handle_reinvite_request(request)
+ self._state = "INVITED"
+ if not self._handle_offer(request, True):
+ self.answer(request, 488)
+ self._state = "ENDED"
+ return False
+ return True
+
+ # re-INVITE Method -------------------------------------------------------
+
+ def _create_reinvite_request(self, offer):
+ # 14.1 UAC Behavior (Modifying an Existing Session)
+ request = self._create_request("INVITE")
+ request.add_header("Contact", SIPContact(None, self._local_uri, self._local_tag))
+ request.set_content(offer)
+ return request
+
+ def _handle_reinvite_response(self, response):
+ # 14.1 UAC Behavior (Modifying an Existing Session)
+ if response.status/100 == 2:
+ self._handle_offer(response, False)
+ self.ack(response)
+ #FIXME return
+
+ def _handle_reinvite_request(self, request):
+ # 14.2 UAS Behavior (Modifying an Existing Session)
+ self.answer(request, 100)
+ if self._state in ("INVITED", "REINVITED"):
+ logger.warning("Already invited, can't handle incoming INVITE")
+ self.answer(request, 500)
+ return False
+ elif self._state in ("INVITING", "REINVITING"):
+ logger.warning("Already inviting, can't handle incoming INVITE")
+ self.answer(request, 491)
+ return False
+ self._state = "REINVITED"
+ if not self._handle_offer(request, False):
+ self.answer(request, 488)
+ return False
+ return True
+
+ # ACK Method -------------------------------------------------------------
+
+ def _create_ack_request(self, response):
+ request = self._create_request("ACK", cseq=response.cseq.number)
+ return request
+
+ def _handle_ack_request(self, request):
+ self._pending_incoming_requests.remove(request) # No need to answer
+ self._state = "CONFIRMED"
+
+ # CANCEL Method ----------------------------------------------------------
+
+ def cancel(self):
+ if not self._early:
+ return
+ request = self._core.cancel(self._initial_request)
+ self._pending_outgoing_requests.append(request)
+ self._state = "ENDED"
+
+ def _handle_cancel_request(self, request):
+ self._pending_incoming_requests.remove(request) # UA core will respond
+ self._state = "ENDED"
+
+ def _handle_cancel_response(self, response):
+ self.dispose()
+
+ # BYE Method -------------------------------------------------------------
+
+ def _create_bye_request(self):
+ request = self._create_request("BYE")
+ return request
+
+ def _handle_bye_request(self, request):
+ self._pending_incoming_requests.remove(request) # UA core will respond
+ self._state = "ENDED"
+
+ def _handle_bye_response(self, response):
+ self.dispose()
+
+ # Timeout Callbacks ------------------------------------------------------
+
+ def on_ringing_timeout(self):
+ self.answer(self._initial_request, 180)
+ self.start_timeout("ringing", 60)
+
+ def on_accept_timeout(self, time=T1):
+ if self._state not in ("INVITED", "REINVITED"):
+ return
+ header = ("Contact", SIPContact(None, self._local_uri, self._local_tag))
+ self.answer(self._last_request, 200, content=self._local_offer,
+ extra_headers=[header])
+
+ # [...] an interval that starts at T1 seconds and doubles for each
+ # retransmission until it reaches T2 seconds (Section 13.3.1.4)
+ next_time = int(min(time * 2, T2))
+ self.start_timeout("accept", time, next_time)
+
+ def on_ack_timeout(self):
+ self._state = "CONFIRMED"
+ self.end()
--- papyon/sip/extensions
+++ papyon/sip/extensions
+(directory)
--- papyon/sip/extensions/__init__.py
+++ papyon/sip/extensions/__init__.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from ms_conversation_id import MSConversationIDExtension
+from ms_proxy_replace import MSProxyReplaceExtension
+from ms_epid import MSEpidExtension
+from ms_mepid import MSMepidExtension
+from outbound import OutboundExtension
+
+def init_extensions(client, core):
+ extensions = []
+ extensions.append(MSConversationIDExtension(client, core))
+ extensions.append(MSProxyReplaceExtension(client, core))
+ extensions.append(MSMepidExtension(client, core))
+ extensions.append(MSEpidExtension(client, core))
+ extensions.append(OutboundExtension(client, core))
+ return extensions
--- papyon/sip/extensions/base.py
+++ papyon/sip/extensions/base.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from papyon.sip.message import SIPResponse, SIPRequest
+
+class SIPExtension(object):
+
+ def __init__(self, client, core):
+ self._client = client
+ self._core = core
+
+ def extend(self, message):
+ if type(message) is SIPResponse:
+ self.extend_response(message)
+ elif type(message) is SIPRequest:
+ self.extend_request(message)
+
+ def extend_response(self, reponse):
+ pass
+
+ def extend_request(self, request):
+ pass
--- papyon/sip/extensions/ms_conversation_id.py
+++ papyon/sip/extensions/ms_conversation_id.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from papyon.sip.extensions.base import SIPExtension
+
+class MSConversationIDExtension(SIPExtension):
+
+ def __init__(self, client, core):
+ SIPExtension.__init__(self, client, core)
+
+ def extend_request(self, message):
+ call = self._client.call_manager.find_call(message)
+ if call is not None and call.media_session.has_video:
+ conversation_id = 1
+ else:
+ conversation_id = 0
+ message.add_header("Ms-Conversation-ID", "f=%s" % conversation_id)
--- papyon/sip/extensions/ms_epid.py
+++ papyon/sip/extensions/ms_epid.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59contact.params["proxy"] = "replace" Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from papyon.sip.extensions.base import SIPExtension
+
+import uuid
+
+class MSEpidExtension(SIPExtension):
+
+ def __init__(self, client, core):
+ SIPExtension.__init__(self, client, core)
+ self._epid = uuid.uuid4().get_hex()[:10]
+
+ def extend_request(self, message):
+ mepid = self._client.machine_guid.upper().replace("-", "")
+ mepid = mepid.replace("{", "")
+ mepid = mepid.replace("}", "")
+ if message.From and "epid" not in message.From.params:
+ message.From.params["epid"] = self._epid
--- papyon/sip/extensions/ms_keep_alive.py
+++ papyon/sip/extensions/ms_keep_alive.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+# MS-Keep-Alive: UAC;hop-hop=yes
+# http://msdn.microsoft.com/en-us/library/cc431508%28v=office.12%29.aspx
+
+from papyon.sip.extensions.base import SIPExtension
+
+class MSKeepAliveExtension(SIPExtension):
+
+ def __init__(self, client, core):
+ SIPExtension.__init__(self, client, core)
+
+ def extend_request(self, message):
+ message.add_header("MS-Keep-Alive", "UAC;hop-hop=yes")
--- papyon/sip/extensions/ms_mepid.py
+++ papyon/sip/extensions/ms_mepid.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59contact.params["proxy"] = "replace" Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from papyon.sip.extensions.base import SIPExtension
+
+class MSMepidExtension(SIPExtension):
+
+ def __init__(self, client, core):
+ SIPExtension.__init__(self, client, core)
+
+ def extend_response(self, message):
+ mepid = self._client.machine_guid.upper().replace("-", "")
+ mepid = mepid.replace("{", "")
+ mepid = mepid.replace("}", "")
+ if message.To and ";mepid=" not in message.To.uri:
+ message.To.uri += ";mepid=" + mepid
+
+ def extend_request(self, message):
+ mepid = self._client.machine_guid.upper().replace("-", "")
+ mepid = mepid.replace("{", "")
+ mepid = mepid.replace("}", "")
+ if message.contact:
+ message.contact.uri += ";mepid=" + mepid
+ if message.From and ";mepid=" not in message.From.uri:
+ message.From.uri += ";mepid=" + mepid
+
+ #FIXME hack
+ if ";mepid=" in message.uri:
+ message.uri = message.uri.split(";")[0]
--- papyon/sip/extensions/ms_proxy_replace.py
+++ papyon/sip/extensions/ms_proxy_replace.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59contact.params["proxy"] = "replace" Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from papyon.sip.extensions.base import SIPExtension
+
+class MSProxyReplaceExtension(SIPExtension):
+
+ def __init__(self, client, core):
+ SIPExtension.__init__(self, client, core)
+
+ def extend_request(self, message):
+ if message.contact:
+ message.contact.params["proxy"] = "replace"
--- papyon/sip/extensions/outbound.py
+++ papyon/sip/extensions/outbound.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+# http://tools.ietf.org/id/draft-ietf-sip-outbound-11.txt
+
+from papyon.sip.extensions.base import SIPExtension
+
+class OutboundExtension(SIPExtension):
+
+ def __init__(self, client, core):
+ SIPExtension.__init__(self, client, core)
+
+ def extend_request(self, message):
+ uuid = "0E04CFC3-0272-5A5C-B7C3-6FBE8DA71EAD"
+ if message.contact:
+ message.contact.params["+sip.instance"] = '"<urn:uuid:%s>"' % uuid
--- papyon/sip/ice.py
+++ papyon/sip/ice.py
@@ -153,13 +153,13 @@
if draft is 19:
cand.priority = int(cand.priority)
if draft is 6:
+ cand.username = fix_b64_padding(cand.username)
+ cand.password = fix_b64_padding(cand.password)
cand.priority = int(float(cand.priority) * 1000)
if cand.priority < 0.5:
cand.type = "relay"
cand.component_id = int(cand.component_id)
- cand.username = fix_b64_padding(cand.username)
- cand.password = fix_b64_padding(cand.password)
cand.port = int(cand.port)
if cand.base_port is not None:
cand.base_port = int(cand.base_port)
--- papyon/sip/message.py
+++ papyon/sip/message.py
@@ -2,7 +2,7 @@
#
# papyon - a python client library for Msn
#
-# Copyright (C) 2009 Collabora Ltd.
+# Copyright (C) 2009-2010 Collabora Ltd.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -23,6 +23,8 @@
import gobject
import logging
+import re
+import weakref
__all__ = ['SIPMessage', 'SIPRequest', 'SIPResponse', 'SIPMessageParser']
@@ -62,6 +64,10 @@
def length(self):
return int(self.get_header("Content-Length", 0))
+ @property
+ def required_extensions(self):
+ return set(self.get_header("Require", "").split())
+
def normalize_name(self, name):
name = name.lower()
if len(name) is 1:
@@ -77,6 +83,18 @@
else:
self._headers.setdefault(name, []).append(value)
+ def parse_header(self, name, value):
+ name = self.normalize_name(name)
+ if name in ("contact", "from", "to"):
+ value = SIPContact.build(value)
+ elif name == "cseq":
+ value = SIPCSeq.build(value)
+ elif name in ("record-route", "route"):
+ value = SIPRoute.build(value)
+ elif name == "via":
+ value = SIPVia.build(value)
+ self.add_header(name, value)
+
def set_header(self, name, value):
name = self.normalize_name(name)
self._headers[name] = [value]
@@ -91,6 +109,17 @@
return value[0]
return value
+ def match_header(self, name, other):
+ value = self.get_header(name)
+ other_value = other.get_header(name)
+ return value == other_value
+
+ def match_headers(self, names, other):
+ for name in names:
+ if not self.match_header(name, other):
+ return False
+ return True
+
def clone_headers(self, name, other, othername=None):
if othername is None:
othername = name
@@ -98,22 +127,34 @@
othername = self.normalize_name(othername)
values = other.get_headers(othername)
if values is not None:
- self._headers[name] = values
-
- def set_content(self, content, type=None):
- if type:
- self.set_header("Content-Type", type)
- self.set_header("Content-Length", len(content))
- self._body = content
+ cloned_values = []
+ for value in values:
+ if hasattr(value, "clone"):
+ cloned_value = value.clone()
+ else:
+ cloned_value = value
+ cloned_values.append(cloned_value)
+ self._headers[name] = cloned_values
+
+ def set_content(self, content):
+ if hasattr(content, "type"):
+ self.set_header("Content-Type", content.type)
+ body = str(content)
+ self.set_header("Content-Length", len(body))
+ self._body = body
def get_header_line(self):
raise NotImplementedError
+ def __getattr__(self, name):
+ name = name.replace('_', '-')
+ return self.get_header(name)
+
def __str__(self):
s = [self.get_header_line()]
for k, v in self._headers.items():
for value in v:
- s.append("%s: %s" % (k, value))
+ s.append("%s: %s" % (k, str(value)))
s.append("")
s.append(self._body)
return "\r\n".join(s)
@@ -123,39 +164,56 @@
def __init__(self, code, uri):
SIPMessage.__init__(self)
- self._code = code
- self._uri = uri
-
- @property
- def code(self):
- return self._code
+ self.code = code
+ self.uri = uri
+ self._transaction_ref = None
- @property
- def uri(self):
- return self._uri
+ @rw_property
+ def transaction():
+ def fget(self):
+ if self._transaction_ref is None:
+ return None
+ return self._transaction_ref()
+ def fset(self, value):
+ if value is None:
+ self._transaction_ref = None
+ else:
+ self._transaction_ref = weakref.ref(value)
+ return locals()
def get_header_line(self):
- return "%s %s SIP/2.0" % (self._code, self._uri)
+ return "%s %s SIP/2.0" % (self.code, self.uri)
def __repr__(self):
- return "<SIP Request %d:%s %s>" % (id(self), self._code, self._uri)
+ return "<SIP Request %d:%s %s>" % (id(self), self.code, self.uri)
class SIPResponse(SIPMessage):
def __init__(self, status, reason=None):
SIPMessage.__init__(self)
+ self._request_ref = None
self._status = status
if not reason:
reason = RESPONSE_CODES[status]
self._reason = reason
+ @rw_property
+ def request():
+ def fget(self):
+ if self._request_ref is None:
+ return None
+ return self._request_ref()
+ def fset(self, value):
+ if value is None:
+ self._request_ref = None
+ else:
+ self._request_ref = weakref.ref(value)
+ return locals()
+
@property
def code(self):
- cseq = self.get_header("CSeq")
- if not cseq:
- return None
- return cseq.split()[1]
+ return self.cseq.method
@property
def status(self):
@@ -198,7 +256,8 @@
while not finished:
finished = self.parse_buffer()
except Exception, err:
- logger.error("Error while parsing received message: %s", err)
+ logger.error("Error while parsing received message")
+ logger.exception(err)
self.reset()
def parse_buffer(self):
@@ -222,7 +281,7 @@
self._state = "body"
else:
name, value = line.split(":", 1)
- self._message.add_header(name, value.strip())
+ self._message.parse_header(name, value.strip())
if self._state == "body":
missing = self._message.length - len(self._message.body)
@@ -259,3 +318,117 @@
ret = self._buffer[0:count]
self._buffer = self._buffer[count:]
return ret
+
+
+### SIP Message Headers
+
+class SIPContact(object):
+ def __init__(self, name, uri, tag=None, params=None):
+ self.name = name
+ self.uri = uri
+ self.tag = tag
+ if params is None:
+ params = {}
+ self.params = params
+
+ @staticmethod
+ def build(line):
+ contact_spec = "(\"(?P<name>[^\"]*)\")? *\<(?P<uri>[^>]*)\>(;tag=(?P<tag>[^;>]*))?(?P<params>;[^;>]*)*"
+ m = re.match(contact_spec, line)
+ if not m:
+ return None
+ params = {}
+ if m.group("params"):
+ for param in m.group("params").split(";"):
+ if "=" in param:
+ key, value = param.split("=")
+ params[key] = value
+ return SIPContact(m.group("name"), m.group("uri"), m.group("tag"), params)
+
+ def clone(self):
+ params = self.params.copy()
+ return SIPContact(self.name, self.uri, self.tag, params)
+
+ def __str__(self):
+ line = ""
+ if self.name:
+ line += "\"%s\" " % self.name
+ line += "<%s>" % str(self.uri)
+ if self.tag:
+ line += ";tag=%s" % self.tag
+ for key,value in self.params.items():
+ line += ";%s=%s" % (key, str(value))
+ return line
+
+ def __eq__(self, other):
+ return (self.uri == other.uri and
+ self.tag == other.tag and
+ self.params == other.params)
+
+class SIPCSeq(object):
+ def __init__(self, number, method):
+ self.number = number
+ self.method = method
+
+ @staticmethod
+ def build(line):
+ number, method = line.split(" ", 2)
+ return SIPCSeq(int(number), method)
+
+ def clone(self):
+ return SIPCSeq(self.number, self.method)
+
+ def __str__(self):
+ return "%i %s" % (self.number, self.method)
+
+ def __eq__(self, other):
+ return (self.number == other.number and self.method == other.method)
+
+class SIPRoute(object):
+ def __init__(self, route_set):
+ self.route_set = route_set
+
+ @staticmethod
+ def build(line):
+ items = line.split(",")
+ route_set = map(lambda item: re.search("<([^>]*)>", item).group(1), items)
+ return SIPRoute(route_set)
+
+ def clone(self):
+ return SIPRoute(self.route_set[:])
+
+ def __str__(self):
+ return ",".join(map(lambda item: "<%s>" % item, self.route_set))
+
+ def __eq__(self, other):
+ return (self.route_set == other.route_set)
+
+class SIPVia(object):
+ def __init__(self, protocol=None, ip=None, port=None, branch=None):
+ self.protocol = protocol.upper()
+ self.ip = ip
+ self.port = port
+ self.branch = branch
+
+ @staticmethod
+ def build(line):
+ via_spec = "SIP/2.0/(?P<protocol>[a-zA-Z]*) (?P<ip>[0-9\.]*):(?P<port>[0-9]*)(;branch=(?P<branch>[^;>]*))?"
+ m = re.match(via_spec, line)
+ if not m:
+ return None
+ return SIPVia(m.group("protocol"), m.group("ip"),
+ int(m.group("port")), m.group("branch"))
+
+ def clone(self):
+ return SIPVia(self.protocol, self.ip, self.port, self.branch)
+
+ def __str__(self):
+ line = "SIP/2.0/%s %s:%d" % (self.protocol, self.ip, self.port)
+ if self.branch:
+ line += ";branch=%s" % self.branch
+ return line
+
+ def __eq__(self, other):
+ return (self.protocol == other.protocol and
+ self.ip == other.ip and self.port == self.port and
+ self.branch == other.branch)
--- papyon/sip/registration.py
+++ papyon/sip/registration.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from papyon.util import Timer
+
+import gobject
+import logger
+
+__all__ = ['SIPRegistration']
+
+logger = logging.getLogger('papyon.sip.registration')
+
+class SIPRegistration(gobject.GObject):
+
+ __gsignals__ = {
+ 'registered': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ([])),
+ 'unregistered': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ([])),
+ 'failed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ([]))
+ }
+
+ def __init__(self, client, core):
+ gobject.GObject.__init__(self)
+ self._client = client
+ self._core = core
+ self._state = "NEW"
+ self._sso = client._sso
+ self._src = None
+ self._request = None
+ self._pending_unregister = False
+ self._tokens = {}
+
+ self._tag = None
+ self._call_id = None
+ self._cseq = None
+
+ @property
+ def registered(self):
+ return (self._state == "REGISTERED")
+
+ def register(self):
+ if self._state in ("REGISTERING", "REGISTERED", "CANCELLED"):
+ return
+ self._state = "REGISTERING"
+ self._do_register(None, None)
+
+ def unregister(self):
+ if self._state in ("NEW", "UNREGISTERING", "UNREGISTERED", "CANCELLED"):
+ return
+ elif self._state == "REGISTERING":
+ if self._request is None:
+ self._state = "CANCELLED"
+ self.emit("unregistered")
+ else:
+ self._pending_unregister = True
+ return
+
+ self._state = "UNREGISTERING"
+ self._pending_unregister = False
+ if self._src is not None:
+ gobject.source_remove(self._src)
+ self._src = None
+ self._do_unregister(None, None)
+
+ def _send_register(self, timeout, auth):
+ self._request = self._core.register(self._tag, self._call_id,
+ self._cseq, timeout, auth)
+ self._tag = self._request.From.tag
+ self._call_id = self._request.call_id
+ self._cseq = self._request.cseq.number + 1
+
+ @RequireSecurityTokens(LiveService.MESSENGER_SECURE)
+ def _do_register(self, callback, errback):
+ # Check if state changed while requesting security token
+ if self._state != "REGISTERING":
+ return
+ auth = "msmsgs:RPS_%s" % self._tokens[LiveService.MESSENGER_SECURE]
+ auth = base64.b64encode(auth).replace("\n", "")
+ self._send_register(900, auth)
+
+ @RequireSecurityTokens(LiveService.MESSENGER_SECURE)
+ def _do_unregister(self, callback, errback):
+ auth = "%s:%s" % (self._account, self._tokens[LiveService.MESSENGER_SECURE])
+ auth = base64.encodestring(auth).replace("\n", "")
+ self._send_register(0, auth)
+
+ def _on_expire(self):
+ self.register()
+ return False
+
+ def handle_response(self, response):
+ if self._state == "UNREGISTERING":
+ self._state = "UNREGISTERED"
+ self.emit("unregistered")
+ elif self._state != "REGISTERING":
+ return # strange !?
+ elif response.status is 200:
+ self._state = "REGISTERED"
+ self.emit("registered")
+ timeout = int(response.get_header("Expires", 30))
+ self._src = gobject.timeout_add_seconds(timeout, self._on_expire)
+ if self._pending_unregister:
+ self.unregister()
+ else:
+ self._state = "UNREGISTERED"
+ self.emit("failed")
--- papyon/sip/sdp.py
+++ papyon/sip/sdp.py
@@ -36,6 +36,10 @@
MediaSessionMessage.__init__(self, session, body)
@property
+ def type(self):
+ return "application/sdp"
+
+ @property
def ip(self):
if self._ip == "" and len(self._descriptions) > 0:
return self._descriptions[0].ip
--- papyon/sip/transaction.py
+++ papyon/sip/transaction.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+from papyon.sip.constants import SIPTransactionError, TIMER_B, TIMER_F, TIMER_H
+from papyon.sip.message import SIPResponse, SIPRequest, SIPCSeq
+from papyon.util.decorator import rw_property
+from papyon.util.timer import Timer
+
+import gobject
+import logging
+
+logger = logging.getLogger('papyon.sip.transaction')
+
+class SIPTransactionLayer(gobject.GObject):
+ """ This class represents the SIP transaction layer as described in
+ section 17 of RFC 2361.
+
+ The transaction layer handles application-layer retransmissions,
+ matching of responses to requests, and application-layer timeouts.
+ Any task that a user agent client (UAC) accomplishes takes place
+ using a series of transactions.
+
+ TODO: Use branch parameter in Via to identify transactions"""
+
+ __gsignals__ = {
+ 'request-received' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
+ 'response-received' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
+ 'error' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object, object))
+ }
+
+ def __init__(self, transport):
+ gobject.GObject.__init__(self)
+ self._transport = transport
+ self._transport.connect("message-received", self._on_message_received)
+ self._transactions = {} # transaction => event handles
+
+ def is_merged_request(self, request):
+ for transaction in self._transactions:
+ if transaction is request.transaction: continue
+ if request.match_headers(("From", "Call-Id", "CSeq"),
+ transaction.request):
+ return True
+ return False
+
+ def find_canceled_transaction(self, cancel):
+ return self._find_transaction(cancel, False)
+
+ def send(self, message):
+ transaction = None
+ if type(message) is SIPRequest:
+ transaction = SIPClientTransaction(self._transport)
+ self._add_transaction(transaction)
+ elif type(message) is SIPResponse:
+ transaction = message.request.transaction
+
+ if transaction is not None:
+ transaction.send(message)
+
+ def _add_transaction(self, transaction):
+ handles = []
+ handles.append(transaction.connect("request-received",
+ self._on_request_received))
+ handles.append(transaction.connect("response-received",
+ self._on_response_received))
+ handles.append(transaction.connect("terminated",
+ self._on_transaction_terminated))
+ handles.append(transaction.connect("error",
+ self._on_transaction_error))
+ self._transactions[transaction] = handles
+
+ def _del_transaction(self, transaction):
+ handles = self._transactions.pop(transaction)
+ for handle in handles:
+ transaction.disconnect(handle)
+
+ def _find_transaction(self, msg, match_method=True):
+ for transaction in self._transactions:
+ if match_message_to_transaction(msg, transaction, match_method):
+ return transaction
+ return None
+
+ def _on_message_received(self, transport, msg):
+ transaction = self._find_transaction(msg)
+ if type(msg) is SIPResponse:
+ if transaction is not None:
+ transaction._on_response_received(msg)
+ else:
+ logger.info("Can't find matching transaction for response")
+ self.emit("response-received", msg)
+ elif type(msg) is SIPRequest:
+ if transaction is None and msg.code != "ACK":
+ transaction = SIPServerTransaction(self._transport, msg)
+ self._add_transaction(transaction)
+ if transaction is not None:
+ transaction._on_request_received(msg)
+ else:
+ self.emit("request-received", msg)
+
+ def _on_request_received(self, transaction, request):
+ self.emit("request-received", request)
+
+ def _on_response_received(self, transaction, response):
+ self.emit("response-received", response)
+
+ def _on_transaction_terminated(self, transaction):
+ self._del_transaction(transaction)
+
+ def _on_transaction_error(self, transaction, error):
+ self.emit("error", transaction, error)
+ self._del_transaction(transaction)
+
+
+class SIPTransaction(gobject.GObject, Timer):
+ """ A SIP transaction consists of a single request and any responses to
+ that request, which include zero or more provisional responses and
+ one or more final responses. In the case of an INVITE transaction, the
+ transaction also includes the ACK only if the final response was not
+ a 2xx response."""
+
+ __gsignals__ = {
+ 'request-received' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
+ 'response-received' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
+ 'terminated' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ()),
+ 'error' : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
+ }
+
+ def __init__(self, transport):
+ gobject.GObject.__init__(self)
+ Timer.__init__(self)
+ self._transport = transport
+ self._branch = None
+ self._method = None
+ self._request = None
+ self.__state = None
+
+ @property
+ def answered(self):
+ """Final response received or not"""
+ return bool(self.__state in ("COMPLETED", "TERMINATED"))
+
+ @rw_property
+ def _state():
+ def fget(self):
+ return self.__state
+ def fset(self, value):
+ if self.__state != value:
+ old_state = self.__state
+ self.__state = value
+ self._on_state_changed(old_state)
+ return locals()
+
+ @property
+ def request(self):
+ return self._request
+
+ @property
+ def is_invite(self):
+ return self.request.code == "INVITE"
+
+ def destroy(self):
+ self._state = "TERMINATED"
+
+ def _send(self, message):
+ self._transport.send(message) #FIXME add errcb
+
+ def _on_state_changed(self, old_state):
+ raise NotImplementedError
+
+ def _on_transport_error(self):
+ self.emit("error", SIPTransactionError.TRANSPORT_ERROR)
+ self._state = "TERMINATED"
+
+
+# Client Transaction ---------------------------------------------------------
+
+class SIPClientTransaction(SIPTransaction):
+ """ Client component of the transaction layer used to send request.
+ It's basically a state machine, working differently wether the request
+ is an INVITE one or not.
+
+ See section 17.1 of RFC 2361 for details.
+
+ Note: We assume that the transport layer is reliable. """
+
+ def __init__(self, transport):
+ SIPTransaction.__init__(self, transport)
+
+ def send(self, message):
+ if type(message) is SIPResponse:
+ logger.error("Tried to send a response from client transaction")
+ self._state = "TERMINATED"
+ return False
+ if self._state is not None:
+ logger.error("Can't send more thant one request per transaction")
+ return False
+
+ self._state = "TRYING"
+ self._request = message
+ self._request.transaction = self
+ self._send(message)
+ if self.is_invite:
+ self.start_timeout("timerB", TIMER_B)
+ else:
+ self.start_timeout("timerF", TIMER_F)
+ return True
+
+ def _ack(self, response):
+ if not self.is_invite:
+ return # no need for ACK in non-INVITE transactions
+ logger.info("Acking non-2xx INVITE response")
+ request = self._create_ack_request(response)
+ self._send(request)
+
+ def _create_ack_request(self, response):
+ # 17.1.1.3 Construction of the ACK Request
+ request = SIPRequest("ACK", self._request.uri)
+ request.clone_headers("Route", self._request)
+ request.clone_headers("To", response)
+ request.clone_headers("From", self._request)
+ request.clone_headers("Call-ID", self._request)
+ request.clone_headers("Via", self._request) #FIXME single via
+ request.add_header("CSeq", SIPCSeq(self._request.cseq.number, "ACK"))
+ return request
+
+ def _on_request_received(self, request):
+ logger.error("Received a request in client transaction")
+
+ def _on_response_received(self, response):
+ response.request = self.request
+ if response.status/100 == 1: # provisional response
+ if self._state == "TRYING":
+ self._state = "PROCEEDING"
+ self.emit("response-received", response)
+ elif response.status/100 == 2: # OK final response
+ if self._state in ("TRYING", "PROCEEDING"):
+ self.emit("response-received", response)
+ self._state = "TERMINATED"
+ elif response.status >= 300: # other final response
+ if self._state in ("TRYING", "PROCEEDING"):
+ self.emit("response-received", response)
+ self._ack(response)
+ self._state = "COMPLETED"
+
+ def _on_state_changed(self, old_state):
+ if self._state == "COMPLETED":
+ self._state = "TERMINATED" # skip COMPLETED state
+ elif self._state == "TERMINATED":
+ self.stop_all_timeout()
+ self.emit("terminated")
+
+ def on_timerB_timeout(self):
+ if self._state == "TRYING":
+ self.emit("error", SIPTransactionError.TIMEOUT)
+ self._state = "TERMINATED"
+
+ def on_timerF_timeout(self):
+ if self._state in ("TRYING", "PROCEEDING"):
+ self.emit("error", SIPTransactionError.TIMEOUT)
+ self._state = "TERMINATED"
+
+ def _on_transport_error(self):
+ self.emit("error", SIPTransactionError.TRANSPORT_ERROR)
+ self._state = "TERMINATED"
+
+
+# Server Transaction ---------------------------------------------------------
+
+class SIPServerTransaction(SIPTransaction):
+ """ Server component of the transaction layer created when receiving requests.
+ It's basically a state machine, working differently wether the request
+ is an INVITE one or not.
+
+ See section 17.2 of RFC 2361 for details.
+
+ Note: We assume that the transport layer is reliable. """
+
+ def __init__(self, transport, request):
+ SIPTransaction.__init__(self, transport)
+ self._request = request
+ self._last_response = None
+ request.transaction = self
+
+ if self.is_invite:
+ self._state = "PROCEEDING"
+ else:
+ self._state = "TRYING"
+
+
+ def send(self, message):
+ if type(message) is SIPRequest:
+ logger.error("Tried to send a request from server transaction")
+ return False
+
+ if self._state not in ("TRYING", "PROCEEDING"):
+ return False
+
+ self._last_response = message
+ if message.status/100 == 1:
+ self._send(message)
+ if self._state == "TRYING":
+ self._state = "PROCEEDING"
+ elif message.status/100 >= 2:
+ self._send(message)
+ if self.is_invite and message.status/100 == 2:
+ self._state = "TERMINATED"
+ else:
+ self._state = "COMPLETED"
+ return True
+
+ def _on_request_received(self, request):
+ # receive ACK for response
+ if request.code == "ACK":
+ self._on_ack_received(request)
+ # request retransmission => send last response received from TU
+ elif self._last_response is not None and \
+ self._state in ("PROCEEDING", "COMPLETED"):
+ self.info("Request retransmission detected => send last response")
+ self._send(self._last_response)
+ else:
+ self.emit("request-received", request)
+
+ def _on_ack_received(self, ack):
+ if self._state != "COMPLETED":
+ return
+ self._state = "CONFIRMED"
+
+ def _on_response_received(self, response):
+ logger.error("Received a response in server transaction")
+
+ def _on_state_changed(self, old_state):
+ if self._state == "COMPLETED":
+ if not self.is_invite:
+ self._state = "TERMINATED" # skip CONFIRMED state
+ else:
+ self.start_timeout("timerH", TIMER_H)
+ elif self._state == "CONFIRMED":
+ self.stop_timeout("timerH")
+ self._state = "TERMINATED" # skip CONFIRMED state
+ elif self._state == "TERMINATED":
+ self.stop_all_timeout()
+ self.emit("terminated")
+
+ def on_timerH_timeout(self):
+ self._state = "TERMINATED"
+ self.emit("error", SIPTransactionError.TIMEOUT)
+
+
+
+# Transaction Matching Util Functions ----------------------------------------
+
+def match_response_to_transaction(response, transaction, match_method=True):
+ """ RFC3261: 17.1.3 Matching Responses to Client Transactions """
+ # Reponse To header might have a mepid and tag added
+ if not response.To.uri.startswith(transaction.request.To.uri):
+ return False
+ headers = ("Call-ID", "CSeq", "From")
+ return response.match_headers(headers, transaction.request)
+
+def match_request_to_transaction(request, transaction, match_method=True):
+ """ RFC3261: 17.2.3 Matching Requests to Server Transactions
+
+ INVITE: Request-URI, Call-Id, top Via, Cseq, From tag, To tag
+ ACK: Request-URI, Call-Id, top Via, Cseq number, From tag
+ Other methods: Request-URI, Call-Id, top Via, Cseq, From tag, To tag
+ """
+ if (request.uri != transaction.request.uri or
+ not request.match_headers(("Call-Id", "Via"), transaction.request) or
+ request.From.tag != transaction.request.From.tag):
+ return False
+
+ if request.code == "ACK" or not match_method:
+ if request.cseq.number != transaction.request.cseq.number:
+ return False
+ elif not request.match_header("CSeq", transaction.request):
+ return False
+
+ if request.code != "ACK" and request.to.tag != transaction.request.to.tag:
+ return False
+
+ return True
+
+def match_message_to_transaction(msg, transaction, match_method=True):
+ if type(msg) is SIPResponse:
+ return match_response_to_transaction(msg, transaction)
+ elif type(msg) is SIPRequest:
+ return match_request_to_transaction(msg, transaction, match_method)
+ return False
+
--- papyon/sip/transport.py
+++ papyon/sip/transport.py
@@ -2,7 +2,7 @@
#
# papyon - a python client library for Msn
#
-# Copyright (C) 2009 Collabora Ltd.
+# Copyright (C) 2009-2010 Collabora Ltd.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -21,7 +21,7 @@
from papyon.gnet.constants import *
from papyon.gnet.io import *
from papyon.msnp.constants import *
-from papyon.sip.message import SIPMessageParser
+from papyon.sip.message import SIPResponse, SIPRequest, SIPMessageParser, SIPVia
import base64
import gobject
@@ -39,6 +39,18 @@
([object]))
}
+ @property
+ def protocol(self):
+ raise NotImplementedError
+
+ @property
+ def ip(self):
+ raise NotImplementedError
+
+ @property
+ def port(self):
+ raise NotImplementedError
+
def __init__(self):
gobject.GObject.__init__(self)
self._parser = SIPMessageParser()
@@ -48,6 +60,12 @@
self.emit("message-received", message)
def send(self, message):
+ if type(message) is SIPRequest:
+ message.add_header("Via", SIPVia(self.protocol, self.ip, self.port))
+ message.add_header("Max-Forwards", 70)
+ self._send(message)
+
+ def _send(self, message):
raise NotImplementedError
def log_message(self, prefix, message):
@@ -55,81 +73,9 @@
logger.debug(prefix + " " + line)
-class SIPTransport(SIPBaseTransport):
- """Default transport on older MSNP versions. The messages are sent over a
- typical SSL TCP connection."""
-
- def __init__(self, host, port):
- SIPBaseTransport.__init__(self)
- self._client = SSLTCPClient(host, port)
- self._client.connect("received", self.on_received)
- self._client.connect("notify::status", self.on_status_changed)
- self._alive_src = None
- self._closing = False
- self._msg_queue = []
-
- @property
- def protocol(self):
- return "tls"
-
- def send(self, message):
- data = str(message)
- if self._client.status == IoStatus.OPEN:
- self.log_message(">>", data)
- self._send(data)
- else:
- self._msg_queue.append(message)
- self._open()
-
- def _open(self):
- if self._client.status == IoStatus.OPEN:
- return
- self._close()
- self._closing = False
- self._client.open()
- self._start_keep_alive()
-
- def _close(self):
- if self._client.status == IoStatus.CLOSED:
- return
- self._stop_keep_alive()
- self._closing = True
- self._client.close()
-
- def _send(self, data):
- self._client.send(data)
-
- def _start_keep_alive(self):
- self._alive_src = gobject.timeout_add(5000, self._on_keep_alive)
-
- def _stop_keep_alive(self):
- if self._alive_src is not None:
- gobject.source_remove(self._alive_src)
- self._alive_src = None
-
- def _on_keep_alive(self):
- if self._client.status == IoStatus.OPEN:
- self._send("\r\n\r\n\r\n\r\n")
- return True
- else:
- return False
-
- def on_received(self, client, chunk, len):
- self.log_message("<<", chunk)
- self._parser.append(chunk)
-
- def on_status_changed(self, client, param):
- if self._client.status == IoStatus.OPEN:
- while self._msg_queue:
- self.send(self._msg_queue.pop(0))
- elif self._client.status == IoStatus.CLOSED:
- if not self._closing:
- self._open()
-
-
class SIPTunneledTransport(SIPBaseTransport):
"""Default SIP transport with newer MSNP versions (>= 18). The messages
- are base64 encoded and sent to the notication server using a UBX
+ are base64 encoded and sent to the notication server using a UBN
command."""
def __init__(self, protocol):
@@ -142,21 +88,38 @@
def protocol(self):
return "tcp"
- def send(self, message):
- call_id = message.call.id
- contact = message.call.contact
+ @property
+ def ip(self):
+ return "127.0.0.1"
+
+ @property
+ def port(self):
+ return 50390
+
+ def _send(self, message):
+ call_id = message.call_id
+ if type(message) is SIPResponse:
+ contact = message.From.uri.replace("sip:", "")
+ else:
+ contact = message.To.uri.replace("sip:", "")
+ #FIXME hack
+ guid = None
+ if ";mepid=" in contact:
+ contact, guid = contact.split(";mepid=")
+ guid = "%s-%s-%s-%s-%s" % (guid[0:8], guid[8:12], guid[12:16],
+ guid[16:20], guid[20:32])
+ guid = guid.lower()
self.log_message(">>", str(message))
data = base64.b64encode(str(message))
data = '<sip e="base64" fid="1" i="%s"><msg>%s</msg></sip>' % \
(call_id, data)
data = data.replace("\r\n", "\n").replace("\n", "\r\n")
- self._protocol.send_user_notification(data, contact,
+ self._protocol.send_user_notification(data, contact, guid,
UserNotificationTypes.TUNNELED_SIP)
- def on_notification_received(self, protocol, type, notification):
+ def on_notification_received(self, protocol, peer, peer_guid, type, message):
if type is not UserNotificationTypes.TUNNELED_SIP:
return
- message = notification.payload
try:
doc = xml.dom.minidom.parseString(message)
sip = doc.firstChild
--- papyon/switchboard_manager.py
+++ papyon/switchboard_manager.py
@@ -28,6 +28,7 @@
import weakref
import papyon.msnp as msnp
+from papyon.profile import Presence
from papyon.transport import ServerType
from papyon.util.weak import WeakSet
from papyon.event import ConversationErrorType, ContactInviteError, MessageError
@@ -36,23 +37,25 @@
logger = logging.getLogger('papyon.protocol.switchboard_manager')
-class SwitchboardClient(object):
- def __init__(self, client, contacts, priority=99):
+class SwitchboardHandler(object):
+ def __init__(self, client, switchboard, contacts, priority=99):
self._client = client
self._switchboard_manager = weakref.proxy(self._client._switchboard_manager)
self.__switchboard = None
self._switchboard_requested = False
self._switchboard_priority = priority
- self._pending_invites = set(contacts)
+ self.participants = set()
+ self._pending_invites = set()
self._pending_messages = []
- self._delivery_callbacks = {}
+ self._pending_handles = {}
+ self._delivery_callbacks = {} # transaction_id => (callback, errback)
- if self._client.protocol_version >= 16:
- self._pending_invites.add(self._client.profile)
+ for contact in contacts:
+ self.__add_pending(contact)
- self.participants = set()
- self._process_pending_queues()
+ if switchboard is not None:
+ self._switchboard = switchboard
@staticmethod
def _can_handle_message(message, switchboard_client=None):
@@ -72,6 +75,8 @@
self.switchboard.connect("notify::inviting",
lambda sb, pspec: self.__on_user_inviting_changed())
+ self.switchboard.connect("notify::state",
+ lambda sb, pspec: self.__on_switchboard_state_changed())
self.switchboard.connect("user-joined",
lambda sb, contact: self.__on_user_joined(contact))
self.switchboard.connect("user-left",
@@ -82,7 +87,8 @@
lambda sb, trid: self.__on_message_delivered(trid))
self.switchboard.connect("message-undelivered",
lambda sb, trid: self.__on_message_undelivered(trid))
- logger.info("New switchboard attached")
+ logger.info("Handler %s attached to switchboard %s" %
+ (repr(self), switchboard.session_id))
def process_pending_queues():
self._process_pending_queues()
return False
@@ -93,7 +99,7 @@
# protected
def _send_message(self, content_type, body, headers={},
- ack=msnp.MessageAcknowledgement.HALF, callback=None, cb_args=()):
+ ack=msnp.MessageAcknowledgement.HALF, callback=None, errback=None):
message = msnp.Message(self._client.profile)
message.add_header('MIME-Version', '1.0')
message.content_type = content_type
@@ -101,11 +107,11 @@
message.add_header(key, value)
message.body = body
- self._pending_messages.append((message, ack, callback, cb_args))
+ self._pending_messages.append((message, ack, callback, errback))
self._process_pending_queues()
def _invite_user(self, contact):
- self._pending_invites.add(contact)
+ self.__add_pending(contact)
self._process_pending_queues()
def _leave(self):
@@ -124,41 +130,79 @@
def _on_contact_left(self, contact):
raise NotImplementedError
+ def _on_switchboard_closed(self):
+ raise NotImplementedError
+
+ def _on_closed(self):
+ raise NotImplementedError
+
def _on_error(self, error_type, error):
raise NotImplementedError
# private
+ def __add_pending(self, contact):
+ if contact in self._pending_invites or contact is self._client.profile:
+ return
+ self._pending_invites.add(contact)
+ handle = contact.connect("notify::presence",
+ lambda contact, pspec: self.__on_user_presence_changed(contact))
+ self._pending_handles[contact] = handle
+
+ def __remove_pending(self, contact):
+ self._pending_invites.discard(contact)
+ if contact in self._pending_handles:
+ contact.disconnect(self._pending_handles[contact])
+ del self._pending_handles[contact]
+
def __on_user_inviting_changed(self):
if not self.switchboard.inviting:
self._process_pending_queues()
def __on_user_joined(self, contact):
self.participants.add(contact)
- self._pending_invites.discard(contact)
+ self.__remove_pending(contact)
self._on_contact_joined(contact)
def __on_user_left(self, contact):
self._on_contact_left(contact)
self.participants.remove(contact)
if len(self.participants) == 0:
- self._pending_invites.add(contact)
- try:
- self._switchboard.leave()
- except:
- pass
+ self.__add_pending(contact)
+
+ def __on_user_presence_changed(self, contact):
+ if (self._switchboard and self.switchboard.state == msnp.ProtocolState.OPEN) or self._switchboard_requested:
+ return
+ for contact in self._pending_invites:
+ if contact.presence != Presence.OFFLINE:
+ return
+ # Switchboard was already closed and there is (almost) no chance
+ # a new one will be created now.
+ logger.info("All pending invites are now offline, closing handler")
+ self._leave()
+
+ def __on_switchboard_state_changed(self):
+ if self._switchboard.state == msnp.ProtocolState.CLOSED:
+ for contact in self._pending_invites:
+ if contact.presence != Presence.OFFLINE:
+ return
+ # Might be that an "appear offline" contact closed the
+ # switchboard or that the switchboard has been explicitely closed
+ self._leave()
def __on_user_invitation_failed(self, contact):
- self._pending_invites.discard(contact)
+ self.__remove_pending(contact)
self._on_error(ConversationErrorType.CONTACT_INVITE,
ContactInviteError.NOT_AVAILABLE)
- def __on_message_delivered(self, transaction_id):
- if transaction_id in self._delivery_callbacks:
- callback, cb_args = self._delivery_callbacks.pop(transaction_id)
- if callback:
- callback(*cb_args)
-
- def __on_message_undelivered(self, transaction_id):
+ def __on_message_delivered(self, trid):
+ callback, errback = self._delivery_callbacks.pop(trid, (None, None))
+ if callback:
+ callback[0](*callback[1:])
+
+ def __on_message_undelivered(self, trid):
+ callback, errback = self._delivery_callbacks.pop(trid, (None, None))
+ if errback:
+ errback[0](*errback[1:])
self._on_error(ConversationErrorType.MESSAGE,
MessageError.DELIVERY_FAILED)
@@ -174,17 +218,20 @@
for contact in self._pending_invites:
if contact not in self.participants:
self.switchboard.invite_user(contact)
+ for contact, handle in self._pending_handles.items():
+ contact.disconnect(handle)
self._pending_invites = set()
+ self._pending_handles = dict()
if not self.switchboard.inviting:
- for message, ack, callback, cb_args in self._pending_messages:
+ for message, ack, callback, errback in self._pending_messages:
# if ack type is FULL or MSNC, wait for ACK before calling back
if ack in (msnp.MessageAcknowledgement.FULL,
msnp.MessageAcknowledgement.MSNC):
transaction_id = self.switchboard.send_message(message, ack)
- self._delivery_callbacks[transaction_id] = (callback, cb_args)
+ self._delivery_callbacks[transaction_id] = (callback, errback)
else:
- self.switchboard.send_message(message, ack, callback, cb_args)
+ self.switchboard.send_message(message, ack, callback)
self._pending_messages = []
@@ -194,9 +241,9 @@
return False
if self._switchboard_requested:
return True
- logger.info("requesting new switchboard")
self._switchboard_requested = True
- self._pending_invites |= self.participants
+ for participant in self.participants:
+ self.__add_pending(participant)
self.participants = set()
self._switchboard_manager.request_switchboard(self, self._switchboard_priority) # may set the switchboard immediatly
return self._switchboard_requested
@@ -224,6 +271,7 @@
self._orphaned_handlers = WeakSet()
self._switchboards = {}
self._orphaned_switchboards = set()
+ self._requested_switchboards = {}
self._pending_switchboards = {}
self._client._protocol.connect("switchboard-invitation-received",
@@ -242,6 +290,8 @@
def request_switchboard(self, handler, priority=99):
handler_participants = handler.total_participants
+ participants = ", ".join(map(lambda c: c.account, handler_participants))
+ logger.info("Requesting switchboard for participant(s) %s" % participants)
# If the Handler was orphan, then it is no more
self._orphaned_handlers.discard(handler)
@@ -250,6 +300,8 @@
for switchboard in self._switchboards.keys():
switchboard_participants = set(switchboard.participants.values())
if handler_participants == switchboard_participants:
+ logger.info("Using already opened switchboard %s" %
+ switchboard.session_id)
self._switchboards[switchboard].add(handler)
handler._switchboard = switchboard
return
@@ -258,25 +310,38 @@
for switchboard in list(self._orphaned_switchboards):
switchboard_participants = set(switchboard.participants.values())
if handler_participants == switchboard_participants:
+ logger.info("Using orphaned switchboard %s" %
+ switchboard.session_id)
self._switchboards[switchboard] = set([handler]) #FIXME: WeakSet ?
self._orphaned_switchboards.discard(switchboard)
handler._switchboard = switchboard
return
- # Check being requested switchboards
+ # Check pending switchboards
for switchboard, handlers in self._pending_switchboards.iteritems():
pending_handler = handlers.pop()
handlers.add(pending_handler)
switchboard_participants = pending_handler.total_participants
if handler_participants == switchboard_participants:
self._pending_switchboards[switchboard].add(handler)
+ logger.info("Using pending switchboard")
return
- self._client._protocol.\
- request_switchboard(priority, self._ns_switchboard_request_response, handler)
+ # Check switchboards being requested for same participants
+ if participants in self._requested_switchboards:
+ self._requested_switchboards[participants].add(handler)
+ logger.info("Using already requested switchboard for same contacts")
+ return
+
+ logger.info("Requesting new switchboard")
+ self._requested_switchboards[participants] = set([handler])
+ self._client._protocol.request_switchboard(priority,
+ self._ns_switchboard_request_response, participants)
def close_handler(self, handler):
+ logger.info("Closing switchboard handler %s" % repr(handler))
self._orphaned_handlers.discard(handler)
+ handler._on_closed()
for switchboard in self._switchboards.keys():
handlers = self._switchboards[switchboard]
handlers.discard(handler)
@@ -292,9 +357,10 @@
del self._pending_switchboards[switchboard]
self._orphaned_switchboards.add(switchboard)
- def _ns_switchboard_request_response(self, session, handler):
+ def _ns_switchboard_request_response(self, session, participants):
switchboard = self._build_switchboard(session)
- self._pending_switchboards[switchboard] = set([handler]) #FIXME: WeakSet ?
+ handlers = self._requested_switchboards.pop(participants, set())
+ self._pending_switchboards[switchboard] = handlers
def _ns_switchboard_invite(self, protocol, session, inviter):
switchboard = self._build_switchboard(session)
@@ -350,6 +416,7 @@
if switchboard in self._switchboards.keys():
for handler in self._switchboards[switchboard]:
self._orphaned_handlers.add(handler)
+ handler._on_switchboard_closed()
del self._switchboards[switchboard]
self._orphaned_switchboards.discard(switchboard)
@@ -367,9 +434,9 @@
continue
if not handler_class._can_handle_message(message):
continue
- handler = handler_class(self._client, (), *extra_args)
+ handler = handler_class.handle_message(self._client,
+ switchboard, message, *extra_args)
handlers.add(handler)
- handler._switchboard = switchboard
self.emit("handler-created", handler_class, handler)
handler._on_message_received(message)
@@ -377,9 +444,9 @@
for handler_class, extra_args in self._handlers_class:
if not handler_class._can_handle_message(message):
continue
- handler = handler_class(self._client, (), *extra_args)
+ handler = handler_class.handle_message(self._client,
+ switchboard, message, *extra_args)
self._switchboards[switchboard] = set([handler]) #FIXME: WeakSet ?
self._orphaned_switchboards.discard(switchboard)
- handler._switchboard = switchboard
self.emit("handler-created", handler_class, handler)
handler._on_message_received(message)
--- papyon/transport.py
+++ papyon/transport.py
@@ -5,6 +5,7 @@
# Copyright (C) 2005-2006 Ali Sabil <ali.sabil at gmail.com>
# Copyright (C) 2006 Johann Prieur <johann.prieur at gmail.com>
# Copyright (C) 2006 Ole André Vadla Ravnås <oleavr at gmail.com>
+# Copyright (C) 2010 Collabora Ltd.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -30,6 +31,8 @@
The classes of this module are structured as follow:
G{classtree BaseTransport}"""
+from gnet.proxy.factory import ProxyFactory
+
import gnet
import gnet.protocol
import msnp
@@ -225,10 +228,7 @@
def __init__(self, server, server_type=ServerType.NOTIFICATION, proxies={}):
BaseTransport.__init__(self, server, server_type, proxies)
- transport = gnet.io.TCPClient(server[0], server[1])
- transport.connect("notify::status", self.__on_status_change)
- transport.connect("error", self.__on_error)
-
+ transport = self._setup_transport(server[0], server[1], proxies)
receiver = gnet.parser.DelimiterParser(transport)
receiver.connect("received", self.__on_received)
@@ -242,6 +242,14 @@
__init__.__doc__ = BaseTransport.__init__.__doc__
+ def _setup_transport(self, host, port, proxies):
+ transport = gnet.io.TCPClient(host, port)
+ if proxies:
+ transport = ProxyFactory(transport, proxies, 'direct')
+ transport.connect("notify::status", self.__on_status_change)
+ transport.connect("error", self.__on_error)
+ return transport
+
### public commands
@property
@@ -292,7 +300,7 @@
def __handle_ping_reply(self, command):
timeout = int(command.arguments[0])
- self.__png_timeout = gobject.timeout_add(timeout * 1000, self.enable_ping)
+ self.__png_timeout = gobject.timeout_add_seconds(timeout, self.enable_ping)
### callbacks
def __on_status_change(self, transport, param):
@@ -350,28 +358,25 @@
self._target_server = server
server = ("gateway.messenger.hotmail.com", 80)
BaseTransport.__init__(self, server, server_type, proxies)
- self._setup_transport()
+ self._setup_transport(server[0], server[1], proxies)
self._command_queue = []
self._waiting_for_response = False # are we waiting for a response
self._session_id = None
self.__error = False
- def _setup_transport(self):
- server = self.server
- proxies = self.proxies
- if 'http' in proxies:
- transport = gnet.protocol.HTTP(server[0], server[1], proxies['http'])
- else:
- transport = gnet.protocol.HTTP(server[0], server[1])
- transport.connect("response-received", self.__on_received)
- transport.connect("request-sent", self.__on_sent)
- transport.connect("error", self.__on_error)
+ def _setup_transport(self, host, port, proxies):
+ handles = []
+ transport = gnet.protocol.HTTP(host, port, proxies)
+ handles.append(transport.connect("response-received", self.__on_received))
+ handles.append(transport.connect("request-sent", self.__on_sent))
+ handles.append(transport.connect("error", self.__on_error))
+ self._transport_handles = handles
self._transport = transport
def establish_connection(self):
logger.debug('<-> Connecting to %s:%d' % self.server)
- self._polling_source_id = gobject.timeout_add(5000, self._poll)
+ self._polling_source_id = gobject.timeout_add_seconds(5, self._poll)
self.emit("connection-success")
def lose_connection(self):
@@ -386,6 +391,16 @@
self._target_server = server
self.emit("connection-reset")
+ def change_gateway(self, server):
+ if self.server == server:
+ return
+ logger.debug('<-> Changing gateway to %s:%d' % server)
+ self.server = server
+ for handle in self._transport_handles:
+ self._transport.disconnect(handle)
+ self._transport.close()
+ self._setup_transport(server[0], server[1], self.proxies)
+
def send_command(self, command, increment=True, callback=None, *cb_args):
self._command_queue.append((command, increment, callback, cb_args))
self._send_command()
@@ -431,12 +446,12 @@
def __on_error(self, transport, reason):
self.__error = True
+ if reason == 403:
+ reason = TransportError.PROXY_FORBIDDEN
self.emit("connection-lost", reason)
+ self.lose_connection()
def __on_received(self, transport, http_response):
- if http_response.status == 403:
- self.emit("connection-lost", TransportError.PROXY_FORBIDDEN)
- self.lose_connection()
if 'X-MSN-Messenger' in http_response.headers:
data = http_response.headers['X-MSN-Messenger'].split(";")
for elem in data:
@@ -444,8 +459,7 @@
if key == 'SessionID':
self._session_id = value
elif key == 'GW-IP':
- self.server = (value, self.server[1])
- self._setup_transport()
+ self.change_gateway((value, self.server[1]))
elif key == 'Session'and value == 'close':
#self.lose_connection()
pass
--- papyon/util/decorator.py
+++ papyon/util/decorator.py
@@ -99,12 +99,12 @@
def process_queue():
if len(self._queue) != 0:
func, args, kwargs = self._queue.pop(0)
- self._last_call_time = time.time() * 1000
+ self._last_call_time = time.time()
func(*args, **kwargs)
return False
def new_function(*args, **kwargs):
- now = time.time() * 1000
+ now = time.time()
if self._last_call_time is None or \
now - self._last_call_time >= self._min_delay:
self._last_call_time = now
@@ -113,7 +113,7 @@
self._queue.append((func, args, kwargs))
last_call_delta = now - self._last_call_time
process_queue_timeout = int(self._min_delay * len(self._queue) - last_call_delta)
- gobject.timeout_add(process_queue_timeout, process_queue)
+ gobject.timeout_add_seconds(process_queue_timeout, process_queue)
new_function.__name__ = func.__name__
new_function.__doc__ = func.__doc__
--- papyon/util/encoding.py
+++ papyon/util/encoding.py
@@ -18,12 +18,73 @@
#
import base64
+import email.quoprimime
+import email.base64mime
+import re
+
+
+PADDING = ('', '=', '==', 'A', 'A=', 'A==')
def fix_b64_padding(string):
- while True:
+ for pad in PADDING:
try:
- base64.b64decode(string)
- break
+ base64.b64decode(string + pad)
+ return string + pad
except:
- string += "="
+ continue
return string
+
+def b64_decode(string):
+ for pad in PADDING:
+ try:
+ return base64.b64decode(string + pad)
+ except:
+ continue
+ raise TypeError
+
+
+# Match encoded-word strings in the form =?charset?q?Hello_World?=
+ecre = re.compile(r'''
+ =\? # literal =?
+ (?P<charset>[^?]*?) # non-greedy up to the next ? is the charset
+ \? # literal ?
+ (?P<encoding>[qb]) # either a "q" or a "b", case insensitive
+ \? # literal ?
+ (?P<encoded>.*?) # non-greedy up to the next ?= is the encoded string
+ \?= # literal ?=
+ (?=[ \t]|$) # whitespace or the end of the string
+ ''', re.VERBOSE | re.IGNORECASE | re.MULTILINE)
+
+def decode_rfc2047_string(string):
+ """ Decode it according to RFC 2047. This code has been adapted from
+ Python code (email.header.decode_header), except we don't strip the
+ unencoded parts and we decode all the parts instead of just returning
+ the encoded parts and charsets. """
+ # If no encoding, just return the string
+ if not ecre.search(string):
+ return string
+
+ decoded = ""
+
+ try:
+ for line in string.splitlines():
+ parts = ecre.split(line)
+ while parts:
+ unenc = parts.pop(0)
+ # don't append single spaces between encoded words to the result
+ if not decoded or not parts or unenc != ' ':
+ decoded += unenc
+ if parts:
+ charset, encoding = [s.lower() for s in parts[0:2]]
+ encoded = parts[2]
+ dec = encoded
+ if encoding == 'q':
+ dec = email.quoprimime.header_decode(encoded)
+ elif encoding == 'b':
+ dec = email.base64mime.decode(encoded)
+
+ decoded += dec.decode(charset) if charset else dec
+ del parts[0:3]
+ except:
+ return string
+ return decoded
--- papyon/util/timer.py
+++ papyon/util/timer.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import gobject
+
+__all__ = ['Timer']
+
+class Timer(object):
+
+ def __init__(self):
+ self._timeout_sources = {} # name => source
+ self._timeout_args = {} # name => callback args
+
+ @property
+ def timeouts(self):
+ return self._timeout_sources.keys()
+
+ def start_timeout(self, name, time, *cb_args):
+ self.stop_timeout(name)
+ source = gobject.timeout_add_seconds(time, self.on_timeout, name)
+ self._timeout_sources[name] = source
+ self._timeout_args[name] = cb_args
+
+ def stop_timeout(self, name):
+ source = self._timeout_sources.get(name, None)
+ if source is not None:
+ gobject.source_remove(source)
+ del self._timeout_sources[name]
+ if name in self._timeout_args:
+ return self._timeout_args.pop(name)
+ return []
+
+ def stop_all_timeout(self):
+ for (name, source) in self._timeout_sources.items():
+ if source is not None:
+ gobject.source_remove(source)
+ self._timeout_sources.clear()
+ self._timeout_args.clear()
+
+ def on_timeout(self, name):
+ cb_args = self.stop_timeout(name)
+ handler = getattr(self, "on_%s_timeout" % name, None)
+ if handler is not None:
+ handler(*cb_args)
--- papyon/util/tlv.py
+++ papyon/util/tlv.py
+# -*- coding: utf-8 -*-
+#
+# papyon - a python client library for Msn
+#
+# Copyright (C) 2010 Collabora Ltd.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import struct
+
+__all__ = ['TLV']
+
+class TLV(object):
+ """Utility class to build and parse data in a TLV representation.
+ Each TLV (type-length-value) element has a 1-byte field for the type,
+ a 1-byte field for the length and a variant size field for the value.
+ The data is padded with null byte (0x0) to the next 4-bytes boundary."""
+
+ def __init__(self, length_dict):
+ """Initialize a TLV object
+ @param length_dict: dict of possible types with their length"""
+
+ self._length_dict = length_dict
+ self._data = {}
+ self._formats = {1: "B", 2: "H", 4: "I", 8: "Q"}
+
+ def size_to_packed_format(self, size):
+ """Determine the correct format to unpack a value (as used by
+ 'struct' module)."""
+ if size in self._formats:
+ return self._formats[size]
+ else:
+ return "%ss" % size
+
+ def get(self, key, default):
+ """Get the value for the given type in the TLV or return the default
+ value if this field is not present in the TLV."""
+ return self._data.get(key, default)
+
+ def update(self, key, value):
+ """Update the value for this type. If the value is null, delete
+ the sequence from the TLV."""
+ if value:
+ self._data[key] = value
+ elif key in self._data:
+ del self._data[key]
+
+ def __len__(self):
+ size = 0
+ for (t, v) in self._data.items():
+ size += 2 + self._length_dict[t]
+ if (size % 4) != 0:
+ size += 4 - (size % 4)
+ return size
+
+ def __str__(self):
+ """Pack data in a string and add padding."""
+ string = ""
+ for (t, v) in self._data.items():
+ if not t in self._length_dict: continue
+ l = self._length_dict[t]
+ f = self.size_to_packed_format(l)
+ string += struct.pack(">BB%s" % f, t, l, v)
+ if (len(string) % 4) != 0:
+ string += '\x00' * (4 - (len(string) % 4))
+ return string
+
+ def parse(self, data, size):
+ """Parse the given TLV data and add values to the internal dict."""
+ offset = 0
+ while offset < size:
+ if ord(data[offset]) is 0: break # ignore padding bytes
+ t = ord(data[offset])
+ l = ord(data[offset + 1])
+ f = self.size_to_packed_format(l)
+ end = offset + 2 + l
+ self._data[t] = struct.unpack(">%s" % f, data[offset + 2:end])[0]
+ offset = end
--- test.py
+++ test.py
@@ -38,14 +38,14 @@
for contact in self._client.address_book.contacts:
print contact
#self._client.profile.personal_message = "Testing papyon, and freeing the pandas!"
- gobject.timeout_add(5000, self._client.start_conversation)
+ gobject.timeout_add_seconds(5, self._client.start_conversation)
def on_client_error(self, error_type, error):
print "ERROR :", error_type, " ->", error
class AnnoyingConversation(papyon.event.ConversationEventInterface):
def on_conversation_user_joined(self, contact):
- gobject.timeout_add(5000, self.annoy_user)
+ gobject.timeout_add_seconds(5, self.annoy_user)
def annoy_user(self):
msg = "Let's free the pandas ! (testing papyon)"
++++++ papyon.yaml
--- papyon.yaml
+++ papyon.yaml
@@ -1,6 +1,6 @@
Name: papyon
Summary: Python libraries for MSN Messenger network
-Version: 0.4.9
+Version: 0.5.2
Release: 1
Group: System/Libraries
License: GPLv2+
More information about the MeeGo-commits
mailing list