├── LICENSE
├── README.rst
├── miranda.py
└── wemo.py
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010, Issac Kelly
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 | * Redistributions of source code must retain the above copyright
7 | notice, this list of conditions and the following disclaimer.
8 | * Redistributions in binary form must reproduce the above copyright
9 | notice, this list of conditions and the following disclaimer in the
10 | documentation and/or other materials provided with the distribution.
11 | * Neither the name of the tastypie nor the
12 | names of its contributors may be used to endorse or promote products
13 | derived from this software without specific prior written permission.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 | DISCLAIMED. IN NO EVENT SHALL tastypie BE LIABLE FOR ANY
19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ============
2 | WeMo Hacking
3 | ============
4 |
5 | I've spent some time reverse engineering my WeMo switch. It's pretty cool and I figured out how to get it to do what I wanted. It's based on UPnP, which I found the miranda
6 | tool to be the best (closest to working with WeMo, and easiest to read)
7 |
8 | I had to make some modifications to the miranda package to get it working, and to get it properly reporting the details of the device.
9 |
10 | To use, download, cd into the wemo folder and open a python intepreter::
11 |
12 | $ python
13 | >>> from wemo import on, off, get
14 | Entering discovery mode for 'upnp:rootdevice', Ctl+C to stop...
15 |
16 | Error updating command completer structure; some command completion features might not work...
17 | Error updating command completer structure; some command completion features might not work...
18 | ****************************************************************
19 | SSDP reply message from 192.168.1.133:49153
20 | XML file is located at http://192.168.1.133:49153/setup.xml
21 | Device is running Linux/2.6.21, UPnP/1.0, Portable SDK for UPnP devices/1.6.6
22 | ****************************************************************
23 |
24 | Discover mode halted...
25 | >>> get()
26 | True
27 | >>> on()
28 | True
29 | >>> off()
30 | True
31 | >>> get()
32 | False
33 | >>> on()
34 | True
35 | >>>
36 |
--------------------------------------------------------------------------------
/miranda.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | ################################
3 | # Interactive UPNP application #
4 | # Craig Heffner #
5 | # www.sourcesec.com #
6 | # 07/16/2008 #
7 | #
8 | # Notes from Issac:
9 | # http://code.google.com/p/miranda-upnp/
10 | # Marks this file as GPL3 licensed by the author
11 | # I have made minor modificatinos to get it to work with the wemo
12 | #
13 | ################################
14 |
15 | try:
16 | import sys,os
17 | from socket import *
18 | from urllib2 import URLError, HTTPError
19 | from platform import system as thisSystem
20 | import xml.dom.minidom as minidom
21 | import IN,urllib,urllib2
22 | import readline,time
23 | import pickle
24 | import struct
25 | import base64
26 | import re
27 | import getopt
28 | except Exception,e:
29 | print 'Unmet dependency:',e
30 | sys.exit(1)
31 |
32 | #Most of the cmdCompleter class was originally written by John Kenyan
33 | #It serves to tab-complete commands inside the program's shell
34 | class cmdCompleter:
35 | def __init__(self,commands):
36 | self.commands = commands
37 |
38 | #Traverses the list of available commands
39 | def traverse(self,tokens,tree):
40 | retVal = []
41 |
42 | #If there are no commands, or no user input, return null
43 | if tree is None or len(tokens) == 0:
44 | return []
45 | #If there is only one word, only auto-complete the primary commands
46 | elif len(tokens) == 1:
47 | retVal = [x+' ' for x in tree if x.startswith(tokens[0])]
48 | #Else auto-complete for the sub-commands
49 | elif tokens[0] in tree.keys():
50 | retVal = self.traverse(tokens[1:],tree[tokens[0]])
51 | return retVal
52 |
53 | #Returns a list of possible commands that match the partial command that the user has entered
54 | def complete(self,text,state):
55 | try:
56 | tokens = readline.get_line_buffer().split()
57 | if not tokens or readline.get_line_buffer()[-1] == ' ':
58 | tokens.append('')
59 | results = self.traverse(tokens,self.commands) + [None]
60 | return results[state]
61 | except:
62 | return
63 |
64 | #UPNP class for getting, sending and parsing SSDP/SOAP XML data (among other things...)
65 | class upnp:
66 | ip = False
67 | port = False
68 | completer = False
69 | msearchHeaders = {
70 | 'MAN' : '"ssdp:discover"',
71 | 'MX' : '2'
72 | }
73 | DEFAULT_IP = "239.255.255.250"
74 | DEFAULT_PORT = 1900
75 | UPNP_VERSION = '1.0'
76 | MAX_RECV = 8192
77 | HTTP_HEADERS = []
78 | ENUM_HOSTS = {}
79 | VERBOSE = False
80 | UNIQ = False
81 | DEBUG = False
82 | LOG_FILE = False
83 | IFACE = None
84 | STARS = '****************************************************************'
85 | csock = False
86 | ssock = False
87 |
88 | def __init__(self, ip=False, port=False, iface=None, appCommands=[]):
89 | if appCommands:
90 | self.completer = cmdCompleter(appCommands)
91 | if self.initSockets(ip, port, iface) == False:
92 | print 'UPNP class initialization failed!'
93 | print 'Bye!'
94 | sys.exit(1)
95 | else:
96 | self.soapEnd = re.compile('<\/.*:envelope>')
97 |
98 | #Initialize default sockets
99 | def initSockets(self, ip, port, iface):
100 | if self.csock:
101 | self.csock.close()
102 | if self.ssock:
103 | self.ssock.close()
104 |
105 | if iface != None:
106 | self.IFACE = iface
107 | if not ip:
108 | ip = self.DEFAULT_IP
109 | if not port:
110 | port = self.DEFAULT_PORT
111 | self.port = port
112 | self.ip = ip
113 |
114 | try:
115 | #This is needed to join a multicast group
116 | self.mreq = struct.pack("4sl",inet_aton(ip),INADDR_ANY)
117 |
118 | #Set up client socket
119 | self.csock = socket(AF_INET,SOCK_DGRAM)
120 | self.csock.setsockopt(IPPROTO_IP,IP_MULTICAST_TTL,2)
121 |
122 | #Set up server socket
123 | self.ssock = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP)
124 | self.ssock.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
125 |
126 | #Only bind to this interface
127 | if self.IFACE != None:
128 | print '\nBinding to interface',self.IFACE,'...\n'
129 | self.ssock.setsockopt(SOL_SOCKET,IN.SO_BINDTODEVICE,struct.pack("%ds" % (len(self.IFACE)+1,), self.IFACE))
130 | self.csock.setsockopt(SOL_SOCKET,IN.SO_BINDTODEVICE,struct.pack("%ds" % (len(self.IFACE)+1,), self.IFACE))
131 |
132 | try:
133 | self.ssock.bind(('',self.port))
134 | except Exception, e:
135 | print "WARNING: Failed to bind %s:%d: %s" , (self.ip,self.port,e)
136 | try:
137 | self.ssock.setsockopt(IPPROTO_IP,IP_ADD_MEMBERSHIP,self.mreq)
138 | except Exception, e:
139 | print 'WARNING: Failed to join multicast group:',e
140 | except Exception, e:
141 | print "Failed to initialize UPNP sockets:",e
142 | return False
143 | return True
144 |
145 | #Clean up file/socket descriptors
146 | def cleanup(self):
147 | if self.LOG_FILE != False:
148 | self.LOG_FILE.close()
149 | self.csock.close()
150 | self.ssock.close()
151 |
152 | #Send network data
153 | def send(self,data,socket):
154 | #By default, use the client socket that's part of this class
155 | if socket == False:
156 | socket = self.csock
157 | try:
158 | socket.sendto(data,(self.ip,self.port))
159 | return True
160 | except Exception, e:
161 | print "SendTo method failed for %s:%d : %s" % (self.ip,self.port,e)
162 | return False
163 |
164 | #Listen for network data
165 | def listen(self,size,socket):
166 | if socket == False:
167 | socket = self.ssock
168 |
169 | try:
170 | return socket.recv(size)
171 | except:
172 | return False
173 |
174 | #Create new UDP socket on ip, bound to port
175 | def createNewListener(self,ip=gethostbyname(gethostname()),port=1900):
176 | try:
177 | newsock = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP)
178 | newsock.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
179 | newsock.bind((ip,port))
180 | return newsock
181 | except:
182 | return False
183 |
184 | #Return the class's primary server socket
185 | def listener(self):
186 | return self.ssock
187 |
188 | #Return the class's primary client socket
189 | def sender(self):
190 | return self.csock
191 |
192 | #Parse a URL, return the host and the page
193 | def parseURL(self,url):
194 | delim = '://'
195 | host = False
196 | page = False
197 |
198 | #Split the host and page
199 | try:
200 | (host,page) = url.split(delim)[1].split('/',1)
201 | page = '/' + page
202 | except:
203 | #If '://' is not in the url, then it's not a full URL, so assume that it's just a relative path
204 | page = url
205 |
206 | return (host,page)
207 |
208 | #Pull the name of the device type from a device type string
209 | #The device type string looks like: 'urn:schemas-upnp-org:device:WANDevice:1'
210 | def parseDeviceTypeName(self,string):
211 | delim1 = 'device:'
212 | delim2 = ':'
213 |
214 | if delim1 in string and not string.endswith(delim1):
215 | return string.split(delim1)[1].split(delim2,1)[0]
216 | return False
217 |
218 | #Pull the name of the service type from a service type string
219 | #The service type string looks like: 'urn:schemas-upnp-org:service:Layer3Forwarding:1'
220 | def parseServiceTypeName(self,string):
221 | delim1 = 'service:'
222 | delim2 = ':'
223 |
224 | if delim1 in string and not string.endswith(delim1):
225 | return string.split(delim1)[1].split(delim2,1)[0]
226 | return False
227 |
228 | #Pull the header info for the specified HTTP header - case insensitive
229 | def parseHeader(self,data,header):
230 | delimiter = "%s:" % header
231 | defaultRet = False
232 |
233 | lowerDelim = delimiter.lower()
234 | dataArray = data.split("\r\n")
235 |
236 | #Loop through each line of the headers
237 | for line in dataArray:
238 | lowerLine = line.lower()
239 | #Does this line start with the header we're looking for?
240 | if lowerLine.startswith(lowerDelim):
241 | try:
242 | return line.split(':',1)[1].strip()
243 | except:
244 | print "Failure parsing header data for %s" % header
245 | return defaultRet
246 |
247 | #Extract the contents of a single XML tag from the data
248 | def extractSingleTag(self,data,tag):
249 | startTag = "<%s" % tag
250 | endTag = "%s>" % tag
251 |
252 | try:
253 | tmp = data.split(startTag)[1]
254 | index = tmp.find('>')
255 | if index != -1:
256 | index += 1
257 | return tmp[index:].split(endTag)[0].strip()
258 | except:
259 | pass
260 | return None
261 |
262 | #Parses SSDP notify and reply packets, and populates the ENUM_HOSTS dict
263 | def parseSSDPInfo(self,data,showUniq,verbose):
264 | hostFound = False
265 | messageType = False
266 | xmlFile = False
267 | host = False
268 | page = False
269 | upnpType = None
270 | knownHeaders = {
271 | 'NOTIFY' : 'notification',
272 | 'HTTP/1.1 200 OK' : 'reply'
273 | }
274 |
275 | #Use the class defaults if these aren't specified
276 | if showUniq == False:
277 | showUniq = self.UNIQ
278 | if verbose == False:
279 | verbose = self.VERBOSE
280 |
281 | #Is the SSDP packet a notification, a reply, or neither?
282 | for text,messageType in knownHeaders.iteritems():
283 | if data.upper().startswith(text):
284 | break
285 | else:
286 | messageType = False
287 |
288 | #If this is a notification or a reply message...
289 | if messageType != False:
290 | #Get the host name and location of it's main UPNP XML file
291 | xmlFile = self.parseHeader(data,"LOCATION")
292 | upnpType = self.parseHeader(data,"SERVER")
293 | (host,page) = self.parseURL(xmlFile)
294 |
295 | #Sanity check to make sure we got all the info we need
296 | if xmlFile == False or host == False or page == False:
297 | print 'ERROR parsing recieved header:'
298 | print self.STARS
299 | print data
300 | print self.STARS
301 | print ''
302 | return False
303 |
304 | #Get the protocol in use (i.e., http, https, etc)
305 | protocol = xmlFile.split('://')[0]+'://'
306 |
307 | #Check if we've seen this host before; add to the list of hosts if:
308 | # 1. This is a new host
309 | # 2. We've already seen this host, but the uniq hosts setting is disabled
310 | for hostID,hostInfo in self.ENUM_HOSTS.iteritems():
311 | if hostInfo['name'] == host:
312 | hostFound = True
313 | if self.UNIQ:
314 | return False
315 |
316 | if (hostFound and not self.UNIQ) or not hostFound:
317 | #Get the new host's index number and create an entry in ENUM_HOSTS
318 | index = len(self.ENUM_HOSTS)
319 | self.ENUM_HOSTS[index] = {
320 | 'name' : host,
321 | 'dataComplete' : False,
322 | 'proto' : protocol,
323 | 'xmlFile' : xmlFile,
324 | 'serverType' : None,
325 | 'upnpServer' : upnpType,
326 | 'deviceList' : {}
327 | }
328 | #Be sure to update the command completer so we can tab complete through this host's data structure
329 | self.updateCmdCompleter(self.ENUM_HOSTS)
330 |
331 | #Print out some basic device info
332 | print self.STARS
333 | print "SSDP %s message from %s" % (messageType,host)
334 |
335 | if xmlFile:
336 | print "XML file is located at %s" % xmlFile
337 |
338 | if upnpType:
339 | print "Device is running %s"% upnpType
340 |
341 | print self.STARS
342 | print ''
343 |
344 | #Send GET request for a UPNP XML file
345 | def getXML(self, url):
346 |
347 | headers = {
348 | 'USER-AGENT':'uPNP/'+self.UPNP_VERSION,
349 | 'CONTENT-TYPE':'text/xml; charset="utf-8"'
350 | }
351 |
352 | try:
353 | #Use urllib2 for the request, it's awesome
354 | req = urllib2.Request(url, None, headers)
355 | response = urllib2.urlopen(req)
356 | output = response.read()
357 | headers = response.info()
358 | return (headers,output)
359 | except Exception, e:
360 | print "Request for '%s' failed: %s" % (url,e)
361 | return (False,False)
362 |
363 | #Send SOAP request
364 | def sendSOAP(self, hostName, serviceType, controlURL, actionName, actionArguments):
365 | argList = ''
366 | soapResponse = ''
367 |
368 | if '://' in controlURL:
369 | urlArray = controlURL.split('/',3)
370 | if len(urlArray) < 4:
371 | controlURL = '/'
372 | else:
373 | controlURL = '/' + urlArray[3]
374 |
375 |
376 | soapRequest = 'POST %s HTTP/1.1\r\n' % controlURL
377 |
378 | #Check if a port number was specified in the host name; default is port 80
379 | if ':' in hostName:
380 | hostNameArray = hostName.split(':')
381 | host = hostNameArray[0]
382 | try:
383 | port = int(hostNameArray[1])
384 | except:
385 | print 'Invalid port specified for host connection:',hostName[1]
386 | return False
387 | else:
388 | host = hostName
389 | port = 80
390 |
391 | #Create a string containing all of the SOAP action's arguments and values
392 | for arg,(val,dt) in actionArguments.iteritems():
393 | argList += '<%s>%s%s>' % (arg,val,arg)
394 |
395 | #Create the SOAP request
396 | soapBody = """
397 |
398 |
399 |
400 | %s
401 |
402 |
403 |
404 | """ % (actionName, serviceType, argList, actionName)
405 |
406 | #Specify the headers to send with the request
407 | headers = {
408 | 'Content-Type':'text/xml; charset="utf-8"',
409 | 'SOAPACTION':'"%s#%s"' % (serviceType,actionName),
410 | 'Content-Length': len(soapBody),
411 | 'HOST':hostName,
412 | 'User-Agent': 'CyberGarage-HTTP/1.0',
413 | }
414 |
415 | #Generate the final payload
416 | for head,value in headers.iteritems():
417 | soapRequest += '%s: %s\r\n' % (head,value)
418 | soapRequest += '\r\n%s' % soapBody
419 |
420 | #Send data and go into recieve loop
421 | try:
422 | sock = socket(AF_INET,SOCK_STREAM)
423 | sock.connect((host,port))
424 | sock.send(soapRequest)
425 | while True:
426 | data = sock.recv(self.MAX_RECV)
427 | if not data:
428 | break
429 | else:
430 | soapResponse += data
431 | if self.soapEnd.search(soapResponse.lower()) != None:
432 | break
433 | sock.close()
434 |
435 | (header,body) = soapResponse.split('\r\n\r\n',1)
436 | if not header.upper().startswith('HTTP/1.1 200'):
437 | print 'SOAP request failed with error code:',header.split('\r\n')[0].split(' ',1)[1]
438 | errorMsg = self.extractSingleTag(body,'errorDescription')
439 | if errorMsg:
440 | print 'SOAP error message:',errorMsg
441 | return False
442 | else:
443 | return body
444 | except Exception, e:
445 | print 'Caught socket exception:',e
446 | sock.close()
447 | return False
448 | except KeyboardInterrupt:
449 | sock.close()
450 | return False
451 |
452 | #Display all info for a given host
453 | def showCompleteHostInfo(self,index,fp):
454 | serviceKeys = ['controlURL','eventSubURL','serviceId','SCPDURL','fullName']
455 | if fp == False:
456 | fp = sys.stdout
457 |
458 | if index < 0 or index >= len(self.ENUM_HOSTS):
459 | fp.write('Specified host does not exist...\n')
460 | return
461 | try:
462 | hostInfo = self.ENUM_HOSTS[index]
463 | if hostInfo['dataComplete'] == False:
464 | print "Cannot show all host info because we don't have it all yet. Try running 'host info %d' first...\n" % index
465 | fp.write('Host name: %s\n' % hostInfo['name'])
466 | fp.write('UPNP XML File: %s\n\n' % hostInfo['xmlFile'])
467 |
468 | fp.write('\nDevice information:\n')
469 | for deviceName,deviceStruct in hostInfo['deviceList'].iteritems():
470 | fp.write('\tDevice Name: %s\n' % deviceName)
471 | for serviceName,serviceStruct in deviceStruct['services'].iteritems():
472 | fp.write('\t\tService Name: %s\n' % serviceName)
473 | for key in serviceKeys:
474 | fp.write('\t\t\t%s: %s\n' % (key,serviceStruct[key]))
475 | fp.write('\t\t\tServiceActions:\n')
476 | for actionName,actionStruct in serviceStruct['actions'].iteritems():
477 | fp.write('\t\t\t\t%s\n' % actionName)
478 | for argName,argStruct in actionStruct['arguments'].iteritems():
479 | fp.write('\t\t\t\t\t%s \n' % argName)
480 | for key,val in argStruct.iteritems():
481 | try:
482 | if key == 'relatedStateVariable':
483 | fp.write('\t\t\t\t\t\t%s:\n' % val)
484 | for k,v in serviceStruct['serviceStateVariables'][val].iteritems():
485 | fp.write('\t\t\t\t\t\t\t%s: %s\n' % (k,v))
486 | else:
487 | fp.write('\t\t\t\t\t\t%s: %s\n' % (key,val))
488 | except:
489 | pass
490 |
491 | except Exception, e:
492 | print 'Caught exception while showing host info:',e
493 |
494 | #Wrapper function...
495 | def getHostInfo(self, xmlData, xmlHeaders, index):
496 | if self.ENUM_HOSTS[index]['dataComplete'] == True:
497 | return
498 |
499 | if index >= 0 and index < len(self.ENUM_HOSTS):
500 | try:
501 | xmlRoot = minidom.parseString(xmlData)
502 | self.parseDeviceInfo(xmlRoot,index)
503 | self.ENUM_HOSTS[index]['serverType'] = xmlHeaders.getheader('Server')
504 | self.ENUM_HOSTS[index]['dataComplete'] = True
505 | return True
506 | except Exception, e:
507 | print 'Caught exception while getting host info:',e
508 | return False
509 |
510 | #Parse device info from the retrieved XML file
511 | def parseDeviceInfo(self,xmlRoot,index):
512 | deviceEntryPointer = False
513 | devTag = "device"
514 | deviceType = "deviceType"
515 | deviceListEntries = "deviceList"
516 | deviceTags = ["friendlyName","modelDescription","modelName","modelNumber","modelURL","presentationURL","UDN","UPC","manufacturer","manufacturerURL"]
517 |
518 | #Find all device entries listed in the XML file
519 | for device in xmlRoot.getElementsByTagName(devTag):
520 | try:
521 | #Get the deviceType string
522 | deviceTypeName = str(device.getElementsByTagName(deviceType)[0].childNodes[0].data)
523 | except:
524 | continue
525 |
526 | #Pull out the action device name from the deviceType string
527 | deviceDisplayName = self.parseDeviceTypeName(deviceTypeName)
528 | if not deviceDisplayName:
529 | continue
530 |
531 | #Create a new device entry for this host in the ENUM_HOSTS structure
532 | deviceEntryPointer = self.ENUM_HOSTS[index][deviceListEntries][deviceDisplayName] = {}
533 | deviceEntryPointer['fullName'] = deviceTypeName
534 |
535 | #Parse out all the device tags for that device
536 | for tag in deviceTags:
537 | try:
538 | deviceEntryPointer[tag] = str(device.getElementsByTagName(tag)[0].childNodes[0].data)
539 | except Exception:
540 | if self.VERBOSE:
541 | print 'Device',deviceEntryPointer['fullName'],'does not have a',tag
542 | continue
543 | #Get a list of all services for this device listing
544 | self.parseServiceList(device,deviceEntryPointer,index)
545 |
546 | #Parse the list of services specified in the XML file
547 | def parseServiceList(self,xmlRoot,device,index):
548 | serviceEntryPointer = False
549 | dictName = "services"
550 | serviceListTag = "serviceList"
551 | serviceTag = "service"
552 | serviceNameTag = "serviceType"
553 | serviceTags = ["serviceId","controlURL","eventSubURL","SCPDURL"]
554 |
555 | try:
556 | device[dictName] = {}
557 | #Get a list of all services offered by this device
558 | for service in xmlRoot.getElementsByTagName(serviceListTag)[0].getElementsByTagName(serviceTag):
559 | #Get the full service descriptor
560 | serviceName = str(service.getElementsByTagName(serviceNameTag)[0].childNodes[0].data)
561 |
562 | #Get the service name from the service descriptor string
563 | serviceDisplayName = self.parseServiceTypeName(serviceName)
564 | if not serviceDisplayName:
565 | continue
566 |
567 | #Create new service entry for the device in ENUM_HOSTS
568 | serviceEntryPointer = device[dictName][serviceDisplayName] = {}
569 | serviceEntryPointer['fullName'] = serviceName
570 |
571 | #Get all of the required service info and add it to ENUM_HOSTS
572 | for tag in serviceTags:
573 | serviceEntryPointer[tag] = str(service.getElementsByTagName(tag)[0].childNodes[0].data)
574 |
575 | #Get specific service info about this service
576 | self.parseServiceInfo(serviceEntryPointer,index)
577 | except Exception, e:
578 | print 'Caught exception while parsing device service list:',e
579 |
580 | #Parse details about each service (arguements, variables, etc)
581 | def parseServiceInfo(self,service,index):
582 | argIndex = 0
583 | argTags = ['direction','relatedStateVariable']
584 | actionList = 'actionList'
585 | actionTag = 'action'
586 | nameTag = 'name'
587 | argumentList = 'argumentList'
588 | argumentTag = 'argument'
589 |
590 | #Get the full path to the service's XML file
591 | xmlFile = self.ENUM_HOSTS[index]['proto'] + self.ENUM_HOSTS[index]['name']
592 | if not xmlFile.endswith('/') and not service['SCPDURL'].startswith('/'):
593 | xmlFile += '/'
594 | if self.ENUM_HOSTS[index]['proto'] in service['SCPDURL']:
595 | xmlFile = service['SCPDURL']
596 | else:
597 | xmlFile += service['SCPDURL']
598 | service['actions'] = {}
599 |
600 | #Get the XML file that describes this service
601 | (xmlHeaders,xmlData) = self.getXML(xmlFile)
602 | if not xmlData:
603 | print 'Failed to retrieve service descriptor located at:',xmlFile
604 | return False
605 |
606 | try:
607 | xmlRoot = minidom.parseString(xmlData)
608 |
609 | #Get a list of actions for this service
610 | try:
611 | actionList = xmlRoot.getElementsByTagName(actionList)[0]
612 | except:
613 | print 'Failed to retrieve action list for service %s!' % service['fullName']
614 | return False
615 | actions = actionList.getElementsByTagName(actionTag)
616 | if actions == []:
617 | print 'Failed to retrieve actions from service actions list for service %s!' % service['fullName']
618 | return False
619 |
620 | #Parse all actions in the service's action list
621 | for action in actions:
622 | #Get the action's name
623 | try:
624 | actionName = str(action.getElementsByTagName(nameTag)[0].childNodes[0].data).strip()
625 | except:
626 | print 'Failed to obtain service action name (%s)!' % service['fullName']
627 | continue
628 |
629 | #Add the action to the ENUM_HOSTS dictonary
630 | service['actions'][actionName] = {}
631 | service['actions'][actionName]['arguments'] = {}
632 |
633 | #Parse all of the action's arguments
634 | try:
635 | argList = action.getElementsByTagName(argumentList)[0]
636 | except:
637 | #Some actions may take no arguments, so continue without raising an error here...
638 | continue
639 |
640 | #Get all the arguments in this action's argument list
641 | arguments = argList.getElementsByTagName(argumentTag)
642 | if arguments == []:
643 | if self.VERBOSE:
644 | print 'Action',actionName,'has no arguments!'
645 | continue
646 |
647 | #Loop through the action's arguments, appending them to the ENUM_HOSTS dictionary
648 | for argument in arguments:
649 | try:
650 | argName = str(argument.getElementsByTagName(nameTag)[0].childNodes[0].data)
651 | except:
652 | print 'Failed to get argument name for',actionName
653 | continue
654 | service['actions'][actionName]['arguments'][argName] = {}
655 |
656 | #Get each required argument tag value and add them to ENUM_HOSTS
657 | for tag in argTags:
658 | try:
659 | service['actions'][actionName]['arguments'][argName][tag] = str(argument.getElementsByTagName(tag)[0].childNodes[0].data)
660 | except:
661 | print 'Failed to find tag %s for argument %s!' % (tag,argName)
662 | continue
663 |
664 | #Parse all of the state variables for this service
665 | self.parseServiceStateVars(xmlRoot,service)
666 |
667 | except Exception, e:
668 | print 'Caught exception while parsing Service info for service %s: %s' % (service['fullName'],str(e))
669 | return False
670 |
671 | return True
672 |
673 | #Get info about a service's state variables
674 | def parseServiceStateVars(self,xmlRoot,servicePointer):
675 |
676 | na = 'N/A'
677 | varVals = ['sendEvents','dataType','defaultValue','allowedValues']
678 | serviceStateTable = 'serviceStateTable'
679 | stateVariable = 'stateVariable'
680 | nameTag = 'name'
681 | dataType = 'dataType'
682 | sendEvents = 'sendEvents'
683 | allowedValueList = 'allowedValueList'
684 | allowedValue = 'allowedValue'
685 | allowedValueRange = 'allowedValueRange'
686 | minimum = 'minimum'
687 | maximum = 'maximum'
688 |
689 | #Create the serviceStateVariables entry for this service in ENUM_HOSTS
690 | servicePointer['serviceStateVariables'] = {}
691 |
692 | #Get a list of all state variables associated with this service
693 | try:
694 | stateVars = xmlRoot.getElementsByTagName(serviceStateTable)[0].getElementsByTagName(stateVariable)
695 | except:
696 | #Don't necessarily want to throw an error here, as there may be no service state variables
697 | return False
698 |
699 | #Loop through all state variables
700 | for var in stateVars:
701 | for tag in varVals:
702 | #Get variable name
703 | try:
704 | varName = str(var.getElementsByTagName(nameTag)[0].childNodes[0].data)
705 | except:
706 | print 'Failed to get service state variable name for service %s!' % servicePointer['fullName']
707 | continue
708 |
709 | servicePointer['serviceStateVariables'][varName] = {}
710 | try:
711 | servicePointer['serviceStateVariables'][varName]['dataType'] = str(var.getElementsByTagName(dataType)[0].childNodes[0].data)
712 | except:
713 | servicePointer['serviceStateVariables'][varName]['dataType'] = na
714 | try:
715 | servicePointer['serviceStateVariables'][varName]['sendEvents'] = str(var.getElementsByTagName(sendEvents)[0].childNodes[0].data)
716 | except:
717 | servicePointer['serviceStateVariables'][varName]['sendEvents'] = na
718 |
719 | servicePointer['serviceStateVariables'][varName][allowedValueList] = []
720 |
721 | #Get a list of allowed values for this variable
722 | try:
723 | vals = var.getElementsByTagName(allowedValueList)[0].getElementsByTagName(allowedValue)
724 | except:
725 | pass
726 | else:
727 | #Add the list of allowed values to the ENUM_HOSTS dictionary
728 | for val in vals:
729 | servicePointer['serviceStateVariables'][varName][allowedValueList].append(str(val.childNodes[0].data))
730 |
731 | #Get allowed value range for this variable
732 | try:
733 | valList = var.getElementsByTagName(allowedValueRange)[0]
734 | except:
735 | pass
736 | else:
737 | #Add the max and min values to the ENUM_HOSTS dictionary
738 | servicePointer['serviceStateVariables'][varName][allowedValueRange] = []
739 | try:
740 | servicePointer['serviceStateVariables'][varName][allowedValueRange].append(str(valList.getElementsByTagName(minimum)[0].childNodes[0].data))
741 | servicePointer['serviceStateVariables'][varName][allowedValueRange].append(str(valList.getElementsByTagName(maximum)[0].childNodes[0].data))
742 | except:
743 | pass
744 | return True
745 |
746 | #Update the command completer
747 | def updateCmdCompleter(self,struct):
748 | indexOnlyList = {
749 | 'host' : ['get','details','summary'],
750 | 'save' : ['info']
751 | }
752 | hostCommand = 'host'
753 | subCommandList = ['info']
754 | sendCommand = 'send'
755 |
756 | try:
757 | structPtr = {}
758 | topLevelKeys = {}
759 | for key,val in struct.iteritems():
760 | structPtr[str(key)] = val
761 | topLevelKeys[str(key)] = None
762 |
763 | #Update the subCommandList
764 | for subcmd in subCommandList:
765 | self.completer.commands[hostCommand][subcmd] = None
766 | self.completer.commands[hostCommand][subcmd] = structPtr
767 |
768 | #Update the indexOnlyList
769 | for cmd,data in indexOnlyList.iteritems():
770 | for subcmd in data:
771 | self.completer.commands[cmd][subcmd] = topLevelKeys
772 |
773 | #This is for updating the sendCommand key
774 | structPtr = {}
775 | for hostIndex,hostData in struct.iteritems():
776 | host = str(hostIndex)
777 | structPtr[host] = {}
778 | if hostData.has_key('deviceList'):
779 | for device,deviceData in hostData['deviceList'].iteritems():
780 | structPtr[host][device] = {}
781 | if deviceData.has_key('services'):
782 | for service,serviceData in deviceData['services'].iteritems():
783 | structPtr[host][device][service] = {}
784 | if serviceData.has_key('actions'):
785 | for action,actionData in serviceData['actions'].iteritems():
786 | structPtr[host][device][service][action] = None
787 | self.completer.commands[hostCommand][sendCommand] = structPtr
788 | except Exception:
789 | print "Error updating command completer structure; some command completion features might not work..."
790 | return
791 |
792 |
793 |
794 |
795 | ################## Action Functions ######################
796 | #These functions handle user commands from the shell
797 |
798 | #Actively search for UPNP devices
799 | def msearch(argc, argv, hp, cycles=99999999):
800 | defaultST = "upnp:rootdevice"
801 | st = "schemas-upnp-org"
802 | myip = gethostbyname(gethostname())
803 | lport = hp.port
804 |
805 | if argc >= 3:
806 | if argc == 4:
807 | st = argv[1]
808 | searchType = argv[2]
809 | searchName = argv[3]
810 | else:
811 | searchType = argv[1]
812 | searchName = argv[2]
813 | st = "urn:%s:%s:%s:%s" % (st,searchType,searchName,hp.UPNP_VERSION.split('.')[0])
814 | else:
815 | st = defaultST
816 |
817 | #Build the request
818 | request = "M-SEARCH * HTTP/1.1\r\n"\
819 | "HOST:%s:%d\r\n"\
820 | "ST:%s\r\n" % (hp.ip,hp.port,st)
821 | for header,value in hp.msearchHeaders.iteritems():
822 | request += header + ':' + value + "\r\n"
823 | request += "\r\n"
824 |
825 | print "Entering discovery mode for '%s', Ctl+C to stop..." % st
826 | print ''
827 |
828 | #Have to create a new socket since replies will be sent directly to our IP, not the multicast IP
829 | server = hp.createNewListener(myip,lport)
830 | if server == False:
831 | print 'Failed to bind port %d' % lport
832 | return
833 |
834 | hp.send(request,server)
835 | while True:
836 | try:
837 | hp.parseSSDPInfo(hp.listen(1024,server),False,False)
838 | except Exception:
839 | print 'Discover mode halted...'
840 | server.close()
841 | break
842 | cycles -= 1
843 | if cycles == 0:
844 | print 'Discover mode halted...'
845 | server.close()
846 | break
847 |
848 | #Passively listen for UPNP NOTIFY packets
849 | def pcap(argc,argv,hp):
850 | print 'Entering passive mode, Ctl+C to stop...'
851 | print ''
852 | while True:
853 | try:
854 | hp.parseSSDPInfo(hp.listen(1024,False),False,False)
855 | except Exception:
856 | print "Passive mode halted..."
857 | break
858 |
859 | #Manipulate M-SEARCH header values
860 | def head(argc,argv,hp):
861 | if argc >= 2:
862 | action = argv[1]
863 | #Show current headers
864 | if action == 'show':
865 | for header,value in hp.msearchHeaders.iteritems():
866 | print header,':',value
867 | return
868 | #Delete the specified header
869 | elif action == 'del':
870 | if argc == 3:
871 | header = argv[2]
872 | if hp.msearchHeaders.has_key(header):
873 | del hp.msearchHeaders[header]
874 | print '%s removed from header list' % header
875 | return
876 | else:
877 | print '%s is not in the current header list' % header
878 | return
879 | #Create/set a headers
880 | elif action == 'set':
881 | if argc == 4:
882 | header = argv[2]
883 | value = argv[3]
884 | hp.msearchHeaders[header] = value
885 | print "Added header: '%s:%s" % (header,value)
886 | return
887 |
888 | showHelp(argv[0])
889 |
890 | #Manipulate application settings
891 | def seti(argc,argv,hp):
892 | if argc >= 2:
893 | action = argv[1]
894 | if action == 'uniq':
895 | hp.UNIQ = toggleVal(hp.UNIQ)
896 | print "Show unique hosts set to: %s" % hp.UNIQ
897 | return
898 | elif action == 'debug':
899 | hp.DEBUG = toggleVal(hp.DEBUG)
900 | print "Debug mode set to: %s" % hp.DEBUG
901 | return
902 | elif action == 'verbose':
903 | hp.VERBOSE = toggleVal(hp.VERBOSE)
904 | print "Verbose mode set to: %s" % hp.VERBOSE
905 | return
906 | elif action == 'version':
907 | if argc == 3:
908 | hp.UPNP_VERSION = argv[2]
909 | print 'UPNP version set to: %s' % hp.UPNP_VERSION
910 | else:
911 | showHelp(argv[0])
912 | return
913 | elif action == 'iface':
914 | if argc == 3:
915 | hp.IFACE = argv[2]
916 | print 'Interface set to %s, re-binding sockets...' % hp.IFACE
917 | if hp.initSockets(hp.ip,hp.port,hp.IFACE):
918 | print 'Interface change successful!'
919 | else:
920 | print 'Failed to bind new interface - are you sure you have root privilages??'
921 | hp.IFACE = None
922 | return
923 | elif action == 'socket':
924 | if argc == 3:
925 | try:
926 | (ip,port) = argv[2].split(':')
927 | port = int(port)
928 | hp.ip = ip
929 | hp.port = port
930 | hp.cleanup()
931 | if hp.initSockets(ip,port,hp.IFACE) == False:
932 | print "Setting new socket %s:%d failed!" % (ip,port)
933 | else:
934 | print "Using new socket: %s:%d" % (ip,port)
935 | except Exception, e:
936 | print 'Caught exception setting new socket:',e
937 | return
938 | elif action == 'show':
939 | print 'Multicast IP: ',hp.ip
940 | print 'Multicast Port: ',hp.port
941 | print 'Network Interface: ',hp.IFACE
942 | print 'Number of known hosts: ',len(hp.ENUM_HOSTS)
943 | print 'UPNP Version: ',hp.UPNP_VERSION
944 | print 'Debug mode: ',hp.DEBUG
945 | print 'Verbose mode: ',hp.VERBOSE
946 | print 'Show only unique hosts:',hp.UNIQ
947 | print 'Using log file: ',hp.LOG_FILE
948 | return
949 |
950 | showHelp(argv[0])
951 | return
952 |
953 | #Host command. It's kind of big.
954 | def host(argc,argv,hp):
955 |
956 | indexList = []
957 | indexError = "Host index out of range. Try the 'host list' command to get a list of known hosts"
958 | if argc >= 2:
959 | action = argv[1]
960 | if action == 'list':
961 | if len(hp.ENUM_HOSTS) == 0:
962 | print "No known hosts - try running the 'msearch' or 'pcap' commands"
963 | return
964 | for index,hostInfo in hp.ENUM_HOSTS.iteritems():
965 | print "\t[%d] %s" % (index,hostInfo['name'])
966 | return
967 | elif action == 'details':
968 | hostInfo = False
969 | if argc == 3:
970 | try:
971 | index = int(argv[2])
972 | except Exception, e:
973 | print indexError
974 | return
975 |
976 | if index < 0 or index >= len(hp.ENUM_HOSTS):
977 | print indexError
978 | return
979 | hostInfo = hp.ENUM_HOSTS[index]
980 |
981 | try:
982 | #If this host data is already complete, just display it
983 | if hostInfo['dataComplete'] == True:
984 | hp.showCompleteHostInfo(index,False)
985 | else:
986 | print "Can't show host info because I don't have it. Please run 'host get %d'" % index
987 | except KeyboardInterrupt, e:
988 | pass
989 | return
990 |
991 | elif action == 'summary':
992 | if argc == 3:
993 |
994 | try:
995 | index = int(argv[2])
996 | hostInfo = hp.ENUM_HOSTS[index]
997 | except:
998 | print indexError
999 | return
1000 |
1001 | print 'Host:',hostInfo['name']
1002 | print 'XML File:',hostInfo['xmlFile']
1003 | for deviceName,deviceData in hostInfo['deviceList'].iteritems():
1004 | print deviceName
1005 | for k,v in deviceData.iteritems():
1006 | try:
1007 | v.has_key(False)
1008 | except:
1009 | print "\t%s: %s" % (k,v)
1010 | print ''
1011 | return
1012 |
1013 | elif action == 'info':
1014 | output = hp.ENUM_HOSTS
1015 | dataStructs = []
1016 | for arg in argv[2:]:
1017 | try:
1018 | arg = int(arg)
1019 | except:
1020 | pass
1021 | output = output[arg]
1022 | try:
1023 | for k,v in output.iteritems():
1024 | try:
1025 | v.has_key(False)
1026 | dataStructs.append(k)
1027 | except:
1028 | print k,':',v
1029 | continue
1030 | except:
1031 | print output
1032 |
1033 | for struct in dataStructs:
1034 | print struct,': {}'
1035 | return
1036 |
1037 | elif action == 'get':
1038 | hostInfo = False
1039 | if argc == 3:
1040 | try:
1041 | index = int(argv[2])
1042 | except:
1043 | print indexError
1044 | return
1045 | if index < 0 or index >= len(hp.ENUM_HOSTS):
1046 | print "Host index out of range. Try the 'host list' command to get a list of known hosts"
1047 | return
1048 | else:
1049 | hostInfo = hp.ENUM_HOSTS[index]
1050 |
1051 | #If this host data is already complete, just display it
1052 | if hostInfo['dataComplete'] == True:
1053 | print 'Data for this host has already been enumerated!'
1054 | return
1055 |
1056 | try:
1057 | #Get extended device and service information
1058 | if hostInfo != False:
1059 | print "Requesting device and service info for %s (this could take a few seconds)..." % hostInfo['name']
1060 | print ''
1061 | if hostInfo['dataComplete'] == False:
1062 | (xmlHeaders,xmlData) = hp.getXML(hostInfo['xmlFile'])
1063 | if xmlData == False:
1064 | print 'Failed to request host XML file:',hostInfo['xmlFile']
1065 | return
1066 | if hp.getHostInfo(xmlData,xmlHeaders,index) == False:
1067 | print "Failed to get device/service info for %s..." % hostInfo['name']
1068 | return
1069 | print 'Host data enumeration complete!'
1070 | hp.updateCmdCompleter(hp.ENUM_HOSTS)
1071 | return
1072 | except KeyboardInterrupt, e:
1073 | return
1074 |
1075 | elif action == 'send':
1076 | #Send SOAP requests
1077 | index = False
1078 | inArgCounter = 0
1079 |
1080 | if argc != 6:
1081 | showHelp(argv[0])
1082 | return
1083 | else:
1084 | try:
1085 | index = int(argv[2])
1086 | except:
1087 | print indexError
1088 | return
1089 | deviceName = argv[3]
1090 | serviceName = argv[4]
1091 | actionName = argv[5]
1092 | hostInfo = hp.ENUM_HOSTS[index]
1093 | actionArgs = False
1094 | sendArgs = {}
1095 | retTags = []
1096 | controlURL = False
1097 | fullServiceName = False
1098 |
1099 | #Get the service control URL and full service name
1100 | try:
1101 | controlURL = hostInfo['proto'] + hostInfo['name']
1102 | controlURL2 = hostInfo['deviceList'][deviceName]['services'][serviceName]['controlURL']
1103 | if not controlURL.endswith('/') and not controlURL2.startswith('/'):
1104 | controlURL += '/'
1105 | controlURL += controlURL2
1106 | except Exception,e:
1107 | print 'Caught exception:',e
1108 | print "Are you sure you've run 'host get %d' and specified the correct service name?" % index
1109 | return False
1110 |
1111 | #Get action info
1112 | try:
1113 | actionArgs = hostInfo['deviceList'][deviceName]['services'][serviceName]['actions'][actionName]['arguments']
1114 | fullServiceName = hostInfo['deviceList'][deviceName]['services'][serviceName]['fullName']
1115 | except Exception,e:
1116 | print 'Caught exception:',e
1117 | print "Are you sure you've specified the correct action?"
1118 | return False
1119 |
1120 | for argName,argVals in actionArgs.iteritems():
1121 | actionStateVar = argVals['relatedStateVariable']
1122 | stateVar = hostInfo['deviceList'][deviceName]['services'][serviceName]['serviceStateVariables'][actionStateVar]
1123 |
1124 | if argVals['direction'].lower() == 'in':
1125 | print "Required argument:"
1126 | print "\tArgument Name: ",argName
1127 | print "\tData Type: ",stateVar['dataType']
1128 | if stateVar.has_key('allowedValueList'):
1129 | print "\tAllowed Values:",stateVar['allowedValueList']
1130 | if stateVar.has_key('allowedValueRange'):
1131 | print "\tValue Min: ",stateVar['allowedValueRange'][0]
1132 | print "\tValue Max: ",stateVar['allowedValueRange'][1]
1133 | if stateVar.has_key('defaultValue'):
1134 | print "\tDefault Value: ",stateVar['defaultValue']
1135 | prompt = "\tSet %s value to: " % argName
1136 | try:
1137 | #Get user input for the argument value
1138 | (argc,argv) = getUserInput(hp,prompt)
1139 | if argv == None:
1140 | print 'Stopping send request...'
1141 | return
1142 | uInput = ''
1143 |
1144 | if argc > 0:
1145 | inArgCounter += 1
1146 |
1147 | for val in argv:
1148 | uInput += val + ' '
1149 |
1150 | uInput = uInput.strip()
1151 | if stateVar['dataType'] == 'bin.base64' and uInput:
1152 | uInput = base64.encodestring(uInput)
1153 |
1154 | sendArgs[argName] = (uInput.strip(),stateVar['dataType'])
1155 | except KeyboardInterrupt:
1156 | return
1157 | print ''
1158 | else:
1159 | retTags.append((argName,stateVar['dataType']))
1160 |
1161 | #Remove the above inputs from the command history
1162 | while inArgCounter:
1163 | readline.remove_history_item(readline.get_current_history_length()-1)
1164 | inArgCounter -= 1
1165 |
1166 | #print 'Requesting',controlURL
1167 | soapResponse = hp.sendSOAP(hostInfo['name'],fullServiceName,controlURL,actionName,sendArgs)
1168 | if soapResponse != False:
1169 | #It's easier to just parse this ourselves...
1170 | for (tag,dataType) in retTags:
1171 | tagValue = hp.extractSingleTag(soapResponse,tag)
1172 | if dataType == 'bin.base64' and tagValue != None:
1173 | tagValue = base64.decodestring(tagValue)
1174 | print tag,':',tagValue
1175 | return
1176 |
1177 |
1178 | showHelp(argv[0])
1179 | return
1180 |
1181 | #Save data
1182 | def save(argc,argv,hp):
1183 | suffix = '%s_%s.mir'
1184 | uniqName = ''
1185 | saveType = ''
1186 | fnameIndex = 3
1187 |
1188 | if argc >= 2:
1189 | if argv[1] == 'help':
1190 | showHelp(argv[0])
1191 | return
1192 | elif argv[1] == 'data':
1193 | saveType = 'struct'
1194 | if argc == 3:
1195 | index = argv[2]
1196 | else:
1197 | index = 'data'
1198 | elif argv[1] == 'info':
1199 | saveType = 'info'
1200 | fnameIndex = 4
1201 | if argc >= 3:
1202 | try:
1203 | index = int(argv[2])
1204 | except Exception, e:
1205 | print 'Host index is not a number!'
1206 | showHelp(argv[0])
1207 | return
1208 | else:
1209 | showHelp(argv[0])
1210 | return
1211 |
1212 | if argc == fnameIndex:
1213 | uniqName = argv[fnameIndex-1]
1214 | else:
1215 | uniqName = index
1216 | else:
1217 | showHelp(argv[0])
1218 | return
1219 |
1220 | fileName = suffix % (saveType,uniqName)
1221 | if os.path.exists(fileName):
1222 | print "File '%s' already exists! Please try again..." % fileName
1223 | return
1224 | if saveType == 'struct':
1225 | try:
1226 | fp = open(fileName,'w')
1227 | pickle.dump(hp.ENUM_HOSTS,fp)
1228 | fp.close()
1229 | print "Host data saved to '%s'" % fileName
1230 | except Exception, e:
1231 | print 'Caught exception saving host data:',e
1232 | elif saveType == 'info':
1233 | try:
1234 | fp = open(fileName,'w')
1235 | hp.showCompleteHostInfo(index,fp)
1236 | fp.close()
1237 | print "Host info for '%s' saved to '%s'" % (hp.ENUM_HOSTS[index]['name'],fileName)
1238 | except Exception, e:
1239 | print 'Failed to save host info:',e
1240 | return
1241 | else:
1242 | showHelp(argv[0])
1243 |
1244 | return
1245 |
1246 | #Load data
1247 | def load(argc,argv,hp):
1248 | if argc == 2 and argv[1] != 'help':
1249 | loadFile = argv[1]
1250 |
1251 | try:
1252 | fp = open(loadFile,'r')
1253 | hp.ENUM_HOSTS = {}
1254 | hp.ENUM_HOSTS = pickle.load(fp)
1255 | fp.close()
1256 | hp.updateCmdCompleter(hp.ENUM_HOSTS)
1257 | print 'Host data restored:'
1258 | print ''
1259 | host(2,['host','list'],hp)
1260 | return
1261 | except Exception, e:
1262 | print 'Caught exception while restoring host data:',e
1263 |
1264 | showHelp(argv[0])
1265 |
1266 | #Open log file
1267 | def log(argc,argv,hp):
1268 | if argc == 2:
1269 | logFile = argv[1]
1270 | try:
1271 | fp = open(logFile,'a')
1272 | except Exception, e:
1273 | print 'Failed to open %s for logging: %s' % (logFile,e)
1274 | return
1275 | try:
1276 | hp.LOG_FILE = fp
1277 | ts = []
1278 | for x in time.localtime():
1279 | ts.append(x)
1280 | theTime = "%d-%d-%d, %d:%d:%d" % (ts[0],ts[1],ts[2],ts[3],ts[4],ts[5])
1281 | hp.LOG_FILE.write("\n### Logging started at: %s ###\n" % theTime)
1282 | except Exception, e:
1283 | print "Cannot write to file '%s': %s" % (logFile,e)
1284 | hp.LOG_FILE = False
1285 | return
1286 | print "Commands will be logged to: '%s'" % logFile
1287 | return
1288 | showHelp(argv[0])
1289 |
1290 | #Show help
1291 | def help(argc,argv,hp):
1292 | showHelp(False)
1293 |
1294 | #Debug, disabled by default
1295 | def debug(argc,argv,hp):
1296 | command = ''
1297 | if hp.DEBUG == False:
1298 | print 'Debug is disabled! To enable, try the seti command...'
1299 | return
1300 | if argc == 1:
1301 | showHelp(argv[0])
1302 | else:
1303 | for cmd in argv[1:]:
1304 | command += cmd + ' '
1305 | command = command.strip()
1306 | print eval(command)
1307 | return
1308 | #Quit!
1309 | def exit(argc,argv,hp):
1310 | quit(argc,argv,hp)
1311 |
1312 | #Quit!
1313 | def quit(argc,argv,hp):
1314 | if argc == 2 and argv[1] == 'help':
1315 | showHelp(argv[0])
1316 | return
1317 | print 'Bye!'
1318 | print ''
1319 | hp.cleanup()
1320 | sys.exit(0)
1321 |
1322 | ################ End Action Functions ######################
1323 |
1324 | #Show command help
1325 | def showHelp(command):
1326 | #Detailed help info for each command
1327 | helpInfo = {
1328 | 'help' : {
1329 | 'longListing':
1330 | 'Description:\n'\
1331 | '\tLists available commands and command descriptions\n\n'\
1332 | 'Usage:\n'\
1333 | '\t%s\n'\
1334 | '\t help',
1335 | 'quickView':
1336 | 'Show program help'
1337 | },
1338 | 'quit' : {
1339 | 'longListing' :
1340 | 'Description:\n'\
1341 | '\tQuits the interactive shell\n\n'\
1342 | 'Usage:\n'\
1343 | '\t%s',
1344 | 'quickView' :
1345 | 'Exit this shell'
1346 | },
1347 | 'exit' : {
1348 |
1349 | 'longListing' :
1350 | 'Description:\n'\
1351 | '\tExits the interactive shell\n\n'\
1352 | 'Usage:\n'\
1353 | '\t%s',
1354 | 'quickView' :
1355 | 'Exit this shell'
1356 | },
1357 | 'save' : {
1358 | 'longListing' :
1359 | 'Description:\n'\
1360 | '\tSaves current host information to disk.\n\n'\
1361 | 'Usage:\n'\
1362 | '\t%s > [file prefix]\n'\
1363 | "\tSpecifying 'data' will save the raw host data to a file suitable for importing later via 'load'\n"\
1364 | "\tSpecifying 'info' will save data for the specified host in a human-readable format\n"\
1365 | "\tSpecifying a file prefix will save files in for format of 'struct_[prefix].mir' and info_[prefix].mir\n\n"\
1366 | 'Example:\n'\
1367 | '\t> save data wrt54g\n'\
1368 | '\t> save info 0 wrt54g\n\n'\
1369 | 'Notes:\n'\
1370 | "\to Data files are saved as 'struct_[prefix].mir'; info files are saved as 'info_[prefix].mir.'\n"\
1371 | "\to If no prefix is specified, the host index number will be used for the prefix.\n"\
1372 | "\to The data saved by the 'save info' command is the same as the output of the 'host details' command.",
1373 | 'quickView' :
1374 | 'Save current host data to file'
1375 | },
1376 | 'seti' : {
1377 | 'longListing' :
1378 | 'Description:\n'\
1379 | '\tAllows you to view and edit application settings.\n\n'\
1380 | 'Usage:\n'\
1381 | '\t%s | iface | socket >\n'\
1382 | "\t'show' displays the current program settings\n"\
1383 | "\t'uniq' toggles the show-only-uniq-hosts setting when discovering UPNP devices\n"\
1384 | "\t'debug' toggles debug mode\n"\
1385 | "\t'verbose' toggles verbose mode\n"\
1386 | "\t'version' changes the UPNP version used\n"\
1387 | "\t'iface' changes the network interface in use\n"\
1388 | "\t'socket' re-sets the multicast IP address and port number used for UPNP discovery\n\n"\
1389 | 'Example:\n'\
1390 | '\t> seti socket 239.255.255.250:1900\n'\
1391 | '\t> seti uniq\n\n'\
1392 | 'Notes:\n'\
1393 | "\tIf given no options, 'seti' will display the current application settings",
1394 | 'quickView' :
1395 | 'Show/define application settings'
1396 | },
1397 | 'head' : {
1398 | 'longListing' :
1399 | 'Description:\n'\
1400 | '\tAllows you to view, set, add and delete the SSDP header values used in SSDP transactions\n\n'\
1401 | 'Usage:\n'\
1402 | '\t%s | set >\n'\
1403 | "\t'set' allows you to set SSDP headers used when sending M-SEARCH queries with the 'msearch' command\n"\
1404 | "\t'del' deletes a current header from the list\n"\
1405 | "\t'show' displays all current header info\n\n"\
1406 | 'Example:\n'\
1407 | '\t> head show\n'\
1408 | '\t> head set MX 3',
1409 | 'quickView' :
1410 | 'Show/define SSDP headers'
1411 | },
1412 | 'host' : {
1413 | 'longListing' :
1414 | 'Description:\n'\
1415 | "\tAllows you to query host information and iteract with a host's actions/services.\n\n"\
1416 | 'Usage:\n'\
1417 | '\t%s [host index #]\n'\
1418 | "\t'list' displays an index of all known UPNP hosts along with their respective index numbers\n"\
1419 | "\t'get' gets detailed information about the specified host\n"\
1420 | "\t'details' gets and displays detailed information about the specified host\n"\
1421 | "\t'summary' displays a short summary describing the specified host\n"\
1422 | "\t'info' allows you to enumerate all elements of the hosts object\n"\
1423 | "\t'send' allows you to send SOAP requests to devices and services *\n\n"\
1424 | 'Example:\n'\
1425 | '\t> host list\n'\
1426 | '\t> host get 0\n'\
1427 | '\t> host summary 0\n'\
1428 | '\t> host info 0 deviceList\n'\
1429 | '\t> host send 0 \n\n'\
1430 | 'Notes:\n'\
1431 | "\to All host commands support full tab completion of enumerated arguments\n"\
1432 | "\to All host commands EXCEPT for the 'host send', 'host info' and 'host list' commands take only one argument: the host index number.\n"\
1433 | "\to The host index number can be obtained by running 'host list', which takes no futher arguments.\n"\
1434 | "\to The 'host send' command requires that you also specify the host's device name, service name, and action name that you wish to send,\n\t in that order (see the last example in the Example section of this output). This information can be obtained by viewing the\n\t 'host details' listing, or by querying the host information via the 'host info' command.\n"\
1435 | "\to The 'host info' command allows you to selectively enumerate the host information data structure. All data elements and their\n\t corresponding values are displayed; a value of '{}' indicates that the element is a sub-structure that can be further enumerated\n\t (see the 'host info' example in the Example section of this output).",
1436 | 'quickView' :
1437 | 'View and send host list and host information'
1438 | },
1439 | 'pcap' : {
1440 | 'longListing' :
1441 | 'Description:\n'\
1442 | '\tPassively listens for SSDP NOTIFY messages from UPNP devices\n\n'\
1443 | 'Usage:\n'\
1444 | '\t%s',
1445 | 'quickView' :
1446 | 'Passively listen for UPNP hosts'
1447 | },
1448 | 'msearch' : {
1449 | 'longListing' :
1450 | 'Description:\n'\
1451 | '\tActively searches for UPNP hosts using M-SEARCH queries\n\n'\
1452 | 'Usage:\n'\
1453 | "\t%s [device | service] [ | ]\n"\
1454 | "\tIf no arguments are specified, 'msearch' searches for upnp:rootdevices\n"\
1455 | "\tSpecific device/services types can be searched for using the 'device' or 'service' arguments\n\n"\
1456 | 'Example:\n'\
1457 | '\t> msearch\n'\
1458 | '\t> msearch service WANIPConnection\n'\
1459 | '\t> msearch device InternetGatewayDevice',
1460 | 'quickView' :
1461 | 'Actively locate UPNP hosts'
1462 | },
1463 | 'load' : {
1464 | 'longListing' :
1465 | 'Description:\n'\
1466 | "\tLoads host data from a struct file previously saved with the 'save data' command\n\n"\
1467 | 'Usage:\n'\
1468 | '\t%s ',
1469 | 'quickView' :
1470 | 'Restore previous host data from file'
1471 | },
1472 | 'log' : {
1473 | 'longListing' :
1474 | 'Description:\n'\
1475 | '\tLogs user-supplied commands to a log file\n\n'\
1476 | 'Usage:\n'\
1477 | '\t%s ',
1478 | 'quickView' :
1479 | 'Logs user-supplied commands to a log file'
1480 | }
1481 | }
1482 |
1483 |
1484 | try:
1485 | print helpInfo[command]['longListing'] % command
1486 | except:
1487 | for command,cmdHelp in helpInfo.iteritems():
1488 | print "%s\t\t%s" % (command,cmdHelp['quickView'])
1489 |
1490 | #Display usage
1491 | def usage():
1492 | print '''
1493 | Command line usage: %s [OPTIONS]
1494 |
1495 | -s Load previous host data from struct file
1496 | -l Log user-supplied commands to log file
1497 | -i Specify the name of the interface to use (Linux only, requires root)
1498 | -u Disable show-uniq-hosts-only option
1499 | -d Enable debug mode
1500 | -v Enable verbose mode
1501 | -h Show help
1502 | ''' % sys.argv[0]
1503 | sys.exit(1)
1504 |
1505 | #Check command line options
1506 | def parseCliOpts(argc,argv,hp):
1507 | try:
1508 | opts,args = getopt.getopt(argv[1:],'s:l:i:udvh')
1509 | except getopt.GetoptError, e:
1510 | print 'Usage Error:',e
1511 | usage()
1512 | else:
1513 | for (opt,arg) in opts:
1514 | if opt == '-s':
1515 | print ''
1516 | load(2,['load',arg],hp)
1517 | print ''
1518 | elif opt == '-l':
1519 | print ''
1520 | log(2,['log',arg],hp)
1521 | print ''
1522 | elif opt == '-u':
1523 | hp.UNIQ = toggleVal(hp.UNIQ)
1524 | elif opt == '-d':
1525 | hp.DEBUG = toggleVal(hp.DEBUG)
1526 | print 'Debug mode enabled!'
1527 | elif opt == '-v':
1528 | hp.VERBOSE = toggleVal(hp.VERBOSE)
1529 | print 'Verbose mode enabled!'
1530 | elif opt == '-h':
1531 | usage()
1532 | elif opt == '-i':
1533 | networkInterfaces = []
1534 | requestedInterface = arg
1535 | interfaceName = None
1536 | found = False
1537 |
1538 | #Get a list of network interfaces. This only works on unix boxes.
1539 | try:
1540 | if thisSystem() != 'Windows':
1541 | fp = open('/proc/net/dev','r')
1542 | for line in fp.readlines():
1543 | if ':' in line:
1544 | interfaceName = line.split(':')[0].strip()
1545 | if interfaceName == requestedInterface:
1546 | found = True
1547 | break
1548 | else:
1549 | networkInterfaces.append(line.split(':')[0].strip())
1550 | fp.close()
1551 | else:
1552 | networkInterfaces.append('Run ipconfig to get a list of available network interfaces!')
1553 | except Exception,e:
1554 | print 'Error opening file:',e
1555 | print "If you aren't running Linux, this file may not exist!"
1556 |
1557 | if not found and len(networkInterfaces) > 0:
1558 | print "Failed to find interface '%s'; try one of these:\n" % requestedInterface
1559 | for iface in networkInterfaces:
1560 | print iface
1561 | print ''
1562 | sys.exit(1)
1563 | else:
1564 | if not hp.initSockets(False,False,interfaceName):
1565 | print 'Binding to interface %s failed; are you sure you have root privilages??' % interfaceName
1566 |
1567 | #Toggle boolean values
1568 | def toggleVal(val):
1569 | if val:
1570 | return False
1571 | else:
1572 | return True
1573 |
1574 | #Prompt for user input
1575 | def getUserInput(hp,shellPrompt):
1576 | defaultShellPrompt = 'upnp> '
1577 | if shellPrompt == False:
1578 | shellPrompt = defaultShellPrompt
1579 |
1580 | try:
1581 | uInput = raw_input(shellPrompt).strip()
1582 | argv = uInput.split()
1583 | argc = len(argv)
1584 | except KeyboardInterrupt, e:
1585 | print '\n'
1586 | if shellPrompt == defaultShellPrompt:
1587 | quit(0,[],hp)
1588 | return (0,None)
1589 | if hp.LOG_FILE != False:
1590 | try:
1591 | hp.LOG_FILE.write("%s\n" % uInput)
1592 | except:
1593 | print 'Failed to log data to log file!'
1594 |
1595 | return (argc,argv)
1596 |
1597 | #Main
1598 | def main(argc,argv):
1599 | #Table of valid commands - all primary commands must have an associated function
1600 | appCommands = {
1601 | 'help' : {
1602 | 'help' : None
1603 | },
1604 | 'quit' : {
1605 | 'help' : None
1606 | },
1607 | 'exit' : {
1608 | 'help' : None
1609 | },
1610 | 'save' : {
1611 | 'data' : None,
1612 | 'info' : None,
1613 | 'help' : None
1614 | },
1615 | 'load' : {
1616 | 'help' : None
1617 | },
1618 | 'seti' : {
1619 | 'uniq' : None,
1620 | 'socket' : None,
1621 | 'show' : None,
1622 | 'iface' : None,
1623 | 'debug' : None,
1624 | 'version' : None,
1625 | 'verbose' : None,
1626 | 'help' : None
1627 | },
1628 | 'head' : {
1629 | 'set' : None,
1630 | 'show' : None,
1631 | 'del' : None,
1632 | 'help': None
1633 | },
1634 | 'host' : {
1635 | 'list' : None,
1636 | 'info' : None,
1637 | 'get' : None,
1638 | 'details' : None,
1639 | 'send' : None,
1640 | 'summary' : None,
1641 | 'help' : None
1642 | },
1643 | 'pcap' : {
1644 | 'help' : None
1645 | },
1646 | 'msearch' : {
1647 | 'device' : None,
1648 | 'service' : None,
1649 | 'help' : None
1650 | },
1651 | 'log' : {
1652 | 'help' : None
1653 | },
1654 | 'debug': {
1655 | 'command' : None,
1656 | 'help' : None
1657 | }
1658 | }
1659 |
1660 | #The load command should auto complete on the contents of the current directory
1661 | for file in os.listdir(os.getcwd()):
1662 | appCommands['load'][file] = None
1663 |
1664 | #Initialize upnp class
1665 | hp = upnp(False, False, None, appCommands);
1666 |
1667 | #Set up tab completion and command history
1668 | readline.parse_and_bind("tab: complete")
1669 | readline.set_completer(hp.completer.complete)
1670 |
1671 | #Set some default values
1672 | hp.UNIQ = True
1673 | hp.VERBOSE = False
1674 | action = False
1675 | funPtr = False
1676 |
1677 | #Check command line options
1678 | parseCliOpts(argc,argv,hp)
1679 |
1680 | #Main loop
1681 | while True:
1682 | #Drop user into shell
1683 | (argc,argv) = getUserInput(hp,False)
1684 | if argc == 0:
1685 | continue
1686 | action = argv[0]
1687 | funcPtr = False
1688 |
1689 | print ''
1690 | #Parse actions
1691 | try:
1692 | if appCommands.has_key(action):
1693 | funcPtr = eval(action)
1694 | except:
1695 | funcPtr = False
1696 | action = False
1697 |
1698 | if callable(funcPtr):
1699 | if argc == 2 and argv[1] == 'help':
1700 | showHelp(argv[0])
1701 | else:
1702 | try:
1703 | funcPtr(argc,argv,hp)
1704 | except KeyboardInterrupt:
1705 | print 'Action interrupted by user...'
1706 | print ''
1707 | continue
1708 | print 'Invalid command. Valid commands are:'
1709 | print ''
1710 | showHelp(False)
1711 | print ''
1712 |
1713 |
1714 | if __name__ == "__main__":
1715 | try:
1716 | main(len(sys.argv),sys.argv)
1717 | except Exception, e:
1718 | print 'Caught main exception:',e
1719 | sys.exit(1)
1720 |
1721 |
--------------------------------------------------------------------------------
/wemo.py:
--------------------------------------------------------------------------------
1 | from miranda import upnp, msearch
2 |
3 | conn = upnp()
4 | msearch(0, 0, conn, 2)
5 |
6 | SWITCHES = []
7 |
8 | # populate all the host info, for every upnp device on the network
9 | for index in conn.ENUM_HOSTS:
10 | hostInfo = conn.ENUM_HOSTS[index]
11 | if hostInfo['dataComplete'] == False:
12 | xmlHeaders, xmlData = conn.getXML(hostInfo['xmlFile'])
13 | conn.getHostInfo(xmlData,xmlHeaders,index)
14 |
15 |
16 | for index in conn.ENUM_HOSTS:
17 | try:
18 | if conn.ENUM_HOSTS[index]['deviceList']['controllee']['modelName'] == 'Socket':
19 | SWITCHES = [index]
20 | except KeyError:
21 | pass
22 |
23 |
24 | def _send(action, args=None):
25 | if not args:
26 | args = {}
27 | host_info = conn.ENUM_HOSTS[SWITCHES[0]]
28 | device_name = 'controllee'
29 | service_name = 'basicevent'
30 | controlURL = host_info['proto'] + host_info['name']
31 | controlURL2 = hostInfo['deviceList'][device_name]['services'][service_name]['controlURL']
32 | if not controlURL.endswith('/') and not controlURL2.startswith('/'):
33 | controlURL += '/'
34 | controlURL += controlURL2
35 |
36 | resp = conn.sendSOAP(
37 | host_info['name'],
38 | 'urn:Belkin:service:basicevent:1',
39 | controlURL,
40 | action,
41 | args
42 | )
43 | return resp
44 |
45 | def get():
46 | """
47 | Gets the value of the first switch that it finds
48 | """
49 | resp = _send('GetBinaryState')
50 | tagValue = conn.extractSingleTag(resp, 'BinaryState')
51 | return True if tagValue == '1' else False
52 |
53 | def on():
54 | """
55 | Turns on the first switch that it finds.
56 |
57 | BinaryState is set to 'Error' in the case that it was already on.
58 | """
59 | resp = _send('SetBinaryState', {'BinaryState': (1, 'Boolean')})
60 | tagValue = conn.extractSingleTag(resp, 'BinaryState')
61 | return True if tagValue in ['1', 'Error'] else False
62 |
63 | def off():
64 | """
65 | Turns off the first switch that it finds.
66 |
67 | BinaryState is set to 'Error' in the case that it was already off.
68 | """
69 | resp = _send('SetBinaryState', {'BinaryState': (0, 'Boolean')})
70 | tagValue = conn.extractSingleTag(resp, 'BinaryState')
71 | return True if tagValue in ['0', 'Error'] else False
72 |
--------------------------------------------------------------------------------