├── 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 = "" % 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' % (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 | --------------------------------------------------------------------------------