instrumentation: add next-share/
[cs-p2p-next.git] / instrumentation / next-share / BaseLib / UPnP / ssdp / ssdpserver.py
1 # Written by Ingar Arntzen
2 # see LICENSE.txt for license information
3
4 """
5 This module implements the SSDP Server deamon, 
6 part of the UPnP archictecture.
7 """
8 import ssdpmessage
9 import ssdpdaemon
10
11 # SSDP Message Configuration
12
13 _ROOT_CONF_TEMPL =  { 
14     'target':  "upnp:rootdevice", 
15     'usn': "uuid:%(uuid)s::upnp:rootdevice",
16     }
17
18 _DEVICE_CONF_TEMPL_1 = {
19     'target': "uuid:%(uuid)s",
20     'usn': "uuid:%(uuid)s",
21     }
22
23 _DEVICE_CONF_TEMPL_2 = {
24     'target':  "urn:%(device_domain)s:device:" + \
25         "%(device_type)s:%(device_version)s", 
26     'usn': "uuid:%(uuid)s::urn:%(device_domain)s:device:" + \
27         "%(device_type)s:%(device_version)s",
28     }
29
30 _SERVICE_CONF_TEMPL = {
31     'target':  "urn:schemas-upnp-org:service:" + \
32         "%(service_type)s:%(service_version)s", 
33     'usn': "uuid:%(uuid)s::urn:schemas-upnp-org:service:" + \
34         "%(service_type)s:%(service_version)s",
35     }
36
37 _MAX_DELAY = 4
38 _MAX_AGE = 1800
39 _REANNOUNCE_AGE = _MAX_AGE * 0.9
40 _SSDP_SERVER_CONFIG = {
41     'max_delay': _MAX_DELAY,
42     'max_age': _MAX_AGE,
43     }
44
45 _LOG_TAG = "SSDPServer"
46
47
48 def _create_msg_config(config_template, kwargs):
49     """Create a single message config dict from a template and 
50     some keywords."""
51     return {
52         'target': config_template['target'] % kwargs,
53         'usn': config_template['usn'] % kwargs
54         }
55
56 def _create_msg_configs(root_device):
57     """Create all message configs for all devices and services."""
58     # Expect rootdevice to be the root of a dictionary hierarchy,
59     # representing the nested organisation of devices and services.
60     # Create 1 special message config for root device
61     configs = [_create_msg_config(_ROOT_CONF_TEMPL, root_device.__dict__)]
62     # Walk the device/service hierarchy (incl. rootdevice)
63     device_queue = [root_device]
64     while device_queue:
65         device = device_queue.pop()
66         # Iterate recursively over all devices (top-down/breath-first)        
67         device_queue += device.get_devices()
68         # Create two messages configs per device
69         conf_1 = _create_msg_config(_DEVICE_CONF_TEMPL_1, device.__dict__)
70         conf_2 = _create_msg_config(_DEVICE_CONF_TEMPL_2, device.__dict__)
71         configs += [conf_1, conf_2]
72         # Create one message config per service in device
73         # todo : should really only create one message per service type
74         for service in device.get_services():
75             service.uuid = device.uuid
76             conf = _create_msg_config(_SERVICE_CONF_TEMPL, service.__dict__)
77             configs.append(conf)
78     return configs
79
80
81 def _initialise_message(ssdp_config, msg):
82     """Utility method for initialising SSDP messages with common data."""
83     msg.init (
84         location=ssdp_config['location'], 
85         osversion=ssdp_config['osversion'],
86         productversion = ssdp_config['productversion'],
87         max_age=ssdp_config['max_age']
88         )
89     return msg
90
91
92 ##############################################
93 # SSDP SERVER
94 ##############################################
95
96 class SSDPServer(ssdpdaemon.SSDPDaemon):
97
98     """
99     This implements the SSDP server deamon, part of the UPnP architecture.
100     
101     This class is implemented in a non-blocking, event-based manner.
102     Execution is outsourced to the given task_runner.
103     """
104
105     def __init__(self, task_runner, logger=None):
106         ssdpdaemon.SSDPDaemon.__init__(self, task_runner, logger)
107         # Service Manager
108         self._sm = None
109         # Announce Timeout Task
110         self._timeout_task = None
111
112         self._root_device = None
113         self._config = None
114
115     def set_service_manager(self, service_manager):
116         """The service manger initialises SSDPServer with a
117         reference to itself."""
118         self._sm = service_manager
119
120     ##############################################
121     # PRIVATE PROTOCOL OPERATIONS
122     ##############################################
123
124     def startup(self):
125         """Extends superclass startup  when taskrunner starts."""
126         ssdpdaemon.SSDPDaemon.startup(self)
127         # RootDevice
128         self._root_device = self._sm.get_root_device()
129         # Config
130         self._config = _SSDP_SERVER_CONFIG
131         self._config['location'] = self._sm.get_description_url()
132         self._config['osversion'] = self._sm.get_os_version()
133         self._config['productversion'] = self._sm.get_product_version()
134         # Initial Announce
135         self.announce()
136
137     ##############################################
138     # OVERRIDE HANDLERS 
139     ##############################################
140
141     def handle_search(self, msg, sock_addr):
142         """Handles the receipt of a SSDP Search message."""
143
144         # Create Reply Message
145         reply_message = ssdpmessage.ReplyMessage()
146         _initialise_message(self._config, reply_message)
147
148         # Decide the number of reply messages, and their configuration.
149
150         if msg.st == "ssdp:all":
151             # Reply messages for all devices and services
152             configs = _create_msg_configs(self._root_device) 
153         elif msg.st == "upnp:rootdevice":
154             # Reply only single special message for root device
155             configs = [_create_msg_config(_ROOT_CONF_TEMPL, \
156                                           self._root_device.__dict__)]
157         else:
158             device_type = msg.st.split(':')[-2]
159             self.log("IGNORE %s %s [%s]" % (msg.type, 
160                                             device_type, sock_addr[0]))
161             return
162         
163         device_type = msg.st.split(':')[-2]
164         self.log("RECEIVE %s %s [%s]" % (msg.type, 
165                                          device_type, sock_addr[0]))
166
167         # Send Replies
168         for conf in configs:
169             reply_message.st = conf['target']
170             reply_message.usn = conf['usn']
171             self.unicast(reply_message, sock_addr)
172
173     def _handle_reply(self, msg, sock_addr):
174         """Handles the receipt of a SSDP Reply message."""
175         self.log("IGNORE %s from %s" % (msg.type, sock_addr))
176
177     def _handle_announce(self, msg, sock_addr):
178         """Handles the receipt of a SSDP Announce message."""
179
180         self.log("IGNORE %s from %s" % (msg.type, sock_addr))
181
182     def _handle_unannounce(self, msg, sock_addr):
183         """Handles the receipt of a SSDP UnAnnounce message."""
184         self.log("IGNORE %s from %s" % (msg.type, sock_addr))
185
186     ##############################################
187     # PUBLIC API
188     ##############################################
189         
190     def announce(self):
191         """Multicast SSDP announce messages."""
192         # Reset timeout task for next announce
193         if self._timeout_task:
194             self._timeout_task.cancel()
195         # Register new announce timeout
196         self._timeout_task = self.task_runner.add_delay_task(
197             _REANNOUNCE_AGE, 
198             self.announce
199             )
200
201         msg = ssdpmessage.AnnounceMessage()
202         _initialise_message(self._config, msg)
203         for conf in _create_msg_configs(self._root_device):
204             msg.nt = conf['target']
205             msg.usn = conf['usn']
206             self.multicast(msg)
207
208     def unannounce(self):
209         """Multicast SSDP unannounce messages."""
210         msg = ssdpmessage.UnAnnounceMessage()
211         for conf in _create_msg_configs(self._root_device):
212             msg.nt = conf['target']
213             msg.usn = conf['usn']
214             self.multicast(msg)
215                
216     def close(self):
217         """Close the SSDP Server deamon. Send unannounce messages."""
218         self.unannounce()
219         ssdpdaemon.SSDPDaemon.close(self)
220        
221
222 ##############################################
223 # MAIN
224 ##############################################
225
226 if __name__ == '__main__':
227
228     import uuid
229     class MockRootDevice:
230         """Mockup root device."""
231         def __init__(self):
232             self.uuid = uuid.uuid1()
233             self.device_domain = "schemas-upnp-org"
234             self.device_type = "Basic"
235             self.device_version = 1
236         def get_devices(self):
237             """Get mock devices."""
238             return []
239         def get_services(self):
240             """Get mock services."""
241             return []
242
243     class MockServiceManager:
244         """Mock up service manager."""
245         def __init__(self):
246             pass
247         def get_root_device(self):
248             """Get mock root device."""
249             return MockRootDevice()
250         def get_description_url(self):
251             """Get mock description URL."""
252             return "http://192.168.1.235:44444/description.xml"
253         def get_os_version(self):
254             """Get mock os version."""
255             return "linux 1.0"
256         def get_product_version(self):
257             """Get mock product version."""
258             return "product 1.0"
259         def set_ssdp_port(self, port):
260             """Set mock Port."""
261             pass
262
263     class MockLogger:
264         """MockLogger object."""
265         def __init__(self):
266             pass
267         def log(self, log_tag, msg):
268             """Log to std out."""
269             print log_tag, msg
270
271     
272     import BaseLib.UPnP.common.taskrunner as taskrunner
273     TR = taskrunner.TaskRunner()
274     SM = MockServiceManager()
275     SERVER = SSDPServer(TR, logger=MockLogger())
276     SERVER.set_service_manager(SM)
277     TR.add_task(SERVER.startup)
278     try:
279         TR.run_forever()
280     except KeyboardInterrupt:
281         print
282     TR.stop()
283     SERVER.close()