From a4dfc57cbe18d4e7e3fa5e58832bd85e7065e89e Mon Sep 17 00:00:00 2001 From: Tim Van Date: Sun, 7 Feb 2016 15:00:06 +0200 Subject: [PATCH 1/6] Fix some issues with possible mismatching serverIDs causing bad ssl connections. --- data/interfaces/default/settings.html | 4 +-- data/interfaces/default/welcome.html | 10 +++--- plexpy/config.py | 2 +- plexpy/plextv.py | 10 ++++++ plexpy/webserve.py | 46 ++++++++++++--------------- 5 files changed, 38 insertions(+), 34 deletions(-) diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html index af706cf1..3e10c49c 100644 --- a/data/interfaces/default/settings.html +++ b/data/interfaces/default/settings.html @@ -355,9 +355,9 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
-

Force PlexPy to connect to your Plex Server via SSL. Your server needs to have remote access enabled.

+

If you have secure connections enabled on your Plex Server, communicate with it securely.

diff --git a/data/interfaces/default/welcome.html b/data/interfaces/default/welcome.html index 5174ade1..8f2325a8 100644 --- a/data/interfaces/default/welcome.html +++ b/data/interfaces/default/welcome.html @@ -83,7 +83,7 @@ from plexpy import common
@@ -244,7 +244,7 @@ from plexpy import common }, render: { option: function (item, escape) { - return '
' + item.value + '
'; + return '
' + item.value + ' (' + item.label + ')
'; }, item: function (item, escape) { // first item is rendered before initialization bug? @@ -254,7 +254,7 @@ from plexpy import common .filter('[value="' + item.value + '"]').data()); } - return '
' + item.value + '
'; + return '
' + item.value + ' (' + item.label + ')
'; } }, onChange: function (item) { @@ -378,8 +378,8 @@ from plexpy import common var pms_ip = $("#pms_ip").val().trim(); var pms_port = $("#pms_port").val().trim(); var pms_identifier = $("#pms_identifier").val(); - var pms_ssl = $("#pms_ssl").val(); - var pms_is_remote = $("#pms_is_remote").val(); + var pms_ssl = $("#pms_ssl").is(':checked') ? 1 : 0; + var pms_is_remote = $("#pms_is_remote").is(':checked') ? 1 : 0; if ((pms_ip !== '') || (pms_port !== '')) { $("#pms-verify-status").html(' Validating server...'); $('#pms-verify-status').fadeIn('fast'); diff --git a/plexpy/config.py b/plexpy/config.py index 45eb1c79..984d57bc 100644 --- a/plexpy/config.py +++ b/plexpy/config.py @@ -25,7 +25,7 @@ _CONFIG_DEFINITIONS = { 'PMS_NAME': (unicode, 'PMS', ''), 'PMS_PORT': (int, 'PMS', 32400), 'PMS_TOKEN': (str, 'PMS', ''), - 'PMS_SSL': (int, 'General', 0), + 'PMS_SSL': (int, 'PMS', 0), 'PMS_URL': (str, 'PMS', ''), 'PMS_USE_BIF': (int, 'PMS', 0), 'PMS_UUID': (str, 'PMS', ''), diff --git a/plexpy/plextv.py b/plexpy/plextv.py index b3774342..df93548e 100644 --- a/plexpy/plextv.py +++ b/plexpy/plextv.py @@ -493,6 +493,16 @@ class PlexTV(object): connections = d.getElementsByTagName('Connection') for c in connections: + # If this is a remote server don't show any local IPs. + if helpers.get_xml_attr(d, 'publicAddressMatches') == '0' and \ + helpers.get_xml_attr(c, 'local') == '1': + continue + + # If this is a local server don't show any remote IPs. + if helpers.get_xml_attr(d, 'publicAddressMatches') == '1' and \ + helpers.get_xml_attr(c, 'local') == '0': + continue + server = {'httpsRequired': helpers.get_xml_attr(d, 'httpsRequired'), 'clientIdentifier': helpers.get_xml_attr(d, 'clientIdentifier'), 'label': helpers.get_xml_attr(d, 'name'), diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 831aaef0..a81bf203 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -1187,6 +1187,11 @@ class WebInterface(object): (kwargs['monitor_remote_access'] != plexpy.CONFIG.MONITOR_REMOTE_ACCESS): reschedule = True + # If we change the SSL setting for PMS, make sure we grab the new url. + if 'pms_ssl' in kwargs and \ + (kwargs['pms_ssl'] != plexpy.CONFIG.PMS_SSL): + server_changed = True + # Remove config with 'hscard-' prefix and change home_stats_cards to list if 'home_stats_cards' in kwargs: for k in kwargs.keys(): @@ -1397,33 +1402,22 @@ class WebInterface(object): def get_server_id(self, hostname=None, port=None, identifier=None, ssl=0, remote=0, **kwargs): from plexpy import http_handler - # Attempt to get the pms_identifier from plex.tv if the server is published - # Works for all PMS SSL settings + # Grab our serverId. + # This endpoint always works over plain HTTP even when secure connections on PMS is set to required. if not identifier and hostname and port: - plex_tv = plextv.PlexTV() - servers = plex_tv.discover() - - for server in servers: - if server['ip'] == hostname and server['port'] == port: - identifier = server['clientIdentifier'] - break - - # Fallback to checking /identity endpoint is server is unpublished - # Cannot set SSL settings on the PMS if unpublished so 'http' is okay - if not identifier: - request_handler = http_handler.HTTPHandler(host=hostname, - port=port, - token=None) - uri = '/identity' - request = request_handler.make_request(uri=uri, - proto='http', - request_type='GET', - output_format='xml', - no_token=True, - timeout=10) - if request: - xml_head = request.getElementsByTagName('MediaContainer')[0] - identifier = xml_head.getAttribute('machineIdentifier') + request_handler = http_handler.HTTPHandler(host=hostname, + port=port, + token=None) + uri = '/identity' + request = request_handler.make_request(uri=uri, + proto='http', + request_type='GET', + output_format='xml', + no_token=True, + timeout=10) + if request: + xml_head = request.getElementsByTagName('MediaContainer')[0] + identifier = xml_head.getAttribute('machineIdentifier') if identifier: cherrypy.response.headers['Content-type'] = 'application/json' From 98c1063e079a5068bdd48f785f058ad332abe626 Mon Sep 17 00:00:00 2001 From: Tim Van Date: Sun, 7 Feb 2016 22:21:46 +0200 Subject: [PATCH 2/6] Allows us to retrieve the serverId again if we have secure connections required. --- lib/IPy.py | 1652 ++++++++++++++++++++++++++++++++++++++++++++ plexpy/helpers.py | 28 +- plexpy/webserve.py | 44 +- 3 files changed, 1706 insertions(+), 18 deletions(-) create mode 100644 lib/IPy.py diff --git a/lib/IPy.py b/lib/IPy.py new file mode 100644 index 00000000..d1ea8457 --- /dev/null +++ b/lib/IPy.py @@ -0,0 +1,1652 @@ +""" +IPy - class and tools for handling of IPv4 and IPv6 addresses and networks. +See README file for learn how to use IPy. + +Further Information might be available at: +https://github.com/haypo/python-ipy +""" + +__version__ = '0.83' + +import bisect +import collections +import sys +import types + +# Definition of the Ranges for IPv4 IPs +# this should include www.iana.org/assignments/ipv4-address-space +# and www.iana.org/assignments/multicast-addresses +IPv4ranges = { + '0': 'PUBLIC', # fall back + '00000000': 'PRIVATE', # 0/8 + '00001010': 'PRIVATE', # 10/8 + '0110010001': 'CARRIER_GRADE_NAT', #100.64/10 + '01111111': 'PRIVATE', # 127.0/8 + '1': 'PUBLIC', # fall back + '1010100111111110': 'PRIVATE', # 169.254/16 + '101011000001': 'PRIVATE', # 172.16/12 + '1100000010101000': 'PRIVATE', # 192.168/16 + '111': 'RESERVED', # 224/3 + } + +# Definition of the Ranges for IPv6 IPs +# http://www.iana.org/assignments/ipv6-address-space/ +# http://www.iana.org/assignments/ipv6-unicast-address-assignments/ +# http://www.iana.org/assignments/ipv6-multicast-addresses/ +IPv6ranges = { + '00000000' : 'RESERVED', # ::/8 + '0' * 96 : 'RESERVED', # ::/96 Formerly IPV4COMP [RFC4291] + '0' * 128 : 'UNSPECIFIED', # ::/128 + '0' * 127 + '1' : 'LOOPBACK', # ::1/128 + '0' * 80 + '1' * 16 : 'IPV4MAP', # ::ffff:0:0/96 + '00000000011001001111111110011011' + '0' * 64 : 'WKP46TRANS', # 0064:ff9b::/96 Well-Known-Prefix [RFC6052] + '00000001' : 'UNASSIGNED', # 0100::/8 + '0000001' : 'RESERVED', # 0200::/7 Formerly NSAP [RFC4048] + '0000010' : 'RESERVED', # 0400::/7 Formerly IPX [RFC3513] + '0000011' : 'RESERVED', # 0600::/7 + '00001' : 'RESERVED', # 0800::/5 + '0001' : 'RESERVED', # 1000::/4 + '001' : 'GLOBAL-UNICAST', # 2000::/3 [RFC4291] + '00100000000000010000000' : 'SPECIALPURPOSE', # 2001::/23 [RFC4773] + '00100000000000010000000000000000' : 'TEREDO', # 2001::/32 [RFC4380] + '00100000000000010000000000000010' + '0' * 16 : 'BMWG', # 2001:0002::/48 Benchmarking [RFC5180] + '0010000000000001000000000001' : 'ORCHID', # 2001:0010::/28 (Temp until 2014-03-21) [RFC4843] + '00100000000000010000001' : 'ALLOCATED APNIC', # 2001:0200::/23 + '00100000000000010000010' : 'ALLOCATED ARIN', # 2001:0400::/23 + '00100000000000010000011' : 'ALLOCATED RIPE NCC', # 2001:0600::/23 + '00100000000000010000100' : 'ALLOCATED RIPE NCC', # 2001:0800::/23 + '00100000000000010000101' : 'ALLOCATED RIPE NCC', # 2001:0a00::/23 + '00100000000000010000110' : 'ALLOCATED APNIC', # 2001:0c00::/23 + '00100000000000010000110110111000' : 'DOCUMENTATION', # 2001:0db8::/32 [RFC3849] + '00100000000000010000111' : 'ALLOCATED APNIC', # 2001:0e00::/23 + '00100000000000010001001' : 'ALLOCATED LACNIC', # 2001:1200::/23 + '00100000000000010001010' : 'ALLOCATED RIPE NCC', # 2001:1400::/23 + '00100000000000010001011' : 'ALLOCATED RIPE NCC', # 2001:1600::/23 + '00100000000000010001100' : 'ALLOCATED ARIN', # 2001:1800::/23 + '00100000000000010001101' : 'ALLOCATED RIPE NCC', # 2001:1a00::/23 + '0010000000000001000111' : 'ALLOCATED RIPE NCC', # 2001:1c00::/22 + '00100000000000010010' : 'ALLOCATED RIPE NCC', # 2001:2000::/20 + '001000000000000100110' : 'ALLOCATED RIPE NCC', # 2001:3000::/21 + '0010000000000001001110' : 'ALLOCATED RIPE NCC', # 2001:3800::/22 + '0010000000000001001111' : 'RESERVED', # 2001:3c00::/22 Possible future allocation to RIPE NCC + '00100000000000010100000' : 'ALLOCATED RIPE NCC', # 2001:4000::/23 + '00100000000000010100001' : 'ALLOCATED AFRINIC', # 2001:4200::/23 + '00100000000000010100010' : 'ALLOCATED APNIC', # 2001:4400::/23 + '00100000000000010100011' : 'ALLOCATED RIPE NCC', # 2001:4600::/23 + '00100000000000010100100' : 'ALLOCATED ARIN', # 2001:4800::/23 + '00100000000000010100101' : 'ALLOCATED RIPE NCC', # 2001:4a00::/23 + '00100000000000010100110' : 'ALLOCATED RIPE NCC', # 2001:4c00::/23 + '00100000000000010101' : 'ALLOCATED RIPE NCC', # 2001:5000::/20 + '0010000000000001100' : 'ALLOCATED APNIC', # 2001:8000::/19 + '00100000000000011010' : 'ALLOCATED APNIC', # 2001:a000::/20 + '00100000000000011011' : 'ALLOCATED APNIC', # 2001:b000::/20 + '0010000000000010' : '6TO4', # 2002::/16 "6to4" [RFC3056] + '001000000000001100' : 'ALLOCATED RIPE NCC', # 2003::/18 + '001001000000' : 'ALLOCATED APNIC', # 2400::/12 + '001001100000' : 'ALLOCATED ARIN', # 2600::/12 + '00100110000100000000000' : 'ALLOCATED ARIN', # 2610::/23 + '00100110001000000000000' : 'ALLOCATED ARIN', # 2620::/23 + '001010000000' : 'ALLOCATED LACNIC', # 2800::/12 + '001010100000' : 'ALLOCATED RIPE NCC', # 2a00::/12 + '001011000000' : 'ALLOCATED AFRINIC', # 2c00::/12 + '00101101' : 'RESERVED', # 2d00::/8 + '0010111' : 'RESERVED', # 2e00::/7 + '0011' : 'RESERVED', # 3000::/4 + '010' : 'RESERVED', # 4000::/3 + '011' : 'RESERVED', # 6000::/3 + '100' : 'RESERVED', # 8000::/3 + '101' : 'RESERVED', # a000::/3 + '110' : 'RESERVED', # c000::/3 + '1110' : 'RESERVED', # e000::/4 + '11110' : 'RESERVED', # f000::/5 + '111110' : 'RESERVED', # f800::/6 + '1111110' : 'ULA', # fc00::/7 [RFC4193] + '111111100' : 'RESERVED', # fe00::/9 + '1111111010' : 'LINKLOCAL', # fe80::/10 + '1111111011' : 'RESERVED', # fec0::/10 Formerly SITELOCAL [RFC4291] + '11111111' : 'MULTICAST', # ff00::/8 + '1111111100000001' : 'NODE-LOCAL MULTICAST', # ff01::/16 + '1111111100000010' : 'LINK-LOCAL MULTICAST', # ff02::/16 + '1111111100000100' : 'ADMIN-LOCAL MULTICAST', # ff04::/16 + '1111111100000101' : 'SITE-LOCAL MULTICAST', # ff05::/16 + '1111111100001000' : 'ORG-LOCAL MULTICAST', # ff08::/16 + '1111111100001110' : 'GLOBAL MULTICAST', # ff0e::/16 + '1111111100001111' : 'RESERVED MULTICAST', # ff0f::/16 + '111111110011' : 'PREFIX-BASED MULTICAST', # ff30::/12 [RFC3306] + '111111110111' : 'RP-EMBEDDED MULTICAST', # ff70::/12 [RFC3956] + } + +MAX_IPV4_ADDRESS = 0xffffffff +MAX_IPV6_ADDRESS = 0xffffffffffffffffffffffffffffffff +IPV6_TEST_MAP = 0xffffffffffffffffffffffff00000000 +IPV6_MAP_MASK = 0x00000000000000000000ffff00000000 + +if sys.version_info >= (3,): + INT_TYPES = (int,) + STR_TYPES = (str,) + xrange = range +else: + INT_TYPES = (int, long) + STR_TYPES = (str, unicode) + + +class IPint(object): + """Handling of IP addresses returning integers. + + Use class IP instead because some features are not implemented for + IPint.""" + + def __init__(self, data, ipversion=0, make_net=0): + """Create an instance of an IP object. + + Data can be a network specification or a single IP. IP + addresses can be specified in all forms understood by + parseAddress(). The size of a network can be specified as + + /prefixlen a.b.c.0/24 2001:658:22a:cafe::/64 + -lastIP a.b.c.0-a.b.c.255 2001:658:22a:cafe::-2001:658:22a:cafe:ffff:ffff:ffff:ffff + /decimal netmask a.b.c.d/255.255.255.0 not supported for IPv6 + + If no size specification is given a size of 1 address (/32 for + IPv4 and /128 for IPv6) is assumed. + + If make_net is True, an IP address will be transformed into the network + address by applying the specified netmask. + + >>> print(IP('127.0.0.0/8')) + 127.0.0.0/8 + >>> print(IP('127.0.0.0/255.0.0.0')) + 127.0.0.0/8 + >>> print(IP('127.0.0.0-127.255.255.255')) + 127.0.0.0/8 + >>> print(IP('127.0.0.1/255.0.0.0', make_net=True)) + 127.0.0.0/8 + + See module documentation for more examples. + """ + + # Print no Prefixlen for /32 and /128 + self.NoPrefixForSingleIp = 1 + + # Do we want prefix printed by default? see _printPrefix() + self.WantPrefixLen = None + + netbits = 0 + prefixlen = -1 + + # handling of non string values in constructor + if isinstance(data, INT_TYPES): + self.ip = int(data) + if ipversion == 0: + if self.ip <= MAX_IPV4_ADDRESS: + ipversion = 4 + else: + ipversion = 6 + if ipversion == 4: + if self.ip > MAX_IPV4_ADDRESS: + raise ValueError("IPv4 Address can't be larger than %x: %x" % (MAX_IPV4_ADDRESS, self.ip)) + prefixlen = 32 + elif ipversion == 6: + if self.ip > MAX_IPV6_ADDRESS: + raise ValueError("IPv6 Address can't be larger than %x: %x" % (MAX_IPV6_ADDRESS, self.ip)) + prefixlen = 128 + else: + raise ValueError("only IPv4 and IPv6 supported") + self._ipversion = ipversion + self._prefixlen = prefixlen + # handle IP instance as an parameter + elif isinstance(data, IPint): + self._ipversion = data._ipversion + self._prefixlen = data._prefixlen + self.ip = data.ip + elif isinstance(data, STR_TYPES): + # TODO: refactor me! + # splitting of a string into IP and prefixlen et. al. + x = data.split('-') + if len(x) == 2: + # a.b.c.0-a.b.c.255 specification ? + (ip, last) = x + (self.ip, parsedVersion) = parseAddress(ip) + if parsedVersion != 4: + raise ValueError("first-last notation only allowed for IPv4") + (last, lastversion) = parseAddress(last) + if lastversion != 4: + raise ValueError("last address should be IPv4, too") + if last < self.ip: + raise ValueError("last address should be larger than first") + size = last - self.ip + netbits = _count1Bits(size) + # make sure the broadcast is the same as the last ip + # otherwise it will return /16 for something like: + # 192.168.0.0-192.168.191.255 + if IP('%s/%s' % (ip, 32-netbits)).broadcast().int() != last: + raise ValueError("the range %s is not on a network boundary." % data) + elif len(x) == 1: + x = data.split('/') + # if no prefix is given use defaults + if len(x) == 1: + ip = x[0] + prefixlen = -1 + elif len(x) > 2: + raise ValueError("only one '/' allowed in IP Address") + else: + (ip, prefixlen) = x + if prefixlen.find('.') != -1: + # check if the user might have used a netmask like + # a.b.c.d/255.255.255.0 + (netmask, vers) = parseAddress(prefixlen) + if vers != 4: + raise ValueError("netmask must be IPv4") + prefixlen = _netmaskToPrefixlen(netmask) + elif len(x) > 2: + raise ValueError("only one '-' allowed in IP Address") + else: + raise ValueError("can't parse") + + (self.ip, parsedVersion) = parseAddress(ip) + if ipversion == 0: + ipversion = parsedVersion + if prefixlen == -1: + bits = _ipVersionToLen(ipversion) + prefixlen = bits - netbits + self._ipversion = ipversion + self._prefixlen = int(prefixlen) + + if make_net: + self.ip = self.ip & _prefixlenToNetmask(self._prefixlen, self._ipversion) + + if not _checkNetaddrWorksWithPrefixlen(self.ip, + self._prefixlen, self._ipversion): + raise ValueError("%s has invalid prefix length (%s)" % (repr(self), self._prefixlen)) + else: + raise TypeError("Unsupported data type: %s" % type(data)) + + def int(self): + """Return the first / base / network addess as an (long) integer. + + The same as IP[0]. + + >>> "%X" % IP('10.0.0.0/8').int() + 'A000000' + """ + return self.ip + + def version(self): + """Return the IP version of this Object. + + >>> IP('10.0.0.0/8').version() + 4 + >>> IP('::1').version() + 6 + """ + return self._ipversion + + def prefixlen(self): + """Returns Network Prefixlen. + + >>> IP('10.0.0.0/8').prefixlen() + 8 + """ + return self._prefixlen + + def net(self): + """ + Return the base (first) address of a network as an (long) integer. + """ + return self.int() + + def broadcast(self): + """ + Return the broadcast (last) address of a network as an (long) integer. + + The same as IP[-1].""" + return self.int() + self.len() - 1 + + def _printPrefix(self, want): + """Prints Prefixlen/Netmask. + + Not really. In fact it is our universal Netmask/Prefixlen printer. + This is considered an internal function. + + want == 0 / None don't return anything 1.2.3.0 + want == 1 /prefix 1.2.3.0/24 + want == 2 /netmask 1.2.3.0/255.255.255.0 + want == 3 -lastip 1.2.3.0-1.2.3.255 + """ + + if (self._ipversion == 4 and self._prefixlen == 32) or \ + (self._ipversion == 6 and self._prefixlen == 128): + if self.NoPrefixForSingleIp: + want = 0 + if want == None: + want = self.WantPrefixLen + if want == None: + want = 1 + if want: + if want == 2: + # this should work with IP and IPint + netmask = self.netmask() + if not isinstance(netmask, INT_TYPES): + netmask = netmask.int() + return "/%s" % (intToIp(netmask, self._ipversion)) + elif want == 3: + return "-%s" % (intToIp(self.ip + self.len() - 1, self._ipversion)) + else: + # default + return "/%d" % (self._prefixlen) + else: + return '' + + # We have different flavours to convert to: + # strFullsize 127.0.0.1 2001:0658:022a:cafe:0200:c0ff:fe8d:08fa + # strNormal 127.0.0.1 2001:658:22a:cafe:200:c0ff:fe8d:08fa + # strCompressed 127.0.0.1 2001:658:22a:cafe::1 + # strHex 0x7F000001 0x20010658022ACAFE0200C0FFFE8D08FA + # strDec 2130706433 42540616829182469433547974687817795834 + + def strBin(self, wantprefixlen = None): + """Return a string representation as a binary value. + + >>> print(IP('127.0.0.1').strBin()) + 01111111000000000000000000000001 + >>> print(IP('2001:0658:022a:cafe:0200::1').strBin()) + 00100000000000010000011001011000000000100010101011001010111111100000001000000000000000000000000000000000000000000000000000000001 + """ + + bits = _ipVersionToLen(self._ipversion) + if self.WantPrefixLen == None and wantprefixlen == None: + wantprefixlen = 0 + ret = _intToBin(self.ip) + return '0' * (bits - len(ret)) + ret + self._printPrefix(wantprefixlen) + + def strCompressed(self, wantprefixlen = None): + """Return a string representation in compressed format using '::' Notation. + + >>> IP('127.0.0.1').strCompressed() + '127.0.0.1' + >>> IP('2001:0658:022a:cafe:0200::1').strCompressed() + '2001:658:22a:cafe:200::1' + >>> IP('ffff:ffff:ffff:ffff:ffff:f:f:fffc/127').strCompressed() + 'ffff:ffff:ffff:ffff:ffff:f:f:fffc/127' + """ + + if self.WantPrefixLen == None and wantprefixlen == None: + wantprefixlen = 1 + + if self._ipversion == 4: + return self.strFullsize(wantprefixlen) + else: + if self.ip >> 32 == 0xffff: + ipv4 = intToIp(self.ip & MAX_IPV4_ADDRESS, 4) + text = "::ffff:" + ipv4 + self._printPrefix(wantprefixlen) + return text + # find the longest sequence of '0' + hextets = [int(x, 16) for x in self.strFullsize(0).split(':')] + # every element of followingzeros will contain the number of zeros + # following the corresponding element of hextets + followingzeros = [0] * 8 + for i in xrange(len(hextets)): + followingzeros[i] = _countFollowingZeros(hextets[i:]) + # compressionpos is the position where we can start removing zeros + compressionpos = followingzeros.index(max(followingzeros)) + if max(followingzeros) > 1: + # genererate string with the longest number of zeros cut out + # now we need hextets as strings + hextets = [x for x in self.strNormal(0).split(':')] + while compressionpos < len(hextets) and hextets[compressionpos] == '0': + del(hextets[compressionpos]) + hextets.insert(compressionpos, '') + if compressionpos + 1 >= len(hextets): + hextets.append('') + if compressionpos == 0: + hextets = [''] + hextets + return ':'.join(hextets) + self._printPrefix(wantprefixlen) + else: + return self.strNormal(0) + self._printPrefix(wantprefixlen) + + def strNormal(self, wantprefixlen = None): + """Return a string representation in the usual format. + + >>> print(IP('127.0.0.1').strNormal()) + 127.0.0.1 + >>> print(IP('2001:0658:022a:cafe:0200::1').strNormal()) + 2001:658:22a:cafe:200:0:0:1 + """ + + if self.WantPrefixLen == None and wantprefixlen == None: + wantprefixlen = 1 + + if self._ipversion == 4: + ret = self.strFullsize(0) + elif self._ipversion == 6: + ret = ':'.join(["%x" % x for x in [int(x, 16) for x in self.strFullsize(0).split(':')]]) + else: + raise ValueError("only IPv4 and IPv6 supported") + + + + return ret + self._printPrefix(wantprefixlen) + + def strFullsize(self, wantprefixlen = None): + """Return a string representation in the non-mangled format. + + >>> print(IP('127.0.0.1').strFullsize()) + 127.0.0.1 + >>> print(IP('2001:0658:022a:cafe:0200::1').strFullsize()) + 2001:0658:022a:cafe:0200:0000:0000:0001 + """ + + if self.WantPrefixLen == None and wantprefixlen == None: + wantprefixlen = 1 + + return intToIp(self.ip, self._ipversion) + self._printPrefix(wantprefixlen) + + def strHex(self, wantprefixlen = None): + """Return a string representation in hex format in lower case. + + >>> print(IP('127.0.0.1').strHex()) + 0x7f000001 + >>> print(IP('2001:0658:022a:cafe:0200::1').strHex()) + 0x20010658022acafe0200000000000001 + """ + + if self.WantPrefixLen == None and wantprefixlen == None: + wantprefixlen = 0 + + x = '0x%x' % self.ip + return x + self._printPrefix(wantprefixlen) + + def strDec(self, wantprefixlen = None): + """Return a string representation in decimal format. + + >>> print(IP('127.0.0.1').strDec()) + 2130706433 + >>> print(IP('2001:0658:022a:cafe:0200::1').strDec()) + 42540616829182469433547762482097946625 + """ + + if self.WantPrefixLen == None and wantprefixlen == None: + wantprefixlen = 0 + + x = '%d' % self.ip + return x + self._printPrefix(wantprefixlen) + + def iptype(self): + """Return a description of the IP type ('PRIVATE', 'RESERVED', etc). + + >>> print(IP('127.0.0.1').iptype()) + PRIVATE + >>> print(IP('192.168.1.1').iptype()) + PRIVATE + >>> print(IP('195.185.1.2').iptype()) + PUBLIC + >>> print(IP('::1').iptype()) + LOOPBACK + >>> print(IP('2001:0658:022a:cafe:0200::1').iptype()) + ALLOCATED RIPE NCC + + The type information for IPv6 is out of sync with reality. + """ + + # this could be greatly improved + + if self._ipversion == 4: + iprange = IPv4ranges + elif self._ipversion == 6: + iprange = IPv6ranges + else: + raise ValueError("only IPv4 and IPv6 supported") + + bits = self.strBin() + for i in xrange(len(bits), 0, -1): + if bits[:i] in iprange: + return iprange[bits[:i]] + return "unknown" + + + def netmask(self): + """Return netmask as an integer. + + >>> "%X" % IP('195.185.0.0/16').netmask().int() + 'FFFF0000' + """ + + # TODO: unify with prefixlenToNetmask? + bits = _ipVersionToLen(self._ipversion) + locallen = bits - self._prefixlen + + return ((2 ** self._prefixlen) - 1) << locallen + + + def strNetmask(self): + """Return netmask as an string. Mostly useful for IPv6. + + >>> print(IP('195.185.0.0/16').strNetmask()) + 255.255.0.0 + >>> print(IP('2001:0658:022a:cafe::0/64').strNetmask()) + /64 + """ + + # TODO: unify with prefixlenToNetmask? + # Note: call to _ipVersionToLen() also validates version is 4 or 6 + bits = _ipVersionToLen(self._ipversion) + if self._ipversion == 4: + locallen = bits - self._prefixlen + return intToIp(((2 ** self._prefixlen) - 1) << locallen, 4) + elif self._ipversion == 6: + return "/%d" % self._prefixlen + + def len(self): + """Return the length of a subnet. + + >>> print(IP('195.185.1.0/28').len()) + 16 + >>> print(IP('195.185.1.0/24').len()) + 256 + """ + + bits = _ipVersionToLen(self._ipversion) + locallen = bits - self._prefixlen + return 2 ** locallen + + + def __nonzero__(self): + """All IPy objects should evaluate to true in boolean context. + Ordinarily they do, but if handling a default route expressed as + 0.0.0.0/0, the __len__() of the object becomes 0, which is used + as the boolean value of the object. + """ + return True + + + def __len__(self): + """ + Return the length of a subnet. + + Called to implement the built-in function len(). + It will break with large IPv6 Networks. + Use the object's len() instead. + """ + return self.len() + + def __add__(self, other): + """Emulate numeric objects through network aggregation""" + if self._ipversion != other._ipversion: + raise ValueError("Only networks with the same IP version can be added.") + if self._prefixlen != other._prefixlen: + raise ValueError("Only networks with the same prefixlen can be added.") + if self._prefixlen < 1: + raise ValueError("Networks with a prefixlen longer than /1 can't be added.") + if self > other: + # fixed by Skinny Puppy + return other.__add__(self) + if other.int() - self[-1].int() != 1: + raise ValueError("Only adjacent networks can be added together.") + ret = IP(self.int(), ipversion=self._ipversion) + ret._prefixlen = self.prefixlen() - 1 + if not _checkNetaddrWorksWithPrefixlen(ret.ip, ret._prefixlen, + ret._ipversion): + raise ValueError("The resulting %s has invalid prefix length (%s)" + % (repr(ret), ret._prefixlen)) + return ret + + def __sub__(self, other): + """Return the prefixes that are in this IP but not in the other""" + return _remove_subprefix(self, other) + + def __getitem__(self, key): + """Called to implement evaluation of self[key]. + + >>> ip=IP('127.0.0.0/30') + >>> for x in ip: + ... print(repr(x)) + ... + IP('127.0.0.0') + IP('127.0.0.1') + IP('127.0.0.2') + IP('127.0.0.3') + >>> ip[2] + IP('127.0.0.2') + >>> ip[-1] + IP('127.0.0.3') + """ + + if isinstance(key, slice): + return [self.ip + int(x) for x in xrange(*key.indices(len(self)))] + if not isinstance(key, INT_TYPES): + raise TypeError + if key < 0: + if abs(key) <= self.len(): + key = self.len() - abs(key) + else: + raise IndexError + else: + if key >= self.len(): + raise IndexError + + return self.ip + int(key) + + + + def __contains__(self, item): + """Called to implement membership test operators. + + Should return true if item is in self, false otherwise. Item + can be other IP-objects, strings or ints. + + >>> IP('195.185.1.1').strHex() + '0xc3b90101' + >>> 0xC3B90101 in IP('195.185.1.0/24') + True + >>> '127.0.0.1' in IP('127.0.0.0/24') + True + >>> IP('127.0.0.0/24') in IP('127.0.0.0/25') + False + """ + + if isinstance(item, IP): + if item._ipversion != self._ipversion: + return False + else: + item = IP(item) + if item.ip >= self.ip and item.ip < self.ip + self.len() - item.len() + 1: + return True + else: + return False + + + def overlaps(self, item): + """Check if two IP address ranges overlap. + + Returns 0 if the two ranges don't overlap, 1 if the given + range overlaps at the end and -1 if it does at the beginning. + + >>> IP('192.168.0.0/23').overlaps('192.168.1.0/24') + 1 + >>> IP('192.168.0.0/23').overlaps('192.168.1.255') + 1 + >>> IP('192.168.0.0/23').overlaps('192.168.2.0') + 0 + >>> IP('192.168.1.0/24').overlaps('192.168.0.0/23') + -1 + """ + + if not isinstance(item, IP): + item = IP(item) + if item.ip >= self.ip and item.ip < self.ip + self.len(): + return 1 + elif self.ip >= item.ip and self.ip < item.ip + item.len(): + return -1 + else: + return 0 + + + def __str__(self): + """Dispatch to the prefered String Representation. + + Used to implement str(IP).""" + + return self.strCompressed() + + + def __repr__(self): + """Print a representation of the Object. + + Used to implement repr(IP). Returns a string which evaluates + to an identical Object (without the wantprefixlen stuff - see + module docstring. + + >>> print(repr(IP('10.0.0.0/24'))) + IP('10.0.0.0/24') + """ + + return("IPint('%s')" % (self.strCompressed(1))) + + + def __cmp__(self, other): + """Called by comparison operations. + + Should return a negative integer if self < other, zero if self + == other, a positive integer if self > other. + + Order is first determined by the address family. IPv4 addresses + are always smaller than IPv6 addresses: + + >>> IP('10.0.0.0') < IP('2001:db8::') + 1 + + Then the first address is compared. Lower addresses are + always smaller: + + >>> IP('10.0.0.0') > IP('10.0.0.1') + 0 + >>> IP('10.0.0.0/24') > IP('10.0.0.1') + 0 + >>> IP('10.0.1.0') > IP('10.0.0.0/24') + 1 + >>> IP('10.0.1.0/24') > IP('10.0.0.0/24') + 1 + >>> IP('10.0.1.0/24') > IP('10.0.0.0') + 1 + + Then the prefix length is compared. Shorter prefixes are + considered smaller than longer prefixes: + + >>> IP('10.0.0.0/24') > IP('10.0.0.0') + 0 + >>> IP('10.0.0.0/24') > IP('10.0.0.0/25') + 0 + >>> IP('10.0.0.0/24') > IP('10.0.0.0/23') + 1 + + """ + if not isinstance(other, IPint): + raise TypeError + + # Lower version -> lower result + if self._ipversion != other._ipversion: + return self._ipversion < other._ipversion and -1 or 1 + + # Lower start address -> lower result + if self.ip != other.ip: + return self.ip < other.ip and -1 or 1 + + # Shorter prefix length -> lower result + if self._prefixlen != other._prefixlen: + return self._prefixlen < other._prefixlen and -1 or 1 + + # No differences found + return 0 + + def __eq__(self, other): + if not isinstance(other, IPint): + return False + return self.__cmp__(other) == 0 + + def __ne__(self, other): + return not self.__eq__(other) + + def __lt__(self, other): + return self.__cmp__(other) < 0 + + def __hash__(self): + """Called for the key object for dictionary operations, and by + the built-in function hash(). Should return a 32-bit integer + usable as a hash value for dictionary operations. The only + required property is that objects which compare equal have the + same hash value + + >>> IP('10.0.0.0/24').__hash__() + -167772185 + """ + + thehash = int(-1) + ip = self.ip + while ip > 0: + thehash = thehash ^ (ip & 0x7fffffff) + ip = ip >> 32 + thehash = thehash ^ self._prefixlen + return int(thehash) + + +class IP(IPint): + """Class for handling IP addresses and networks.""" + + def net(self): + """Return the base (first) address of a network as an IP object. + + The same as IP[0]. + + >>> IP('10.0.0.0/8').net() + IP('10.0.0.0') + """ + return IP(IPint.net(self), ipversion=self._ipversion) + + def broadcast(self): + """Return the broadcast (last) address of a network as an IP object. + + The same as IP[-1]. + + >>> IP('10.0.0.0/8').broadcast() + IP('10.255.255.255') + """ + return IP(IPint.broadcast(self)) + + def netmask(self): + """Return netmask as an IP object. + + >>> IP('10.0.0.0/8').netmask() + IP('255.0.0.0') + """ + return IP(IPint.netmask(self), ipversion=self._ipversion) + + def _getIPv4Map(self): + if self._ipversion != 6: + return None + if (self.ip >> 32) != 0xffff: + return None + ipv4 = self.ip & MAX_IPV4_ADDRESS + if self._prefixlen != 128: + ipv4 = '%s/%s' % (ipv4, 32-(128-self._prefixlen)) + return IP(ipv4, ipversion=4) + + def reverseNames(self): + """Return a list with values forming the reverse lookup. + + >>> IP('213.221.113.87/32').reverseNames() + ['87.113.221.213.in-addr.arpa.'] + >>> IP('213.221.112.224/30').reverseNames() + ['224.112.221.213.in-addr.arpa.', '225.112.221.213.in-addr.arpa.', '226.112.221.213.in-addr.arpa.', '227.112.221.213.in-addr.arpa.'] + >>> IP('127.0.0.0/24').reverseNames() + ['0.0.127.in-addr.arpa.'] + >>> IP('127.0.0.0/23').reverseNames() + ['0.0.127.in-addr.arpa.', '1.0.127.in-addr.arpa.'] + >>> IP('127.0.0.0/16').reverseNames() + ['0.127.in-addr.arpa.'] + >>> IP('127.0.0.0/15').reverseNames() + ['0.127.in-addr.arpa.', '1.127.in-addr.arpa.'] + >>> IP('128.0.0.0/8').reverseNames() + ['128.in-addr.arpa.'] + >>> IP('128.0.0.0/7').reverseNames() + ['128.in-addr.arpa.', '129.in-addr.arpa.'] + >>> IP('::1:2').reverseNames() + ['2.0.0.0.1.ip6.arpa.'] + """ + + if self._ipversion == 4: + ret = [] + # TODO: Refactor. Add support for IPint objects + if self.len() < 2**8: + for x in self: + ret.append(x.reverseName()) + elif self.len() < 2**16: + for i in xrange(0, self.len(), 2**8): + ret.append(self[i].reverseName()[2:]) + elif self.len() < 2**24: + for i in xrange(0, self.len(), 2**16): + ret.append(self[i].reverseName()[4:]) + else: + for i in xrange(0, self.len(), 2**24): + ret.append(self[i].reverseName()[6:]) + return ret + elif self._ipversion == 6: + ipv4 = self._getIPv4Map() + if ipv4 is not None: + return ipv4.reverseNames() + s = "%x" % self.ip + if self._prefixlen % 4 != 0: + raise NotImplementedError("can't create IPv6 reverse names at sub nibble level") + s = list(s) + s.reverse() + s = '.'.join(s) + first_nibble_index = int(32 - (self._prefixlen // 4)) * 2 + return ["%s.ip6.arpa." % s[first_nibble_index:]] + else: + raise ValueError("only IPv4 and IPv6 supported") + + def reverseName(self): + """Return the value for reverse lookup/PTR records as RFC 2317 look alike. + + RFC 2317 is an ugly hack which only works for sub-/24 e.g. not + for /23. Do not use it. Better set up a zone for every + address. See reverseName for a way to achieve that. + + >>> print(IP('195.185.1.1').reverseName()) + 1.1.185.195.in-addr.arpa. + >>> print(IP('195.185.1.0/28').reverseName()) + 0-15.1.185.195.in-addr.arpa. + >>> IP('::1:2').reverseName() + '2.0.0.0.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.' + >>> IP('ff02::/64').reverseName() + '0.0.0.0.0.0.0.0.0.0.0.0.2.0.f.f.ip6.arpa.' + """ + + if self._ipversion == 4: + s = self.strFullsize(0) + s = s.split('.') + s.reverse() + first_byte_index = int(4 - (self._prefixlen // 8)) + if self._prefixlen % 8 != 0: + nibblepart = "%s-%s" % (s[3-(self._prefixlen // 8)], intToIp(self.ip + self.len() - 1, 4).split('.')[-1]) + nibblepart += '.' + else: + nibblepart = "" + + s = '.'.join(s[first_byte_index:]) + return "%s%s.in-addr.arpa." % (nibblepart, s) + + elif self._ipversion == 6: + ipv4 = self._getIPv4Map() + if ipv4 is not None: + return ipv4.reverseName() + s = '%032x' % self.ip + if self._prefixlen % 4 != 0: + nibblepart = "%s-%x" % (s[self._prefixlen:], self.ip + self.len() - 1) + nibblepart += '.' + else: + nibblepart = "" + s = list(s) + s.reverse() + s = '.'.join(s) + first_nibble_index = int(32 - (self._prefixlen // 4)) * 2 + return "%s%s.ip6.arpa." % (nibblepart, s[first_nibble_index:]) + else: + raise ValueError("only IPv4 and IPv6 supported") + + def make_net(self, netmask): + """Transform a single IP address into a network specification by + applying the given netmask. + + Returns a new IP instance. + + >>> print(IP('127.0.0.1').make_net('255.0.0.0')) + 127.0.0.0/8 + """ + if '/' in str(netmask): + raise ValueError("invalid netmask (%s)" % netmask) + return IP('%s/%s' % (self, netmask), make_net=True) + + def __getitem__(self, key): + """Called to implement evaluation of self[key]. + + >>> ip=IP('127.0.0.0/30') + >>> for x in ip: + ... print(str(x)) + ... + 127.0.0.0 + 127.0.0.1 + 127.0.0.2 + 127.0.0.3 + >>> print(str(ip[2])) + 127.0.0.2 + >>> print(str(ip[-1])) + 127.0.0.3 + """ + if isinstance(key, slice): + return [IP(IPint.__getitem__(self, x), ipversion=self._ipversion) for x in xrange(*key.indices(len(self)))] + return IP(IPint.__getitem__(self, key), ipversion=self._ipversion) + + def __repr__(self): + """Print a representation of the Object. + + >>> IP('10.0.0.0/8') + IP('10.0.0.0/8') + """ + + return("IP('%s')" % (self.strCompressed(1))) + + def get_mac(self): + """ + Get the 802.3 MAC address from IPv6 RFC 2464 address, in lower case. + Return None if the address is an IPv4 or not a IPv6 RFC 2464 address. + + >>> IP('fe80::f66d:04ff:fe47:2fae').get_mac() + 'f4:6d:04:47:2f:ae' + """ + if self._ipversion != 6: + return None + if (self.ip & 0x20000ffff000000) != 0x20000fffe000000: + return None + return '%02x:%02x:%02x:%02x:%02x:%02x' % ( + (((self.ip >> 56) & 0xff) & 0xfd), + (self.ip >> 48) & 0xff, + (self.ip >> 40) & 0xff, + (self.ip >> 16) & 0xff, + (self.ip >> 8) & 0xff, + self.ip & 0xff, + ) + + def v46map(self): + """ + Returns the IPv6 mapped address of an IPv4 address, or the corresponding + IPv4 address if the IPv6 address is in the appropriate range. + Raises a ValueError if the IPv6 address is not translatable. See RFC 4291. + + >>> IP('192.168.1.1').v46map() + IP('::ffff:192.168.1.1') + >>> IP('::ffff:192.168.1.1').v46map() + IP('192.168.1.1') + """ + if self._ipversion == 4: + return IP(str(IPV6_MAP_MASK + self.ip) + + "/%s" % (self._prefixlen + 96)) + else: + if self.ip & IPV6_TEST_MAP == IPV6_MAP_MASK: + return IP(str(self.ip - IPV6_MAP_MASK) + + "/%s" % (self._prefixlen - 96)) + raise ValueError("%s cannot be converted to an IPv4 address." + % repr(self)) + +class IPSet(collections.MutableSet): + def __init__(self, iterable=[]): + # Make sure it's iterable, otherwise wrap + if not isinstance(iterable, collections.Iterable): + raise TypeError("'%s' object is not iterable" % type(iterable).__name__) + + # Make sure we only accept IP objects + for prefix in iterable: + if not isinstance(prefix, IP): + raise ValueError('Only IP objects can be added to an IPSet') + + # Store and optimize + self.prefixes = iterable[:] + self.optimize() + + def __contains__(self, ip): + valid_masks = self.prefixtable.keys() + if isinstance(ip, IP): + #Don't dig through more-specific ranges + ip_mask = ip._prefixlen + valid_masks = [x for x in valid_masks if x <= ip_mask] + for mask in sorted(valid_masks): + i = bisect.bisect(self.prefixtable[mask], ip) + # Because of sorting order, a match can only occur in the prefix + # that comes before the result of the search. + if i and ip in self.prefixtable[mask][i - 1]: + return True + + def __iter__(self): + for prefix in self.prefixes: + yield prefix + + def __len__(self): + return self.len() + + def __add__(self, other): + return IPSet(self.prefixes + other.prefixes) + + def __sub__(self, other): + new = IPSet(self.prefixes) + for prefix in other: + new.discard(prefix) + return new + + def __and__(self, other): + left = iter(self.prefixes) + right = iter(other.prefixes) + result = [] + try: + l = next(left) + r = next(right) + while True: + # iterate over prefixes in order, keeping the smaller of the + # two if they overlap + if l in r: + result.append(l) + l = next(left) + continue + elif r in l: + result.append(r) + r = next(right) + continue + if l < r: + l = next(left) + else: + r = next(right) + except StopIteration: + return IPSet(result) + + def __repr__(self): + return '%s([' % self.__class__.__name__ + ', '.join(map(repr, self.prefixes)) + '])' + + def len(self): + return sum(prefix.len() for prefix in self.prefixes) + + def add(self, value): + # Make sure it's iterable, otherwise wrap + if not isinstance(value, collections.Iterable): + value = [value] + + # Check type + for prefix in value: + if not isinstance(prefix, IP): + raise ValueError('Only IP objects can be added to an IPSet') + + # Append and optimize + self.prefixes.extend(value) + self.optimize() + + def discard(self, value): + # Make sure it's iterable, otherwise wrap + if not isinstance(value, collections.Iterable): + value = [value] + + # This is much faster than iterating over the addresses + if isinstance(value, IPSet): + value = value.prefixes + + # Remove + for del_prefix in value: + if not isinstance(del_prefix, IP): + raise ValueError('Only IP objects can be removed from an IPSet') + + # First check if this prefix contains anything in our list + found = False + d = 0 + for i in range(len(self.prefixes)): + if self.prefixes[i - d] in del_prefix: + self.prefixes.pop(i - d) + d = d + 1 + found = True + + if found: + # If the prefix was bigger than an existing prefix, then it's + # certainly not a subset of one, so skip the rest + continue + + # Maybe one of our prefixes contains this prefix + found = False + for i in range(len(self.prefixes)): + if del_prefix in self.prefixes[i]: + self.prefixes[i:i+1] = self.prefixes[i] - del_prefix + break + + self.optimize() + + def isdisjoint(self, other): + left = iter(self.prefixes) + right = iter(other.prefixes) + try: + l = next(left) + r = next(right) + while True: + if l in r or r in l: + return False + if l < r: + l = next(left) + else: + r = next(right) + except StopIteration: + return True + + def optimize(self): + # The algorithm below *depends* on the sort order + self.prefixes.sort() + + # First eliminate all values that are a subset of other values + addrlen = len(self.prefixes) + i = 0 + while i < addrlen: + # Everything that might be inside this prefix follows + # directly behind it + j = i+1 + while j < addrlen and self.prefixes[j] in self.prefixes[i]: + # Mark for deletion by overwriting with None + self.prefixes[j] = None + j += 1 + + # Continue where we left off + i = j + + # Try to merge as many prefixes as possible + run_again = True + while run_again: + # Filter None values. This happens when a subset is eliminated + # above, or when two prefixes are merged below + self.prefixes = [a for a in self.prefixes if a is not None] + + # We'll set run_again to True when we make changes that require + # re-evaluation of the whole list + run_again = False + + # We can merge two prefixes that have the same version, same + # prefix length and differ only on the last bit of the prefix + addrlen = len(self.prefixes) + i = 0 + while i < addrlen-1: + j = i + 1 + + try: + # The next line will throw an exception when merging + # is not possible + self.prefixes[i] += self.prefixes[j] + self.prefixes[j] = None + i = j + 1 + run_again = True + except ValueError: + # Can't be merged, see if position j can be merged + i = j + + # O(n) insertion now by prefix means faster searching on __contains__ + # when lots of ranges with the same length exist + self.prefixtable = {} + for address in self.prefixes: + try: + self.prefixtable[address._prefixlen].append(address) + except KeyError: + self.prefixtable[address._prefixlen] = [address] + +def _parseAddressIPv6(ipstr): + """ + Internal function used by parseAddress() to parse IPv6 address with ':'. + + >>> print(_parseAddressIPv6('::')) + 0 + >>> print(_parseAddressIPv6('::1')) + 1 + >>> print(_parseAddressIPv6('0:0:0:0:0:0:0:1')) + 1 + >>> print(_parseAddressIPv6('0:0:0::0:0:1')) + 1 + >>> print(_parseAddressIPv6('0:0:0:0:0:0:0:0')) + 0 + >>> print(_parseAddressIPv6('0:0:0::0:0:0')) + 0 + + >>> print(_parseAddressIPv6('FEDC:BA98:7654:3210:FEDC:BA98:7654:3210')) + 338770000845734292534325025077361652240 + >>> print(_parseAddressIPv6('1080:0000:0000:0000:0008:0800:200C:417A')) + 21932261930451111902915077091070067066 + >>> print(_parseAddressIPv6('1080:0:0:0:8:800:200C:417A')) + 21932261930451111902915077091070067066 + >>> print(_parseAddressIPv6('1080:0::8:800:200C:417A')) + 21932261930451111902915077091070067066 + >>> print(_parseAddressIPv6('1080::8:800:200C:417A')) + 21932261930451111902915077091070067066 + >>> print(_parseAddressIPv6('FF01:0:0:0:0:0:0:43')) + 338958331222012082418099330867817087043 + >>> print(_parseAddressIPv6('FF01:0:0::0:0:43')) + 338958331222012082418099330867817087043 + >>> print(_parseAddressIPv6('FF01::43')) + 338958331222012082418099330867817087043 + >>> print(_parseAddressIPv6('0:0:0:0:0:0:13.1.68.3')) + 218186755 + >>> print(_parseAddressIPv6('::13.1.68.3')) + 218186755 + >>> print(_parseAddressIPv6('0:0:0:0:0:FFFF:129.144.52.38')) + 281472855454758 + >>> print(_parseAddressIPv6('::FFFF:129.144.52.38')) + 281472855454758 + >>> print(_parseAddressIPv6('1080:0:0:0:8:800:200C:417A')) + 21932261930451111902915077091070067066 + >>> print(_parseAddressIPv6('1080::8:800:200C:417A')) + 21932261930451111902915077091070067066 + >>> print(_parseAddressIPv6('::1:2:3:4:5:6')) + 1208962713947218704138246 + >>> print(_parseAddressIPv6('1:2:3:4:5:6::')) + 5192455318486707404433266432802816 + """ + + # Split string into a list, example: + # '1080:200C::417A' => ['1080', '200C', '417A'] and fill_pos=2 + # and fill_pos is the position of '::' in the list + items = [] + index = 0 + fill_pos = None + while index < len(ipstr): + text = ipstr[index:] + if text.startswith("::"): + if fill_pos is not None: + # Invalid IPv6, eg. '1::2::' + raise ValueError("%r: Invalid IPv6 address: more than one '::'" % ipstr) + fill_pos = len(items) + index += 2 + continue + pos = text.find(':') + if pos == 0: + # Invalid IPv6, eg. '1::2:' + raise ValueError("%r: Invalid IPv6 address" % ipstr) + if pos != -1: + items.append(text[:pos]) + if text[pos:pos+2] == "::": + index += pos + else: + index += pos+1 + + if index == len(ipstr): + # Invalid IPv6, eg. '1::2:' + raise ValueError("%r: Invalid IPv6 address" % ipstr) + else: + items.append(text) + break + + if items and '.' in items[-1]: + # IPv6 ending with IPv4 like '::ffff:192.168.0.1' + if (fill_pos is not None) and not (fill_pos <= len(items)-1): + # Invalid IPv6: 'ffff:192.168.0.1::' + raise ValueError("%r: Invalid IPv6 address: '::' after IPv4" % ipstr) + value = parseAddress(items[-1])[0] + items = items[:-1] + ["%04x" % (value >> 16), "%04x" % (value & 0xffff)] + + # Expand fill_pos to fill with '0' + # ['1','2'] with fill_pos=1 => ['1', '0', '0', '0', '0', '0', '0', '2'] + if fill_pos is not None: + diff = 8 - len(items) + if diff <= 0: + raise ValueError("%r: Invalid IPv6 address: '::' is not needed" % ipstr) + items = items[:fill_pos] + ['0']*diff + items[fill_pos:] + + # Here we have a list of 8 strings + if len(items) != 8: + # Invalid IPv6, eg. '1:2:3' + raise ValueError("%r: Invalid IPv6 address: should have 8 hextets" % ipstr) + + # Convert strings to long integer + value = 0 + index = 0 + for item in items: + try: + item = int(item, 16) + error = not(0 <= item <= 0xffff) + except ValueError: + error = True + if error: + raise ValueError("%r: Invalid IPv6 address: invalid hexlet %r" % (ipstr, item)) + value = (value << 16) + item + index += 1 + return value + +def parseAddress(ipstr): + """ + Parse a string and return the corresponding IP address (as integer) + and a guess of the IP version. + + Following address formats are recognized: + + >>> def testParseAddress(address): + ... ip, version = parseAddress(address) + ... print(("%s (IPv%s)" % (ip, version))) + ... + >>> testParseAddress('0x0123456789abcdef') # IPv4 if <= 0xffffffff else IPv6 + 81985529216486895 (IPv6) + >>> testParseAddress('123.123.123.123') # IPv4 + 2071690107 (IPv4) + >>> testParseAddress('123.123') # 0-padded IPv4 + 2071658496 (IPv4) + >>> testParseAddress('127') + 2130706432 (IPv4) + >>> testParseAddress('255') + 4278190080 (IPv4) + >>> testParseAddress('256') + 256 (IPv4) + >>> testParseAddress('108000000000000000080800200C417A') + 21932261930451111902915077091070067066 (IPv6) + >>> testParseAddress('0x108000000000000000080800200C417A') + 21932261930451111902915077091070067066 (IPv6) + >>> testParseAddress('1080:0000:0000:0000:0008:0800:200C:417A') + 21932261930451111902915077091070067066 (IPv6) + >>> testParseAddress('1080:0:0:0:8:800:200C:417A') + 21932261930451111902915077091070067066 (IPv6) + >>> testParseAddress('1080:0::8:800:200C:417A') + 21932261930451111902915077091070067066 (IPv6) + >>> testParseAddress('::1') + 1 (IPv6) + >>> testParseAddress('::') + 0 (IPv6) + >>> testParseAddress('0:0:0:0:0:FFFF:129.144.52.38') + 281472855454758 (IPv6) + >>> testParseAddress('::13.1.68.3') + 218186755 (IPv6) + >>> testParseAddress('::FFFF:129.144.52.38') + 281472855454758 (IPv6) + """ + + try: + hexval = int(ipstr, 16) + except ValueError: + hexval = None + try: + intval = int(ipstr, 10) + except ValueError: + intval = None + + if ipstr.startswith('0x') and hexval is not None: + if hexval > MAX_IPV6_ADDRESS: + raise ValueError("IP Address can't be larger than %x: %x" % (MAX_IPV6_ADDRESS, hexval)) + if hexval <= MAX_IPV4_ADDRESS: + return (hexval, 4) + else: + return (hexval, 6) + + if ipstr.find(':') != -1: + return (_parseAddressIPv6(ipstr), 6) + + elif len(ipstr) == 32 and hexval is not None: + # assume IPv6 in pure hexadecimal notation + return (hexval, 6) + + elif ipstr.find('.') != -1 or (intval is not None and intval < 256): + # assume IPv4 ('127' gets interpreted as '127.0.0.0') + bytes = ipstr.split('.') + if len(bytes) > 4: + raise ValueError("IPv4 Address with more than 4 bytes") + bytes += ['0'] * (4 - len(bytes)) + bytes = [int(x) for x in bytes] + for x in bytes: + if x > 255 or x < 0: + raise ValueError("%r: single byte must be 0 <= byte < 256" % (ipstr)) + return ((bytes[0] << 24) + (bytes[1] << 16) + (bytes[2] << 8) + bytes[3], 4) + + elif intval is not None: + # we try to interprete it as a decimal digit - + # this ony works for numbers > 255 ... others + # will be interpreted as IPv4 first byte + if intval > MAX_IPV6_ADDRESS: + raise ValueError("IP Address can't be larger than %x: %x" % (MAX_IPV6_ADDRESS, intval)) + if intval <= MAX_IPV4_ADDRESS: + return (intval, 4) + else: + return (intval, 6) + + raise ValueError("IP Address format was invalid: %s" % ipstr) + + +def intToIp(ip, version): + """Transform an integer string into an IP address.""" + + # just to be sure and hoping for Python 2.2 + ip = int(ip) + + if ip < 0: + raise ValueError("IPs can't be negative: %d" % (ip)) + + ret = '' + if version == 4: + if ip > MAX_IPV4_ADDRESS: + raise ValueError("IPv4 Address can't be larger than %x: %x" % (MAX_IPV4_ADDRESS, ip)) + for l in xrange(4): + ret = str(ip & 0xff) + '.' + ret + ip = ip >> 8 + ret = ret[:-1] + elif version == 6: + if ip > MAX_IPV6_ADDRESS: + raise ValueError("IPv6 Address can't be larger than %x: %x" % (MAX_IPV6_ADDRESS, ip)) + l = "%032x" % ip + for x in xrange(1, 33): + ret = l[-x] + ret + if x % 4 == 0: + ret = ':' + ret + ret = ret[1:] + else: + raise ValueError("only IPv4 and IPv6 supported") + + return ret + +def _ipVersionToLen(version): + """Return number of bits in address for a certain IP version. + + >>> _ipVersionToLen(4) + 32 + >>> _ipVersionToLen(6) + 128 + >>> _ipVersionToLen(5) + Traceback (most recent call last): + File "", line 1, in ? + File "IPy.py", line 1076, in _ipVersionToLen + raise ValueError("only IPv4 and IPv6 supported") + ValueError: only IPv4 and IPv6 supported + """ + + if version == 4: + return 32 + elif version == 6: + return 128 + else: + raise ValueError("only IPv4 and IPv6 supported") + + +def _countFollowingZeros(l): + """Return number of elements containing 0 at the beginning of the list.""" + if len(l) == 0: + return 0 + elif l[0] != 0: + return 0 + else: + return 1 + _countFollowingZeros(l[1:]) + + +_BitTable = {'0': '0000', '1': '0001', '2': '0010', '3': '0011', + '4': '0100', '5': '0101', '6': '0110', '7': '0111', + '8': '1000', '9': '1001', 'a': '1010', 'b': '1011', + 'c': '1100', 'd': '1101', 'e': '1110', 'f': '1111'} + +def _intToBin(val): + """Return the binary representation of an integer as string.""" + + if val < 0: + raise ValueError("Only positive values allowed") + s = "%x" % val + ret = '' + for x in s: + ret += _BitTable[x] + # remove leading zeros + while ret[0] == '0' and len(ret) > 1: + ret = ret[1:] + return ret + +def _count1Bits(num): + """Find the highest bit set to 1 in an integer.""" + ret = 0 + while num > 0: + num = num >> 1 + ret += 1 + return ret + +def _count0Bits(num): + """Find the highest bit set to 0 in an integer.""" + + # this could be so easy if _count1Bits(~int(num)) would work as excepted + num = int(num) + if num < 0: + raise ValueError("Only positive Numbers please: %s" % (num)) + ret = 0 + while num > 0: + if num & 1 == 1: + break + num = num >> 1 + ret += 1 + return ret + + +def _checkPrefix(ip, prefixlen, version): + """Check the validity of a prefix + + Checks if the variant part of a prefix only has 0s, and the length is + correct. + + >>> _checkPrefix(0x7f000000, 24, 4) + 1 + >>> _checkPrefix(0x7f000001, 24, 4) + 0 + >>> repr(_checkPrefix(0x7f000001, -1, 4)) + 'None' + >>> repr(_checkPrefix(0x7f000001, 33, 4)) + 'None' + """ + + # TODO: unify this v4/v6/invalid code in a function + bits = _ipVersionToLen(version) + + if prefixlen < 0 or prefixlen > bits: + return None + + if ip == 0: + zbits = bits + 1 + else: + zbits = _count0Bits(ip) + if zbits < bits - prefixlen: + return 0 + else: + return 1 + + +def _checkNetmask(netmask, masklen): + """Checks if a netmask is expressable as a prefixlen.""" + + num = int(netmask) + bits = masklen + + # remove zero bits at the end + while (num & 1) == 0 and bits != 0: + num = num >> 1 + bits -= 1 + if bits == 0: + break + # now check if the rest consists only of ones + while bits > 0: + if (num & 1) == 0: + raise ValueError("Netmask 0x%x can't be expressed as an prefix." % netmask) + num = num >> 1 + bits -= 1 + + +def _checkNetaddrWorksWithPrefixlen(net, prefixlen, version): + """Check if a base addess of a network is compatible with a prefixlen""" + try: + return (net & _prefixlenToNetmask(prefixlen, version) == net) + except ValueError: + return False + + +def _netmaskToPrefixlen(netmask): + """Convert an Integer representing a netmask to a prefixlen. + + E.g. 0xffffff00 (255.255.255.0) returns 24 + """ + + netlen = _count0Bits(netmask) + masklen = _count1Bits(netmask) + _checkNetmask(netmask, masklen) + return masklen - netlen + + +def _prefixlenToNetmask(prefixlen, version): + """Return a mask of n bits as a long integer. + + From 'IP address conversion functions with the builtin socket module' + by Alex Martelli + http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66517 + """ + if prefixlen == 0: + return 0 + elif prefixlen < 0: + raise ValueError("Prefixlen must be > 0") + return ((2<','>') else: - return '' \ No newline at end of file + return '' + +def is_ip_public(host): + ip_address = get_ip(host) + ip = IP(ip_address) + if ip.iptype() == 'PUBLIC': + return True + + return False + +def get_ip(host): + from plexpy import logger + ip_address = '' + try: + socket.inet_aton(host) + ip_address = host + except socket.error: + try: + ip_address = socket.gethostbyname(host) + logger.debug(u"IP Checker :: Resolved %s to %s." % (host, ip_address)) + except: + logger.error(u"IP Checker :: Bad IP or hostname provided.") + + return ip_address diff --git a/plexpy/webserve.py b/plexpy/webserve.py index a81bf203..bd1485d4 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -14,7 +14,7 @@ # along with PlexPy. If not, see . from plexpy import logger, notifiers, plextv, pmsconnect, common, log_reader, datafactory, graphs, users, libraries -from plexpy.helpers import checked, radio +from plexpy.helpers import checked, radio, get_ip from mako.lookup import TemplateLookup from mako import exceptions @@ -1402,22 +1402,34 @@ class WebInterface(object): def get_server_id(self, hostname=None, port=None, identifier=None, ssl=0, remote=0, **kwargs): from plexpy import http_handler - # Grab our serverId. - # This endpoint always works over plain HTTP even when secure connections on PMS is set to required. + # Attempt to get the pms_identifier from plex.tv if the server is published + # Works for all PMS SSL settings if not identifier and hostname and port: - request_handler = http_handler.HTTPHandler(host=hostname, - port=port, - token=None) - uri = '/identity' - request = request_handler.make_request(uri=uri, - proto='http', - request_type='GET', - output_format='xml', - no_token=True, - timeout=10) - if request: - xml_head = request.getElementsByTagName('MediaContainer')[0] - identifier = xml_head.getAttribute('machineIdentifier') + plex_tv = plextv.PlexTV() + servers = plex_tv.discover() + ip_address = get_ip(hostname) + + for server in servers: + if (server['ip'] == hostname or server['ip'] == ip_address) and server['port'] == port: + identifier = server['clientIdentifier'] + break + + # Fallback to checking /identity endpoint is server is unpublished + # Cannot set SSL settings on the PMS if unpublished so 'http' is okay + if not identifier: + request_handler = http_handler.HTTPHandler(host=hostname, + port=port, + token=None) + uri = '/identity' + request = request_handler.make_request(uri=uri, + proto='http', + request_type='GET', + output_format='xml', + no_token=True, + timeout=10) + if request: + xml_head = request.getElementsByTagName('MediaContainer')[0] + identifier = xml_head.getAttribute('machineIdentifier') if identifier: cherrypy.response.headers['Content-type'] = 'application/json' From ac42563c5ef2ed4bb8b0211c591d4b6f402224a3 Mon Sep 17 00:00:00 2001 From: Tim Van Date: Sun, 7 Feb 2016 22:28:04 +0200 Subject: [PATCH 3/6] Refresh PMS URL when changing is_remote option in settings. --- plexpy/webserve.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plexpy/webserve.py b/plexpy/webserve.py index bd1485d4..7cfbdd41 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -1192,6 +1192,11 @@ class WebInterface(object): (kwargs['pms_ssl'] != plexpy.CONFIG.PMS_SSL): server_changed = True + # If we change the PMS remote setting, make sure we grab the new url. + if 'pms_is_remote' in kwargs and \ + (kwargs['pms_is_remote'] != plexpy.CONFIG.PMS_IS_REMOTE): + server_changed = True + # Remove config with 'hscard-' prefix and change home_stats_cards to list if 'home_stats_cards' in kwargs: for k in kwargs.keys(): From 64191902723f2a7d7750776c132f61b1c0df29fa Mon Sep 17 00:00:00 2001 From: Tim Van Date: Sun, 7 Feb 2016 22:34:38 +0200 Subject: [PATCH 4/6] Revert silly naming bug. --- plexpy/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plexpy/config.py b/plexpy/config.py index 984d57bc..45eb1c79 100644 --- a/plexpy/config.py +++ b/plexpy/config.py @@ -25,7 +25,7 @@ _CONFIG_DEFINITIONS = { 'PMS_NAME': (unicode, 'PMS', ''), 'PMS_PORT': (int, 'PMS', 32400), 'PMS_TOKEN': (str, 'PMS', ''), - 'PMS_SSL': (int, 'PMS', 0), + 'PMS_SSL': (int, 'General', 0), 'PMS_URL': (str, 'PMS', ''), 'PMS_USE_BIF': (int, 'PMS', 0), 'PMS_UUID': (str, 'PMS', ''), From 16756ddb8c872b7c120dde15d4e7b452c5c4665a Mon Sep 17 00:00:00 2001 From: Tim Van Date: Mon, 8 Feb 2016 00:21:40 +0200 Subject: [PATCH 5/6] Don't chose a custom URL when picking a hostname for local SSL configs. --- plexpy/plextv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plexpy/plextv.py b/plexpy/plextv.py index df93548e..bb8de76b 100644 --- a/plexpy/plextv.py +++ b/plexpy/plextv.py @@ -86,7 +86,7 @@ def get_real_pms_url(): plexpy.CONFIG.__setattr__('PMS_URL', item['uri']) plexpy.CONFIG.write() logger.info(u"PlexPy PlexTV :: Server URL retrieved.") - if not plexpy.CONFIG.PMS_IS_REMOTE and item['local'] == '1': + if not plexpy.CONFIG.PMS_IS_REMOTE and item['local'] == '1' and 'plex.direct' in item['uri']: plexpy.CONFIG.__setattr__('PMS_URL', item['uri']) plexpy.CONFIG.write() logger.info(u"PlexPy PlexTV :: Server URL retrieved.") From 82b7128c04900b8ce11c581d148fb7996c1cf3f3 Mon Sep 17 00:00:00 2001 From: Tim Van Date: Mon, 8 Feb 2016 01:28:58 +0200 Subject: [PATCH 6/6] Allow secure websocket connections. --- data/interfaces/default/settings.html | 2 +- plexpy/web_socket.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html index 3e10c49c..84b97e32 100644 --- a/data/interfaces/default/settings.html +++ b/data/interfaces/default/settings.html @@ -490,7 +490,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents() Use Websocket (requires restart) [experimental]

Instead of polling the server at regular intervals let the server tell us when something happens.
- This is currently experimental. Encrypted websocket is not currently supported.

+ This is currently experimental.