comparison sat/plugins/plugin_misc_nat-port.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/plugins/plugin_misc_nat-port.py@78c7992a26ed
children 56f94936df1e
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT plugin for NAT port mapping
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from sat.core.i18n import _
21 from sat.core.constants import Const as C
22 from sat.core.log import getLogger
23 log = getLogger(__name__)
24 from sat.core import exceptions
25 from twisted.internet import threads
26 from twisted.internet import defer
27 from twisted.python import failure
28 import threading
29
30 try:
31 import miniupnpc
32 except ImportError:
33 raise exceptions.MissingModule(u"Missing module MiniUPnPc, please download/install it (and its Python binding) at http://miniupnp.free.fr/ (or use pip install miniupnpc)")
34
35
36 PLUGIN_INFO = {
37 C.PI_NAME: "NAT port mapping",
38 C.PI_IMPORT_NAME: "NAT-PORT",
39 C.PI_TYPE: C.PLUG_TYPE_MISC,
40 C.PI_MAIN: "NatPort",
41 C.PI_HANDLER: "no",
42 C.PI_DESCRIPTION: _("""Automatic NAT port mapping using UPnP"""),
43 }
44
45 STARTING_PORT = 6000 # starting point to automatically find a port
46 DEFAULT_DESC = u'SaT port mapping' # we don't use "à" here as some bugged NAT don't manage charset correctly
47
48
49 class MappingError(Exception):
50 pass
51
52
53 class NatPort(object):
54 # TODO: refresh data if a new connection is detected (see plugin_misc_ip)
55
56 def __init__(self, host):
57 log.info(_("plugin NAT Port initialization"))
58 self.host = host
59 self._external_ip = None
60 self._initialised = defer.Deferred()
61 self._upnp = miniupnpc.UPnP() # will be None if no device is available
62 self._upnp.discoverdelay=200
63 self._mutex = threading.Lock() # used to protect access to self._upnp
64 self._starting_port_cache = None # used to cache the first available port
65 self._to_unmap = [] # list of tuples (ext_port, protocol) of ports to unmap on unload
66 discover_d = threads.deferToThread(self._discover)
67 discover_d.chainDeferred(self._initialised)
68 self._initialised.addErrback(self._init_failed)
69
70 def unload(self):
71 if self._to_unmap:
72 log.info(u"Cleaning mapped ports")
73 return threads.deferToThread(self._unmapPortsBlocking)
74
75 def _init_failed(self, failure_):
76 e = failure_.trap(exceptions.NotFound, exceptions.FeatureNotFound)
77 if e == exceptions.FeatureNotFound:
78 log.info(u"UPnP-IGD seems to be not activated on the device")
79 else:
80 log.info(u"UPnP-IGD not available")
81 self._upnp = None
82
83 def _discover(self):
84 devices = self._upnp.discover()
85 if devices:
86 log.info(u"{nb} UPnP-IGD device(s) found".format(nb=devices))
87 else:
88 log.info(u"Can't find UPnP-IGD device on the local network")
89 raise failure.Failure(exceptions.NotFound())
90 self._upnp.selectigd()
91 try:
92 self._external_ip = self._upnp.externalipaddress()
93 except Exception:
94 raise failure.Failure(exceptions.FeatureNotFound())
95
96 def getIP(self, local=False):
97 """Return IP address found with UPnP-IGD
98
99 @param local(bool): True to get external IP address, False to get local network one
100 @return (None, str): found IP address, or None of something got wrong
101 """
102 def getIP(dummy):
103 if self._upnp is None:
104 return None
105 # lanaddr can be the empty string if not found,
106 # we need to return None in this case
107 return (self._upnp.lanaddr or None) if local else self._external_ip
108 return self._initialised.addCallback(getIP)
109
110 def _unmapPortsBlocking(self):
111 """Unmap ports mapped in this session"""
112 self._mutex.acquire()
113 try:
114 for port, protocol in self._to_unmap:
115 log.info(u"Unmapping port {}".format(port))
116 unmapping = self._upnp.deleteportmapping(
117 # the last parameter is remoteHost, we don't use it
118 port, protocol, '')
119
120 if not unmapping:
121 log.error(u"Can't unmap port {port} ({protocol})".format(
122 port=port, protocol=protocol))
123 del self._to_unmap[:]
124 finally:
125 self._mutex.release()
126
127 def _mapPortBlocking(self, int_port, ext_port, protocol, desc):
128 """Internal blocking method to map port
129
130 @param int_port(int): internal port to use
131 @param ext_port(int): external port to use, or None to find one automatically
132 @param protocol(str): 'TCP' or 'UDP'
133 @param desc(str): description of the mapping
134 @param return(int, None): external port used in case of success, otherwise None
135 """
136 # we use mutex to avoid race condition if 2 threads
137 # try to acquire a port at the same time
138 self._mutex.acquire()
139 try:
140 if ext_port is None:
141 # find a free port
142 starting_port = self._starting_port_cache
143 ext_port = STARTING_PORT if starting_port is None else starting_port
144 ret = self._upnp.getspecificportmapping(ext_port, protocol)
145 while ret != None and ext_port < 65536:
146 ext_port += 1
147 ret = self._upnp.getspecificportmapping(ext_port, protocol)
148 if starting_port is None:
149 # XXX: we cache the first successfuly found external port
150 # to avoid testing again the first series the next time
151 self._starting_port_cache = ext_port
152
153 try:
154 mapping = self._upnp.addportmapping(
155 # the last parameter is remoteHost, we don't use it
156 ext_port, protocol, self._upnp.lanaddr, int_port, desc, '')
157 except Exception as e:
158 log.error(_(u"addportmapping error: {msg}").format(msg=e))
159 raise failure.Failure(MappingError())
160
161 if not mapping:
162 raise failure.Failure(MappingError())
163 else:
164 self._to_unmap.append((ext_port, protocol))
165 finally:
166 self._mutex.release()
167
168 return ext_port
169
170 def mapPort(self, int_port, ext_port=None, protocol='TCP', desc=DEFAULT_DESC):
171 """Add a port mapping
172
173 @param int_port(int): internal port to use
174 @param ext_port(int,None): external port to use, or None to find one automatically
175 @param protocol(str): 'TCP' or 'UDP'
176 @param desc(unicode): description of the mapping
177 Some UPnP IGD devices have broken encoding. It's probably a good idea to avoid non-ascii chars here
178 @return (D(int, None)): external port used in case of success, otherwise None
179 """
180 if self._upnp is None:
181 return defer.succeed(None)
182 def mappingCb(ext_port):
183 log.info(u"{protocol} mapping from {int_port} to {ext_port} successful".format(
184 protocol = protocol,
185 int_port = int_port,
186 ext_port = ext_port,
187 ))
188 return ext_port
189 def mappingEb(failure_):
190 failure_.trap(MappingError)
191 log.warning(u"Can't map internal {int_port}".format(int_port=int_port))
192 def mappingUnknownEb(failure_):
193 log.error(_(u"error while trying to map ports: {msg}").format(msg=failure_))
194 d = threads.deferToThread(self._mapPortBlocking, int_port, ext_port, protocol, desc)
195 d.addCallbacks(mappingCb, mappingEb)
196 d.addErrback(mappingUnknownEb)
197 return d