示例#1
0
    def __init__(self):
        self.service = None
        self.external_ip = None
        self.internal_ip = '127.0.0.1'
        self.portlist = []

        upnp = upnpy.UPnP()
        upnp.discover()
        device = upnp.get_igd()
        for service in device.get_services():
            if 'WANIPConn1' in service.id:
                self.service = device['WANIPConn1']
                self.external_ip = self.service.GetExternalIPAddress().get(
                    'NewExternalIPAddress')

        if self.service is None:
            print("Router doesn't support UPNP, or UPNP is not enabled")
            print("Error: Cannot port forward")

        if self.external_ip is not None:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect((self.external_ip, 80))
            self.internal_ip = s.getsockname()[0]
            s.close()
        #make sure we close these ports when CTRL+C closed
        atexit.register(self.__del__)
async def forward_port(port: int):
    import upnpy
    import socket

    upnp = upnpy.UPnP()
    upnp.discover()
    device = upnp.get_igd()

    service = device['WANPPPConnection.1']

    # get own lan IP
    ip = socket.gethostbyname(socket.gethostname())

    # This specific action returns an empty dict: {}
    service.AddPortMapping(
        NewRemoteHost='',
        NewExternalPort=port,
        NewProtocol='TCP',
        NewInternalPort=port,
        NewInternalClient=ip,
        NewEnabled=1,
        NewPortMappingDescription='Berserker\'s Multiworld',
        NewLeaseDuration=60 * 60 * 24  # 24 hours
    )

    logging.info(
        f"Attempted to forward port {port} to {ip}, your local ip address.")
示例#3
0
def portMapping():
    upnp = upnpy.UPnP()
    devs = upnp.discover()
    log.debug(f"UPnP devices: {devs}")
    dev = upnp.get_igd()
    dev.get_services()
    service = dev['WANPPPConnection.1']
    service.get_actions()
    service.AddPortMapping.get_input_arguments()
    service.AddPortMapping(
        NewRemoteHost='',
        NewExternalPort=1111,
        NewProtocol='TCP',
        NewInternalPort=1111,
        NewInternalClient='127.0.0.1',
        NewEnabled=1,
        NewPortMappingDescription='Test port mapping entry from UPnPy.',
        NewLeaseDuration=0)
示例#4
0
async def scan_for_devices(timeout=2):
    global found_heos_devices

    upnp = upnpy.UPnP()
    devices = upnp.discover(delay=timeout)
    found_ips = list()
    for device in devices:
        if b"urn:schemas-denon-com:device:AiosServices:1" in device.description:
            if not any(heos_dev['name'] == device.friendly_name
                       for heos_dev in found_heos_devices):
                append_device = {
                    'name': device.friendly_name,
                    'host': device.host,
                    'port': device.port,
                    'type': device.type_,
                    'base_url': device.base_url,
                    'services': []
                }
                found_ips.append(device.host)
                for service in device.get_services():
                    append_device['services'].append({
                        'service':
                        service.service,
                        'type':
                        service.type_,
                        'version':
                        service.version,
                        'base_url':
                        service.base_url,
                        'control_url':
                        service.control_url
                    })
                found_heos_devices.append(append_device)
    global heos_manager
    if heos_manager:
        await heos_manager.initialize(found_ips)
        await heos_manager.start_watch_events()
示例#5
0
import upnpy
import sys
from defusedxml.ElementTree import fromstring
import requests

upnp = upnpy.UPnP()
devices = upnp.discover()

# search devices looking for TP-Link with DLNA
the_device = None
found = False
for dev in devices:
    desc = fromstring(dev.description)
    if desc:
        for child in desc:
            if child.tag.endswith('device'):
                for i in child:
                    if i.tag.endswith('modelDescription') and 'DLNA' in i.text:
                        print('Found the device!')
                        the_device = dev
                        found = True
                        break
            if found:
                break
    if found:
        break

if the_device is None:
    print("Couldn't find the TP-Link device.")
    sys.exit(1)
示例#6
0
def open_ports():
    '''
    Enables port forwarding to our local machine through a router that supports UPnP protocol.
    The ports in CLIENT_PORT_RANGE are tunneled to local ports through the router if it supports it.
    '''

    if not ENABLE_UPNP:
        logger.info(
            'ENABLE_UPNP=False in globals.py so not attempting to forward ports in CLIENT_PORT_RANGE'
        )
        return
    import upnpy
    logger.info(
        'Attempting to open necessary UDP ports with upnpy version {} (https://github.com/5kyc0d3r/upnpy, https://upnpy.readthedocs.io/en/latest/)'
        .format(upnpy.__version__))
    logger.setLevel(logging.DEBUG)
    upnp = upnpy.UPnP()

    # Discover UPnP devices on the network
    # Returns a list of devices e.g.: [Device <Broadcom ADSL Router>]
    devices = None
    tries = 5
    for t in range(tries):
        try:
            devices = upnp.discover(delay=2)
            logger.debug('found IGD devices list {}'.format(devices))
            break
        except Exception as e:
            logger.warning(
                '(Try {} of {}): exception "{}" trying to discover IGD'.format(
                    t, tries, e))
            sleep(.5)

    # Select the IGD
    # alternatively you can select the device directly from the list
    # device = devices[0]
    device = upnp.get_igd()
    logger.debug('selected device {}'.format(device))

    # Get the services available for this device
    # Returns a list of services available for the device
    # e.g.: [<Service (WANPPPConnection) id="WANPPPConnection.1">, ...]
    services = device.get_services()
    logger.debug('found services {}'.format(services))

    # We can now access a specific service on the device by its ID
    # The IDs for the services in this case contain illegal values so we can't access it by an attribute
    # If the service ID didn't contain illegal values we could access it via an attribute like this:
    # service = device.WANPPPConnection

    service = None
    for s in services:
        if s.type_ == 'WANIPConnection':
            service = s
            logger.debug('found WANPPPConnection service {}'.format(service))
            break

    if service is None:
        raise RuntimeError(
            'Could not find service WANIPConnecton in UPnP router device {}'.
            format(device))

    # Get the actions available for the service
    # Returns a list of actions for the service:
    #   [<Action name="SetConnectionType">,
    #   <Action name="GetConnectionTypeInfo">,
    #   <Action name="RequestConnection">,
    #   <Action name="ForceTermination">,
    #   <Action name="GetStatusInfo">,
    #   <Action name="GetNATRSIPStatus">,
    #   <Action name="GetGenericPortMappingEntry">,
    #   <Action name="GetSpecificPortMappingEntry">,
    #   <Action name="AddPortMapping">,
    #   <Action name="DeletePortMapping">,
    #   <Action name="GetExternalIPAddress">]
    actions = service.get_actions()
    logger.debug('found actions {}'.format(actions))

    # The action we are looking for is the "AddPortMapping" action
    # Lets see what arguments the action accepts
    # Use the get_input_arguments() or get_output_arguments() method of the action
    # for a list of input / output arguments.
    # Example output of the get_input_arguments method for the "AddPortMapping" action
    # Returns a dictionary:
    # [
    #     {
    #         "name": "NewRemoteHost",
    #         "data_type": "string",
    #         "allowed_value_list": []
    #     },
    #     {
    #         "name": "NewExternalPort",
    #         "data_type": "ui2",
    #         "allowed_value_list": []
    #     },
    #     {
    #         "name": "NewProtocol",
    #         "data_type": "string",
    #         "allowed_value_list": [
    #             "TCP",
    #             "UDP"
    #         ]
    #     },
    #     {
    #         "name": "NewInternalPort",
    #         "data_type": "ui2",
    #         "allowed_value_list": []
    #     },
    #     {
    #         "name": "NewInternalClient",
    #         "data_type": "string",
    #         "allowed_value_list": []
    #     },
    #     {
    #         "name": "NewEnabled",
    #         "data_type": "boolean",
    #         "allowed_value_list": []
    #     },
    #     {
    #         "name": "NewPortMappingDescription",
    #         "data_type": "string",
    #         "allowed_value_list": []
    #     },
    #     {
    #         "name": "NewLeaseDuration",
    #         "data_type": "ui4",
    #         "allowed_value_list": []
    #     }
    # ]
    # service.AddPortMapping.get_input_arguments()
    logger.debug('adding port mappings for CLIENT_PORT_RANGE {}'.format(
        CLIENT_PORT_RANGE))

    my_ip = get_local_ip_address()
    logger.debug('Determined our own IP address is {}'.format(my_ip))

    s = CLIENT_PORT_RANGE.split('-')
    if len(s) != 2:
        raise RuntimeError(
            'port range {} should be of form start-end, e.g. 50100-50200'.
            format(CLIENT_PORT_RANGE))
    start_port = int(s[0])
    end_port = int(s[1])
    for p in range(start_port, end_port):
        try:
            # Finally, add the new port mapping to the IGD
            # This specific action returns an empty dict: {}
            service.AddPortMapping(
                NewRemoteHost=[],
                NewExternalPort=p,
                NewProtocol='UDP',
                NewInternalPort=p,
                NewInternalClient=my_ip,
                NewEnabled=1,
                NewPortMappingDescription='l2race client mapping',
                NewLeaseDuration=UPNP_LEASE_TIME)
        except Exception as e:
            logger.warning('could not open port {}; caught "{}"'.format(p, e))
示例#7
0
def upnp_add(socktype, info, options):
    from xpra.log import Logger
    log = Logger("network", "upnp")
    log("upnp_add%s", (socktype, info, options))

    def err(*msgs):
        log("pnp_add%s", (info, options), exc_info=True)
        log.error("Error: cannot add UPnP port mapping")
        for msg in msgs:
            if msg:
                log.error(" %s", msg)
        return None

    #find the port number:
    try:
        internal_host, internal_port = info
    except (ValueError, TypeError):
        return err("cannot identify the host and port number from %s" %
                   (info, ))
    try:
        import upnpy
    except ImportError as e:
        return err(e)
    try:
        #prepare the port mapping attributes early:
        #(in case this causes errors)
        remote_host = options.get("upnp-remote-host", "")
        external_port = int(options.get("upnp-external-port", internal_port))
        protocol = "UDP" if socktype == "udp" else "TCP"
        duration = int(options.get("upnp-duration", 600))

        upnp = upnpy.UPnP()
        log("upnp=%s", upnp)
        #find the device to use:
        try:
            devices = upnp.discover()
        except Exception as e:
            log("discover()", exc_info=True)
            return err("error discovering devices", e)
        d = options.get("upnp-device", "igd")
        if d == "igd":
            try:
                device = upnp.get_igd()
                log("using IGD device %s", device)
            except Exception as e:
                dstr = ()
                if devices:
                    dstr = (
                        "%i devices:" % len(devices),
                        ": %s" % devices,
                    )
                return err(e, *dstr)
        else:
            try:
                #the device could be given as an index:
                no = int(d)
                try:
                    device = devices[no]
                except IndexError:
                    return err("no device number %i" % no,
                               "%i devices found" % len(devices))
                log("using device %i: %s", no, device)
            except ValueError:
                #try using the value as a device name:
                device = getattr(upnp, d, None)
                if device is None:
                    return err("device name '%s' not found" % d)
                log("using device %s", device)
        log("device: %s", device.get_friendly_name())
        log("device address: %s", device.address)
        if internal_host in ("0.0.0.0", "::/0", "::"):
            #we need to figure out the specific IP
            #which is connected to this device
            import netifaces
            gateways = netifaces.gateways()
            if not gateways:
                return err("internal host IP not found: no gateways")
            UPNP_IPV6 = False
            INET = {
                "INET": netifaces.AF_INET,
            }
            if UPNP_IPV6:
                INET["INET6"] = netifaces.AF_INET6

            def get_device_interface():
                default_gw = gateways.get(
                    "default")  #ie: {2: ('192.168.3.1', 'eth0')}
                if default_gw:
                    for v in INET.values():  #ie: AF_INET
                        inet = default_gw.get(v)  #ie: ('192.168.3.1', 'eth0')
                        if inet and len(inet) >= 2:
                            return inet[1]
                for v in INET.values():
                    #ie: gws = [('192.168.3.1', 'eth0', True), ('192.168.0.1', 'wlan0', False)]}
                    gws = gateways.get(v)
                    if not gws:
                        continue
                    for inet in gws:
                        if inet and len(inet) >= 2:
                            return inet[1]

            interface = get_device_interface()
            if not interface:
                return err("cannot identify the network interface for '%s'" %
                           (device.address, ))
            log("identified interface '%s' for device address %s", interface,
                device.address)
            addrs = netifaces.ifaddresses(interface)
            log("ifaddresses(%s)=%s", interface, addrs)

            #ie: {17: [{'addr': '30:52:cb:85:54:03', 'broadcast': 'ff:ff:ff:ff:ff:ff'}],
            #      2: [{'addr': '192.168.0.111', 'netmask': '255.255.255.0', 'broadcast': '192.168.0.255'}],
            #     10: [{'addr': 'fe80::1944:64a7:ab7b:9d67%wlan0', 'netmask': 'ffff:ffff:ffff:ffff::/64'}]}
            def get_interface_address():
                for name, v in INET.items():
                    #ie: inet=[{'addr': '192.168.0.111', 'netmask': '255.255.255.0', 'broadcast': '192.168.0.255'}]
                    inet = addrs.get(v)
                    log("addresses[%s]=%s", name, inet)
                    if not inet:
                        continue
                    for a in inet:
                        #ie: host = {'addr': '192.168.0.111', 'netmask': '255.255.255.0', 'broadcast': '192.168.0.255'}
                        host = a.get("addr")
                        if host:
                            return host
                return None

            internal_host = get_interface_address()
            if not internal_host:
                return err("no address found for interface '%s'", interface)

        #find the service:
        services = device.get_services()
        if not services:
            return err("device %s does not have any services" % device)
        log("services=%s", csv(services))
        s = options.get("upnp-service", "")
        if s:
            try:
                #the service could be given as an index:
                no = int(s)
                try:
                    service = services[no]
                except IndexError:
                    return err(
                        "no service number %i on device %s" % (no, device),
                        "%i services found" % len(services))
                log("using service %i: %s", no, service)
            except ValueError:
                #find the service by id
                matches = [v for v in services if v.id.split(":")[-1] == s]
                if len(matches) > 1:
                    return err("more than one service matches '%s'" % (s, ))
                if len(matches) != 1:
                    return err("service '%s' not found on %s" % (s, device))
                service = matches[0]
                log("using service %s", service)
        else:
            #find the service with a "AddPortMapping" action:
            service = None
            for v in services:
                if get_action(v, "AddPortMapping"):
                    service = v
                    break
            if not service:
                return err(
                    "device %s does not have a service with a port mapping action"
                    % device)
        add = get_action(service, "AddPortMapping")
        delete = get_action(service, "DeletePortMapping")
        if not add:
            return err("service %s does not support 'AddPortMapping'")
        if not delete:
            return err("service %s does not support 'DeletePortMapping'")
        kwargs = {
            "NewRemoteHost": remote_host,
            "NewExternalPort": external_port,
            "NewProtocol": protocol,
            "NewInternalPort": internal_port,
            "NewInternalClient": internal_host,
            "NewEnabled": True,
            "NewPortMappingDescription": "Xpra-%s" % socktype,
            "NewLeaseDuration": duration,
        }
        log("%s%s", add, kwargs)
        add(**kwargs)
        #UPNP_INFO = ("GetConnectionTypeInfo", "GetStatusInfo", "GetNATRSIPStatus")
        UPNP_INFO = ("GetConnectionTypeInfo", "GetStatusInfo")
        for action_name in UPNP_INFO:
            action = get_action(service, action_name)
            if action:
                try:
                    r = action()
                    log("%s=%s", action_name, r)
                except Exception:
                    log("%s", action, exc_info=True)
        getip = get_action(service, "GetExternalIPAddress")
        if getip:
            try:
                reply = getip()
                ip = (reply or {}).get("NewExternalIPAddress")
                if ip:
                    log.info("UPnP port mapping added for %s:%s", ip,
                             external_port)
                    options["upnp-address"] = (ip, external_port)
            except Exception as e:
                log("%s", getip, exc_info=True)

        def cleanup():
            try:
                kwargs = {
                    "NewRemoteHost": remote_host,
                    "NewExternalPort": external_port,
                    "NewProtocol": protocol,
                }
                log("%s%s", delete, kwargs)
                delete(**kwargs)
                log.info("UPnP port mapping removed for %s:%s", ip,
                         external_port)
            except Exception as e:
                log("%s", delete, exc_info=True)
                log.error("Error removing port UPnP port mapping")
                log.error(" for external port %i,", external_port)
                log.error(" internal port %i (%s):", internal_port, socktype)
                log.error(" %s", e)

        return cleanup
    except Exception as e:
        return err(e)
示例#8
0
def main():
    # Greet the user and disable debug by default
    print('NATCracker version', __version__)
    debug = False

    # DEFAULT VARIABLE INITIALIZATION
    portmapService = 'WANIPConn1'                   # UPnP service to be used to interact with portmaps
    portmapAddAction = 'AddPortMapping'             # UPnP action to be used to add a portmap
    portmapGetAction = 'GetGenericPortMappingEntry' # UPnP action to be used to list portmaps
    portmapRemAction = 'DeletePortMapping'          # UPnP action to be used to remove a portmap

    localPort = 0                                   # Local port for the port forwarding
    remotePort = 0                                  # Remote port for the port forwarding
    internalIP = ''                                 # Internal IP to forwarad traffic to
    host = ''                                       # Remote host that is allowed to use the forwarding
    protocol = 'BOTH'                               # Protocol to forward (TCP, UDP or BOTH)
    duration = 86400                                # Portmap duration in seconds. Defaults to one day
    name = 'NATCracker'                             # Name for the mapping

    # PARSING THE COMMAND LINE ARGUMENTS
    # Check the --help to see what switch does what
    try:
        options, commands = getopt.getopt(sys.argv[1:],'l:r:i:h:p:d:n:',['service=','add-action=','remove-action=','get-action=','local=','remote=','ip=','host=','protocol=','duration=','name=','help','debug'])
    except getopt.GetoptError:
        printUsageAndExit()

    # Parse the switches and assign them
    for opt, arg in options:
        if opt == '--service':
            portmapService = arg
        elif opt == '--add-action':
            portmapAddAction = arg
        elif opt == '--remove-action':
            portmapRemAction = arg
        elif opt == '--get-action':
            portmapGetAction = arg
        elif opt == '--help':
            printUsageAndExit()
        elif opt == '--debug':
            debug = True
        elif opt in ('-l', '--local'):
            localPort = arg
        elif opt in ('-r', '--remote'):
            remotePort = arg
        elif opt in ('-i', '--ip'):
            internalIP = arg
        elif opt in ('-h', '--host'):
            host = arg
        elif opt in ('-p', '--protocol'):
            # Check wether the protocol is valid
            if arg.upper() in ('TCP', 'UDP', 'BOTH'):
                protocol = arg.upper()
            else:
                print('\nERROR: Invalid protocol', arg.upper())
                exit(1)
        elif opt in ('-d', '--duration'):
            # Check wether the duration is a positive integer
            if isPositiveInteger(arg):
                duration = int(arg)
            else:
                print('\nERROR: Invalid value for duration: must be a positive integer.')
                exit(1)
        elif opt in ('-n', '--name'):
            name = arg

    # Debug print of all parameters
    if debug:
        print('\nDEBUG - VARIABLES STATE:')
        print('\tService:', portmapService)
        print('\tAdd Action:', portmapAddAction)
        print('\tRemove Action:', portmapRemAction)
        print('\tGet Action:', portmapGetAction)
        print('\tLocal Port:', localPort)
        print('\tRemote Port:', remotePort)
        print('\tInternal IP:', internalIP)
        print('\tRemote Host:', host if host != '' else '0.0.0.0')
        print('\tProtocol:', protocol)
        print('\tDuration:', duration)
        print('\tName:', name)

    # Initialize dictionary with None
    commandList = {'VERB':None,'NOUN':None,'ADDR':None,'SERV':None,'ACTN':None}

    # Parse commands. If we exceed the bundary ignore all further assignements and proceed
    try:
        commandList['VERB'] = commands[0].upper()
        commandList['NOUN'] = commands[1].upper()
        commandList['ADDR'] = commands[2]
        commandList['SERV'] = commands[3]
        commandList['ACTN'] = commands[4]
    except IndexError:
        pass

    # If we don't have a verb quit with an error
    if commandList['VERB'] == None:
        print('\nERROR: Missing verb (either list, add or remove)')
        exit(1)

    # INITIALIZE THE UPnP CLIENT
    # Construct the client and scan for UPnP devices on the network
    client = upnpy.UPnP()
    devices = client.discover()

    # Initialize gateway variable
    gateway = None

    # Find an IGD - but only if we need it
    if commandList['VERB'] in ('ADD', 'REMOVE') or (commandList['VERB'] == 'LIST' and (commandList['NOUN'] == 'MAPPINGS' or commandList['NOUN'] is None)):
        igds = []
        addr = []

        for device in devices:
            if isIGD(device):
                igds.append(device)
                addr.append(device.host)

        if len(igds) == 0:
            print('\nERROR: No IGD (Internet Gateway Device) found on this network')
            print('Your device might be offline or have UPnP disabled')
            print('Please also check that your firewall rules are not blocking UPnP')
            print('Cannot proceed further. Aborting...')
            exit(1)
        elif len(igds) > 1:
            print('\nWARNING: There are multiple IGDs (Internet Gateway Device) on this network')
            print('You must manually choose your gateway (or device you intend to operate) from the following list')
            print('If your device does not appear on this list it might be offline or have UPnP disabled')
            print('Please also check that your firewall rules are not blocking UPnP')

            for igd in igds:
                print('    ', igd.host, '\t(', igd.friendly_name, ')', sep='')

            while True:
                try:
                    choice = input('Please type the IP address of the device you intend to interact with: ')
                except EOFError:
                    print('\n\nAborting...')
                    exit(0)

                if not isValidIP(choice) or choice not in addr:
                    print('\nERROR: Your input is not a valid address from the above list. Try again or press CTRL-D to abort')
                    continue

                gateway = getDeviceByIP(choice, devices)
                break
        else:
            gateway = igds[0]

    # HANDLE LIST REQUEST
    if commandList['VERB'] == 'LIST':
        if commandList['NOUN'] is None or commandList['NOUN'] == 'MAPPINGS':
            try:
                service = getServiceByName(gateway, portmapService)
                action = getActionByName(service, portmapGetAction)
            except upnpy.exceptions.ServiceNotFoundError:
                printServiceNotFoundAndExit(gateway.host, portmapService)
            except upnpy.exceptions.ActionNotFoundError:
                printActionNotFoundAndExit(gateway.host, portmapService, portmapGetAction)

            # Retrieve the entries one at a time using an incremental index
            index = 0

            while True:
                try:
                    # Optionally take an IP address as argument - only show mappings towards that IP
                    Mapping = action(NewPortMappingIndex=index)
                    if not isValidIP(commandList['ADDR']) or commandList['ADDR'] == Mapping['NewInternalClient']:
                        print('[', Mapping['NewRemoteHost'] if Mapping['NewRemoteHost'] != '' else '0.0.0.0', ':', Mapping['NewExternalPort'], '] -> [', Mapping['NewInternalClient'], ':', Mapping['NewInternalPort'], '] protocol ', Mapping['NewProtocol'], ' for ', Mapping['NewLeaseDuration'], ' seconds (', Mapping['NewPortMappingDescription'], ')', sep='')

                    index += 1
                except upnpy.exceptions.SOAPError as exception:
                    # Error 713 is Index Out Of Bounds - we have reached the end of the list
                    if exception.error == 713:
                        if index == 0:
                            print('\nNo mapping found on the IGD.')
                        break
                    elif exception.error == 501:
                        print('\nERROR: An error occurred on the remote device while it attempted to execute the requested action.')
                        exit(1)
                    else:
                        print('\nERROR: An unspecified error occurred while retrivering mappings from the IGD:', exception.description)
                        exit(1)

        elif commandList['NOUN'] in ('DEVICES','IGDS'):
            if commandList['NOUN'] == 'IGDS':
                targets = []

                for device in devices:
                    if isIGD(device):
                        targets.append(device)

            else:
                targets = devices

            if len(targets) == 0:
                print('\nThere are no UPnP', 'devices' if commandList['NOUN'] == 'DEVICES' else 'IGDs', 'on this network')
                exit(0)

            print('\nListing ', len(targets), ' UPnP ', 'device' if commandList['NOUN'] == 'DEVICES' else 'IGD', 's' if len(targets) > 1 else '', ' on the network:', sep='')
            if commandList['NOUN'] == 'DEVICES':
                print('NOTE: Entries preceeded by a star are IGDs')

            for device in targets:
                    print('  * ' if isIGD(device) and commandList['NOUN'] == 'DEVICES' else '    ', '[', device.host, ']\t', device.friendly_name, ' (', getType(device), ')', sep='')

        elif commandList['NOUN'] == 'SERVICES':
            if not isValidIP(commandList['ADDR']):
                print('\nERROR: Invalid usage: you must specify a valid IP address')
                exit(1)

            device = getDeviceByIP(commandList['ADDR'], devices)

            if device is None:
                print('\nERROR: No UPnP device found at address', commandList['ADDR'])
                exit(1)

            services = device.get_services()

            # No need to check wether len(services) == 0 as it is assumed that an UPnP device will expose at least one
            print('\nListing', len(services), 'services' if len(services) > 1 else 'service', 'on device', device.host)
            for s in services:
                serviceName, serviceVersion = s.service.split(':')[-2:]
                print('    ', serviceName, ' (Version ', serviceVersion, ', ID ', s.id.split(':')[-1], ')', sep='')

        elif commandList['NOUN'] == 'ACTIONS':
            if not (isValidIP(commandList['ADDR']) and commandList['SERV'] is not None):
                print('\nERROR: Invalid usage: you must specify a valid IP address and service ID in this order')
                exit(1)

            device = getDeviceByIP(commandList['ADDR'], devices)

            if device is None:
                print('\nERROR: No UPnP device found at address', commandList['ADDR'])
                exit(1)

            try:
                service = getServiceByName(device, commandList['SERV'])
            except upnpy.exceptions.ServiceNotFoundError:
                printServiceNotFoundAndExit(device.host, commandList['SERV'])

            actions = service.get_actions()
            print('\nListing', len(actions), 'actions' if len(actions) > 1 else 'action', 'in service', service.id.split(':')[-1], 'on device', device.host)
            for action in service.get_actions():
                print('   ', action.name)

        elif commandList['NOUN'] == 'PARAMETERS':
            if not (isValidIP(commandList['ADDR']) and commandList['SERV'] is not None and commandList['ACTN'] is not None):
                print('\nERROR: Invalid usage: you must specify a valid IP address, service ID and action name in this order')
                exit(1)

            device = getDeviceByIP(commandList['ADDR'], devices)

            if device is None:
                print('\nERROR: No UPnP device found at address', commandList['ADDR'])
                exit(1)

            try:
                service = getServiceByName(device, commandList['SERV'])
                action = getActionByName(service, commandList['ACTN'])
            except upnpy.exceptions.ServiceNotFoundError:
                printServiceNotFoundAndExit(device.host, commandList['SERV'])
            except upnpy.exceptions.ActionNotFoundError:
                printActionNotFoundAndExit(device.host, commandList['SERV'], commandList['ACTN'])

            if len(action.arguments) > 0:
                print('\nListing', len(action.arguments), 'parameters' if len(action.arguments) > 1 else 'parameter', 'for action', action.name, 'in service', service.id.split(':')[-1], 'on device', device.host)

                for argument in action.arguments:
                    print('   ', argument.name, '(Input)' if argument.direction == 'in' else '(Output)')
            else:
                print('\nThere are no parameters for action', action.name, 'in service', service.id.split(':')[-1], 'on device', device.host)

        else:
            print('\nERROR: Unknown action', commandList['NOUN'])
            exit(1)


    elif commandList['VERB'] == 'ADD':
        # Check that local and remote ports have been defined
        if not (isPositiveInteger(localPort, strictlyPositive=True) and isPositiveInteger(remotePort, strictlyPositive=True)):
            print('\nERROR: Invalid port definition')
            exit(1)

        # Then, validate the Host and internal IP
        if not ((isValidIP(host) or host == '') and isValidIP(internalIP)):
            print('\nERROR: Invalid host or internal IP')
            exit(1)

        try:
            service = getServiceByName(gateway, portmapService)
            action = getActionByName(service, portmapAddAction)
        except upnpy.exceptions.ServiceNotFoundError:
            printServiceNotFoundAndExit(gateway.host, portmapService)
        except upnpy.exceptions.ActionNotFoundError:
            printActionNotFoundAndExit(gateway.host, portmapService, portmapAddAction)

        # Warn the user if they try to add a permanent mapping
        if duration == 0:
            print('\nWARNING: You are about to add a permanent mapping')
            print('This mapping might not be discarded unless you manually remove it later')
            print('Adding a permament mapping and forgetting to later remove it may expose your network devices to malicious actors')

            try:
                response = input('Please confirm that you know what you are doing by typing uppercase yes: ')
                if response != 'YES':
                    print('\nAborting...')
                    exit(0)
            except EOFError:
                print('\n\nAborting...')
                exit(0)

        print('\nAdding mapping [', host if host != '' else '0.0.0.0', ':', remotePort, '] -> [', internalIP, ':', localPort, '] protocol ', protocol if protocol != 'BOTH' else 'TCP and UPD', ' for ', duration, ' seconds', sep='')

        try:
            if protocol == 'BOTH':
                action(NewRemoteHost=host,NewExternalPort=remotePort, NewProtocol='TCP', NewInternalPort=localPort, NewInternalClient=internalIP, NewEnabled=1, NewPortMappingDescription=name, NewLeaseDuration=duration)
                action(NewRemoteHost=host,NewExternalPort=remotePort, NewProtocol='UDP', NewInternalPort=localPort, NewInternalClient=internalIP, NewEnabled=1, NewPortMappingDescription=name, NewLeaseDuration=duration)
            else:
                action(NewRemoteHost=host,NewExternalPort=remotePort, NewProtocol=protocol, NewInternalPort=localPort, NewInternalClient=internalIP, NewEnabled=1, NewPortMappingDescription=name, NewLeaseDuration=duration)
        except upnpy.exceptions.SOAPError as exception:
            if exception.error == 501:
                print('\nERROR: An error occurred on the remote device while it attempted to execute the requested action.')
                exit(1)
            else:
                print('\nERROR: An unspecified error occurred while adding', 'mappings' if protocol == 'BOTH' else 'a mapping', 'for host [', host if host != '' else '0.0.0.0', ':', remotePort, '] protocol TCP: ', exception.description, sep='')
                exit(1)

    elif commandList['VERB'] == 'REMOVE':
        if host != '' and not isValidIP(host):
            print('\nERROR: Invalid remote IP', host)
            exit(1)

        if not isPositiveInteger(remotePort, strictlyPositive=True):
            print('\nERROR: Remote port must be a positive integer')
            exit(1)

        try:
            service = getServiceByName(gateway, portmapService)
            action = getActionByName(service, portmapRemAction)
        except upnpy.exceptions.ServiceNotFoundError:
            printServiceNotFoundAndExit(gateway.host, portmapService)
        except upnpy.exceptions.ActionNotFoundError:
            printActionNotFoundAndExit(gateway.host, portmapService, portmapRemAction)

        print('\nRemoving all mappings for host [', host if host != '' else '0.0.0.0', ':', remotePort, '] protocol ', protocol if protocol != 'BOTH' else 'TCP and UDP', sep='')

        if protocol == 'BOTH':
            while True:
                try:
                    action(NewRemoteHost=host, NewExternalPort=remotePort, NewProtocol='TCP')
                except upnpy.exceptions.SOAPError as exception:
                    if exception.error == 714:
                        break
                    elif exception.error == 501:
                        print('\nERROR: An error occurred on the remote device while it attempted to execute the requested action.')
                        exit(1)
                    else:
                        print('\nERROR: An unspecified error occurred while removing mappings for host [', host if host != '' else '0.0.0.0', ':', remotePort, '] protocol TCP: ', exception.description, sep='')
                        exit(1)
            while True:
                try:
                    action(NewRemoteHost=host, NewExternalPort=remotePort, NewProtocol='UDP')
                except upnpy.exceptions.SOAPError as exception:
                    if exception.error == 714:
                        break
                    elif exception.error == 501:
                        print('\nERROR: An error occurred on the remote device while it attempted to execute the requested action.')
                        exit(1)
                    else:
                        print('\nERROR: An unspecified error occurred while removing mappings for host [', host if host != '' else '0.0.0.0', ':', remotePort, '] protocol UDP: ', exception.description, sep='')
                        exit(1)
        else:
            while True:
                try:
                    action(NewRemoteHost=host, NewExternalPort=remotePort, NewProtocol=protocol)
                except upnpy.exceptions.SOAPError as exception:
                    if exception.error == 714:
                        break
                    elif exception.error == 501:
                        print('\nERROR: An error occurred on the remote device while it attempted to execute the requested action.')
                        exit(1)
                    else:
                        print('\nERROR: An unspecified error occurred while removing mappings for host [', host if host != '' else '0.0.0.0', ':', remotePort, '] protocol ', protocol, ': ', exception.description, sep='')
                        exit(1)

    else:
        print('\nERROR: Unknown action', commandList['VERB'])
        exit(1)