# Name:UPNP console # File:upnp_console.py # Author: Ján Trenčanský # Original: Craig Heffner # License: GNU GPL v3 # Created: 30.07.2016 # Last modified: 21.08.2016 # Disclaimer: most of the code is ported to python 3 from his awesome miranda-upnp tool # his tool has only one disadvantage, very bad UI in my opinion # https://code.google.com/archive/p/mirandaupnptool/ import core.io import time import base64 import cmd import socket import struct import select import requests import xml.dom.minidom import re import traceback import sys import core.globals import interface.utils from interface.messages import print_error, print_help, print_info, print_warning, print_red, \ print_success class Upnp(cmd.Cmd): """ # Name:UPNP console # Author: Ján Trenčanský # Original: Craig Heffner # License: GNU GPL v3 # Disclaimer: most of the code is ported to python 3 from his awesome miranda-upnp tool # his tool has only one disadvantage, very bad UI in my opinion # https://code.google.com/archive/p/mirandaupnptool/ """ host = "239.255.255.250" # Should be modifiable port = 1900 # and this msearchHeaders = {'MAN': '"ssdp:discover"', 'MX': '2'} upnp_version = '1.0' max_recv = 8192 max_hosts = 0 timeout = 0 # and this http_headers = [] enum_hosts = {} verbose = False # and this uniq = True # and this log_file = False # and this batch_file = None # and this interface = None # and this csock = False ssock = False mreq = None soapEnd = None def __init__(self): cmd.Cmd.__init__(self) interface.utils.change_prompt(self, core.globals.active_module_path + core.globals.active_script) self.soapEnd = re.compile('</.*:envelope>') self.initialize_sockets() self.cmdloop() def initialize_sockets(self): try: # This is needed to join a multicast group self.mreq = struct.pack("4sl", socket.inet_aton(self.host), socket.INADDR_ANY) # Set up client socket self.csock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.csock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) # Set up server socket self.ssock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) self.ssock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # BSD systems also need to set SO_REUSEPORT try: self.ssock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) except Exception: pass # Only bind to this interface if self.interface is not None: print_info("Binding to interface: " + self.interface) self.ssock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, struct.pack("%ds" % (len(self.interface) + 1,), self.interface)) self.csock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, struct.pack("%ds" % (len(self.interface) + 1,), self.interface)) try: self.ssock.bind(('', self.port)) except Exception: print_warning("failed to bind: " + self.host + ":" + str(self.port) + " ") try: self.ssock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, self.mreq) except Exception: print_warning("failed to join multicast group") except Exception: print_error("failed to initialize UPNP sockets") return False return True # Clean up file/socket descriptors def cleanup(self): self.csock.close() self.ssock.close() # Send network data def send(self, data, sock): # By default, use the client socket that's part of this class if not sock: sock = self.csock try: sock.sendto(bytes(data, 'UTF-8'), (self.host, self.port)) return True except Exception as e: print_error("send method failed for " + self.host + ":" + str(self.port)) traceback.print_tb(e) return False # Receive network data def recieve(self, size, sock): if not sock: sock = self.ssock if self.timeout: sock.setblocking(0) ready = select.select([sock], [], [], self.timeout)[0] else: sock.setblocking(1) ready = True try: if ready: return sock.recv(size) else: return False except: return False # Create new UDP socket on ip, bound to port def create_new_listener(self, ip, port): try: newsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) newsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # BSD systems also need to set SO_REUSEPORT try: newsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) except: pass newsock.bind((ip, port)) return newsock except Exception: return False # Return the class's primary server socket def listener(self): return self.ssock # Return the class's primary client socket def sender(self): return self.csock # Parse a URL, return the host and the page def parse_url(self, url): delim = '://' host = False # page = False # Split the host and page try: (host, page) = url.split(delim)[1].split('/', 1) page = '/' + page except: # If '://' is not in the url, then it's not a full URL, so assume that it's just a relative path page = url return host, page # Pull the header info for the specified HTTP header - case insensitive def parse_header(self, data, header): delimiter = "%s:" % header lowerdelim = delimiter.lower() dataarray = data.split("\r\n") # Loop through each line of the headers for line in dataarray: lowerline = line.lower() # Does this line start with the header we're looking for? if lowerline.startswith(lowerdelim): try: return line.split(':', 1)[1].strip() except: print_error("parsing header data failed for: " + header) # Parses SSDP notify and reply packets, and populates the ENUM_HOSTS dict def parse_ssdp_info(self, data, show_uniq, verbose): data = data.decode('utf-8') # When Ctl-C is pressed data is set to False and exception should be raised host_found = False # found_location = False message_type = None # xml_file = None # host = False # page = False # upnp_type = None known_headers = {'NOTIFY': 'notification', 'HTTP/1.1 200 OK': 'reply'} # Use the class defaults if these aren't specified # if not show_uniq: # show_uniq = self.uniq # if not verbose: # verbose = self.verbose # Is the SSDP packet a notification, a reply, or neither? for text, message_type in known_headers.items(): if data.upper().startswith(text): break else: message_type = False # If this is a notification or a reply message... if message_type: # Get the host name and location of its main UPNP XML file xml_file = self.parse_header(data, "LOCATION") upnp_type = self.parse_header(data, "SERVER") (host, page) = self.parse_url(xml_file) # Sanity check to make sure we got all the info we need if xml_file is None or host is False or page is False: print_error("parsing recieved header:") print_red(data) return False # Get the protocol in use (i.e., http, https, etc) protocol = xml_file.split('://')[0] + '://' # Check if we've seen this host before; add to the list of hosts if: # 1. This is a new host # 2. We've already seen this host, but the uniq hosts setting is disabled for hostID, hostInfo in self.enum_hosts.items(): if hostInfo['name'] == host: host_found = True if self.uniq: return False if (host_found and not self.uniq) or not host_found: # Get the new host's index number and create an entry in ENUM_HOSTS index = len(self.enum_hosts) self.enum_hosts[index] = { 'name': host, 'dataComplete': False, 'proto': protocol, 'xml_file': xml_file, 'serverType': None, 'upnpServer': upnp_type, 'deviceList': {} } # Be sure to update the command completer so we can tab complete through this host's data structure # self.updateCmdCompleter(self.ENUM_HOSTS) # Print out some basic device info print_info("SSDP " + message_type + " message from " + host) if xml_file: # found_location = True print_info("XML file is located at " + xml_file) if upnp_type: print_info("Device is running: " + upnp_type) return True # Send GET request for a UPNP XML file def get_xml(self, url): headers = {'USER-AGENT': 'uPNP/' + self.upnp_version, 'CONTENT-TYPE': 'text/xml; charset="utf-8"'} try: # Use urllib2 for the request, it's awesome # req = urllib.Request(url, None, headers) # This is GET # response = urllib.urlopen(req) response = requests.get(url, headers=headers, timeout=60) output = response.text headers = response.headers return headers, output except Exception: print_error("Request for '%s' failed" % url) return False, False # Pull the name of the device type from a device type string # The device type string looks like: 'urn:schemas-upnp-org:device:WANDevice:1' def parse_device_type_name(self, string): delim1 = 'device:' delim2 = ':' if delim1 in string and not string.endswith(delim1): return string.split(delim1)[1].split(delim2, 1)[0] return False # Pull the name of the service type from a service type string # The service type string looks like: 'urn:schemas-upnp-org:service:Layer3Forwarding:1' def parse_service_type_name(self, string): delim1 = 'service:' delim2 = ':' if delim1 in string and not string.endswith(delim1): return string.split(delim1)[1].split(delim2, 1)[0] return False # Get info about a service's state variables def parse_service_state_vars(self, xml_root, service_pointer): na = 'N/A' var_vals = ['sendEvents', 'dataType', 'defaultValue', 'allowedValues'] service_state_table = 'serviceStateTable' state_variable = 'stateVariable' name_tag = 'name' data_type = 'dataType' send_events = 'sendEvents' allowed_value_list = 'allowedValueList' allowed_value = 'allowedValue' allowed_value_range = 'allowedValueRange' minimum = 'minimum' maximum = 'maximum' # Create the serviceStateVariables entry for this service in ENUM_HOSTS service_pointer['serviceStateVariables'] = {} # Get a list of all state variables associated with this service try: state_vars = xml_root.getElementsByTagName(service_state_table)[0].getElementsByTagName(state_variable) except: # Don't necessarily want to throw an error here, as there may be no service state variables return False # Loop through all state variables for var in state_vars: for tag in var_vals: # Get variable name try: var_name = str(var.getElementsByTagName(name_tag)[0].childNodes[0].data) except: print_error( 'Failed to get service state variable name for service %s!' % service_pointer['fullName']) continue service_pointer['serviceStateVariables'][var_name] = {} try: service_pointer['serviceStateVariables'][var_name]['dataType'] = str( var.getElementsByTagName(data_type)[0].childNodes[0].data) except: service_pointer['serviceStateVariables'][var_name]['dataType'] = na try: service_pointer['serviceStateVariables'][var_name]['sendEvents'] = str( var.getElementsByTagName(send_events)[0].childNodes[0].data) except: service_pointer['serviceStateVariables'][var_name]['sendEvents'] = na service_pointer['serviceStateVariables'][var_name][allowed_value_list] = [] # Get a list of allowed values for this variable try: vals = var.getElementsByTagName(allowed_value_list)[0].getElementsByTagName(allowed_value) except: pass else: # Add the list of allowed values to the ENUM_HOSTS dictionary for val in vals: service_pointer['serviceStateVariables'][var_name][allowed_value_list].append( str(val.childNodes[0].data)) # Get allowed value range for this variable try: val_list = var.getElementsByTagName(allowed_value_range)[0] except: pass else: # Add the max and min values to the ENUM_HOSTS dictionary service_pointer['serviceStateVariables'][var_name][allowed_value_range] = [] try: service_pointer['serviceStateVariables'][var_name][allowed_value_range].append( str(val_list.getElementsByTagName(minimum)[0].childNodes[0].data)) service_pointer['serviceStateVariables'][var_name][allowed_value_range].append( str(val_list.getElementsByTagName(maximum)[0].childNodes[0].data)) except: pass return True # Parse details about each service (arguements, variables, etc) def parse_service_info(self, service, index): # argIndex = 0 arg_tags = ['direction', 'relatedStateVariable'] action_list = 'actionList' action_tag = 'action' name_tag = 'name' argument_list = 'argumentList' argument_tag = 'argument' # Get the full path to the service's XML file xml_file = self.enum_hosts[index]['proto'] + self.enum_hosts[index]['name'] if not xml_file.endswith('/') and not service['SCPDURL'].startswith('/'): try: xml_service_file = self.enum_hosts[index]['xml_file'] slash_index = xml_service_file.rfind('/') xml_file = xml_service_file[:slash_index] + '/' except: xml_file += '/' if self.enum_hosts[index]['proto'] in service['SCPDURL']: xml_file = service['SCPDURL'] else: xml_file += service['SCPDURL'] service['actions'] = {} # Get the XML file that describes this service (xml_headers, xml_data) = self.get_xml(xml_file) if not xml_data: print_error('Failed to retrieve service descriptor located at:', xml_file) return False try: xml_root = xml.dom.minidom.parseString(xml_data) # Get a list of actions for this service try: action_list = xml_root.getElementsByTagName(action_list)[0] except: print_error('Failed to retrieve action list for service %s!' % service['fullName']) return False actions = action_list.getElementsByTagName(action_tag) if not actions: return False # Parse all actions in the service's action list for action in actions: # Get the action's name try: action_name = str(action.getElementsByTagName(name_tag)[0].childNodes[0].data).strip() except: print_error('Failed to obtain service action name (%s)!' % service['fullName']) continue # Add the action to the ENUM_HOSTS dictonary service['actions'][action_name] = {} service['actions'][action_name]['arguments'] = {} # Parse all of the action's arguments try: arg_list = action.getElementsByTagName(argument_list)[0] except: # Some actions may take no arguments, so continue without raising an error here... continue # Get all the arguments in this action's argument list arguments = arg_list.getElementsByTagName(argument_tag) if not arguments: if self.verbose: print_error('Action', action_name, 'has no arguments!') continue # Loop through the action's arguments, appending them to the ENUM_HOSTS dictionary for argument in arguments: try: arg_name = str(argument.getElementsByTagName(name_tag)[0].childNodes[0].data) except: print_error('Failed to get argument name for', action_name) continue service['actions'][action_name]['arguments'][arg_name] = {} # Get each required argument tag value and add them to ENUM_HOSTS for tag in arg_tags: try: service['actions'][action_name]['arguments'][arg_name][tag] = str( argument.getElementsByTagName(tag)[0].childNodes[0].data) except: print_error('Failed to find tag %s for argument %s!' % (tag, arg_name)) continue # Parse all of the state variables for this service self.parse_service_state_vars(xml_root, service) except Exception as e: print_error( 'Caught exception while parsing Service info for service %s: %s' % (service['fullName'], str(e))) return False return True # Parse the list of services specified in the XML file def parse_service_list(self, xml_root, device, index): # serviceEntryPointer = False dict_name = "services" service_list_tag = "serviceList" service_tag = "service" service_name_tag = "serviceType" service_tags = ["serviceId", "controlURL", "eventSubURL", "SCPDURL"] try: device[dict_name] = {} # Get a list of all services offered by this device for service in xml_root.getElementsByTagName(service_list_tag)[0].getElementsByTagName(service_tag): # Get the full service descriptor service_name = str(service.getElementsByTagName(service_name_tag)[0].childNodes[0].data) # Get the service name from the service descriptor string service_display_name = self.parse_service_type_name(service_name) if not service_display_name: continue # Create new service entry for the device in ENUM_HOSTS service_entry_pointer = device[dict_name][service_display_name] = {} service_entry_pointer['fullName'] = service_name # Get all of the required service info and add it to ENUM_HOSTS for tag in service_tags: service_entry_pointer[tag] = str(service.getElementsByTagName(tag)[0].childNodes[0].data) # Get specific service info about this service self.parse_service_info(service_entry_pointer, index) except Exception as e: print_error('Caught exception while parsing device service list:', e) # Parse device info from the retrieved XML file def parse_device_info(self, xml_root, index): # device_entry_pointer = False dev_tag = "device" device_type = "deviceType" device_list_entries = "deviceList" device_tags = ["friendlyName", "modelDescription", "modelName", "modelNumber", "modelURL", "presentationURL", "UDN", "UPC", "manufacturer", "manufacturerURL"] # Find all device entries listed in the XML file for device in xml_root.getElementsByTagName(dev_tag): try: # Get the deviceType string device_type_name = str(device.getElementsByTagName(device_type)[0].childNodes[0].data) except: continue # Pull out the action device name from the deviceType string device_display_name = self.parse_device_type_name(device_type_name) if not device_display_name: continue # Create a new device entry for this host in the ENUM_HOSTS structure device_entry_pointer = self.enum_hosts[index][device_list_entries][device_display_name] = {} device_entry_pointer['fullName'] = device_type_name # Parse out all the device tags for that device for tag in device_tags: try: device_entry_pointer[tag] = str(device.getElementsByTagName(tag)[0].childNodes[0].data) except Exception as e: if self.verbose: print_error('Device', device_entry_pointer['fullName'], 'does not have a', tag) continue # Get a list of all services for this device listing self.parse_service_list(device, device_entry_pointer, index) return # Display all info for a given host def show_complete_host_info(self, index, fp=False): # na = 'N/A' service_keys = ['controlURL', 'eventSubURL', 'serviceId', 'SCPDURL', 'fullName'] if not fp: fp = sys.stdout if index < 0 or index >= len(self.enum_hosts): fp.write('Specified host does not exist...\n') return try: host_info = self.enum_hosts[index] if not host_info['dataComplete']: print_warning( "Cannot show all host info because I don't have it all yet. Try running 'host info %d' first...\n" % index) fp.write('Host name: %s\n' % host_info['name']) fp.write('UPNP XML File: %s\n\n' % host_info['xml_file']) fp.write('\nDevice information:\n') for deviceName, deviceStruct in host_info['deviceList'].items(): fp.write('\tDevice Name: %s\n' % deviceName) for serviceName, serviceStruct in deviceStruct['services'].items(): fp.write('\t\tService Name: %s\n' % serviceName) for key in service_keys: fp.write('\t\t\t%s: %s\n' % (key, serviceStruct[key])) fp.write('\t\t\tServiceActions:\n') for actionName, actionStruct in serviceStruct['actions'].items(): fp.write('\t\t\t\t%s\n' % actionName) for argName, argStruct in actionStruct['arguments'].items(): fp.write('\t\t\t\t\t%s \n' % argName) for key, val in argStruct.items(): if key == 'relatedStateVariable': fp.write('\t\t\t\t\t\t%s:\n' % val) for k, v in serviceStruct['serviceStateVariables'][val].items(): fp.write('\t\t\t\t\t\t\t%s: %s\n' % (k, v)) else: fp.write('\t\t\t\t\t\t%s: %s\n' % (key, val)) except Exception as e: print_error('Caught exception while showing host info:') traceback.print_stack(e) # Wrapper function... def get_host_information(self, xml_data, xml_headers, index): if self.enum_hosts[index]['dataComplete']: return if 0 <= index < len(self.enum_hosts): try: xml_root = xml.dom.minidom.parseString(xml_data) self.parse_device_info(xml_root, index) # self.enum_hosts[index]['serverType'] = xml_headers.getheader('Server') self.enum_hosts[index]['serverType'] = xml_headers['Server'] self.enum_hosts[index]['dataComplete'] = True return True except Exception as e: print_error('Caught exception while getting host info:') traceback.print_stack(e) return False def do_back(self, e): return True def do_set(self, e): args = e.split(' ') try: if args[0] == "host": if interface.utils.validate_ipv4(args[1]): self.host = args[1] else: print_error("please provide valid IPv4 address") elif args[0] == "port": if str.isdigit(args[1]): self.port = args[1] else: print_error("port value must be integer") except IndexError: print_error("please specify value for variable") def complete_set(self, text, line, begidx, endidx): modules = ["host", "port"] module_line = line.partition(' ')[2] igon = len(module_line) - len(text) return [s[igon:] for s in modules if s.startswith(module_line)] def do_info(self, e): print(self.__doc__) def do_host(self, e): print_info(self.host) def do_port(self, e): print_info(str(self.port)) def help_back(self): print_help("Exit script") def help_host(self): print_help("Prints current value of host") def help_port(self): print_help("Prints current value of port") def help_set(self): print_help("Set value of variable: \"set host 192.168.1.1\"") def help_info(self): print_help("Show info about loaded module") def help_msearch(self): print_help("Actively locate UPNP hosts") def help_device(self): print_help("Allows you to query host information and iteract with a host's actions/services.") print("""\n\tdevice <list | get | info | summary | details | send> [host index #] 'list' displays an index of all known UPNP hosts along with their respective index numbers 'get' gets detailed information about the specified host 'details' gets and displays detailed information about the specified host 'summary' displays a short summary describing the specified host 'info' allows you to enumerate all elements of the hosts object 'send' allows you to send SOAP requests to devices and services Example: > device list > device get 0 > device summary 0 > device info 0 deviceList > device send 0 <device name> <service name> <action name> Notes: - All device commands EXCEPT for the 'device send', 'device info' and 'device list' commands take only one argument: the device index number. - The device index number can be obtained by running 'device list', which takes no futher arguments. - The 'device send' command requires that you also specify the device's device name, service name, and action name that you wish to send, in that order (see the last example in the Example section of this output). This information can be obtained by viewing the 'device details' listing, or by querying the host information via the 'device info' command. - The 'device info' command allows you to selectively enumerate the device information data structure. All data elements and their corresponding values are displayed; a value of '{}' indicates that the element is a sub-structure that can be further enumerated (see the 'device info' example in the Example section of this output). """) print_help("Originally this was miranda host command, but REXT already has host command") def help_add(self): print_help("Allows you to manually add device (e.g. shodan search result)") print("\tusage:add [device name] [device xml root]") print("\texample: add 192.168.1.2:49152 http://192.168.1.2:49152/description.xml") def help_pcap(self): print_help("Passively listens for SSDP NOTIFY messages from UPNP devices") # Passively listen for UPNP NOTIFY packets def do_pcap(self, e): print_info('Entering passive mode, Ctrl+C') count = 0 start = time.time() while True: try: if 0 < self.max_hosts <= count: break if 0 < self.timeout < (time.time() - start): raise Exception("Timeout exceeded") if self.parse_ssdp_info(self.recieve(1024, False), False, False): count += 1 except Exception as e: print("\n") print_info("Passive mode halted...") break def do_msearch(self, e): default_st = "upnp:rootdevice" st = "schemas-upnp-org" myip = '' lport = self.port # if argc >= 3: # if argc == 4: # st = argv[1] # searchType = argv[2] # searchName = argv[3] # else: # searchType = argv[1] # searchName = argv[2] # st = "urn:%s:%s:%s:%s" % (st,searchType,searchName,hp.UPNP_VERSION.split('.')[0]) # else: st = default_st # Build the request request = "M-SEARCH * HTTP/1.1\r\n" \ "HOST:%s:%d\r\n" \ "ST:%s\r\n" % (self.host, self.port, st) for header, value in self.msearchHeaders.items(): request += header + ':' + value + "\r\n" request += "\r\n" print_info("Entering discovery mode for '%s', Ctl+C to stop..." % st) # Have to create a new socket since replies will be sent directly to our IP, not the multicast IP server = self.create_new_listener(myip, lport) if not server: print_error('Failed to bind port %d' % lport) return self.send(request, server) count = 0 start = time.time() while True: try: if 0 < self.max_hosts <= count: break if 0 < self.timeout < (time.time() - start): raise Exception("Timeout exceeded") if self.parse_ssdp_info(self.recieve(1024, server), False, False): count += 1 except AttributeError: # On Ctrl-C parseSSDPInfo raises AttributeError exception print('\n') print_info('Discover mode halted...') break def get_host_info(self, host_info, index): if host_info is not None: # If this host data is already complete, just display it if host_info['dataComplete']: print_warning('Data for this host has already been enumerated!') return try: # Get extended device and service information if host_info: print_info("Requesting device and service info for " + host_info['name'] + " (this could take a few seconds)...") if not host_info['dataComplete']: (xml_headers, xml_data) = self.get_xml(host_info['xml_file']) # print(xmlHeaders) # print(xmlData) if not xml_data: print_error('Failed to request host XML file:' + host_info['xml_file']) return if not self.get_host_information(xml_data, xml_headers, index): print_error("Failed to get device/service info for " + host_info['name']) return print_success('Host data enumeration complete!') # hp.updateCmdCompleter(hp.ENUM_HOSTS) return except KeyboardInterrupt: return def getUserInput(self, shellPrompt): defaultShellPrompt = 'upnp> ' if shellPrompt == False: shellPrompt = defaultShellPrompt try: uInput = input(shellPrompt).strip() argv = uInput.split() argc = len(argv) except KeyboardInterrupt as e: print('\n') return 0, None return argc, argv # Send SOAP request def send_soap(self, host_name, service_type, control_url, action_name, action_arguments): arg_list = '' soap_response = '' if '://' in control_url: url_array = control_url.split('/', 3) if len(url_array) < 4: control_url = '/' else: control_url = '/' + url_array[3] soap_request = 'POST %s HTTP/1.1\r\n' % control_url # Check if a port number was specified in the host name; default is port 80 if ':' in host_name: host_name_array = host_name.split(':') host = host_name_array[0] try: port = int(host_name_array[1]) except: print_error('Invalid port specified for host connection:', host_name[1]) return False else: host = host_name port = 80 # Create a string containing all of the SOAP action's arguments and values for arg, (val, dt) in action_arguments.items(): arg_list += '<%s>%s</%s>' % (arg, val, arg) # Create the SOAP request soap_body = '<?xml version="1.0"?>\n' \ '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">\n' \ '<SOAP-ENV:Body>\n' \ '\t<m:%s xmlns:m="%s">\n' \ '%s\n' \ '\t</m:%s>\n' \ '</SOAP-ENV:Body>\n' \ '</SOAP-ENV:Envelope>' % (action_name, service_type, arg_list, action_name) # Specify the headers to send with the request headers = { 'Host': host_name, 'Content-Length': len(soap_body), 'Content-Type': 'text/xml', 'SOAPAction': '"%s#%s"' % (service_type, action_name) } # Generate the final payload for head, value in headers.items(): soap_request += '%s: %s\r\n' % (head, value) soap_request += '\r\n%s' % soap_body # Send data and go into recieve loop sock = None try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((host, port)) # DEBUG = 0 # if DEBUG: # print(soap_request) sock.send(bytes(soap_request, 'UTF-8')) while True: data = sock.recv(self.max_recv) if not data: break else: soap_response += data.decode('UTF-8') if self.soapEnd.search(soap_response.lower()) is not None: break sock.close() (header, body) = soap_response.split('\r\n\r\n', 1) if not header.upper().startswith('HTTP/1.') and ' 200 ' in header.split('\r\n')[0]: print_error('SOAP request failed with error code:', header.split('\r\n')[0].split(' ', 1)[1]) error_msg = self.extract_single_tag(body, 'errorDescription') if error_msg: print_error('SOAP error message:', error_msg) return False else: return body except Exception as e: print_error('Caught socket exception:') traceback.print_tb(e) sock.close() return False except KeyboardInterrupt: sock.close() return False # Extract the contents of a single XML tag from the data def extract_single_tag(self, data, tag): start_tag = "<%s" % tag end_tag = "</%s>" % tag try: tmp = data.split(start_tag)[1] index = tmp.find('>') if index != -1: index += 1 return tmp[index:].split(end_tag)[0].strip() except: pass return None def do_add(self, e): args = e.split(' ') if len(args) != 2: print_error("Invalid number of arguments") else: index = len(self.enum_hosts) self.enum_hosts[index] = { 'name': args[0], 'dataComplete': False, 'proto': 'http://', 'xml_file': args[1], 'serverType': None, 'upnpServer': None, 'deviceList': {} } def do_device(self, e): # This was originally host command but since REXT uses host on something else... # host_info = None args = e.split(' ') if args[0] == "get": if len(args) != 2: print_error("Invalid number of arguments") return try: index = int(args[1]) host_info = self.enum_hosts[index] except Exception: print_error("Second argument is not a number") return self.get_host_info(host_info, index) elif args[0] == "details": try: index = int(args[1]) host_info = self.enum_hosts[index] except Exception as e: print_error("Index error") return try: # If this host data is already complete, just display it if host_info['dataComplete']: self.show_complete_host_info(index) else: print_error("Can't show host info because I don't have it. Please run 'host get %d'" % index) except KeyboardInterrupt as e: pass return elif args[0] == "list": if len(self.enum_hosts) == 0: print_info("No known hosts - try running the 'msearch' or 'pcap' commands") return for index, host_info in self.enum_hosts.items(): print_info("[%d] %s" % (index, host_info['name'])) return elif args[0] == "summary": try: index = int(args[1]) host_info = self.enum_hosts[index] except: print_error("Please provide correct device id") return print('Host:', host_info['name']) print('XML File:', host_info['xml_file']) for device_name, deviceData in host_info['deviceList'].items(): print(device_name) for k, v in deviceData.items(): if isinstance(v, dict): continue else: print("\t%s: %s" % (k, v)) # try: # v.has_key(False) # Has key removed in python3 # except: # print("\t%s: %s" % (k,v)) print('') return elif args[0] == 'info': output = self.enum_hosts data_structs = [] for arg in args[1:]: try: arg = int(arg) except: pass output = output[arg] try: for k, v in output.items(): if isinstance(v, dict): data_structs.append(k) else: print(k, ':', v) continue # try: # v.has_key(False) # dataStructs.append(k) # except: # print(k,':',v) # continue except: print(output) for struct in data_structs: print(struct, ': {}') return elif args[0] == 'send': # Send SOAP requests # index = False in_arg_counter = 0 if len(args) != 5: # showHelp(argv[0]) return else: try: index = int(args[1]) host_info = self.enum_hosts[index] except: print('indexError') return device_name = args[2] service_name = args[3] action_name = args[4] # action_args = False send_args = {} ret_tags = [] # controlURL = False # full_service_name = False # Get the service control URL and full service name try: control_url = host_info['proto'] + host_info['name'] control_url2 = host_info['deviceList'][device_name]['services'][service_name]['controlURL'] if not control_url.endswith('/') and not control_url2.startswith('/'): control_url += '/' control_url += control_url2 except Exception as e: print('Caught exception:') traceback.print_tb(e) print("Are you sure you've run 'host get %d' and specified the correct service name?" % index) return False # Get action info try: action_args = \ host_info['deviceList'][device_name]['services'][service_name]['actions'][action_name][ 'arguments'] full_service_name = host_info['deviceList'][device_name]['services'][service_name]['fullName'] except Exception as e: print('Caught exception:') traceback.print_tb(e) print("Are you sure you've specified the correct action?") return False for argName, argVals in action_args.items(): action_state_var = argVals['relatedStateVariable'] state_var = host_info['deviceList'][device_name]['services'][service_name]['serviceStateVariables'][ action_state_var] if argVals['direction'].lower() == 'in': print_info("Required argument:") print("\tArgument Name: ", argName) print("\tData Type: ", state_var['dataType']) if 'allowedValueList' in state_var: print("\tAllowed Values:", state_var['allowedValueList']) if 'allowedValueRange' in state_var: print("\tValue Min: ", state_var['allowedValueRange'][0]) print("\tValue Max: ", state_var['allowedValueRange'][1]) if 'defaultValue' in state_var: print("\tDefault Value: ", state_var['defaultValue']) prompt = "\tSet %s value to: " % argName try: # Get user input for the argument value (argc, argv) = self.getUserInput(prompt) if argv is None: print_warning('Stopping send request...') return u_input = '' if argc > 0: in_arg_counter += 1 for val in argv: u_input += val + ' ' u_input = u_input.strip() if state_var['dataType'] == 'bin.base64' and u_input: u_input = base64.encodebytes(bytes(u_input, 'UTF-8')) send_args[argName] = (u_input.strip(), state_var['dataType']) except KeyboardInterrupt: print("") return print('') else: ret_tags.append((argName, state_var['dataType'])) # Remove the above inputs from the command history # while inArgCounter: # try: # readline.remove_history_item(readline.get_current_history_length() - 1) # except: # pass # # inArgCounter -= 1 # print 'Requesting',controlURL soap_response = self.send_soap(host_info['name'], full_service_name, control_url, action_name, send_args) if soap_response: # It's easier to just parse this ourselves... for (tag, dataType) in ret_tags: tag_value = self.extract_single_tag(soap_response, tag) if dataType == 'bin.base64' and tag_value is not None: # print(tagValue) tag_value = base64.decodebytes(bytes(tag_value, 'UTF-8')) print(tag, ':', tag_value) return # Creates dictionary structure from host_enum data if data enumeration was completed def parse_device_autocomplete(self, index): autocomplete_structure = {} host = self.enum_hosts[index] if host['dataComplete']: try: for device, deviceData in host['deviceList'].items(): autocomplete_structure[device] = {} for service, serviceData in deviceData['services'].items(): autocomplete_structure[device][service] = {} for action, actionData in serviceData['actions'].items(): autocomplete_structure[device][service][action] = [] except KeyError: print_error("Error in autocomplete") return autocomplete_structure def complete_device(self, text, line, begidx, endidx): number_of_hosts = range(len(self.enum_hosts)) complete_dict = {'get': number_of_hosts, 'info': number_of_hosts, 'summarny': number_of_hosts, 'list': [], 'details': number_of_hosts, 'send': number_of_hosts} # Trick for finding integers in string # Maybe I should also check if send command is actually present # but index of device is always last argument in command except send command index = [int(s) for s in line.split() if s.isdigit()] if index: complete_dict['send'] = {index[0]: self.parse_device_autocomplete(index[0])} complete_array = interface.utils.dict_to_str(complete_dict) complete_line = line.partition(' ')[2] igon = len(complete_line) - len(text) return [s[igon:] for s in complete_array if s.startswith(complete_line)] Upnp()