upnpigd.py 3.21 KB
Newer Older
1
from functools import wraps
Guillaume Bury's avatar
Guillaume Bury committed
2
import miniupnpc
3
import logging
4
import time
Guillaume Bury's avatar
Guillaume Bury committed
5

6

7 8 9 10 11 12 13 14 15 16 17 18
class UPnPException(Exception):
    pass


class Forwarder(object):

    next_refresh = 0
    _next_retry = -1
    _next_port = 1024

    def __init__(self, description):
        self._description = description
19 20
        self._u = miniupnpc.UPnP()
        self._u.discoverdelay = 200
21 22
        self._rules = []

23 24 25
    def __getattr__(self, name):
        wrapped = getattr(self._u, name)
        def wrapper(*args, **kw):
Julien Muchembled's avatar
Julien Muchembled committed
26
            try:
27
                return wrapped(*args, **kw)
Julien Muchembled's avatar
Julien Muchembled committed
28
            except Exception, e:
29 30 31 32 33 34 35 36 37 38 39 40 41 42
                raise UPnPException(str(e))
        return wraps(wrapped)(wrapper)

    def checkExternalIp(self, ip=None):
        if not ip:
            ip = self.refresh()
            if not ip:
                return ()
        # If port is None, we assume we're not NATed.
        return [(ip, str(port or local), proto)
                for local, proto, port in self._rules]

    def addRule(self, local_port, proto):
        self._rules.append([local_port, proto, None])
Guillaume Bury's avatar
Guillaume Bury committed
43

44
    def refresh(self):
45 46 47 48 49
        if self._next_retry:
            if time.time() < self._next_retry:
                return
            self._next_retry = 0
        else:
50
            try:
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
                return self._refresh()
            except UPnPException, e:
                logging.debug("UPnP failure", exc_info=1)
                self.clear()
        try:
            self.discover()
            self.selectigd()
            return self._refresh()
        except UPnPException, e:
            self.next_refresh = self._next_retry = time.time() + 60
            logging.info(str(e))
            self.clear()

    def _refresh(self):
        force = self.next_refresh < time.time()
        if force:
            self.next_refresh = time.time() + 500
            logging.debug('Refreshing port forwarding')
        ip = self.externalipaddress()
        lanaddr = self._u.lanaddr
        for r in self._rules:
            local, proto, port = r
            if port and not force:
                continue
            desc = '%s (%u/%s)' % (self._description, local, proto)
            args = proto.upper(), lanaddr, local, desc, ''
            while True:
                if port is None:
                    port = self._next_port
                    if port > 65535:
                        raise UPnPException('No free port to redirect %s'
                                            % desc)
                    self._next_port = port + 1
                try:
                    self.addportmapping(port, *args)
                    break
                except UPnPException, e:
                    if str(e) != 'ConflictInMappingEntry':
                        raise
                    port = None
            if r[2] != port:
                logging.debug('%s forwarded from %s:%u', desc, ip, port)
                r[2] = port
        return ip
Julien Muchembled's avatar
Julien Muchembled committed
95 96

    def clear(self):
97 98 99 100 101 102 103 104 105 106 107 108
        try:
            del self._next_port
        except AttributeError:
            return
        for r in self._rules:
            port = r[2]
            if port:
                r[2] = None
                try:
                    self.deleteportmapping(port, r[1].upper())
                except UPnPException:
                    pass